mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 06:06:41 +01:00
Full CRUD for Lists
This commit is contained in:
parent
4b42118742
commit
ff1a9fa444
8 changed files with 621 additions and 34 deletions
|
@ -39,7 +39,7 @@ registerRoute(imageRoute);
|
||||||
// - /api/v1/preferences
|
// - /api/v1/preferences
|
||||||
// - /api/v1/lists/:id
|
// - /api/v1/lists/:id
|
||||||
const apiExtendedRoute = new RegExpRoute(
|
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({
|
new StaleWhileRevalidate({
|
||||||
cacheName: 'api-extended',
|
cacheName: 'api-extended',
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
@ -260,6 +260,28 @@
|
||||||
animation: shine 1s ease-in-out 1s;
|
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) {
|
@media (min-width: 40em) {
|
||||||
.timeline-start .account-container {
|
.timeline-start .account-container {
|
||||||
--item-radius: 16px;
|
--item-radius: 16px;
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import './account-info.css';
|
import './account-info.css';
|
||||||
|
|
||||||
import {
|
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
|
||||||
Menu,
|
import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
|
||||||
MenuDivider,
|
|
||||||
MenuHeader,
|
|
||||||
MenuItem,
|
|
||||||
SubMenu,
|
|
||||||
} from '@szhsin/react-menu';
|
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
|
@ -24,6 +18,8 @@ import AccountBlock from './account-block';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
|
import ListAddEdit from './list-add-edit';
|
||||||
|
import Loader from './loader';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import TranslationBlock from './translation-block';
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
|
@ -487,6 +483,7 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
const menuInstanceRef = useRef(null);
|
const menuInstanceRef = useRef(null);
|
||||||
|
|
||||||
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
||||||
|
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -583,6 +580,17 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate bio</span>
|
<span>Translate bio</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{/* Add/remove from lists is only possible if following the account */}
|
||||||
|
{following && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddRemoveLists(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="list" />
|
||||||
|
<span>Add/remove from Lists</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -840,6 +848,18 @@ function RelatedActions({ info, instance, authenticated }) {
|
||||||
<TranslatedBioSheet note={note} fields={fields} />
|
<TranslatedBioSheet note={note} fields={fields} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{!!showAddRemoveLists && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowAddRemoveLists(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddRemoveListsSheet accountID={accountID.current} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -900,4 +920,127 @@ function TranslatedBioSheet({ note, fields }) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div class="sheet" id="list-add-remove-container">
|
||||||
|
<header>
|
||||||
|
<h2>Add/Remove from Lists</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{lists.length > 0 ? (
|
||||||
|
<ul class="list-add-remove">
|
||||||
|
{lists.map((list) => {
|
||||||
|
const inList = listsContainingAccount.some(
|
||||||
|
(l) => l.id === list.id,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`light ${inList ? 'checked' : ''}`}
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
setUiState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (inList) {
|
||||||
|
await masto.v1.lists.removeAccount(list.id, {
|
||||||
|
accountIds: [accountID],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await masto.v1.lists.addAccount(list.id, {
|
||||||
|
accountIds: [accountID],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// setUiState('default');
|
||||||
|
reload();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUiState('error');
|
||||||
|
alert(
|
||||||
|
inList
|
||||||
|
? 'Unable to remove from list.'
|
||||||
|
: 'Unable to add to list.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="check-circle" />
|
||||||
|
<span>{list.title}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : uiState === 'error' ? (
|
||||||
|
<p class="ui-state">Unable to load lists.</p>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state">No lists.</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain2"
|
||||||
|
onClick={() => setShowListAddEditModal(true)}
|
||||||
|
disabled={uiState !== 'default'}
|
||||||
|
>
|
||||||
|
<Icon icon="plus" size="l" /> <span>New list</span>
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
{showListAddEditModal && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowListAddEditModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListAddEdit
|
||||||
|
list={showListAddEditModal?.list}
|
||||||
|
onClose={(result) => {
|
||||||
|
if (result.state === 'success') {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
setShowListAddEditModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default AccountInfo;
|
export default AccountInfo;
|
||||||
|
|
133
src/components/list-add-edit.jsx
Normal file
133
src/components/list-add-edit.jsx
Normal file
|
@ -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 (
|
||||||
|
<div class="sheet">
|
||||||
|
<header>
|
||||||
|
<h2>{editMode ? 'Edit list' : 'New list'}</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<form
|
||||||
|
class="list-form"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault(); // Get form values
|
||||||
|
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const title = formData.get('title');
|
||||||
|
const repliesPolicy = formData.get('replies_policy');
|
||||||
|
console.log({
|
||||||
|
title,
|
||||||
|
repliesPolicy,
|
||||||
|
});
|
||||||
|
setUiState('loading');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
let listResult;
|
||||||
|
|
||||||
|
if (editMode) {
|
||||||
|
listResult = await masto.v1.lists.update(list.id, {
|
||||||
|
title,
|
||||||
|
replies_policy: repliesPolicy,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
listResult = await masto.v1.lists.create({
|
||||||
|
title,
|
||||||
|
replies_policy: repliesPolicy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(listResult);
|
||||||
|
setUiState('default');
|
||||||
|
onClose({
|
||||||
|
state: 'success',
|
||||||
|
list: listResult,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUiState('error');
|
||||||
|
alert(
|
||||||
|
editMode ? 'Unable to edit list.' : 'Unable to create list.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="list-form-row">
|
||||||
|
<label for="list-title">
|
||||||
|
Name{' '}
|
||||||
|
<input
|
||||||
|
ref={nameFieldRef}
|
||||||
|
type="text"
|
||||||
|
id="list-title"
|
||||||
|
name="title"
|
||||||
|
required
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="list-form-row">
|
||||||
|
<select
|
||||||
|
ref={repliesPolicyFieldRef}
|
||||||
|
name="replies_policy"
|
||||||
|
required
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
>
|
||||||
|
<option value="list">Show replies to list members</option>
|
||||||
|
<option value="followed">Show replies to people I follow</option>
|
||||||
|
<option value="none">Don't show replies</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="list-form-footer">
|
||||||
|
<button type="submit" disabled={uiState === 'loading'}>
|
||||||
|
{editMode ? 'Save' : 'Create'}
|
||||||
|
</button>
|
||||||
|
{editMode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="light danger"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
const yes = confirm('Delete this list?');
|
||||||
|
if (!yes) return;
|
||||||
|
setUiState('loading');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.lists.remove(list.id);
|
||||||
|
setUiState('default');
|
||||||
|
onClose({
|
||||||
|
state: 'deleted',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUiState('error');
|
||||||
|
alert('Unable to delete list.');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete…
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListAddEdit;
|
|
@ -194,6 +194,9 @@ button,
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
border: 1px solid var(--outline-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) {
|
:is(button, .button).light.danger:not(:disabled, .disabled) {
|
||||||
color: var(--red-color);
|
color: var(--red-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import './lists.css';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
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 Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
|
import ListAddEdit from '../components/list-add-edit';
|
||||||
|
import Modal from '../components/modal';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import { filteredItems } from '../utils/filters';
|
import { filteredItems } from '../utils/filters';
|
||||||
|
@ -14,7 +21,9 @@ const LIMIT = 20;
|
||||||
function List(props) {
|
function List(props) {
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
const id = props?.id || useParams()?.id;
|
const id = props?.id || useParams()?.id;
|
||||||
|
const navigate = useNavigate();
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
// const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||||
|
|
||||||
const listIterator = useRef();
|
const listIterator = useRef();
|
||||||
async function fetchList(firstLoad) {
|
async function fetchList(firstLoad) {
|
||||||
|
@ -55,22 +64,28 @@ function List(props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [title, setTitle] = useState(`List`);
|
const [list, setList] = useState({ title: 'List' });
|
||||||
useTitle(title, `/l/:id`);
|
// const [title, setTitle] = useState(`List`);
|
||||||
|
useTitle(list.title, `/l/:id`);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const list = await masto.v1.lists.fetch(id);
|
const list = await masto.v1.lists.fetch(id);
|
||||||
setTitle(list.title);
|
setList(list);
|
||||||
|
// setTitle(list.title);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const [showListAddEditModal, setShowListAddEditModal] = useState(false);
|
||||||
|
const [showManageMembersModal, setShowManageMembersModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Timeline
|
<Timeline
|
||||||
title={title}
|
title={list.title}
|
||||||
id="list"
|
id="list"
|
||||||
emptyText="Nothing yet."
|
emptyText="Nothing yet."
|
||||||
errorText="Unable to load posts."
|
errorText="Unable to load posts."
|
||||||
|
@ -80,12 +95,200 @@ function List(props) {
|
||||||
useItemID
|
useItemID
|
||||||
boostsCarousel
|
boostsCarousel
|
||||||
allowFilters
|
allowFilters
|
||||||
|
// refresh={reloadCount}
|
||||||
headerStart={
|
headerStart={
|
||||||
<Link to="/l" class="button plain">
|
<Link to="/l" class="button plain">
|
||||||
<Icon icon="list" size="l" />
|
<Icon icon="list" size="l" />
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
headerEnd={
|
||||||
|
<Menu
|
||||||
|
portal={{
|
||||||
|
target: document.body,
|
||||||
|
}}
|
||||||
|
setDownOverflow
|
||||||
|
overflow="auto"
|
||||||
|
viewScroll="close"
|
||||||
|
position="anchor"
|
||||||
|
boundingBoxPadding="8 8 8 8"
|
||||||
|
menuButton={
|
||||||
|
<button type="button" class="plain">
|
||||||
|
<Icon icon="more" size="l" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
setShowListAddEditModal({
|
||||||
|
list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon icon="pencil" size="l" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => setShowManageMembersModal(true)}>
|
||||||
|
<Icon icon="group" size="l" />
|
||||||
|
<span>Manage members</span>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
{showListAddEditModal && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowListAddEditModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListAddEdit
|
||||||
|
list={showListAddEditModal?.list}
|
||||||
|
onClose={(result) => {
|
||||||
|
if (result.state === 'success' && result.list) {
|
||||||
|
setList(result.list);
|
||||||
|
// reload();
|
||||||
|
} else if (result.state === 'deleted') {
|
||||||
|
navigate('/l');
|
||||||
|
}
|
||||||
|
setShowListAddEditModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{showManageMembersModal && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowManageMembersModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListManageMembers listID={id} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div class="sheet" id="list-manage-members-container">
|
||||||
|
<header>
|
||||||
|
<h2>Manage members</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<ul>
|
||||||
|
{members.map((member) => (
|
||||||
|
<li key={member.id}>
|
||||||
|
<AccountBlock account={member} instance={instance} />
|
||||||
|
<RemoveAddButton account={member} listID={listID} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{showMore && uiState === 'default' && (
|
||||||
|
<InView as="li" onChange={(inView) => inView && fetchMembers()}>
|
||||||
|
<button type="button" class="light block" onClick={fetchMembers}>
|
||||||
|
Show more…
|
||||||
|
</button>
|
||||||
|
</InView>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RemoveAddButton({ account, listID }) {
|
||||||
|
const { masto } = api();
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [removed, setRemoved] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`light ${removed ? '' : 'danger'}`}
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
if (removed) {
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.lists.addAccount(listID, {
|
||||||
|
accountIds: [account.id],
|
||||||
|
});
|
||||||
|
setUIState('default');
|
||||||
|
setRemoved(false);
|
||||||
|
} catch (e) {
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
const yes = confirm(`Remove ${account.username} from this list?`);
|
||||||
|
if (!yes) return;
|
||||||
|
setUIState('loading');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.lists.removeAccount(listID, {
|
||||||
|
accountIds: [account.id],
|
||||||
|
});
|
||||||
|
setUIState('default');
|
||||||
|
setRemoved(true);
|
||||||
|
} catch (e) {
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{removed ? 'Add' : 'Remove…'}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
33
src/pages/lists.css
Normal file
33
src/pages/lists.css
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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 Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
|
import ListAddEdit from '../components/list-add-edit';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import Menu from '../components/menu';
|
import Menu from '../components/menu';
|
||||||
|
import Modal from '../components/modal';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -12,6 +16,7 @@ function Lists() {
|
||||||
useTitle(`Lists`, `/l`);
|
useTitle(`Lists`, `/l`);
|
||||||
const [uiState, setUiState] = useState('default');
|
const [uiState, setUiState] = useState('default');
|
||||||
|
|
||||||
|
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||||
const [lists, setLists] = useState([]);
|
const [lists, setLists] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUiState('loading');
|
setUiState('loading');
|
||||||
|
@ -26,7 +31,9 @@ function Lists() {
|
||||||
setUiState('error');
|
setUiState('error');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, [reloadCount]);
|
||||||
|
|
||||||
|
const [showListAddEditModal, setShowListAddEditModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="lists-page" class="deck-container" tabIndex="-1">
|
<div id="lists-page" class="deck-container" tabIndex="-1">
|
||||||
|
@ -40,7 +47,15 @@ function Lists() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<h1>Lists</h1>
|
<h1>Lists</h1>
|
||||||
<div class="header-side" />
|
<div class="header-side">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain"
|
||||||
|
onClick={() => setShowListAddEditModal(true)}
|
||||||
|
>
|
||||||
|
<Icon icon="plus" size="l" alt="New list" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
@ -49,7 +64,22 @@ function Lists() {
|
||||||
{lists.map((list) => (
|
{lists.map((list) => (
|
||||||
<li>
|
<li>
|
||||||
<Link to={`/l/${list.id}`}>
|
<Link to={`/l/${list.id}`}>
|
||||||
|
<span>
|
||||||
<Icon icon="list" /> <span>{list.title}</span>
|
<Icon icon="list" /> <span>{list.title}</span>
|
||||||
|
</span>
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
class="plain"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowListAddEditModal({
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="pencil" />
|
||||||
|
</button> */}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -65,6 +95,26 @@ function Lists() {
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
{showListAddEditModal && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowListAddEditModal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListAddEdit
|
||||||
|
list={showListAddEditModal?.list}
|
||||||
|
onClose={(result) => {
|
||||||
|
if (result.state === 'success') {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
setShowListAddEditModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue