refs #4653 Add media view

This commit is contained in:
AkiraFukushima 2023-12-02 19:26:45 +09:00
parent 2124cc7516
commit 112c310ecb
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
13 changed files with 79 additions and 19 deletions

View File

@ -0,0 +1,26 @@
import { Modal } from 'flowbite-react'
import { Entity } from 'megalodon'
type Props = {
open: boolean
close: () => void
attachment: Entity.Attachment | null
}
export default function Media(props: Props) {
return (
<Modal show={props.open} onClose={props.close} size="6xl">
<Modal.Header />
<Modal.Body className="max-h-full max-w-full">
{props.attachment && (
<img
src={props.attachment.url}
alt={props.attachment.description}
title={props.attachment.description}
className="object-contain max-h-full max-w-full m-auto"
/>
)}
</Modal.Body>
</Modal>
)
}

View File

@ -2,12 +2,13 @@ import { useRouter } from 'next/router'
import { HTMLAttributes, useEffect, useState } from 'react'
import { FaChevronLeft, FaX } from 'react-icons/fa6'
import Thread from './Thread'
import { MegalodonInterface } from 'megalodon'
import { Entity, MegalodonInterface } from 'megalodon'
import Reply from './Reply'
import Profile from './Profile'
type Props = {
client: MegalodonInterface
openMedia: (media: Entity.Attachment) => void
} & HTMLAttributes<HTMLElement>
export default function Detail(props: Props) {
@ -42,9 +43,11 @@ export default function Detail(props: Props) {
<FaChevronLeft onClick={back} className="cursor-pointer text-lg" />
<FaX onClick={close} className="cursor-pointer text-lg" />
</div>
{target === 'status' && <Thread client={props.client} status_id={router.query.status_id as string} />}
{target === 'reply' && <Reply client={props.client} status_id={router.query.reply_target_id as string} />}
{target === 'profile' && <Profile client={props.client} user_id={router.query.user_id as string} />}
{target === 'status' && <Thread client={props.client} status_id={router.query.status_id as string} openMedia={props.openMedia} />}
{target === 'reply' && (
<Reply client={props.client} status_id={router.query.reply_target_id as string} openMedia={props.openMedia} />
)}
{target === 'profile' && <Profile client={props.client} user_id={router.query.user_id as string} openMedia={props.openMedia} />}
</div>
)}
</>

View File

