refs #4653 Add media view
This commit is contained in:
parent
2124cc7516
commit
112c310ecb
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue