Add check to composer user autocomplete (#8253)
* Add check to composer autocomplete web * Add check to composer autocomplete mobile
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import Animated, {FadeInDown, FadeOut} from 'react-native-reanimated'
|
import Animated, {FadeInDown, FadeOut} from 'react-native-reanimated'
|
||||||
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
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 {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
|
|
||||||
export function Autocomplete({
|
export function Autocomplete({
|
||||||
prefix,
|
prefix,
|
||||||
@@ -42,51 +45,15 @@ export function Autocomplete({
|
|||||||
{suggestions?.length ? (
|
{suggestions?.length ? (
|
||||||
suggestions.slice(0, 5).map((item, index, arr) => {
|
suggestions.slice(0, 5).map((item, index, arr) => {
|
||||||
return (
|
return (
|
||||||
<View
|
<AutocompleteProfileCard
|
||||||
style={[
|
key={item.did}
|
||||||
index !== arr.length - 1 && a.border_b,
|
profile={item}
|
||||||
t.atoms.border_contrast_high,
|
itemIndex={index}
|
||||||
a.px_sm,
|
totalItems={arr.length}
|
||||||
a.py_md,
|
onPress={() => {
|
||||||
]}
|
onSelect(item.handle)
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
@@ -97,3 +64,64 @@ export function Autocomplete({
|
|||||||
</Animated.View>
|
</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 {forwardRef, useEffect, useImperativeHandle, useState} from 'react'
|
||||||
import {Pressable, StyleSheet, View} from 'react-native'
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
import {ReactRenderer} from '@tiptap/react'
|
import {ReactRenderer} from '@tiptap/react'
|
||||||
import {
|
import {
|
||||||
SuggestionKeyDownProps,
|
type SuggestionKeyDownProps,
|
||||||
SuggestionOptions,
|
type SuggestionOptions,
|
||||||
SuggestionProps,
|
type SuggestionProps,
|
||||||
} from '@tiptap/suggestion'
|
} 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 {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
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 {Text} from '#/view/com/util/text/Text'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
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 {useGrapheme} from '../hooks/useGrapheme'
|
||||||
|
|
||||||
interface MentionListRef {
|
interface MentionListRef {
|
||||||
@@ -95,7 +99,6 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
|||||||
function MentionListImpl(props: SuggestionProps, ref) {
|
function MentionListImpl(props: SuggestionProps, ref) {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {getGraphemeString} = useGrapheme()
|
|
||||||
|
|
||||||
const selectItem = (index: number) => {
|
const selectItem = (index: number) => {
|
||||||
const item = props.items[index]
|
const item = props.items[index]
|
||||||
@@ -149,45 +152,19 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
|||||||
<View style={[pal.borderDark, pal.view, styles.container]}>
|
<View style={[pal.borderDark, pal.view, styles.container]}>
|
||||||
{items.length > 0 ? (
|
{items.length > 0 ? (
|
||||||
items.map((item, index) => {
|
items.map((item, index) => {
|
||||||
const {name: displayName} = getGraphemeString(
|
|
||||||
sanitizeDisplayName(
|
|
||||||
item.displayName || sanitizeHandle(item.handle),
|
|
||||||
),
|
|
||||||
30, // Heuristic value; can be modified
|
|
||||||
)
|
|
||||||
const isSelected = selectedIndex === index
|
const isSelected = selectedIndex === index
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<AutocompleteProfileCard
|
||||||
key={item.handle}
|
key={item.handle}
|
||||||
style={[
|
profile={item}
|
||||||
isSelected ? pal.viewLight : undefined,
|
isSelected={isSelected}
|
||||||
pal.borderDark,
|
itemIndex={index}
|
||||||
styles.mentionContainer,
|
totalItems={items.length}
|
||||||
index === 0
|
|
||||||
? styles.firstMention
|
|
||||||
: index === items.length - 1
|
|
||||||
? styles.lastMention
|
|
||||||
: undefined,
|
|
||||||
]}
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
selectItem(index)
|
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({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
width: 500,
|
width: 500,
|
||||||
@@ -216,7 +258,7 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
gap: 4,
|
gap: 16,
|
||||||
},
|
},
|
||||||
firstMention: {
|
firstMention: {
|
||||||
borderTopLeftRadius: 2,
|
borderTopLeftRadius: 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user