From 8c6384175c8343c0cdc14b4fbefd53127f3f1866 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 11 Oct 2024 08:35:53 -0700 Subject: [PATCH 01/14] Fix dropdown shift on web (#5710) --- bskyweb/templates/base.html | 4 ++++ web/index.html | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 9a50fae69e9..59cf39d02c4 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -75,6 +75,10 @@ top: 50%; transform: translateX(-50%) translateY(-50%) translateY(-50px); } + /* We need this style to prevent web dropdowns from shifting the display when opening */ + body { + width: 100%; + } {% include "scripts.html" %} diff --git a/web/index.html b/web/index.html index 2a406429c73..7b29597bb7d 100644 --- a/web/index.html +++ b/web/index.html @@ -80,6 +80,10 @@ top: 50%; transform: translateX(-50%) translateY(-50%) translateY(-50px); } + /* We need this style to prevent web dropdowns from shifting the display when opening */ + body { + width: 100%; + } From 7e5c522718108b26d6eddc46aba47a2e086a2fe3 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 11 Oct 2024 09:30:30 -0700 Subject: [PATCH 02/14] Move intent handler to a child of `InnerApp` (#5695) --- src/App.native.tsx | 2 -- src/lib/hooks/useIntentHandler.ts | 11 ++++++++++- src/view/shell/index.tsx | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/App.native.tsx b/src/App.native.tsx index 0b9f112eee1..668fb91fcdf 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -14,7 +14,6 @@ import * as SplashScreen from 'expo-splash-screen' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {QueryProvider} from '#/lib/react-query' import { initialize, @@ -85,7 +84,6 @@ function InnerApp() { const theme = useColorModeTheme() const {_} = useLingui() - useIntentHandler() const hasCheckedReferrer = useStarterPackEntry() // init diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 98ba4ec0261..a33aff23716 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -13,6 +13,9 @@ type IntentType = 'compose' | 'verify-email' const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/ +// This needs to stay outside of react to persist between account switches +let previousIntentUrl = '' + export function useIntentHandler() { const incomingUrl = Linking.useURL() const composeIntent = useComposeIntent() @@ -68,7 +71,13 @@ export function useIntentHandler() { } } - if (incomingUrl) handleIncomingURL(incomingUrl) + if (incomingUrl) { + if (previousIntentUrl === incomingUrl) { + return + } + handleIncomingURL(incomingUrl) + previousIntentUrl = incomingUrl + } }, [incomingUrl, composeIntent, verifyEmailIntent]) } diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 9f7569bebf2..79fc1a06942 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -14,6 +14,7 @@ import {StatusBar} from 'expo-status-bar' import {useNavigation, useNavigationState} from '@react-navigation/native' import {useDedupe} from '#/lib/hooks/useDedupe' +import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler' import {usePalette} from '#/lib/hooks/usePalette' import {useNotificationsRegistration} from '#/lib/notifications/notifications' @@ -129,6 +130,8 @@ export const Shell: React.FC = function ShellImpl() { const {fullyExpandedCount} = useDialogStateControlContext() const pal = usePalette('default') const theme = useTheme() + useIntentHandler() + React.useEffect(() => { if (isAndroid) { NavigationBar.setBackgroundColorAsync(theme.palette.default.background) From f7852d02bad388069240839bd3311f2c859571f8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 11 Oct 2024 14:41:12 -0700 Subject: [PATCH 03/14] Protect against zero-width chars in display name sanitation (see https://github.com/bluesky-social/social-app/pull/5703#issuecomment-2407459187) (#5729) --- src/lib/strings/display-names.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts index 23f3255129b..a95bfd6798c 100644 --- a/src/lib/strings/display-names.ts +++ b/src/lib/strings/display-names.ts @@ -7,7 +7,7 @@ import {ModerationUI} from '@atproto/api' const CHECK_MARKS_RE = /[\u2705\u2713\u2714\u2611]/gu const CONTROL_CHARS_RE = /[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g -const MULTIPLE_SPACES_RE = /[\s][\s]+/g +const MULTIPLE_SPACES_RE = /[\s][\s\u200B]+/g export function sanitizeDisplayName( str: string, From 157011efe3f8c3f1069eda93de6e0b1efcf0f9eb Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 11 Oct 2024 16:12:14 -0700 Subject: [PATCH 04/14] Fix performance of feed reordering and add layout animations (#5714) * Rework SavedFeeds editor to make changes transactionally * Fix hit slops * Add layout animations * Fix: dont let down go too far down * Speed up layout transitions --- src/view/screens/SavedFeeds.tsx | 203 ++++++++++++++++++-------------- 1 file changed, 114 insertions(+), 89 deletions(-) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 66bbd9b8a0c..2334abb5dbb 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -1,22 +1,23 @@ import React from 'react' import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' +import Animated, {LinearTransition} from 'react-native-reanimated' import {AppBskyActorDefs} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' +import {useNavigation} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useHaptics} from '#/lib/haptics' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {colors, s} from '#/lib/styles' import {logger} from '#/logger' import { useOverwriteSavedFeedsMutation, usePreferencesQuery, - useUpdateSavedFeedsMutation, } from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSetMinimalShellMode} from '#/state/shell' @@ -29,43 +30,40 @@ import {CenteredView, ScrollView} from '#/view/com/util/Views' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' - -const HITSLOP_TOP = { - top: 20, - left: 20, - bottom: 5, - right: 20, -} -const HITSLOP_BOTTOM = { - top: 5, - left: 20, - bottom: 20, - right: 20, -} +import {Loader} from '#/components/Loader' type Props = NativeStackScreenProps export function SavedFeeds({}: Props) { + const {data: preferences} = usePreferencesQuery() + if (!preferences) { + return + } + return +} + +function SavedFeedsInner({ + preferences, +}: { + preferences: UsePreferencesQueryResponse +}) { const pal = usePalette('default') const {_} = useLingui() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const {isMobile, isTabletOrDesktop, isDesktop} = useWebMediaQueries() const setMinimalShellMode = useSetMinimalShellMode() - const {data: preferences} = usePreferencesQuery() - const { - mutateAsync: overwriteSavedFeeds, - variables: optimisticSavedFeedsResponse, - reset: resetSaveFeedsMutationState, - error: savedFeedsError, - } = useOverwriteSavedFeedsMutation() + const {mutateAsync: overwriteSavedFeeds, isPending: isOverwritePending} = + useOverwriteSavedFeedsMutation() + const navigation = useNavigation() /* * Use optimistic data if exists and no error, otherwise fallback to remote * data */ - const currentFeeds = - optimisticSavedFeedsResponse && !savedFeedsError - ? optimisticSavedFeedsResponse - : preferences?.savedFeeds || [] + const [currentFeeds, setCurrentFeeds] = React.useState( + () => preferences.savedFeeds || [], + ) + const hasUnsavedChanges = currentFeeds !== preferences.savedFeeds const pinnedFeeds = currentFeeds.filter(f => f.pinned) const unpinnedFeeds = currentFeeds.filter(f => !f.pinned) const noSavedFeedsOfAnyType = pinnedFeeds.length + unpinnedFeeds.length === 0 @@ -78,6 +76,35 @@ export function SavedFeeds({}: Props) { }, [setMinimalShellMode]), ) + const onSaveChanges = React.useCallback(async () => { + try { + await overwriteSavedFeeds(currentFeeds) + Toast.show(_(msg`Feeds updated!`)) + navigation.navigate('Feeds') + } catch (e) { + Toast.show(_(msg`There was an issue contacting the server`), 'xmark') + logger.error('Failed to toggle pinned feed', {message: e}) + } + }, [_, overwriteSavedFeeds, currentFeeds, navigation]) + + const renderHeaderBtn = React.useCallback(() => { + return ( + + ) + }, [_, isDesktop, onSaveChanges, hasUnsavedChanges, isOverwritePending]) + return ( - + {noSavedFeedsOfAnyType && ( )) @@ -161,9 +192,8 @@ export function SavedFeeds({}: Props) { key={f.id} feed={f} isPinned={false} - overwriteSavedFeeds={overwriteSavedFeeds} - resetSaveFeedsMutationState={resetSaveFeedsMutationState} currentFeeds={currentFeeds} + setCurrentFeeds={setCurrentFeeds} preferences={preferences} /> )) @@ -197,44 +227,27 @@ function ListItem({ feed, isPinned, currentFeeds, - overwriteSavedFeeds, - resetSaveFeedsMutationState, + setCurrentFeeds, }: { feed: AppBskyActorDefs.SavedFeed isPinned: boolean currentFeeds: AppBskyActorDefs.SavedFeed[] - overwriteSavedFeeds: ReturnType< - typeof useOverwriteSavedFeedsMutation - >['mutateAsync'] - resetSaveFeedsMutationState: ReturnType< - typeof useOverwriteSavedFeedsMutation - >['reset'] + setCurrentFeeds: React.Dispatch preferences: UsePreferencesQueryResponse }) { - const pal = usePalette('default') const {_} = useLingui() + const pal = usePalette('default') const playHaptic = useHaptics() - const {isPending: isUpdatePending, mutateAsync: updateSavedFeeds} = - useUpdateSavedFeedsMutation() const feedUri = feed.value const onTogglePinned = React.useCallback(async () => { playHaptic() - - try { - resetSaveFeedsMutationState() - - await updateSavedFeeds([ - { - ...feed, - pinned: !feed.pinned, - }, - ]) - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to toggle pinned feed', {message: e}) - } - }, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState]) + setCurrentFeeds( + currentFeeds.map(f => + f.id === feed.id ? {...feed, pinned: !feed.pinned} : f, + ), + ) + }, [playHaptic, feed, currentFeeds, setCurrentFeeds]) const onPressUp = React.useCallback(async () => { if (!isPinned) return @@ -250,13 +263,8 @@ function ListItem({ nextFeeds[index], ] - try { - await overwriteSavedFeeds(nextFeeds) - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to set pinned feed order', {message: e}) - } - }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) + setCurrentFeeds(nextFeeds) + }, [feed, isPinned, setCurrentFeeds, currentFeeds]) const onPressDown = React.useCallback(async () => { if (!isPinned) return @@ -266,22 +274,25 @@ function ListItem({ const index = ids.indexOf(feed.id) const nextIndex = index + 1 - if (index === -1 || index >= nextFeeds.length - 1) return + if (index === -1 || index >= nextFeeds.filter(f => f.pinned).length - 1) + return ;[nextFeeds[index], nextFeeds[nextIndex]] = [ nextFeeds[nextIndex], nextFeeds[index], ] - try { - await overwriteSavedFeeds(nextFeeds) - } catch (e) { - Toast.show(_(msg`There was an issue contacting the server`), 'xmark') - logger.error('Failed to set pinned feed order', {message: e}) - } - }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) + setCurrentFeeds(nextFeeds) + }, [feed, isPinned, setCurrentFeeds, currentFeeds]) + + const onPressRemove = React.useCallback(async () => { + playHaptic() + setCurrentFeeds(currentFeeds.filter(f => f.id !== feed.id)) + }, [playHaptic, feed, currentFeeds, setCurrentFeeds]) return ( - + {feed.type === 'timeline' ? ( ) : ( @@ -290,25 +301,22 @@ function ListItem({ feedUri={feedUri} style={[isPinned && {paddingRight: 8}]} showMinimalPlaceholder - showSaveBtn={!isPinned} hideTopBorder={true} /> )} {isPinned ? ( <> ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, marginRight: 8, - opacity: - state.hovered || state.pressed || isUpdatePending ? 0.5 : 1, + opacity: state.hovered || state.pressed ? 0.5 : 1, })}> ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, marginRight: 8, - opacity: - state.hovered || state.pressed || isUpdatePending ? 0.5 : 1, + opacity: state.hovered || state.pressed ? 0.5 : 1, })}> - ) : null} + ) : ( + ({ + marginRight: 8, + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 4, + opacity: state.hovered || state.focused ? 0.5 : 1, + })}> + + + )} ({ backgroundColor: pal.viewLight.backgroundColor, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 4, - opacity: - state.hovered || state.focused || isUpdatePending ? 0.5 : 1, + opacity: state.hovered || state.focused ? 0.5 : 1, })}> - + ) } From e53b9729570d2b58756972f75a40ae4000d03ebd Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 11 Oct 2024 16:23:30 -0700 Subject: [PATCH 05/14] Make default search language 'all languages' (#5731) --- src/view/screens/Search/Search.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 97888eec56a..f4bbde5671f 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -436,19 +436,16 @@ function SearchLanguageDropdown({ } function useQueryManager({initialQuery}: {initialQuery: string}) { - const {contentLanguages} = useLanguagePrefs() const {query, params: initialParams} = React.useMemo(() => { return parseSearchQuery(initialQuery || '') }, [initialQuery]) const prevInitialQuery = React.useRef(initialQuery) - const [lang, setLang] = React.useState( - initialParams.lang || contentLanguages[0], - ) + const [lang, setLang] = React.useState(initialParams.lang || '') if (initialQuery !== prevInitialQuery.current) { // handle new queryParam change (from manual search entry) prevInitialQuery.current = initialQuery - setLang(initialParams.lang || contentLanguages[0]) + setLang(initialParams.lang || '') } const params = React.useMemo( From 907b8d17f5784d520d917712dea3acef591db514 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 11 Oct 2024 16:24:22 -0700 Subject: [PATCH 06/14] shift hitslop of avi follow button (#5730) * shift the hitslop of the follow button * increase slop --- src/view/com/posts/AviFollowButton.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/view/com/posts/AviFollowButton.tsx b/src/view/com/posts/AviFollowButton.tsx index 00428cbe61c..269d4eb5a9e 100644 --- a/src/view/com/posts/AviFollowButton.tsx +++ b/src/view/com/posts/AviFollowButton.tsx @@ -5,7 +5,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {createHitslop} from '#/lib/constants' import {NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {useProfileShadow} from '#/state/cache/profile-shadow' @@ -85,7 +84,12 @@ export function AviFollowButton({ {!isFollowing && (