owncast/web/pages/admin/federation/followers.tsx

373 lines
9.6 KiB
TypeScript

import React, { useEffect, useState, useContext, ReactElement } from 'react';
import { Table, Avatar, Button, Tabs } from 'antd';
import { ColumnsType, SortOrder } from 'antd/lib/table/interface';
import format from 'date-fns/format';
import dynamic from 'next/dynamic';
import { ServerStatusContext } from '../../../utils/server-status-context';
import {
FOLLOWERS,
FOLLOWERS_PENDING,
SET_FOLLOWER_APPROVAL,
FOLLOWERS_BLOCKED,
fetchData,
} from '../../../utils/apis';
import { isEmptyObject } from '../../../utils/format';
import { AdminLayout } from '../../../components/layouts/AdminLayout';
// Lazy loaded components
const UserAddOutlined = dynamic(() => import('@ant-design/icons/UserAddOutlined'), {
ssr: false,
});
const UserDeleteOutlined = dynamic(() => import('@ant-design/icons/UserDeleteOutlined'), {
ssr: false,
});
export interface Follower {
link: string;
username: string;
image: string;
name: string;
timestamp: Date;
approved: Date;
}
const FollowersTable = ({ data, tableColumns, totalCount, setCurrentPage }) => (
<Table
dataSource={data}
columns={tableColumns}
size="small"
rowKey={row => row.link}
pagination={{
pageSize: 25,
hideOnSinglePage: true,
showSizeChanger: false,
total: totalCount,
}}
onChange={pagination => {
const page = pagination.current;
setCurrentPage(page);
}}
/>
);
export default function FediverseFollowers() {
const [followersPending, setFollowersPending] = useState<Follower[]>([]);
const [followersBlocked, setFollowersBlocked] = useState<Follower[]>([]);
const [followers, setFollowers] = useState<Follower[]>([]);
const [totalCount, setTotalCount] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(0);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig } = serverStatusData || {};
const { federation } = serverConfig;
const { isPrivate } = federation;
const getFollowers = async () => {
try {
const limit = 25;
const offset = currentPage * limit;
const u = `${FOLLOWERS}?offset=${offset}&limit=${limit}`;
// Active followers
const result = await fetchData(u, { auth: true });
const { results, total } = result;
if (isEmptyObject(results)) {
setFollowers([]);
} else {
setTotalCount(total);
setFollowers(results);
}
// Pending follow requests
const pendingFollowersResult = await fetchData(FOLLOWERS_PENDING, { auth: true });
if (isEmptyObject(pendingFollowersResult)) {
setFollowersPending([]);
} else {
setFollowersPending(pendingFollowersResult);
}
// Blocked/rejected followers
const blockedFollowersResult = await fetchData(FOLLOWERS_BLOCKED, { auth: true });
if (isEmptyObject(followersBlocked)) {
setFollowersBlocked([]);
} else {
setFollowersBlocked(blockedFollowersResult);
}
} catch (error) {
console.log('==== error', error);
}
};
useEffect(() => {
getFollowers();
}, [currentPage]);
const columns: ColumnsType<Follower> = [
{
title: '',
dataIndex: 'image',
key: 'image',
width: 90,
render: image => <Avatar size={40} src={image || '/img/logo.svg'} />,
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (_, follower) => (
<a href={follower.link} target="_blank" rel="noreferrer">
{follower.name || follower.username}
</a>
),
},
{
title: 'URL',
dataIndex: 'link',
key: 'link',
render: (_, follower) => (
<a href={follower.link} target="_blank" rel="noreferrer">
{follower.link}
</a>
),
},
];
async function approveFollowRequest(request) {
try {
await fetchData(SET_FOLLOWER_APPROVAL, {
auth: true,
method: 'POST',
data: {
actorIRI: request.link,
approved: true,
},
});
// Refetch and update the current data.
getFollowers();
} catch (err) {
console.error(err);
}
}
async function rejectFollowRequest(request) {
try {
await fetchData(SET_FOLLOWER_APPROVAL, {
auth: true,
method: 'POST',
data: {
actorIRI: request.link,
approved: false,
},
});
// Refetch and update the current data.
getFollowers();
} catch (err) {
console.error(err);
}
}
const pendingColumns: ColumnsType<Follower> = [...columns];
pendingColumns.unshift(
{
title: 'Approve',
dataIndex: null,
key: null,
render: request => (
<Button
type="primary"
icon={<UserAddOutlined />}
onClick={() => {
approveFollowRequest(request);
}}
/>
),
width: 50,
},
{
title: 'Reject',
dataIndex: null,
key: null,
render: request => (
<Button
type="primary"
danger
icon={<UserDeleteOutlined />}
onClick={() => {
rejectFollowRequest(request);
}}
/>
),
width: 50,
},
);
pendingColumns.push({
title: 'Requested',
dataIndex: 'timestamp',
key: 'requested',
width: 200,
render: timestamp => {
const dateObject = new Date(timestamp);
return <>{format(dateObject, 'P')}</>;
},
sorter: (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
sortDirections: ['descend', 'ascend'] as SortOrder[],
defaultSortOrder: 'descend' as SortOrder,
});
const blockedColumns: ColumnsType<Follower> = [...columns];
blockedColumns.unshift({
title: 'Approve',
dataIndex: null,
key: null,
render: request => (
<Button
type="primary"
icon={<UserAddOutlined />}
size="large"
onClick={() => {
approveFollowRequest(request);
}}
/>
),
width: 50,
});
blockedColumns.push(
{
title: 'Requested',
dataIndex: 'timestamp',
key: 'requested',
width: 200,
render: timestamp => {
const dateObject = new Date(timestamp);
return <>{format(dateObject, 'P')}</>;
},
sorter: (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
sortDirections: ['descend', 'ascend'] as SortOrder[],
defaultSortOrder: 'descend' as SortOrder,
},
{
title: 'Rejected/Blocked',
dataIndex: 'timestamp',
key: 'disabled_at',
width: 200,
render: timestamp => {
const dateObject = new Date(timestamp);
return <>{format(dateObject, 'P')}</>;
},
sorter: (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
sortDirections: ['descend', 'ascend'] as SortOrder[],
defaultSortOrder: 'descend' as SortOrder,
},
);
const followersColumns: ColumnsType<Follower> = [...columns];
followersColumns.push(
{
title: 'Added',
dataIndex: 'timestamp',
key: 'timestamp',
width: 200,
render: timestamp => {
const dateObject = new Date(timestamp);
return <>{format(dateObject, 'P')}</>;
},
sorter: (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
sortDirections: ['descend', 'ascend'] as SortOrder[],
defaultSortOrder: 'descend' as SortOrder,
},
{
title: 'Remove',
dataIndex: null,
key: null,
render: request => (
<Button
type="primary"
danger
icon={<UserDeleteOutlined />}
onClick={() => {
rejectFollowRequest(request);
}}
/>
),
width: 50,
},
);
const followersTabTitle = <span>Followers {totalCount > 0 && `(${totalCount})`}</span>;
const followersTab = (
<>
<p>The following accounts get notified when you go live or send a post.</p>
<FollowersTable
data={followers}
tableColumns={followersColumns}
totalCount={totalCount}
setCurrentPage={setCurrentPage}
/>
</>
);
const pendingRequestsTabTitle = (
<span>Requests {followersPending.length > 0 && `(${followersPending.length})`}</span>
);
const pendingRequestsTab = (
<>
<p>
The following people are requesting to follow your Owncast server on the{' '}
<a href="https://en.wikipedia.org/wiki/Fediverse" target="_blank" rel="noopener noreferrer">
Fediverse
</a>{' '}
and be alerted to when you go live. Each must be approved.
</p>
<Table
dataSource={followersPending}
columns={pendingColumns}
size="small"
rowKey={row => row.link}
/>
</>
);
const blockedTabTitle = (
<span>Blocked {followersBlocked.length > 0 && `(${followersBlocked.length})`}</span>
);
const blockedTab = (
<>
<p>
The following people were either rejected or blocked by you. You can approve them as a
follower.
</p>
<p>
<Table
dataSource={followersBlocked}
columns={blockedColumns}
size="small"
rowKey={row => row.link}
/>
</p>
</>
);
const items = [
{ label: followersTabTitle, key: '1', children: followersTab },
isPrivate && { label: pendingRequestsTabTitle, key: '2', children: pendingRequestsTab },
{ label: blockedTabTitle, key: '3', children: blockedTab },
];
return (
<div className="followers-section">
<Tabs defaultActiveKey="1" items={items} />
</div>
);
}
FediverseFollowers.getLayout = function getLayout(page: ReactElement) {
return <AdminLayout page={page} />;
};