Experiment: allow Search in Shortcuts

This commit is contained in:
Lim Chee Aun 2023-12-22 18:01:41 +08:00
parent 6bcee318e4
commit da58336285
5 changed files with 125 additions and 23 deletions

View file

@ -9,6 +9,7 @@ import List from '../pages/list';
import Mentions from '../pages/mentions'; import Mentions from '../pages/mentions';
import Notifications from '../pages/notifications'; import Notifications from '../pages/notifications';
import Public from '../pages/public'; import Public from '../pages/public';
import Search from '../pages/search';
import Trending from '../pages/trending'; import Trending from '../pages/trending';
import states from '../utils/states'; import states from '../utils/states';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
@ -33,8 +34,11 @@ function Columns() {
hashtag: Hashtag, hashtag: Hashtag,
mentions: Mentions, mentions: Mentions,
trending: Trending, trending: Trending,
search: Search,
}[type]; }[type];
if (!Component) return null; if (!Component) return null;
// Don't show Search column with no query, for now
if (type === 'search' && !params.query) return null;
return ( return (
<Component key={type + JSON.stringify(params)} {...params} columnMode /> <Component key={type + JSON.stringify(params)} {...params} columnMode />
); );

View file

@ -123,6 +123,11 @@
min-width: 0; min-width: 0;
max-width: 320px; max-width: 320px;
} }
#shortcut-settings-form .form-note {
display: flex;
gap: 6px;
align-items: center;
}
#shortcut-settings-form form footer { #shortcut-settings-form form footer {
display: flex; display: flex;
gap: 16px; gap: 16px;

View file

