this repo has no description
1import './shortcuts.css';
2
3import { Trans, useLingui } from '@lingui/react/macro';
4import { MenuDivider } from '@szhsin/react-menu';
5import { memo } from 'preact/compat';
6import { useEffect, useRef, useState } from 'preact/hooks';
7import { useHotkeys } from 'react-hotkeys-hook';
8import { useNavigate } from 'react-router-dom';
9import { useSnapshot } from 'valtio';
10
11import { SHORTCUTS_META } from '../components/shortcuts-settings';
12import { api } from '../utils/api';
13import { getLists } from '../utils/lists';
14import states from '../utils/states';
15
16import AsyncText from './AsyncText';
17import Icon from './icon';
18import Link from './link';
19import ListExclusiveBadge from './list-exclusive-badge';
20import MenuLink from './menu-link';
21import Menu2 from './menu2';
22import SubMenu2 from './submenu2';
23
24function Shortcuts() {
25 const { t, _ } = useLingui();
26 const { instance } = api();
27 const snapStates = useSnapshot(states);
28 const { shortcuts, settings } = snapStates;
29
30 if (!shortcuts.length) {
31 return null;
32 }
33 const isMultiColumnMode =
34 settings.shortcutsViewMode === 'multi-column' ||
35 (!settings.shortcutsViewMode && settings.shortcutsColumnsMode);
36
37 const menuRef = useRef();
38 const tabBarRef = useRef();
39
40 const hasLists = useRef(false);
41 const formattedShortcuts = shortcuts
42 .map((pin, i) => {
43 const { type, ...data } = pin;
44 if (!SHORTCUTS_META[type]) return null;
45 let { id, path, title, subtitle, icon } = SHORTCUTS_META[type];
46
47 if (typeof id === 'function') {
48 id = id(data, i);
49 }
50 if (typeof path === 'function') {
51 path = path(
52 {
53 ...data,
54 instance: data.instance || instance,
55 },
56 i,
57 );
58 }
59 if (typeof title === 'function') {
60 title = title(data, i);
61 } else if (title?.id) {
62 // Check if it's MessageDescriptor
63 title = _(title);
64 }
65 if (typeof subtitle === 'function') {
66 subtitle = subtitle(data, i);
67 } else if (subtitle?.id) {
68 // Check if it's MessageDescriptor
69 subtitle = _(subtitle);
70 }
71 if (typeof icon === 'function') {
72 icon = icon(data, i);
73 }
74
75 if (id === 'lists') {
76 hasLists.current = true;
77 }
78
79 return {
80 id,
81 path,
82 title,
83 subtitle,
84 icon,
85 };
86 })
87 .filter(Boolean);
88
89 // Auto-scroll to active tab on first render
90 useEffect(() => {
91 if (
92 snapStates.settings.shortcutsViewMode === 'tab-menu-bar' &&
93 tabBarRef.current
94 ) {
95 const timeoutId = setTimeout(() => {
96 const activeTab = tabBarRef.current?.querySelector('.is-active');
97 if (activeTab) {
98 activeTab.scrollIntoView({
99 behavior: 'smooth',
100 block: 'nearest',
101 inline: 'center',
102 });
103 }
104 }, 100);
105
106 return () => clearTimeout(timeoutId);
107 }
108 }, []);
109
110 const navigate = useNavigate();
111 useHotkeys(
112 ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
113 (e) => {
114 const index = parseInt(e.key, 10) - 1;
115 if (index < formattedShortcuts.length) {
116 const { path } = formattedShortcuts[index];
117 if (path) {
118 navigate(path);
119 menuRef.current?.closeMenu?.();
120 }
121 }
122 },
123 {
124 enabled: !isMultiColumnMode,
125 useKey: true,
126 ignoreEventWhen: (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey,
127 },
128 );
129
130 const [lists, setLists] = useState([]);
131
132 if (isMultiColumnMode) {
133 return null;
134 }
135
136 return (
137 <div id="shortcuts">
138 {snapStates.settings.shortcutsViewMode === 'tab-menu-bar' ? (
139 <nav
140 ref={tabBarRef}
141 class="tab-bar"
142 onContextMenu={(e) => {
143 e.preventDefault();
144 states.showShortcutsSettings = true;
145 }}
146 >
147 <ul>
148 {formattedShortcuts.map(
149 ({ id, path, title, subtitle, icon }, i) => {
150 return (
151 <li key={`${i}-${id}-${title}-${subtitle}-${path}`}>
152 <Link
153 class={subtitle ? 'has-subtitle' : ''}
154 to={path}
155 onClick={(e) => {
156 if (e.target.classList.contains('is-active')) {
157 e.preventDefault();
158 const page = document.getElementById(`${id}-page`);
159 console.log(id, page);
160 if (page) {
161 page.scrollTop = 0;
162 const updatesButton =
163 page.querySelector('.updates-button');
164 if (updatesButton) {
165 updatesButton.click();
166 }
167 }
168 }
169 }}
170 >
171 <Icon icon={icon} size="xl" />
172 <span>
173 <AsyncText>{title}</AsyncText>
174 {subtitle && (
175 <>
176 <br />
177 <small>{subtitle}</small>
178 </>
179 )}
180 </span>
181 </Link>
182 </li>
183 );
184 },
185 )}
186 </ul>
187 </nav>
188 ) : (
189 <Menu2
190 instanceRef={menuRef}
191 overflow="auto"
192 viewScroll="close"
193 menuClassName="glass-menu shortcuts-menu"
194 gap={8}
195 position="anchor"
196 onMenuChange={(e) => {
197 if (e.open && hasLists.current) {
198 getLists().then(setLists);
199 }
200 }}
201 menuButton={
202 <button
203 type="button"
204 id="shortcuts-button"
205 class="plain"
206 onContextMenu={(e) => {
207 e.preventDefault();
208 states.showShortcutsSettings = true;
209 }}
210 onTransitionStart={(e) => {
211 // Close menu if the button disappears
212 try {
213 const { target } = e;
214 if (getComputedStyle(target).pointerEvents === 'none') {
215 menuRef.current?.closeMenu?.();
216 }
217 } catch (e) {}
218 }}
219 >
220 <Icon icon="shortcut" size="xl" alt={t`Shortcuts`} />
221 </button>
222 }
223 >
224 {formattedShortcuts.map(({ id, path, title, subtitle, icon }, i) => {
225 if (id === 'lists') {
226 return (
227 <SubMenu2
228 menuClassName="glass-menu"
229 overflow="auto"
230 gap={-8}
231 label={
232 <>
233 <Icon icon={icon} size="l" />
234 <span class="menu-grow">
235 <AsyncText>{title}</AsyncText>
236 </span>
237 <Icon icon="chevron-right" />
238 </>
239 }
240 >
241 <MenuLink to="/l">
242 <span>
243 <Trans>All Lists</Trans>
244 </span>
245 </MenuLink>
246 <MenuDivider />
247 {lists?.map((list) => (
248 <MenuLink key={list.id} to={`/l/${list.id}`}>
249 <span>
250 {list.title}
251 {list.exclusive && (
252 <>
253 {' '}
254 <ListExclusiveBadge />
255 </>
256 )}
257 </span>
258 </MenuLink>
259 ))}
260 </SubMenu2>
261 );
262 }
263
264 return (
265 <MenuLink
266 to={path}
267 key={`${i}-${id}-${title}-${subtitle}-${path}`}
268 class="glass-menu-item"
269 >
270 <Icon icon={icon} size="l" />{' '}
271 <span class="menu-grow">
272 <span>
273 <AsyncText>{title}</AsyncText>
274 </span>
275 {subtitle && (
276 <>
277 {' '}
278 <small class="more-insignificant">{subtitle}</small>
279 </>
280 )}
281 </span>
282 <span class="menu-shortcut hide-until-focus-visible">
283 {i + 1}
284 </span>
285 </MenuLink>
286 );
287 })}
288 </Menu2>
289 )}
290 </div>
291 );
292}
293
294export default memo(Shortcuts);