diff --git a/src/components/Link.tsx b/src/components/Link.tsx index a2e952a6ef..d8ac829b67 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -12,7 +12,8 @@ import { isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers' -import {isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' +import {shouldClickOpenNewTab} from '#/platform/urls' import {useModalControls} from '#/state/modals' import {useOpenLink} from '#/state/preferences/in-app-browser' import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' @@ -116,16 +117,7 @@ export function useLink({ if (isExternal) { openLink(href) } else { - /** - * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch - * of @ts-ignore below. - */ - const event = e as any - const isMiddleClick = isWeb && event.button === 1 - const isMetaKey = - isWeb && - (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) - const shouldOpenInNewTab = isMetaKey || isMiddleClick + const shouldOpenInNewTab = shouldClickOpenNewTab(e) if (isBskyDownloadUrl(href)) { shareUrl(BSKY_DOWNLOAD_URL) diff --git a/src/lib/hooks/useMinimalShellMode.tsx b/src/lib/hooks/useMinimalShellTransform.ts similarity index 51% rename from src/lib/hooks/useMinimalShellMode.tsx rename to src/lib/hooks/useMinimalShellTransform.ts index e81fc434fe..9875840d65 100644 --- a/src/lib/hooks/useMinimalShellMode.tsx +++ b/src/lib/hooks/useMinimalShellTransform.ts @@ -1,23 +1,15 @@ import {interpolate, useAnimatedStyle} from 'react-native-reanimated' -import {useMinimalShellMode as useMinimalShellModeState} from '#/state/shell/minimal-mode' + +import {useMinimalShellMode} from '#/state/shell/minimal-mode' import {useShellLayout} from '#/state/shell/shell-layout' -export function useMinimalShellMode() { - const mode = useMinimalShellModeState() - const {footerHeight, headerHeight} = useShellLayout() +// Keep these separated so that we only pay for useAnimatedStyle that gets used. - const footerMinimalShellTransform = useAnimatedStyle(() => { - return { - pointerEvents: mode.value === 0 ? 'auto' : 'none', - opacity: Math.pow(1 - mode.value, 2), - transform: [ - { - translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]), - }, - ], - } - }) - const headerMinimalShellTransform = useAnimatedStyle(() => { +export function useMinimalShellHeaderTransform() { + const mode = useMinimalShellMode() + const {headerHeight} = useShellLayout() + + const headerTransform = useAnimatedStyle(() => { return { pointerEvents: mode.value === 0 ? 'auto' : 'none', opacity: Math.pow(1 - mode.value, 2), @@ -28,7 +20,32 @@ export function useMinimalShellMode() { ], } }) - const fabMinimalShellTransform = useAnimatedStyle(() => { + + return headerTransform +} + +export function useMinimalShellFooterTransform() { + const mode = useMinimalShellMode() + const {footerHeight} = useShellLayout() + + const footerTransform = useAnimatedStyle(() => { + return { + pointerEvents: mode.value === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - mode.value, 2), + transform: [ + { + translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]), + }, + ], + } + }) + return footerTransform +} + +export function useMinimalShellFabTransform() { + const mode = useMinimalShellMode() + + const fabTransform = useAnimatedStyle(() => { return { transform: [ { @@ -37,9 +54,5 @@ export function useMinimalShellMode() { ], } }) - return { - footerMinimalShellTransform, - headerMinimalShellTransform, - fabMinimalShellTransform, - } + return fabTransform } diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 55a7948ef3..58aa4087c0 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -72,14 +72,17 @@ export function useNotificationsRegistration() { export function useRequestNotificationsPermission() { const gate = useGate() + const {currentAccount} = useSession() - return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => { + return async ( + context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home', + ) => { const permissions = await Notifications.getPermissionsAsync() if ( !isNative || permissions?.status === 'granted' || - permissions?.status === 'denied' + (permissions?.status === 'denied' && !permissions.canAskAgain) ) { return } @@ -95,6 +98,9 @@ export function useRequestNotificationsPermission() { ) { return } + if (context === 'Home' && !currentAccount) { + return + } const res = await Notifications.requestPermissionsAsync() logEvent('notifications:request', { diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 753734edd8..9939f60c9c 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -17,7 +17,7 @@ export type LogEvents = { } 'notifications:openApp': {} 'notifications:request': { - context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' + context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home' status: 'granted' | 'denied' | 'undetermined' } 'state:background:sampled': { diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 96be12c2d8..4481935f77 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -3,4 +3,3 @@ export type Gate = | 'request_notifications_permission_after_onboarding_v2' | 'show_avi_follow_button' | 'show_follow_back_label_v2' - | 'show_notification_badge_mobile_web' diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index 24ab678934..cbaaf445a8 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -116,7 +116,7 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { const langs = appLanguage.split(',').filter(Boolean) for (const lang of langs) { - switch (lang) { + switch (fixLegacyLanguageCode(lang)) { case 'en': return AppLanguage.en case 'ca': @@ -157,3 +157,20 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { } return AppLanguage.en } + +export function fixLegacyLanguageCode(code: string | null): string | null { + // handle some legacy code conversions, see https://xml.coverpages.org/iso639a.html + if (code === 'in') { + // indonesian + return 'id' + } + if (code === 'iw') { + // hebrew + return 'he' + } + if (code === 'ji') { + // yiddish + return 'yi' + } + return code +} diff --git a/src/platform/detection.ts b/src/platform/detection.ts index fffde1be2d..0c0360a82a 100644 --- a/src/platform/detection.ts +++ b/src/platform/detection.ts @@ -2,6 +2,7 @@ import {Platform} from 'react-native' import {isReducedMotion} from 'react-native-reanimated' import {getLocales} from 'expo-localization' +import {fixLegacyLanguageCode} from '#/locale/helpers' import {dedupArray} from 'lib/functions' export const isIOS = Platform.OS === 'ios' @@ -17,7 +18,7 @@ export const isMobileWeb = export const deviceLocales = dedupArray( getLocales?.() - .map?.(locale => locale.languageCode) + .map?.(locale => fixLegacyLanguageCode(locale.languageCode)) .filter(code => typeof code === 'string'), ) as string[] diff --git a/src/platform/urls.tsx b/src/platform/urls.tsx index fd844d9331..fd9d297aa2 100644 --- a/src/platform/urls.tsx +++ b/src/platform/urls.tsx @@ -1,4 +1,5 @@ -import {Linking} from 'react-native' +import {GestureResponderEvent, Linking} from 'react-native' + import {isNative, isWeb} from './detection' export async function getInitialURL(): Promise { @@ -23,3 +24,15 @@ export function clearHash() { window.location.hash = '' } } + +export function shouldClickOpenNewTab(e: GestureResponderEvent) { + /** + * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch + * of @ts-ignore below. + */ + const event = e as any + const isMiddleClick = isWeb && event.button === 1 + const isMetaKey = + isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + return isMetaKey || isMiddleClick +} diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index cc52eef3bb..201c8f7e07 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -56,7 +56,7 @@ export const ProfileFeedSection = React.forwardRef< })) const renderPostsEmpty = React.useCallback(() => { - return + return }, [_]) React.useEffect(() => { diff --git a/src/state/persisted/store.ts b/src/state/persisted/store.ts index 421bdccdf7..ca023a6363 100644 --- a/src/state/persisted/store.ts +++ b/src/state/persisted/store.ts @@ -13,7 +13,13 @@ export async function write(value: Schema) { export async function read(): Promise { const rawData = await AsyncStorage.getItem(BSKY_STORAGE) const objData = rawData ? JSON.parse(rawData) : undefined + + // new user + if (!objData) return undefined + + // existing user, validate const parsed = schema.safeParse(objData) + if (parsed.success) { return objData } else { diff --git a/src/state/queries/messages/list-converations.tsx b/src/state/queries/messages/list-converations.tsx index ce2cd70798..eeab246ab3 100644 --- a/src/state/queries/messages/list-converations.tsx +++ b/src/state/queries/messages/list-converations.tsx @@ -16,6 +16,7 @@ import { useInfiniteQuery, useQueryClient, } from '@tanstack/react-query' +import throttle from 'lodash.throttle' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useMessagesEventBus} from '#/state/messages/events' @@ -89,6 +90,15 @@ export function ListConvosProviderInner({ const {currentConvoId} = useCurrentConvoId() const {currentAccount} = useSession() + const debouncedRefetch = useMemo( + () => + throttle(refetch, 500, { + leading: true, + trailing: true, + }), + [refetch], + ) + useEffect(() => { const unsub = messagesBus.on( events => { @@ -96,7 +106,7 @@ export function ListConvosProviderInner({ events.logs.forEach(log => { if (ChatBskyConvoDefs.isLogBeginConvo(log)) { - refetch() + debouncedRefetch() } else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) { queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => optimisticDelete(log.convoId, old), @@ -170,7 +180,7 @@ export function ListConvosProviderInner({ }), } } else { - refetch() + debouncedRefetch() } }) } @@ -183,7 +193,14 @@ export function ListConvosProviderInner({ ) return () => unsub() - }, [messagesBus, currentConvoId, refetch, queryClient, currentAccount?.did]) + }, [ + messagesBus, + currentConvoId, + refetch, + queryClient, + currentAccount?.did, + debouncedRefetch, + ]) const ctx = useMemo(() => { return data?.pages.flatMap(page => page.convos) ?? [] diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index fac08a7113..5bcac2e67d 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -305,7 +305,13 @@ export const ComposePost = observer(function ComposePost({ localThumb: undefined, } as apilib.ExternalEmbedDraft) } - setError(cleanError(e.message)) + let err = cleanError(e.message) + if (err.includes('not locate record')) { + err = _( + msg`We're sorry! The post you are replying to has been deleted.`, + ) + } + setError(err) setIsProcessing(false) return } finally { @@ -785,11 +791,12 @@ const styles = StyleSheet.create({ }, errorLine: { flexDirection: 'row', + alignItems: 'center', backgroundColor: colors.red1, borderRadius: 6, marginHorizontal: 16, - paddingHorizontal: 8, - paddingVertical: 6, + paddingHorizontal: 12, + paddingVertical: 10, marginBottom: 8, }, reminderLine: { diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 589f674b44..a617894342 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -1,5 +1,12 @@ import React from 'react' -import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import { + Linking, + Pressable, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native' import {AtUri} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' @@ -26,6 +33,7 @@ import {RichText} from '#/components/RichText' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' import hairlineWidth = StyleSheet.hairlineWidth +import {shouldClickOpenNewTab} from '#/platform/urls' export function FeedSourceCard({ feedUri, @@ -203,17 +211,30 @@ export function FeedSourceCardLoaded({ style, {borderTopWidth: hideTopBorder ? 0 : hairlineWidth}, ]} - onPress={() => { + onPress={e => { + const shouldOpenInNewTab = shouldClickOpenNewTab(e) if (feed.type === 'feed') { - navigation.push('ProfileFeed', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) + if (shouldOpenInNewTab) { + Linking.openURL( + `/profile/${feed.creatorDid}/feed/${new AtUri(feed.uri).rkey}`, + ) + } else { + navigation.push('ProfileFeed', { + name: feed.creatorDid, + rkey: new AtUri(feed.uri).rkey, + }) + } } else if (feed.type === 'list') { - navigation.push('ProfileList', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) + if (shouldOpenInNewTab) { + Linking.openURL( + `/profile/${feed.creatorDid}/lists/${new AtUri(feed.uri).rkey}`, + ) + } else { + navigation.push('ProfileList', { + name: feed.creatorDid, + rkey: new AtUri(feed.uri).rkey, + }) + } } }} key={feed.uri}> diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index f00a15b3f4..474fc392da 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react' import {CogIcon} from '#/lib/icons' import {useSession} from '#/state/session' import {useShellLayout} from '#/state/shell/shell-layout' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {Logo} from '#/view/icons/Logo' @@ -39,7 +39,7 @@ function HomeHeaderLayoutDesktopAndTablet({ tabBarAnchor: JSX.Element | null | undefined }) { const pal = usePalette('default') - const {headerMinimalShellTransform} = useMinimalShellMode() + const headerMinimalShellTransform = useMinimalShellHeaderTransform() const {headerHeight} = useShellLayout() const {hasSession} = useSession() const {_} = useLingui() diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 78fa9af865..5ce1d80a72 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -10,7 +10,7 @@ import {useSession} from '#/state/session' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' import {HITSLOP_10} from 'lib/constants' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {Logo} from '#/view/icons/Logo' @@ -30,7 +30,7 @@ export function HomeHeaderLayoutMobile({ const {_} = useLingui() const setDrawerOpen = useSetDrawerOpen() const {headerHeight} = useShellLayout() - const {headerMinimalShellTransform} = useMinimalShellMode() + const headerMinimalShellTransform = useMinimalShellHeaderTransform() const {hasSession} = useSession() const onPressAvi = React.useCallback(() => { diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index a152bc9095..2b77739d32 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -49,8 +49,7 @@ export function FeedErrorMessage({ if ( typeof knownError !== 'undefined' && knownError !== KnownError.Unknown && - (savedFeedConfig?.type === 'feed' || - knownError === KnownError.FeedNSFPublic) + feedDesc.startsWith('feedgen') ) { return ( }) { const pal = usePalette('default') + const {isTabletOrDesktop} = useWebMediaQueries() + const iconSize = isTabletOrDesktop ? 80 : 64 return ( - + style={[ + styles.container, + isWeb && pal.border, + isTabletOrDesktop && {paddingRight: 20}, + style, + ]}> + {icon === 'user-group' ? ( - + + ) : icon === 'growth' ? ( + ) : ( )} - + {message} @@ -51,16 +63,23 @@ export function EmptyState({ const styles = StyleSheet.create({ container: { - paddingVertical: 24, - paddingHorizontal: 36, borderTopWidth: isWeb ? 1 : undefined, }, iconContainer: { flexDirection: 'row', - }, - icon: { + alignItems: 'center', + justifyContent: 'center', + height: 100, + width: 100, marginLeft: 'auto', marginRight: 'auto', + borderRadius: 80, + marginTop: 30, + }, + iconContainerBig: { + width: 140, + height: 140, + marginTop: 50, }, text: { textAlign: 'center', diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 4c0f0e3e5c..95b6e290c8 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -8,7 +8,7 @@ import {useNavigation} from '@react-navigation/native' import {useSetDrawerOpen} from '#/state/shell' import {useAnalytics} from 'lib/analytics/analytics' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {NavigationProp} from 'lib/routes/types' @@ -197,7 +197,7 @@ function Container({ showBorder?: boolean }) { const pal = usePalette('default') - const {headerMinimalShellTransform} = useMinimalShellMode() + const headerMinimalShellTransform = useMinimalShellHeaderTransform() if (!hideOnScroll) { return ( diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index c9443127b8..e6fb0ad465 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -4,7 +4,7 @@ import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {LinearGradient} from 'expo-linear-gradient' -import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' +import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {clamp} from '#/lib/numbers' import {gradients} from '#/lib/styles' @@ -20,7 +20,7 @@ export interface FABProps export function FABInner({testID, icon, ...props}: FABProps) { const insets = useSafeAreaInsets() const {isMobile, isTablet} = useWebMediaQueries() - const {fabMinimalShellTransform} = useMinimalShellMode() + const fabMinimalShellTransform = useMinimalShellFabTransform() const { state: pressed, onIn: onPressIn, diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index a6b40f9f52..9de03a4f33 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -6,7 +6,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useMediaQuery} from 'react-responsive' import {HITSLOP_20} from '#/lib/constants' -import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' +import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {clamp} from '#/lib/numbers' @@ -30,7 +30,7 @@ export function LoadLatestBtn({ const pal = usePalette('default') const {hasSession} = useSession() const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() - const {fabMinimalShellTransform} = useMinimalShellMode() + const fabMinimalShellTransform = useMinimalShellFabTransform() const insets = useSafeAreaInsets() // move button inline if it starts overlapping the left nav diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 8105084479..81e89d42d9 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' import {useRequireAuth} from '#/state/session' import {atoms as a, useTheme} from '#/alf' @@ -64,7 +65,8 @@ let RepostButton = ({ } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`} shape="round" variant="ghost" - color="secondary"> + color="secondary" + hitSlop={big ? HITSLOP_20 : HITSLOP_10}> {typeof repostCount !== 'undefined' && repostCount > 0 ? ( { + requestNotificationsPermission('Home') + }, [requestNotificationsPermission]) + const pagerRef = React.useRef(null) const lastPagerReportedIndexRef = React.useRef(selectedIndex) React.useLayoutEffect(() => { diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 67b00021ee..765b783e8f 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -30,6 +30,7 @@ import {TextLink} from 'view/com/util/Link' import {ListMethods} from 'view/com/util/List' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {CenteredView} from 'view/com/util/Views' +import {Loader} from '#/components/Loader' import {Feed} from '../com/notifications/Feed' import {FAB} from '../com/util/fab/FAB' import {MainScrollProvider} from '../com/util/MainScrollProvider' @@ -43,6 +44,7 @@ export function NotificationsScreen({}: Props) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) const scrollElRef = React.useRef(null) const {screen} = useAnalytics() const pal = usePalette('default') @@ -68,9 +70,13 @@ export function NotificationsScreen({}: Props) { truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) } else { // check with the server - unreadApi.checkUnread({invalidate: true}) + setIsLoadingLatest(true) + unreadApi + .checkUnread({invalidate: true}) + .catch(() => undefined) + .then(() => setIsLoadingLatest(false)) } - }, [scrollToTop, queryClient, unreadApi, hasNew]) + }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest]) const onFocusCheckLatest = useNonReactiveCallback(() => { // on focus, check for latest, but only invalidate if the user @@ -139,11 +145,20 @@ export function NotificationsScreen({}: Props) { } onPress={emitSoftReset} /> + {isLoadingLatest ? : <>} ) } return <> - }, [isDesktop, pal, hasNew]) + }, [isDesktop, pal, hasNew, isLoadingLatest]) + + const renderHeaderSpinner = React.useCallback(() => { + return ( + + {isLoadingLatest ? : <>} + + ) + }, [isLoadingLatest]) return ( export function PostThreadScreen({route}: Props) { const queryClient = useQueryClient() const {hasSession} = useSession() - const {fabMinimalShellTransform} = useMinimalShellMode() + const fabMinimalShellTransform = useMinimalShellFabTransform() const setMinimalShellMode = useSetMinimalShellMode() const {openComposer} = useComposerControls() const safeAreaInsets = useSafeAreaInsets() diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 3dd8c3ac85..f272b90a03 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -468,7 +468,7 @@ const FeedSection = React.forwardRef( }, [onScrollToTop, isScreenFocused]) const renderPostsEmpty = useCallback(() => { - return + return }, [_]) return ( diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 0c2cecbfce..72de428f30 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -726,7 +726,7 @@ const FeedSection = React.forwardRef( }, [onScrollToTop, isScreenFocused]) const renderPostsEmpty = useCallback(() => { - return + return }, [_]) return ( diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 0a7897f17e..b5ad92b4c4 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -10,7 +10,7 @@ import {StackActions} from '@react-navigation/native' import {useAnalytics} from '#/lib/analytics/analytics' import {useHaptics} from '#/lib/haptics' import {useDedupe} from '#/lib/hooks/useDedupe' -import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' +import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' import {usePalette} from '#/lib/hooks/usePalette' import {clamp} from '#/lib/numbers' @@ -66,7 +66,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { useNavigationTabState() const numUnreadNotifications = useUnreadNotifications() const numUnreadMessages = useUnreadMessageCount() - const {footerMinimalShellTransform} = useMinimalShellMode() + const footerMinimalShellTransform = useMinimalShellFooterTransform() const {data: profile} = useProfileQuery({did: currentAccount?.did}) const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index fcd4c86082..21c253ee00 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -6,7 +6,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigationState} from '@react-navigation/native' -import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' +import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' import {usePalette} from '#/lib/hooks/usePalette' import {clamp} from '#/lib/numbers' import {getCurrentRoute, isTab} from '#/lib/routes/helpers' @@ -16,7 +16,6 @@ import {s} from '#/lib/styles' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' -import {useGate} from 'lib/statsig/statsig' import {useUnreadMessageCount} from 'state/queries/messages/list-converations' import {useUnreadNotifications} from 'state/queries/notifications/unread' import {Button} from '#/view/com/util/forms/Button' @@ -49,8 +48,7 @@ export function BottomBarWeb() { const {hasSession, currentAccount} = useSession() const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() - const gate = useGate() - const {footerMinimalShellTransform} = useMinimalShellMode() + const footerMinimalShellTransform = useMinimalShellFooterTransform() const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() const iconWidth = 26 @@ -115,14 +113,13 @@ export function BottomBarWeb() { width={iconWidth - 1} style={[styles.ctrlIcon, pal.text, styles.messagesIcon]} /> - {unreadMessageCount.count > 0 && - gate('show_notification_badge_mobile_web') && ( - - - {unreadMessageCount.numUnread} - - - )} + {unreadMessageCount.count > 0 && ( + + + {unreadMessageCount.numUnread} + + + )} ) }} @@ -136,14 +133,13 @@ export function BottomBarWeb() { width={iconWidth} style={[styles.ctrlIcon, pal.text, styles.bellIcon]} /> - {notificationCountStr !== '' && - gate('show_notification_badge_mobile_web') && ( - - - {notificationCountStr} - - - )} + {notificationCountStr !== '' && ( + + + {notificationCountStr} + + + )} ) }}