diff --git a/src/view/com/composer/text-input/hooks/useGrapheme.tsx b/src/view/com/composer/text-input/hooks/useGrapheme.tsx new file mode 100644 index 00000000..25947c3e --- /dev/null +++ b/src/view/com/composer/text-input/hooks/useGrapheme.tsx @@ -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, + } +} diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx index 7806241f..c9b8b84b 100644 --- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx +++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx @@ -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 ( - - - {view.suggestions.slice(0, 5).map(item => ( - onSelect(item.handle)} - accessibilityLabel={`Select ${item.handle}`} - accessibilityHint={`Autocompletes to ${item.handle}`}> - - {item.displayName || item.handle} - -  @{item.handle} - + + {view.isActive ? ( + + {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 ( + onSelect(item.handle)} + accessibilityLabel={`Select ${item.handle}`} + accessibilityHint=""> + + + + {displayName} + + + + @{displayHandle} + + + ) + }) + ) : ( + + No result - - ))} - - + )} + + ) : null} + ) }, ) 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, }, }) diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 20dbbbbe..475ec119 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -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( (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( 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 (
{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( - {item.handle} + @{item.handle} )