phanpy/src/components/shortcuts-settings.jsx

608 lines
17 KiB
React
Raw Normal View History

2023-02-16 09:51:54 +00:00
import './shortcuts-settings.css';
import mem from 'mem';
2023-04-08 14:16:13 +00:00
import { useEffect, useRef, useState } from 'preact/hooks';
2023-02-16 09:51:54 +00:00
import { useSnapshot } from 'valtio';
2023-03-09 15:37:25 +00:00
import floatingButtonUrl from '../assets/floating-button.svg';
import multiColumnUrl from '../assets/multi-column.svg';
import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
2023-02-16 09:51:54 +00:00
import { api } from '../utils/api';
import states from '../utils/states';
import AsyncText from './AsyncText';
import Icon from './icon';
import Modal from './modal';
2023-02-16 09:51:54 +00:00
const SHORTCUTS_LIMIT = 9;
2023-02-16 09:51:54 +00:00
const TYPES = [
'following',
2023-04-06 11:32:26 +00:00
'mentions',
2023-02-16 09:51:54 +00:00
'notifications',
'list',
'public',
2023-04-06 11:32:26 +00:00
'trending',
2023-02-18 12:48:24 +00:00
// NOTE: Hide for now
// 'search', // Search on Mastodon ain't great
// 'account-statuses', // Need @acct search first
2023-04-06 11:32:26 +00:00
'hashtag',
2023-02-16 09:51:54 +00:00
'bookmarks',
'favourites',
];
const TYPE_TEXT = {
2023-02-18 12:48:24 +00:00
following: 'Home / Following',
2023-02-16 09:51:54 +00:00
notifications: 'Notifications',
list: 'List',
2023-04-06 10:57:20 +00:00
public: 'Public (Local / Federated)',
2023-02-16 09:51:54 +00:00
search: 'Search',
'account-statuses': 'Account',
bookmarks: 'Bookmarks',
favourites: 'Favourites',
hashtag: 'Hashtag',
trending: 'Trending',
2023-04-06 11:32:26 +00:00
mentions: 'Mentions',
2023-02-16 09:51:54 +00:00
};
const TYPE_PARAMS = {
list: [
{
text: 'List ID',
name: 'id',
},
],
public: [
{
text: 'Local only',
name: 'local',
type: 'checkbox',
},
{
text: 'Instance',
name: 'instance',
type: 'text',
placeholder: 'e.g. mastodon.social',
},
],
trending: [
{
text: 'Instance',
name: 'instance',
type: 'text',
placeholder: 'e.g. mastodon.social',
},
],
2023-02-16 09:51:54 +00:00
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',
2023-02-25 02:04:30 +00:00
placeholder: 'e.g. PixelArt (Max 5, space-separated)',
pattern: '[^#]+',
2023-02-16 09:51:54 +00:00
},
{
text: 'Instance',
name: 'instance',
type: 'text',
placeholder: 'Optional, e.g. mastodon.social',
notRequired: true,
},
2023-02-16 09:51:54 +00:00
],
};
export const SHORTCUTS_META = {
following: {
2023-02-27 16:35:07 +00:00
id: 'home',
2023-02-27 15:59:41 +00:00
title: (_, index) => (index === 0 ? 'Home' : 'Following'),
path: '/',
2023-02-16 09:51:54 +00:00
icon: 'home',
},
2023-04-06 11:32:26 +00:00
mentions: {
id: 'mentions',
title: 'Mentions',
path: '/mentions',
icon: 'at',
},
2023-02-16 09:51:54 +00:00
notifications: {
2023-02-27 15:59:41 +00:00
id: 'notifications',
2023-02-16 09:51:54 +00:00
title: 'Notifications',
path: '/notifications',
icon: 'notification',
},
list: {
2023-02-27 15:59:41 +00:00
id: 'list',
title: mem(
async ({ id }) => {
const list = await api().masto.v1.lists.fetch(id);
return list.title;
},
{
cacheKey: ([{ id }]) => id,
},
),
2023-02-16 09:51:54 +00:00
path: ({ id }) => `/l/${id}`,
icon: 'list',
},
public: {
2023-02-27 15:59:41 +00:00
id: 'public',
title: ({ local }) => (local ? 'Local' : 'Federated'),
subtitle: ({ instance }) => instance,
2023-02-16 09:51:54 +00:00
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',
},
2023-02-16 09:51:54 +00:00
search: {
2023-02-27 15:59:41 +00:00
id: 'search',
2023-02-16 09:51:54 +00:00
title: ({ query }) => query,
path: ({ query }) => `/search?q=${query}`,
icon: 'search',
},
'account-statuses': {
2023-02-27 15:59:41 +00:00
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,
},
),
2023-02-16 09:51:54 +00:00
path: ({ id }) => `/a/${id}`,
icon: 'user',
},
bookmarks: {
2023-02-27 15:59:41 +00:00
id: 'bookmarks',
2023-02-16 09:51:54 +00:00
title: 'Bookmarks',
path: '/b',
icon: 'bookmark',
},
favourites: {
2023-02-27 15:59:41 +00:00
id: 'favourites',
2023-02-16 09:51:54 +00:00
title: 'Favourites',
path: '/f',
icon: 'heart',
},
hashtag: {
2023-02-27 15:59:41 +00:00
id: 'hashtag',
2023-02-16 09:51:54 +00:00
title: ({ hashtag }) => hashtag,
subtitle: ({ instance }) => instance,
2023-02-25 06:58:11 +00:00
path: ({ hashtag }) => `/t/${hashtag.split(/\s+/).join('+')}`,
2023-02-16 09:51:54 +00:00
icon: 'hashtag',
},
};
function ShortcutsSettings() {
const snapStates = useSnapshot(states);
const { masto } = api();
const { shortcuts } = snapStates;
2023-02-16 09:51:54 +00:00
const [lists, setLists] = useState([]);
const [followedHashtags, setFollowedHashtags] = useState([]);
const [showForm, setShowForm] = useState(false);
2023-02-16 09:51:54 +00:00
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 (
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
<header>
<h2>
<Icon icon="shortcut" /> Shortcuts{' '}
<sup
style={{
fontSize: 12,
opacity: 0.5,
textTransform: 'uppercase',
}}
>
beta
</sup>
</h2>
</header>
<main>
<p>
2023-03-09 15:37:25 +00:00
Specify a list of shortcuts that'll appear&nbsp;as:
<div class="shortcuts-view-mode">
{[
{
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 }) => (
<label>
<input
type="radio"
name="shortcuts-view-mode"
value={value}
checked={
snapStates.settings.shortcutsViewMode === value ||
(value === 'float-button' &&
!snapStates.settings.shortcutsViewMode)
}
2023-03-09 15:37:25 +00:00
onChange={(e) => {
states.settings.shortcutsViewMode = e.target.value;
}}
/>{' '}
<img src={imgURL} alt="" width="80" height="58" />{' '}
<span>{label}</span>
2023-03-09 15:37:25 +00:00
</label>
))}
</div>
{/* <select
2023-02-27 15:59:41 +00:00
value={snapStates.settings.shortcutsViewMode || 'float-button'}
onChange={(e) => {
states.settings.shortcutsViewMode = e.target.value;
}}
>
<option value="float-button">Floating button</option>
<option value="multi-column">Multi-column</option>
<option value="tab-menu-bar">Tab/Menu bar </option>
2023-03-09 15:37:25 +00:00
</select> */}
2023-02-27 15:59:41 +00:00
</p>
{/* <p>
2023-02-18 12:48:24 +00:00
<details>
<summary class="insignificant">
Experimental Multi-column mode
</summary>
<label>
<input
type="checkbox"
checked={snapStates.settings.shortcutsColumnsMode}
onChange={(e) => {
states.settings.shortcutsColumnsMode = e.target.checked;
}}
/>{' '}
Show shortcuts in multiple columns instead of the floating button.
</label>
</details>
2023-02-27 15:59:41 +00:00
</p> */}
2023-02-18 12:48:24 +00:00
{shortcuts.length > 0 ? (
2023-02-16 09:51:54 +00:00
<ol class="shortcuts-list">
2023-04-08 14:16:13 +00:00
{shortcuts.filter(Boolean).map((shortcut, i) => {
2023-02-16 09:51:54 +00:00
const key = i + Object.values(shortcut);
const { type } = shortcut;
2023-02-19 02:42:56 +00:00
if (!SHORTCUTS_META[type]) return null;
let { icon, title, subtitle } = SHORTCUTS_META[type];
2023-02-16 09:51:54 +00:00
if (typeof title === 'function') {
2023-02-28 11:12:16 +00:00
title = title(shortcut, i);
2023-02-16 09:51:54 +00:00
}
if (typeof subtitle === 'function') {
subtitle = subtitle(shortcut, i);
}
2023-02-16 09:51:54 +00:00
if (typeof icon === 'function') {
2023-02-28 11:12:16 +00:00
icon = icon(shortcut, i);
2023-02-16 09:51:54 +00:00
}
return (
<li key={key}>
<Icon icon={icon} />
<span class="shortcut-text">
<AsyncText>{title}</AsyncText>
{subtitle && (
<>
{' '}
<small class="ib insignificant">{subtitle}</small>
</>
)}
2023-02-16 09:51:54 +00:00
</span>
<span class="shortcut-actions">
2023-02-16 09:51:54 +00:00
<button
type="button"
class="plain small"
disabled={i === 0}
onClick={() => {
const shortcutsArr = Array.from(states.shortcuts);
if (i > 0) {
const temp = states.shortcuts[i - 1];
shortcutsArr[i - 1] = shortcut;
shortcutsArr[i] = temp;
states.shortcuts = shortcutsArr;
}
}}
>
<Icon icon="arrow-up" alt="Move up" />
</button>
<button
type="button"
class="plain small"
disabled={i === shortcuts.length - 1}
2023-02-16 09:51:54 +00:00
onClick={() => {
const shortcutsArr = Array.from(states.shortcuts);
if (i < states.shortcuts.length - 1) {
const temp = states.shortcuts[i + 1];
shortcutsArr[i + 1] = shortcut;
shortcutsArr[i] = temp;
states.shortcuts = shortcutsArr;
}
}}
>
<Icon icon="arrow-down" alt="Move down" />
</button>
<button
2023-04-08 14:16:13 +00:00
type="button"
class="plain small"
onClick={() => {
setShowForm({
shortcut,
shortcutIndex: i,
});
}}
>
<Icon icon="pencil" alt="Edit" />
</button>
{/* <button
2023-02-16 09:51:54 +00:00
type="button"
class="plain small"
onClick={() => {
states.shortcuts.splice(i, 1);
}}
>
<Icon icon="x" alt="Remove" />
2023-04-08 14:16:13 +00:00
</button> */}
2023-02-16 09:51:54 +00:00
</span>
</li>
);
})}
</ol>
) : (
<p class="ui-state insignificant">
No shortcuts yet. Add one from the form below.
</p>
)}
<p
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
2023-02-16 09:51:54 +00:00
}}
>
<span class="insignificant">
{shortcuts.length >= SHORTCUTS_LIMIT &&
`Max ${SHORTCUTS_LIMIT} shortcuts`}
</span>
<button
type="button"
disabled={shortcuts.length >= SHORTCUTS_LIMIT}
onClick={() => setShowForm(true)}
>
<Icon icon="plus" /> <span>Add shortcut</span>
</button>
</p>
2023-02-16 09:51:54 +00:00
</main>
{showForm && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowForm(false);
}
}}
>
<ShortcutForm
2023-04-08 14:16:13 +00:00
shortcut={showForm.shortcut}
shortcutIndex={showForm.shortcutIndex}
disabled={shortcuts.length >= SHORTCUTS_LIMIT}
lists={lists}
followedHashtags={followedHashtags}
2023-04-08 14:16:13 +00:00
onSubmit={({ result, mode }) => {
console.log('onSubmit', result);
if (mode === 'edit') {
states.shortcuts[showForm.shortcutIndex] = result;
} else {
states.shortcuts.push(result);
}
}}
onClose={() => setShowForm(false)}
/>
</Modal>
)}
2023-02-16 09:51:54 +00:00
</div>
);
}
function ShortcutForm({
lists,
followedHashtags,
onSubmit,
disabled,
2023-04-08 14:16:13 +00:00
shortcut,
shortcutIndex,
onClose = () => {},
}) {
2023-04-08 14:16:13 +00:00
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 }) => {
const input = form.querySelector(`[name="${name}"]`);
if (input && shortcut[name]) {
input.value = shortcut[name];
}
});
}
}, [editMode, currentType]);
2023-02-16 09:51:54 +00:00
return (
<div id="shortcut-settings-form" class="sheet">
<header>
2023-04-08 14:16:13 +00:00
<h2>{editMode ? 'Edit' : 'Add'} shortcut</h2>
</header>
<main tabindex="-1">
<form
2023-04-08 14:16:13 +00:00
ref={formRef}
onSubmit={(e) => {
// Construct a nice object from form
e.preventDefault();
const data = new FormData(e.target);
const result = {};
data.forEach((value, key) => {
result[key] = value?.trim();
});
2023-04-08 14:16:13 +00:00
console.log('result', result);
if (!result.type) return;
2023-04-08 14:16:13 +00:00
onSubmit({
result,
mode: editMode ? 'edit' : 'add',
});
// Reset
e.target.reset();
setCurrentType(null);
onClose();
}}
>
<p>
<label>
<span>Timeline</span>
<select
required
disabled={disabled}
onChange={(e) => {
setCurrentType(e.target.value);
}}
2023-04-08 14:16:13 +00:00
defaultValue={editMode ? shortcut.type : undefined}
name="type"
>
<option></option>
{TYPES.map((type) => (
<option value={type}>{TYPE_TEXT[type]}</option>
))}
</select>
</label>
</p>
{TYPE_PARAMS[currentType]?.map?.(
({ text, name, type, placeholder, pattern, notRequired }) => {
if (currentType === 'list') {
return (
<p>
<label>
<span>List</span>
<select
name="id"
required={!notRequired}
disabled={disabled}
>
{lists.map((list) => (
<option value={list.id}>{list.title}</option>
))}
</select>
</label>
</p>
);
}
2023-02-16 09:51:54 +00:00
return (
<p>
<label>
<span>{text}</span>{' '}
<input
type={type}
name={name}
placeholder={placeholder}
required={type === 'text' && !notRequired}
disabled={disabled}
list={
currentType === 'hashtag'
? 'followed-hashtags-datalist'
: null
}
autocorrect="off"
autocapitalize="off"
spellcheck={false}
pattern={pattern}
/>
{currentType === 'hashtag' &&
followedHashtags.length > 0 && (
<datalist id="followed-hashtags-datalist">
{followedHashtags.map((tag) => (
<option value={tag.name} />
))}
</datalist>
)}
2023-02-16 09:51:54 +00:00
</label>
</p>
);
},
)}
2023-04-08 14:16:13 +00:00
<footer>
<button type="submit" class="block" disabled={disabled}>
{editMode ? 'Save' : 'Add'}
</button>
{editMode && (
<button
type="button"
class="light danger"
onClick={() => {
states.shortcuts.splice(shortcutIndex, 1);
onClose();
}}
>
Remove
</button>
)}
</footer>
</form>
</main>
</div>
2023-02-16 09:51:54 +00:00
);
}
export default ShortcutsSettings;