Remove old message composer (#10951)
This commit is contained in:
@@ -10,7 +10,6 @@ export enum Features {
|
|||||||
ImportContactsSettingsDisable = 'import_contacts:settings:disable',
|
ImportContactsSettingsDisable = 'import_contacts:settings:disable',
|
||||||
LiveNowBetaDisable = 'live_now_beta:disable',
|
LiveNowBetaDisable = 'live_now_beta:disable',
|
||||||
GroupChatsDisable = 'group_chats:disable',
|
GroupChatsDisable = 'group_chats:disable',
|
||||||
DmsNewMessageComposerEnable = 'dms:new_message_composer:enable',
|
|
||||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||||
NotificationsExpandedProfileCardEnable = 'notifications:expanded_profile_card:enable',
|
NotificationsExpandedProfileCardEnable = 'notifications:expanded_profile_card:enable',
|
||||||
|
|||||||
@@ -334,8 +334,7 @@ function SubmitButton({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove export when MessageInput is deleted
|
function ComposerContainer({children}: {children: React.ReactNode}) {
|
||||||
export function ComposerContainer({children}: {children: React.ReactNode}) {
|
|
||||||
const {bottom: bottomInset} = useSafeAreaInsets()
|
const {bottom: bottomInset} = useSafeAreaInsets()
|
||||||
const {progress} = useReanimatedKeyboardAnimation()
|
const {progress} = useReanimatedKeyboardAnimation()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|||||||
@@ -1,288 +0,0 @@
|
|||||||
import {useCallback, useState} from 'react'
|
|
||||||
import {Pressable, TextInput, useWindowDimensions} from 'react-native'
|
|
||||||
import {
|
|
||||||
useFocusedInputHandler,
|
|
||||||
useKeyboardHandler,
|
|
||||||
useReanimatedKeyboardAnimation,
|
|
||||||
} from 'react-native-keyboard-controller'
|
|
||||||
import Animated, {
|
|
||||||
measure,
|
|
||||||
runOnJS,
|
|
||||||
useAnimatedProps,
|
|
||||||
useAnimatedRef,
|
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
||||||
import {GlassContainer} from 'expo-glass-effect'
|
|
||||||
import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
|
||||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
|
||||||
|
|
||||||
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
|
||||||
import {useEmail} from '#/state/email-verification'
|
|
||||||
import {
|
|
||||||
useMessageDraft,
|
|
||||||
useSaveMessageDraft,
|
|
||||||
} from '#/state/messages/message-drafts'
|
|
||||||
import {atoms as a, platform, tokens, useTheme} from '#/alf'
|
|
||||||
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
|
||||||
import {GlassView} from '#/components/GlassView'
|
|
||||||
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import * as Toast from '#/components/Toast'
|
|
||||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
|
||||||
import {ComposerContainer} from './MessageComposer'
|
|
||||||
import {
|
|
||||||
type MessageEmbedState,
|
|
||||||
useExtractEmbedFromFacets,
|
|
||||||
} from './MessageInputEmbed'
|
|
||||||
|
|
||||||
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
|
|
||||||
|
|
||||||
const MIN_HEIGHT = 40
|
|
||||||
|
|
||||||
export function MessageInput({
|
|
||||||
textInputId,
|
|
||||||
onSendMessage,
|
|
||||||
messageEmbed,
|
|
||||||
setEmbed,
|
|
||||||
children,
|
|
||||||
loading = false,
|
|
||||||
}: {
|
|
||||||
textInputId?: string
|
|
||||||
onSendMessage: (
|
|
||||||
message: string,
|
|
||||||
embed?: MessageEmbedState,
|
|
||||||
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
|
||||||
) => Promise<void> | void
|
|
||||||
messageEmbed: MessageEmbedState | undefined
|
|
||||||
setEmbed: (embedUrl: string | undefined) => void
|
|
||||||
children?: React.ReactNode
|
|
||||||
loading?: boolean
|
|
||||||
}) {
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
const playHaptic = useHaptics()
|
|
||||||
const {getDraft, clearDraft} = useMessageDraft()
|
|
||||||
const {replyTo, clearReply} = useMessageReplies()
|
|
||||||
|
|
||||||
// Input layout
|
|
||||||
const {top: topInset} = useSafeAreaInsets()
|
|
||||||
const {height: windowHeight} = useWindowDimensions()
|
|
||||||
const {height: keyboardHeight} = useReanimatedKeyboardAnimation()
|
|
||||||
const maxHeight = useSharedValue<undefined | number>(undefined)
|
|
||||||
const isInputScrollable = useSharedValue(false)
|
|
||||||
|
|
||||||
const [message, setMessage] = useState(getDraft)
|
|
||||||
const inputRef = useAnimatedRef<TextInput>()
|
|
||||||
const [shouldEnforceClear, setShouldEnforceClear] = useState(false)
|
|
||||||
|
|
||||||
const {needsEmailVerification} = useEmail()
|
|
||||||
const editable = !needsEmailVerification && !loading
|
|
||||||
|
|
||||||
useSaveMessageDraft(message)
|
|
||||||
useExtractEmbedFromFacets(message, setEmbed)
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
|
||||||
if (!editable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!messageEmbed && message.trim() === '') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
|
||||||
Toast.show(l`Message is too long`, {
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clearDraft()
|
|
||||||
playHaptic()
|
|
||||||
// Capture the embed before clearing - the deferred send below reads it.
|
|
||||||
const embed = messageEmbed
|
|
||||||
setEmbed(undefined)
|
|
||||||
setMessage('')
|
|
||||||
// Capture the reply before clearing - the deferred send below reads it.
|
|
||||||
const reply = replyTo
|
|
||||||
clearReply()
|
|
||||||
if (IS_IOS) {
|
|
||||||
setShouldEnforceClear(true)
|
|
||||||
}
|
|
||||||
if (IS_WEB) {
|
|
||||||
// Pressing the send button causes the text input to lose focus, so we need to
|
|
||||||
// re-focus it after sending
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.focus()
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
void onSendMessage(
|
|
||||||
message,
|
|
||||||
embed,
|
|
||||||
reply
|
|
||||||
? {...reply, $type: 'chat.bsky.convo.defs#messageView'}
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
editable,
|
|
||||||
messageEmbed,
|
|
||||||
message,
|
|
||||||
clearDraft,
|
|
||||||
onSendMessage,
|
|
||||||
playHaptic,
|
|
||||||
setEmbed,
|
|
||||||
inputRef,
|
|
||||||
l,
|
|
||||||
replyTo,
|
|
||||||
clearReply,
|
|
||||||
])
|
|
||||||
|
|
||||||
useFocusedInputHandler(
|
|
||||||
{
|
|
||||||
onChangeText: () => {
|
|
||||||
'worklet'
|
|
||||||
const measurement = measure(inputRef)
|
|
||||||
if (!measurement) return
|
|
||||||
|
|
||||||
const max = windowHeight - -keyboardHeight.get() - topInset - 150
|
|
||||||
const availableSpace = max - measurement.height
|
|
||||||
|
|
||||||
maxHeight.set(max)
|
|
||||||
isInputScrollable.set(availableSpace < 30)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[windowHeight, topInset],
|
|
||||||
)
|
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
|
||||||
maxHeight: maxHeight.get(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const animatedProps = useAnimatedProps(() => ({
|
|
||||||
scrollEnabled: isInputScrollable.get(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const submitDisabled =
|
|
||||||
!editable || (!messageEmbed && message.trim().length === 0)
|
|
||||||
|
|
||||||
const blur = useCallback(() => {
|
|
||||||
inputRef.current?.blur()
|
|
||||||
}, [inputRef])
|
|
||||||
|
|
||||||
useKeyboardHandler({
|
|
||||||
onEnd: evt => {
|
|
||||||
'worklet'
|
|
||||||
// small hack: interactive dismiss on Android sometimes doesn't blur the input
|
|
||||||
if (IS_ANDROID && evt.progress === 0) {
|
|
||||||
runOnJS(blur)()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComposerContainer>
|
|
||||||
<GlassContainer
|
|
||||||
style={[a.flex_row, a.align_end, a.gap_sm]}
|
|
||||||
spacing={tokens.space.xs}>
|
|
||||||
<GlassView
|
|
||||||
isInteractive
|
|
||||||
glassEffectStyle="regular"
|
|
||||||
style={[a.flex_1, a.rounded_xl, {minHeight: MIN_HEIGHT}]}
|
|
||||||
tintColor={t.palette.contrast_50}
|
|
||||||
fallbackStyle={[t.atoms.bg_contrast_50]}>
|
|
||||||
{children}
|
|
||||||
<AnimatedTextInput
|
|
||||||
nativeID={textInputId}
|
|
||||||
accessibilityLabel={l`Message input field`}
|
|
||||||
accessibilityHint={l`Type your message here`}
|
|
||||||
placeholder={l`Message`}
|
|
||||||
placeholderTextColor={t.palette.contrast_500}
|
|
||||||
value={message}
|
|
||||||
onChange={evt => {
|
|
||||||
// bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen
|
|
||||||
// including the button we just pressed - and this overrides clearing the input! so we watch for the
|
|
||||||
// next change and double make sure the input is cleared. It should *always* send an onChange event after
|
|
||||||
// clearing via setMessage('') that happens in onSubmit()
|
|
||||||
// -sfn
|
|
||||||
if (IS_IOS && shouldEnforceClear) {
|
|
||||||
setShouldEnforceClear(false)
|
|
||||||
setMessage('')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const text = evt.nativeEvent.text
|
|
||||||
setMessage(text)
|
|
||||||
}}
|
|
||||||
multiline={true}
|
|
||||||
style={[
|
|
||||||
{flexBasis: 'auto', minHeight: MIN_HEIGHT},
|
|
||||||
a.flex_shrink_0,
|
|
||||||
a.flex_grow,
|
|
||||||
a.text_md,
|
|
||||||
a.px_lg,
|
|
||||||
t.atoms.text,
|
|
||||||
platform({
|
|
||||||
android: {paddingTop: 2, paddingBottom: 3},
|
|
||||||
ios: {paddingTop: 10, paddingBottom: 5},
|
|
||||||
}),
|
|
||||||
animatedStyle,
|
|
||||||
]}
|
|
||||||
verticalAlign="middle"
|
|
||||||
keyboardAppearance={t.scheme}
|
|
||||||
submitBehavior="newline"
|
|
||||||
ref={inputRef}
|
|
||||||
hitSlop={HITSLOP_10}
|
|
||||||
animatedProps={animatedProps}
|
|
||||||
editable={editable}
|
|
||||||
/>
|
|
||||||
</GlassView>
|
|
||||||
<GlassView
|
|
||||||
isInteractive
|
|
||||||
glassEffectStyle="regular"
|
|
||||||
style={[a.rounded_full]}
|
|
||||||
tintColor={
|
|
||||||
submitDisabled ? t.palette.contrast_100 : t.palette.primary_500
|
|
||||||
}
|
|
||||||
fallbackStyle={{
|
|
||||||
backgroundColor: submitDisabled
|
|
||||||
? t.palette.contrast_100
|
|
||||||
: t.palette.primary_500,
|
|
||||||
}}>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={
|
|
||||||
loading
|
|
||||||
? l({message: 'Loading chat…', context: 'placeholder'})
|
|
||||||
: l({message: 'Message', context: 'action'})
|
|
||||||
}
|
|
||||||
accessibilityHint=""
|
|
||||||
hitSlop={HITSLOP_10}
|
|
||||||
style={[
|
|
||||||
a.rounded_full,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
{
|
|
||||||
height: MIN_HEIGHT,
|
|
||||||
width: MIN_HEIGHT,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPress={onSubmit}
|
|
||||||
disabled={submitDisabled}>
|
|
||||||
{loading ? (
|
|
||||||
<Loader size="md" fill={t.palette.white} style={[a.mb_2xs]} />
|
|
||||||
) : (
|
|
||||||
<PaperPlaneIcon
|
|
||||||
size="md"
|
|
||||||
fill={t.palette.white}
|
|
||||||
style={[a.mb_2xs]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</GlassView>
|
|
||||||
</GlassContainer>
|
|
||||||
</ComposerContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
import {useCallback, useRef, useState} from 'react'
|
|
||||||
import {Pressable, View} from 'react-native'
|
|
||||||
import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
|
||||||
import {flushSync} from 'react-dom'
|
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
|
||||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
|
||||||
|
|
||||||
import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
|
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|
||||||
import {
|
|
||||||
useMessageDraft,
|
|
||||||
useSaveMessageDraft,
|
|
||||||
} from '#/state/messages/message-drafts'
|
|
||||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
|
||||||
import {Button} from '#/components/Button'
|
|
||||||
import {useMessageReplies} from '#/components/dms/MessageReplies'
|
|
||||||
import * as EmojiPicker from '#/components/EmojiPicker'
|
|
||||||
import {useSharedInputStyles} from '#/components/forms/TextField'
|
|
||||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
|
||||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
|
||||||
import * as Toast from '#/components/Toast'
|
|
||||||
import {IS_WEB_SAFARI, IS_WEB_TOUCH_DEVICE} from '#/env'
|
|
||||||
import {
|
|
||||||
type MessageEmbedState,
|
|
||||||
useExtractEmbedFromFacets,
|
|
||||||
} from './MessageInputEmbed'
|
|
||||||
|
|
||||||
export function MessageInput({
|
|
||||||
onSendMessage,
|
|
||||||
messageEmbed,
|
|
||||||
setEmbed,
|
|
||||||
children,
|
|
||||||
loading = false,
|
|
||||||
}: {
|
|
||||||
onSendMessage: (
|
|
||||||
message: string,
|
|
||||||
embed?: MessageEmbedState,
|
|
||||||
replyTo?: $Typed<ChatBskyConvoDefs.MessageView>,
|
|
||||||
) => void
|
|
||||||
messageEmbed: MessageEmbedState | undefined
|
|
||||||
setEmbed: (embedUrl: string | undefined) => void
|
|
||||||
children?: React.ReactNode
|
|
||||||
loading?: boolean
|
|
||||||
}) {
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const {t: l} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
const {getDraft, clearDraft} = useMessageDraft()
|
|
||||||
const {replyTo, clearReply} = useMessageReplies()
|
|
||||||
const [message, setMessage] = useState(getDraft)
|
|
||||||
|
|
||||||
const inputStyles = useSharedInputStyles()
|
|
||||||
const isComposing = useRef(false)
|
|
||||||
const [isFocused, setIsFocused] = useState(false)
|
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
|
||||||
const [textAreaHeight, setTextAreaHeight] = useState(38)
|
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
|
||||||
if (!messageEmbed && message.trim() === '') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
|
||||||
Toast.show(l`Message is too long`, {
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clearDraft()
|
|
||||||
onSendMessage(
|
|
||||||
message,
|
|
||||||
messageEmbed,
|
|
||||||
replyTo
|
|
||||||
? {...replyTo, $type: 'chat.bsky.convo.defs#messageView'}
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
clearReply()
|
|
||||||
setMessage('')
|
|
||||||
setEmbed(undefined)
|
|
||||||
}, [
|
|
||||||
message,
|
|
||||||
onSendMessage,
|
|
||||||
l,
|
|
||||||
clearDraft,
|
|
||||||
messageEmbed,
|
|
||||||
setEmbed,
|
|
||||||
replyTo,
|
|
||||||
clearReply,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
// Don't submit the form when the Japanese or any other IME is composing
|
|
||||||
if (isComposing.current) return
|
|
||||||
|
|
||||||
// see https://github.com/bluesky-social/social-app/issues/4178
|
|
||||||
// see https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/
|
|
||||||
// see https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
|
|
||||||
//
|
|
||||||
// On Safari, the final keydown event to dismiss the IME - which is the enter key - is also "Enter" below.
|
|
||||||
// Obviously, this causes problems because the final dismissal should _not_ submit the text, but should just
|
|
||||||
// stop the IME editing. This is the behavior of Chrome and Firefox, but not Safari.
|
|
||||||
//
|
|
||||||
// Keycode is deprecated, however the alternative seems to only be to compare the timestamp from the
|
|
||||||
// onCompositionEnd event to the timestamp of the keydown event, which is not reliable. For example, this hack
|
|
||||||
// uses that method: https://github.com/ProseMirror/prosemirror-view/pull/44. However, from my 500ms resulted in
|
|
||||||
// far too long of a delay, and a subsequent enter press would often just end up doing nothing. A shorter time
|
|
||||||
// frame was also not great, since it was too short to be reliable (i.e. an older system might have a larger
|
|
||||||
// time gap between the two events firing.
|
|
||||||
if (IS_WEB_SAFARI && e.key === 'Enter' && e.keyCode === 229) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
if (e.shiftKey) return
|
|
||||||
e.preventDefault()
|
|
||||||
onSubmit()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onSubmit],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setMessage(e.target.value)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onEmojiInserted = useCallback(
|
|
||||||
(emoji: EmojiPicker.Emoji) => {
|
|
||||||
if (!textAreaRef.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const position = textAreaRef.current.selectionStart ?? 0
|
|
||||||
flushSync(() => {
|
|
||||||
setMessage(
|
|
||||||
message =>
|
|
||||||
message.slice(0, position) + emoji.native + message.slice(position),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
textAreaRef.current.selectionStart = position + emoji.native.length
|
|
||||||
textAreaRef.current.selectionEnd = position + emoji.native.length
|
|
||||||
},
|
|
||||||
[setMessage],
|
|
||||||
)
|
|
||||||
|
|
||||||
useSaveMessageDraft(message)
|
|
||||||
useExtractEmbedFromFacets(message, setEmbed)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={a.p_sm}>
|
|
||||||
{children}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{
|
|
||||||
paddingRight: a.p_sm.padding - 2,
|
|
||||||
paddingLeft: a.p_sm.padding - 2,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 23,
|
|
||||||
borderColor: 'transparent',
|
|
||||||
height: textAreaHeight + 23,
|
|
||||||
},
|
|
||||||
isHovered && inputStyles.chromeHover,
|
|
||||||
isFocused && inputStyles.chromeFocus,
|
|
||||||
]}
|
|
||||||
// @ts-expect-error web only
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}>
|
|
||||||
{loading ? null : (
|
|
||||||
<EmojiPicker.Root
|
|
||||||
onEmojiSelect={onEmojiInserted}
|
|
||||||
nextFocusRef={textAreaRef}>
|
|
||||||
<EmojiPicker.Trigger label={l`Open emoji picker`}>
|
|
||||||
{({props, state}) => (
|
|
||||||
<Button
|
|
||||||
style={[
|
|
||||||
a.rounded_full,
|
|
||||||
a.overflow_hidden,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
{
|
|
||||||
marginTop: 5,
|
|
||||||
height: 30,
|
|
||||||
width: 30,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
label={props.accessibilityLabel}
|
|
||||||
{...props}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
a.inset_0,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
{
|
|
||||||
backgroundColor:
|
|
||||||
state.hovered || state.focused || state.pressed
|
|
||||||
? t.atoms.bg.backgroundColor
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<EmojiSmile size="lg" />
|
|
||||||
</View>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</EmojiPicker.Trigger>
|
|
||||||
<EmojiPicker.Picker />
|
|
||||||
</EmojiPicker.Root>
|
|
||||||
)}
|
|
||||||
<TextareaAutosize
|
|
||||||
ref={textAreaRef}
|
|
||||||
disabled={loading}
|
|
||||||
style={flatten([
|
|
||||||
a.flex_1,
|
|
||||||
a.px_sm,
|
|
||||||
a.border_0,
|
|
||||||
t.atoms.text,
|
|
||||||
{
|
|
||||||
paddingTop: 10,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
resize: 'none',
|
|
||||||
},
|
|
||||||
])}
|
|
||||||
maxRows={12}
|
|
||||||
placeholder={
|
|
||||||
loading
|
|
||||||
? l({message: 'Loading chat…', context: 'placeholder'})
|
|
||||||
: l({message: 'Message', context: 'action'})
|
|
||||||
}
|
|
||||||
defaultValue=""
|
|
||||||
value={message}
|
|
||||||
dirName="ltr"
|
|
||||||
autoFocus={true}
|
|
||||||
onFocus={() => setIsFocused(true)}
|
|
||||||
onBlur={() => setIsFocused(false)}
|
|
||||||
onCompositionStart={() => {
|
|
||||||
isComposing.current = true
|
|
||||||
}}
|
|
||||||
onCompositionEnd={() => {
|
|
||||||
isComposing.current = false
|
|
||||||
}}
|
|
||||||
onHeightChange={height => setTextAreaHeight(height)}
|
|
||||||
onChange={onChange}
|
|
||||||
// On mobile web phones, we want to keep the same behavior as the native app. Do not submit the message
|
|
||||||
// in these cases.
|
|
||||||
onKeyDown={IS_WEB_TOUCH_DEVICE && isMobile ? undefined : onKeyDown}
|
|
||||||
/>
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={l`Send message`}
|
|
||||||
accessibilityHint=""
|
|
||||||
disabled={loading}
|
|
||||||
style={[
|
|
||||||
a.rounded_full,
|
|
||||||
a.align_center,
|
|
||||||
a.justify_center,
|
|
||||||
{
|
|
||||||
height: 30,
|
|
||||||
width: 30,
|
|
||||||
marginTop: 5,
|
|
||||||
backgroundColor: t.palette.primary_500,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPress={onSubmit}>
|
|
||||||
<PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -57,7 +57,6 @@ import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util'
|
|||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {List, type ListMethods} from '#/view/com/util/List'
|
import {List, type ListMethods} from '#/view/com/util/List'
|
||||||
import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
|
import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
|
||||||
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, platform, tokens, useTheme, web} from '#/alf'
|
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
||||||
import {DateDivider} from '#/components/dms/DateDivider'
|
import {DateDivider} from '#/components/dms/DateDivider'
|
||||||
@@ -704,9 +703,6 @@ export function MessagesList({
|
|||||||
messageEmbed={messageEmbed}
|
messageEmbed={messageEmbed}
|
||||||
setEmbed={setEmbed}
|
setEmbed={setEmbed}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
useNewComposer={ax.features.enabled(
|
|
||||||
ax.features.DmsNewMessageComposerEnable,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ConversationFooter>
|
</ConversationFooter>
|
||||||
@@ -735,7 +731,6 @@ function Composer({
|
|||||||
messageEmbed,
|
messageEmbed,
|
||||||
setEmbed,
|
setEmbed,
|
||||||
loading,
|
loading,
|
||||||
useNewComposer,
|
|
||||||
}: {
|
}: {
|
||||||
textInputId: string
|
textInputId: string
|
||||||
onSendMessage: (
|
onSendMessage: (
|
||||||
@@ -746,7 +741,6 @@ function Composer({
|
|||||||
messageEmbed: MessageEmbedState | undefined
|
messageEmbed: MessageEmbedState | undefined
|
||||||
setEmbed: (embedUrl: string | undefined) => void
|
setEmbed: (embedUrl: string | undefined) => void
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
useNewComposer: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const handleSendMessage = useNonReactiveCallback(
|
const handleSendMessage = useNonReactiveCallback(
|
||||||
(
|
(
|
||||||
@@ -758,31 +752,16 @@ function Composer({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const previews = (
|
return (
|
||||||
<>
|
|
||||||
<MessageInputReply />
|
|
||||||
<MessageInputEmbed embed={messageEmbed} setEmbed={setEmbed} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return useNewComposer ? (
|
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
textInputId={textInputId}
|
textInputId={textInputId}
|
||||||
onSendMessage={handleSendMessage}
|
onSendMessage={handleSendMessage}
|
||||||
messageEmbed={messageEmbed}
|
messageEmbed={messageEmbed}
|
||||||
setEmbed={setEmbed}
|
setEmbed={setEmbed}
|
||||||
loading={loading}>
|
loading={loading}>
|
||||||
{previews}
|
<MessageInputReply />
|
||||||
|
<MessageInputEmbed embed={messageEmbed} setEmbed={setEmbed} />
|
||||||
</MessageComposer>
|
</MessageComposer>
|
||||||
) : (
|
|
||||||
<MessageInput
|
|
||||||
textInputId={textInputId}
|
|
||||||
onSendMessage={handleSendMessage}
|
|
||||||
messageEmbed={messageEmbed}
|
|
||||||
setEmbed={setEmbed}
|
|
||||||
loading={loading}>
|
|
||||||
{previews}
|
|
||||||
</MessageInput>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user