Merge pull request #4777 from h3poteto/iss-4653/notification-detail

refs #4653 Enable detail panel in notifications
This commit is contained in:
AkiraFukushima 2024-01-15 00:06:47 +09:00 committed by GitHub
commit 1f6683018d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 63 deletions

View File

@ -5,6 +5,8 @@ import { FormattedMessage, useIntl } from 'react-intl'
import { Virtuoso } from 'react-virtuoso' import { Virtuoso } from 'react-virtuoso'
import Notification from './notification/Notification' import Notification from './notification/Notification'
import { Input, Spinner } from '@material-tailwind/react' import { Input, Spinner } from '@material-tailwind/react'
import { useRouter } from 'next/router'
import Detail from '../detail/Detail'
const TIMELINE_STATUSES_COUNT = 30 const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647 const TIMELINE_MAX_STATUSES = 2147483647
@ -23,6 +25,7 @@ export default function Notifications(props: Props) {
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
const scrollerRef = useRef<HTMLElement | null>(null) const scrollerRef = useRef<HTMLElement | null>(null)
const streaming = useRef<WebSocketInterface | null>(null) const streaming = useRef<WebSocketInterface | null>(null)
const router = useRouter()
useEffect(() => { useEffect(() => {
const f = async () => { const f = async () => {
@ -99,8 +102,16 @@ export default function Notifications(props: Props) {
return false return false
}, [firstItemIndex, notifications, setNotifications, unreads]) }, [firstItemIndex, notifications, setNotifications, unreads])
const timelineClass = () => {
if (router.query.detail) {
return 'timeline-with-drawer'
}
return 'timeline'
}
return ( return (
<section className="h-full timeline-wrapper"> <div className="flex timeline-wrapper">
<section className={`h-full ${timelineClass()}`}>
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between"> <div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
<div className="text-lg font-bold"> <div className="text-lg font-bold">
<FormattedMessage id="timeline.notifications" /> <FormattedMessage id="timeline.notifications" />
@ -141,5 +152,7 @@ export default function Notifications(props: Props) {
)} )}
</div> </div>
</section> </section>
<Detail client={props.client} account={props.account} className="detail" openMedia={media => props.setAttachment(media)} />
</div>
) )
} }

View File

