Merge pull request #5088 from h3poteto/iss-4798/follow_requests
refs #4798 Add FollowRequests menu
This commit is contained in:
commit
cd7402985b
|
@ -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": {
|
||||
|
|
|
@ -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)}
|
||||
>
|
||||
<FaClipboard className="text-xl" />
|
||||
|
|
|
@ -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<Account | null>(null)
|
||||
const [lists, setLists] = useState<Array<Entity.List>>([])
|
||||
const [followedTags, setFollowedTags] = useState<Array<Entity.Tag>>([])
|
||||
const [openJump, setOpenJump] = useState(false)
|
||||
const [client, setClient] = useState<MegalodonInterface>()
|
||||
const [followRequests, setFollowRequests] = useState<Array<Timeline>>([])
|
||||
|
||||
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: <FaUserPlus />,
|
||||
path: `/accounts/${router.query.id}/follow_requests`
|
||||
}
|
||||
])
|
||||
} else {
|
||||
setFollowRequests([])
|
||||
}
|
||||
}
|
||||
r()
|
||||
}, [account])
|
||||
|
||||
const pages: Array<Timeline> = [
|
||||
|
@ -127,7 +155,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||
<p>@{account?.domain}</p>
|
||||
</div>
|
||||
<List className="min-w-[64px]">
|
||||
{pages.map(page => (
|
||||
{pages.concat(followRequests).map(page => (
|
||||
<ListItem
|
||||
key={page.id}
|
||||
selected={router.asPath.includes(page.path)}
|
||||
|
@ -137,10 +165,11 @@ export default function Layout({ children }: LayoutProps) {
|
|||
>
|
||||
<ListItemPrefix>{page.icon}</ListItemPrefix>
|
||||
<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">
|
||||
<Chip
|
||||
value={unreads[account.id.toString()]}
|
||||
value={unreads[account.id.toString()] || unreads[`${account.id.toString()}_follow_requests`]}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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 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 (
|
||||
<Notifications
|
||||
account={account}
|
||||
client={client}
|
||||
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||
}
|
||||
/>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<Search
|
||||
client={client}
|
||||
account={account}
|
||||
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||
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={router.query.timeline as string}
|
||||
account={account}
|
||||
client={client}
|
||||
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!account || !client) return null
|
||||
return (
|
||||
<>
|
||||
{(router.query.timeline as string) === 'notifications' ? (
|
||||
<Notifications
|
||||
account={account}
|
||||
client={client}
|
||||
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{(router.query.timeline as string) === 'search' ? (
|
||||
<Search
|
||||
client={client}
|
||||
account={account}
|
||||
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Timeline
|
||||
timeline={router.query.timeline as string}
|
||||
account={account}
|
||||
client={client}
|
||||
openMedia={(media: Array<Entity.Attachment>, index: number) =>
|
||||
dispatch({ target: 'media', value: true, object: media, index: index })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{timeline(router.query.timeline as string)}
|
||||
<Media
|
||||
open={modalState.media.opened}
|
||||
close={() => dispatch({ target: 'media', value: false, object: [], index: -1 })}
|
||||
|
|
Loading…
Reference in New Issue