From dd4c8d8e4f5c5d84f11fe2412b3a2fa8f5d21157 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 17 May 2024 08:21:35 -0700 Subject: [PATCH 1/4] =?UTF-8?q?[=F0=9F=90=B4]=20Additional=20tweaks=20to?= =?UTF-8?q?=20the=20message=20list=20(#4075)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * more cleanup and little fixes another nit nit small annoyance add a comment only use `scrollTo` when necessary remove now unnecessary styles * move `setHasScrolled` to `onContentSizeChanged` * account for block footer --- .../Messages/Conversation/MessagesList.tsx | 109 +++++++----------- src/screens/Messages/Conversation/index.tsx | 33 +++--- 2 files changed, 57 insertions(+), 85 deletions(-) diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 182eb63ec9..d36fac8ae2 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -2,7 +2,6 @@ import React, {useCallback, useRef} from 'react' import {FlatList, View} from 'react-native' import Animated, { runOnJS, - runOnUI, scrollTo, useAnimatedKeyboard, useAnimatedReaction, @@ -24,7 +23,7 @@ import {isWeb} from 'platform/detection' import {List} from 'view/com/util/List' import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, useBreakpoints} from '#/alf' import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' @@ -64,8 +63,13 @@ function onScrollToIndexFailed() { // Placeholder function. You have to give FlatList something or else it will error. } -export function MessagesList() { - const t = useTheme() +export function MessagesList({ + hasScrolled, + setHasScrolled, +}: { + hasScrolled: boolean + setHasScrolled: React.Dispatch> +}) { const convo = useConvoActive() const {getAgent} = useAgent() const flatListRef = useAnimatedRef() @@ -88,22 +92,9 @@ export function MessagesList() { // We don't want to call `scrollToEnd` again if we are already scolling to the end, because this creates a bit of jank // Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not. const isMomentumScrolling = useSharedValue(false) - const hasInitiallyScrolled = useSharedValue(false) const keyboardIsAnimating = useSharedValue(false) const layoutHeight = useSharedValue(0) - // Since we are using the native web methods for scrolling on `List`, we only use the reanimated `scrollTo` on native - const scrollToOffset = React.useCallback( - (offset: number, animated: boolean) => { - if (isWeb) { - flatListRef.current?.scrollToOffset({offset, animated}) - } else { - runOnUI(scrollTo)(flatListRef, 0, offset, animated) - } - }, - [flatListRef], - ) - // Every time the content size changes, that means one of two things is happening: // 1. New messages are being added from the log or from a message you have sent // 2. Old messages are being prepended to the top @@ -118,8 +109,11 @@ export function MessagesList() { (_: number, height: number) => { // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the // previous off whenever we add new content to the previous offset whenever we add new content to the list. - if (isWeb && isAtTop.value && hasInitiallyScrolled.value) { - scrollToOffset(height - contentHeight.value, false) + if (isWeb && isAtTop.value && hasScrolled) { + flatListRef.current?.scrollToOffset({ + offset: height - contentHeight.value, + animated: false, + }) } // This number _must_ be the height of the MaybeLoader component @@ -130,40 +124,46 @@ export function MessagesList() { // really large - and the normal chat behavior would be to still scroll to the end if it's only one // message - we ignore this rule if there's only one additional message if ( - hasInitiallyScrolled.value && + hasScrolled && height - contentHeight.value > layoutHeight.value - 50 && convo.items.length - prevItemCount.current > 1 ) { newOffset = contentHeight.value - 50 setShowNewMessagesPill(true) + } else if (!hasScrolled && !convo.isFetchingHistory) { + setHasScrolled(true) } - scrollToOffset(newOffset, hasInitiallyScrolled.value) + + flatListRef.current?.scrollToOffset({ + offset: newOffset, + animated: hasScrolled, + }) isMomentumScrolling.value = true } contentHeight.value = height prevItemCount.current = convo.items.length }, [ - contentHeight, - scrollToOffset, - isMomentumScrolling, + hasScrolled, convo.items.length, - // All of these are stable + convo.isFetchingHistory, + setHasScrolled, + // all of these are stable + contentHeight, + flatListRef, isAtBottom.value, + isAtTop.value, + isMomentumScrolling, keyboardIsAnimating.value, layoutHeight.value, - hasInitiallyScrolled.value, - isAtTop.value, ], ) - // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached` - // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls. const onStartReached = useCallback(() => { - if (hasInitiallyScrolled.value) { + if (hasScrolled) { convo.fetchMessageHistory() } - }, [convo, hasInitiallyScrolled]) + }, [convo, hasScrolled]) const onSendMessage = useCallback( async (text: string) => { @@ -208,34 +208,20 @@ export function MessagesList() { // when a new message is added, hence the 100 pixel offset isAtBottom.value = e.contentSize.height - 100 < bottomOffset isAtTop.value = e.contentOffset.y <= 1 - - // This number _must_ be the height of the MaybeLoader component. - // We don't check for zero, because the `MaybeLoader` component is always present, even when not visible, which - // adds a 50 pixel offset. - if (contentHeight.value > 50 && !hasInitiallyScrolled.value) { - hasInitiallyScrolled.value = true - } }, - [ - layoutHeight, - showNewMessagesPill, - isAtBottom, - isAtTop, - hasInitiallyScrolled, - contentHeight.value, - ], + [layoutHeight, showNewMessagesPill, isAtBottom, isAtTop], ) + // This tells us when we are no longer scrolling const onMomentumEnd = React.useCallback(() => { 'worklet' isMomentumScrolling.value = false }, [isMomentumScrolling]) - const scrollToEnd = React.useCallback(() => { + const scrollToEndNow = React.useCallback(() => { if (isMomentumScrolling.value) return - scrollToOffset(contentHeight.value, true) - isMomentumScrolling.value = true - }, [contentHeight.value, isMomentumScrolling, scrollToOffset]) + flatListRef.current?.scrollToEnd({animated: false}) + }, [flatListRef, isMomentumScrolling.value]) // -- Keyboard animation handling const animatedKeyboard = useAnimatedKeyboard() @@ -269,24 +255,15 @@ export function MessagesList() { // This changes the size of the `ListFooterComponent`. Whenever this changes, the content size will change and our // `onContentSizeChange` function will handle scrolling to the appropriate offset. - const animatedFooterStyle = useAnimatedStyle(() => ({ + const animatedStyle = useAnimatedStyle(() => ({ marginBottom: animatedKeyboard.height.value > bottomOffset ? animatedKeyboard.height.value : bottomOffset, })) - // At a minimum we want the bottom to be whatever the height of our insets and bottom bar is. If the keyboard's height - // is greater than that however, we use that value. - const animatedInputStyle = useAnimatedStyle(() => ({ - bottom: - animatedKeyboard.height.value > bottomOffset - ? animatedKeyboard.height.value - : bottomOffset, - })) - return ( - <> + {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} } - ListFooterComponent={} /> + {showNewMessagesPill && } - - - - + ) } diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index 070175d478..3ab00bca12 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -71,23 +71,15 @@ function Inner() { const convoState = useConvo() const {_} = useLingui() - const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false) - - // HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have - // to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause - // a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent - // this, we will wait until the first render has completed to remove the loading overlay. - React.useEffect(() => { - if ( - !hasInitiallyRendered && - isConvoActive(convoState) && - !convoState.isFetchingHistory - ) { - setTimeout(() => { - setHasInitiallyRendered(true) - }, 15) - } - }, [convoState, hasInitiallyRendered]) + // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, + // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be + // empty. So, we also check for that possible state as well and render once we can. + const [hasScrolled, setHasScrolled] = React.useState(false) + const readyToShow = + hasScrolled || + (convoState.status === ConvoStatus.Ready && + !convoState.isFetchingHistory && + convoState.items.length === 0) if (convoState.status === ConvoStatus.Error) { return ( @@ -110,11 +102,14 @@ function Inner() {
{isConvoActive(convoState) ? ( - + ) : ( )} - {!hasInitiallyRendered && ( + {!readyToShow && ( Date: Fri, 17 May 2024 09:23:46 -0700 Subject: [PATCH 2/4] =?UTF-8?q?[=F0=9F=90=B4]=20Don't=20always=20show=20no?= =?UTF-8?q?tification=20for=20everything=20(#4083)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * don't always show a notification * nit --- src/lib/hooks/useNotificationHandler.ts | 2 +- src/lib/notifications/notifications.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index e288ac3ad4..3f1cd439d7 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -41,7 +41,7 @@ type NotificationPayload = } const DEFAULT_HANDLER_OPTIONS = { - shouldShowAlert: true, + shouldShowAlert: false, shouldPlaySound: false, shouldSetBadge: true, } diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 1182bfcbbe..6f28608f44 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -71,12 +71,14 @@ export function useNotificationsRegistration() { export function useRequestNotificationsPermission() { const gate = useGate() + const {currentAccount} = useSession() return React.useCallback( async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => { const permissions = await Notifications.getPermissionsAsync() if ( + !currentAccount || !isNative || permissions?.status === 'granted' || (permissions?.status === 'denied' && !permissions?.canAskAgain) @@ -107,7 +109,7 @@ export function useRequestNotificationsPermission() { getPushToken(true) } }, - [gate], + [gate, currentAccount], ) } From 115041f4bfa537579deb4b8aa92ddb200de4d48e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 May 2024 20:17:59 +0100 Subject: [PATCH 3/4] =?UTF-8?q?[=F0=9F=90=B4]=20Reduce=20header=20size=20(?= =?UTF-8?q?#4078)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * make text input container smaller * make header much smaller * improve web styling --- .../Messages/Conversation/MessageInput.tsx | 2 +- src/screens/Messages/Conversation/index.tsx | 85 +++++++++++-------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx index a6ec125b71..48c3aeb37c 100644 --- a/src/screens/Messages/Conversation/MessageInput.tsx +++ b/src/screens/Messages/Conversation/MessageInput.tsx @@ -82,7 +82,7 @@ export function MessageInput({ ) return ( - + - {!gtTablet ? ( + {!gtTablet && ( - ) : ( - )} {profile && moderationOpts ? ( ) : ( <> - + - - + + + + @@ -248,26 +251,34 @@ function HeaderReady({ return ( <> - - - + + + {displayName} {!isDeletedAccount && ( - + @{profile.handle} )} - + {isConvoActive(convoState) && ( Date: Fri, 17 May 2024 20:24:06 +0100 Subject: [PATCH 4/4] =?UTF-8?q?[=F0=9F=90=B4]=20NUX=20(#4062)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove type assertion * DMs NUX * delete button for testing * tweak styles and copy * rm log * style tweaks * reduce amount of words * Fix not showing on first load * Spacing tweaks --------- Co-authored-by: Eric Bailey --- src/components/dms/MessagesNUX.tsx | 172 ++++++++++++++++++ src/screens/Messages/List/index.tsx | 3 + src/screens/Messages/Settings.tsx | 4 +- .../queries/messages/actor-declaration.ts | 22 ++- src/view/screens/Settings/index.tsx | 13 ++ 5 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 src/components/dms/MessagesNUX.tsx diff --git a/src/components/dms/MessagesNUX.tsx b/src/components/dms/MessagesNUX.tsx new file mode 100644 index 0000000000..81d1cfff4e --- /dev/null +++ b/src/components/dms/MessagesNUX.tsx @@ -0,0 +1,172 @@ +import React, {useCallback, useEffect} from 'react' +import {View} from 'react-native' +import {ChatBskyActorDeclaration} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useTheme, web} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {Message_Stroke2_Corner0_Rounded} from '#/components/icons/Message' +import {Text} from '#/components/Typography' + +export function MessagesNUX() { + const control = Dialog.useDialogControl() + + const {currentAccount} = useSession() + const {data: profile} = useProfileQuery({ + did: currentAccount!.did, + }) + + useEffect(() => { + if (profile && typeof profile.associated?.chat === 'undefined') { + const timeout = setTimeout(() => { + control.open() + }, 1000) + + return () => { + clearTimeout(timeout) + } + } + }, [profile, control]) + + if (!profile) return null + + return ( + + + + + ) +} + +function DialogInner({ + chatDeclation, +}: { + chatDeclation?: ChatBskyActorDeclaration.Record +}) { + const control = Dialog.useDialogContext() + const {_} = useLingui() + const t = useTheme() + const {mutate: updateDeclaration} = useUpdateActorDeclaration({ + onError: () => { + Toast.show(_(msg`Failed to update settings`)) + }, + }) + + const onSelectItem = useCallback( + (keys: string[]) => { + const key = keys[0] + if (!key) return + updateDeclaration(key as 'all' | 'none' | 'following') + }, + [updateDeclaration], + ) + + useEffect(() => { + if (!chatDeclation) { + updateDeclaration('following') + } + }, [chatDeclation, updateDeclaration]) + + return ( + + + + + + Direct messages are here! + + + Privately chat with other users. + + + + + + Who can message you? + + + You can change this at any time. + + + + + + + + Everyone + + + + + + Users I follow + + + + + + No one + + + + + + + + + + + + ) +} diff --git a/src/screens/Messages/List/index.tsx b/src/screens/Messages/List/index.tsx index 060dac6301..e36d1edf2d 100644 --- a/src/screens/Messages/List/index.tsx +++ b/src/screens/Messages/List/index.tsx @@ -17,6 +17,7 @@ import {CenteredView} from '#/view/com/util/Views' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {DialogControlProps, useDialogControl} from '#/components/Dialog' +import {MessagesNUX} from '#/components/dms/MessagesNUX' import {NewChat} from '#/components/dms/NewChat' import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' @@ -131,6 +132,7 @@ export function MessagesScreen({navigation, route}: Props) { if (conversations.length < 1) { return ( + {gtMobile ? ( + {!gtMobile && ( + }) const {preferences, setPref} = useBackgroundNotificationPreferences() const {mutate: updateDeclaration} = useUpdateActorDeclaration({ diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index c8cc4acbdc..0886af3829 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -21,9 +21,9 @@ export function useUpdateActorDeclaration({ if (!currentAccount) throw new Error('Not logged in') // TODO(sam): remove validate: false once PDSes have the new lexicon const result = await getAgent().api.com.atproto.repo.putRecord({ + repo: currentAccount.did, collection: 'chat.bsky.actor.declaration', rkey: 'self', - repo: currentAccount.did, validate: false, record: { $type: 'chat.bsky.actor.declaration', @@ -62,3 +62,23 @@ export function useUpdateActorDeclaration({ }, }) } + +// for use in the settings screen for testing +export function useDeleteActorDeclaration() { + const {currentAccount} = useSession() + const {getAgent} = useAgent() + + return useMutation({ + mutationFn: async () => { + if (!currentAccount) throw new Error('Not logged in') + // TODO(sam): remove validate: false once PDSes have the new lexicon + const result = await getAgent().api.com.atproto.repo.deleteRecord({ + repo: currentAccount.did, + collection: 'chat.bsky.actor.declaration', + rkey: 'self', + validate: false, + }) + return result + }, + }) +} diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index c3864e5a91..b3b937c615 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -26,6 +26,7 @@ import { useInAppBrowser, useSetInAppBrowser, } from '#/state/preferences/in-app-browser' +import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useClearPreferencesMutation} from '#/state/queries/preferences' import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile' @@ -305,6 +306,8 @@ export function SettingsScreen({}: Props) { Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`)) }, [_]) + const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration() + return ( @@ -826,6 +829,16 @@ export function SettingsScreen({}: Props) { Reset preferences state + onPressDeleteChatDeclaration()} + accessibilityRole="button" + accessibilityLabel={_(msg`Delete chat declaration record`)} + accessibilityHint={_(msg`Deletes the chat declaration record`)}> + + Delete chat declaration record + +