@ -3,6 +3,7 @@ import { FaUserPlus } from 'react-icons/fa6'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import emojify from '@/utils/emojify' import emojify from '@/utils/emojify'
import { Avatar } from '@material-tailwind/react' import { Avatar } from '@material-tailwind/react'
import { useRouter } from 'next/router'
type Props = { type Props = {
notification: Entity.Notification notification: Entity.Notification
@ -10,6 +11,11 @@ type Props = {
export default function Follow(props: Props) { export default function Follow(props: Props) {
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
const router = useRouter()
const openUser = (id: string) => {
router.push({ query: { id: router.query.id, timeline: router.query.timeline, user_id: id, detail: true } })
}
return ( return (
<div className="border-b mr-2 py-1"> <div className="border-b mr-2 py-1">
@ -17,7 +23,7 @@ export default function Follow(props: Props) {
<div style={{ width: '56px' }}> <div style={{ width: '56px' }}>
<FaUserPlus className="text-blue-600 w-4 mr-2 ml-auto" /> <FaUserPlus className="text-blue-600 w-4 mr-2 ml-auto" />
</div> </div>
<div style={{ width: 'calc(100% - 56px)' }}> <div className="cursor-pointer" style={{ width: 'calc(100% - 56px)' }} onClick={() => openUser(props.notification.account.id)}>
<span <span
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: emojify( __html: emojify(
@ -34,11 +40,16 @@ export default function Follow(props: Props) {
</div> </div>
</div> </div>
<div className="flex"> <div className="flex">
<div className="p-2" style={{ width: '56px' }}> <div className="p-2 cursor-pointer" style={{ width: '56px' }}>
<Avatar src={props.notification.account.avatar} /> <Avatar
src={props.notification.account.avatar}
onClick={() => openUser(props.notification.account.id)}
variant="rounded"
style={{ width: '40px', height: '40px' }}
/>
</div> </div>
<div style={{ width: 'calc(100% - 56px)' }}> <div style={{ width: 'calc(100% - 56px)' }}>
<div className="flex"> <div className="flex cursor-pointer" onClick={() => openUser(props.notification.account.id)}>
<span <span
className="text-gray-950 text-ellipsis break-all overflow-hidden" className="text-gray-950 text-ellipsis break-all overflow-hidden"
dangerouslySetInnerHTML={{ __html: emojify(props.notification.account.display_name, props.notification.account.emojis) }} dangerouslySetInnerHTML={{ __html: emojify(props.notification.account.display_name, props.notification.account.emojis) }}

View File

@ -9,6 +9,7 @@ import { FaBarsProgress, FaHouse, FaPenToSquare, FaRetweet, FaStar } from 'react
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import { useState } from 'react' import { useState } from 'react'
import { Avatar } from '@material-tailwind/react' import { Avatar } from '@material-tailwind/react'
import { useRouter } from 'next/router'
type Props = { type Props = {
notification: Entity.Notification notification: Entity.Notification
@ -21,17 +22,26 @@ export default function Reaction(props: Props) {
const status = props.notification.status const status = props.notification.status
const [spoilered, setSpoilered] = useState(status.spoiler_text.length > 0) const [spoilered, setSpoilered] = useState(status.spoiler_text.length > 0)
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
const router = useRouter()
const refresh = async () => { const refresh = async () => {
const res = await props.client.getStatus(status.id) const res = await props.client.getStatus(status.id)
props.onRefresh(res.data) props.onRefresh(res.data)
} }
const openStatus = () => {
router.push({ query: { id: router.query.id, timeline: router.query.timeline, status_id: status.id, detail: true } })
}
const openUser = (id: string) => {
router.push({ query: { id: router.query.id, timeline: router.query.timeline, user_id: id, detail: true } })
}
return ( return (
<div className="border-b mr-2 py-1"> <div className="border-b mr-2 py-1">
<div className="flex items-center"> <div className="flex items-center">
<div style={{ width: '56px' }}>{actionIcon(props.notification)}</div> <div style={{ width: '56px' }}>{actionIcon(props.notification)}</div>
<div style={{ width: 'calc(100% - 56px)' }}> <div className="cursor-pointer" style={{ width: 'calc(100% - 56px)' }} onClick={() => openUser(props.notification.account.id)}>
<span <span
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: emojify( __html: emojify(
@ -48,19 +58,24 @@ export default function Reaction(props: Props) {
</div> </div>
</div> </div>
<div className="flex"> <div className="flex">
<div className="p-2" style={{ width: '56px' }}> <div className="p-2 cursor-pointer" style={{ width: '56px' }}>
<Avatar src={status.account.avatar} /> <Avatar
src={status.account.avatar}
onClick={() => openUser(status.account.id)}
variant="rounded"
style={{ width: '40px', height: '40px' }}
/>
</div> </div>
<div className="text-gray-600 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}> <div className="text-gray-600 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex"> <div className="flex cursor-pointer" onClick={() => openUser(status.account.id)}>
<span <span
className="text-gray-600 text-ellipsis break-all overflow-hidden" className="text-gray-600 text-ellipsis break-all overflow-hidden"
dangerouslySetInnerHTML={{ __html: emojify(status.account.display_name, status.account.emojis) }} dangerouslySetInnerHTML={{ __html: emojify(status.account.display_name, status.account.emojis) }}
></span> ></span>
<span className="text-gray-600 text-ellipsis break-all overflow-hidden">@{status.account.acct}</span> <span className="text-gray-600 text-ellipsis break-all overflow-hidden">@{status.account.acct}</span>
</div> </div>
<div className="text-gray-600 text-right"> <div className="text-gray-600 text-right cursor-pointer" onClick={openStatus}>
<time dateTime={status.created_at}>{dayjs(status.created_at).format('YYYY-MM-DD HH:mm:ss')}</time> <time dateTime={status.created_at}>{dayjs(status.created_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div> </div>
</div> </div>

View File

@ -48,18 +48,15 @@ export default function Page() {
}, [router.query.modal, router.query.report_target_id]) }, [router.query.modal, router.query.report_target_id])
if (!account || !client) return null if (!account || !client) return null
switch (router.query.timeline as string) {
case 'notifications': {
return <Notifications account={account} client={client} setAttachment={setAttachment} />
}
default: {
return ( return (
<> <>
{(router.query.timeline as string) === 'notifications' ? (
<Notifications account={account} client={client} setAttachment={setAttachment} />
) : (
<Timeline timeline={router.query.timeline as string} account={account} client={client} setAttachment={setAttachment} /> <Timeline timeline={router.query.timeline as string} account={account} client={client} setAttachment={setAttachment} />
)}
<Media open={attachment !== null} close={() => setAttachment(null)} attachment={attachment} /> <Media open={attachment !== null} close={() => setAttachment(null)} attachment={attachment} />
{report && <Report open={report !== null} close={() => setReport(null)} status={report} client={client} />} {report && <Report open={report !== null} close={() => setReport(null)} status={report} client={client} />}
</> </>
) )
} }
}
}