import './shortcuts-settings.css'; import mem from 'mem'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useSnapshot } from 'valtio'; import floatingButtonUrl from '../assets/floating-button.svg'; import multiColumnUrl from '../assets/multi-column.svg'; import tabMenuBarUrl from '../assets/tab-menu-bar.svg'; import { api } from '../utils/api'; import states from '../utils/states'; import AsyncText from './AsyncText'; import Icon from './icon'; import Modal from './modal'; const SHORTCUTS_LIMIT = 9; const TYPES = [ 'following', 'mentions', 'notifications', 'list', 'public', 'trending', // NOTE: Hide for now // 'search', // Search on Mastodon ain't great // 'account-statuses', // Need @acct search first 'hashtag', 'bookmarks', 'favourites', ]; const TYPE_TEXT = { following: 'Home / Following', notifications: 'Notifications', list: 'List', public: 'Public (Local / Federated)', search: 'Search', 'account-statuses': 'Account', bookmarks: 'Bookmarks', favourites: 'Favourites', hashtag: 'Hashtag', trending: 'Trending', mentions: 'Mentions', }; const TYPE_PARAMS = { list: [ { text: 'List ID', name: 'id', }, ], public: [ { text: 'Local only', name: 'local', type: 'checkbox', }, { text: 'Instance', name: 'instance', type: 'text', placeholder: 'Optional, e.g. mastodon.social', notRequired: true, }, ], trending: [ { text: 'Instance', name: 'instance', type: 'text', placeholder: 'Optional, e.g. mastodon.social', notRequired: true, }, ], search: [ { text: 'Search term', name: 'query', type: 'text', }, ], 'account-statuses': [ { text: '@', name: 'id', type: 'text', placeholder: 'cheeaun@mastodon.social', }, ], hashtag: [ { text: '#', name: 'hashtag', type: 'text', placeholder: 'e.g. PixelArt (Max 5, space-separated)', pattern: '[^#]+', }, { text: 'Instance', name: 'instance', type: 'text', placeholder: 'Optional, e.g. mastodon.social', notRequired: true, }, ], }; export const SHORTCUTS_META = { following: { id: 'home', title: (_, index) => (index === 0 ? 'Home' : 'Following'), path: '/', icon: 'home', }, mentions: { id: 'mentions', title: 'Mentions', path: '/mentions', icon: 'at', }, notifications: { id: 'notifications', title: 'Notifications', path: '/notifications', icon: 'notification', }, list: { id: 'list', title: mem( async ({ id }) => { const list = await api().masto.v1.lists.fetch(id); return list.title; }, { cacheKey: ([{ id }]) => id, }, ), path: ({ id }) => `/l/${id}`, icon: 'list', }, public: { id: 'public', title: ({ local }) => (local ? 'Local' : 'Federated'), subtitle: ({ instance }) => instance, path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`, icon: ({ local }) => (local ? 'group' : 'earth'), }, trending: { id: 'trending', title: 'Trending', subtitle: ({ instance }) => instance, path: ({ instance }) => `/${instance}/trending`, icon: 'chart', }, search: { id: 'search', title: ({ query }) => query, path: ({ query }) => `/search?q=${query}`, icon: 'search', }, 'account-statuses': { id: 'account-statuses', title: mem( async ({ id }) => { const account = await api().masto.v1.accounts.fetch(id); return account.username || account.acct || account.displayName; }, { cacheKey: ([{ id }]) => id, }, ), path: ({ id }) => `/a/${id}`, icon: 'user', }, bookmarks: { id: 'bookmarks', title: 'Bookmarks', path: '/b', icon: 'bookmark', }, favourites: { id: 'favourites', title: 'Favourites', path: '/f', icon: 'heart', }, hashtag: { id: 'hashtag', title: ({ hashtag }) => hashtag, subtitle: ({ instance }) => instance, path: ({ hashtag, instance }) => `${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}`, icon: 'hashtag', }, }; function ShortcutsSettings() { const snapStates = useSnapshot(states); const { masto } = api(); const { shortcuts } = snapStates; const [lists, setLists] = useState([]); const [followedHashtags, setFollowedHashtags] = useState([]); const [showForm, setShowForm] = useState(false); useEffect(() => { (async () => { try { const lists = await masto.v1.lists.list(); setLists(lists); } catch (e) { console.error(e); } })(); (async () => { try { const iterator = masto.v1.followedTags.list(); const tags = []; do { const { value, done } = await iterator.next(); if (done || value?.length === 0) break; tags.push(...value); } while (true); setFollowedHashtags(tags); } catch (e) { console.error(e); } })(); }, []); return (

Shortcuts{' '} beta

Specify a list of shortcuts that'll appear as:

{[ { value: 'float-button', label: 'Floating button', imgURL: floatingButtonUrl, }, { value: 'tab-menu-bar', label: 'Tab/Menu bar', imgURL: tabMenuBarUrl, }, { value: 'multi-column', label: 'Multi-column', imgURL: multiColumnUrl, }, ].map(({ value, label, imgURL }) => ( ))}
{/* */}

{/*

Experimental Multi-column mode

*/} {shortcuts.length > 0 ? (
    {shortcuts.filter(Boolean).map((shortcut, i) => { const key = i + Object.values(shortcut); const { type } = shortcut; if (!SHORTCUTS_META[type]) return null; let { icon, title, subtitle } = SHORTCUTS_META[type]; if (typeof title === 'function') { title = title(shortcut, i); } if (typeof subtitle === 'function') { subtitle = subtitle(shortcut, i); } if (typeof icon === 'function') { icon = icon(shortcut, i); } return (
  1. {title} {subtitle && ( <> {' '} {subtitle} )} {/* */}
  2. ); })}
) : (

No shortcuts yet. Tap on the Add shortcut button.

Not sure what to add?
Try adding{' '} { e.preventDefault(); states.shortcuts = [ { type: 'following', }, { type: 'notifications', }, ]; }} > Home / Following and Notifications {' '} first.

)}

