diff --git a/next.config.js b/next.config.js index f01b2b0..9ea3d45 100644 --- a/next.config.js +++ b/next.config.js @@ -7,8 +7,7 @@ const withSerwist = withSerwistInit({ export default withSerwist({ trailingSlash: true, - images: { - unoptimized: true, - }, + images: { unoptimized: true }, + reactStrictMode: false, output: "export", }) diff --git a/package.json b/package.json index 858a9ed..fef1d2b 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.50.0", + "react-mfm": "0.4.0", "react-textarea-autosize": "8.5.3", "swr": "2.2.4", "uuid": "9.0.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02fcea2..a47330d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ dependencies: react-hook-form: specifier: 7.50.0 version: 7.50.0(react@18.2.0) + react-mfm: + specifier: 0.4.0 + version: 0.4.0(@types/react@18.2.54)(react@18.2.0) react-textarea-autosize: specifier: 8.5.3 version: 8.5.3(@types/react@18.2.54)(react@18.2.0) @@ -1395,6 +1398,10 @@ packages: '@types/trusted-types': 2.0.7 dev: true + /@shikijs/core@1.0.0: + resolution: {integrity: sha512-UMKGMZ+8b88N0/n6DWwWth1PHsOaxjW+R2u+hzSiargZWTv+l3s1l8dhuIxUSsEUPlBDKLs2CSMiFZeviKQM1w==} + dev: false + /@sindresorhus/is@4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -1543,6 +1550,15 @@ packages: engines: {node: '>=10.13.0'} dev: true + /@twemoji/api@15.0.3: + resolution: {integrity: sha512-BI+4nY1o3NAtsMpSTXW2nNjlpanYZ3ndVhslxmGplahO7fgxqWrtAezBs5Yze7vbT98BVx8krJSpr6qJMgYCWw==} + dependencies: + '@twemoji/parser': 15.0.0 + fs-extra: 8.1.0 + jsonfile: 5.0.0 + universalify: 0.1.2 + dev: false + /@twemoji/parser@15.0.0: resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==} dev: false @@ -2372,6 +2388,11 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + /common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -3302,6 +3323,15 @@ packages: universalify: 2.0.1 dev: true + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -3481,7 +3511,6 @@ packages: /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -3895,6 +3924,20 @@ packages: minimist: 1.2.8 dev: true + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /jsonfile@5.0.0: + resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==} + dependencies: + universalify: 0.1.2 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -3918,6 +3961,13 @@ packages: object.values: 1.1.7 dev: true + /katex@0.16.9: + resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -4987,6 +5037,21 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true + /react-mfm@0.4.0(@types/react@18.2.54)(react@18.2.0): + resolution: {integrity: sha512-14pm/oku3z+JAuEltjaWBnVvFyuSU5zjTY2CJMHPlV9SJUyoo69mYxIvt70YfmFeVsGePPkg92GK+rPhHQlnng==} + peerDependencies: + react: '>=17.0.0' + dependencies: + '@twemoji/api': 15.0.3 + jotai: 2.6.4(@types/react@18.2.54)(react@18.2.0) + katex: 0.16.9 + mfm-js: 0.24.0 + react: 18.2.0 + shiki: 1.0.0 + transitivePeerDependencies: + - '@types/react' + dev: false + /react-remove-scroll-bar@2.3.4(@types/react@18.2.54)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} @@ -5320,6 +5385,12 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + /shiki@1.0.0: + resolution: {integrity: sha512-rOUGJa3yFGgOrEoiELYxraoBbag3ZWf9bpodlr05Wjm85Scx8OIX+otdSefq9Pk7L47TKEzGodSQb4L38jka6A==} + dependencies: + '@shikijs/core': 1.0.0 + dev: false + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -5797,6 +5868,11 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 22e6acf..7eb04c4 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,7 +1,12 @@ +"use client" + +import { useMfmProvider } from "~/features/mfm" import TLProvider from "~/features/timeline/TLProvider" import Header from "./Header" export default function MainLayout({ children }: { children: React.ReactNode }) { + useMfmProvider() + return (
diff --git a/src/app/global.css b/src/app/global.css deleted file mode 100644 index b5c61c9..0000000 --- a/src/app/global.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d346c6c..31312dc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,7 @@ import clsx from "clsx" import { Metadata } from "next" import { Fira_Code, Inter, Zen_Kaku_Gothic_New } from "next/font/google" -import "./global.css" +import "~/global.css" export const metadata: Metadata = { metadataBase: new URL("https://minskey.dyama.net"), diff --git a/src/features/api/index.ts b/src/features/api/index.ts index f97d180..d49537d 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -38,3 +38,11 @@ export function useStream(channel: T) { const stream = useAtomValue(streamConnectAtom) return stream?.useChannel(channel) ?? null } + +// utils + +export function fetchEmoji(name: string, host: string) { + return fetch(`https://${host}/api/emoji?name=${name}`) + .then(res => res.json()) + .catch(e => (console.warn(e), {})) +} diff --git a/src/features/mfm/CustomEmoji.tsx b/src/features/mfm/CustomEmoji.tsx new file mode 100644 index 0000000..c11712c --- /dev/null +++ b/src/features/mfm/CustomEmoji.tsx @@ -0,0 +1,35 @@ +"use client" + +import { atom, useAtom, useAtomValue } from "jotai" +import { Suspense, createContext, use, useContext } from "react" +import { CustomEmojiProps } from "react-mfm" +import { fetchEmoji } from "~/features/api" + +export const CustomEmojiCtx = createContext<{ host: string | null }>({ host: null }) + +const cacheAtom = atom<{ [host: string]: { [name: string]: string } }>({}) + +const EmojiImg = ({ name, url }: { name: string; url?: string }) => + !url ? `:${name}:` : {name} + +function FetchEmoji({ name, host }: { name: string; host: string }) { + const [cache, setCache] = useAtom(cacheAtom) + if (host in cache && name in cache[host]) return + const { url } = use(fetchEmoji(name, host)) + setCache({ + ...cache, + [host]: { ...cache[host], [name]: url }, + }) + return +} + +export default function CustomEmoji({ name }: CustomEmojiProps) { + const cache = useAtomValue(cacheAtom) + const { host } = useContext(CustomEmojiCtx) + if (!host) return + return ( + }> + + + ) +} diff --git a/src/features/mfm/index.tsx b/src/features/mfm/index.tsx new file mode 100644 index 0000000..6aa86f4 --- /dev/null +++ b/src/features/mfm/index.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react" +import { useMfmConfig } from "react-mfm" +import CustomEmoji from "./CustomEmoji" + +export function useMfmProvider() { + const [, setMfmConfig] = useMfmConfig() + + useEffect(() => { + setMfmConfig(config => ({ + ...config, + CustomEmoji, + })) + }, []) +} diff --git a/src/features/note/NotePreview/index.tsx b/src/features/note/NotePreview/index.tsx index 32c9fff..2aa1291 100644 --- a/src/features/note/NotePreview/index.tsx +++ b/src/features/note/NotePreview/index.tsx @@ -3,9 +3,10 @@ import { entities } from "misskey-js" import Image from "next/image" import Link from "next/link" import { memo } from "react" - +import Mfm, { MfmSimple } from "react-mfm" import TimeText from "~/features/common/TimeText" import FilePreview from "~/features/drive/FilePreview" +import { CustomEmojiCtx } from "~/features/mfm/CustomEmoji" import { profileLink } from "~/features/profile" import NavMore from "./NavMore" import NavRN from "./NavRN" @@ -17,8 +18,16 @@ type NotePreviewProps = { renote?: entities.Note } -const NotePreviewMemo = memo(NotePreview) -export default NotePreviewMemo +// なんかいい感じにできねーかな +function NotePreviewWithHost(props: NotePreviewProps) { + return ( + + + + ) +} + +export default memo(NotePreviewWithHost) // todo: 設定に応じて自動でリフレッシュ function NotePreview({ note, renote }: NotePreviewProps) { @@ -29,19 +38,7 @@ function NotePreview({ note, renote }: NotePreviewProps) { return (
- {renote && ( -
-

- - - {renote.user.name} - -

- - - -
- )} + {renote && }
- - {note.user.name} + +

@{note.user.username} @@ -63,7 +60,11 @@ function NotePreview({ note, renote }: NotePreviewProps) {

-

{note.text}

+ {note.text && ( +

+ +

+ )} {!!note.files.length && ( // todo: grid layout
@@ -85,3 +86,21 @@ function NotePreview({ note, renote }: NotePreviewProps) {
) } + +function RenoteBar({ renote }: { renote: entities.Note }) { + return ( + +
+

+ + + + +

+ + + +
+
+ ) +} diff --git a/src/global.css b/src/global.css new file mode 100644 index 0000000..0e0b3f1 --- /dev/null +++ b/src/global.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.mfm-blockCode { + font-size: 0.8em; +} + +.mfm-plainCE .mfm-customEmoji { + height: 1.25em; + vertical-align: -0.25em; +}