diff --git a/locales/en/translation.json b/locales/en/translation.json index 01ca5210..3a490b25 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -6,6 +6,7 @@ "public": "Public", "favourites": "Favourites", "bookmarks": "Bookmarks", + "follow_requests": "Follow Requests", "search": "Search", "status": { "avatar": "Aavtar of {user}", @@ -100,6 +101,10 @@ "body": "{user} requested to follow you" } }, + "follow_requests": { + "authorize": "Authorize", + "reject": "Reject" + }, "compose": { "placeholder": "What's on your mind?", "spoiler": { diff --git a/renderer/components/accounts/New.tsx b/renderer/components/accounts/New.tsx index 1b4aee23..e8bc5772 100644 --- a/renderer/components/accounts/New.tsx +++ b/renderer/components/accounts/New.tsx @@ -187,7 +187,7 @@ export default function New(props: NewProps) { size="sm" variant="text" color="blue" - title={formatMessage({ id: 'accounts.copy_authorization_url' })} + title={formatMessage({ id: 'accounts.new.copy_authorization_url' })} onClick={() => copyText(appData.url)} > diff --git a/renderer/components/layouts/timelines.tsx b/renderer/components/layouts/timelines.tsx index 24d6166a..0d402f2d 100644 --- a/renderer/components/layouts/timelines.tsx +++ b/renderer/components/layouts/timelines.tsx @@ -4,7 +4,7 @@ import generator, { Entity, MegalodonInterface } from 'megalodon' import { useRouter } from 'next/router' import { createContext, useContext, useEffect, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' -import { FaBell, FaBookmark, FaGlobe, FaHouse, FaList, FaStar, FaUsers, FaHashtag } from 'react-icons/fa6' +import { FaBell, FaBookmark, FaGlobe, FaHouse, FaList, FaStar, FaUsers, FaHashtag, FaUserPlus } from 'react-icons/fa6' import { useIntl } from 'react-intl' import Jump from '../Jump' import { useUnreads } from '@/provider/unreads' @@ -37,13 +37,14 @@ export type Timeline = { export default function Layout({ children }: LayoutProps) { const router = useRouter() const { formatMessage } = useIntl() - const { unreads } = useUnreads() + const { unreads, setUnreads } = useUnreads() const [account, setAccount] = useState(null) const [lists, setLists] = useState>([]) const [followedTags, setFollowedTags] = useState>([]) const [openJump, setOpenJump] = useState(false) const [client, setClient] = useState() + const [followRequests, setFollowRequests] = useState>([]) useHotkeys('mod+k', () => setOpenJump(current => !current)) @@ -68,10 +69,37 @@ export default function Layout({ children }: LayoutProps) { } f() const g = async () => { - const res = await c.getFollowedTags() - setFollowedTags(res.data) + try { + const res = await c.getFollowedTags() + setFollowedTags(res.data) + } catch (err) { + setFollowedTags([]) + console.error(err) + } } g() + const r = async () => { + const res = await c.getFollowRequests() + if (res.data.length > 0) { + setUnreads(current => + Object.assign({}, current, { + [`${account.id?.toString()}_follow_requests`]: res.data.length + }) + ) + console.log(unreads) + setFollowRequests([ + { + id: 'follow_requests', + title: formatMessage({ id: 'timeline.follow_requests' }), + icon: , + path: `/accounts/${router.query.id}/follow_requests` + } + ]) + } else { + setFollowRequests([]) + } + } + r() }, [account]) const pages: Array = [ @@ -127,7 +155,7 @@ export default function Layout({ children }: LayoutProps) {

@{account?.domain}

- {pages.map(page => ( + {pages.concat(followRequests).map(page => ( {page.icon} {page.title} - {page.id === 'notifications' && unreads[account?.id?.toString()] ? ( + {(page.id === 'notifications' && unreads[account?.id?.toString()]) || + (page.id === 'follow_requests' && unreads[`${account.id.toString()}_follow_requests`]) ? ( , index: number) => void +} + +export default function FollowRequests(props: Props) { + const router = useRouter() + const { setUnreads } = useUnreads() + const [requests, setRequests] = useState>([]) + + useEffect(() => { + refreshRequests() + }, []) + + const refreshRequests = async () => { + const res = await props.client.getFollowRequests() + setRequests(res.data) + return res.data + } + + const updateUnreads = (length: number) => { + setUnreads(current => + Object.assign({}, current, { + [`${props.account.id?.toString()}_follow_requests`]: length + }) + ) + } + + const timelineClass = () => { + if (router.query.detail) { + return 'timeline-with-drawer' + } + return 'timeline' + } + + return ( + <> +
+
+
+
+ +
+
+
+ {requests.map(r => ( + <> + { + const data = await refreshRequests() + updateUnreads(data.length) + }} + /> + + ))} +
+
+ +
+ + ) +} diff --git a/renderer/components/timelines/followRequests/User.tsx b/renderer/components/timelines/followRequests/User.tsx new file mode 100644 index 00000000..5ccbca01 --- /dev/null +++ b/renderer/components/timelines/followRequests/User.tsx @@ -0,0 +1,58 @@ +import emojify from '@/utils/emojify' +import { Avatar } from '@material-tailwind/react' +import { Entity, MegalodonInterface } from 'megalodon' +import { useRouter } from 'next/router' +import { FaCheck, FaXmark } from 'react-icons/fa6' +import { useIntl } from 'react-intl' + +type Props = { + user: Entity.Account | Entity.FollowRequest + client: MegalodonInterface + refresh: () => Promise +} + +export default function User(props: Props) { + const router = useRouter() + const { formatMessage } = useIntl() + + const openUser = (id: string) => { + router.push({ query: { id: router.query.id, timeline: router.query.timeline, user_id: id, detail: true } }) + } + + const authorize = async () => { + await props.client.acceptFollowRequest(`${props.user.id}`) + await props.refresh() + } + + const reject = async () => { + await props.client.rejectFollowRequest(`${props.user.id}`) + await props.refresh() + } + + return ( +
+
+
openUser(`${props.user.id}`)}> +
+ +
+
+

+

@{props.user.acct}

+
+
+
+ + +
+
+
+ ) +} diff --git a/renderer/pages/accounts/[id]/[timeline].tsx b/renderer/pages/accounts/[id]/[timeline].tsx index 8b3eaab3..3df9d5c1 100644 --- a/renderer/pages/accounts/[id]/[timeline].tsx +++ b/renderer/pages/accounts/[id]/[timeline].tsx @@ -7,6 +7,7 @@ import Notifications from '@/components/timelines/Notifications' import Search from '@/components/timelines/Search' import Media from '@/components/Media' import Report from '@/components/report/Report' +import FollowRequests from '@/components/timelines/FollowRequests' export default function Page() { const router = useRouter() @@ -58,39 +59,56 @@ export default function Page() { }) } + const timeline = (timeline: string) => { + switch (timeline) { + case 'notifications': + return ( + , index: number) => + dispatch({ target: 'media', value: true, object: media, index: index }) + } + /> + ) + case 'search': + return ( + , index: number) => + dispatch({ target: 'media', value: true, object: media, index: index }) + } + /> + ) + case 'follow_requests': + return ( + , index: number) => + dispatch({ target: 'media', value: true, object: media, index: index }) + } + /> + ) + default: + return ( + , index: number) => + dispatch({ target: 'media', value: true, object: media, index: index }) + } + /> + ) + } + } + if (!account || !client) return null return ( <> - {(router.query.timeline as string) === 'notifications' ? ( - , index: number) => - dispatch({ target: 'media', value: true, object: media, index: index }) - } - /> - ) : ( - <> - {(router.query.timeline as string) === 'search' ? ( - , index: number) => - dispatch({ target: 'media', value: true, object: media, index: index }) - } - /> - ) : ( - , index: number) => - dispatch({ target: 'media', value: true, object: media, index: index }) - } - /> - )} - - )} + {timeline(router.query.timeline as string)} dispatch({ target: 'media', value: false, object: [], index: -1 })}