phanpy/src/pages/list.jsx

317 lines
8.6 KiB
React
Raw Normal View History

2023-04-05 18:30:26 +03:00
import './lists.css';
import { Menu, MenuItem } from '@szhsin/react-menu';
2023-02-10 19:05:18 +03:00
import { useEffect, useRef, useState } from 'preact/hooks';
2023-04-05 18:30:26 +03:00
import { InView } from 'react-intersection-observer';
import { useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
2023-02-10 19:05:18 +03:00
2023-04-05 18:30:26 +03:00
import AccountBlock from '../components/account-block';
2023-02-10 19:05:18 +03:00
import Icon from '../components/icon';
import Link from '../components/link';
2023-04-05 18:30:26 +03:00
import ListAddEdit from '../components/list-add-edit';
2023-06-13 12:46:37 +03:00
import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
2023-04-05 18:30:26 +03:00
import Modal from '../components/modal';
2023-02-10 19:05:18 +03:00
import Timeline from '../components/timeline';
import { api } from '../utils/api';
2023-03-21 19:09:36 +03:00
import { filteredItems } from '../utils/filters';
import states, { saveStatus } from '../utils/states';
2023-02-10 19:05:18 +03:00
import useTitle from '../utils/useTitle';
const LIMIT = 20;
2023-02-18 15:48:24 +03:00
function List(props) {
const snapStates = useSnapshot(states);
2023-02-18 19:05:46 +03:00
const { masto, instance } = api();
2023-02-18 15:48:24 +03:00
const id = props?.id || useParams()?.id;
// const navigate = useNavigate();
2023-02-11 11:28:03 +03:00
const latestItem = useRef();
2023-04-05 18:30:26 +03:00
// const [reloadCount, reload] = useReducer((c) => c + 1, 0);
2023-02-11 11:28:03 +03:00
2023-02-10 19:05:18 +03:00
const listIterator = useRef();
async function fetchList(firstLoad) {
if (firstLoad || !listIterator.current) {
listIterator.current = masto.v1.timelines.listList(id, {
limit: LIMIT,
});
}
2023-02-11 11:28:03 +03:00
const results = await listIterator.current.next();
2023-03-21 19:09:36 +03:00
let { value } = results;
2023-02-11 11:28:03 +03:00
if (value?.length) {
if (firstLoad) {
latestItem.current = value[0].id;
}
2023-03-21 19:09:36 +03:00
value = filteredItems(value, 'home');
value.forEach((item) => {
saveStatus(item, instance);
});
2023-02-11 11:28:03 +03:00
}
2023-05-14 16:13:36 +03:00
return {
...results,
value,
};
2023-02-11 11:28:03 +03:00
}
async function checkForUpdates() {
try {
const results = await masto.v1.timelines.listList(id, {
limit: 1,
since_id: latestItem.current,
});
2023-03-21 19:09:36 +03:00
let { value } = results;
value = filteredItems(value, 'home');
2023-02-11 11:28:03 +03:00
if (value?.length) {
return true;
}
return false;
} catch (e) {
return false;
}
2023-02-10 19:05:18 +03:00
}
2023-04-05 18:30:26 +03:00
const [list, setList] = useState({ title: 'List' });
// const [title, setTitle] = useState(`List`);
useTitle(list.title, `/l/:id`);
2023-02-10 19:05:18 +03:00
useEffect(() => {
(async () => {
try {
const list = await masto.v1.lists.fetch(id);
2023-04-05 18:30:26 +03:00
setList(list);
// setTitle(list.title);
2023-02-10 19:05:18 +03:00
} catch (e) {
console.error(e);
}
})();
}, [id]);
2023-04-05 18:30:26 +03:00
const [showListAddEditModal, setShowListAddEditModal] = useState(false);
const [showManageMembersModal, setShowManageMembersModal] = useState(false);
2023-02-10 19:05:18 +03:00
return (
2023-04-05 18:30:26 +03:00
<>
<Timeline
key={id}
2023-04-05 18:30:26 +03:00
title={list.title}
id="list"
emptyText="Nothing yet."
errorText="Unable to load posts."
instance={instance}
fetchItems={fetchList}
checkForUpdates={checkForUpdates}
useItemID
boostsCarousel={snapStates.settings.boostsCarousel}
2023-04-05 18:30:26 +03:00
allowFilters
// refresh={reloadCount}
headerStart={
<Link to="/l" class="button plain">
<Icon icon="list" size="l" />
</Link>
}
headerEnd={
2023-06-13 12:46:37 +03:00
<Menu2
portal
2023-04-05 18:30:26 +03:00
setDownOverflow
overflow="auto"
viewScroll="close"
position="anchor"
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>
2023-06-13 12:46:37 +03:00
</Menu2>
2023-04-05 18:30:26 +03:00
}
/>
{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');
location.hash = '/l';
2023-04-05 18:30:26 +03:00
}
setShowListAddEditModal(false);
}}
/>
</Modal>
)}
{showManageMembersModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowManageMembersModal(false);
}
}}
>
2023-04-20 11:10:57 +03:00
<ListManageMembers
listID={id}
onClose={() => setShowManageMembersModal(false)}
/>
2023-04-05 18:30:26 +03:00
</Modal>
)}
</>
);
}
2023-04-06 06:33:13 +03:00
const MEMBERS_LIMIT = 40;
2023-04-20 11:10:57 +03:00
function ListManageMembers({ listID, onClose }) {
2023-04-05 18:30:26 +03:00
// 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');
2023-02-10 19:05:18 +03:00
}
2023-04-05 18:30:26 +03:00
})();
}
useEffect(() => {
fetchMembers(true);
}, []);
return (
<div class="sheet" id="list-manage-members-container">
2023-04-20 11:10:57 +03:00
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
2023-04-05 18:30:26 +03:00
<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&hellip;
</button>
</InView>
)}
</ul>
</main>
</div>
);
}
function RemoveAddButton({ account, listID }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [removed, setRemoved] = useState(false);
return (
<MenuConfirm
confirm={!removed}
confirmLabel={<span>Remove @{account.username} from list?</span>}
align="end"
menuItemClassName="danger"
2023-04-05 18:30:26 +03:00
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;
2023-04-05 18:30:26 +03:00
setUIState('loading');
(async () => {
try {
await masto.v1.lists.removeAccount(listID, {
accountIds: [account.id],
});
setUIState('default');
setRemoved(true);
} catch (e) {
setUIState('error');
}
})();
}
}}
>
<button
type="button"
class={`light ${removed ? '' : 'danger'}`}
disabled={uiState === 'loading'}
>
{removed ? 'Add' : 'Remove…'}
</button>
</MenuConfirm>
2023-02-10 19:05:18 +03:00
);
}
export default List;