diff --git a/src/app.css b/src/app.css
index c55b9a2f..6f5e2a68 100644
--- a/src/app.css
+++ b/src/app.css
@@ -2288,10 +2288,10 @@ ul.link-list li a .icon {
filter: none !important;
}
.nav-menu-button .avatar {
- transition: box-shadow 0.3s ease-out;
+ box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--link-light-color) !important;
}
.nav-menu-button:is(:hover, :focus, .active) .avatar {
- box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--link-light-color);
+ box-shadow: 0 0 0 2px var(--bg-color), 0 0 0 4px var(--link-color) !important;
}
.nav-menu-button.with-avatar .icon {
position: absolute;
diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx
index 0128be01..13402ff6 100644
--- a/src/components/account-info.jsx
+++ b/src/components/account-info.jsx
@@ -14,6 +14,7 @@ import { api } from '../utils/api';
import enhanceContent from '../utils/enhance-content';
import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links';
+import { getLists } from '../utils/lists';
import niceDateTime from '../utils/nice-date-time';
import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number';
@@ -1558,13 +1559,12 @@ function AddRemoveListsSheet({ accountID, onClose }) {
setUIState('loading');
(async () => {
try {
- const lists = await masto.v1.lists.list();
- lists.sort((a, b) => a.title.localeCompare(b.title));
+ const lists = await getLists();
+ setLists(lists);
const listsContainingAccount = await masto.v1.accounts
.$select(accountID)
.lists.list();
console.log({ lists, listsContainingAccount });
- setLists(lists);
setListsContainingAccount(listsContainingAccount);
setUIState('default');
} catch (e) {
diff --git a/src/components/columns.jsx b/src/components/columns.jsx
index 47036dce..f21e1165 100644
--- a/src/components/columns.jsx
+++ b/src/components/columns.jsx
@@ -39,6 +39,8 @@ function Columns() {
if (!Component) return null;
// Don't show Search column with no query, for now
if (type === 'search' && !params.query) return null;
+ // Don't show List column with no list, for now
+ if (type === 'list' && !params.id) return null;
return (
);
diff --git a/src/components/list-add-edit.jsx b/src/components/list-add-edit.jsx
index 3bf6ff03..7360a7f2 100644
--- a/src/components/list-add-edit.jsx
+++ b/src/components/list-add-edit.jsx
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api';
+import { addListStore, deleteListStore, updateListStore } from '../utils/lists';
import supports from '../utils/supports';
import Icon from './icon';
@@ -75,6 +76,14 @@ function ListAddEdit({ list, onClose }) {
state: 'success',
list: listResult,
});
+
+ setTimeout(() => {
+ if (editMode) {
+ updateListStore(listResult);
+ } else {
+ addListStore(listResult);
+ }
+ }, 1);
} catch (e) {
console.error(e);
setUIState('error');
@@ -146,6 +155,9 @@ function ListAddEdit({ list, onClose }) {
onClose?.({
state: 'deleted',
});
+ setTimeout(() => {
+ deleteListStore(list.id);
+ }, 1);
} catch (e) {
console.error(e);
setUIState('error');
diff --git a/src/components/nav-menu.jsx b/src/components/nav-menu.jsx
index 0d4fe98c..ba40dd94 100644
--- a/src/components/nav-menu.jsx
+++ b/src/components/nav-menu.jsx
@@ -7,11 +7,12 @@ import {
SubMenu,
} from '@szhsin/react-menu';
import { memo } from 'preact/compat';
-import { useEffect, useRef, useState } from 'preact/hooks';
+import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useLongPress } from 'use-long-press';
import { useSnapshot } from 'valtio';
import { api } from '../utils/api';
+import { getLists } from '../utils/lists';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import states from '../utils/states';
import store from '../utils/store';
@@ -24,16 +25,12 @@ function NavMenu(props) {
const snapStates = useSnapshot(states);
const { masto, instance, authenticated } = api();
- const [currentAccount, setCurrentAccount] = useState();
- const [moreThanOneAccount, setMoreThanOneAccount] = useState(false);
-
- useEffect(() => {
+ const [currentAccount, moreThanOneAccount] = useMemo(() => {
const accounts = store.local.getJSON('accounts') || [];
const acc = accounts.find(
(account) => account.info.id === store.session.get('currentAccount'),
);
- if (acc) setCurrentAccount(acc);
- setMoreThanOneAccount(accounts.length > 1);
+ return [acc, accounts.length > 1];
}, []);
// Home = Following
@@ -89,6 +86,13 @@ function NavMenu(props) {
return results;
}
+ const [lists, setLists] = useState([]);
+ useEffect(() => {
+ if (menuState === 'open') {
+ getLists().then(setLists);
+ }
+ }, [menuState === 'open']);
+
const buttonClickTS = useRef();
return (
<>
@@ -97,7 +101,7 @@ function NavMenu(props) {
type="button"
class={`button plain nav-menu-button ${
moreThanOneAccount ? 'with-avatar' : ''
- } ${open ? 'active' : ''}`}
+ } ${menuState === 'open' ? 'active' : ''}`}
style={{ position: 'relative' }}
onClick={() => {
buttonClickTS.current = Date.now();
@@ -203,9 +207,38 @@ function NavMenu(props) {
Profile
)}
-
- Lists
-
+ {lists?.length > 0 ? (
+
+
+
+
+ >
+ }
+ >
+
+ All Lists
+
+ {lists?.length > 0 && (
+ <>
+
+ {lists.map((list) => (
+
+ {list.title}
+
+ ))}
+ >
+ )}
+
+ ) : (
+
+
+ Lists
+
+ )}
Bookmarks
diff --git a/src/components/shortcuts-settings.jsx b/src/components/shortcuts-settings.jsx
index 94162dff..5ccfdb4c 100644
--- a/src/components/shortcuts-settings.jsx
+++ b/src/components/shortcuts-settings.jsx
@@ -14,6 +14,7 @@ import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
import { api } from '../utils/api';
import { fetchFollowedTags } from '../utils/followed-tags';
+import { getLists, getListTitle } from '../utils/lists';
import pmem from '../utils/pmem';
import showToast from '../utils/show-toast';
import states from '../utils/states';
@@ -43,7 +44,7 @@ const TYPES = [
const TYPE_TEXT = {
following: 'Home / Following',
notifications: 'Notifications',
- list: 'List',
+ list: 'Lists',
public: 'Public (Local / Federated)',
search: 'Search',
'account-statuses': 'Account',
@@ -58,6 +59,7 @@ const TYPE_PARAMS = {
{
text: 'List ID',
name: 'id',
+ notRequired: true,
},
],
public: [
@@ -122,10 +124,6 @@ const TYPE_PARAMS = {
},
],
};
-const fetchListTitle = pmem(async ({ id }) => {
- const list = await api().masto.v1.lists.$select(id).fetch();
- return list.title;
-});
const fetchAccountTitle = pmem(async ({ id }) => {
const account = await api().masto.v1.accounts.$select(id).fetch();
return account.username || account.acct || account.displayName;
@@ -150,10 +148,11 @@ export const SHORTCUTS_META = {
icon: 'notification',
},
list: {
- id: 'list',
- title: fetchListTitle,
- path: ({ id }) => `/l/${id}`,
+ id: ({ id }) => (id ? 'list' : 'lists'),
+ title: ({ id }) => (id ? getListTitle(id) : 'Lists'),
+ path: ({ id }) => (id ? `/l/${id}` : '/l'),
icon: 'list',
+ excludeViewMode: ({ id }) => (!id ? ['multi-column'] : []),
},
public: {
id: 'public',
@@ -496,18 +495,8 @@ function ShortcutsSettings({ onClose }) {
);
}
-const FETCH_MAX_AGE = 1000 * 60; // 1 minute
-const fetchLists = pmem(
- () => {
- const { masto } = api();
- return masto.v1.lists.list();
- },
- {
- maxAge: FETCH_MAX_AGE,
- },
-);
-
const FORM_NOTES = {
+ list: `Specific list is optional. For multi-column mode, list is required, else the column will not be shown.`,
search: `For multi-column mode, search term is required, else the column will not be shown.`,
hashtag: 'Multiple hashtags are supported. Space-separated.',
};
@@ -532,8 +521,7 @@ function ShortcutForm({
if (currentType !== 'list') return;
try {
setUIState('loading');
- const lists = await fetchLists();
- lists.sort((a, b) => a.title.localeCompare(b.title));
+ const lists = await getLists();
setLists(lists);
setUIState('default');
} catch (e) {
@@ -644,6 +632,7 @@ function ShortcutForm({
disabled={disabled || uiState === 'loading'}
defaultValue={editMode ? shortcut.id : undefined}
>
+
{lists.map((list) => (
))}
diff --git a/src/components/shortcuts.jsx b/src/components/shortcuts.jsx
index fbb3520d..c4645c7b 100644
--- a/src/components/shortcuts.jsx
+++ b/src/components/shortcuts.jsx
@@ -1,14 +1,15 @@
import './shortcuts.css';
-import { Menu, MenuItem } from '@szhsin/react-menu';
+import { MenuDivider, SubMenu } from '@szhsin/react-menu';
import { memo } from 'preact/compat';
-import { useMemo, useRef } from 'preact/hooks';
+import { useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { useSnapshot } from 'valtio';
import { SHORTCUTS_META } from '../components/shortcuts-settings';
import { api } from '../utils/api';
+import { getLists } from '../utils/lists';
import states from '../utils/states';
import AsyncText from './AsyncText';
@@ -34,47 +35,48 @@ function Shortcuts() {
const menuRef = useRef();
- const formattedShortcuts = useMemo(
- () =>
- shortcuts
- .map((pin, i) => {
- const { type, ...data } = pin;
- if (!SHORTCUTS_META[type]) return null;
- let { id, path, title, subtitle, icon } = SHORTCUTS_META[type];
+ const hasLists = useRef(false);
+ const formattedShortcuts = shortcuts
+ .map((pin, i) => {
+ const { type, ...data } = pin;
+ if (!SHORTCUTS_META[type]) return null;
+ let { id, path, title, subtitle, icon } = SHORTCUTS_META[type];
- if (typeof id === 'function') {
- id = id(data, i);
- }
- if (typeof path === 'function') {
- path = path(
- {
- ...data,
- instance: data.instance || instance,
- },
- i,
- );
- }
- if (typeof title === 'function') {
- title = title(data, i);
- }
- if (typeof subtitle === 'function') {
- subtitle = subtitle(data, i);
- }
- if (typeof icon === 'function') {
- icon = icon(data, i);
- }
+ if (typeof id === 'function') {
+ id = id(data, i);
+ }
+ if (typeof path === 'function') {
+ path = path(
+ {
+ ...data,
+ instance: data.instance || instance,
+ },
+ i,
+ );
+ }
+ if (typeof title === 'function') {
+ title = title(data, i);
+ }
+ if (typeof subtitle === 'function') {
+ subtitle = subtitle(data, i);
+ }
+ if (typeof icon === 'function') {
+ icon = icon(data, i);
+ }
- return {
- id,
- path,
- title,
- subtitle,
- icon,
- };
- })
- .filter(Boolean),
- [shortcuts],
- );
+ if (id === 'lists') {
+ hasLists.current = true;
+ }
+
+ return {
+ id,
+ path,
+ title,
+ subtitle,
+ icon,
+ };
+ })
+ .filter(Boolean);
const navigate = useNavigate();
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
@@ -88,6 +90,8 @@ function Shortcuts() {
}
});
+ const [lists, setLists] = useState([]);
+
return (
{snapStates.settings.shortcutsViewMode === 'tab-menu-bar' ? (
@@ -147,6 +151,11 @@ function Shortcuts() {
menuClassName="glass-menu shortcuts-menu"
gap={8}
position="anchor"
+ onMenuChange={(e) => {
+ if (e.open && hasLists.current) {
+ getLists().then(setLists);
+ }
+ }}
menuButton={
+ }
+ >
+
+ All Lists
+
+ {lists?.length > 0 && (
+ <>
+
+ {lists.map((list) => (
+
+ {list.title}
+
+ ))}
+ >
+ )}
+
}
headerEnd={
{
try {
- const lists = await masto.v1.lists.list();
- lists.sort((a, b) => a.title.localeCompare(b.title));
+ const lists = await fetchLists();
console.log(lists);
setLists(lists);
setUIState('default');
diff --git a/src/utils/lists.js b/src/utils/lists.js
new file mode 100644
index 00000000..1edc000c
--- /dev/null
+++ b/src/utils/lists.js
@@ -0,0 +1,114 @@
+import { api } from './api';
+import pmem from './pmem';
+import store from './store';
+
+const FETCH_MAX_AGE = 1000 * 60; // 1 minute
+const MAX_AGE = 24 * 60 * 60 * 1000; // 1 day
+
+export const fetchLists = pmem(
+ async () => {
+ const { masto } = api();
+ const lists = await masto.v1.lists.list();
+ lists.sort((a, b) => a.title.localeCompare(b.title));
+
+ if (lists.length) {
+ setTimeout(() => {
+ // Save to local storage, with saved timestamp
+ store.account.set('lists', {
+ lists,
+ updatedAt: Date.now(),
+ });
+ }, 1);
+ }
+
+ return lists;
+ },
+ {
+ maxAge: FETCH_MAX_AGE,
+ },
+);
+
+export async function getLists() {
+ try {
+ const { lists, updatedAt } = store.account.get('lists') || {};
+ if (!lists?.length) return await fetchLists();
+ if (Date.now() - updatedAt > MAX_AGE) {
+ // Stale-while-revalidate
+ fetchLists();
+ return lists;
+ }
+ return lists;
+ } catch (e) {
+ return [];
+ }
+}
+
+export const fetchList = pmem(
+ (id) => {
+ const { masto } = api();
+ return masto.v1.lists.$select(id).fetch();
+ },
+ {
+ maxAge: FETCH_MAX_AGE,
+ },
+);
+
+export async function getList(id) {
+ const { lists } = store.account.get('lists') || {};
+ console.log({ lists });
+ if (lists?.length) {
+ const theList = lists.find((l) => l.id === id);
+ if (theList) return theList;
+ }
+ try {
+ return fetchList(id);
+ } catch (e) {
+ return null;
+ }
+}
+
+export async function getListTitle(id) {
+ const list = await getList(id);
+ return list?.title || '';
+}
+
+export function addListStore(list) {
+ const { lists } = store.account.get('lists') || {};
+ if (lists?.length) {
+ lists.push(list);
+ lists.sort((a, b) => a.title.localeCompare(b.title));
+ store.account.set('lists', {
+ lists,
+ updatedAt: Date.now(),
+ });
+ }
+}
+
+export function updateListStore(list) {
+ const { lists } = store.account.get('lists') || {};
+ if (lists?.length) {
+ const index = lists.findIndex((l) => l.id === list.id);
+ if (index !== -1) {
+ lists[index] = list;
+ lists.sort((a, b) => a.title.localeCompare(b.title));
+ store.account.set('lists', {
+ lists,
+ updatedAt: Date.now(),
+ });
+ }
+ }
+}
+
+export function deleteListStore(listID) {
+ const { lists } = store.account.get('lists') || {};
+ if (lists?.length) {
+ const index = lists.findIndex((l) => l.id === listID);
+ if (index !== -1) {
+ lists.splice(index, 1);
+ store.account.set('lists', {
+ lists,
+ updatedAt: Date.now(),
+ });
+ }
+ }
+}