diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index bca196c706..1526fb426a 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -10,7 +10,6 @@ export enum Features { ImportContactsSettingsDisable = 'import_contacts:settings:disable', LiveNowBetaDisable = 'live_now_beta:disable', GroupChatsDisable = 'group_chats:disable', - DmsNewMessageComposerEnable = 'dms:new_message_composer:enable', ComposerLanguageDetectionEnable = 'composer:language_detection:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable', NotificationsExpandedProfileCardEnable = 'notifications:expanded_profile_card:enable', diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index 6cdc756bb3..29e498285b 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -334,8 +334,7 @@ function SubmitButton({ ) } -// TODO: remove export when MessageInput is deleted -export function ComposerContainer({children}: {children: React.ReactNode}) { +function ComposerContainer({children}: {children: React.ReactNode}) { const {bottom: bottomInset} = useSafeAreaInsets() const {progress} = useReanimatedKeyboardAnimation() const t = useTheme() diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx deleted file mode 100644 index 441fd75fa4..0000000000 --- a/src/screens/Messages/components/MessageInput.tsx +++ /dev/null @@ -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, - ) => Promise | 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) - const isInputScrollable = useSharedValue(false) - - const [message, setMessage] = useState(getDraft) - const inputRef = useAnimatedRef() - 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 ( - - - - {children} - { - // 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} - /> - - - - {loading ? ( - - ) : ( - - )} - - - - - ) -} diff --git a/src/screens/Messages/components/MessageInput.web.tsx b/src/screens/Messages/components/MessageInput.web.tsx deleted file mode 100644 index 6142528f85..0000000000 --- a/src/screens/Messages/components/MessageInput.web.tsx +++ /dev/null @@ -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, - ) => 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(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) => { - // 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) => { - 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 ( - - {children} - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)}> - {loading ? null : ( - - - {({props, state}) => ( - - )} - - - - )} - 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} - /> - - - - - - ) -} diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 8fbfa88e04..d549ffb904 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -57,7 +57,6 @@ import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' import {useAgent, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' -import {MessageInput} from '#/screens/Messages/components/MessageInput' import {MessageListError} from '#/screens/Messages/components/MessageListError' import {atoms as a, platform, tokens, useTheme, web} from '#/alf' import {DateDivider} from '#/components/dms/DateDivider' @@ -704,9 +703,6 @@ export function MessagesList({ messageEmbed={messageEmbed} setEmbed={setEmbed} loading={loading} - useNewComposer={ax.features.enabled( - ax.features.DmsNewMessageComposerEnable, - )} /> )} @@ -735,7 +731,6 @@ function Composer({ messageEmbed, setEmbed, loading, - useNewComposer, }: { textInputId: string onSendMessage: ( @@ -746,7 +741,6 @@ function Composer({ messageEmbed: MessageEmbedState | undefined setEmbed: (embedUrl: string | undefined) => void loading?: boolean - useNewComposer: boolean }) { const handleSendMessage = useNonReactiveCallback( ( @@ -758,31 +752,16 @@ function Composer({ }, ) - const previews = ( - <> - - - - ) - - return useNewComposer ? ( + return ( - {previews} + + - ) : ( - - {previews} - ) }