{shortcuts.length >= SHORTCUTS_LIMIT && `Max ${SHORTCUTS_LIMIT} shortcuts`}

{showForm && ( { if (e.target === e.currentTarget) { setShowForm(false); } }} > = SHORTCUTS_LIMIT} lists={lists} followedHashtags={followedHashtags} onSubmit={({ result, mode }) => { console.log('onSubmit', result); if (mode === 'edit') { states.shortcuts[showForm.shortcutIndex] = result; } else { states.shortcuts.push(result); } }} onClose={() => setShowForm(false)} /> )}
); } function ShortcutForm({ lists, followedHashtags, onSubmit, disabled, shortcut, shortcutIndex, onClose = () => {}, }) { console.log('shortcut', shortcut); const editMode = !!shortcut; const [currentType, setCurrentType] = useState(shortcut?.type || null); const formRef = useRef(); useEffect(() => { if (editMode && currentType && TYPE_PARAMS[currentType]) { // Populate form const form = formRef.current; TYPE_PARAMS[currentType].forEach(({ name, type }) => { const input = form.querySelector(`[name="${name}"]`); if (input && shortcut[name]) { if (type === 'checkbox') { input.checked = shortcut[name] === 'on' ? true : false; } else { input.value = shortcut[name]; } } }); } }, [editMode, currentType]); return (

{editMode ? 'Edit' : 'Add'} shortcut

{ // Construct a nice object from form e.preventDefault(); const data = new FormData(e.target); const result = {}; data.forEach((value, key) => { result[key] = value?.trim(); }); console.log('result', result); if (!result.type) return; onSubmit({ result, mode: editMode ? 'edit' : 'add', }); // Reset e.target.reset(); setCurrentType(null); onClose(); }} >

{TYPE_PARAMS[currentType]?.map?.( ({ text, name, type, placeholder, pattern, notRequired }) => { if (currentType === 'list') { return (

); } return (

); }, )}
); } export default ShortcutsSettings;