Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into update-korean-local…
Browse files Browse the repository at this point in the history
…ization
  • Loading branch information
quiple committed May 19, 2024
2 parents b321741 + 3ca671d commit 00dbdee
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 88 deletions.
3 changes: 2 additions & 1 deletion src/components/dms/MessagesListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useCallback} from 'react'
import {TouchableOpacity, View} from 'react-native'
import {Keyboard, TouchableOpacity, View} from 'react-native'
import {
AppBskyActorDefs,
ModerationCause,
Expand Down Expand Up @@ -46,6 +46,7 @@ export let MessagesListHeader = ({
if (isWeb) {
navigation.replace('Messages', {})
} else {
Keyboard.dismiss()
navigation.goBack()
}
}, [navigation])
Expand Down
30 changes: 26 additions & 4 deletions src/lib/hooks/useNotificationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ export function useNotificationsHandler() {
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()

// On Android, we cannot control which sound is used for a notification on Android
// 28 or higher. Instead, we have to configure a notification channel ahead of time
// which has the sounds we want in the configuration for that channel. These two
// channels allow for the mute/unmute functionality we want for the background
// handler.
React.useEffect(() => {
if (!isAndroid) return

Notifications.setNotificationChannelAsync('chat-messages', {
name: 'Chat',
importance: Notifications.AndroidImportance.MAX,
Expand Down Expand Up @@ -99,9 +103,27 @@ export function useNotificationsHandler() {
} else {
navigation.dispatch(state => {
if (state.routes[0].name === 'Messages') {
return CommonActions.navigate('MessagesConversation', {
conversation: payload.convoId,
})
if (
state.routes[state.routes.length - 1].name ===
'MessagesConversation'
) {
return CommonActions.reset({
...state,
routes: [
...state.routes.slice(0, state.routes.length - 1),
{
name: 'MessagesConversation',
params: {
conversation: payload.convoId,
},
},
],
})
} else {
return CommonActions.navigate('MessagesConversation', {
conversation: payload.convoId,
})
}
} else {
return CommonActions.navigate('MessagesTab', {
screen: 'Messages',
Expand Down
8 changes: 2 additions & 6 deletions src/screens/Messages/Conversation/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/ico

export function MessageInput({
onSendMessage,
scrollToEnd,
}: {
onSendMessage: (message: string) => void
scrollToEnd: () => void
}) {
const {_} = useLingui()
const t = useTheme()
Expand Down Expand Up @@ -75,14 +73,12 @@ export function MessageInput({

setMaxHeight(max)
setIsInputScrollable(availableSpace < 30)

scrollToEnd()
},
[scrollToEnd, topInset],
[topInset],
)

return (
<View style={[a.px_md, a.py_sm]}>
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
<View
style={[
a.w_full,
Expand Down
1 change: 0 additions & 1 deletion src/screens/Messages/Conversation/MessageInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function MessageInput({
onSendMessage,
}: {
onSendMessage: (message: string) => void
scrollToEnd: () => void
}) {
const {_} = useLingui()
const t = useTheme()
Expand Down
186 changes: 113 additions & 73 deletions src/screens/Messages/Conversation/MessagesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,14 @@ export function MessagesList({

// Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing
// onStartReached to fire.
const contentHeight = useSharedValue(0)
const prevContentHeight = useRef(0)
const prevItemCount = useRef(0)

// 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 keyboardIsAnimating = useSharedValue(false)
const isDragging = useSharedValue(false)
const layoutHeight = useSharedValue(0)

// -- Scroll handling

// 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
Expand All @@ -117,85 +116,78 @@ export function MessagesList({
// previous off whenever we add new content to the previous offset whenever we add new content to the list.
if (isWeb && isAtTop.value && hasScrolled) {
flatListRef.current?.scrollToOffset({
offset: height - contentHeight.value,
offset: height - prevContentHeight.current,
animated: false,
})
}

// This number _must_ be the height of the MaybeLoader component
if (height > 50 && isAtBottom.value && !keyboardIsAnimating.value) {
let newOffset = height
if (height > 50 && isAtBottom.value) {
// If the size of the content is changing by more than the height of the screen, then we should only
// scroll 1 screen down, and let the user scroll the rest. However, because a single message could be
// 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 (
hasScrolled &&
height - contentHeight.value > layoutHeight.value - 50 &&
height - prevContentHeight.current > layoutHeight.value - 50 &&
convoState.items.length - prevItemCount.current > 1
) {
newOffset = contentHeight.value - 50
flatListRef.current?.scrollToOffset({
offset: height - layoutHeight.value + 50,
animated: hasScrolled,
})
setShowNewMessagesPill(true)
} else if (!hasScrolled && !convoState.isFetchingHistory) {
setHasScrolled(true)
}
} else {
flatListRef.current?.scrollToOffset({
offset: height,
animated: hasScrolled,
})

flatListRef.current?.scrollToOffset({
offset: newOffset,
animated: hasScrolled,
})
isMomentumScrolling.value = true
// HACK Unfortunately, we need to call `setHasScrolled` after a brief delay,
// because otherwise there is too much of a delay between the time the content
// scrolls and the time the screen appears, causing a flicker.
// We cannot actually use a synchronous scroll here, because `onContentSizeChange`
// is actually async itself - all the info has to come across the bridge first.
if (!hasScrolled && !convoState.isFetchingHistory) {
setTimeout(() => {
setHasScrolled(true)
}, 100)
}
}
}
contentHeight.value = height

prevContentHeight.current = height
prevItemCount.current = convoState.items.length
},
[
hasScrolled,
convoState.items.length,
convoState.isFetchingHistory,
setHasScrolled,
// all of these are stable
contentHeight,
convoState.isFetchingHistory,
convoState.items.length,
// these are stable
flatListRef,
isAtBottom.value,
isAtTop.value,
isMomentumScrolling,
keyboardIsAnimating.value,
isAtBottom.value,
layoutHeight.value,
],
)

const onBeginDrag = React.useCallback(() => {
'worklet'
isDragging.value = true
}, [isDragging])

const onEndDrag = React.useCallback(() => {
'worklet'
isDragging.value = false
}, [isDragging])

const onStartReached = useCallback(() => {
if (hasScrolled) {
convoState.fetchMessageHistory()
}
}, [convoState, hasScrolled])

const onSendMessage = useCallback(
async (text: string) => {
let rt = new RichText({text}, {cleanNewlines: true})
await rt.detectFacets(getAgent())
rt = shortenLinks(rt)

// filter out any mention facets that didn't map to a user
rt.facets = rt.facets?.filter(facet => {
const mention = facet.features.find(feature =>
AppBskyRichtextFacet.isMention(feature),
)
if (mention && !mention.did) {
return false
}
return true
})

convoState.sendMessage({
text: rt.text,
facets: rt.facets,
})
},
[convoState, getAgent],
)

const onScroll = React.useCallback(
(e: ReanimatedScrollEvent) => {
'worklet'
Expand All @@ -218,22 +210,12 @@ export function MessagesList({
[layoutHeight, showNewMessagesPill, isAtBottom, isAtTop],
)

// This tells us when we are no longer scrolling
const onMomentumEnd = React.useCallback(() => {
'worklet'
isMomentumScrolling.value = false
}, [isMomentumScrolling])

const scrollToEndNow = React.useCallback(() => {
if (isMomentumScrolling.value) return
flatListRef.current?.scrollToEnd({animated: false})
}, [flatListRef, isMomentumScrolling.value])

// -- Keyboard animation handling
const animatedKeyboard = useAnimatedKeyboard()
const {bottom: bottomInset} = useSafeAreaInsets()
const nativeBottomBarHeight = isIOS ? 42 : 60
const bottomOffset = isWeb ? 0 : bottomInset + nativeBottomBarHeight
const finalKeyboardHeight = useSharedValue(0)

// On web, we don't want to do anything.
// On native, we want to scroll the list to the bottom every frame that the keyboard is opening. `scrollTo` runs
Expand All @@ -244,16 +226,24 @@ export function MessagesList({
'worklet'
// This never applies on web
if (isWeb) {
keyboardIsAnimating.value = false
return
}

// We only need to scroll to end while the keyboard is _opening_. During close, the position changes as we
// "expand" the view.
if (prev && now > prev) {
scrollTo(flatListRef, 0, contentHeight.value + now, false)
// We are setting some arbitrarily high number here to ensure that we end up scrolling to the bottom. There is not
// any other way to synchronously scroll to the bottom of the list, since we cannot get the content size of the
// scrollview synchronously.
// On iOS we could have used `dispatchCommand('scrollToEnd', [])` since the underlying view has a `scrollToEnd`
// method. It doesn't exist on Android though. That's probably why `scrollTo` which is implemented in Reanimated
// doesn't support a `scrollToEnd`.
if (prev && now > 0 && now >= prev) {
scrollTo(flatListRef, 0, 1e7, false)
}

// We want to store the full keyboard height after it fully opens so we can make some
// assumptions in onLayout
if (finalKeyboardHeight.value === 0 && prev && now > 0 && now === prev) {
finalKeyboardHeight.value = now
}
keyboardIsAnimating.value = Boolean(prev) && now !== prev
},
)

Expand All @@ -266,10 +256,62 @@ export function MessagesList({
: bottomOffset,
}))

// -- Message sending
const onSendMessage = useCallback(
async (text: string) => {
let rt = new RichText({text}, {cleanNewlines: true})
await rt.detectFacets(getAgent())
rt = shortenLinks(rt)

// filter out any mention facets that didn't map to a user
rt.facets = rt.facets?.filter(facet => {
const mention = facet.features.find(feature =>
AppBskyRichtextFacet.isMention(feature),
)
if (mention && !mention.did) {
return false
}
return true
})

convoState.sendMessage({
text: rt.text,
facets: rt.facets,
})
},
[convoState, getAgent],
)

// Any time the List layout changes, we want to scroll to the bottom. This only happens whenever
// the _lists_ size changes, _not_ the content size which is handled by `onContentSizeChange`.
// This accounts for things like the emoji keyboard opening, changes in block state, etc.
const onListLayout = React.useCallback(() => {
if (isDragging.value) return

const kh = animatedKeyboard.height.value
const fkh = finalKeyboardHeight.value

// We only run the layout scroll if:
// - We're on web
// - The keyboard is not open. This accounts for changing block states
// - The final keyboard height has been initially set and the keyboard height is greater than that
if (isWeb || kh === 0 || (fkh > 0 && kh >= fkh)) {
flatListRef.current?.scrollToEnd({animated: true})
}
}, [
flatListRef,
finalKeyboardHeight.value,
animatedKeyboard.height.value,
isDragging.value,
])

return (
<Animated.View style={[a.flex_1, animatedStyle]}>
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
<ScrollProvider
onScroll={onScroll}
onBeginDrag={onBeginDrag}
onEndDrag={onEndDrag}>
<List
ref={flatListRef}
data={convoState.items}
Expand All @@ -288,6 +330,7 @@ export function MessagesList({
removeClippedSubviews={false}
sideBorders={false}
onContentSizeChange={onContentSizeChange}
onLayout={onListLayout}
onStartReached={onStartReached}
onScrollToIndexFailed={onScrollToIndexFailed}
scrollEventThrottle={100}
Expand All @@ -301,10 +344,7 @@ export function MessagesList({
{convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled />
) : (
<MessageInput
onSendMessage={onSendMessage}
scrollToEnd={scrollToEndNow}
/>
<MessageInput onSendMessage={onSendMessage} />
)}
</>
) : (
Expand Down
2 changes: 2 additions & 0 deletions src/screens/Messages/Conversation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export function MessagesConversationScreen({route}: Props) {

if (isWeb && !gtMobile) {
setMinimalShellMode(true)
} else {
setMinimalShellMode(false)
}

return () => {
Expand Down
Loading

0 comments on commit 00dbdee

Please sign in to comment.