2023-02-16 12:51:54 +03:00
|
|
|
|
import './shortcuts-settings.css';
|
|
|
|
|
|
2023-10-23 03:42:40 +03:00
|
|
|
|
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
2023-08-15 15:14:09 +03:00
|
|
|
|
import {
|
|
|
|
|
compressToEncodedURIComponent,
|
|
|
|
|
decompressFromEncodedURIComponent,
|
|
|
|
|
} from 'lz-string';
|
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
2023-02-16 12:51:54 +03:00
|
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
|
2023-03-09 18:37:25 +03:00
|
|
|
|
import floatingButtonUrl from '../assets/floating-button.svg';
|
|
|
|
|
import multiColumnUrl from '../assets/multi-column.svg';
|
|
|
|
|
import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
|
2023-10-03 10:07:47 +03:00
|
|
|
|
|
2023-02-16 12:51:54 +03:00
|
|
|
|
import { api } from '../utils/api';
|
2023-12-14 20:58:29 +03:00
|
|
|
|
import { fetchFollowedTags } from '../utils/followed-tags';
|
2024-03-23 18:52:05 +03:00
|
|
|
|
import { getLists, getListTitle } from '../utils/lists';
|
2023-10-14 15:33:40 +03:00
|
|
|
|
import pmem from '../utils/pmem';
|
2023-08-15 15:14:09 +03:00
|
|
|
|
import showToast from '../utils/show-toast';
|
2023-02-16 12:51:54 +03:00
|
|
|
|
import states from '../utils/states';
|
2024-02-26 09:00:53 +03:00
|
|
|
|
import store from '../utils/store';
|
2023-02-16 12:51:54 +03:00
|
|
|
|
|
|
|
|
|
import AsyncText from './AsyncText';
|
|
|
|
|
import Icon from './icon';
|
2023-08-15 15:14:09 +03:00
|
|
|
|
import MenuConfirm from './menu-confirm';
|
2023-03-01 12:48:52 +03:00
|
|
|
|
import Modal from './modal';
|
2023-02-16 12:51:54 +03:00
|
|
|
|
|
2023-11-01 05:00:05 +03:00
|
|
|
|
export const SHORTCUTS_LIMIT = 9;
|
2023-02-18 14:39:17 +03:00
|
|
|
|
|
2023-02-16 12:51:54 +03:00
|
|
|
|
const TYPES = [
|
|
|
|
|
'following',
|
2023-04-06 14:32:26 +03:00
|
|
|
|
'mentions',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
'notifications',
|
|
|
|
|
'list',
|
|
|
|
|
'public',
|
2023-04-06 14:32:26 +03:00
|
|
|
|
'trending',
|
2023-12-22 13:01:41 +03:00
|
|
|
|
'search',
|
2023-04-06 14:32:26 +03:00
|
|
|
|
'hashtag',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
'bookmarks',
|
|
|
|
|
'favourites',
|
2023-12-22 13:01:41 +03:00
|
|
|
|
// NOTE: Hide for now
|
|
|
|
|
// 'account-statuses', // Need @acct search first
|
2023-02-16 12:51:54 +03:00
|
|
|
|
];
|
|
|
|
|
const TYPE_TEXT = {
|
2023-02-18 15:48:24 +03:00
|
|
|
|
following: 'Home / Following',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
notifications: 'Notifications',
|
2024-03-23 18:52:05 +03:00
|
|
|
|
list: 'Lists',
|
2023-04-06 13:57:20 +03:00
|
|
|
|
public: 'Public (Local / Federated)',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
search: 'Search',
|
|
|
|
|
'account-statuses': 'Account',
|
|
|
|
|
bookmarks: 'Bookmarks',
|
2023-10-25 08:55:12 +03:00
|
|
|
|
favourites: 'Likes',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
hashtag: 'Hashtag',
|
2023-04-05 20:23:56 +03:00
|
|
|
|
trending: 'Trending',
|
2023-04-06 14:32:26 +03:00
|
|
|
|
mentions: 'Mentions',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
};
|
|
|
|
|
const TYPE_PARAMS = {
|
|
|
|
|
list: [
|
|
|
|
|
{
|
|
|
|
|
text: 'List ID',
|
|
|
|
|
name: 'id',
|
2024-03-23 18:52:05 +03:00
|
|
|
|
notRequired: true,
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
public: [
|
|
|
|
|
{
|
|
|
|
|
text: 'Local only',
|
|
|
|
|
name: 'local',
|
|
|
|
|
type: 'checkbox',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
text: 'Instance',
|
|
|
|
|
name: 'instance',
|
|
|
|
|
type: 'text',
|
2023-04-14 06:13:14 +03:00
|
|
|
|
placeholder: 'Optional, e.g. mastodon.social',
|
|
|
|
|
notRequired: true,
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
|
|
|
|
],
|
2023-04-05 20:23:56 +03:00
|
|
|
|
trending: [
|
|
|
|
|
{
|
|
|
|
|
text: 'Instance',
|
|
|
|
|
name: 'instance',
|
|
|
|
|
type: 'text',
|
2023-04-14 06:13:14 +03:00
|
|
|
|
placeholder: 'Optional, e.g. mastodon.social',
|
|
|
|
|
notRequired: true,
|
2023-04-05 20:23:56 +03:00
|
|
|
|
},
|
|
|
|
|
],
|
2023-02-16 12:51:54 +03:00
|
|
|
|
search: [
|
|
|
|
|
{
|
|
|
|
|
text: 'Search term',
|
|
|
|
|
name: 'query',
|
|
|
|
|
type: 'text',
|
2023-12-22 13:01:41 +03:00
|
|
|
|
placeholder: 'Optional, unless for multi-column mode',
|
|
|
|
|
notRequired: true,
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
'account-statuses': [
|
|
|
|
|
{
|
|
|
|
|
text: '@',
|
|
|
|
|
name: 'id',
|
|
|
|
|
type: 'text',
|
|
|
|
|
placeholder: 'cheeaun@mastodon.social',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
hashtag: [
|
|
|
|
|
{
|
|
|
|
|
text: '#',
|
|
|
|
|
name: 'hashtag',
|
|
|
|
|
type: 'text',
|
2023-02-25 05:04:30 +03:00
|
|
|
|
placeholder: 'e.g. PixelArt (Max 5, space-separated)',
|
|
|
|
|
pattern: '[^#]+',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
2023-10-29 16:41:03 +03:00
|
|
|
|
{
|
|
|
|
|
text: 'Media only',
|
|
|
|
|
name: 'media',
|
|
|
|
|
type: 'checkbox',
|
|
|
|
|
},
|
2023-04-08 15:42:38 +03:00
|
|
|
|
{
|
|
|
|
|
text: 'Instance',
|
|
|
|
|
name: 'instance',
|
|
|
|
|
type: 'text',
|
|
|
|
|
placeholder: 'Optional, e.g. mastodon.social',
|
|
|
|
|
notRequired: true,
|
|
|
|
|
},
|
2023-02-16 12:51:54 +03:00
|
|
|
|
],
|
|
|
|
|
};
|
2023-11-03 20:11:29 +03:00
|
|
|
|
const fetchAccountTitle = pmem(async ({ id }) => {
|
|
|
|
|
const account = await api().masto.v1.accounts.$select(id).fetch();
|
|
|
|
|
return account.username || account.acct || account.displayName;
|
|
|
|
|
});
|
2023-02-16 12:51:54 +03:00
|
|
|
|
export const SHORTCUTS_META = {
|
|
|
|
|
following: {
|
2023-02-27 19:35:07 +03:00
|
|
|
|
id: 'home',
|
2023-02-27 18:59:41 +03:00
|
|
|
|
title: (_, index) => (index === 0 ? 'Home' : 'Following'),
|
|
|
|
|
path: '/',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
icon: 'home',
|
|
|
|
|
},
|
2023-04-06 14:32:26 +03:00
|
|
|
|
mentions: {
|
|
|
|
|
id: 'mentions',
|
|
|
|
|
title: 'Mentions',
|
|
|
|
|
path: '/mentions',
|
|
|
|
|
icon: 'at',
|
|
|
|
|
},
|
2023-02-16 12:51:54 +03:00
|
|
|
|
notifications: {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'notifications',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
title: 'Notifications',
|
|
|
|
|
path: '/notifications',
|
|
|
|
|
icon: 'notification',
|
|
|
|
|
},
|
|
|
|
|
list: {
|
2024-03-23 18:52:05 +03:00
|
|
|
|
id: ({ id }) => (id ? 'list' : 'lists'),
|
|
|
|
|
title: ({ id }) => (id ? getListTitle(id) : 'Lists'),
|
|
|
|
|
path: ({ id }) => (id ? `/l/${id}` : '/l'),
|
2023-02-16 12:51:54 +03:00
|
|
|
|
icon: 'list',
|
2024-03-23 18:52:05 +03:00
|
|
|
|
excludeViewMode: ({ id }) => (!id ? ['multi-column'] : []),
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
|
|
|
|
public: {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'public',
|
2023-04-08 15:42:38 +03:00
|
|
|
|
title: ({ local }) => (local ? 'Local' : 'Federated'),
|
2023-04-17 12:38:53 +03:00
|
|
|
|
subtitle: ({ instance }) => instance || api().instance,
|
2023-02-16 12:51:54 +03:00
|
|
|
|
path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`,
|
2023-12-28 06:57:48 +03:00
|
|
|
|
icon: ({ local }) => (local ? 'building' : 'earth'),
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
2023-04-05 20:23:56 +03:00
|
|
|
|
trending: {
|
|
|
|
|
id: 'trending',
|
|
|
|
|
title: 'Trending',
|
2023-04-17 12:38:53 +03:00
|
|
|
|
subtitle: ({ instance }) => instance || api().instance,
|
2023-04-05 20:23:56 +03:00
|
|
|
|
path: ({ instance }) => `/${instance}/trending`,
|
|
|
|
|
icon: 'chart',
|
|
|
|
|
},
|
2023-02-16 12:51:54 +03:00
|
|
|
|
search: {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'search',
|
2024-01-19 20:45:36 +03:00
|
|
|
|
title: ({ query }) => (query ? `“${query}”` : 'Search'),
|
2023-12-22 13:01:41 +03:00
|
|
|
|
path: ({ query }) =>
|
2024-01-04 17:00:27 +03:00
|
|
|
|
query
|
|
|
|
|
? `/search?q=${encodeURIComponent(query)}&type=statuses`
|
|
|
|
|
: '/search',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
icon: 'search',
|
2023-12-22 13:01:41 +03:00
|
|
|
|
excludeViewMode: ({ query }) => (!query ? ['multi-column'] : []),
|
2023-02-16 12:51:54 +03:00
|
|
|
|
},
|
|
|
|
|
'account-statuses': {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'account-statuses',
|
2023-11-03 20:11:29 +03:00
|
|
|
|
title: fetchAccountTitle,
|
2023-02-16 12:51:54 +03:00
|
|
|
|
path: ({ id }) => `/a/${id}`,
|
|
|
|
|
icon: 'user',
|
|
|
|
|
},
|
|
|
|
|
bookmarks: {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'bookmarks',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
title: 'Bookmarks',
|
|
|
|
|
path: '/b',
|
|
|
|
|
icon: 'bookmark',
|
|
|
|
|
},
|
|
|
|
|
favourites: {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'favourites',
|
2023-10-25 08:55:12 +03:00
|
|
|
|
title: 'Likes',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
path: '/f',
|
|
|
|
|
icon: 'heart',
|
|
|
|
|
},
|
|
|
|
|
hashtag: {
|
2023-02-27 18:59:41 +03:00
|
|
|
|
id: 'hashtag',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
title: ({ hashtag }) => hashtag,
|
2023-04-17 12:38:53 +03:00
|
|
|
|
subtitle: ({ instance }) => instance || api().instance,
|
2023-10-29 16:41:03 +03:00
|
|
|
|
path: ({ hashtag, instance, media }) =>
|
|
|
|
|
`${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}${
|
|
|
|
|
media ? '?media=1' : ''
|
|
|
|
|
}`,
|
2023-02-16 12:51:54 +03:00
|
|
|
|
icon: 'hashtag',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2023-04-20 11:10:57 +03:00
|
|
|
|
function ShortcutsSettings({ onClose }) {
|
2023-02-16 12:51:54 +03:00
|
|
|
|
const snapStates = useSnapshot(states);
|
2023-02-18 14:39:17 +03:00
|
|
|
|
const { shortcuts } = snapStates;
|
2023-03-01 12:48:52 +03:00
|
|
|
|
const [showForm, setShowForm] = useState(false);
|
2023-08-15 15:14:09 +03:00
|
|
|
|
const [showImportExport, setShowImportExport] = useState(false);
|
2023-02-16 12:51:54 +03:00
|
|
|
|
|
2023-10-23 03:42:40 +03:00
|
|
|
|
const [shortcutsListParent] = useAutoAnimate();
|
|
|
|
|
|
2023-02-16 12:51:54 +03:00
|
|
|
|
return (
|
|
|
|
|
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
|
2023-04-20 11:10:57 +03:00
|
|
|
|
{!!onClose && (
|
|
|
|
|
<button type="button" class="sheet-close" onClick={onClose}>
|
|
|
|
|
<Icon icon="x" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2023-02-16 12:51:54 +03:00
|
|
|
|
<header>
|
|
|
|
|
<h2>
|
|
|
|
|
<Icon icon="shortcut" /> Shortcuts{' '}
|
|
|
|
|
<sup
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
opacity: 0.5,
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
beta
|
|
|
|
|
</sup>
|
|
|
|
|
</h2>
|
|
|
|
|
</header>
|
|
|
|
|
<main>
|
2023-10-19 15:50:32 +03:00
|
|
|
|
<p>Specify a list of shortcuts that'll appear as:</p>
|
|
|
|
|
<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,
|
|
|
|
|
},
|
2023-10-24 04:58:41 +03:00
|
|
|
|
].map(({ value, label, imgURL }) => {
|
|
|
|
|
const checked =
|
|
|
|
|
snapStates.settings.shortcutsViewMode === value ||
|
|
|
|
|
(value === 'float-button' &&
|
|
|
|
|
!snapStates.settings.shortcutsViewMode);
|
|
|
|
|
return (
|
|
|
|
|
<label key={value} class={checked ? 'checked' : ''}>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
name="shortcuts-view-mode"
|
|
|
|
|
value={value}
|
|
|
|
|
checked={checked}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
states.settings.shortcutsViewMode = e.target.value;
|
|
|
|
|
}}
|
|
|
|
|
/>{' '}
|
|
|
|
|
<img src={imgURL} alt="" width="80" height="58" />{' '}
|
|
|
|
|
<span>{label}</span>
|
|
|
|
|
</label>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2023-10-19 15:50:32 +03:00
|
|
|
|
</div>
|
2023-02-18 15:48:24 +03:00
|
|
|
|
{shortcuts.length > 0 ? (
|
2024-01-10 09:48:29 +03:00
|
|
|
|
<>
|
|
|
|
|
<ol class="shortcuts-list" ref={shortcutsListParent}>
|
|
|
|
|
{shortcuts.filter(Boolean).map((shortcut, i) => {
|
|
|
|
|
// const key = i + Object.values(shortcut);
|
|
|
|
|
const key = Object.values(shortcut).join('-');
|
|
|
|
|
const { type } = shortcut;
|
|
|
|
|
if (!SHORTCUTS_META[type]) return null;
|
|
|
|
|
let { icon, title, subtitle, excludeViewMode } =
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
if (typeof excludeViewMode === 'function') {
|
|
|
|
|
excludeViewMode = excludeViewMode(shortcut, i);
|
|
|
|
|
}
|
|
|
|
|
const excludedViewMode = excludeViewMode?.includes(
|
|
|
|
|
snapStates.settings.shortcutsViewMode,
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<li key={key}>
|
|
|
|
|
<Icon icon={icon} />
|
|
|
|
|
<span class="shortcut-text">
|
|
|
|
|
<AsyncText>{title}</AsyncText>
|
|
|
|
|
{subtitle && (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
<small class="ib insignificant">{subtitle}</small>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{excludedViewMode && (
|
|
|
|
|
<span class="tag">
|
|
|
|
|
Not available in current view mode
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="shortcut-actions">
|
|
|
|
|
<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}
|
|
|
|
|
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
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain small"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowForm({
|
|
|
|
|
shortcut,
|
|
|
|
|
shortcutIndex: i,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="pencil" alt="Edit" />
|
|
|
|
|
</button>
|
|
|
|
|
{/* <button
|
2023-02-16 12:51:54 +03:00
|
|
|
|
type="button"
|
|
|
|
|
class="plain small"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
states.shortcuts.splice(i, 1);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="x" alt="Remove" />
|
2023-04-08 17:16:13 +03:00
|
|
|
|
</button> */}
|
2024-01-10 09:48:29 +03:00
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ol>
|
|
|
|
|
{shortcuts.length === 1 &&
|
|
|
|
|
snapStates.settings.shortcutsViewMode !== 'float-button' && (
|
|
|
|
|
<div class="ui-state insignificant">
|
|
|
|
|
<Icon icon="info" />{' '}
|
|
|
|
|
<small>
|
|
|
|
|
Add more than one shortcut/column to make this work.
|
|
|
|
|
</small>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2023-02-16 12:51:54 +03:00
|
|
|
|
) : (
|
2023-04-10 15:23:58 +03:00
|
|
|
|
<div class="ui-state insignificant">
|
2024-03-02 05:08:10 +03:00
|
|
|
|
<p>
|
|
|
|
|
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
|
|
|
|
? 'No columns yet. Tap on the Add column button.'
|
|
|
|
|
: 'No shortcuts yet. Tap on the Add shortcut button.'}
|
|
|
|
|
</p>
|
2023-04-10 15:23:58 +03:00
|
|
|
|
<p>
|
|
|
|
|
Not sure what to add?
|
|
|
|
|
<br />
|
|
|
|
|
Try adding{' '}
|
|
|
|
|
<a
|
|
|
|
|
href="#"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
states.shortcuts = [
|
|
|
|
|
{
|
|
|
|
|
type: 'following',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'notifications',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Home / Following and Notifications
|
|
|
|
|
</a>{' '}
|
|
|
|
|
first.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2023-02-16 12:51:54 +03:00
|
|
|
|
)}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
<p class="insignificant">
|
|
|
|
|
{shortcuts.length >= SHORTCUTS_LIMIT &&
|
2024-03-02 05:08:10 +03:00
|
|
|
|
(snapStates.settings.shortcutsViewMode === 'multi-column'
|
|
|
|
|
? `Max ${SHORTCUTS_LIMIT} columns`
|
|
|
|
|
: `Max ${SHORTCUTS_LIMIT} shortcuts`)}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
</p>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
<p
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
2023-02-16 12:51:54 +03:00
|
|
|
|
}}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
>
|
2023-08-15 15:14:09 +03:00
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="light"
|
|
|
|
|
onClick={() => setShowImportExport(true)}
|
|
|
|
|
>
|
|
|
|
|
Import/export
|
|
|
|
|
</button>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={shortcuts.length >= SHORTCUTS_LIMIT}
|
|
|
|
|
onClick={() => setShowForm(true)}
|
|
|
|
|
>
|
2024-01-10 09:48:08 +03:00
|
|
|
|
<Icon icon="plus" />{' '}
|
|
|
|
|
<span>
|
|
|
|
|
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
|
|
|
|
? 'Add column…'
|
|
|
|
|
: 'Add shortcut…'}
|
|
|
|
|
</span>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
</button>
|
|
|
|
|
</p>
|
2023-02-16 12:51:54 +03:00
|
|
|
|
</main>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
{showForm && (
|
|
|
|
|
<Modal
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
setShowForm(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ShortcutForm
|
2023-04-08 17:16:13 +03:00
|
|
|
|
shortcut={showForm.shortcut}
|
|
|
|
|
shortcutIndex={showForm.shortcutIndex}
|
|
|
|
|
onSubmit={({ result, mode }) => {
|
|
|
|
|
console.log('onSubmit', result);
|
|
|
|
|
if (mode === 'edit') {
|
|
|
|
|
states.shortcuts[showForm.shortcutIndex] = result;
|
|
|
|
|
} else {
|
|
|
|
|
states.shortcuts.push(result);
|
|
|
|
|
}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
}}
|
|
|
|
|
onClose={() => setShowForm(false)}
|
|
|
|
|
/>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
{showImportExport && (
|
|
|
|
|
<Modal
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
setShowImportExport(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ImportExport
|
|
|
|
|
shortcuts={shortcuts}
|
|
|
|
|
onClose={() => setShowImportExport(false)}
|
|
|
|
|
/>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
2023-02-16 12:51:54 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-22 13:01:41 +03:00
|
|
|
|
const FORM_NOTES = {
|
2024-03-23 18:52:05 +03:00
|
|
|
|
list: `Specific list is optional. For multi-column mode, list is required, else the column will not be shown.`,
|
2023-12-22 13:01:41 +03:00
|
|
|
|
search: `For multi-column mode, search term is required, else the column will not be shown.`,
|
|
|
|
|
hashtag: 'Multiple hashtags are supported. Space-separated.',
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-01 12:48:52 +03:00
|
|
|
|
function ShortcutForm({
|
|
|
|
|
onSubmit,
|
|
|
|
|
disabled,
|
2023-04-08 17:16:13 +03:00
|
|
|
|
shortcut,
|
|
|
|
|
shortcutIndex,
|
2023-04-20 11:10:57 +03:00
|
|
|
|
onClose,
|
2023-03-01 12:48:52 +03:00
|
|
|
|
}) {
|
2023-04-08 17:16:13 +03:00
|
|
|
|
console.log('shortcut', shortcut);
|
|
|
|
|
const editMode = !!shortcut;
|
|
|
|
|
const [currentType, setCurrentType] = useState(shortcut?.type || null);
|
2023-11-06 18:31:00 +03:00
|
|
|
|
const { masto } = api();
|
2023-04-08 17:16:13 +03:00
|
|
|
|
|
2023-11-03 20:11:29 +03:00
|
|
|
|
const [uiState, setUIState] = useState('default');
|
|
|
|
|
const [lists, setLists] = useState([]);
|
|
|
|
|
const [followedHashtags, setFollowedHashtags] = useState([]);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
(async () => {
|
|
|
|
|
if (currentType !== 'list') return;
|
|
|
|
|
try {
|
|
|
|
|
setUIState('loading');
|
2024-03-23 18:52:05 +03:00
|
|
|
|
const lists = await getLists();
|
2023-11-03 20:11:29 +03:00
|
|
|
|
setLists(lists);
|
|
|
|
|
setUIState('default');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
setUIState('error');
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
if (currentType !== 'hashtag') return;
|
|
|
|
|
try {
|
2023-12-14 20:58:29 +03:00
|
|
|
|
const tags = await fetchFollowedTags();
|
2023-11-03 20:11:29 +03:00
|
|
|
|
setFollowedHashtags(tags);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}, [currentType]);
|
|
|
|
|
|
2023-04-08 17:16:13 +03:00
|
|
|
|
const formRef = useRef();
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (editMode && currentType && TYPE_PARAMS[currentType]) {
|
|
|
|
|
// Populate form
|
|
|
|
|
const form = formRef.current;
|
2023-04-08 17:37:05 +03:00
|
|
|
|
TYPE_PARAMS[currentType].forEach(({ name, type }) => {
|
2023-04-08 17:16:13 +03:00
|
|
|
|
const input = form.querySelector(`[name="${name}"]`);
|
|
|
|
|
if (input && shortcut[name]) {
|
2023-04-08 17:37:05 +03:00
|
|
|
|
if (type === 'checkbox') {
|
|
|
|
|
input.checked = shortcut[name] === 'on' ? true : false;
|
|
|
|
|
} else {
|
|
|
|
|
input.value = shortcut[name];
|
|
|
|
|
}
|
2023-04-08 17:16:13 +03:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [editMode, currentType]);
|
|
|
|
|
|
2023-02-16 12:51:54 +03:00
|
|
|
|
return (
|
2023-03-01 12:48:52 +03:00
|
|
|
|
<div id="shortcut-settings-form" class="sheet">
|
2023-04-20 11:10:57 +03:00
|
|
|
|
{!!onClose && (
|
|
|
|
|
<button type="button" class="sheet-close" onClick={onClose}>
|
|
|
|
|
<Icon icon="x" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
<header>
|
2023-04-08 17:16:13 +03:00
|
|
|
|
<h2>{editMode ? 'Edit' : 'Add'} shortcut</h2>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
</header>
|
|
|
|
|
<main tabindex="-1">
|
|
|
|
|
<form
|
2023-04-08 17:16:13 +03:00
|
|
|
|
ref={formRef}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
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-19 05:42:26 +03:00
|
|
|
|
if (key === 'instance') {
|
|
|
|
|
// Remove protocol and trailing slash
|
|
|
|
|
result[key] = result[key]
|
|
|
|
|
.replace(/^https?:\/\//, '')
|
|
|
|
|
.replace(/\/+$/, '');
|
|
|
|
|
// Remove @acct@ or acct@ from instance URL
|
|
|
|
|
result[key] = result[key].replace(/^@?[^@]+@/, '');
|
|
|
|
|
}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
});
|
2023-04-08 17:16:13 +03:00
|
|
|
|
console.log('result', result);
|
2023-03-01 12:48:52 +03:00
|
|
|
|
if (!result.type) return;
|
2023-04-08 17:16:13 +03:00
|
|
|
|
onSubmit({
|
|
|
|
|
result,
|
|
|
|
|
mode: editMode ? 'edit' : 'add',
|
|
|
|
|
});
|
2023-03-01 12:48:52 +03:00
|
|
|
|
// Reset
|
|
|
|
|
e.target.reset();
|
|
|
|
|
setCurrentType(null);
|
2023-04-20 11:10:57 +03:00
|
|
|
|
onClose?.();
|
2023-03-01 12:48:52 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<p>
|
|
|
|
|
<label>
|
|
|
|
|
<span>Timeline</span>
|
|
|
|
|
<select
|
|
|
|
|
required
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setCurrentType(e.target.value);
|
|
|
|
|
}}
|
2023-04-08 17:16:13 +03:00
|
|
|
|
defaultValue={editMode ? shortcut.type : undefined}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
name="type"
|
|
|
|
|
>
|
|
|
|
|
<option></option>
|
|
|
|
|
{TYPES.map((type) => (
|
|
|
|
|
<option value={type}>{TYPE_TEXT[type]}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</p>
|
|
|
|
|
{TYPE_PARAMS[currentType]?.map?.(
|
2023-04-08 15:42:38 +03:00
|
|
|
|
({ text, name, type, placeholder, pattern, notRequired }) => {
|
2023-03-01 12:48:52 +03:00
|
|
|
|
if (currentType === 'list') {
|
|
|
|
|
return (
|
|
|
|
|
<p>
|
|
|
|
|
<label>
|
|
|
|
|
<span>List</span>
|
2023-04-08 15:42:38 +03:00
|
|
|
|
<select
|
|
|
|
|
name="id"
|
|
|
|
|
required={!notRequired}
|
2023-11-03 20:11:29 +03:00
|
|
|
|
disabled={disabled || uiState === 'loading'}
|
|
|
|
|
defaultValue={editMode ? shortcut.id : undefined}
|
2023-04-08 15:42:38 +03:00
|
|
|
|
>
|
2024-03-23 18:52:05 +03:00
|
|
|
|
<option value=""></option>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
{lists.map((list) => (
|
|
|
|
|
<option value={list.id}>{list.title}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</p>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-16 12:51:54 +03:00
|
|
|
|
return (
|
|
|
|
|
<p>
|
|
|
|
|
<label>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
<span>{text}</span>{' '}
|
|
|
|
|
<input
|
|
|
|
|
type={type}
|
2023-12-22 13:01:41 +03:00
|
|
|
|
switch={type === 'checkbox' || undefined}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
name={name}
|
|
|
|
|
placeholder={placeholder}
|
2023-04-08 15:42:38 +03:00
|
|
|
|
required={type === 'text' && !notRequired}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
disabled={disabled}
|
|
|
|
|
list={
|
|
|
|
|
currentType === 'hashtag'
|
|
|
|
|
? 'followed-hashtags-datalist'
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
autocorrect="off"
|
|
|
|
|
autocapitalize="off"
|
2024-03-07 11:33:56 +03:00
|
|
|
|
spellCheck={false}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
pattern={pattern}
|
|
|
|
|
/>
|
|
|
|
|
{currentType === 'hashtag' &&
|
|
|
|
|
followedHashtags.length > 0 && (
|
|
|
|
|
<datalist id="followed-hashtags-datalist">
|
|
|
|
|
{followedHashtags.map((tag) => (
|
|
|
|
|
<option value={tag.name} />
|
|
|
|
|
))}
|
|
|
|
|
</datalist>
|
|
|
|
|
)}
|
2023-02-16 12:51:54 +03:00
|
|
|
|
</label>
|
|
|
|
|
</p>
|
|
|
|
|
);
|
2023-03-01 12:48:52 +03:00
|
|
|
|
},
|
|
|
|
|
)}
|
2023-12-22 13:01:41 +03:00
|
|
|
|
{!!FORM_NOTES[currentType] && (
|
|
|
|
|
<p class="form-note insignificant">
|
|
|
|
|
<Icon icon="info" />
|
|
|
|
|
{FORM_NOTES[currentType]}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2023-04-08 17:16:13 +03:00
|
|
|
|
<footer>
|
2023-11-03 20:11:29 +03:00
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
class="block"
|
|
|
|
|
disabled={disabled || uiState === 'loading'}
|
|
|
|
|
>
|
2023-04-08 17:16:13 +03:00
|
|
|
|
{editMode ? 'Save' : 'Add'}
|
|
|
|
|
</button>
|
|
|
|
|
{editMode && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="light danger"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
states.shortcuts.splice(shortcutIndex, 1);
|
2023-04-20 11:10:57 +03:00
|
|
|
|
onClose?.();
|
2023-04-08 17:16:13 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Remove
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</footer>
|
2023-03-01 12:48:52 +03:00
|
|
|
|
</form>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
2023-02-16 12:51:54 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
2023-03-01 12:48:52 +03:00
|
|
|
|
|
2023-08-15 15:14:09 +03:00
|
|
|
|
function ImportExport({ shortcuts, onClose }) {
|
2024-02-26 09:00:53 +03:00
|
|
|
|
const { masto } = api();
|
2023-08-16 11:39:22 +03:00
|
|
|
|
const shortcutsStr = useMemo(() => {
|
|
|
|
|
if (!shortcuts) return '';
|
|
|
|
|
if (!shortcuts.filter(Boolean).length) return '';
|
|
|
|
|
return compressToEncodedURIComponent(
|
|
|
|
|
JSON.stringify(shortcuts.filter(Boolean)),
|
|
|
|
|
);
|
|
|
|
|
}, [shortcuts]);
|
2023-08-15 15:14:09 +03:00
|
|
|
|
const [importShortcutStr, setImportShortcutStr] = useState('');
|
|
|
|
|
const [importUIState, setImportUIState] = useState('default');
|
|
|
|
|
const parsedImportShortcutStr = useMemo(() => {
|
|
|
|
|
if (!importShortcutStr) {
|
|
|
|
|
setImportUIState('default');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(
|
|
|
|
|
decompressFromEncodedURIComponent(importShortcutStr),
|
|
|
|
|
);
|
|
|
|
|
// Very basic validation, I know
|
|
|
|
|
if (!Array.isArray(parsed)) throw new Error('Not an array');
|
|
|
|
|
setImportUIState('default');
|
|
|
|
|
return parsed;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Fallback to JSON string parsing
|
|
|
|
|
// There's a chance that someone might want to import a JSON string instead of the compressed version
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(importShortcutStr);
|
|
|
|
|
if (!Array.isArray(parsed)) throw new Error('Not an array');
|
|
|
|
|
setImportUIState('default');
|
|
|
|
|
return parsed;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setImportUIState('error');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [importShortcutStr]);
|
|
|
|
|
const hasCurrentSettings = states.shortcuts.length > 0;
|
|
|
|
|
|
2024-02-26 09:00:53 +03:00
|
|
|
|
const shortcutsImportFieldRef = useRef();
|
|
|
|
|
|
2023-08-15 15:14:09 +03:00
|
|
|
|
return (
|
|
|
|
|
<div id="import-export-container" class="sheet">
|
|
|
|
|
{!!onClose && (
|
|
|
|
|
<button type="button" class="sheet-close" onClick={onClose}>
|
|
|
|
|
<Icon icon="x" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<header>
|
|
|
|
|
<h2>
|
2023-08-15 17:40:58 +03:00
|
|
|
|
Import/Export <small class="ib insignificant">Shortcuts</small>
|
2023-08-15 15:14:09 +03:00
|
|
|
|
</h2>
|
|
|
|
|
</header>
|
|
|
|
|
<main tabindex="-1">
|
|
|
|
|
<section>
|
|
|
|
|
<h3>
|
|
|
|
|
<Icon icon="arrow-down-circle" size="l" class="insignificant" />{' '}
|
|
|
|
|
<span>Import</span>
|
|
|
|
|
</h3>
|
2024-02-26 09:00:53 +03:00
|
|
|
|
<p class="field-button">
|
2023-08-15 15:14:09 +03:00
|
|
|
|
<input
|
2024-02-26 09:00:53 +03:00
|
|
|
|
ref={shortcutsImportFieldRef}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
type="text"
|
|
|
|
|
name="import"
|
2023-08-15 17:40:58 +03:00
|
|
|
|
placeholder="Paste shortcuts here"
|
2023-08-15 15:14:09 +03:00
|
|
|
|
class="block"
|
|
|
|
|
onInput={(e) => {
|
|
|
|
|
setImportShortcutStr(e.target.value);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2024-02-26 09:00:53 +03:00
|
|
|
|
{states.settings.shortcutSettingsCloudImportExport && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain2 small"
|
|
|
|
|
disabled={importUIState === 'cloud-downloading'}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
setImportUIState('cloud-downloading');
|
|
|
|
|
const currentAccount = store.session.get('currentAccount');
|
|
|
|
|
showToast(
|
|
|
|
|
'Downloading saved shortcuts from instance server…',
|
|
|
|
|
);
|
|
|
|
|
try {
|
|
|
|
|
const relationships =
|
|
|
|
|
await masto.v1.accounts.relationships.fetch({
|
|
|
|
|
id: [currentAccount],
|
|
|
|
|
});
|
|
|
|
|
const relationship = relationships[0];
|
|
|
|
|
if (relationship) {
|
|
|
|
|
const { note = '' } = relationship;
|
|
|
|
|
if (
|
|
|
|
|
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
|
|
|
|
|
note,
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
const settings = note.match(
|
|
|
|
|
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
|
|
|
|
|
)[1];
|
|
|
|
|
const { v, dt, data } = JSON.parse(settings);
|
|
|
|
|
shortcutsImportFieldRef.current.value = data;
|
|
|
|
|
shortcutsImportFieldRef.current.dispatchEvent(
|
|
|
|
|
new Event('input'),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setImportUIState('default');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
setImportUIState('error');
|
|
|
|
|
showToast('Unable to download shortcuts');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
title="Download shortcuts from instance server"
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="cloud" />
|
|
|
|
|
<Icon icon="arrow-down" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
</p>
|
|
|
|
|
{!!parsedImportShortcutStr &&
|
|
|
|
|
Array.isArray(parsedImportShortcutStr) && (
|
|
|
|
|
<>
|
|
|
|
|
<p>
|
|
|
|
|
<b>{parsedImportShortcutStr.length}</b> shortcut
|
|
|
|
|
{parsedImportShortcutStr.length > 1 ? 's' : ''}{' '}
|
|
|
|
|
<small class="insignificant">
|
|
|
|
|
({importShortcutStr.length} characters)
|
|
|
|
|
</small>
|
|
|
|
|
</p>
|
|
|
|
|
<ol class="import-settings-list">
|
|
|
|
|
{parsedImportShortcutStr.map((shortcut) => (
|
|
|
|
|
<li>
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
opacity: shortcuts.some((s) =>
|
|
|
|
|
// Compare all properties
|
|
|
|
|
Object.keys(s).every(
|
|
|
|
|
(key) => s[key] === shortcut[key],
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
? 1
|
|
|
|
|
: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
*
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
{TYPE_TEXT[shortcut.type]}
|
|
|
|
|
{shortcut.type === 'list' && ' ⚠️'}{' '}
|
|
|
|
|
{TYPE_PARAMS[shortcut.type]?.map?.(
|
|
|
|
|
({ text, name, type }) =>
|
|
|
|
|
shortcut[name] ? (
|
|
|
|
|
<>
|
|
|
|
|
<span class="tag collapsed insignificant">
|
|
|
|
|
{text}:{' '}
|
|
|
|
|
{type === 'checkbox'
|
|
|
|
|
? shortcut[name] === 'on'
|
|
|
|
|
? '✅'
|
|
|
|
|
: '❌'
|
|
|
|
|
: shortcut[name]}
|
|
|
|
|
</span>{' '}
|
|
|
|
|
</>
|
|
|
|
|
) : null,
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ol>
|
|
|
|
|
<p>
|
2023-08-15 17:40:58 +03:00
|
|
|
|
<small>* Exists in current shortcuts</small>
|
2023-08-15 15:14:09 +03:00
|
|
|
|
<br />
|
|
|
|
|
<small>
|
|
|
|
|
⚠️ List may not work if it's from a different account.
|
|
|
|
|
</small>
|
|
|
|
|
</p>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{importUIState === 'error' && (
|
|
|
|
|
<p class="error">
|
|
|
|
|
<small>⚠️ Invalid settings format</small>
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<p>
|
|
|
|
|
{hasCurrentSettings && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuConfirm
|
2023-08-15 17:40:58 +03:00
|
|
|
|
confirmLabel="Append to current shortcuts?"
|
2023-08-15 15:14:09 +03:00
|
|
|
|
menuFooter={
|
|
|
|
|
<div class="footer">
|
2023-08-15 17:40:58 +03:00
|
|
|
|
Only shortcuts that don’t exist in current shortcuts will
|
2023-08-15 15:14:09 +03:00
|
|
|
|
be appended.
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
// states.shortcuts = [
|
|
|
|
|
// ...states.shortcuts,
|
|
|
|
|
// ...parsedImportShortcutStr,
|
|
|
|
|
// ];
|
|
|
|
|
// Append non-unique shortcuts only
|
|
|
|
|
const nonUniqueShortcuts = parsedImportShortcutStr.filter(
|
|
|
|
|
(shortcut) =>
|
|
|
|
|
!states.shortcuts.some((s) =>
|
|
|
|
|
// Compare all properties
|
|
|
|
|
Object.keys(s).every(
|
|
|
|
|
(key) => s[key] === shortcut[key],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
if (!nonUniqueShortcuts.length) {
|
|
|
|
|
showToast('No new shortcuts to import');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let newShortcuts = [
|
|
|
|
|
...states.shortcuts,
|
|
|
|
|
...nonUniqueShortcuts,
|
|
|
|
|
];
|
|
|
|
|
const exceededLimit = newShortcuts.length > SHORTCUTS_LIMIT;
|
|
|
|
|
if (exceededLimit) {
|
|
|
|
|
// If exceeded, trim it
|
|
|
|
|
newShortcuts = newShortcuts.slice(0, SHORTCUTS_LIMIT);
|
|
|
|
|
}
|
|
|
|
|
states.shortcuts = newShortcuts;
|
|
|
|
|
showToast(
|
|
|
|
|
exceededLimit
|
2023-08-15 17:40:58 +03:00
|
|
|
|
? `Shortcuts imported. Exceeded max ${SHORTCUTS_LIMIT}, so the rest are not imported.`
|
|
|
|
|
: 'Shortcuts imported',
|
2023-08-15 15:14:09 +03:00
|
|
|
|
);
|
|
|
|
|
onClose?.();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain2"
|
|
|
|
|
disabled={!parsedImportShortcutStr}
|
|
|
|
|
>
|
|
|
|
|
Import & append…
|
|
|
|
|
</button>
|
|
|
|
|
</MenuConfirm>{' '}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<MenuConfirm
|
|
|
|
|
confirmLabel={
|
|
|
|
|
hasCurrentSettings
|
2023-08-15 17:40:58 +03:00
|
|
|
|
? 'Override current shortcuts?'
|
|
|
|
|
: 'Import shortcuts?'
|
2023-08-15 15:14:09 +03:00
|
|
|
|
}
|
|
|
|
|
menuItemClassName={hasCurrentSettings ? 'danger' : undefined}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
states.shortcuts = parsedImportShortcutStr;
|
2023-08-15 17:40:58 +03:00
|
|
|
|
showToast('Shortcuts imported');
|
2023-08-15 15:14:09 +03:00
|
|
|
|
onClose?.();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain2"
|
|
|
|
|
disabled={!parsedImportShortcutStr}
|
|
|
|
|
>
|
|
|
|
|
{hasCurrentSettings ? 'or override…' : 'Import…'}
|
|
|
|
|
</button>
|
|
|
|
|
</MenuConfirm>
|
|
|
|
|
</p>
|
|
|
|
|
</section>
|
|
|
|
|
<section>
|
|
|
|
|
<h3>
|
|
|
|
|
<Icon icon="arrow-up-circle" size="l" class="insignificant" />{' '}
|
|
|
|
|
<span>Export</span>
|
|
|
|
|
</h3>
|
|
|
|
|
<p>
|
|
|
|
|
<input
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
type="text"
|
|
|
|
|
value={shortcutsStr}
|
|
|
|
|
readOnly
|
|
|
|
|
onClick={(e) => {
|
2023-08-16 11:39:22 +03:00
|
|
|
|
if (!e.target.value) return;
|
2023-08-15 15:14:09 +03:00
|
|
|
|
e.target.select();
|
|
|
|
|
// Copy url to clipboard
|
|
|
|
|
try {
|
|
|
|
|
navigator.clipboard.writeText(e.target.value);
|
2023-08-15 17:40:58 +03:00
|
|
|
|
showToast('Shortcuts copied');
|
2023-08-15 15:14:09 +03:00
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
2023-08-15 17:40:58 +03:00
|
|
|
|
showToast('Unable to copy shortcuts');
|
2023-08-15 15:14:09 +03:00
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</p>
|
|
|
|
|
<p>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain2"
|
2023-08-16 11:39:22 +03:00
|
|
|
|
disabled={!shortcutsStr}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
onClick={() => {
|
|
|
|
|
try {
|
|
|
|
|
navigator.clipboard.writeText(shortcutsStr);
|
|
|
|
|
showToast('Shortcut settings copied');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
showToast('Unable to copy shortcut settings');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="clipboard" /> <span>Copy</span>
|
|
|
|
|
</button>{' '}
|
|
|
|
|
{navigator?.share &&
|
|
|
|
|
navigator?.canShare?.({
|
|
|
|
|
text: shortcutsStr,
|
|
|
|
|
}) && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain2"
|
2023-08-16 11:39:22 +03:00
|
|
|
|
disabled={!shortcutsStr}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
onClick={() => {
|
|
|
|
|
try {
|
|
|
|
|
navigator.share({
|
|
|
|
|
text: shortcutsStr,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
alert("Sharing doesn't seem to work.");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="share" /> <span>Share</span>
|
|
|
|
|
</button>
|
|
|
|
|
)}{' '}
|
2024-02-26 09:00:53 +03:00
|
|
|
|
{states.settings.shortcutSettingsCloudImportExport && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="plain2"
|
|
|
|
|
disabled={importUIState === 'cloud-uploading'}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
setImportUIState('cloud-uploading');
|
|
|
|
|
const currentAccount = store.session.get('currentAccount');
|
|
|
|
|
try {
|
|
|
|
|
const relationships =
|
|
|
|
|
await masto.v1.accounts.relationships.fetch({
|
|
|
|
|
id: [currentAccount],
|
|
|
|
|
});
|
|
|
|
|
const relationship = relationships[0];
|
|
|
|
|
if (relationship) {
|
|
|
|
|
const { note = '' } = relationship;
|
|
|
|
|
// const newNote = `${note}\n\n\n$<phanpy-shortcuts-settings>{shortcutsStr}</phanpy-shortcuts-settings>`;
|
|
|
|
|
let newNote = '';
|
|
|
|
|
if (
|
|
|
|
|
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
|
|
|
|
|
note,
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
const settingsJSON = JSON.stringify({
|
|
|
|
|
v: '1', // version
|
|
|
|
|
dt: Date.now(), // datetime stamp
|
|
|
|
|
data: shortcutsStr, // shortcuts settings string
|
|
|
|
|
});
|
|
|
|
|
newNote = note.replace(
|
|
|
|
|
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
|
|
|
|
|
`<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
newNote = `${note}\n\n\n<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`;
|
|
|
|
|
}
|
|
|
|
|
showToast('Saving shortcuts to instance server…');
|
|
|
|
|
await masto.v1.accounts
|
|
|
|
|
.$select(currentAccount)
|
|
|
|
|
.note.create({
|
|
|
|
|
comment: newNote,
|
|
|
|
|
});
|
|
|
|
|
setImportUIState('default');
|
|
|
|
|
showToast('Shortcuts saved');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
setImportUIState('error');
|
|
|
|
|
showToast('Unable to save shortcuts');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
title="Sync to instance server"
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="cloud" />
|
|
|
|
|
<Icon icon="arrow-up" />
|
|
|
|
|
</button>
|
|
|
|
|
)}{' '}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
{shortcutsStr.length > 0 && (
|
2024-02-26 09:00:53 +03:00
|
|
|
|
<small class="insignificant ib">
|
2023-08-15 15:14:09 +03:00
|
|
|
|
{shortcutsStr.length} characters
|
|
|
|
|
</small>
|
|
|
|
|
)}
|
|
|
|
|
</p>
|
2023-08-16 11:39:22 +03:00
|
|
|
|
{!!shortcutsStr && (
|
|
|
|
|
<details>
|
|
|
|
|
<summary class="insignificant">
|
|
|
|
|
<small>Raw Shortcuts JSON</small>
|
|
|
|
|
</summary>
|
|
|
|
|
<textarea style={{ width: '100%' }} rows={10} readOnly>
|
|
|
|
|
{JSON.stringify(shortcuts.filter(Boolean), null, 2)}
|
|
|
|
|
</textarea>
|
|
|
|
|
</details>
|
|
|
|
|
)}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
</section>
|
2024-02-26 09:00:53 +03:00
|
|
|
|
{states.settings.shortcutSettingsCloudImportExport && (
|
|
|
|
|
<footer>
|
|
|
|
|
<p>
|
|
|
|
|
<Icon icon="cloud" /> Import/export settings from/to instance
|
|
|
|
|
server (Very experimental)
|
|
|
|
|
</p>
|
|
|
|
|
</footer>
|
|
|
|
|
)}
|
2023-08-15 15:14:09 +03:00
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 12:48:52 +03:00
|
|
|
|
export default ShortcutsSettings;
|