this repo has no description
0
fork

Configure Feed

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

New feature: Shortcuts

+676 -43
+34 -4
src/app.css
··· 1011 1011 /* MENU POPUP */ 1012 1012 1013 1013 .szh-menu { 1014 - padding: 8px 0 !important; 1014 + padding: 8px 0; 1015 1015 margin: 0; 1016 1016 font-size: 16px; 1017 - background-color: var(--bg-color) !important; 1018 - border: 1px solid var(--outline-color) !important; 1017 + background-color: var(--bg-color); 1018 + border: 1px solid var(--outline-color); 1019 1019 border-radius: 8px; 1020 1020 box-shadow: 0 3px 6px var(--drop-shadow-color); 1021 1021 text-align: left; 1022 1022 animation: appear 0.15s ease-in-out; 1023 1023 width: 16em; 1024 1024 max-width: 90vw; 1025 + overflow: hidden; 1026 + } 1027 + .szh-menu__item--focusable { 1028 + background-color: transparent; 1025 1029 } 1026 1030 .szh-menu .szh-menu__item { 1027 1031 padding: 8px 16px !important; ··· 1036 1040 .szh-menu .szh-menu__item a { 1037 1041 overflow: hidden; 1038 1042 text-overflow: ellipsis; 1039 - display: block; 1043 + display: flex; 1040 1044 color: inherit; 1041 1045 text-decoration: none; 1042 1046 padding: 8px 16px !important; 1043 1047 margin: -8px -16px !important; 1048 + gap: 8px; 1049 + } 1050 + .szh-menu .szh-menu__item a.is-active { 1051 + font-weight: bold; 1052 + } 1053 + .szh-menu .szh-menu__item .icon { 1054 + opacity: 0.5; 1044 1055 } 1045 1056 .szh-menu 1046 1057 .szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) { ··· 1052 1063 } 1053 1064 .szh-menu__divider { 1054 1065 background-color: var(--divider-color); 1066 + } 1067 + .szh-menu .szh-menu__item .menu-grow { 1068 + flex-grow: 1; 1069 + } 1070 + .szh-menu .szh-menu__item .menu-shortcut { 1071 + opacity: 0.5; 1072 + font-weight: normal; 1073 + } 1074 + 1075 + /* GLASS MENU */ 1076 + 1077 + .glass-menu { 1078 + background-color: var(--bg-blur-color); 1079 + backdrop-filter: blur(8px) saturate(3); 1080 + border: 0; 1081 + box-shadow: 0 3px 8px -1px var(--drop-shadow-color); 1082 + } 1083 + .glass-menu .szh-menu__item--hover { 1084 + background-color: var(--button-bg-blur-color); 1055 1085 } 1056 1086 1057 1087 /* DONUT METER */
+22 -18
src/app.jsx
··· 25 25 import Loader from './components/loader'; 26 26 import MediaModal from './components/media-modal'; 27 27 import Modal from './components/modal'; 28 + import Shortcuts from './components/shortcuts'; 29 + import ShortcutsSettings from './components/shortcuts-settings'; 28 30 import NotFound from './pages/404'; 29 31 import AccountStatuses from './pages/account-statuses'; 30 32 import Bookmarks from './pages/bookmarks'; ··· 146 148 return () => clearTimeout(timer); 147 149 }; 148 150 useEffect(focusDeck, [location]); 149 - const showModal = useMemo(() => { 150 - return ( 151 - snapStates.showCompose || 152 - snapStates.showSettings || 153 - snapStates.showAccount || 154 - snapStates.showDrafts || 155 - snapStates.showMediaModal 156 - ); 157 - }, [ 158 - snapStates.showCompose, 159 - snapStates.showSettings, 160 - snapStates.showAccount, 161 - snapStates.showDrafts, 162 - snapStates.showMediaModal, 163 - ]); 151 + const showModal = 152 + snapStates.showCompose || 153 + snapStates.showSettings || 154 + snapStates.showAccount || 155 + snapStates.showDrafts || 156 + snapStates.showMediaModal || 157 + snapStates.showShortcutsSettings; 164 158 useEffect(() => { 165 - if (!showModal) { 166 - focusDeck(); 167 - } 159 + if (!showModal) focusDeck(); 168 160 }, [showModal]); 169 161 170 162 // useEffect(() => { ··· 306 298 </Link> 307 299 </li> 308 300 </nav> 301 + <Shortcuts /> 309 302 {!!snapStates.showCompose && ( 310 303 <Modal> 311 304 <Compose ··· 414 407 states.showMediaModal = false; 415 408 }} 416 409 /> 410 + </Modal> 411 + )} 412 + {!!snapStates.showShortcutsSettings && ( 413 + <Modal 414 + onClick={(e) => { 415 + if (e.target === e.currentTarget) { 416 + states.showShortcutsSettings = false; 417 + } 418 + }} 419 + > 420 + <ShortcutsSettings /> 417 421 </Modal> 418 422 )} 419 423 </>
+12
src/components/AsyncText.jsx
··· 1 + import { useEffect, useState } from 'preact/hooks'; 2 + 3 + function AsyncText({ children }) { 4 + if (typeof children === 'string') return children; 5 + const [text, setText] = useState(''); 6 + useEffect(() => { 7 + Promise.resolve(children).then(setText); 8 + }, [children]); 9 + return text; 10 + } 11 + 12 + export default AsyncText;
+21
src/components/MenuLink.jsx
··· 1 + import { FocusableItem } from '@szhsin/react-menu'; 2 + 3 + import Link from './link'; 4 + 5 + function MenuLink(props) { 6 + return ( 7 + <FocusableItem> 8 + {({ ref, closeMenu }) => ( 9 + <Link 10 + {...props} 11 + ref={ref} 12 + onClick={({ detail }) => 13 + closeMenu(detail === 0 ? 'Enter' : undefined) 14 + } 15 + /> 16 + )} 17 + </FocusableItem> 18 + ); 19 + } 20 + 21 + export default MenuLink;
+3
src/components/icon.jsx
··· 53 53 search: 'mingcute:search-2-line', 54 54 hashtag: 'mingcute:hashtag-line', 55 55 info: 'mingcute:information-line', 56 + shortcut: 'mingcute:lightning-line', 57 + user: 'mingcute:user-4-line', 58 + following: 'mingcute:walk-line', 56 59 }; 57 60 58 61 const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
+11 -19
src/components/menu.jsx
··· 1 - import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; 1 + import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; 2 2 import { useSnapshot } from 'valtio'; 3 3 4 4 import { api } from '../utils/api'; 5 5 import states from '../utils/states'; 6 6 7 7 import Icon from './icon'; 8 - import Link from './link'; 8 + import MenuLink from './MenuLink'; 9 9 10 10 function NavMenu(props) { 11 11 const snapStates = useSnapshot(states); ··· 69 69 <MenuDivider /> 70 70 <MenuItem 71 71 onClick={() => { 72 + states.showShortcutsSettings = true; 73 + }} 74 + > 75 + <Icon icon="shortcut" size="l" />{' '} 76 + <span>Shortcuts Settings&hellip;</span> 77 + </MenuItem> 78 + <MenuItem 79 + onClick={() => { 72 80 states.showSettings = true; 73 81 }} 74 82 > 75 - <Icon icon="gear" size="l" alt="Settings" /> <span>Settings</span> 83 + <Icon icon="gear" size="l" /> <span>Settings&hellip;</span> 76 84 </MenuItem> 77 85 </> 78 86 )} 79 87 </Menu> 80 - ); 81 - } 82 - 83 - function MenuLink(props) { 84 - return ( 85 - <FocusableItem> 86 - {({ ref, closeMenu }) => ( 87 - <Link 88 - {...props} 89 - ref={ref} 90 - onClick={({ detail }) => 91 - closeMenu(detail === 0 ? 'Enter' : undefined) 92 - } 93 - /> 94 - )} 95 - </FocusableItem> 96 88 ); 97 89 } 98 90
+69
src/components/shortcuts-settings.css
··· 1 + #shortcuts-settings-container .shortcuts-list { 2 + line-height: 1.5; 3 + padding: 0; 4 + margin: 8px 0 0; 5 + counter-reset: index; 6 + border-radius: 8px; 7 + overflow: hidden; 8 + } 9 + #shortcuts-settings-container .shortcuts-list li { 10 + display: flex; 11 + align-items: center; 12 + padding: 8px; 13 + gap: 4px; 14 + background-color: var(--bg-faded-color); 15 + } 16 + #shortcuts-settings-container .shortcuts-list li::before { 17 + content: counter(index); 18 + counter-increment: index; 19 + display: inline-block; 20 + width: 1.2em; 21 + text-align: right; 22 + margin-right: 8px; 23 + color: var(--text-insignificant-color); 24 + font-size: 90%; 25 + } 26 + #shortcuts-settings-container .shortcuts-list li .shortcut-text { 27 + flex-grow: 1; 28 + } 29 + 30 + #shortcuts-settings-container form { 31 + display: flex; 32 + gap: 8px; 33 + flex-wrap: wrap; 34 + align-items: center; 35 + padding: 16px; 36 + background-color: var(--bg-faded-color); 37 + border-radius: 16px; 38 + } 39 + 40 + #shortcuts-settings-container form header { 41 + display: flex; 42 + align-items: center; 43 + justify-content: space-between; 44 + } 45 + 46 + #shortcuts-settings-container form > * { 47 + flex-basis: max(320px, 100%); 48 + margin: 0; 49 + padding: 0; 50 + } 51 + 52 + #shortcuts-settings-container form label { 53 + display: flex; 54 + flex-direction: row; 55 + gap: 8px; 56 + align-items: center; 57 + } 58 + #shortcuts-settings-container form label > span:first-child { 59 + flex-basis: 5em; 60 + text-align: right; 61 + } 62 + #shortcuts-settings-container form :is(input[type='text'], select) { 63 + flex-grow: 1; 64 + flex-basis: 70%; 65 + flex-shrink: 1; 66 + /* width: calc(100% - 32px); */ 67 + min-width: 0; 68 + max-width: 320px; 69 + }
+351
src/components/shortcuts-settings.jsx
··· 1 + import './shortcuts-settings.css'; 2 + 3 + import { useEffect, useState } from 'preact/hooks'; 4 + import { useSnapshot } from 'valtio'; 5 + 6 + import { api } from '../utils/api'; 7 + import states from '../utils/states'; 8 + 9 + import AsyncText from './AsyncText'; 10 + import Icon from './icon'; 11 + 12 + const TYPES = [ 13 + 'following', 14 + 'notifications', 15 + 'list', 16 + 'public', 17 + 'search', 18 + // NOTE: Hide for now, can't think of a good way to handle this 19 + // 'account-statuses', 20 + 'bookmarks', 21 + 'favourites', 22 + 'hashtag', 23 + ]; 24 + const TYPE_TEXT = { 25 + following: 'Home', 26 + notifications: 'Notifications', 27 + list: 'List', 28 + public: 'Public', 29 + search: 'Search', 30 + 'account-statuses': 'Account', 31 + bookmarks: 'Bookmarks', 32 + favourites: 'Favourites', 33 + hashtag: 'Hashtag', 34 + }; 35 + const TYPE_PARAMS = { 36 + list: [ 37 + { 38 + text: 'List ID', 39 + name: 'id', 40 + }, 41 + ], 42 + public: [ 43 + { 44 + text: 'Local only', 45 + name: 'local', 46 + type: 'checkbox', 47 + }, 48 + { 49 + text: 'Instance', 50 + name: 'instance', 51 + type: 'text', 52 + placeholder: 'e.g. mastodon.social', 53 + }, 54 + ], 55 + search: [ 56 + { 57 + text: 'Search term', 58 + name: 'query', 59 + type: 'text', 60 + }, 61 + ], 62 + 'account-statuses': [ 63 + { 64 + text: '@', 65 + name: 'id', 66 + type: 'text', 67 + placeholder: 'cheeaun@mastodon.social', 68 + }, 69 + ], 70 + hashtag: [ 71 + { 72 + text: '#', 73 + name: 'hashtag', 74 + type: 'text', 75 + placeholder: 'e.g PixelArt', 76 + }, 77 + ], 78 + }; 79 + export const SHORTCUTS_META = { 80 + following: { 81 + title: 'Home', 82 + path: (_, index) => (index === 0 ? '/' : '/l/f'), 83 + icon: 'home', 84 + }, 85 + notifications: { 86 + title: 'Notifications', 87 + path: '/notifications', 88 + icon: 'notification', 89 + }, 90 + list: { 91 + title: async ({ id }) => { 92 + const list = await api().masto.v1.lists.fetch(id); 93 + return list.title; 94 + }, 95 + path: ({ id }) => `/l/${id}`, 96 + icon: 'list', 97 + }, 98 + public: { 99 + title: ({ local, instance }) => 100 + `${local ? 'Local' : 'Federated'} (${instance})`, 101 + path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`, 102 + icon: ({ local }) => (local ? 'group' : 'earth'), 103 + }, 104 + search: { 105 + title: ({ query }) => query, 106 + path: ({ query }) => `/search?q=${query}`, 107 + icon: 'search', 108 + }, 109 + 'account-statuses': { 110 + title: async ({ id }) => { 111 + const account = await api().masto.v1.accounts.fetch(id); 112 + return account.username || account.acct || account.displayName; 113 + }, 114 + path: ({ id }) => `/a/${id}`, 115 + icon: 'user', 116 + }, 117 + bookmarks: { 118 + title: 'Bookmarks', 119 + path: '/b', 120 + icon: 'bookmark', 121 + }, 122 + favourites: { 123 + title: 'Favourites', 124 + path: '/f', 125 + icon: 'heart', 126 + }, 127 + hashtag: { 128 + title: ({ hashtag }) => hashtag, 129 + path: ({ hashtag }) => `/t/${hashtag}`, 130 + icon: 'hashtag', 131 + }, 132 + }; 133 + 134 + function ShortcutsSettings() { 135 + const snapStates = useSnapshot(states); 136 + const { masto } = api(); 137 + 138 + const [lists, setLists] = useState([]); 139 + const [followedHashtags, setFollowedHashtags] = useState([]); 140 + 141 + useEffect(() => { 142 + (async () => { 143 + try { 144 + const lists = await masto.v1.lists.list(); 145 + setLists(lists); 146 + } catch (e) { 147 + console.error(e); 148 + } 149 + })(); 150 + 151 + (async () => { 152 + try { 153 + const iterator = masto.v1.followedTags.list(); 154 + const tags = []; 155 + do { 156 + const { value, done } = await iterator.next(); 157 + if (done || value?.length === 0) break; 158 + tags.push(...value); 159 + } while (true); 160 + setFollowedHashtags(tags); 161 + } catch (e) { 162 + console.error(e); 163 + } 164 + })(); 165 + }, []); 166 + 167 + return ( 168 + <div id="shortcuts-settings-container" class="sheet" tabindex="-1"> 169 + <header> 170 + <h2> 171 + <Icon icon="shortcut" /> Shortcuts{' '} 172 + <sup 173 + style={{ 174 + fontSize: 12, 175 + opacity: 0.5, 176 + textTransform: 'uppercase', 177 + }} 178 + > 179 + beta 180 + </sup> 181 + </h2> 182 + </header> 183 + <main> 184 + <p> 185 + Specify a list of shortcuts that'll appear in the floating Shortcuts 186 + button. 187 + </p> 188 + {snapStates.shortcuts.length > 0 ? ( 189 + <ol class="shortcuts-list"> 190 + {snapStates.shortcuts.map((shortcut, i) => { 191 + const key = i + Object.values(shortcut); 192 + const { type } = shortcut; 193 + let { icon, title } = SHORTCUTS_META[type]; 194 + if (typeof title === 'function') { 195 + title = title(shortcut); 196 + } 197 + if (typeof icon === 'function') { 198 + icon = icon(shortcut); 199 + } 200 + return ( 201 + <li key={key}> 202 + <Icon icon={icon} /> 203 + <span class="shortcut-text"> 204 + <AsyncText>{title}</AsyncText> 205 + </span> 206 + <span> 207 + <button 208 + type="button" 209 + class="plain small" 210 + disabled={i === 0} 211 + onClick={() => { 212 + const shortcutsArr = Array.from(states.shortcuts); 213 + if (i > 0) { 214 + const temp = states.shortcuts[i - 1]; 215 + shortcutsArr[i - 1] = shortcut; 216 + shortcutsArr[i] = temp; 217 + states.shortcuts = shortcutsArr; 218 + } 219 + }} 220 + > 221 + <Icon icon="arrow-up" alt="Move up" /> 222 + </button> 223 + <button 224 + type="button" 225 + class="plain small" 226 + disabled={i === snapStates.shortcuts.length - 1} 227 + onClick={() => { 228 + const shortcutsArr = Array.from(states.shortcuts); 229 + if (i < states.shortcuts.length - 1) { 230 + const temp = states.shortcuts[i + 1]; 231 + shortcutsArr[i + 1] = shortcut; 232 + shortcutsArr[i] = temp; 233 + states.shortcuts = shortcutsArr; 234 + } 235 + }} 236 + > 237 + <Icon icon="arrow-down" alt="Move down" /> 238 + </button> 239 + <button 240 + type="button" 241 + class="plain small" 242 + onClick={() => { 243 + states.shortcuts.splice(i, 1); 244 + }} 245 + > 246 + <Icon icon="x" alt="Remove" /> 247 + </button> 248 + </span> 249 + </li> 250 + ); 251 + })} 252 + </ol> 253 + ) : ( 254 + <p class="ui-state insignificant"> 255 + No shortcuts yet. Add one from the form below. 256 + </p> 257 + )} 258 + <hr /> 259 + <ShortcutForm 260 + lists={lists} 261 + followedHashtags={followedHashtags} 262 + onSubmit={(data) => { 263 + console.log('onSubmit', data); 264 + states.shortcuts.push(data); 265 + }} 266 + /> 267 + </main> 268 + </div> 269 + ); 270 + } 271 + 272 + export default ShortcutsSettings; 273 + function ShortcutForm({ type, lists, followedHashtags, onSubmit }) { 274 + const [currentType, setCurrentType] = useState(type); 275 + return ( 276 + <> 277 + <form 278 + onSubmit={(e) => { 279 + // Construct a nice object from form 280 + e.preventDefault(); 281 + const data = new FormData(e.target); 282 + const result = {}; 283 + data.forEach((value, key) => { 284 + result[key] = value; 285 + }); 286 + if (!result.type) return; 287 + onSubmit(result); 288 + // Reset 289 + e.target.reset(); 290 + setCurrentType(null); 291 + }} 292 + > 293 + <header> 294 + <h3>Add a shortcut</h3> 295 + <button type="submit">Add</button> 296 + </header> 297 + <p> 298 + <label> 299 + <span>Timeline</span> 300 + <select 301 + onChange={(e) => { 302 + setCurrentType(e.target.value); 303 + }} 304 + name="type" 305 + > 306 + <option></option> 307 + {TYPES.map((type) => ( 308 + <option value={type}>{TYPE_TEXT[type]}</option> 309 + ))} 310 + </select> 311 + </label> 312 + </p> 313 + {TYPE_PARAMS[currentType]?.map?.( 314 + ({ text, name, type, placeholder }) => { 315 + if (currentType === 'list') { 316 + return ( 317 + <p> 318 + <label> 319 + <span>List</span> 320 + <select name="id"> 321 + {lists.map((list) => ( 322 + <option value={list.id}>{list.title}</option> 323 + ))} 324 + </select> 325 + </label> 326 + </p> 327 + ); 328 + } 329 + 330 + return ( 331 + <p> 332 + <label> 333 + <span>{text}</span>{' '} 334 + <input type={type} name={name} placeholder={placeholder} /> 335 + {currentType === 'hashtag' && followedHashtags.length > 0 && ( 336 + <datalist> 337 + {followedHashtags.map((tag) => ( 338 + <option value={tag.name} /> 339 + ))} 340 + </datalist> 341 + )} 342 + </label> 343 + </p> 344 + ); 345 + }, 346 + )} 347 + <footer></footer> 348 + </form> 349 + </> 350 + ); 351 + }
+39
src/components/shortcuts.css
··· 1 + #shortcuts-button { 2 + position: fixed; 3 + bottom: 16px; 4 + bottom: max(16px, env(safe-area-inset-bottom)); 5 + left: 16px; 6 + left: max(16px, env(safe-area-inset-left)); 7 + padding: 16px; 8 + background-color: var(--bg-faded-blur-color); 9 + z-index: 101; 10 + box-shadow: 0 3px 8px -1px var(--drop-shadow-color); 11 + transition: all 0.3s ease-in-out; 12 + } 13 + #shortcuts-button .icon { 14 + transform: translateY(2px); /* Balance the icon's vertical alignment */ 15 + } 16 + #app:has(header[hidden]) #shortcuts-button, 17 + #shortcuts-button[hidden] { 18 + transform: translateY(200%); 19 + pointer-events: none; 20 + user-select: none; 21 + } 22 + #shortcuts-button:is(:hover, :focus) { 23 + background-color: var(--button-color); 24 + filter: none; 25 + } 26 + #shortcuts-button:active { 27 + filter: brightness(0.75); 28 + } 29 + 30 + @media (min-width: calc(40em + 56px + 8px)) { 31 + #shortcuts-button { 32 + right: 16px; 33 + right: max(16px, env(safe-area-inset-right)); 34 + left: auto; 35 + top: 16px; 36 + top: max(16px, env(safe-area-inset-top)); 37 + bottom: auto; 38 + } 39 + }
+103
src/components/shortcuts.jsx
··· 1 + import './shortcuts.css'; 2 + 3 + import { Menu, MenuItem } from '@szhsin/react-menu'; 4 + import { useRef } from 'preact/hooks'; 5 + import { useHotkeys } from 'react-hotkeys-hook'; 6 + import { useNavigate } from 'react-router-dom'; 7 + import { useSnapshot } from 'valtio'; 8 + 9 + import { SHORTCUTS_META } from '../components/shortcuts-settings'; 10 + import states from '../utils/states'; 11 + 12 + import AsyncText from './AsyncText'; 13 + import Icon from './icon'; 14 + import MenuLink from './MenuLink'; 15 + 16 + function Shortcuts() { 17 + const snapStates = useSnapshot(states); 18 + const { shortcuts } = snapStates; 19 + 20 + if (!shortcuts.length) { 21 + return null; 22 + } 23 + 24 + const menuRef = useRef(); 25 + 26 + const formattedShortcuts = shortcuts.map((pin, i) => { 27 + const { type, ...data } = pin; 28 + let { path, title, icon } = SHORTCUTS_META[type]; 29 + 30 + if (typeof path === 'function') { 31 + path = path(data, i); 32 + } 33 + if (typeof title === 'function') { 34 + title = title(data); 35 + } 36 + if (typeof icon === 'function') { 37 + icon = icon(data); 38 + } 39 + 40 + return { 41 + path, 42 + title, 43 + icon, 44 + }; 45 + }); 46 + 47 + const navigate = useNavigate(); 48 + useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => { 49 + const index = parseInt(handler.keys[0], 10) - 1; 50 + if (index < formattedShortcuts.length) { 51 + const { path } = formattedShortcuts[index]; 52 + if (path) { 53 + navigate(path); 54 + } 55 + } 56 + }); 57 + 58 + return ( 59 + <div id="shortcuts"> 60 + <Menu 61 + instanceRef={menuRef} 62 + overflow="auto" 63 + viewScroll="close" 64 + boundingBoxPadding="8 8 8 8" 65 + menuClassName="glass-menu shortcuts-menu" 66 + offsetY={4} 67 + position="anchor" 68 + menuButton={ 69 + <button 70 + type="button" 71 + id="shortcuts-button" 72 + class="plain" 73 + onTransitionStart={(e) => { 74 + // Close menu if the button disappears 75 + try { 76 + const { target } = e; 77 + if (getComputedStyle(target).pointerEvents === 'none') { 78 + menuRef.current?.closeMenu?.(); 79 + } 80 + } catch (e) {} 81 + }} 82 + > 83 + <Icon icon="shortcut" size="xl" alt="Shortcuts" /> 84 + </button> 85 + } 86 + > 87 + {formattedShortcuts.map(({ path, title, icon }, i) => { 88 + return ( 89 + <MenuLink to={path} key={i + title} class="glass-menu-item"> 90 + <Icon icon={icon} size="l" />{' '} 91 + <span class="menu-grow"> 92 + <AsyncText>{title}</AsyncText> 93 + </span> 94 + <span class="menu-shortcut">{i + 1}</span> 95 + </MenuLink> 96 + ); 97 + })} 98 + </Menu> 99 + </div> 100 + ); 101 + } 102 + 103 + export default Shortcuts;
+1 -1
src/pages/home.jsx
··· 62 62 } 63 63 }} 64 64 > 65 - <Icon icon="quill" size="xxl" alt="Compose" /> 65 + <Icon icon="quill" size="xl" alt="Compose" /> 66 66 </button> 67 67 </> 68 68 );
+10 -1
src/utils/states.js
··· 1 - import { proxy } from 'valtio'; 1 + import { proxy, subscribe } from 'valtio'; 2 2 import { subscribeKey } from 'valtio/utils'; 3 3 4 4 import { api } from './api'; ··· 30 30 showAccount: false, 31 31 showDrafts: false, 32 32 showMediaModal: false, 33 + showShortcutsSettings: false, 34 + // Shortcuts 35 + shortcuts: store.account.get('shortcuts') ?? [], 33 36 // Settings 34 37 settings: { 35 38 boostsCarousel: store.account.get('settings-boostCarousel') ?? true, ··· 44 47 }); 45 48 subscribeKey(states, 'settings-boostCarousel', (v) => { 46 49 store.account.set('settings-boostCarousel', !!v); 50 + }); 51 + subscribe(states, (v) => { 52 + const [action, path, value] = v[0]; 53 + if (path?.[0] === 'shortcuts') { 54 + store.account.set('shortcuts', states.shortcuts); 55 + } 47 56 }); 48 57 49 58 export function hideAllModals() {