Add check to composer user autocomplete (#8253)

* Add check to composer autocomplete web

* Add check to composer autocomplete mobile
This commit is contained in:
Eric Bailey
2025-04-21 16:32:29 -05:00
committed by GitHub
parent 4aaf81fedd
commit 8fdefb7e94
2 changed files with 154 additions and 84 deletions
@@ -1,5 +1,6 @@
import {View} from 'react-native'
import Animated, {FadeInDown, FadeOut} from 'react-native-reanimated'
import {type AppBskyActorDefs} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
@@ -9,6 +10,8 @@ import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
export function Autocomplete({
prefix,
@@ -42,51 +45,15 @@ export function Autocomplete({
{suggestions?.length ? (
suggestions.slice(0, 5).map((item, index, arr) => {
return (
<View
style={[
index !== arr.length - 1 && a.border_b,
t.atoms.border_contrast_high,
a.px_sm,
a.py_md,
]}
key={item.did}>
<PressableScale
testID="autocompleteButton"
style={[
a.flex_row,
a.gap_sm,
a.justify_between,
a.align_center,
]}
onPress={() => onSelect(item.handle)}
accessibilityLabel={`Select ${item.handle}`}
accessibilityHint="">
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
<UserAvatar
avatar={item.avatar ?? null}
size={24}
type={item.associated?.labeler ? 'labeler' : 'user'}
/>
<Text
style={[a.flex_1, a.text_md, a.font_bold]}
emoji
numberOfLines={1}>
{sanitizeDisplayName(
item.displayName || sanitizeHandle(item.handle),
)}
</Text>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_right,
{maxWidth: '50%'},
]}
numberOfLines={1}>
{sanitizeHandle(item.handle, '@')}
</Text>
</View>
</PressableScale>
</View>
<AutocompleteProfileCard
key={item.did}
profile={item}
itemIndex={index}
totalItems={arr.length}
onPress={() => {
onSelect(item.handle)
}}
/>
)
})
) : (
@@ -97,3 +64,64 @@ export function Autocomplete({
</Animated.View>
)
}
function AutocompleteProfileCard({
profile,
itemIndex,
totalItems,
onPress,
}: {
profile: AppBskyActorDefs.ProfileViewBasic
itemIndex: number
totalItems: number
onPress: () => void
}) {
const t = useTheme()
const state = useSimpleVerificationState({profile})
const displayName = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
)
return (
<View
style={[
itemIndex !== totalItems - 1 && a.border_b,
t.atoms.border_contrast_high,
a.px_sm,
a.py_md,
]}
key={profile.did}>
<PressableScale
testID="autocompleteButton"
style={[a.flex_row, a.gap_lg, a.justify_between, a.align_center]}
onPress={onPress}
accessibilityLabel={`Select ${profile.handle}`}
accessibilityHint="">
<View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_1]}>
<UserAvatar
avatar={profile.avatar ?? null}
size={24}
type={profile.associated?.labeler ? 'labeler' : 'user'}
/>
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
<Text style={[a.text_md, a.font_bold]} emoji numberOfLines={1}>
{displayName}
</Text>
{state.isVerified && (
<View>
<VerificationCheck
width={12}
verifier={state.role === 'verifier'}
/>
</View>
)}
</View>
</View>
<Text
style={[t.atoms.text_contrast_medium, a.text_right]}
numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</PressableScale>
</View>
)
}
@@ -1,20 +1,24 @@
import {forwardRef, useEffect, useImperativeHandle, useState} from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {type AppBskyActorDefs} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {ReactRenderer} from '@tiptap/react'
import {
SuggestionKeyDownProps,
SuggestionOptions,
SuggestionProps,
type SuggestionKeyDownProps,
type SuggestionOptions,
type SuggestionProps,
} from '@tiptap/suggestion'
import tippy, {Instance as TippyInstance} from 'tippy.js'
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 {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
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'
interface MentionListRef {
@@ -95,7 +99,6 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
function MentionListImpl(props: SuggestionProps, ref) {
const [selectedIndex, setSelectedIndex] = useState(0)
const pal = usePalette('default')
const {getGraphemeString} = useGrapheme()
const selectItem = (index: number) => {
const item = props.items[index]
@@ -149,45 +152,19 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
<View style={[pal.borderDark, pal.view, styles.container]}>
{items.length > 0 ? (
items.map((item, index) => {
const {name: displayName} = getGraphemeString(
sanitizeDisplayName(
item.displayName || sanitizeHandle(item.handle),
),
30, // Heuristic value; can be modified
)
const isSelected = selectedIndex === index
return (
<Pressable
<AutocompleteProfileCard
key={item.handle}
style={[
isSelected ? pal.viewLight : undefined,
pal.borderDark,
styles.mentionContainer,
index === 0
? styles.firstMention
: index === items.length - 1
? styles.lastMention
: undefined,
]}
profile={item}
isSelected={isSelected}
itemIndex={index}
totalItems={items.length}
onPress={() => {
selectItem(index)
}}
accessibilityRole="button">
<View style={styles.avatarAndDisplayName}>
<UserAvatar
avatar={item.avatar ?? null}
size={26}
type={item.associated?.labeler ? 'labeler' : 'user'}
/>
<Text emoji style={pal.text} numberOfLines={1}>
{displayName}
</Text>
</View>
<Text type="xs" style={pal.textLight} numberOfLines={1}>
{sanitizeHandle(item.handle, '@')}
</Text>
</Pressable>
/>
)
})
) : (
@@ -201,6 +178,71 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
},
)
function AutocompleteProfileCard({
profile,
isSelected,
itemIndex,
totalItems,
onPress,
}: {
profile: AppBskyActorDefs.ProfileViewBasic
isSelected: boolean
itemIndex: number
totalItems: number
onPress: () => void
}) {
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,
})
return (
<Pressable
style={[
isSelected ? pal.viewLight : undefined,
pal.borderDark,
styles.mentionContainer,
itemIndex === 0
? styles.firstMention
: itemIndex === totalItems - 1
? styles.lastMention
: undefined,
]}
onPress={onPress}
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>
</Pressable>
)
}
const styles = StyleSheet.create({
container: {
width: 500,
@@ -216,7 +258,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
paddingHorizontal: 12,
paddingVertical: 8,
gap: 4,
gap: 16,
},
firstMention: {
borderTopLeftRadius: 2,