this repo has no description
0
fork

Configure Feed

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

at main 242 lines 6.5 kB view raw
1import { Trans, useLingui } from '@lingui/react/macro'; 2import { useEffect, useRef, useState } from 'preact/hooks'; 3import { useHotkeys } from 'react-hotkeys-hook'; 4import { useDebouncedCallback } from 'use-debounce'; 5 6import { api } from '../utils/api'; 7import { fetchRelationships } from '../utils/relationships'; 8 9import AccountBlock from './account-block'; 10import Icon from './icon'; 11import Loader from './loader'; 12 13function MentionModal({ 14 onClose = () => {}, 15 onSelect = () => {}, 16 defaultSearchTerm, 17}) { 18 const { t } = useLingui(); 19 const { masto } = api(); 20 const [uiState, setUIState] = useState('default'); 21 const [accounts, setAccounts] = useState([]); 22 const [relationshipsMap, setRelationshipsMap] = useState({}); 23 24 const [selectedIndex, setSelectedIndex] = useState(0); 25 26 const loadRelationships = async (accounts) => { 27 if (!accounts?.length) return; 28 const relationships = await fetchRelationships(accounts, relationshipsMap); 29 if (relationships) { 30 setRelationshipsMap({ 31 ...relationshipsMap, 32 ...relationships, 33 }); 34 } 35 }; 36 37 const loadAccounts = (term) => { 38 if (!term) return; 39 setUIState('loading'); 40 (async () => { 41 try { 42 const accounts = await masto.v1.accounts.search.list({ 43 q: term, 44 limit: 40, 45 resolve: false, 46 }); 47 setAccounts(accounts); 48 loadRelationships(accounts); 49 setUIState('default'); 50 } catch (e) { 51 setUIState('error'); 52 console.error(e); 53 } 54 })(); 55 }; 56 57 const debouncedLoadAccounts = useDebouncedCallback(loadAccounts, 1000); 58 59 useEffect(() => { 60 loadAccounts(); 61 }, [loadAccounts]); 62 63 const inputRef = useRef(); 64 useEffect(() => { 65 if (inputRef.current) { 66 inputRef.current.focus(); 67 // Put cursor at the end 68 if (inputRef.current.value) { 69 inputRef.current.selectionStart = inputRef.current.value.length; 70 inputRef.current.selectionEnd = inputRef.current.value.length; 71 } 72 } 73 }, []); 74 75 useEffect(() => { 76 if (defaultSearchTerm) { 77 loadAccounts(defaultSearchTerm); 78 } 79 }, [defaultSearchTerm]); 80 81 const selectAccount = (account) => { 82 const socialAddress = account.acct; 83 onSelect(socialAddress); 84 onClose(); 85 }; 86 87 useHotkeys( 88 'enter', 89 () => { 90 const selectedAccount = accounts[selectedIndex]; 91 if (selectedAccount) { 92 selectAccount(selectedAccount); 93 } 94 }, 95 { 96 preventDefault: true, 97 enableOnFormTags: ['input'], 98 useKey: true, 99 ignoreEventWhen: (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey, 100 }, 101 ); 102 103 const listRef = useRef(); 104 useHotkeys( 105 'down', 106 () => { 107 if (selectedIndex < accounts.length - 1) { 108 setSelectedIndex(selectedIndex + 1); 109 } else { 110 setSelectedIndex(0); 111 } 112 setTimeout(() => { 113 const selectedItem = listRef.current.querySelector('.selected'); 114 if (selectedItem) { 115 selectedItem.scrollIntoView({ 116 behavior: 'smooth', 117 block: 'center', 118 inline: 'center', 119 }); 120 } 121 }, 1); 122 }, 123 { 124 preventDefault: true, 125 enableOnFormTags: ['input'], 126 useKey: true, 127 ignoreEventWhen: (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey, 128 }, 129 ); 130 131 useHotkeys( 132 'up', 133 () => { 134 if (selectedIndex > 0) { 135 setSelectedIndex(selectedIndex - 1); 136 } else { 137 setSelectedIndex(accounts.length - 1); 138 } 139 setTimeout(() => { 140 const selectedItem = listRef.current.querySelector('.selected'); 141 if (selectedItem) { 142 selectedItem.scrollIntoView({ 143 behavior: 'smooth', 144 block: 'center', 145 inline: 'center', 146 }); 147 } 148 }, 1); 149 }, 150 { 151 preventDefault: true, 152 enableOnFormTags: ['input'], 153 useKey: true, 154 ignoreEventWhen: (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey, 155 }, 156 ); 157 158 return ( 159 <div id="mention-sheet" class="sheet"> 160 {!!onClose && ( 161 <button type="button" class="sheet-close" onClick={onClose}> 162 <Icon icon="x" alt={t`Close`} /> 163 </button> 164 )} 165 <header> 166 <form 167 onSubmit={(e) => { 168 e.preventDefault(); 169 debouncedLoadAccounts.flush?.(); 170 // const searchTerm = inputRef.current.value; 171 // debouncedLoadAccounts(searchTerm); 172 }} 173 > 174 <input 175 ref={inputRef} 176 required 177 type="search" 178 class="block" 179 placeholder={t`Search accounts`} 180 onInput={(e) => { 181 const { value } = e.target; 182 debouncedLoadAccounts(value); 183 }} 184 autocomplete="off" 185 autocorrect="off" 186 autocapitalize="off" 187 spellCheck="false" 188 dir="auto" 189 defaultValue={defaultSearchTerm || ''} 190 /> 191 </form> 192 </header> 193 <main> 194 {accounts?.length > 0 ? ( 195 <ul 196 ref={listRef} 197 class={`accounts-list ${uiState === 'loading' ? 'loading' : ''}`} 198 > 199 {accounts.map((account, i) => { 200 const relationship = relationshipsMap[account.id]; 201 return ( 202 <li 203 key={account.id} 204 class={i === selectedIndex ? 'selected' : ''} 205 > 206 <AccountBlock 207 avatarSize="xxl" 208 account={account} 209 relationship={relationship} 210 showStats 211 showActivity 212 /> 213 <button 214 type="button" 215 class="plain2" 216 onClick={() => { 217 selectAccount(account); 218 }} 219 > 220 <Icon icon="plus" size="xl" alt={t`Add`} /> 221 </button> 222 </li> 223 ); 224 })} 225 </ul> 226 ) : uiState === 'loading' ? ( 227 <div class="ui-state"> 228 <Loader abrupt /> 229 </div> 230 ) : uiState === 'error' ? ( 231 <div class="ui-state"> 232 <p> 233 <Trans>Error loading accounts</Trans> 234 </p> 235 </div> 236 ) : null} 237 </main> 238 </div> 239 ); 240} 241 242export default MentionModal;