Merge remote-tracking branch 'upstream/main' into invite-code-warning

This commit is contained in:
Samuel Newman 2023-11-30 17:11:51 +00:00
commit b164f151cc
76 changed files with 1718 additions and 1240 deletions

View file

@ -1,23 +1,30 @@
import React from 'react'
import {ScrollView, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {UserAvatar} from '../../util/UserAvatar'
import {s} from 'lib/styles'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {styles} from './styles'
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
@ -48,11 +55,19 @@ function AccountItem({
{account.handle}
</Text>
</Text>
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
{isCurrentAccount ? (
<FontAwesomeIcon
icon="check"
size={16}
style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]}
/>
) : (
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
)}
</View>
</TouchableOpacity>
)
@ -67,8 +82,9 @@ export const ChooseAccountForm = ({
const {track, screen} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const {accounts} = useSession()
const {accounts, currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
React.useEffect(() => {
screen('Choose Account')
@ -77,13 +93,21 @@ export const ChooseAccountForm = ({
const onSelect = React.useCallback(
async (account: SessionAccount) => {
if (account.accessJwt) {
await initSession(account)
track('Sign In', {resumedSession: true})
if (account.did === currentAccount?.did) {
setShowLoggedOut(false)
Toast.show(`Already signed in as @${account.handle}`)
} else {
await initSession(account)
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
}, 100)
}
} else {
onSelectAccount(account)
}
},
[track, initSession, onSelectAccount],
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut],
)
return (
@ -94,7 +118,12 @@ export const ChooseAccountForm = ({
<Trans>Sign in as...</Trans>
</Text>
{accounts.map(account => (
<AccountItem key={account.did} account={account} onSelect={onSelect} />
<AccountItem
key={account.did}
account={account}
onSelect={onSelect}
isCurrentAccount={account.did === currentAccount?.did}
/>
))}
<TouchableOpacity
testID="chooseNewAccountBtn"

View file

@ -62,7 +62,7 @@ export function FeedPage({
const onSoftReset = React.useCallback(() => {
if (isPageFocused) {
scrollToTop()
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}
}, [isPageFocused, scrollToTop, queryClient, feed, setHasNew])
@ -83,7 +83,7 @@ export function FeedPage({
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollToTop, feed, queryClient, setHasNew])

View file

@ -17,12 +17,14 @@ import {useModalControls} from '#/state/modals'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
usePinFeedMutation,
UsePreferencesQueryResponse,
usePreferencesQuery,
useSaveFeedMutation,
useRemoveFeedMutation,
} from '#/state/queries/preferences'
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
export function FeedSourceCard({
feedUri,
@ -30,17 +32,27 @@ export function FeedSourceCard({
showSaveBtn = false,
showDescription = false,
showLikes = false,
LoadingComponent,
pinOnSave = false,
}: {
feedUri: string
style?: StyleProp<ViewStyle>
showSaveBtn?: boolean
showDescription?: boolean
showLikes?: boolean
LoadingComponent?: JSX.Element
pinOnSave?: boolean
}) {
const {data: preferences} = usePreferencesQuery()
const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
if (!feed || !preferences) return null
if (!feed || !preferences) {
return LoadingComponent ? (
LoadingComponent
) : (
<FeedLoadingPlaceholder style={{flex: 1}} />
)
}
return (
<FeedSourceCardLoaded
@ -50,6 +62,7 @@ export function FeedSourceCard({
showSaveBtn={showSaveBtn}
showDescription={showDescription}
showLikes={showLikes}
pinOnSave={pinOnSave}
/>
)
}
@ -61,6 +74,7 @@ export function FeedSourceCardLoaded({
showSaveBtn = false,
showDescription = false,
showLikes = false,
pinOnSave = false,
}: {
feed: FeedSourceInfo
preferences: UsePreferencesQueryResponse
@ -68,6 +82,7 @@ export function FeedSourceCardLoaded({
showSaveBtn?: boolean
showDescription?: boolean
showLikes?: boolean
pinOnSave?: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
@ -78,6 +93,7 @@ export function FeedSourceCardLoaded({
useSaveFeedMutation()
const {isPending: isRemovePending, mutateAsync: removeFeed} =
useRemoveFeedMutation()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed.uri))
@ -103,14 +119,18 @@ export function FeedSourceCardLoaded({
})
} else {
try {
await saveFeed({uri: feed.uri})
if (pinOnSave) {
await pinFeed({uri: feed.uri})
} else {
await saveFeed({uri: feed.uri})
}
Toast.show('Added to my feeds')
} catch (e) {
Toast.show('There was an issue contacting your server')
logger.error('Failed to save feed', {error: e})
}
}
}, [isSaved, openModal, feed, removeFeed, saveFeed, _])
}, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed])
if (!feed || !preferences) return null
@ -150,7 +170,7 @@ export function FeedSourceCardLoaded({
{showSaveBtn && feed.type === 'feed' && (
<View>
<Pressable
disabled={isSavePending || isRemovePending}
disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button"
accessibilityLabel={
isSaved ? 'Remove from my feeds' : 'Add to my feeds'

View file

@ -35,15 +35,13 @@ export function Feed({
const [isPTRing, setIsPTRing] = React.useState(false)
const moderationOpts = useModerationOpts()
const {markAllRead} = useUnreadNotificationsApi()
const {markAllRead, checkUnread} = useUnreadNotificationsApi()
const {
data,
isLoading,
isFetching,
isFetched,
isError,
error,
refetch,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
@ -52,13 +50,11 @@ export function Feed({
const firstItem = data?.pages[0]?.items[0]
// mark all read on fresh data
// (this will fire each time firstItem changes)
React.useEffect(() => {
let cleanup
if (firstItem) {
const to = setTimeout(() => markAllRead(), 250)
cleanup = () => clearTimeout(to)
markAllRead()
}
return cleanup
}, [firstItem, markAllRead])
const items = React.useMemo(() => {
@ -83,7 +79,7 @@ export function Feed({
const onRefresh = React.useCallback(async () => {
try {
setIsPTRing(true)
await refetch()
await checkUnread({invalidate: true})
} catch (err) {
logger.error('Failed to refresh notifications feed', {
error: err,
@ -91,7 +87,7 @@ export function Feed({
} finally {
setIsPTRing(false)
}
}, [refetch, setIsPTRing])
}, [checkUnread, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
@ -136,21 +132,6 @@ export function Feed({
[onPressRetryLoadMore, moderationOpts],
)
const showHeaderSpinner = !isPTRing && isFetching && !isLoading
const FeedHeader = React.useCallback(
() => (
<View>
{ListHeaderComponent ? <ListHeaderComponent /> : null}
{showHeaderSpinner ? (
<View style={{padding: 10}}>
<ActivityIndicator />
</View>
) : null}
</View>
),
[ListHeaderComponent, showHeaderSpinner],
)
const FeedFooter = React.useCallback(
() =>
isFetchingNextPage ? (
@ -180,7 +161,7 @@ export function Feed({
data={items}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListHeaderComponent={FeedHeader}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl

View file

@ -12,6 +12,9 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSession} from '#/state/session'
import {TextLink} from '#/view/com/util/Link'
import {CenteredView} from '../util/Views'
import {isWeb} from 'platform/detection'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@ -79,11 +82,37 @@ function FeedsTabBarPublic() {
function FeedsTabBarTablet(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) {
const feeds = usePinnedFeedsInfos()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const pal = usePalette('default')
const {hasSession} = useSession()
const navigation = useNavigation<NavigationProp>()
const {headerMinimalShellTransform} = useMinimalShellMode()
const {headerHeight} = useShellLayout()
const items = feeds.map(f => f.displayName)
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
const items = showFeedsLinkInTabBar
? pinnedDisplayNames.concat('Feeds ✨')
: pinnedDisplayNames
const onPressDiscoverFeeds = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
const onSelect = React.useCallback(
(index: number) => {
if (showFeedsLinkInTabBar && index === items.length - 1) {
onPressDiscoverFeeds()
} else if (props.onSelect) {
props.onSelect(index)
}
},
[items.length, onPressDiscoverFeeds, props, showFeedsLinkInTabBar],
)
return (
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
@ -95,6 +124,7 @@ function FeedsTabBarTablet(
<TabBar
key={items.join(',')}
{...props}
onSelect={onSelect}
items={items}
indicatorColor={pal.colors.link}
/>

View file

@ -18,6 +18,9 @@ import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useSession} from '#/state/session'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {isWeb} from 'platform/detection'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
export function FeedsTabBar(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
@ -26,11 +29,36 @@ export function FeedsTabBar(
const {isSandbox, hasSession} = useSession()
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const feeds = usePinnedFeedsInfos()
const navigation = useNavigation<NavigationProp>()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
const {headerHeight} = useShellLayout()
const {headerMinimalShellTransform} = useMinimalShellMode()
const items = feeds.map(f => f.displayName)
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
const items = showFeedsLinkInTabBar
? pinnedDisplayNames.concat('Feeds ✨')
: pinnedDisplayNames
const onPressFeedsLink = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
const onSelect = React.useCallback(
(index: number) => {
if (showFeedsLinkInTabBar && index === items.length - 1) {
onPressFeedsLink()
} else if (props.onSelect) {
props.onSelect(index)
}
},
[items.length, onPressFeedsLink, props, showFeedsLinkInTabBar],
)
const onPressAvi = React.useCallback(() => {
setDrawerOpen(true)
@ -84,7 +112,7 @@ export function FeedsTabBar(
key={items.join(',')}
onPressSelected={props.onPressSelected}
selectedPage={props.selectedPage}
onSelect={props.onSelect}
onSelect={onSelect}
testID={props.testID}
items={items}
indicatorColor={pal.colors.link}

View file

@ -108,6 +108,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
pointerEvents: isHeaderReady ? 'auto' : 'none',
}}>
<TabBar
testID={testID}
items={items}
selectedPage={currentPage}
onSelect={props.onSelect}
@ -127,6 +128,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
isMobile,
onTabBarLayout,
onHeaderOnlyLayout,
testID,
],
)

View file

@ -68,6 +68,7 @@ export function TabBar({
return (
<View testID={testID} style={[pal.view, styles.outer]}>
<DraggableScrollView
testID={`${testID}-selector`}
horizontal={true}
showsHorizontalScrollIndicator={false}
ref={scrollElRef}
@ -76,6 +77,7 @@ export function TabBar({
const selected = i === selectedPage
return (
<PressableWithHover
testID={`${testID}-selector-${i}`}
key={item}
onLayout={e => onItemLayout(e, i)}
style={[styles.item, selected && indicatorStyle]}

View file

@ -23,6 +23,7 @@ import {
FeedDescriptor,
FeedParams,
usePostFeedQuery,
pollLatest,
} from '#/state/queries/post-feed'
import {useModerationOpts} from '#/state/queries/preferences'
@ -84,22 +85,21 @@ let Feed = ({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
pollLatest,
} = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
const checkForNew = React.useCallback(async () => {
if (!isFetched || isFetching || !onHasNew) {
if (!data?.pages[0] || isFetching || !onHasNew) {
return
}
try {
if (await pollLatest()) {
if (await pollLatest(data.pages[0])) {
onHasNew(true)
}
} catch (e) {
logger.error('Poll latest failed', {feed, error: String(e)})
}
}, [feed, isFetched, isFetching, pollLatest, onHasNew])
}, [feed, data, isFetching, onHasNew])
React.useEffect(() => {
// we store the interval handler in a ref to avoid needless

View file

@ -17,28 +17,15 @@ import {EmptyState} from '../util/EmptyState'
import {cleanError} from '#/lib/strings/errors'
import {useRemoveFeedMutation} from '#/state/queries/preferences'
enum KnownError {
Block,
FeedgenDoesNotExist,
FeedgenMisconfigured,
FeedgenBadResponse,
FeedgenOffline,
FeedgenUnknown,
Unknown,
}
const MESSAGES = {
[KnownError.Unknown]: '',
[KnownError.Block]: '',
[KnownError.FeedgenDoesNotExist]: `Hmmm, we're having trouble finding this feed. It may have been deleted.`,
[KnownError.FeedgenMisconfigured]:
'Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.',
[KnownError.FeedgenBadResponse]:
'Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.',
[KnownError.FeedgenOffline]:
'Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.',
[KnownError.FeedgenUnknown]:
'Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.',
export enum KnownError {
Block = 'Block',
FeedgenDoesNotExist = 'FeedgenDoesNotExist',
FeedgenMisconfigured = 'FeedgenMisconfigured',
FeedgenBadResponse = 'FeedgenBadResponse',
FeedgenOffline = 'FeedgenOffline',
FeedgenUnknown = 'FeedgenUnknown',
FeedNSFPublic = 'FeedNSFPublic',
Unknown = 'Unknown',
}
export function FeedErrorMessage({
@ -90,7 +77,32 @@ function FeedgenErrorMessage({
const pal = usePalette('default')
const {_: _l} = useLingui()
const navigation = useNavigation<NavigationProp>()
const msg = MESSAGES[knownError]
const msg = React.useMemo(
() =>
({
[KnownError.Unknown]: '',
[KnownError.Block]: '',
[KnownError.FeedgenDoesNotExist]: _l(
msgLingui`Hmmm, we're having trouble finding this feed. It may have been deleted.`,
),
[KnownError.FeedgenMisconfigured]: _l(
msgLingui`Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.`,
),
[KnownError.FeedgenBadResponse]: _l(
msgLingui`Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.`,
),
[KnownError.FeedgenOffline]: _l(
msgLingui`Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.`,
),
[KnownError.FeedNSFPublic]: _l(
msgLingui`We're sorry, but this content is not viewable without a Bluesky account.`,
),
[KnownError.FeedgenUnknown]: _l(
msgLingui`Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.`,
),
}[knownError]),
[_l, knownError],
)
const [_, uri] = feedDesc.split('|')
const [ownerDid] = safeParseFeedgenUri(uri)
const {openModal, closeModal} = useModalControls()
@ -121,6 +133,36 @@ function FeedgenErrorMessage({
})
}, [openModal, closeModal, uri, removeFeed, _l])
const cta = React.useMemo(() => {
switch (knownError) {
case KnownError.FeedNSFPublic: {
return null
}
case KnownError.FeedgenDoesNotExist:
case KnownError.FeedgenMisconfigured:
case KnownError.FeedgenBadResponse:
case KnownError.FeedgenOffline:
case KnownError.FeedgenUnknown: {
return (
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
{knownError === KnownError.FeedgenDoesNotExist && (
<Button
type="inverted"
label="Remove feed"
onPress={onRemoveFeed}
/>
)}
<Button
type="default-light"
label="View profile"
onPress={onViewProfile}
/>
</View>
)
}
}
}, [knownError, onViewProfile, onRemoveFeed])
return (
<View
style={[
@ -134,16 +176,7 @@ function FeedgenErrorMessage({
},
]}>
<Text style={pal.text}>{msg}</Text>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
{knownError === KnownError.FeedgenDoesNotExist && (
<Button type="inverted" label="Remove feed" onPress={onRemoveFeed} />
)}
<Button
type="default-light"
label="View profile"
onPress={onViewProfile}
/>
</View>
{cta}
</View>
)
}
@ -196,5 +229,8 @@ function detectKnownError(
if (error.includes('feed provided an invalid response')) {
return KnownError.FeedgenBadResponse
}
if (error.includes(KnownError.FeedNSFPublic)) {
return KnownError.FeedNSFPublic
}
return KnownError.FeedgenUnknown
}

View file

@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
noFeedback?: boolean
asAnchor?: boolean
anchorNoUnderline?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
}
export const Link = memo(function Link({
@ -58,6 +59,7 @@ export const Link = memo(function Link({
asAnchor,
accessible,
anchorNoUnderline,
navigationAction,
...props
}: Props) {
const {closeModal} = useModalControls()
@ -67,10 +69,16 @@ export const Link = memo(function Link({
const onPress = React.useCallback(
(e?: Event) => {
if (typeof href === 'string') {
return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
return onPressInner(
closeModal,
navigation,
sanitizeUrl(href),
navigationAction,
e,
)
}
},
[closeModal, navigation, href],
[closeModal, navigation, navigationAction, href],
)
if (noFeedback) {
@ -146,6 +154,7 @@ export const TextLink = memo(function TextLink({
title,
onPress,
warnOnMismatchingLabel,
navigationAction,
...orgProps
}: {
testID?: string
@ -158,6 +167,7 @@ export const TextLink = memo(function TextLink({
dataSet?: any
title?: string
warnOnMismatchingLabel?: boolean
navigationAction?: 'push' | 'replace' | 'navigate'
} & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)})
const navigation = useNavigation<NavigationProp>()
@ -185,7 +195,13 @@ export const TextLink = memo(function TextLink({
// @ts-ignore function signature differs by platform -prf
return onPress()
}
return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
return onPressInner(
closeModal,
navigation,
sanitizeUrl(href),
navigationAction,
e,
)
},
[
onPress,
@ -195,6 +211,7 @@ export const TextLink = memo(function TextLink({
href,
text,
warnOnMismatchingLabel,
navigationAction,
],
)
const hrefAttrs = useMemo(() => {
@ -241,6 +258,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
accessibilityLabel?: string
accessibilityHint?: string
title?: string
navigationAction?: 'push' | 'replace' | 'navigate'
}
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
testID,
@ -250,6 +268,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
text,
numberOfLines,
lineHeight,
navigationAction,
...props
}: TextLinkOnWebOnlyProps) {
if (isWeb) {
@ -263,6 +282,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
numberOfLines={numberOfLines}
lineHeight={lineHeight}
title={props.title}
navigationAction={navigationAction}
{...props}
/>
)
@ -296,6 +316,7 @@ function onPressInner(
closeModal = () => {},
navigation: NavigationProp,
href: string,
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
e?: Event,
) {
let shouldHandle = false
@ -328,8 +349,18 @@ function onPressInner(
} else {
closeModal() // close any active modals
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href)))
if (navigationAction === 'push') {
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href)))
} else if (navigationAction === 'replace') {
// @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.replace(...router.matchPath(href)))
} else if (navigationAction === 'navigate') {
// @ts-ignore we're not able to type check on this one -prf
navigation.navigate(...router.matchPath(href))
} else {
throw Error('Unsupported navigator action.')
}
}
}
}

View file

@ -171,14 +171,22 @@ export function ProfileCardFeedLoadingPlaceholder() {
export function FeedLoadingPlaceholder({
style,
showLowerPlaceholder = true,
showTopBorder = true,
}: {
style?: StyleProp<ViewStyle>
showTopBorder?: boolean
showLowerPlaceholder?: boolean
}) {
const pal = usePalette('default')
return (
<View
style={[
{paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
{
paddingHorizontal: 12,
paddingVertical: 18,
borderTopWidth: showTopBorder ? 1 : 0,
},
pal.border,
style,
]}>
@ -193,14 +201,16 @@ export function FeedLoadingPlaceholder({
<LoadingPlaceholder width={120} height={8} />
</View>
</View>
<View style={{paddingHorizontal: 5}}>
<LoadingPlaceholder
width={260}
height={8}
style={{marginVertical: 12}}
/>
<LoadingPlaceholder width={120} height={8} />
</View>
{showLowerPlaceholder && (
<View style={{paddingHorizontal: 5}}>
<LoadingPlaceholder
width={260}
height={8}
style={{marginVertical: 12}}
/>
<LoadingPlaceholder width={120} height={8} />
</View>
)}
</View>
)
}

View file

@ -437,6 +437,7 @@ export function FeedsScreen(_props: Props) {
showSaveBtn={hasSession}
showDescription
showLikes
pinOnSave
/>
)
} else if (item.type === 'popularFeedsNoResults') {

View file

@ -40,6 +40,12 @@ function HomeScreenReady({
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const [selectedPage, setSelectedPage] = React.useState(0)
/**
* Used to ensure that we re-compute `customFeeds` AND force a re-render of
* the pager with the new order of feeds.
*/
const pinnedFeedOrderKey = JSON.stringify(preferences.feeds.pinned)
const customFeeds = React.useMemo(() => {
const pinned = preferences.feeds.pinned
const feeds: FeedDescriptor[] = []
@ -83,7 +89,6 @@ function HomeScreenReady({
emitSoftReset()
}, [])
// TODO(pwi) may need this in public view
const onPageScrollStateChanged = React.useCallback(
(state: 'idle' | 'dragging' | 'settling') => {
if (state === 'dragging') {
@ -118,6 +123,7 @@ function HomeScreenReady({
return hasSession ? (
<Pager
key={pinnedFeedOrderKey}
testID="homeScreen"
onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged}

View file

@ -19,7 +19,10 @@ import {logger} from '#/logger'
import {useSetMinimalShellMode} from '#/state/shell'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {
useUnreadNotifications,
useUnreadNotificationsApi,
} from '#/state/queries/notifications/unread'
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
import {listenSoftReset, emitSoftReset} from '#/state/events'
@ -35,8 +38,9 @@ export function NotificationsScreen({}: Props) {
const {screen} = useAnalytics()
const pal = usePalette('default')
const {isDesktop} = useWebMediaQueries()
const unreadNotifs = useUnreadNotifications()
const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications()
const unreadApi = useUnreadNotificationsApi()
const hasNew = !!unreadNotifs
// event handlers
@ -48,10 +52,16 @@ export function NotificationsScreen({}: Props) {
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
queryClient.invalidateQueries({
queryKey: NOTIFS_RQKEY(),
})
}, [scrollToTop, queryClient])
if (hasNew) {
// render what we have now
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
} else {
// check with the server
unreadApi.checkUnread({invalidate: true})
}
}, [scrollToTop, queryClient, unreadApi, hasNew])
// on-visible setup
// =

View file

@ -267,6 +267,7 @@ function ProfileScreenLoaded({
screenDescription="profile"
moderation={moderation.account}>
<PagerWithHeader
testID="profilePager"
isHeaderReady={true}
items={sectionTitles}
onPageSelected={onPageSelected}
@ -403,7 +404,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({

View file

@ -353,6 +353,7 @@ export function ProfileFeedScreenInner({
style={styles.btn}
/>
<Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
type={isPinned ? 'default' : 'inverted'}
label={isPinned ? 'Unpin' : 'Pin to home'}
@ -501,7 +502,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])

View file

@ -127,7 +127,7 @@ function ProfileListScreenLoaded({
list,
onChange() {
if (isCurateList) {
queryClient.invalidateQueries({
queryClient.resetQueries({
// TODO(eric) should construct these strings with a fn too
queryKey: FEED_RQKEY(`list|${list.uri}`),
})
@ -530,7 +530,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({

View file

@ -1,14 +1,7 @@
import React from 'react'
import {
StyleSheet,
View,
ActivityIndicator,
Pressable,
TouchableOpacity,
} from 'react-native'
import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
@ -32,9 +25,8 @@ import {
usePinFeedMutation,
useUnpinFeedMutation,
useSetSaveFeedsMutation,
preferencesQueryKey,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
const HITSLOP_TOP = {
top: 20,
@ -57,6 +49,24 @@ export function SavedFeeds({}: Props) {
const {screen} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode()
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: setSavedFeeds,
variables: optimisticSavedFeedsResponse,
reset: resetSaveFeedsMutationState,
error: setSavedFeedsError,
} = useSetSaveFeedsMutation()
/*
* Use optimistic data if exists and no error, otherwise fallback to remote
* data
*/
const currentFeeds =
optimisticSavedFeedsResponse && !setSavedFeedsError
? optimisticSavedFeedsResponse
: preferences?.feeds || {saved: [], pinned: []}
const unpinned = currentFeeds.saved.filter(f => {
return !currentFeeds.pinned?.includes(f)
})
useFocusEffect(
React.useCallback(() => {
@ -80,7 +90,7 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
{preferences?.feeds ? (
!preferences.feeds.pinned.length ? (
!currentFeeds.pinned.length ? (
<View
style={[
pal.border,
@ -93,8 +103,15 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
) : (
preferences?.feeds?.pinned?.map(uri => (
<ListItem key={uri} feedUri={uri} isPinned />
currentFeeds.pinned.map(uri => (
<ListItem
key={uri}
feedUri={uri}
isPinned
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
/>
))
)
) : (
@ -106,7 +123,7 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
{preferences?.feeds ? (
!preferences.feeds.unpinned.length ? (
!unpinned.length ? (
<View
style={[
pal.border,
@ -119,8 +136,15 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
) : (
preferences.feeds.unpinned.map(uri => (
<ListItem key={uri} feedUri={uri} isPinned={false} />
unpinned.map(uri => (
<ListItem
key={uri}
feedUri={uri}
isPinned={false}
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
/>
))
)
) : (
@ -151,22 +175,30 @@ export function SavedFeeds({}: Props) {
function ListItem({
feedUri,
isPinned,
currentFeeds,
setSavedFeeds,
resetSaveFeedsMutationState,
}: {
feedUri: string // uri
isPinned: boolean
currentFeeds: {saved: string[]; pinned: string[]}
setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
resetSaveFeedsMutationState: ReturnType<
typeof useSetSaveFeedsMutation
>['reset']
}) {
const pal = usePalette('default')
const queryClient = useQueryClient()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
useUnpinFeedMutation()
const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
useSetSaveFeedsMutation()
const isPending = isPinPending || isUnpinPending
const onTogglePinned = React.useCallback(async () => {
Haptics.default()
try {
resetSaveFeedsMutationState()
if (isPinned) {
await unpinFeed({uri: feedUri})
} else {
@ -176,23 +208,20 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to toggle pinned feed', {error: e})
}
}, [feedUri, isPinned, pinFeed, unpinFeed])
}, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
const onPressUp = React.useCallback(async () => {
if (!isPinned) return
const feeds =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)?.feeds
const pinned = feeds?.pinned ?? []
// create new array, do not mutate
const pinned = [...currentFeeds.pinned]
const index = pinned.indexOf(feedUri)
if (index === -1 || index === 0) return
;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
try {
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
await setSavedFeeds({saved: currentFeeds.saved, pinned})
track('CustomFeed:Reorder', {
uri: feedUri,
index: pinned.indexOf(feedUri),
@ -201,23 +230,19 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to set pinned feed order', {error: e})
}
}, [feedUri, isPinned, queryClient, setSavedFeeds])
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
const onPressDown = React.useCallback(async () => {
if (!isPinned) return
const feeds =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)?.feeds
const pinned = feeds?.pinned ?? []
const pinned = [...currentFeeds.pinned]
const index = pinned.indexOf(feedUri)
if (index === -1 || index >= pinned.length - 1) return
;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
try {
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
await setSavedFeeds({saved: currentFeeds.saved, pinned})
track('CustomFeed:Reorder', {
uri: feedUri,
index: pinned.indexOf(feedUri),
@ -226,7 +251,7 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to set pinned feed order', {error: e})
}
}, [feedUri, isPinned, queryClient, setSavedFeeds])
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
return (
<Pressable
@ -234,24 +259,30 @@ function ListItem({
style={[styles.itemContainer, pal.border]}>
{isPinned ? (
<View style={styles.webArrowButtonsContainer}>
<TouchableOpacity
disabled={isMovePending}
<Pressable
disabled={isPending}
accessibilityRole="button"
onPress={onPressUp}
hitSlop={HITSLOP_TOP}>
hitSlop={HITSLOP_TOP}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="arrow-up"
size={12}
style={[pal.text, styles.webArrowUpButton]}
/>
</TouchableOpacity>
<TouchableOpacity
disabled={isMovePending}
</Pressable>
<Pressable
disabled={isPending}
accessibilityRole="button"
onPress={onPressDown}
hitSlop={HITSLOP_BOTTOM}>
hitSlop={HITSLOP_BOTTOM}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
</TouchableOpacity>
</Pressable>
</View>
) : null}
<FeedSourceCard
@ -259,18 +290,28 @@ function ListItem({
feedUri={feedUri}
style={styles.noBorder}
showSaveBtn
LoadingComponent={
<FeedLoadingPlaceholder
style={{flex: 1}}
showLowerPlaceholder={false}
showTopBorder={false}
/>
}
/>
<TouchableOpacity
disabled={isPinPending || isUnpinPending}
<Pressable
disabled={isPending}
accessibilityRole="button"
hitSlop={10}
onPress={onTogglePinned}>
onPress={onTogglePinned}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="thumb-tack"
size={20}
color={isPinned ? colors.blue3 : pal.colors.icon}
/>
</TouchableOpacity>
</Pressable>
</Pressable>
)
}

View file

@ -10,11 +10,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import {
useFocusEffect,
useNavigation,
StackActions,
} from '@react-navigation/native'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@ -73,6 +69,8 @@ import {STATUS_PAGE_URL} from 'lib/constants'
import {Plural, Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
function SettingsAccountCard({account}: {account: SessionAccount}) {
const pal = usePalette('default')
@ -154,13 +152,14 @@ export function SettingsScreen({}: Props) {
const {screen, track} = useAnalytics()
const {openModal} = useModalControls()
const {isSwitchingAccounts, accounts, currentAccount} = useSession()
const {clearCurrentAccount} = useSessionApi()
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
getAgent(),
)
const {mutate: clearPreferences} = useClearPreferencesMutation()
const {data: invites} = useInviteCodesQuery()
const invitesAvailable = invites?.available?.length ?? 0
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const primaryBg = useCustomPalette<ViewStyle>({
light: {backgroundColor: colors.blue0},
@ -189,10 +188,9 @@ export function SettingsScreen({}: Props) {
const onPressAddAccount = React.useCallback(() => {
track('Settings:AddAccountButtonClicked')
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
clearCurrentAccount()
}, [track, navigation, clearCurrentAccount])
setShowLoggedOut(true)
closeAllActiveElements()
}, [track, setShowLoggedOut, closeAllActiveElements])
const onPressChangeHandle = React.useCallback(() => {
track('Settings:ChangeHandleButtonClicked')

View file

@ -141,7 +141,7 @@ export function DrawerContent() {
} else {
if (tab === 'Notifications') {
// fetch new notifs on view
queryClient.invalidateQueries({
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
}

View file

@ -62,7 +62,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
} else {
if (tab === 'Notifications') {
// fetch new notifs on view
queryClient.invalidateQueries({
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
}

View file

@ -137,7 +137,7 @@ const NavItem: React.FC<{
: isTab(currentRoute.name, routeName)
return (
<Link href={href} style={styles.ctrl}>
<Link href={href} style={styles.ctrl} navigationAction="navigate">
{children({isActive})}
</Link>
)

View file

@ -11,7 +11,7 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
export function DesktopFeeds() {
const pal = usePalette('default')
const {_} = useLingui()
const feeds = usePinnedFeedsInfos()
const {feeds} = usePinnedFeedsInfos()
const route = useNavigationState(state => {
if (!state) {

View file

@ -150,7 +150,7 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
} else {
if (href === '/notifications') {
// fetch new notifs on view
queryClient.invalidateQueries({
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
}