refs #4653 Enable detail panel in notifications
This commit is contained in:
parent
add0d5bb29
commit
b7808289a0
|
@ -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,47 +102,57 @@ 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">
|
||||||
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
|
<section className={`h-full ${timelineClass()}`}>
|
||||||
<div className="text-lg font-bold">
|
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
|
||||||
<FormattedMessage id="timeline.notifications" />
|
<div className="text-lg font-bold">
|
||||||
</div>
|
<FormattedMessage id="timeline.notifications" />
|
||||||
<div className="w-64 text-xs">
|
|
||||||
<form>
|
|
||||||
<Input type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
|
|
||||||
{notifications.length > 0 ? (
|
|
||||||
<Virtuoso
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
scrollerRef={ref => {
|
|
||||||
scrollerRef.current = ref as HTMLElement
|
|
||||||
}}
|
|
||||||
firstItemIndex={firstItemIndex}
|
|
||||||
atTopStateChange={prependUnreads}
|
|
||||||
className="timeline-scrollable"
|
|
||||||
data={notifications}
|
|
||||||
endReached={loadMore}
|
|
||||||
itemContent={(_, notification) => (
|
|
||||||
<Notification
|
|
||||||
client={props.client}
|
|
||||||
account={props.account}
|
|
||||||
notification={notification}
|
|
||||||
onRefresh={updateStatus}
|
|
||||||
key={notification.id}
|
|
||||||
openMedia={media => props.setAttachment(media)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-full pt-6" style={{ height: '100%' }}>
|
|
||||||
<Spinner className="m-auto" />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-64 text-xs">
|
||||||
</div>
|
<form>
|
||||||
</section>
|
<Input type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
|
||||||
|
{notifications.length > 0 ? (
|
||||||
|
<Virtuoso
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
scrollerRef={ref => {
|
||||||
|
scrollerRef.current = ref as HTMLElement
|
||||||
|
}}
|
||||||
|
firstItemIndex={firstItemIndex}
|
||||||
|
atTopStateChange={prependUnreads}
|
||||||
|
className="timeline-scrollable"
|
||||||
|
data={notifications}
|
||||||
|
endReached={loadMore}
|
||||||
|
itemContent={(_, notification) => (
|
||||||
|
<Notification
|
||||||
|
client={props.client}
|
||||||
|
account={props.account}
|
||||||
|
notification={notification}
|
||||||
|
onRefresh={updateStatus}
|
||||||
|
key={notification.id}
|
||||||
|
openMedia={media => props.setAttachment(media)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full pt-6" style={{ height: '100%' }}>
|
||||||
|
<Spinner className="m-auto" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Detail client={props.client} account={props.account} className="detail" openMedia={media => props.setAttachment(media)} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
return (
|
||||||
case 'notifications': {
|
<>
|
||||||
return <Notifications account={account} client={client} setAttachment={setAttachment} />
|
{(router.query.timeline as string) === 'notifications' ? (
|
||||||
}
|
<Notifications account={account} client={client} setAttachment={setAttachment} />
|
||||||
default: {
|
) : (
|
||||||
return (
|
<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} />}
|
</>
|
||||||
</>
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue