From 1317d881ed2f583a65bffeb835b2d57670cce235 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 10 Jun 2024 15:08:13 +0100 Subject: [PATCH 01/13] debounce refetching listconvos (#4455) --- .../queries/messages/list-converations.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/state/queries/messages/list-converations.tsx b/src/state/queries/messages/list-converations.tsx index ce2cd70798..306d4cae5a 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 debounce from 'lodash.debounce' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useMessagesEventBus} from '#/state/messages/events' @@ -89,6 +90,11 @@ export function ListConvosProviderInner({ const {currentConvoId} = useCurrentConvoId() const {currentAccount} = useSession() + const debouncedRefetch = useMemo( + () => debounce(() => refetch, 500), + [refetch], + ) + useEffect(() => { const unsub = messagesBus.on( events => { @@ -96,7 +102,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 +176,7 @@ export function ListConvosProviderInner({ }), } } else { - refetch() + debouncedRefetch() } }) } @@ -183,7 +189,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) ?? [] From fd03ea3fe1d4f3c6a4079272b0dbd21c4e0d2b1b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 10 Jun 2024 16:02:57 +0100 Subject: [PATCH 02/13] Throttle instead of debounce (#4456) * throttle instead of debounce * trailing: true * Fix throttle call --------- Co-authored-by: Dan Abramov --- src/state/queries/messages/list-converations.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/state/queries/messages/list-converations.tsx b/src/state/queries/messages/list-converations.tsx index 306d4cae5a..eeab246ab3 100644 --- a/src/state/queries/messages/list-converations.tsx +++ b/src/state/queries/messages/list-converations.tsx @@ -16,7 +16,7 @@ import { useInfiniteQuery, useQueryClient, } from '@tanstack/react-query' -import debounce from 'lodash.debounce' +import throttle from 'lodash.throttle' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useMessagesEventBus} from '#/state/messages/events' @@ -91,7 +91,11 @@ export function ListConvosProviderInner({ const {currentAccount} = useSession() const debouncedRefetch = useMemo( - () => debounce(() => refetch, 500), + () => + throttle(refetch, 500, { + leading: true, + trailing: true, + }), [refetch], ) From b688da8d5860569108a7f487dc97c5f0b403aee1 Mon Sep 17 00:00:00 2001 From: Francesco Lodovici <2643961+frncs-eu@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:19:28 +0200 Subject: [PATCH 03/13] Fix (#4430): Use separate hooks for shell mode animated styles (#4451) * Fix (#4430): Use separate hooks for shell mode animated styles * Consolidate in one file --------- Co-authored-by: Dan Abramov --- ...llMode.tsx => useMinimalShellTransform.ts} | 57 ++++++++++++------- src/view/com/home/HomeHeaderLayout.web.tsx | 4 +- src/view/com/home/HomeHeaderLayoutMobile.tsx | 4 +- src/view/com/util/ViewHeader.tsx | 4 +- src/view/com/util/fab/FABInner.tsx | 4 +- .../com/util/load-latest/LoadLatestBtn.tsx | 4 +- src/view/screens/PostThread.tsx | 4 +- src/view/shell/bottom-bar/BottomBar.tsx | 4 +- src/view/shell/bottom-bar/BottomBarWeb.tsx | 4 +- 9 files changed, 51 insertions(+), 38 deletions(-) rename src/lib/hooks/{useMinimalShellMode.tsx => useMinimalShellTransform.ts} (51%) 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/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/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/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 70378f4b81..89234c46b8 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -14,7 +14,7 @@ import { import {useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' -import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {useMinimalShellFabTransform} from 'lib/hooks/useMinimalShellTransform' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {makeRecordUri} from 'lib/strings/url-helpers' @@ -26,7 +26,7 @@ type Props = NativeStackScreenProps 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/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..ff52af07bd 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' @@ -50,7 +50,7 @@ export function BottomBarWeb() { 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 From 620ab887132855d447f0a328bf58989c8f3f3328 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 10 Jun 2024 18:17:26 +0200 Subject: [PATCH 04/13] Ungate mobile web notification badge (#4459) --- src/lib/statsig/gates.ts | 1 - src/view/shell/bottom-bar/BottomBarWeb.tsx | 32 ++++++++++------------ 2 files changed, 14 insertions(+), 19 deletions(-) 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/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index ff52af07bd..21c253ee00 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -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,7 +48,6 @@ export function BottomBarWeb() { const {hasSession, currentAccount} = useSession() const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() - const gate = useGate() const footerMinimalShellTransform = useMinimalShellFooterTransform() const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() @@ -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} + + + )} ) }} From 59f49bef68500c1719ed44470121f553208edc85 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 10 Jun 2024 11:37:14 -0700 Subject: [PATCH 05/13] Override legacy language codes for indonesian, hebrew, and yiddish (#4461) * Manually override incorrect 'in' to 'id' lang code * Fix additional legacy language code issues --- src/locale/helpers.ts | 19 ++++++++++++++++++- src/platform/detection.ts | 3 ++- 2 files changed, 20 insertions(+), 2 deletions(-) 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[] From 90ec22a6749555f48bfd21eec81f877f8eae0524 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 10 Jun 2024 11:44:13 -0700 Subject: [PATCH 06/13] Add support for new-tab clicks on feeds (#4462) --- src/components/Link.tsx | 14 ++------- src/platform/urls.tsx | 15 +++++++++- src/view/com/feeds/FeedSourceCard.tsx | 41 ++++++++++++++++++++------- 3 files changed, 48 insertions(+), 22 deletions(-) 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/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/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}> From 5dd195bcb748820eb293348501348d658021c658 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 10 Jun 2024 12:14:00 -0700 Subject: [PATCH 07/13] Check `canAskAgain` for notification permissions (#4460) --- src/lib/notifications/notifications.ts | 10 ++++++++-- src/lib/statsig/events.ts | 2 +- src/view/screens/Home.tsx | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) 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/view/screens/Home.tsx b/src/view/screens/Home.tsx index 34d3abf266..e49f2fbb21 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -20,6 +20,7 @@ import { } from '#/state/shell' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' +import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {FeedPage} from 'view/com/feeds/FeedPage' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' @@ -67,10 +68,15 @@ function HomeScreenReady({ const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed) const selectedIndex = Math.max(0, maybeFoundIndex) const selectedFeed = allFeeds[selectedIndex] + const requestNotificationsPermission = useRequestNotificationsPermission() useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) useOTAUpdates() + React.useEffect(() => { + requestNotificationsPermission('Home') + }, [requestNotificationsPermission]) + const pagerRef = React.useRef(null) const lastPagerReportedIndexRef = React.useRef(selectedIndex) React.useLayoutEffect(() => { From 7356763e4965f4bd4b7c5de91b78a19f97b259b3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 10 Jun 2024 15:00:45 -0500 Subject: [PATCH 08/13] Only validate persisted data for existing users (#4465) --- src/state/persisted/store.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 { From 0404111f031bdea33665c4e1df87da5e50a2ed5a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 10 Jun 2024 13:01:14 -0700 Subject: [PATCH 09/13] Fix: show known feed error UI more reliably (#4463) --- src/view/com/posts/FeedErrorMessage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 ( Date: Mon, 10 Jun 2024 13:02:00 -0700 Subject: [PATCH 10/13] Give a meaningful error in the composer when replying to a deleted post (#4464) --- src/view/com/composer/Composer.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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: { From 4efd576f6a5102e871784c047e108e808839e4b8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 10 Jun 2024 13:44:21 -0700 Subject: [PATCH 11/13] Visually improve the empty state of feeds (#4466) --- src/screens/Profile/Sections/Feed.tsx | 2 +- src/view/com/util/EmptyState.tsx | 55 ++++++++++++++++++--------- src/view/screens/ProfileFeed.tsx | 2 +- src/view/screens/ProfileList.tsx | 2 +- 4 files changed, 40 insertions(+), 21 deletions(-) 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/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index 150a16aaa3..42f0ab6d0a 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -5,10 +5,13 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' -import {Text} from './text/Text' -import {UserGroupIcon} from 'lib/icons' + +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {usePalette} from 'lib/hooks/usePalette' +import {UserGroupIcon} from 'lib/icons' import {isWeb} from 'platform/detection' +import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth' +import {Text} from './text/Text' export function EmptyState({ testID, @@ -17,32 +20,41 @@ export function EmptyState({ style, }: { testID?: string - icon: IconProp | 'user-group' + icon: IconProp | 'user-group' | 'growth' message: string style?: StyleProp }) { 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/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 ( From 86e81650d3e9dec1179f30a3ca321e6a34144591 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 10 Jun 2024 16:17:34 -0700 Subject: [PATCH 12/13] Add a loading indicator to notifications when loading latest (#4468) * Add a loading indicator to notifications when loading latest * Adjust size and alignment --- src/view/screens/Notifications.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 ( Date: Tue, 11 Jun 2024 00:43:04 +0100 Subject: [PATCH 13/13] add hitslop to repost button (#4469) --- src/view/com/util/post-ctrls/RepostButton.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 ? (