Bluesky app fork with some witchin' additions 💫

Close web mention suggestions popup on `Escape` (#8605)

* alf web typeahead

* fix type error

* fix escape behaviour

* change selection on hover

* rm React.

* undo random change

authored by samuel.fm and committed by

GitHub 8a439860 27c10585

+246 -266
+5 -5
src/view/com/composer/Composer.tsx
··· 114 114 import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage' 115 115 // TODO: Prevent naming components that coincide with RN primitives 116 116 // due to linting false positives 117 - import { 118 - TextInput, 119 - type TextInputRef, 120 - } from '#/view/com/composer/text-input/TextInput' 117 + import {TextInput} from '#/view/com/composer/text-input/TextInput' 121 118 import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn' 122 119 import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog' 123 120 import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' ··· 155 152 processVideo, 156 153 type VideoState, 157 154 } from './state/video' 155 + import {type TextInputRef} from './text-input/TextInput.types' 158 156 import {getVideoMetadata} from './videos/pickVideo' 159 157 import {clearThumbnailCache} from './videos/VideoTranscodeBackdrop' 160 158 ··· 306 304 ) 307 305 308 306 const onPressCancel = useCallback(() => { 309 - if ( 307 + if (textInput.current?.maybeClosePopup()) { 308 + return 309 + } else if ( 310 310 thread.posts.some( 311 311 post => 312 312 post.shortenedGraphemeLength > 0 ||
+18 -39
src/view/com/composer/text-input/TextInput.tsx
··· 1 - import React, { 2 - type ComponentProps, 3 - forwardRef, 1 + import { 4 2 useCallback, 3 + useImperativeHandle, 5 4 useMemo, 6 5 useRef, 7 6 useState, ··· 9 8 import { 10 9 type NativeSyntheticEvent, 11 10 Text as RNText, 12 - type TextInput as RNTextInput, 13 11 type TextInputSelectionChangeEventData, 14 12 View, 15 13 } from 'react-native' ··· 33 31 import {atoms as a, useAlf} from '#/alf' 34 32 import {normalizeTextStyles} from '#/alf/typography' 35 33 import {Autocomplete} from './mobile/Autocomplete' 36 - 37 - export interface TextInputRef { 38 - focus: () => void 39 - blur: () => void 40 - getCursorPosition: () => DOMRect | undefined 41 - } 42 - 43 - interface TextInputProps extends ComponentProps<typeof RNTextInput> { 44 - richtext: RichText 45 - placeholder: string 46 - webForceMinHeight: boolean 47 - hasRightPadding: boolean 48 - isActive: boolean 49 - setRichText: (v: RichText) => void 50 - onPhotoPasted: (uri: string) => void 51 - onPressPublish: (richtext: RichText) => void 52 - onNewLink: (uri: string) => void 53 - onError: (err: string) => void 54 - } 34 + import {type TextInputProps} from './TextInput.types' 55 35 56 36 interface Selection { 57 37 start: number 58 38 end: number 59 39 } 60 40 61 - export const TextInput = forwardRef(function TextInputImpl( 62 - { 63 - richtext, 64 - placeholder, 65 - hasRightPadding, 66 - setRichText, 67 - onPhotoPasted, 68 - onNewLink, 69 - onError, 70 - ...props 71 - }: TextInputProps, 41 + export function TextInput({ 72 42 ref, 73 - ) { 43 + richtext, 44 + placeholder, 45 + hasRightPadding, 46 + setRichText, 47 + onPhotoPasted, 48 + onNewLink, 49 + onError, 50 + ...props 51 + }: TextInputProps) { 74 52 const {theme: t, fonts} = useAlf() 75 53 const textInput = useRef<PasteInputRef>(null) 76 54 const textInputSelection = useRef<Selection>({start: 0, end: 0}) 77 55 const theme = useTheme() 78 56 const [autocompletePrefix, setAutocompletePrefix] = useState('') 79 - const prevLength = React.useRef(richtext.length) 57 + const prevLength = useRef(richtext.length) 80 58 81 - React.useImperativeHandle(ref, () => ({ 59 + useImperativeHandle(ref, () => ({ 82 60 focus: () => textInput.current?.focus(), 83 61 blur: () => { 84 62 textInput.current?.blur() 85 63 }, 86 64 getCursorPosition: () => undefined, // Not implemented on native 65 + maybeClosePopup: () => false, // Not needed on native 87 66 })) 88 67 89 68 const pastSuggestedUris = useRef(new Set<string>()) ··· 185 164 [onChangeText, richtext, setAutocompletePrefix], 186 165 ) 187 166 188 - const inputTextStyle = React.useMemo(() => { 167 + const inputTextStyle = useMemo(() => { 189 168 const style = normalizeTextStyles( 190 169 [a.text_lg, a.leading_snug, t.atoms.text], 191 170 { ··· 277 256 /> 278 257 </View> 279 258 ) 280 - }) 259 + }
+42
src/view/com/composer/text-input/TextInput.types.ts
··· 1 + import {type TextInput} from 'react-native' 2 + import {type RichText} from '@atproto/api' 3 + 4 + export type TextInputRef = { 5 + focus: () => void 6 + blur: () => void 7 + /** 8 + * @platform web 9 + */ 10 + getCursorPosition: () => 11 + | {left: number; right: number; top: number; bottom: number} 12 + | undefined 13 + /** 14 + * Closes the autocomplete popup if it is open. 15 + * Returns `true` if the popup was closed, `false` otherwise. 16 + * 17 + * @platform web 18 + */ 19 + maybeClosePopup: () => boolean 20 + } 21 + 22 + export type TextInputProps = { 23 + ref: React.Ref<TextInputRef> 24 + richtext: RichText 25 + webForceMinHeight: boolean 26 + hasRightPadding: boolean 27 + isActive: boolean 28 + setRichText: (v: RichText) => void 29 + onPhotoPasted: (uri: string) => void 30 + onPressPublish: (richtext: RichText) => void 31 + onNewLink: (uri: string) => void 32 + onError: (err: string) => void 33 + onFocus: () => void 34 + } & Pick< 35 + React.ComponentProps<typeof TextInput>, 36 + | 'placeholder' 37 + | 'autoFocus' 38 + | 'style' 39 + | 'accessible' 40 + | 'accessibilityLabel' 41 + | 'accessibilityHint' 42 + >
+49 -55
src/view/com/composer/text-input/TextInput.web.tsx
··· 1 - import React, {useRef} from 'react' 1 + import { 2 + useCallback, 3 + useEffect, 4 + useImperativeHandle, 5 + useMemo, 6 + useRef, 7 + useState, 8 + } from 'react' 2 9 import {StyleSheet, View} from 'react-native' 3 10 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 4 11 import {AppBskyRichtextFacet, RichText} from '@atproto/api' ··· 16 23 import Graphemer from 'graphemer' 17 24 18 25 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 19 - import {usePalette} from '#/lib/hooks/usePalette' 20 26 import {blobToDataUri, isUriImage} from '#/lib/media/util' 21 27 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' 22 28 import { ··· 27 33 import {atoms as a, useAlf} from '#/alf' 28 34 import {normalizeTextStyles} from '#/alf/typography' 29 35 import {Portal} from '#/components/Portal' 30 - import {Text} from '../../util/text/Text' 31 - import {createSuggestion} from './web/Autocomplete' 36 + import {Text} from '#/components/Typography' 37 + import {type TextInputProps} from './TextInput.types' 38 + import {type AutocompleteRef, createSuggestion} from './web/Autocomplete' 32 39 import {type Emoji} from './web/EmojiPicker' 33 40 import {LinkDecorator} from './web/LinkDecorator' 34 41 import {TagDecorator} from './web/TagDecorator' 35 42 36 - export interface TextInputRef { 37 - focus: () => void 38 - blur: () => void 39 - getCursorPosition: () => DOMRect | undefined 40 - } 41 - 42 - interface TextInputProps { 43 - richtext: RichText 44 - placeholder: string 45 - suggestedLinks: Set<string> 46 - webForceMinHeight: boolean 47 - hasRightPadding: boolean 48 - isActive: boolean 49 - setRichText: (v: RichText | ((v: RichText) => RichText)) => void 50 - onPhotoPasted: (uri: string) => void 51 - onPressPublish: (richtext: RichText) => void 52 - onNewLink: (uri: string) => void 53 - onError: (err: string) => void 54 - onFocus: () => void 55 - } 56 - 57 - export const TextInput = React.forwardRef(function TextInputImpl( 58 - { 59 - richtext, 60 - placeholder, 61 - webForceMinHeight, 62 - hasRightPadding, 63 - isActive, 64 - setRichText, 65 - onPhotoPasted, 66 - onPressPublish, 67 - onNewLink, 68 - onFocus, 69 - }: // onError, TODO 70 - TextInputProps, 43 + export function TextInput({ 71 44 ref, 72 - ) { 45 + richtext, 46 + placeholder, 47 + webForceMinHeight, 48 + hasRightPadding, 49 + isActive, 50 + setRichText, 51 + onPhotoPasted, 52 + onPressPublish, 53 + onNewLink, 54 + onFocus, 55 + }: TextInputProps) { 73 56 const {theme: t, fonts} = useAlf() 74 57 const autocomplete = useActorAutocompleteFn() 75 - const pal = usePalette('default') 76 58 const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') 77 59 78 - const [isDropping, setIsDropping] = React.useState(false) 60 + const [isDropping, setIsDropping] = useState(false) 61 + const autocompleteRef = useRef<AutocompleteRef>(null) 79 62 80 - const extensions = React.useMemo( 63 + const extensions = useMemo( 81 64 () => [ 82 65 Document, 83 66 LinkDecorator, ··· 86 69 HTMLAttributes: { 87 70 class: 'mention', 88 71 }, 89 - suggestion: createSuggestion({autocomplete}), 72 + suggestion: createSuggestion({autocomplete, autocompleteRef}), 90 73 }), 91 74 Paragraph, 92 75 Placeholder.configure({ ··· 99 82 [autocomplete, placeholder], 100 83 ) 101 84 102 - React.useEffect(() => { 85 + useEffect(() => { 103 86 if (!isActive) { 104 87 return 105 88 } ··· 109 92 } 110 93 }, [onPressPublish, isActive]) 111 94 112 - React.useEffect(() => { 95 + useEffect(() => { 113 96 if (!isActive) { 114 97 return 115 98 } ··· 119 102 } 120 103 }, [isActive, onPhotoPasted]) 121 104 122 - React.useEffect(() => { 105 + useEffect(() => { 123 106 if (!isActive) { 124 107 return 125 108 } ··· 296 279 [modeClass], 297 280 ) 298 281 299 - const onEmojiInserted = React.useCallback( 282 + const onEmojiInserted = useCallback( 300 283 (emoji: Emoji) => { 301 284 editor?.chain().focus().insertContent(emoji.native).run() 302 285 }, 303 286 [editor], 304 287 ) 305 - React.useEffect(() => { 288 + useEffect(() => { 306 289 if (!isActive) { 307 290 return 308 291 } ··· 312 295 } 313 296 }, [onEmojiInserted, isActive]) 314 297 315 - React.useImperativeHandle(ref, () => ({ 298 + useImperativeHandle(ref, () => ({ 316 299 focus: () => { 317 300 editor?.chain().focus() 318 301 }, ··· 323 306 const pos = editor?.state.selection.$anchor.pos 324 307 return pos ? editor?.view.coordsAtPos(pos) : undefined 325 308 }, 309 + maybeClosePopup: () => autocompleteRef.current?.maybeClose() ?? false, 326 310 })) 327 311 328 - const inputStyle = React.useMemo(() => { 312 + const inputStyle = useMemo(() => { 329 313 const style = normalizeTextStyles( 330 314 [a.text_lg, a.leading_snug, t.atoms.text], 331 315 { ··· 360 344 style={styles.dropContainer} 361 345 entering={FadeIn.duration(80)} 362 346 exiting={FadeOut.duration(80)}> 363 - <View style={[pal.view, pal.border, styles.dropModal]}> 347 + <View 348 + style={[ 349 + t.atoms.bg, 350 + t.atoms.border_contrast_low, 351 + styles.dropModal, 352 + ]}> 364 353 <Text 365 - type="lg" 366 - style={[pal.text, pal.borderDark, styles.dropText]}> 354 + style={[ 355 + a.text_lg, 356 + a.font_bold, 357 + t.atoms.text_contrast_medium, 358 + t.atoms.border_contrast_high, 359 + styles.dropText, 360 + ]}> 367 361 <Trans>Drop to add images</Trans> 368 362 </Text> 369 363 </View> ··· 372 366 )} 373 367 </> 374 368 ) 375 - }) 369 + } 376 370 377 371 function editorJsonToText( 378 372 json: JSONContent,
+132 -167
src/view/com/composer/text-input/web/Autocomplete.tsx
··· 1 1 import {forwardRef, useEffect, useImperativeHandle, useState} from 'react' 2 - import {Pressable, StyleSheet, View} from 'react-native' 3 - import {type AppBskyActorDefs} from '@atproto/api' 2 + import {Pressable, View} from 'react-native' 3 + import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api' 4 4 import {Trans} from '@lingui/macro' 5 5 import {ReactRenderer} from '@tiptap/react' 6 6 import { ··· 10 10 } from '@tiptap/suggestion' 11 11 import tippy, {type Instance as TippyInstance} from 'tippy.js' 12 12 13 - import {usePalette} from '#/lib/hooks/usePalette' 14 - import {sanitizeDisplayName} from '#/lib/strings/display-names' 15 - import {sanitizeHandle} from '#/lib/strings/handles' 13 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 14 import {type ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' 17 - import {Text} from '#/view/com/util/text/Text' 18 - import {UserAvatar} from '#/view/com/util/UserAvatar' 19 - import {atoms as a} from '#/alf' 20 - import {useSimpleVerificationState} from '#/components/verification' 21 - import {VerificationCheck} from '#/components/verification/VerificationCheck' 22 - import {useGrapheme} from '../hooks/useGrapheme' 15 + import {atoms as a, useTheme} from '#/alf' 16 + import * as ProfileCard from '#/components/ProfileCard' 17 + import {Text} from '#/components/Typography' 23 18 24 19 interface MentionListRef { 25 20 onKeyDown: (props: SuggestionKeyDownProps) => boolean 26 21 } 27 22 23 + export interface AutocompleteRef { 24 + maybeClose: () => boolean 25 + } 26 + 28 27 export function createSuggestion({ 29 28 autocomplete, 29 + autocompleteRef, 30 30 }: { 31 31 autocomplete: ActorAutocompleteFn 32 + autocompleteRef: React.Ref<AutocompleteRef> 32 33 }): Omit<SuggestionOptions, 'editor'> { 33 34 return { 34 35 async items({query}) { ··· 40 41 let component: ReactRenderer<MentionListRef> | undefined 41 42 let popup: TippyInstance[] | undefined 42 43 44 + const hide = () => { 45 + popup?.[0]?.destroy() 46 + component?.destroy() 47 + } 48 + 43 49 return { 44 50 onStart: props => { 45 51 component = new ReactRenderer(MentionList, { 46 - props, 52 + props: {...props, autocompleteRef, hide}, 47 53 editor: props.editor, 48 54 }) 49 55 ··· 78 84 79 85 onKeyDown(props) { 80 86 if (props.event.key === 'Escape') { 81 - popup?.[0]?.hide() 82 - 83 - return true 87 + return false 84 88 } 85 89 86 90 return component?.ref?.onKeyDown(props) || false 87 91 }, 88 92 89 93 onExit() { 90 - popup?.[0]?.destroy() 91 - component?.destroy() 94 + hide() 92 95 }, 93 96 } 94 97 }, 95 98 } 96 99 } 97 100 98 - const MentionList = forwardRef<MentionListRef, SuggestionProps>( 99 - function MentionListImpl(props: SuggestionProps, ref) { 100 - const [selectedIndex, setSelectedIndex] = useState(0) 101 - const pal = usePalette('default') 101 + const MentionList = forwardRef< 102 + MentionListRef, 103 + SuggestionProps & { 104 + autocompleteRef: React.Ref<AutocompleteRef> 105 + hide: () => void 106 + } 107 + >(function MentionListImpl({items, command, hide, autocompleteRef}, ref) { 108 + const [selectedIndex, setSelectedIndex] = useState(0) 109 + const t = useTheme() 110 + const moderationOpts = useModerationOpts() 102 111 103 - const selectItem = (index: number) => { 104 - const item = props.items[index] 112 + const selectItem = (index: number) => { 113 + const item = items[index] 105 114 106 - if (item) { 107 - props.command({id: item.handle}) 108 - } 115 + if (item) { 116 + command({id: item.handle}) 109 117 } 118 + } 110 119 111 - const upHandler = () => { 112 - setSelectedIndex( 113 - (selectedIndex + props.items.length - 1) % props.items.length, 114 - ) 115 - } 120 + const upHandler = () => { 121 + setSelectedIndex((selectedIndex + items.length - 1) % items.length) 122 + } 116 123 117 - const downHandler = () => { 118 - setSelectedIndex((selectedIndex + 1) % props.items.length) 119 - } 124 + const downHandler = () => { 125 + setSelectedIndex((selectedIndex + 1) % items.length) 126 + } 120 127 121 - const enterHandler = () => { 122 - selectItem(selectedIndex) 123 - } 128 + const enterHandler = () => { 129 + selectItem(selectedIndex) 130 + } 124 131 125 - useEffect(() => setSelectedIndex(0), [props.items]) 132 + useEffect(() => setSelectedIndex(0), [items]) 126 133 127 - useImperativeHandle(ref, () => ({ 128 - onKeyDown: ({event}) => { 129 - if (event.key === 'ArrowUp') { 130 - upHandler() 131 - return true 132 - } 134 + useImperativeHandle(autocompleteRef, () => ({ 135 + maybeClose: () => { 136 + hide() 137 + return true 138 + }, 139 + })) 133 140 134 - if (event.key === 'ArrowDown') { 135 - downHandler() 136 - return true 137 - } 141 + useImperativeHandle(ref, () => ({ 142 + onKeyDown: ({event}) => { 143 + if (event.key === 'ArrowUp') { 144 + upHandler() 145 + return true 146 + } 147 + 148 + if (event.key === 'ArrowDown') { 149 + downHandler() 150 + return true 151 + } 138 152 139 - if (event.key === 'Enter' || event.key === 'Tab') { 140 - enterHandler() 141 - return true 142 - } 153 + if (event.key === 'Enter' || event.key === 'Tab') { 154 + enterHandler() 155 + return true 156 + } 143 157 144 - return false 145 - }, 146 - })) 158 + return false 159 + }, 160 + })) 147 161 148 - const {items} = props 162 + if (!moderationOpts) return null 149 163 150 - return ( 151 - <div className="items"> 152 - <View style={[pal.borderDark, pal.view, styles.container]}> 153 - {items.length > 0 ? ( 154 - items.map((item, index) => { 155 - const isSelected = selectedIndex === index 164 + return ( 165 + <div className="items"> 166 + <View 167 + style={[ 168 + t.atoms.border_contrast_low, 169 + t.atoms.bg, 170 + a.rounded_sm, 171 + a.border, 172 + a.p_xs, 173 + {width: 300}, 174 + ]}> 175 + {items.length > 0 ? ( 176 + items.map((item, index) => { 177 + const isSelected = selectedIndex === index 156 178 157 - return ( 158 - <AutocompleteProfileCard 159 - key={item.handle} 160 - profile={item} 161 - isSelected={isSelected} 162 - itemIndex={index} 163 - totalItems={items.length} 164 - onPress={() => { 165 - selectItem(index) 166 - }} 167 - /> 168 - ) 169 - }) 170 - ) : ( 171 - <Text type="sm" style={[pal.text, styles.noResult]}> 172 - <Trans>No result</Trans> 173 - </Text> 174 - )} 175 - </View> 176 - </div> 177 - ) 178 - }, 179 - ) 179 + return ( 180 + <AutocompleteProfileCard 181 + key={item.handle} 182 + profile={item} 183 + isSelected={isSelected} 184 + onPress={() => selectItem(index)} 185 + onHover={() => setSelectedIndex(index)} 186 + moderationOpts={moderationOpts} 187 + /> 188 + ) 189 + }) 190 + ) : ( 191 + <Text style={[a.text_sm, a.px_md, a.py_md]}> 192 + <Trans>No result</Trans> 193 + </Text> 194 + )} 195 + </View> 196 + </div> 197 + ) 198 + }) 180 199 181 200 function AutocompleteProfileCard({ 182 201 profile, 183 202 isSelected, 184 - itemIndex, 185 - totalItems, 186 203 onPress, 204 + onHover, 205 + moderationOpts, 187 206 }: { 188 207 profile: AppBskyActorDefs.ProfileViewBasic 189 208 isSelected: boolean 190 - itemIndex: number 191 - totalItems: number 192 209 onPress: () => void 210 + onHover: () => void 211 + moderationOpts: ModerationOpts 193 212 }) { 194 - const pal = usePalette('default') 195 - const {getGraphemeString} = useGrapheme() 196 - const {name: displayName} = getGraphemeString( 197 - sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle)), 198 - 30, // Heuristic value; can be modified 199 - ) 200 - const state = useSimpleVerificationState({ 201 - profile, 202 - }) 213 + const t = useTheme() 214 + 203 215 return ( 204 216 <Pressable 205 217 style={[ 206 - isSelected ? pal.viewLight : undefined, 207 - pal.borderDark, 208 - styles.mentionContainer, 209 - itemIndex === 0 210 - ? styles.firstMention 211 - : itemIndex === totalItems - 1 212 - ? styles.lastMention 213 - : undefined, 218 + isSelected && t.atoms.bg_contrast_25, 219 + a.align_center, 220 + a.justify_between, 221 + a.flex_row, 222 + a.px_md, 223 + a.py_sm, 224 + a.gap_2xl, 225 + a.rounded_xs, 226 + a.transition_color, 214 227 ]} 215 228 onPress={onPress} 229 + onPointerEnter={onHover} 216 230 accessibilityRole="button"> 217 - <View style={[styles.avatarAndDisplayName, a.flex_1]}> 218 - <UserAvatar 219 - avatar={profile.avatar ?? null} 220 - size={26} 221 - type={profile.associated?.labeler ? 'labeler' : 'user'} 222 - /> 223 - <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}> 224 - <Text emoji style={[pal.text]} numberOfLines={1}> 225 - {displayName} 226 - </Text> 227 - {state.isVerified && ( 228 - <View> 229 - <VerificationCheck 230 - width={12} 231 - verifier={state.role === 'verifier'} 232 - /> 233 - </View> 234 - )} 235 - </View> 236 - </View> 237 - <View> 238 - <Text type="xs" style={pal.textLight} numberOfLines={1}> 239 - {sanitizeHandle(profile.handle, '@')} 240 - </Text> 231 + <View style={[a.flex_1]}> 232 + <ProfileCard.Header> 233 + <ProfileCard.Avatar 234 + profile={profile} 235 + moderationOpts={moderationOpts} 236 + disabledPreview 237 + /> 238 + <ProfileCard.NameAndHandle 239 + profile={profile} 240 + moderationOpts={moderationOpts} 241 + /> 242 + </ProfileCard.Header> 241 243 </View> 242 244 </Pressable> 243 245 ) 244 246 } 245 - 246 - const styles = StyleSheet.create({ 247 - container: { 248 - width: 500, 249 - borderRadius: 6, 250 - borderWidth: 1, 251 - borderStyle: 'solid', 252 - padding: 4, 253 - }, 254 - mentionContainer: { 255 - display: 'flex', 256 - alignItems: 'center', 257 - justifyContent: 'space-between', 258 - flexDirection: 'row', 259 - paddingHorizontal: 12, 260 - paddingVertical: 8, 261 - gap: 16, 262 - }, 263 - firstMention: { 264 - borderTopLeftRadius: 2, 265 - borderTopRightRadius: 2, 266 - }, 267 - lastMention: { 268 - borderBottomLeftRadius: 2, 269 - borderBottomRightRadius: 2, 270 - }, 271 - avatarAndDisplayName: { 272 - display: 'flex', 273 - flexDirection: 'row', 274 - alignItems: 'center', 275 - gap: 6, 276 - }, 277 - noResult: { 278 - paddingHorizontal: 12, 279 - paddingVertical: 8, 280 - }, 281 - })