this repo has no description
0
fork

Configure Feed

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

Experiment: allow Search in Shortcuts

+125 -23
+4
src/components/columns.jsx
··· 9 9 import Mentions from '../pages/mentions'; 10 10 import Notifications from '../pages/notifications'; 11 11 import Public from '../pages/public'; 12 + import Search from '../pages/search'; 12 13 import Trending from '../pages/trending'; 13 14 import states from '../utils/states'; 14 15 import useTitle from '../utils/useTitle'; ··· 33 34 hashtag: Hashtag, 34 35 mentions: Mentions, 35 36 trending: Trending, 37 + search: Search, 36 38 }[type]; 37 39 if (!Component) return null; 40 + // Don't show Search column with no query, for now 41 + if (type === 'search' && !params.query) return null; 38 42 return ( 39 43 <Component key={type + JSON.stringify(params)} {...params} columnMode /> 40 44 );
+5
src/components/shortcuts-settings.css
··· 123 123 min-width: 0; 124 124 max-width: 320px; 125 125 } 126 + #shortcut-settings-form .form-note { 127 + display: flex; 128 + gap: 6px; 129 + align-items: center; 130 + } 126 131 #shortcut-settings-form form footer { 127 132 display: flex; 128 133 gap: 16px;
+34 -6
src/components/shortcuts-settings.jsx
··· 32 32 'list', 33 33 'public', 34 34 'trending', 35 - // NOTE: Hide for now 36 - // 'search', // Search on Mastodon ain't great 37 - // 'account-statuses', // Need @acct search first 35 + 'search', 38 36 'hashtag', 39 37 'bookmarks', 40 38 'favourites', 39 + // NOTE: Hide for now 40 + // 'account-statuses', // Need @acct search first 41 41 ]; 42 42 const TYPE_TEXT = { 43 43 following: 'Home / Following', ··· 87 87 text: 'Search term', 88 88 name: 'query', 89 89 type: 'text', 90 + placeholder: 'Optional, unless for multi-column mode', 91 + notRequired: true, 90 92 }, 91 93 ], 92 94 'account-statuses': [ ··· 168 170 }, 169 171 search: { 170 172 id: 'search', 171 - title: ({ query }) => query, 172 - path: ({ query }) => `/search?q=${query}`, 173 + title: ({ query }) => (query ? `"${query}"` : 'Search'), 174 + path: ({ query }) => 175 + query ? `/search?q=${query}&type=statuses` : '/search', 173 176 icon: 'search', 177 + excludeViewMode: ({ query }) => (!query ? ['multi-column'] : []), 174 178 }, 175 179 'account-statuses': { 176 180 id: 'account-statuses', ··· 279 283 const key = Object.values(shortcut).join('-'); 280 284 const { type } = shortcut; 281 285 if (!SHORTCUTS_META[type]) return null; 282 - let { icon, title, subtitle } = SHORTCUTS_META[type]; 286 + let { icon, title, subtitle, excludeViewMode } = 287 + SHORTCUTS_META[type]; 283 288 if (typeof title === 'function') { 284 289 title = title(shortcut, i); 285 290 } ··· 289 294 if (typeof icon === 'function') { 290 295 icon = icon(shortcut, i); 291 296 } 297 + if (typeof excludeViewMode === 'function') { 298 + excludeViewMode = excludeViewMode(shortcut, i); 299 + } 300 + const excludedViewMode = excludeViewMode?.includes( 301 + snapStates.settings.shortcutsViewMode, 302 + ); 292 303 return ( 293 304 <li key={key}> 294 305 <Icon icon={icon} /> ··· 299 310 {' '} 300 311 <small class="ib insignificant">{subtitle}</small> 301 312 </> 313 + )} 314 + {excludedViewMode && ( 315 + <span class="tag"> 316 + Not available in current view mode 317 + </span> 302 318 )} 303 319 </span> 304 320 <span class="shortcut-actions"> ··· 468 484 }, 469 485 ); 470 486 487 + const FORM_NOTES = { 488 + search: `For multi-column mode, search term is required, else the column will not be shown.`, 489 + hashtag: 'Multiple hashtags are supported. Space-separated.', 490 + }; 491 + 471 492 function ShortcutForm({ 472 493 onSubmit, 473 494 disabled, ··· 615 636 <span>{text}</span>{' '} 616 637 <input 617 638 type={type} 639 + switch={type === 'checkbox' || undefined} 618 640 name={name} 619 641 placeholder={placeholder} 620 642 required={type === 'text' && !notRequired} ··· 641 663 </p> 642 664 ); 643 665 }, 666 + )} 667 + {!!FORM_NOTES[currentType] && ( 668 + <p class="form-note insignificant"> 669 + <Icon icon="info" /> 670 + {FORM_NOTES[currentType]} 671 + </p> 644 672 )} 645 673 <footer> 646 674 <button
+40 -11
src/pages/search.css
··· 1 1 #search-page .deck > header .header-grid { 2 2 grid-template-columns: auto 1fr auto; 3 3 } 4 - #search-page header input { 5 - width: 100%; 6 - padding: 8px 16px; 7 - border: 0; 8 - border-radius: 999px; 9 - background-color: var(--bg-faded-color); 10 - border: 2px solid transparent; 4 + #search-page header { 5 + input { 6 + width: 100%; 7 + padding: 8px 16px; 8 + border: 0; 9 + border-radius: 999px; 10 + background-color: var(--bg-faded-color); 11 + border: 2px solid transparent; 12 + 13 + &:focus { 14 + outline: 0; 15 + background-color: var(--bg-color); 16 + border-color: var(--link-color); 17 + } 18 + 19 + #columns & { 20 + font-weight: bold; 21 + background-color: transparent; 22 + text-align: center; 23 + padding-inline: 8px; 24 + text-overflow: ellipsis; 25 + } 26 + } 11 27 } 12 - #search-page header input:focus { 13 - outline: 0; 14 - background-color: var(--bg-color); 15 - border-color: var(--link-color); 28 + 29 + #columns #search-page { 30 + .header-grid { 31 + .header-side { 32 + min-width: 40px; 33 + 34 + &:last-of-type { 35 + button { 36 + display: block; 37 + 38 + &:not(:hover, :focus) { 39 + color: var(--text-insignificant-color); 40 + } 41 + } 42 + } 43 + } 44 + } 16 45 } 17 46 18 47 #search-page ul.accounts-list {
+42 -6
src/pages/search.jsx
··· 16 16 import { api } from '../utils/api'; 17 17 import { fetchRelationships } from '../utils/relationships'; 18 18 import shortenNumber from '../utils/shorten-number'; 19 + import usePageVisibility from '../utils/usePageVisibility'; 20 + import useScroll from '../utils/useScroll'; 19 21 import useTitle from '../utils/useTitle'; 20 22 21 23 const SHORT_LIMIT = 5; 22 24 const LIMIT = 40; 25 + const emptySearchParams = new URLSearchParams(); 23 26 24 - function Search(props) { 25 - const params = useParams(); 27 + function Search({ columnMode, ...props }) { 28 + const params = columnMode ? {} : useParams(); 26 29 const { masto, instance, authenticated } = api({ 27 30 instance: params.instance, 28 31 }); 29 32 const [uiState, setUIState] = useState('default'); 30 - const [searchParams] = useSearchParams(); 33 + const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams(); 31 34 const searchFormRef = useRef(); 32 35 const q = props?.query || searchParams.get('q'); 33 - const type = props?.type || searchParams.get('type'); 36 + const type = columnMode 37 + ? 'statuses' 38 + : props?.type || searchParams.get('type'); 34 39 useTitle( 35 40 q 36 41 ? `Search: ${q}${ ··· 86 91 }; 87 92 88 93 function loadResults(firstLoad) { 94 + if (firstLoad) { 95 + offsetRef.current = 0; 96 + } 97 + 89 98 if (!firstLoad && !authenticated) { 90 99 // Search results pagination is only available to authenticated users 91 100 return; ··· 142 151 })(); 143 152 } 144 153 154 + const { reachStart } = useScroll({ 155 + scrollableRef, 156 + }); 157 + const lastHiddenTime = useRef(); 158 + usePageVisibility((visible) => { 159 + if (visible && reachStart) { 160 + const timeDiff = Date.now() - lastHiddenTime.current; 161 + if (!lastHiddenTime.current || timeDiff > 1000 * 3) { 162 + // 3 seconds 163 + loadResults(true); 164 + } else { 165 + lastHiddenTime.current = Date.now(); 166 + } 167 + } 168 + }); 169 + 145 170 useEffect(() => { 146 171 if (q) { 147 172 searchFormRef.current?.setValue?.(q); ··· 172 197 <NavMenu /> 173 198 </div> 174 199 <SearchForm ref={searchFormRef} /> 175 - <div class="header-side">&nbsp;</div> 200 + <div class="header-side"> 201 + <button 202 + type="button" 203 + class="plain" 204 + onClick={() => { 205 + loadResults(true); 206 + }} 207 + disabled={uiState === 'loading'} 208 + > 209 + <Icon icon="search" size="l" /> 210 + </button> 211 + </div> 176 212 </div> 177 213 </header> 178 214 <main> 179 - {!!q && ( 215 + {!!q && !columnMode && ( 180 216 <div 181 217 ref={filterBarParent} 182 218 class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}