@ -11,6 +11,7 @@ import Followers from './profile/Followers'
type Props = {
client: MegalodonInterface
user_id: string
openMedia: (media: Entity.Attachment) => void
}
const customTheme: CustomFlowbiteTheme = {
@ -111,7 +112,7 @@ export default function Profile(props: Props) {
<div>
<Tabs.Group aria-label="Tabs with icons" style="underline">
<Tabs.Item active title={formatMessage({ id: 'profile.timeline' })}>
<Timeline client={props.client} user_id={props.user_id} />
<Timeline client={props.client} user_id={props.user_id} openMedia={props.openMedia} />
</Tabs.Item>
<Tabs.Item title={formatMessage({ id: 'profile.followings' })}>
<Followings client={props.client} user_id={props.user_id} />

View File

@ -7,6 +7,7 @@ import Compose from '../compose/Compose'
type Props = {
client: MegalodonInterface
status_id: string
openMedia: (media: Entity.Attachment) => void
}
export default function Reply(props: Props) {
@ -47,7 +48,9 @@ export default function Reply(props: Props) {
<Virtuoso
style={{ height: `calc(100% - ${composeHeight}px)` }}
data={[...ancestors, status].filter(s => s !== null)}
itemContent={(_, status) => <Status client={props.client} status={status} key={status.id} onRefresh={() => {}} />}
itemContent={(_, status) => (
<Status client={props.client} status={status} key={status.id} onRefresh={() => {}} openMedia={props.openMedia} />
)}
/>
<div ref={composeRef}>
<Compose client={props.client} in_reply_to={status} />

View File

@ -6,6 +6,7 @@ import Status from '../timelines/status/Status'
type Props = {
client: MegalodonInterface
status_id: string
openMedia: (media: Entity.Attachment) => void
}
export default function Thread(props: Props) {
@ -31,7 +32,9 @@ export default function Thread(props: Props) {
<Virtuoso
style={{ height: 'calc(100% - 50px)' }}
data={[...ancestors, status, ...descendants].filter(s => s !== null)}
itemContent={(_, status) => <Status client={props.client} status={status} key={status.id} onRefresh={() => {}} />}
itemContent={(_, status) => (
<Status client={props.client} status={status} key={status.id} onRefresh={() => {}} openMedia={props.openMedia} />
)}
/>
</>
)

View File

@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'
type Props = {
client: MegalodonInterface
user_id: string
openMedia: (media: Entity.Attachment) => void
}
export default function Timeline(props: Props) {
@ -45,6 +46,7 @@ export default function Timeline(props: Props) {
status={status}
key={index}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
openMedia={props.openMedia}
/>
))}
</>

View File

@ -1,7 +1,7 @@
import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { Virtuoso } from 'react-virtuoso'
import Notification from './notification/Notification'
@ -12,6 +12,7 @@ const TIMELINE_MAX_STATUSES = 2147483647
type Props = {
account: Account
client: MegalodonInterface
setAttachment: Dispatch<SetStateAction<Entity.Attachment | null>>
}
export default function Notifications(props: Props) {
@ -121,7 +122,13 @@ export default function Notifications(props: Props) {
data={notifications}
endReached={loadMore}
itemContent={(_, notification) => (
<Notification client={props.client} notification={notification} onRefresh={updateStatus} key={notification.id} />
<Notification
client={props.client}
notification={notification}
onRefresh={updateStatus}
key={notification.id}
openMedia={media => props.setAttachment(media)}
/>
)}
/>
</div>

View File

@ -1,7 +1,7 @@
import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
import { Virtuoso } from 'react-virtuoso'
import Status from './status/Status'
import { FormattedMessage, useIntl } from 'react-intl'
@ -16,6 +16,7 @@ type Props = {
timeline: string
account: Account
client: MegalodonInterface
setAttachment: Dispatch<SetStateAction<Entity.Attachment | null>>
}
export default function Timeline(props: Props) {
const [statuses, setStatuses] = useState<Array<Entity.Status>>([])
@ -184,6 +185,7 @@ export default function Timeline(props: Props) {
status={status}
key={status.id}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
openMedia={media => props.setAttachment(media)}
/>
)}
/>
@ -192,7 +194,7 @@ export default function Timeline(props: Props) {
</div>
</div>
</section>
<Detail client={props.client} className="detail" />
<Detail client={props.client} className="detail" openMedia={media => props.setAttachment(media)} />
</div>
)
}

View File

@ -7,13 +7,14 @@ type Props = {
notification: Entity.Notification
client: MegalodonInterface
onRefresh: (status: Entity.Status) => void
openMedia: (media: Entity.Attachment) => void
}
export default function Notification(props: Props) {
switch (props.notification.type) {
case 'mention': {
if (props.notification.status) {
return <Status client={props.client} status={props.notification.status} onRefresh={props.onRefresh} />
return <Status client={props.client} status={props.notification.status} onRefresh={props.onRefresh} openMedia={props.openMedia} />
} else {
return null
}
@ -28,7 +29,7 @@ export default function Notification(props: Props) {
case 'emoji_reaction':
case 'reaction': {
if (props.notification.status) {
return <Reaction client={props.client} notification={props.notification} onRefresh={props.onRefresh} />
return <Reaction client={props.client} notification={props.notification} onRefresh={props.onRefresh} openMedia={props.openMedia} />
} else {
return null
}

View File

@ -14,6 +14,7 @@ type Props = {
notification: Entity.Notification
client: MegalodonInterface
onRefresh: (status: Entity.Status) => void
openMedia: (media: Entity.Attachment) => void
}
export default function Reaction(props: Props) {
@ -68,7 +69,7 @@ export default function Reaction(props: Props) {
<>
{status.poll && <Poll poll={status.poll} onRefresh={refresh} client={props.client} className="text-gray-600" />}
{status.card && <Card card={status.card} />}
<Media media={status.media_attachments} sensitive={status.sensitive} />
<Media media={status.media_attachments} sensitive={status.sensitive} openMedia={props.openMedia} />
</>
)}
</div>

View File

@ -7,6 +7,7 @@ import { FormattedMessage } from 'react-intl'
type Props = {
media: Array<Entity.Attachment>
sensitive: boolean
openMedia: (media: Entity.Attachment) => void
}
export default function Media(props: Props) {
const [sensitive, setSensitive] = useState(props.sensitive)
@ -26,7 +27,7 @@ export default function Media(props: Props) {
<div className="mt-2 flex flex-wrap gap-2">
{props.media.map((media, key) => (
<div key={key}>
<Attachment attachment={media} />
<Attachment attachment={media} openMedia={() => props.openMedia(media)} />
</div>
))}
</div>
@ -41,6 +42,7 @@ export default function Media(props: Props) {
type AttachmentProps = {
attachment: Entity.Attachment
openMedia: () => void
}
function Attachment(props: AttachmentProps) {
@ -51,6 +53,7 @@ function Attachment(props: AttachmentProps) {
style={{ height: '144px' }}
alt={props.attachment.description}
title={props.attachment.description}
onClick={props.openMedia}
/>
)
}

View File

@ -15,6 +15,7 @@ type Props = {
status: Entity.Status
client: MegalodonInterface
onRefresh: (status: Entity.Status) => void
openMedia: (media: Entity.Attachment) => void
}
export default function Status(props: Props) {
@ -60,7 +61,7 @@ export default function Status(props: Props) {
<>
{status.poll && <Poll poll={status.poll} onRefresh={onRefresh} client={props.client} />}
{status.card && <Card card={status.card} />}
<Media media={status.media_attachments} sensitive={status.sensitive} />
<Media media={status.media_attachments} sensitive={status.sensitive} openMedia={props.openMedia} />
</>
)}

View File

@ -2,13 +2,15 @@ import { useRouter } from 'next/router'
import Timeline from '@/components/timelines/Timeline'
import { useEffect, useState } from 'react'
import { Account, db } from '@/db'
import generator, { MegalodonInterface } from 'megalodon'
import generator, { Entity, MegalodonInterface } from 'megalodon'
import Notifications from '@/components/timelines/Notifications'
import Media from '@/components/Media'
export default function Page() {
const router = useRouter()
const [account, setAccount] = useState<Account | null>(null)
const [client, setClient] = useState<MegalodonInterface>(null)
const [attachment, setAttachment] = useState<Entity.Attachment | null>(null)
useEffect(() => {
if (router.query.id) {
@ -28,10 +30,15 @@ export default function Page() {
if (!account || !client) return null
switch (router.query.timeline as string) {
case 'notifications': {
return <Notifications account={account} client={client} />
return <Notifications account={account} client={client} setAttachment={setAttachment} />
}
default: {
return <Timeline timeline={router.query.timeline as string} account={account} client={client} />
return (
<>
<Timeline timeline={router.query.timeline as string} account={account} client={client} setAttachment={setAttachment} />
<Media open={attachment !== null} close={() => setAttachment(null)} attachment={attachment} />
</>
)
}
}
}