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
This commit is contained in:
@@ -114,10 +114,7 @@ import {SelectPostLanguagesBtn} from '#/view/com/composer/select-language/Select
|
||||
import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
|
||||
// TODO: Prevent naming components that coincide with RN primitives
|
||||
// due to linting false positives
|
||||
import {
|
||||
TextInput,
|
||||
type TextInputRef,
|
||||
} from '#/view/com/composer/text-input/TextInput'
|
||||
import {TextInput} from '#/view/com/composer/text-input/TextInput'
|
||||
import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn'
|
||||
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
|
||||
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
|
||||
@@ -155,6 +152,7 @@ import {
|
||||
processVideo,
|
||||
type VideoState,
|
||||
} from './state/video'
|
||||
import {type TextInputRef} from './text-input/TextInput.types'
|
||||
import {getVideoMetadata} from './videos/pickVideo'
|
||||
import {clearThumbnailCache} from './videos/VideoTranscodeBackdrop'
|
||||
|
||||
@@ -306,7 +304,9 @@ export const ComposePost = ({
|
||||
)
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
if (
|
||||
if (textInput.current?.maybeClosePopup()) {
|
||||
return
|
||||
} else if (
|
||||
thread.posts.some(
|
||||
post =>
|
||||
post.shortenedGraphemeLength > 0 ||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {
|
||||
type ComponentProps,
|
||||
forwardRef,
|
||||
import {
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -9,7 +8,6 @@ import React, {
|
||||
import {
|
||||
type NativeSyntheticEvent,
|
||||
Text as RNText,
|
||||
type TextInput as RNTextInput,
|
||||
type TextInputSelectionChangeEventData,
|
||||
View,
|
||||
} from 'react-native'
|
||||
@@ -33,57 +31,38 @@ import {
|
||||
import {atoms as a, useAlf} from '#/alf'
|
||||
import {normalizeTextStyles} from '#/alf/typography'
|
||||
import {Autocomplete} from './mobile/Autocomplete'
|
||||
|
||||
export interface TextInputRef {
|
||||
focus: () => void
|
||||
blur: () => void
|
||||
getCursorPosition: () => DOMRect | undefined
|
||||
}
|
||||
|
||||
interface TextInputProps extends ComponentProps<typeof RNTextInput> {
|
||||
richtext: RichText
|
||||
placeholder: string
|
||||
webForceMinHeight: boolean
|
||||
hasRightPadding: boolean
|
||||
isActive: boolean
|
||||
setRichText: (v: RichText) => void
|
||||
onPhotoPasted: (uri: string) => void
|
||||
onPressPublish: (richtext: RichText) => void
|
||||
onNewLink: (uri: string) => void
|
||||
onError: (err: string) => void
|
||||
}
|
||||
import {type TextInputProps} from './TextInput.types'
|
||||
|
||||
interface Selection {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export const TextInput = forwardRef(function TextInputImpl(
|
||||
{
|
||||
richtext,
|
||||
placeholder,
|
||||
hasRightPadding,
|
||||
setRichText,
|
||||
onPhotoPasted,
|
||||
onNewLink,
|
||||
onError,
|
||||
...props
|
||||
}: TextInputProps,
|
||||
export function TextInput({
|
||||
ref,
|
||||
) {
|
||||
richtext,
|
||||
placeholder,
|
||||
hasRightPadding,
|
||||
setRichText,
|
||||
onPhotoPasted,
|
||||
onNewLink,
|
||||
onError,
|
||||
...props
|
||||
}: TextInputProps) {
|
||||
const {theme: t, fonts} = useAlf()
|
||||
const textInput = useRef<PasteInputRef>(null)
|
||||
const textInputSelection = useRef<Selection>({start: 0, end: 0})
|
||||
const theme = useTheme()
|
||||
const [autocompletePrefix, setAutocompletePrefix] = useState('')
|
||||
const prevLength = React.useRef(richtext.length)
|
||||
const prevLength = useRef(richtext.length)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => textInput.current?.focus(),
|
||||
blur: () => {
|
||||
textInput.current?.blur()
|
||||
},
|
||||
getCursorPosition: () => undefined, // Not implemented on native
|
||||
maybeClosePopup: () => false, // Not needed on native
|
||||
}))
|
||||
|
||||
const pastSuggestedUris = useRef(new Set<string>())
|
||||
@@ -185,7 +164,7 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
[onChangeText, richtext, setAutocompletePrefix],
|
||||
)
|
||||
|
||||
const inputTextStyle = React.useMemo(() => {
|
||||
const inputTextStyle = useMemo(() => {
|
||||
const style = normalizeTextStyles(
|
||||
[a.text_lg, a.leading_snug, t.atoms.text],
|
||||
{
|
||||
@@ -277,4 +256,4 @@ export const TextInput = forwardRef(function TextInputImpl(
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {type TextInput} from 'react-native'
|
||||
import {type RichText} from '@atproto/api'
|
||||
|
||||
export type TextInputRef = {
|
||||
focus: () => void
|
||||
blur: () => void
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
getCursorPosition: () =>
|
||||
| {left: number; right: number; top: number; bottom: number}
|
||||
| undefined
|
||||
/**
|
||||
* Closes the autocomplete popup if it is open.
|
||||
* Returns `true` if the popup was closed, `false` otherwise.
|
||||
*
|
||||
* @platform web
|
||||
*/
|
||||
maybeClosePopup: () => boolean
|
||||
}
|
||||
|
||||
export type TextInputProps = {
|
||||
ref: React.Ref<TextInputRef>
|
||||
richtext: RichText
|
||||
webForceMinHeight: boolean
|
||||
hasRightPadding: boolean
|
||||
isActive: boolean
|
||||
setRichText: (v: RichText) => void
|
||||
onPhotoPasted: (uri: string) => void
|
||||
onPressPublish: (richtext: RichText) => void
|
||||
onNewLink: (uri: string) => void
|
||||
onError: (err: string) => void
|
||||
onFocus: () => void
|
||||
} & Pick<
|
||||
React.ComponentProps<typeof TextInput>,
|
||||
| 'placeholder'
|
||||
| 'autoFocus'
|
||||
| 'style'
|
||||
| 'accessible'
|
||||
| 'accessibilityLabel'
|
||||
| 'accessibilityHint'
|
||||
>
|
||||
@@ -1,4 +1,11 @@
|
||||
import React, {useRef} from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||
@@ -16,7 +23,6 @@ import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
|
||||
import Graphemer from 'graphemer'
|
||||
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {blobToDataUri, isUriImage} from '#/lib/media/util'
|
||||
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
||||
import {
|
||||
@@ -27,57 +33,34 @@ import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEm
|
||||
import {atoms as a, useAlf} from '#/alf'
|
||||
import {normalizeTextStyles} from '#/alf/typography'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {createSuggestion} from './web/Autocomplete'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type TextInputProps} from './TextInput.types'
|
||||
import {type AutocompleteRef, createSuggestion} from './web/Autocomplete'
|
||||
import {type Emoji} from './web/EmojiPicker'
|
||||
import {LinkDecorator} from './web/LinkDecorator'
|
||||
import {TagDecorator} from './web/TagDecorator'
|
||||
|
||||
export interface TextInputRef {
|
||||
focus: () => void
|
||||
blur: () => void
|
||||
getCursorPosition: () => DOMRect | undefined
|
||||
}
|
||||
|
||||
interface TextInputProps {
|
||||
richtext: RichText
|
||||
placeholder: string
|
||||
suggestedLinks: Set<string>
|
||||
webForceMinHeight: boolean
|
||||
hasRightPadding: boolean
|
||||
isActive: boolean
|
||||
setRichText: (v: RichText | ((v: RichText) => RichText)) => void
|
||||
onPhotoPasted: (uri: string) => void
|
||||
onPressPublish: (richtext: RichText) => void
|
||||
onNewLink: (uri: string) => void
|
||||
onError: (err: string) => void
|
||||
onFocus: () => void
|
||||
}
|
||||
|
||||
export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
{
|
||||
richtext,
|
||||
placeholder,
|
||||
webForceMinHeight,
|
||||
hasRightPadding,
|
||||
isActive,
|
||||
setRichText,
|
||||
onPhotoPasted,
|
||||
onPressPublish,
|
||||
onNewLink,
|
||||
onFocus,
|
||||
}: // onError, TODO
|
||||
TextInputProps,
|
||||
export function TextInput({
|
||||
ref,
|
||||
) {
|
||||
richtext,
|
||||
placeholder,
|
||||
webForceMinHeight,
|
||||
hasRightPadding,
|
||||
isActive,
|
||||
setRichText,
|
||||
onPhotoPasted,
|
||||
onPressPublish,
|
||||
onNewLink,
|
||||
onFocus,
|
||||
}: TextInputProps) {
|
||||
const {theme: t, fonts} = useAlf()
|
||||
const autocomplete = useActorAutocompleteFn()
|
||||
const pal = usePalette('default')
|
||||
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
|
||||
|
||||
const [isDropping, setIsDropping] = React.useState(false)
|
||||
const [isDropping, setIsDropping] = useState(false)
|
||||
const autocompleteRef = useRef<AutocompleteRef>(null)
|
||||
|
||||
const extensions = React.useMemo(
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
Document,
|
||||
LinkDecorator,
|
||||
@@ -86,7 +69,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion: createSuggestion({autocomplete}),
|
||||
suggestion: createSuggestion({autocomplete, autocompleteRef}),
|
||||
}),
|
||||
Paragraph,
|
||||
Placeholder.configure({
|
||||
@@ -99,7 +82,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
[autocomplete, placeholder],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
@@ -109,7 +92,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
}
|
||||
}, [onPressPublish, isActive])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
@@ -119,7 +102,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
}
|
||||
}, [isActive, onPhotoPasted])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
@@ -296,13 +279,13 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
[modeClass],
|
||||
)
|
||||
|
||||
const onEmojiInserted = React.useCallback(
|
||||
const onEmojiInserted = useCallback(
|
||||
(emoji: Emoji) => {
|
||||
editor?.chain().focus().insertContent(emoji.native).run()
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
@@ -312,7 +295,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
}
|
||||
}, [onEmojiInserted, isActive])
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
editor?.chain().focus()
|
||||
},
|
||||
@@ -323,9 +306,10 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
const pos = editor?.state.selection.$anchor.pos
|
||||
return pos ? editor?.view.coordsAtPos(pos) : undefined
|
||||
},
|
||||
maybeClosePopup: () => autocompleteRef.current?.maybeClose() ?? false,
|
||||
}))
|
||||
|
||||
const inputStyle = React.useMemo(() => {
|
||||
const inputStyle = useMemo(() => {
|
||||
const style = normalizeTextStyles(
|
||||
[a.text_lg, a.leading_snug, t.atoms.text],
|
||||
{
|
||||
@@ -360,10 +344,20 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
style={styles.dropContainer}
|
||||
entering={FadeIn.duration(80)}
|
||||
exiting={FadeOut.duration(80)}>
|
||||
<View style={[pal.view, pal.border, styles.dropModal]}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.dropModal,
|
||||
]}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[pal.text, pal.borderDark, styles.dropText]}>
|
||||
style={[
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_medium,
|
||||
t.atoms.border_contrast_high,
|
||||
styles.dropText,
|
||||
]}>
|
||||
<Trans>Drop to add images</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
@@ -372,7 +366,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function editorJsonToText(
|
||||
json: JSONContent,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {forwardRef, useEffect, useImperativeHandle, useState} from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {ReactRenderer} from '@tiptap/react'
|
||||
import {
|
||||
@@ -10,25 +10,26 @@ import {
|
||||
} from '@tiptap/suggestion'
|
||||
import tippy, {type Instance as TippyInstance} from 'tippy.js'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {type ActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useSimpleVerificationState} from '#/components/verification'
|
||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||
import {useGrapheme} from '../hooks/useGrapheme'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
interface MentionListRef {
|
||||
onKeyDown: (props: SuggestionKeyDownProps) => boolean
|
||||
}
|
||||
|
||||
export interface AutocompleteRef {
|
||||
maybeClose: () => boolean
|
||||
}
|
||||
|
||||
export function createSuggestion({
|
||||
autocomplete,
|
||||
autocompleteRef,
|
||||
}: {
|
||||
autocomplete: ActorAutocompleteFn
|
||||
autocompleteRef: React.Ref<AutocompleteRef>
|
||||
}): Omit<SuggestionOptions, 'editor'> {
|
||||
return {
|
||||
async items({query}) {
|
||||
@@ -40,10 +41,15 @@ export function createSuggestion({
|
||||
let component: ReactRenderer<MentionListRef> | undefined
|
||||
let popup: TippyInstance[] | undefined
|
||||
|
||||
const hide = () => {
|
||||
popup?.[0]?.destroy()
|
||||
component?.destroy()
|
||||
}
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
props: {...props, autocompleteRef, hide},
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
@@ -78,204 +84,163 @@ export function createSuggestion({
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup?.[0]?.hide()
|
||||
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
return component?.ref?.onKeyDown(props) || false
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup?.[0]?.destroy()
|
||||
component?.destroy()
|
||||
hide()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
||||
function MentionListImpl(props: SuggestionProps, ref) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const pal = usePalette('default')
|
||||
const MentionList = forwardRef<
|
||||
MentionListRef,
|
||||
SuggestionProps & {
|
||||
autocompleteRef: React.Ref<AutocompleteRef>
|
||||
hide: () => void
|
||||
}
|
||||
>(function MentionListImpl({items, command, hide, autocompleteRef}, ref) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
const item = props.items[index]
|
||||
const selectItem = (index: number) => {
|
||||
const item = items[index]
|
||||
|
||||
if (item) {
|
||||
props.command({id: item.handle})
|
||||
if (item) {
|
||||
command({id: item.handle})
|
||||
}
|
||||
}
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + items.length - 1) % items.length)
|
||||
}
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % items.length)
|
||||
}
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex)
|
||||
}
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [items])
|
||||
|
||||
useImperativeHandle(autocompleteRef, () => ({
|
||||
maybeClose: () => {
|
||||
hide()
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({event}) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length,
|
||||
)
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length)
|
||||
}
|
||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||
enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex)
|
||||
}
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items])
|
||||
if (!moderationOpts) return null
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({event}) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler()
|
||||
return true
|
||||
}
|
||||
return (
|
||||
<div className="items">
|
||||
<View
|
||||
style={[
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
a.p_xs,
|
||||
{width: 300},
|
||||
]}>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, index) => {
|
||||
const isSelected = selectedIndex === index
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||
enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
const {items} = props
|
||||
|
||||
return (
|
||||
<div className="items">
|
||||
<View style={[pal.borderDark, pal.view, styles.container]}>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, index) => {
|
||||
const isSelected = selectedIndex === index
|
||||
|
||||
return (
|
||||
<AutocompleteProfileCard
|
||||
key={item.handle}
|
||||
profile={item}
|
||||
isSelected={isSelected}
|
||||
itemIndex={index}
|
||||
totalItems={items.length}
|
||||
onPress={() => {
|
||||
selectItem(index)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Text type="sm" style={[pal.text, styles.noResult]}>
|
||||
<Trans>No result</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<AutocompleteProfileCard
|
||||
key={item.handle}
|
||||
profile={item}
|
||||
isSelected={isSelected}
|
||||
onPress={() => selectItem(index)}
|
||||
onHover={() => setSelectedIndex(index)}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Text style={[a.text_sm, a.px_md, a.py_md]}>
|
||||
<Trans>No result</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function AutocompleteProfileCard({
|
||||
profile,
|
||||
isSelected,
|
||||
itemIndex,
|
||||
totalItems,
|
||||
onPress,
|
||||
onHover,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
isSelected: boolean
|
||||
itemIndex: number
|
||||
totalItems: number
|
||||
onPress: () => void
|
||||
onHover: () => void
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {getGraphemeString} = useGrapheme()
|
||||
const {name: displayName} = getGraphemeString(
|
||||
sanitizeDisplayName(profile.displayName || sanitizeHandle(profile.handle)),
|
||||
30, // Heuristic value; can be modified
|
||||
)
|
||||
const state = useSimpleVerificationState({
|
||||
profile,
|
||||
})
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
isSelected ? pal.viewLight : undefined,
|
||||
pal.borderDark,
|
||||
styles.mentionContainer,
|
||||
itemIndex === 0
|
||||
? styles.firstMention
|
||||
: itemIndex === totalItems - 1
|
||||
? styles.lastMention
|
||||
: undefined,
|
||||
isSelected && t.atoms.bg_contrast_25,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.flex_row,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
a.gap_2xl,
|
||||
a.rounded_xs,
|
||||
a.transition_color,
|
||||
]}
|
||||
onPress={onPress}
|
||||
onPointerEnter={onHover}
|
||||
accessibilityRole="button">
|
||||
<View style={[styles.avatarAndDisplayName, a.flex_1]}>
|
||||
<UserAvatar
|
||||
avatar={profile.avatar ?? null}
|
||||
size={26}
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
|
||||
<Text emoji style={[pal.text]} numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
{state.isVerified && (
|
||||
<View>
|
||||
<VerificationCheck
|
||||
width={12}
|
||||
verifier={state.role === 'verifier'}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<Text type="xs" style={pal.textLight} numberOfLines={1}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
<View style={[a.flex_1]}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
disabledPreview
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: 500,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
padding: 4,
|
||||
},
|
||||
mentionContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
gap: 16,
|
||||
},
|
||||
firstMention: {
|
||||
borderTopLeftRadius: 2,
|
||||
borderTopRightRadius: 2,
|
||||
},
|
||||
lastMention: {
|
||||
borderBottomLeftRadius: 2,
|
||||
borderBottomRightRadius: 2,
|
||||
},
|
||||
avatarAndDisplayName: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
noResult: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user