mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at rn-stack-repro 235 lines 6.1 kB view raw
1import React, { 2 forwardRef, 3 useEffect, 4 useImperativeHandle, 5 useState, 6} from 'react' 7import {Pressable, StyleSheet, View} from 'react-native' 8import {ReactRenderer} from '@tiptap/react' 9import tippy, {Instance as TippyInstance} from 'tippy.js' 10import { 11 SuggestionOptions, 12 SuggestionProps, 13 SuggestionKeyDownProps, 14} from '@tiptap/suggestion' 15import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' 16import {usePalette} from 'lib/hooks/usePalette' 17import {Text} from 'view/com/util/text/Text' 18import {UserAvatar} from 'view/com/util/UserAvatar' 19import {useGrapheme} from '../hooks/useGrapheme' 20import {Trans} from '@lingui/macro' 21 22interface MentionListRef { 23 onKeyDown: (props: SuggestionKeyDownProps) => boolean 24} 25 26export function createSuggestion({ 27 autocomplete, 28}: { 29 autocomplete: ActorAutocompleteFn 30}): Omit<SuggestionOptions, 'editor'> { 31 return { 32 async items({query}) { 33 const suggestions = await autocomplete({query}) 34 return suggestions.slice(0, 8) 35 }, 36 37 render: () => { 38 let component: ReactRenderer<MentionListRef> | undefined 39 let popup: TippyInstance[] | undefined 40 41 return { 42 onStart: props => { 43 component = new ReactRenderer(MentionList, { 44 props, 45 editor: props.editor, 46 }) 47 48 if (!props.clientRect) { 49 return 50 } 51 52 // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf 53 popup = tippy('body', { 54 getReferenceClientRect: props.clientRect, 55 appendTo: () => document.body, 56 content: component.element, 57 showOnCreate: true, 58 interactive: true, 59 trigger: 'manual', 60 placement: 'bottom-start', 61 }) 62 }, 63 64 onUpdate(props) { 65 component?.updateProps(props) 66 67 if (!props.clientRect) { 68 return 69 } 70 71 popup?.[0]?.setProps({ 72 // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf 73 getReferenceClientRect: props.clientRect, 74 }) 75 }, 76 77 onKeyDown(props) { 78 if (props.event.key === 'Escape') { 79 popup?.[0]?.hide() 80 81 return true 82 } 83 84 return component?.ref?.onKeyDown(props) || false 85 }, 86 87 onExit() { 88 popup?.[0]?.destroy() 89 component?.destroy() 90 }, 91 } 92 }, 93 } 94} 95 96const MentionList = forwardRef<MentionListRef, SuggestionProps>( 97 function MentionListImpl(props: SuggestionProps, ref) { 98 const [selectedIndex, setSelectedIndex] = useState(0) 99 const pal = usePalette('default') 100 const {getGraphemeString} = useGrapheme() 101 102 const selectItem = (index: number) => { 103 const item = props.items[index] 104 105 if (item) { 106 props.command({id: item.handle}) 107 } 108 } 109 110 const upHandler = () => { 111 setSelectedIndex( 112 (selectedIndex + props.items.length - 1) % props.items.length, 113 ) 114 } 115 116 const downHandler = () => { 117 setSelectedIndex((selectedIndex + 1) % props.items.length) 118 } 119 120 const enterHandler = () => { 121 selectItem(selectedIndex) 122 } 123 124 useEffect(() => setSelectedIndex(0), [props.items]) 125 126 useImperativeHandle(ref, () => ({ 127 onKeyDown: ({event}) => { 128 if (event.key === 'ArrowUp') { 129 upHandler() 130 return true 131 } 132 133 if (event.key === 'ArrowDown') { 134 downHandler() 135 return true 136 } 137 138 if (event.key === 'Enter' || event.key === 'Tab') { 139 enterHandler() 140 return true 141 } 142 143 return false 144 }, 145 })) 146 147 const {items} = props 148 149 return ( 150 <div className="items"> 151 <View style={[pal.borderDark, pal.view, styles.container]}> 152 {items.length > 0 ? ( 153 items.map((item, index) => { 154 const {name: displayName} = getGraphemeString( 155 item.displayName ?? item.handle, 156 30, // Heuristic value; can be modified 157 ) 158 const isSelected = selectedIndex === index 159 160 return ( 161 <Pressable 162 key={item.handle} 163 style={[ 164 isSelected ? pal.viewLight : undefined, 165 pal.borderDark, 166 styles.mentionContainer, 167 index === 0 168 ? styles.firstMention 169 : index === items.length - 1 170 ? styles.lastMention 171 : undefined, 172 ]} 173 onPress={() => { 174 selectItem(index) 175 }} 176 accessibilityRole="button"> 177 <View style={styles.avatarAndDisplayName}> 178 <UserAvatar avatar={item.avatar ?? null} size={26} /> 179 <Text style={pal.text} numberOfLines={1}> 180 {displayName} 181 </Text> 182 </View> 183 <Text type="xs" style={pal.textLight} numberOfLines={1}> 184 @{item.handle} 185 </Text> 186 </Pressable> 187 ) 188 }) 189 ) : ( 190 <Text type="sm" style={[pal.text, styles.noResult]}> 191 <Trans>No result</Trans> 192 </Text> 193 )} 194 </View> 195 </div> 196 ) 197 }, 198) 199 200const styles = StyleSheet.create({ 201 container: { 202 width: 500, 203 borderRadius: 6, 204 borderWidth: 1, 205 borderStyle: 'solid', 206 padding: 4, 207 }, 208 mentionContainer: { 209 display: 'flex', 210 alignItems: 'center', 211 justifyContent: 'space-between', 212 flexDirection: 'row', 213 paddingHorizontal: 12, 214 paddingVertical: 8, 215 gap: 4, 216 }, 217 firstMention: { 218 borderTopLeftRadius: 2, 219 borderTopRightRadius: 2, 220 }, 221 lastMention: { 222 borderBottomLeftRadius: 2, 223 borderBottomRightRadius: 2, 224 }, 225 avatarAndDisplayName: { 226 display: 'flex', 227 flexDirection: 'row', 228 alignItems: 'center', 229 gap: 6, 230 }, 231 noResult: { 232 paddingHorizontal: 12, 233 paddingVertical: 8, 234 }, 235})