Whalebird-desktop-client-ma.../renderer/components/timelines/Timeline.tsx

201 lines
6.6 KiB
TypeScript
Raw Normal View History

2023-11-03 12:37:40 +01:00
import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
2023-11-04 16:22:50 +01:00
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
2023-12-02 11:26:45 +01:00
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
2023-11-03 12:37:40 +01:00
import { Virtuoso } from 'react-virtuoso'
import Status from './status/Status'
2023-11-04 07:32:37 +01:00
import { FormattedMessage, useIntl } from 'react-intl'
2023-11-15 16:15:44 +01:00
import Detail from '../detail/Detail'
2023-11-17 15:01:54 +01:00
import { useRouter } from 'next/router'
2023-11-21 16:56:26 +01:00
import Compose from '../compose/Compose'
2023-11-03 12:37:40 +01:00
2023-11-05 17:09:32 +01:00
const TIMELINE_STATUSES_COUNT = 30
2023-11-04 16:22:50 +01:00
const TIMELINE_MAX_STATUSES = 2147483647
2023-11-03 12:37:40 +01:00
type Props = {
timeline: string
account: Account
client: MegalodonInterface
2023-12-02 11:26:45 +01:00
setAttachment: Dispatch<SetStateAction<Entity.Attachment | null>>
2023-11-03 12:37:40 +01:00
}
export default function Timeline(props: Props) {
const [statuses, setStatuses] = useState<Array<Entity.Status>>([])
2023-11-04 16:22:50 +01:00
const [unreads, setUnreads] = useState<Array<Entity.Status>>([])
const [firstItemIndex, setFirstItemIndex] = useState(TIMELINE_MAX_STATUSES)
2023-11-21 16:56:26 +01:00
const [composeHeight, setComposeHeight] = useState(120)
2023-11-04 16:22:50 +01:00
2023-11-17 15:01:54 +01:00
const router = useRouter()
2023-11-04 07:32:37 +01:00
const { formatMessage } = useIntl()
2023-11-04 16:22:50 +01:00
const scrollerRef = useRef<HTMLElement | null>(null)
const streaming = useRef<WebSocketInterface | null>(null)
2023-11-23 06:07:48 +01:00
const composeRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const observer = new ResizeObserver(entries => {
entries.forEach(el => {
setComposeHeight(el.contentRect.height)
})
})
if (composeRef.current) {
observer.observe(composeRef.current)
}
return () => {
observer.disconnect()
}
}, [])
2023-11-03 12:37:40 +01:00
useEffect(() => {
const f = async () => {
const res = await loadTimeline(props.timeline, props.client)
setStatuses(res)
2023-11-04 16:22:50 +01:00
const instance = await props.client.getInstance()
const c = generator(props.account.sns, instance.data.urls.streaming_api, props.account.access_token, 'Whalebird')
switch (props.timeline) {
case 'home': {
streaming.current = c.userSocket()
break
}
case 'local': {
streaming.current = c.localSocket()
break
}
case 'public': {
streaming.current = c.publicSocket()
break
}
}
if (streaming.current) {
streaming.current.on('connect', () => {
console.log(`connected to ${props.timeline}`)
})
streaming.current.on('update', (status: Entity.Status) => {
if (scrollerRef.current && scrollerRef.current.scrollTop > 10) {
setUnreads(current => [status, ...current])
} else {
setStatuses(current => [status, ...current])
}
})
}
2023-11-03 12:37:40 +01:00
}
f()
2023-11-04 16:22:50 +01:00
return () => {
if (streaming.current) {
streaming.current.removeAllListeners()
streaming.current.stop()
2023-11-09 15:29:10 +01:00
streaming.current = null
2023-11-04 16:22:50 +01:00
console.log(`closed ${props.timeline}`)
}
}
}, [props.timeline, props.client, props.account])
2023-11-03 12:37:40 +01:00
const loadTimeline = async (tl: string, client: MegalodonInterface, maxId?: string): Promise<Array<Entity.Status>> => {
let options = { limit: 30 }
if (maxId) {
options = Object.assign({}, options, { max_id: maxId })
}
switch (tl) {
case 'home': {
const res = await client.getHomeTimeline(options)
return res.data
}
case 'local': {
const res = await client.getLocalTimeline(options)
return res.data
}
case 'public': {
const res = await client.getPublicTimeline(options)
return res.data
}
default: {
return []
}
}
}
2023-11-04 05:03:56 +01:00
const updateStatus = (current: Array<Entity.Status>, status: Entity.Status) => {
const renew = current.map(s => {
if (s.id === status.id) {
return status
} else if (s.reblog && s.reblog.id === status.id) {
return Object.assign({}, s, { reblog: status })
} else if (status.reblog && s.id === status.reblog.id) {
return status.reblog
} else if (status.reblog && s.reblog && s.reblog.id === status.reblog.id) {
return Object.assign({}, s, { reblog: status.reblog })
} else {
return s
}
})
return renew
}
2023-11-04 15:03:47 +01:00
const loadMore = useCallback(async () => {
console.debug('appending')
const maxId = statuses[statuses.length - 1].id
const append = await loadTimeline(props.timeline, props.client, maxId)
setStatuses(last => [...last, ...append])
}, [props.client, statuses, setStatuses])
2023-11-04 16:22:50 +01:00
const prependUnreads = useCallback(() => {
console.debug('prepending')
const u = unreads.slice().reverse().slice(0, TIMELINE_STATUSES_COUNT).reverse()
const remains = u.slice(0, -1 * TIMELINE_STATUSES_COUNT)
setUnreads(() => remains)
setFirstItemIndex(() => firstItemIndex - u.length)
setStatuses(() => [...u, ...statuses])
return false
}, [firstItemIndex, statuses, setStatuses, unreads])
2023-11-17 15:01:54 +01:00
const timelineClass = () => {
2023-11-28 16:37:21 +01:00
if (router.query.detail) {
2023-11-17 15:01:54 +01:00
return 'timeline-with-drawer'
}
return 'timeline'
}
2023-11-03 12:37:40 +01:00
return (
<div className="flex timeline-wrapper">
2023-11-17 15:01:54 +01:00
<section className={`h-full ${timelineClass()}`}>
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
<div className="text-lg font-bold">
<FormattedMessage id={`timeline.${props.timeline}`} />
</div>
<div className="w-64 text-xs">
<form>
<TextInput type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled sizing="sm" />
</form>
</div>
2023-11-04 07:32:37 +01:00
</div>
2023-11-28 16:37:21 +01:00
<div className="overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
2023-11-17 15:01:54 +01:00
<Virtuoso
2023-11-21 16:56:26 +01:00
style={{ height: `calc(100% - ${composeHeight}px)` }}
2023-11-17 15:01:54 +01:00
scrollerRef={ref => {
scrollerRef.current = ref as HTMLElement
}}
2023-12-02 07:13:28 +01:00
className="timeline-scrollable"
2023-11-17 15:01:54 +01:00
firstItemIndex={firstItemIndex}
atTopStateChange={prependUnreads}
data={statuses}
endReached={loadMore}
itemContent={(_, status) => (
<Status
client={props.client}
status={status}
key={status.id}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
2023-12-02 11:26:45 +01:00
openMedia={media => props.setAttachment(media)}
2023-11-17 15:01:54 +01:00
/>
)}
/>
2023-11-23 06:07:48 +01:00
<div ref={composeRef}>
<Compose client={props.client} />
</div>
2023-11-03 12:37:40 +01:00
</div>
2023-11-17 15:01:54 +01:00
</section>
2023-12-02 11:26:45 +01:00
<Detail client={props.client} className="detail" openMedia={media => props.setAttachment(media)} />
2023-11-17 15:01:54 +01:00
</div>
2023-11-03 12:37:40 +01:00
)
}