this repo has no description
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;