diff --git a/public/sw.js b/public/sw.js
index 14514039..b44a94b0 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -39,7 +39,7 @@ registerRoute(imageRoute);
// - /api/v1/preferences
// - /api/v1/lists/:id
const apiExtendedRoute = new RegExpRoute(
- /^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis|preferences|lists\/\d+)/,
+ /^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis|preferences|lists\/\d+)$/,
new StaleWhileRevalidate({
cacheName: 'api-extended',
plugins: [
diff --git a/src/components/account-info.css b/src/components/account-info.css
index f14780bb..06bf133b 100644
--- a/src/components/account-info.css
+++ b/src/components/account-info.css
@@ -260,6 +260,28 @@
animation: shine 1s ease-in-out 1s;
}
+#list-add-remove-container .list-add-remove {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0;
+ padding: 8px 0;
+ list-style: none;
+}
+#list-add-remove-container .list-add-remove button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+}
+#list-add-remove-container .list-add-remove button .icon {
+ opacity: 0.15;
+}
+#list-add-remove-container .list-add-remove button.checked .icon {
+ opacity: 1;
+ color: var(--green-color);
+}
+
@media (min-width: 40em) {
.timeline-start .account-container {
--item-radius: 16px;
diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx
index a7fa85c2..0acc64b4 100644
--- a/src/components/account-info.jsx
+++ b/src/components/account-info.jsx
@@ -1,13 +1,7 @@
import './account-info.css';
-import {
- Menu,
- MenuDivider,
- MenuHeader,
- MenuItem,
- SubMenu,
-} from '@szhsin/react-menu';
-import { useEffect, useRef, useState } from 'preact/hooks';
+import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
+import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
@@ -24,6 +18,8 @@ import AccountBlock from './account-block';
import Avatar from './avatar';
import Icon from './icon';
import Link from './link';
+import ListAddEdit from './list-add-edit';
+import Loader from './loader';
import Modal from './modal';
import TranslationBlock from './translation-block';
@@ -487,6 +483,7 @@ function RelatedActions({ info, instance, authenticated }) {
const menuInstanceRef = useRef(null);
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
+ const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
return (
<>
@@ -583,6 +580,17 @@ function RelatedActions({ info, instance, authenticated }) {
Translate bio
+ {/* Add/remove from lists is only possible if following the account */}
+ {following && (
+
+ )}
>
)}
@@ -840,6 +848,18 @@ function RelatedActions({ info, instance, authenticated }) {
)}
+ {!!showAddRemoveLists && (
+ {
+ if (e.target === e.currentTarget) {
+ setShowAddRemoveLists(false);
+ }
+ }}
+ >
+
+
+ )}
>
);
}
@@ -900,4 +920,127 @@ function TranslatedBioSheet({ note, fields }) {
);
}
+
+function AddRemoveListsSheet({ accountID }) {
+ const { masto } = api();
+ const [uiState, setUiState] = useState('default');
+ const [lists, setLists] = useState([]);
+ const [listsContainingAccount, setListsContainingAccount] = useState([]);
+ const [reloadCount, reload] = useReducer((c) => c + 1, 0);
+
+ useEffect(() => {
+ setUiState('loading');
+ (async () => {
+ try {
+ const lists = await masto.v1.lists.list();
+ const listsContainingAccount = await masto.v1.accounts.listLists(
+ accountID,
+ );
+ console.log({ lists, listsContainingAccount });
+ setLists(lists);
+ setListsContainingAccount(listsContainingAccount);
+ setUiState('default');
+ } catch (e) {
+ console.error(e);
+ setUiState('error');
+ }
+ })();
+ }, [reloadCount]);
+
+ const [showListAddEditModal, setShowListAddEditModal] = useState(false);
+
+ return (
+
+
+ Add/Remove from Lists
+
+
+ {lists.length > 0 ? (
+
+ {lists.map((list) => {
+ const inList = listsContainingAccount.some(
+ (l) => l.id === list.id,
+ );
+ return (
+ -
+
+
+ );
+ })}
+
+ ) : uiState === 'loading' ? (
+
+
+
+ ) : uiState === 'error' ? (
+ Unable to load lists.
+ ) : (
+ No lists.
+ )}
+
+
+ {showListAddEditModal && (
+
{
+ if (e.target === e.currentTarget) {
+ setShowListAddEditModal(false);
+ }
+ }}
+ >
+ {
+ if (result.state === 'success') {
+ reload();
+ }
+ setShowListAddEditModal(false);
+ }}
+ />
+
+ )}
+
+ );
+}
+
export default AccountInfo;
diff --git a/src/components/list-add-edit.jsx b/src/components/list-add-edit.jsx
new file mode 100644
index 00000000..a3fd8f3f
--- /dev/null
+++ b/src/components/list-add-edit.jsx
@@ -0,0 +1,133 @@
+import { useEffect, useRef, useState } from 'preact/hooks';
+
+import { api } from '../utils/api';
+
+function ListAddEdit({ list, onClose = () => {} }) {
+ const { masto } = api();
+ const [uiState, setUiState] = useState('default');
+ const editMode = !!list;
+ const nameFieldRef = useRef();
+ const repliesPolicyFieldRef = useRef();
+ useEffect(() => {
+ if (editMode) {
+ nameFieldRef.current.value = list.title;
+ repliesPolicyFieldRef.current.value = list.repliesPolicy;
+ }
+ }, [editMode]);
+ return (
+
+
+ {editMode ? 'Edit list' : 'New list'}
+
+
+
+
+
+ );
+}
+
+export default ListAddEdit;
diff --git a/src/index.css b/src/index.css
index 339df71f..1a124fe5 100644
--- a/src/index.css
+++ b/src/index.css
@@ -194,6 +194,9 @@ button,
color: var(--text-color);
border: 1px solid var(--outline-color);
}
+:is(button, .button).light:not(:disabled, .disabled):is(:hover, :focus) {
+ border-color: var(--outline-hover-color);
+}
:is(button, .button).light.danger:not(:disabled, .disabled) {
color: var(--red-color);
}
diff --git a/src/pages/list.jsx b/src/pages/list.jsx
index 54c53c58..3b9155dc 100644
--- a/src/pages/list.jsx
+++ b/src/pages/list.jsx
@@ -1,8 +1,15 @@
-import { useEffect, useRef, useState } from 'preact/hooks';
-import { useParams } from 'react-router-dom';
+import './lists.css';
+import { Menu, MenuItem } from '@szhsin/react-menu';
+import { useEffect, useRef, useState } from 'preact/hooks';
+import { InView } from 'react-intersection-observer';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import AccountBlock from '../components/account-block';
import Icon from '../components/icon';
import Link from '../components/link';
+import ListAddEdit from '../components/list-add-edit';
+import Modal from '../components/modal';
import Timeline from '../components/timeline';
import { api } from '../utils/api';
import { filteredItems } from '../utils/filters';
@@ -14,7 +21,9 @@ const LIMIT = 20;
function List(props) {
const { masto, instance } = api();
const id = props?.id || useParams()?.id;
+ const navigate = useNavigate();
const latestItem = useRef();
+ // const [reloadCount, reload] = useReducer((c) => c + 1, 0);
const listIterator = useRef();
async function fetchList(firstLoad) {
@@ -55,37 +64,231 @@ function List(props) {
}
}
- const [title, setTitle] = useState(`List`);
- useTitle(title, `/l/:id`);
+ const [list, setList] = useState({ title: 'List' });
+ // const [title, setTitle] = useState(`List`);
+ useTitle(list.title, `/l/:id`);
useEffect(() => {
(async () => {
try {
const list = await masto.v1.lists.fetch(id);
- setTitle(list.title);
+ setList(list);
+ // setTitle(list.title);
} catch (e) {
console.error(e);
}
})();
}, [id]);
+ const [showListAddEditModal, setShowListAddEditModal] = useState(false);
+ const [showManageMembersModal, setShowManageMembersModal] = useState(false);
+
return (
-
-
-
+ <>
+
+
+
+ }
+ headerEnd={
+
+ }
+ />
+ {showListAddEditModal && (
+ {
+ if (e.target === e.currentTarget) {
+ setShowListAddEditModal(false);
+ }
+ }}
+ >
+ {
+ if (result.state === 'success' && result.list) {
+ setList(result.list);
+ // reload();
+ } else if (result.state === 'deleted') {
+ navigate('/l');
+ }
+ setShowListAddEditModal(false);
+ }}
+ />
+
+ )}
+ {showManageMembersModal && (
+ {
+ if (e.target === e.currentTarget) {
+ setShowManageMembersModal(false);
+ }
+ }}
+ >
+
+
+ )}
+ >
+ );
+}
+
+const MEMBERS_LIMIT = 10;
+function ListManageMembers({ listID }) {
+ // Show list of members with [Remove] button
+ // API only returns 40 members at a time, so this need to be paginated with infinite scroll
+ // Show [Add] button after removing a member
+ const { masto, instance } = api();
+ const [members, setMembers] = useState([]);
+ const [uiState, setUIState] = useState('default');
+ const [showMore, setShowMore] = useState(false);
+
+ const membersIterator = useRef();
+
+ async function fetchMembers(firstLoad) {
+ setShowMore(false);
+ setUIState('loading');
+ (async () => {
+ try {
+ if (firstLoad || !membersIterator.current) {
+ membersIterator.current = masto.v1.lists.listAccounts(listID, {
+ limit: MEMBERS_LIMIT,
+ });
+ }
+ const results = await membersIterator.current.next();
+ let { done, value } = results;
+ if (value?.length) {
+ if (firstLoad) {
+ setMembers(value);
+ } else {
+ setMembers(members.concat(value));
+ }
+ setShowMore(!done);
+ } else {
+ setShowMore(false);
+ }
+ setUIState('default');
+ } catch (e) {
+ setUIState('error');
}
- />
+ })();
+ }
+
+ useEffect(() => {
+ fetchMembers(true);
+ }, []);
+
+ return (
+
+
+
+
+ {members.map((member) => (
+ -
+
+
+
+ ))}
+ {showMore && uiState === 'default' && (
+ inView && fetchMembers()}>
+
+
+ )}
+
+
+
+ );
+}
+
+function RemoveAddButton({ account, listID }) {
+ const { masto } = api();
+ const [uiState, setUIState] = useState('default');
+ const [removed, setRemoved] = useState(false);
+
+ return (
+
);
}
diff --git a/src/pages/lists.css b/src/pages/lists.css
new file mode 100644
index 00000000..dcf5ffc6
--- /dev/null
+++ b/src/pages/lists.css
@@ -0,0 +1,33 @@
+.list-form {
+ padding: 8px 0;
+ display: flex;
+ gap: 8px;
+ flex-direction: column;
+}
+
+.list-form-row :is(input, select) {
+ width: 100%;
+}
+
+.list-form-footer {
+ display: flex;
+ gap: 8px;
+ justify-content: space-between;
+}
+.list-form-footer button[type='submit'] {
+ padding-inline: 24px;
+}
+
+#list-manage-members-container ul {
+ display: block;
+ list-style: none;
+ padding: 8px 0;
+ margin: 0;
+}
+#list-manage-members-container ul li {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 0;
+}
diff --git a/src/pages/lists.jsx b/src/pages/lists.jsx
index 48ba6899..5aa3ceea 100644
--- a/src/pages/lists.jsx
+++ b/src/pages/lists.jsx
@@ -1,9 +1,13 @@
-import { useEffect, useState } from 'preact/hooks';
+import './lists.css';
+
+import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
import Icon from '../components/icon';
import Link from '../components/link';
+import ListAddEdit from '../components/list-add-edit';
import Loader from '../components/loader';
import Menu from '../components/menu';
+import Modal from '../components/modal';
import { api } from '../utils/api';
import useTitle from '../utils/useTitle';
@@ -12,6 +16,7 @@ function Lists() {
useTitle(`Lists`, `/l`);
const [uiState, setUiState] = useState('default');
+ const [reloadCount, reload] = useReducer((c) => c + 1, 0);
const [lists, setLists] = useState([]);
useEffect(() => {
setUiState('loading');
@@ -26,7 +31,9 @@ function Lists() {
setUiState('error');
}
})();
- }, []);
+ }, [reloadCount]);
+
+ const [showListAddEditModal, setShowListAddEditModal] = useState(false);
return (
@@ -40,7 +47,15 @@ function Lists() {
Lists
-
+
@@ -49,7 +64,22 @@ function Lists() {
{lists.map((list) => (
- {list.title}
+
+ {list.title}
+
+ {/* */}
))}
@@ -65,6 +95,26 @@ function Lists() {
)}
+ {showListAddEditModal && (
+ {
+ if (e.target === e.currentTarget) {
+ setShowListAddEditModal(false);
+ }
+ }}
+ >
+ {
+ if (result.state === 'success') {
+ reload();
+ }
+ setShowListAddEditModal(false);
+ }}
+ />
+
+ )}
);
}