interactive dismiss for ios
This commit is contained in:
@@ -29,6 +29,7 @@ import * as Toast from '#/view/com/util/Toast'
|
|||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {useSharedInputStyles} from '#/components/forms/TextField'
|
import {useSharedInputStyles} from '#/components/forms/TextField'
|
||||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
||||||
|
import {MessageInputBackground} from './MessageInputBackground'
|
||||||
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
|
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
|
||||||
|
|
||||||
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
|
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
|
||||||
@@ -127,7 +128,7 @@ export function MessageInput({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
|
<MessageInputBackground style={[a.px_md, a.py_xs]}>
|
||||||
{children}
|
{children}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -160,7 +161,7 @@ export function MessageInput({
|
|||||||
animatedStyle,
|
animatedStyle,
|
||||||
]}
|
]}
|
||||||
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||||
blurOnSubmit={false}
|
submitBehavior="submit"
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -184,6 +185,6 @@ export function MessageInput({
|
|||||||
<PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
|
<PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</MessageInputBackground>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {BlurView} from 'expo-blur'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||||
|
|
||||||
|
export function MessageInputBackground({
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<ViewStyleProp>) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<BlurView style={[a.relative, style]} tint={t.scheme} intensity={100}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.inset_0,
|
||||||
|
t.atoms.bg,
|
||||||
|
a.pointer_events_none,
|
||||||
|
{opacity: 0.8},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</BlurView>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
|
||||||
|
import {useTheme, ViewStyleProp} from '#/alf'
|
||||||
|
|
||||||
|
export function MessageInputBackground({
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<ViewStyleProp>) {
|
||||||
|
const t = useTheme()
|
||||||
|
return <View style={[t.atoms.bg, style]}>{children}</View>
|
||||||
|
}
|
||||||
@@ -4,15 +4,14 @@ import {useKeyboardHandler} from 'react-native-keyboard-controller'
|
|||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
scrollTo,
|
scrollTo,
|
||||||
|
useAnimatedProps,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
|
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
||||||
import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
|
import {AppBskyEmbedRecord, AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
|
|
||||||
import {clamp} from '#/lib/numbers'
|
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
|
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
|
||||||
import {
|
import {
|
||||||
@@ -20,20 +19,22 @@ import {
|
|||||||
isBskyPostUrl,
|
isBskyPostUrl,
|
||||||
} from '#/lib/strings/url-helpers'
|
} from '#/lib/strings/url-helpers'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isIOS, isNative} from '#/platform/detection'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {isConvoActive, useConvoActive} from '#/state/messages/convo'
|
import {isConvoActive, useConvoActive} from '#/state/messages/convo'
|
||||||
import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
|
import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
|
||||||
import {useGetPost} from '#/state/queries/post'
|
import {useGetPost} from '#/state/queries/post'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {
|
import {
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
EmojiPickerState,
|
EmojiPickerState,
|
||||||
} from '#/view/com/composer/text-input/web/EmojiPicker.web'
|
} from '#/view/com/composer/text-input/web/EmojiPicker.web'
|
||||||
import {List, ListMethods} from '#/view/com/util/List'
|
import {List, ListMethods, ListProps} from '#/view/com/util/List'
|
||||||
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
|
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
|
||||||
import {MessageInput} from '#/screens/Messages/components/MessageInput'
|
import {MessageInput} from '#/screens/Messages/components/MessageInput'
|
||||||
import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
|
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
|
||||||
import {MessageItem} from '#/components/dms/MessageItem'
|
import {MessageItem} from '#/components/dms/MessageItem'
|
||||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||||
@@ -90,6 +91,8 @@ export function MessagesList({
|
|||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const getPost = useGetPost()
|
const getPost = useGetPost()
|
||||||
const {embedUri, setEmbed} = useMessageEmbed()
|
const {embedUri, setEmbed} = useMessageEmbed()
|
||||||
|
const {footerHeight} = useShellLayout()
|
||||||
|
const messageInputHeight = useSharedValue(52)
|
||||||
|
|
||||||
const flatListRef = useAnimatedRef<ListMethods>()
|
const flatListRef = useAnimatedRef<ListMethods>()
|
||||||
|
|
||||||
@@ -237,9 +240,6 @@ export function MessagesList({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// -- Keyboard animation handling
|
// -- Keyboard animation handling
|
||||||
const {bottom: bottomInset} = useSafeAreaInsets()
|
|
||||||
const bottomOffset = isWeb ? 0 : clamp(60 + bottomInset, 60, 75)
|
|
||||||
|
|
||||||
const keyboardHeight = useSharedValue(0)
|
const keyboardHeight = useSharedValue(0)
|
||||||
const keyboardIsOpening = useSharedValue(false)
|
const keyboardIsOpening = useSharedValue(false)
|
||||||
|
|
||||||
@@ -263,28 +263,68 @@ export function MessagesList({
|
|||||||
onMove: e => {
|
onMove: e => {
|
||||||
'worklet'
|
'worklet'
|
||||||
keyboardHeight.set(e.height)
|
keyboardHeight.set(e.height)
|
||||||
if (e.height > bottomOffset) {
|
if (isIOS || e.height > footerHeight.get() + messageInputHeight.get()) {
|
||||||
scrollTo(flatListRef, 0, 1e7, false)
|
scrollTo(flatListRef, 0, 1e7, false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onInteractive: e => {
|
||||||
|
'worklet'
|
||||||
|
keyboardHeight.set(e.height)
|
||||||
|
},
|
||||||
onEnd: e => {
|
onEnd: e => {
|
||||||
'worklet'
|
'worklet'
|
||||||
keyboardHeight.set(e.height)
|
keyboardHeight.set(e.height)
|
||||||
if (e.height > bottomOffset) {
|
if (isIOS || e.height > footerHeight.get() + messageInputHeight.get()) {
|
||||||
scrollTo(flatListRef, 0, 1e7, false)
|
scrollTo(flatListRef, 0, 1e7, false)
|
||||||
}
|
}
|
||||||
keyboardIsOpening.set(false)
|
keyboardIsOpening.set(false)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[bottomOffset],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const animatedListStyle = useAnimatedStyle(() => ({
|
const animatedListStyle = useAnimatedStyle(() => {
|
||||||
marginBottom: Math.max(keyboardHeight.get(), bottomOffset),
|
if (!isIOS) {
|
||||||
}))
|
return {
|
||||||
|
marginBottom: Math.max(
|
||||||
|
keyboardHeight.get(),
|
||||||
|
footerHeight.get() + messageInputHeight.get(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const animatedProps = useAnimatedProps(() => {
|
||||||
|
if (isIOS) {
|
||||||
|
return {
|
||||||
|
scrollIndicatorInsets: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 1,
|
||||||
|
bottom:
|
||||||
|
Math.max(keyboardHeight.get(), footerHeight.get()) +
|
||||||
|
messageInputHeight.get(),
|
||||||
|
},
|
||||||
|
contentInset: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom:
|
||||||
|
Math.max(keyboardHeight.get(), footerHeight.get()) +
|
||||||
|
messageInputHeight.get(),
|
||||||
|
},
|
||||||
|
} satisfies Partial<ListProps>
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
const animatedStickyViewStyle = useAnimatedStyle(() => ({
|
const animatedStickyViewStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{translateY: -Math.max(keyboardHeight.get(), bottomOffset)}],
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: -Math.max(keyboardHeight.get(), footerHeight.get()),
|
||||||
|
},
|
||||||
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// -- Message sending
|
// -- Message sending
|
||||||
@@ -406,11 +446,12 @@ export function MessagesList({
|
|||||||
disableFullWindowScroll={true}
|
disableFullWindowScroll={true}
|
||||||
disableVirtualization={true}
|
disableVirtualization={true}
|
||||||
style={animatedListStyle}
|
style={animatedListStyle}
|
||||||
|
animatedProps={animatedProps}
|
||||||
// The extra two items account for the header and the footer components
|
// The extra two items account for the header and the footer components
|
||||||
initialNumToRender={isNative ? 32 : 62}
|
initialNumToRender={isNative ? 32 : 62}
|
||||||
maxToRenderPerBatch={isWeb ? 32 : 62}
|
maxToRenderPerBatch={isWeb ? 32 : 62}
|
||||||
keyboardDismissMode="on-drag"
|
keyboardDismissMode={isIOS ? 'interactive' : 'on-drag'}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps={isIOS ? 'always' : 'handled'}
|
||||||
maintainVisibleContentPosition={{
|
maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 0,
|
minIndexForVisible: 0,
|
||||||
}}
|
}}
|
||||||
@@ -424,9 +465,15 @@ export function MessagesList({
|
|||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||||
}
|
}
|
||||||
|
automaticallyAdjustContentInsets={false}
|
||||||
/>
|
/>
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
<Animated.View style={animatedStickyViewStyle}>
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{bottom: 0, left: 0, right: 0},
|
||||||
|
animatedStickyViewStyle,
|
||||||
|
]}>
|
||||||
{convoState.status === ConvoStatus.Disabled ? (
|
{convoState.status === ConvoStatus.Disabled ? (
|
||||||
<ChatDisabled />
|
<ChatDisabled />
|
||||||
) : blocked ? (
|
) : blocked ? (
|
||||||
@@ -436,13 +483,20 @@ export function MessagesList({
|
|||||||
{isConvoActive(convoState) &&
|
{isConvoActive(convoState) &&
|
||||||
!convoState.isFetchingHistory &&
|
!convoState.isFetchingHistory &&
|
||||||
convoState.items.length === 0 && <ChatEmptyPill />}
|
convoState.items.length === 0 && <ChatEmptyPill />}
|
||||||
<MessageInput
|
<View
|
||||||
onSendMessage={onSendMessage}
|
onLayout={evt =>
|
||||||
hasEmbed={!!embedUri}
|
messageInputHeight.set(evt.nativeEvent.layout.height)
|
||||||
setEmbed={setEmbed}
|
}>
|
||||||
openEmojiPicker={pos => setEmojiPickerState({isOpen: true, pos})}>
|
<MessageInput
|
||||||
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
|
onSendMessage={onSendMessage}
|
||||||
</MessageInput>
|
hasEmbed={!!embedUri}
|
||||||
|
setEmbed={setEmbed}
|
||||||
|
openEmojiPicker={pos =>
|
||||||
|
setEmojiPickerState({isOpen: true, pos})
|
||||||
|
}>
|
||||||
|
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
|
||||||
|
</MessageInput>
|
||||||
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
Reference in New Issue
Block a user