Add avatar to mobile autocomplete and create grapheme hook (#602)

* Add avatar to mobile autocomplete and create grapheme hook

* Remove comment, update filename, cut out redundant logic
zio/stable
Ollie H 2023-05-09 10:13:23 -07:00 committed by GitHub
parent 9a91b0c538
commit 8f6b5d3df9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 66 deletions

View File

@ -0,0 +1,36 @@
import Graphemer from 'graphemer'
import {useCallback, useMemo} from 'react'
export const useGrapheme = () => {
const splitter = useMemo(() => new Graphemer(), [])
const getGraphemeString = useCallback(
(name: string, length: number) => {
let remainingCharacters = 0
if (name.length > length) {
const graphemes = splitter.splitGraphemes(name)
if (graphemes.length > length) {
remainingCharacters = 0
name = `${graphemes.slice(0, length).join('')}...`
} else {
remainingCharacters = length - graphemes.length
name = graphemes.join('')
}
} else {
remainingCharacters = length - name.length
}
return {
name,
remainingCharacters,
}
},
[splitter],
)
return {
getGraphemeString,
}
}

View File

@ -5,6 +5,8 @@ import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useGrapheme} from '../hooks/useGrapheme'
export const Autocomplete = observer(
({
@ -16,6 +18,7 @@ export const Autocomplete = observer(
}) => {
const pal = usePalette('default')
const positionInterp = useAnimatedValue(0)
const {getGraphemeString} = useGrapheme()
useEffect(() => {
Animated.timing(positionInterp, {
@ -35,58 +38,83 @@ export const Autocomplete = observer(
},
],
}
return (
<Animated.View style={topAnimStyle}>
{view.isActive ? (
<View style={[pal.view, styles.container, pal.border]}>
{view.suggestions.length > 0 ? (
view.suggestions.slice(0, 5).map(item => {
// Eventually use an average length
const MAX_CHARS = 40
const MAX_HANDLE_CHARS = 20
// Using this approach because styling is not respecting
// bounding box wrapping (before converting to ellipsis)
const {name: displayHandle, remainingCharacters} =
getGraphemeString(item.handle, MAX_HANDLE_CHARS)
const {name: displayName} = getGraphemeString(
item.displayName ?? item.handle,
MAX_CHARS -
MAX_HANDLE_CHARS +
(remainingCharacters > 0 ? remainingCharacters : 0),
)
return (
<View style={[styles.container, view.isActive && styles.visible]}>
<Animated.View
style={[
styles.animatedContainer,
pal.view,
pal.border,
topAnimStyle,
view.isActive && styles.visible,
]}>
{view.suggestions.slice(0, 5).map(item => (
<TouchableOpacity
testID="autocompleteButton"
key={item.handle}
style={[pal.border, styles.item]}
onPress={() => onSelect(item.handle)}
accessibilityLabel={`Select ${item.handle}`}
accessibilityHint={`Autocompletes to ${item.handle}`}>
accessibilityHint="">
<View style={styles.avatarAndHandle}>
<UserAvatar avatar={item.avatar ?? null} size={24} />
<Text type="md-medium" style={pal.text}>
{item.displayName || item.handle}
<Text type="sm" style={pal.textLight}>
&nbsp;@{item.handle}
{displayName}
</Text>
</View>
<Text type="sm" style={pal.textLight} numberOfLines={1}>
@{displayHandle}
</Text>
</TouchableOpacity>
))}
</Animated.View>
)
})
) : (
<Text type="sm" style={[pal.text, pal.border, styles.noResults]}>
No result
</Text>
)}
</View>
) : null}
</Animated.View>
)
},
)
const styles = StyleSheet.create({
container: {
display: 'none',
height: 250,
},
animatedContainer: {
display: 'none',
position: 'absolute',
left: -64,
right: 0,
top: 0,
marginLeft: -54,
top: 10,
borderTopWidth: 1,
},
visible: {
display: 'flex',
},
item: {
borderBottomWidth: 1,
paddingVertical: 16,
paddingHorizontal: 16,
height: 50,
paddingVertical: 12,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 6,
},
avatarAndHandle: {
display: 'flex',
flexDirection: 'row',
gap: 6,
alignItems: 'center',
},
noResults: {
paddingVertical: 12,
},
})

View File

@ -1,9 +1,7 @@
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react'
import {StyleSheet, View} from 'react-native'
@ -16,9 +14,9 @@ import {
} from '@tiptap/suggestion'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {usePalette} from 'lib/hooks/usePalette'
import Graphemer from 'graphemer'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useGrapheme} from '../hooks/useGrapheme'
interface MentionListRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean
@ -99,7 +97,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
(props: SuggestionProps, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0)
const pal = usePalette('default')
const splitter = useMemo(() => new Graphemer(), [])
const {getGraphemeString} = useGrapheme()
const selectItem = (index: number) => {
const item = props.items[index]
@ -148,32 +146,14 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
const {items} = props
const getDisplayedName = useCallback(
(name: string) => {
// Heuristic value based on max display name and handle lengths
const DISPLAY_LIMIT = 30
if (name.length > DISPLAY_LIMIT) {
const graphemes = splitter.splitGraphemes(name)
if (graphemes.length > DISPLAY_LIMIT) {
return graphemes.length > DISPLAY_LIMIT
? `${graphemes.slice(0, DISPLAY_LIMIT).join('')}...`
: name.substring(0, DISPLAY_LIMIT)
}
}
return name
},
[splitter],
)
return (
<div className="items">
<View style={[pal.borderDark, pal.view, styles.container]}>
{items.length > 0 ? (
items.map((item, index) => {
const displayName = getDisplayedName(
const {name: displayName} = getGraphemeString(
item.displayName ?? item.handle,
30, // Heuristic value; can be modified
)
const isSelected = selectedIndex === index
@ -197,7 +177,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
</Text>
</View>
<Text type="xs" style={pal.textLight} numberOfLines={1}>
{item.handle}
@{item.handle}
</Text>
</View>
)