@ -32,12 +32,12 @@ const TYPES = [
'list', 'list',
'public', 'public',
'trending', 'trending',
// NOTE: Hide for now 'search',
// 'search', // Search on Mastodon ain't great
// 'account-statuses', // Need @acct search first
'hashtag', 'hashtag',
'bookmarks', 'bookmarks',
'favourites', 'favourites',
// NOTE: Hide for now
// 'account-statuses', // Need @acct search first
]; ];
const TYPE_TEXT = { const TYPE_TEXT = {
following: 'Home / Following', following: 'Home / Following',
@ -87,6 +87,8 @@ const TYPE_PARAMS = {
text: 'Search term', text: 'Search term',
name: 'query', name: 'query',
type: 'text', type: 'text',
placeholder: 'Optional, unless for multi-column mode',
notRequired: true,
}, },
], ],
'account-statuses': [ 'account-statuses': [
@ -168,9 +170,11 @@ export const SHORTCUTS_META = {
}, },
search: { search: {
id: 'search', id: 'search',
title: ({ query }) => query, title: ({ query }) => (query ? `"${query}"` : 'Search'),
path: ({ query }) => `/search?q=${query}`, path: ({ query }) =>
query ? `/search?q=${query}&type=statuses` : '/search',
icon: 'search', icon: 'search',
excludeViewMode: ({ query }) => (!query ? ['multi-column'] : []),
}, },
'account-statuses': { 'account-statuses': {
id: 'account-statuses', id: 'account-statuses',
@ -279,7 +283,8 @@ function ShortcutsSettings({ onClose }) {
const key = Object.values(shortcut).join('-'); const key = Object.values(shortcut).join('-');
const { type } = shortcut; const { type } = shortcut;
if (!SHORTCUTS_META[type]) return null; if (!SHORTCUTS_META[type]) return null;
let { icon, title, subtitle } = SHORTCUTS_META[type]; let { icon, title, subtitle, excludeViewMode } =
SHORTCUTS_META[type];
if (typeof title === 'function') { if (typeof title === 'function') {
title = title(shortcut, i); title = title(shortcut, i);
} }
@ -289,6 +294,12 @@ function ShortcutsSettings({ onClose }) {
if (typeof icon === 'function') { if (typeof icon === 'function') {
icon = icon(shortcut, i); icon = icon(shortcut, i);
} }
if (typeof excludeViewMode === 'function') {
excludeViewMode = excludeViewMode(shortcut, i);
}
const excludedViewMode = excludeViewMode?.includes(
snapStates.settings.shortcutsViewMode,
);
return ( return (
<li key={key}> <li key={key}>
<Icon icon={icon} /> <Icon icon={icon} />
@ -300,6 +311,11 @@ function ShortcutsSettings({ onClose }) {
<small class="ib insignificant">{subtitle}</small> <small class="ib insignificant">{subtitle}</small>
</> </>
)} )}
{excludedViewMode && (
<span class="tag">
Not available in current view mode
</span>
)}
</span> </span>
<span class="shortcut-actions"> <span class="shortcut-actions">
<button <button
@ -468,6 +484,11 @@ const fetchLists = pmem(
}, },
); );
const FORM_NOTES = {
search: `For multi-column mode, search term is required, else the column will not be shown.`,
hashtag: 'Multiple hashtags are supported. Space-separated.',
};
function ShortcutForm({ function ShortcutForm({
onSubmit, onSubmit,
disabled, disabled,
@ -615,6 +636,7 @@ function ShortcutForm({
<span>{text}</span>{' '} <span>{text}</span>{' '}
<input <input
type={type} type={type}
switch={type === 'checkbox' || undefined}
name={name} name={name}
placeholder={placeholder} placeholder={placeholder}
required={type === 'text' && !notRequired} required={type === 'text' && !notRequired}
@ -642,6 +664,12 @@ function ShortcutForm({
); );
}, },
)} )}
{!!FORM_NOTES[currentType] && (
<p class="form-note insignificant">
<Icon icon="info" />
{FORM_NOTES[currentType]}
</p>
)}
<footer> <footer>
<button <button
type="submit" type="submit"

View file

@ -1,20 +1,49 @@
#search-page .deck > header .header-grid { #search-page .deck > header .header-grid {
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
} }
#search-page header input { #search-page header {
input {
width: 100%; width: 100%;
padding: 8px 16px; padding: 8px 16px;
border: 0; border: 0;
border-radius: 999px; border-radius: 999px;
background-color: var(--bg-faded-color); background-color: var(--bg-faded-color);
border: 2px solid transparent; border: 2px solid transparent;
}
#search-page header input:focus { &:focus {
outline: 0; outline: 0;
background-color: var(--bg-color); background-color: var(--bg-color);
border-color: var(--link-color); border-color: var(--link-color);
} }
#columns & {
font-weight: bold;
background-color: transparent;
text-align: center;
padding-inline: 8px;
text-overflow: ellipsis;
}
}
}
#columns #search-page {
.header-grid {
.header-side {
min-width: 40px;
&:last-of-type {
button {
display: block;
&:not(:hover, :focus) {
color: var(--text-insignificant-color);
}
}
}
}
}
}
#search-page ul.accounts-list { #search-page ul.accounts-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -16,21 +16,26 @@ import Status from '../components/status';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { fetchRelationships } from '../utils/relationships'; import { fetchRelationships } from '../utils/relationships';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import usePageVisibility from '../utils/usePageVisibility';
import useScroll from '../utils/useScroll';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const SHORT_LIMIT = 5; const SHORT_LIMIT = 5;
const LIMIT = 40; const LIMIT = 40;
const emptySearchParams = new URLSearchParams();
function Search(props) { function Search({ columnMode, ...props }) {
const params = useParams(); const params = columnMode ? {} : useParams();
const { masto, instance, authenticated } = api({ const { masto, instance, authenticated } = api({
instance: params.instance, instance: params.instance,
}); });
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
const [searchParams] = useSearchParams(); const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams();
const searchFormRef = useRef(); const searchFormRef = useRef();
const q = props?.query || searchParams.get('q'); const q = props?.query || searchParams.get('q');
const type = props?.type || searchParams.get('type'); const type = columnMode
? 'statuses'
: props?.type || searchParams.get('type');
useTitle( useTitle(
q q
? `Search: ${q}${ ? `Search: ${q}${
@ -86,6 +91,10 @@ function Search(props) {
}; };
function loadResults(firstLoad) { function loadResults(firstLoad) {
if (firstLoad) {
offsetRef.current = 0;
}
if (!firstLoad && !authenticated) { if (!firstLoad && !authenticated) {
// Search results pagination is only available to authenticated users // Search results pagination is only available to authenticated users
return; return;
@ -142,6 +151,22 @@ function Search(props) {
})(); })();
} }
const { reachStart } = useScroll({
scrollableRef,
});
const lastHiddenTime = useRef();
usePageVisibility((visible) => {
if (visible && reachStart) {
const timeDiff = Date.now() - lastHiddenTime.current;
if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
// 3 seconds
loadResults(true);
} else {
lastHiddenTime.current = Date.now();
}
}
});
useEffect(() => { useEffect(() => {
if (q) { if (q) {
searchFormRef.current?.setValue?.(q); searchFormRef.current?.setValue?.(q);
@ -172,11 +197,22 @@ function Search(props) {
<NavMenu /> <NavMenu />
</div> </div>
<SearchForm ref={searchFormRef} /> <SearchForm ref={searchFormRef} />
<div class="header-side">&nbsp;</div> <div class="header-side">
<button
type="button"
class="plain"
onClick={() => {
loadResults(true);
}}
disabled={uiState === 'loading'}
>
<Icon icon="search" size="l" />
</button>
</div>
</div> </div>
</header> </header>
<main> <main>
{!!q && ( {!!q && !columnMode && (
<div <div
ref={filterBarParent} ref={filterBarParent}
class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`} class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}