refs #4798 Add FollowRequests menu
This commit is contained in:
parent
8994dbfd0e
commit
fc3cbcc603
|
@ -6,6 +6,7 @@
|
||||||
"public": "Public",
|
"public": "Public",
|
||||||
"favourites": "Favourites",
|
"favourites": "Favourites",
|
||||||
"bookmarks": "Bookmarks",
|
"bookmarks": "Bookmarks",
|
||||||
|
"follow_requests": "Follow Requests",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"status": {
|
"status": {
|
||||||
"avatar": "Aavtar of {user}",
|
"avatar": "Aavtar of {user}",
|
||||||
|
@ -100,6 +101,10 @@
|
||||||
"body": "{user} requested to follow you"
|
"body": "{user} requested to follow you"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"follow_requests": {
|
||||||
|
"authorize": "Authorize",
|
||||||
|
"reject": "Reject"
|
||||||
|
},
|
||||||
"compose": {
|
"compose": {
|
||||||
"placeholder": "What's on your mind?",
|
"placeholder": "What's on your mind?",
|
||||||
"spoiler": {
|
"spoiler": {
|
||||||
|
|
|
@ -187,7 +187,7 @@ export default function New(props: NewProps) {
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="text"
|
variant="text"
|
||||||
color="blue"
|
color="blue"
|
||||||
title={formatMessage({ id: 'accounts.copy_authorization_url' })}
|
title={formatMessage({ id: 'accounts.new.copy_authorization_url' })}
|
||||||
onClick={() => copyText(appData.url)}
|
onClick={() => copyText(appData.url)}
|
||||||
>
|
>
|
||||||
<FaClipboard className="text-xl" />
|
<FaClipboard className="text-xl" />
|
||||||
|
|
|
@ -4,7 +4,7 @@ import generator, { Entity, MegalodonInterface } from 'megalodon'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
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 { useIntl } from 'react-intl'
|
||||||
import Jump from '../Jump'
|
import Jump from '../Jump'
|
||||||
import { useUnreads } from '@/provider/unreads'
|
import { useUnreads } from '@/provider/unreads'
|
||||||
|
@ -37,13 +37,14 @@ export type Timeline = {
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { formatMessage } = useIntl()
|
const { formatMessage } = useIntl()
|
||||||
const { unreads } = useUnreads()
|
const { unreads, setUnreads } = useUnreads()
|
||||||
|
|
||||||
const [account, setAccount] = useState<Account | null>(null)
|
const [account, setAccount] = useState<Account | null>(null)
|
||||||
const [lists, setLists] = useState<Array<Entity.List>>([])
|
const [lists, setLists] = useState<Array<Entity.List>>([])
|
||||||
const [followedTags, setFollowedTags] = useState<Array<Entity.Tag>>([])
|
const [followedTags, setFollowedTags] = useState<Array<Entity.Tag>>([])
|
||||||
const [openJump, setOpenJump] = useState(false)
|
const [openJump, setOpenJump] = useState(false)
|
||||||
const [client, setClient] = useState<MegalodonInterface>()
|
const [client, setClient] = useState<MegalodonInterface>()
|
||||||
|
const [followRequests, setFollowRequests] = useState<Array<Timeline>>([])
|
||||||
|
|
||||||
useHotkeys('mod+k', () => setOpenJump(current => !current))
|
useHotkeys('mod+k', () => setOpenJump(current => !current))
|
||||||
|
|
||||||
|
@ -68,10 +69,37 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
}
|
}
|
||||||
f()
|
f()
|
||||||
const g = async () => {
|
const g = async () => {
|
||||||
|
try {
|
||||||
const res = await c.getFollowedTags()
|
const res = await c.getFollowedTags()
|
||||||
setFollowedTags(res.data)
|
setFollowedTags(res.data)
|
||||||
|
} catch (err) {
|
||||||
|
setFollowedTags([])
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g()
|
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: <FaUserPlus />,
|
||||||
|
path: `/accounts/${router.query.id}/follow_requests`
|
||||||
|
}
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
setFollowRequests([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r()
|
||||||
}, [account])
|
}, [account])
|
||||||
|
|
||||||
const pages: Array<Timeline> = [
|
const pages: Array<Timeline> = [
|
||||||
|
@ -127,7 +155,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
<p>@{account?.domain}</p>
|
<p>@{account?.domain}</p>
|
||||||
</div>
|
</div>
|
||||||
<List className="min-w-[64px]">
|
<List className="min-w-[64px]">
|
||||||
{pages.map(page => (
|
{pages.concat(followRequests).map(page => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={page.id}
|
key={page.id}
|
||||||
selected={router.asPath.includes(page.path)}
|
selected={router.asPath.includes(page.path)}
|
||||||
|
@ -137,10 +165,11 @@ export default function Layout({ children }: LayoutProps) {
|
||||||
>
|
>
|
||||||
<ListItemPrefix>{page.icon}</ListItemPrefix>
|
<ListItemPrefix>{page.icon}</ListItemPrefix>
|
||||||
<span className="sidebar-menu text-ellipsis whitespace-nowrap overflow-hidden">{page.title}</span>
|
<span className="sidebar-menu text-ellipsis whitespace-nowrap overflow-hidden">{page.title}</span>
|
||||||
{page.id === 'notifications' && unreads[account?.id?.toString()] ? (
|
{(page.id === 'notifications' && unreads[account?.id?.toString()]) ||
|
||||||
|
(page.id === 'follow_requests' && unreads[`${account.id.toString()}_follow_requests`]) ? (
|
||||||
<ListItemSuffix className="sidebar-toolchip">
|
<ListItemSuffix className="sidebar-toolchip">
|
||||||
<Chip
|
<Chip
|
||||||
value={unreads[account.id.toString()]}
|
value={unreads[account.id.toString()] || unreads[`${account.id.toString()}_follow_requests`]}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full theme-text-primary theme-badge"
|
className="rounded-full theme-text-primary theme-badge"
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Entity, MegalodonInterface } from 'megalodon'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { FormattedMessage } from 'react-intl'
|
||||||
|
import User from './followRequests/User'
|
||||||
|
import Detail from '../detail/Detail'
|
||||||
|
import { Account } from '@/db'
|
||||||
|
import { useUnreads } from '@/provider/unreads'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
client: MegalodonInterface
|
||||||
|
account: Account
|
||||||
|
openMedia: (media: Array<Entity.Attachment>, index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FollowRequests(props: Props) {
|
||||||
|
const router = useRouter()
|
||||||
|
const { setUnreads } = useUnreads()
|
||||||
|
const [requests, setRequests] = useState<Array<Entity.FollowRequest | Entity.Account>>([])
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className="flex timeline-wrapper">
|
||||||
|
<section className={`h-full ${timelineClass()}`}>
|
||||||
|
<div className="w-full theme-bg theme-text-primary p-2 flex justify-between">
|
||||||
|
<div className="text-lg font-bold cursor-pointer">
|
||||||
|
<FormattedMessage id="timeline.follow_requests" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 44px)' }}>
|
||||||
|
{requests.map(r => (
|
||||||
|
<>
|
||||||
|
<User
|
||||||
|
key={r.id}
|
||||||
|
user={r}
|
||||||
|
client={props.client}
|
||||||
|
refresh={async () => {
|
||||||
|
const data = await refreshRequests()
|
||||||
|
updateUnreads(data.length)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Detail client={props.client} account={props.account} className="detail" openMedia={props.openMedia} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="border-b border-gray-200 dark:border-gray-700 mr-2 p-1">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex" onClick={() => openUser(`${props.user.id}`)}>
|
||||||
|
<div className="p2 cursor-pointer" style={{ width: '56px' }}>
|
||||||
|
<Avatar src={props.user.avatar} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className="text-gray-800 dark:text-gray-200"
|
||||||
|
dangerouslySetInnerHTML={{ __html: emojify(props.user.display_name, props.user.emojis) }}
|
||||||
|
/>
|
||||||
|
<p className="text-gray-500">@{props.user.acct}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button title={formatMessage({ id: 'follow_requests.authorize' })} onClick={authorize}>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
<button title={formatMessage({ id: 'follow_requests.reject' })} onClick={reject}>
|
||||||
|
<FaXmark />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import Notifications from '@/components/timelines/Notifications'
|
||||||
import Search from '@/components/timelines/Search'
|
import Search from '@/components/timelines/Search'
|
||||||
import Media from '@/components/Media'
|
import Media from '@/components/Media'
|
||||||
import Report from '@/components/report/Report'
|
import Report from '@/components/report/Report'
|
||||||
|
import FollowRequests from '@/components/timelines/FollowRequests'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -58,10 +59,10 @@ export default function Page() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account || !client) return null
|
const timeline = (timeline: string) => {
|
||||||
|
switch (timeline) {
|
||||||
|
case 'notifications':
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{(router.query.timeline as string) === 'notifications' ? (
|
|
||||||
<Notifications
|
<Notifications
|
||||||
account={account}
|
account={account}
|
||||||
client={client}
|
client={client}
|
||||||
|
@ -69,9 +70,9 @@ export default function Page() {
|
||||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
<>
|
case 'search':
|
||||||
{(router.query.timeline as string) === 'search' ? (
|
return (
|
||||||
<Search
|
<Search
|
||||||
client={client}
|
client={client}
|
||||||
account={account}
|
account={account}
|
||||||
|
@ -79,7 +80,19 @@ export default function Page() {
|
||||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
|
case 'follow_requests':
|
||||||
|
return (
|
||||||
|
<FollowRequests
|
||||||
|
client={client}
|
||||||
|
account={account}
|
||||||
|
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||||
|
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
<Timeline
|
<Timeline
|
||||||
timeline={router.query.timeline as string}
|
timeline={router.query.timeline as string}
|
||||||
account={account}
|
account={account}
|
||||||
|
@ -88,9 +101,14 @@ export default function Page() {
|
||||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
</>
|
}
|
||||||
)}
|
}
|
||||||
|
|
||||||
|
if (!account || !client) return null
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{timeline(router.query.timeline as string)}
|
||||||
<Media
|
<Media
|
||||||
open={modalState.media.opened}
|
open={modalState.media.opened}
|
||||||
close={() => dispatch({ target: 'media', value: false, object: [], index: -1 })}
|
close={() => dispatch({ target: 'media', value: false, object: [], index: -1 })}
|
||||||
|
|
Loading…
Reference in New Issue