this repo has no description
0
fork

Configure Feed

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

at main 294 lines 8.8 kB view raw
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);