mirror of
https://github.com/h3poteto/whalebird-desktop
synced 2025-02-05 03:38:55 +01:00
Implement timeline statuses
This commit is contained in:
parent
5518619f05
commit
3909148b17
@ -11,12 +11,14 @@
|
|||||||
"postinstall": "electron-builder install-app-deps"
|
"postinstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"electron-serve": "^1.1.0",
|
"electron-serve": "^1.1.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"flowbite": "^2.0.0",
|
"flowbite": "^2.0.0",
|
||||||
"flowbite-react": "^0.6.4",
|
"flowbite-react": "^0.6.4",
|
||||||
"megalodon": "^9.1.1"
|
"megalodon": "^9.1.1",
|
||||||
|
"react-virtuoso": "^4.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/runtime-corejs3": "^7.23.2",
|
"@babel/runtime-corejs3": "^7.23.2",
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.emojione {
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
}
|
||||||
|
67
renderer/components/timelines/Timeline.tsx
Normal file
67
renderer/components/timelines/Timeline.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Account } from '@/db'
|
||||||
|
import { TextInput } from 'flowbite-react'
|
||||||
|
import { Entity, MegalodonInterface } from 'megalodon'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Virtuoso } from 'react-virtuoso'
|
||||||
|
import Status from './status/Status'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
timeline: string
|
||||||
|
account: Account
|
||||||
|
client: MegalodonInterface
|
||||||
|
}
|
||||||
|
export default function Timeline(props: Props) {
|
||||||
|
const [statuses, setStatuses] = useState<Array<Entity.Status>>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const f = async () => {
|
||||||
|
const res = await loadTimeline(props.timeline, props.client)
|
||||||
|
setStatuses(res)
|
||||||
|
}
|
||||||
|
f()
|
||||||
|
}, [props.timeline, props.client])
|
||||||
|
|
||||||
|
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 []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="h-full w-full">
|
||||||
|
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
|
||||||
|
<div className="text-lg font-bold">{props.timeline}</div>
|
||||||
|
<div className="w-64 text-xs">
|
||||||
|
<form>
|
||||||
|
<TextInput type="text" placeholder="search" disabled sizing="sm" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
|
||||||
|
<Virtuoso
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
data={statuses}
|
||||||
|
itemContent={(_, status) => <Status client={props.client} status={status} key={status.id} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
18
renderer/components/timelines/status/Body.tsx
Normal file
18
renderer/components/timelines/status/Body.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Entity } from 'megalodon'
|
||||||
|
import { HTMLAttributes } from 'react'
|
||||||
|
import emojify from '@/utils/emojify'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: Entity.Status
|
||||||
|
} & HTMLAttributes<HTMLElement>
|
||||||
|
|
||||||
|
export default function Body(props: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={Object.assign({ wordWrap: 'break-word' }, props.style)}
|
||||||
|
dangerouslySetInnerHTML={{ __html: emojify(props.status.content, props.status.emojis) }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
63
renderer/components/timelines/status/Status.tsx
Normal file
63
renderer/components/timelines/status/Status.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Avatar } from 'flowbite-react'
|
||||||
|
import { Entity, MegalodonInterface } from 'megalodon'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import Body from './Body'
|
||||||
|
import emojify from '@/utils/emojify'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: Entity.Status
|
||||||
|
client: MegalodonInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Status(props: Props) {
|
||||||
|
const status = originalStatus(props.status)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b mr-2 py-1">
|
||||||
|
{rebloggedHeader(props.status)}
|
||||||
|
<div className="flex">
|
||||||
|
<div className="p-2" style={{ width: '56px' }}>
|
||||||
|
<Avatar img={status.account.avatar} />
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-950 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<span
|
||||||
|
className="text-gray-950 text-ellipsis break-all overflow-hidden"
|
||||||
|
dangerouslySetInnerHTML={{ __html: emojify(status.account.display_name, status.account.emojis) }}
|
||||||
|
></span>
|
||||||
|
<span className="text-gray-600 text-ellipsis break-all overflow-hidden">@{status.account.acct}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600 text-right">
|
||||||
|
<time dateTime={status.created_at}>{dayjs(status.created_at).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Body status={status} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalStatus = (status: Entity.Status) => {
|
||||||
|
if (status.reblog && !status.quote) {
|
||||||
|
return status.reblog
|
||||||
|
} else {
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rebloggedHeader = (status: Entity.Status) => {
|
||||||
|
if (status.reblog && !status.quote) {
|
||||||
|
return (
|
||||||
|
<div className="flex text-gray-600">
|
||||||
|
<div className="grid justify-items-end pr-2" style={{ width: '56px' }}>
|
||||||
|
<Avatar img={status.account.avatar} size="xs" />
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 'calc(100% - 56px)' }}>{status.account.username} boosted</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,27 @@
|
|||||||
import { useRouter } from 'next/router'
|
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'
|
||||||
|
|
||||||
export default function Timeline() {
|
export default function Page() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return <div>{router.query.timeline}</div>
|
const [account, setAccount] = useState<Account | null>(null)
|
||||||
|
const [client, setClient] = useState<MegalodonInterface>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.query.id) {
|
||||||
|
const f = async () => {
|
||||||
|
const a = await db.accounts.get(parseInt(router.query.id as string))
|
||||||
|
if (a) {
|
||||||
|
setAccount(a)
|
||||||
|
const c = generator(a.sns, a.url, a.access_token, 'Whalebird')
|
||||||
|
setClient(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}, [router.query.id])
|
||||||
|
|
||||||
|
return <>{account && client && <Timeline timeline={router.query.timeline as string} account={account} client={client} />}</>
|
||||||
}
|
}
|
||||||
|
21
renderer/utils/emojify.ts
Normal file
21
renderer/utils/emojify.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Entity } from 'megalodon'
|
||||||
|
|
||||||
|
const emojify = (str: string | any, customEmoji: Array<Entity.Emoji> = []): string | null => {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
const message = `Provided string is not a string: ${str}`
|
||||||
|
console.error(message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let result = str
|
||||||
|
customEmoji.map(emoji => {
|
||||||
|
const reg = new RegExp(`:${emoji.shortcode}:`, 'g')
|
||||||
|
const match = result.match(reg)
|
||||||
|
if (!match) return emoji
|
||||||
|
const replaceTag = `<img draggable="false" class="emojione" alt="${emoji.shortcode}" title="${emoji.shortcode}" src="${emoji.url}" />`
|
||||||
|
result = result.replace(reg, replaceTag)
|
||||||
|
return emoji
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export default emojify
|
@ -3664,6 +3664,11 @@ react-indiana-drag-scroll@^2.2.0:
|
|||||||
debounce "^1.2.0"
|
debounce "^1.2.0"
|
||||||
easy-bem "^1.1.1"
|
easy-bem "^1.1.1"
|
||||||
|
|
||||||
|
react-virtuoso@^4.6.2:
|
||||||
|
version "4.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.6.2.tgz#74b59ebe3260e1f73e92340ffec84a6853285a12"
|
||||||
|
integrity sha512-vvlqvzPif+MvBrJ09+hJJrVY0xJK9yran+A+/1iwY78k0YCVKsyoNPqoLxOxzYPggspNBNXqUXEcvckN29OxyQ==
|
||||||
|
|
||||||
react@^18.2.0:
|
react@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user