Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 216 lines 6.9 kB view raw
1import {useCallback, useState} from 'react' 2import {Pressable, TextInput, useWindowDimensions, View} from 'react-native' 3import { 4 useFocusedInputHandler, 5 useReanimatedKeyboardAnimation, 6} from 'react-native-keyboard-controller' 7import Animated, { 8 measure, 9 useAnimatedProps, 10 useAnimatedRef, 11 useAnimatedStyle, 12 useSharedValue, 13} from 'react-native-reanimated' 14import {useSafeAreaInsets} from 'react-native-safe-area-context' 15import {msg} from '@lingui/core/macro' 16import {useLingui} from '@lingui/react' 17import {countGraphemes} from 'unicode-segmenter/grapheme' 18 19import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20import {useHaptics} from '#/lib/haptics' 21import {useEmail} from '#/state/email-verification' 22import { 23 useMessageDraft, 24 useSaveMessageDraft, 25} from '#/state/messages/message-drafts' 26import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 27import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker' 28import {android, atoms as a, useTheme, utils} from '#/alf' 29import {useSharedInputStyles} from '#/components/forms/TextField' 30import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' 31import * as Toast from '#/components/Toast' 32import {IS_IOS, IS_WEB} from '#/env' 33import {useExtractEmbedFromFacets} from './MessageInputEmbed' 34 35const AnimatedTextInput = Animated.createAnimatedComponent(TextInput) 36 37export function MessageInput({ 38 onSendMessage, 39 hasEmbed, 40 setEmbed, 41 children, 42}: { 43 onSendMessage: (message: string) => void 44 hasEmbed: boolean 45 setEmbed: (embedUrl: string | undefined) => void 46 children?: React.ReactNode 47 openEmojiPicker?: (pos: EmojiPickerPosition) => void 48}) { 49 const {_} = useLingui() 50 const t = useTheme() 51 const playHaptic = useHaptics() 52 const {getDraft, clearDraft} = useMessageDraft() 53 54 // Input layout 55 const {top: topInset} = useSafeAreaInsets() 56 const {height: windowHeight} = useWindowDimensions() 57 const {height: keyboardHeight} = useReanimatedKeyboardAnimation() 58 const maxHeight = useSharedValue<undefined | number>(undefined) 59 const isInputScrollable = useSharedValue(false) 60 61 const inputStyles = useSharedInputStyles() 62 const [isFocused, setIsFocused] = useState(false) 63 const [message, setMessage] = useState(getDraft) 64 const inputRef = useAnimatedRef<TextInput>() 65 const [shouldEnforceClear, setShouldEnforceClear] = useState(false) 66 67 const {needsEmailVerification} = useEmail() 68 69 const enableSquareButtons = useEnableSquareButtons() 70 71 useSaveMessageDraft(message) 72 useExtractEmbedFromFacets(message, setEmbed) 73 74 const onSubmit = useCallback(() => { 75 if (needsEmailVerification) { 76 return 77 } 78 if (!hasEmbed && message.trim() === '') { 79 return 80 } 81 if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { 82 Toast.show(_(msg`Message is too long`), { 83 type: 'error', 84 }) 85 return 86 } 87 clearDraft() 88 onSendMessage(message) 89 playHaptic() 90 setEmbed(undefined) 91 setMessage('') 92 if (IS_IOS) { 93 setShouldEnforceClear(true) 94 } 95 if (IS_WEB) { 96 // Pressing the send button causes the text input to lose focus, so we need to 97 // re-focus it after sending 98 setTimeout(() => { 99 inputRef.current?.focus() 100 }, 100) 101 } 102 }, [ 103 needsEmailVerification, 104 hasEmbed, 105 message, 106 clearDraft, 107 onSendMessage, 108 playHaptic, 109 setEmbed, 110 inputRef, 111 _, 112 ]) 113 114 useFocusedInputHandler( 115 { 116 onChangeText: () => { 117 'worklet' 118 const measurement = measure(inputRef) 119 if (!measurement) return 120 121 const max = windowHeight - -keyboardHeight.get() - topInset - 150 122 const availableSpace = max - measurement.height 123 124 maxHeight.set(max) 125 isInputScrollable.set(availableSpace < 30) 126 }, 127 }, 128 [windowHeight, topInset], 129 ) 130 131 const animatedStyle = useAnimatedStyle(() => ({ 132 maxHeight: maxHeight.get(), 133 })) 134 135 const animatedProps = useAnimatedProps(() => ({ 136 scrollEnabled: isInputScrollable.get(), 137 })) 138 139 return ( 140 <View style={[a.px_md, a.pb_sm, a.pt_xs]}> 141 {children} 142 <View 143 style={[ 144 a.w_full, 145 a.flex_row, 146 t.atoms.bg_contrast_25, 147 { 148 padding: a.p_sm.padding - 2, 149 paddingLeft: a.p_md.padding - 2, 150 borderWidth: 1, 151 borderRadius: enableSquareButtons ? 11 : 23, 152 borderColor: 'transparent', 153 }, 154 isFocused && inputStyles.chromeFocus, 155 ]}> 156 <AnimatedTextInput 157 accessibilityLabel={_(msg`Message input field`)} 158 accessibilityHint={_(msg`Type your message here`)} 159 placeholder={_(msg`Write a message`)} 160 placeholderTextColor={t.palette.contrast_500} 161 value={message} 162 onChange={evt => { 163 // bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen 164 // including the button we just pressed - and this overrides clearing the input! so we watch for the 165 // next change and double make sure the input is cleared. It should *always* send an onChange event after 166 // clearing via setMessage('') that happens in onSubmit() 167 // -sfn 168 if (IS_IOS && shouldEnforceClear) { 169 setShouldEnforceClear(false) 170 setMessage('') 171 return 172 } 173 const text = evt.nativeEvent.text 174 setMessage(text) 175 }} 176 multiline={true} 177 style={[ 178 a.flex_1, 179 a.text_md, 180 a.px_sm, 181 t.atoms.text, 182 android({paddingTop: 0}), 183 {paddingBottom: IS_IOS ? 5 : 0}, 184 animatedStyle, 185 ]} 186 selectionColor={utils.alpha(t.palette.primary_500, 0.4)} 187 cursorColor={t.palette.primary_500} 188 selectionHandleColor={t.palette.primary_500} 189 keyboardAppearance={t.scheme} 190 submitBehavior="newline" 191 onFocus={() => setIsFocused(true)} 192 onBlur={() => setIsFocused(false)} 193 ref={inputRef} 194 hitSlop={HITSLOP_10} 195 animatedProps={animatedProps} 196 editable={!needsEmailVerification} 197 /> 198 <Pressable 199 accessibilityRole="button" 200 accessibilityLabel={_(msg`Send message`)} 201 accessibilityHint="" 202 hitSlop={HITSLOP_10} 203 style={[ 204 enableSquareButtons ? a.rounded_sm : a.rounded_full, 205 a.align_center, 206 a.justify_center, 207 {height: 30, width: 30, backgroundColor: t.palette.primary_500}, 208 ]} 209 onPress={onSubmit} 210 disabled={needsEmailVerification}> 211 <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} /> 212 </Pressable> 213 </View> 214 </View> 215 ) 216}