From 3909148b176a90e781648226852481062794e322 Mon Sep 17 00:00:00 2001 From: AkiraFukushima Date: Fri, 3 Nov 2023 20:37:40 +0900 Subject: [PATCH] Implement timeline statuses --- package.json | 4 +- renderer/app.css | 5 ++ renderer/components/timelines/Timeline.tsx | 67 +++++++++++++++++++ renderer/components/timelines/status/Body.tsx | 18 +++++ .../components/timelines/status/Status.tsx | 63 +++++++++++++++++ renderer/pages/accounts/[id]/[timeline].tsx | 25 ++++++- renderer/utils/emojify.ts | 21 ++++++ yarn.lock | 5 ++ 8 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 renderer/components/timelines/Timeline.tsx create mode 100644 renderer/components/timelines/status/Body.tsx create mode 100644 renderer/components/timelines/status/Status.tsx create mode 100644 renderer/utils/emojify.ts diff --git a/package.json b/package.json index 5de2a660..11eea3e9 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "postinstall": "electron-builder install-app-deps" }, "dependencies": { + "dayjs": "^1.11.10", "dexie": "^3.2.4", "electron-serve": "^1.1.0", "electron-store": "^8.1.0", "flowbite": "^2.0.0", "flowbite-react": "^0.6.4", - "megalodon": "^9.1.1" + "megalodon": "^9.1.1", + "react-virtuoso": "^4.6.2" }, "devDependencies": { "@babel/runtime-corejs3": "^7.23.2", diff --git a/renderer/app.css b/renderer/app.css index b5c61c95..3aae1c6a 100644 --- a/renderer/app.css +++ b/renderer/app.css @@ -1,3 +1,8 @@ @tailwind base; @tailwind components; @tailwind utilities; + +.emojione { + width: 1.2rem; + height: 1.2rem; +} diff --git a/renderer/components/timelines/Timeline.tsx b/renderer/components/timelines/Timeline.tsx new file mode 100644 index 00000000..cee7d5f5 --- /dev/null +++ b/renderer/components/timelines/Timeline.tsx @@ -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>([]) + + 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> => { + 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 ( +
+
+
{props.timeline}
+
+
+ + +
+
+
+ } + /> +
+
+ ) +} diff --git a/renderer/components/timelines/status/Body.tsx b/renderer/components/timelines/status/Body.tsx new file mode 100644 index 00000000..c2ba8034 --- /dev/null +++ b/renderer/components/timelines/status/Body.tsx @@ -0,0 +1,18 @@ +import { Entity } from 'megalodon' +import { HTMLAttributes } from 'react' +import emojify from '@/utils/emojify' + +type Props = { + status: Entity.Status +} & HTMLAttributes + +export default function Body(props: Props) { + return ( + <> +
+ + ) +} diff --git a/renderer/components/timelines/status/Status.tsx b/renderer/components/timelines/status/Status.tsx new file mode 100644 index 00000000..89221fb0 --- /dev/null +++ b/renderer/components/timelines/status/Status.tsx @@ -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 ( +
+ {rebloggedHeader(props.status)} +
+
+ +
+
+
+
+ + @{status.account.acct} +
+
+ +
+
+ +
+
+
+ ) +} + +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 ( +
+
+ +
+
{status.account.username} boosted
+
+ ) + } else { + return null + } +} diff --git a/renderer/pages/accounts/[id]/[timeline].tsx b/renderer/pages/accounts/[id]/[timeline].tsx index 438920e4..d02ad70e 100644 --- a/renderer/pages/accounts/[id]/[timeline].tsx +++ b/renderer/pages/accounts/[id]/[timeline].tsx @@ -1,6 +1,27 @@ 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() - return
{router.query.timeline}
+ const [account, setAccount] = useState(null) + const [client, setClient] = useState(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 && } } diff --git a/renderer/utils/emojify.ts b/renderer/utils/emojify.ts new file mode 100644 index 00000000..5532b1c4 --- /dev/null +++ b/renderer/utils/emojify.ts @@ -0,0 +1,21 @@ +import { Entity } from 'megalodon' + +const emojify = (str: string | any, customEmoji: Array = []): 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 = `${emoji.shortcode}` + result = result.replace(reg, replaceTag) + return emoji + }) + return result +} + +export default emojify diff --git a/yarn.lock b/yarn.lock index a47cceb6..21ef50aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3664,6 +3664,11 @@ react-indiana-drag-scroll@^2.2.0: debounce "^1.2.0" 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: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"