Skip to content

Commit

Permalink
add react-mfm
Browse files Browse the repository at this point in the history
  • Loading branch information
yamader committed Feb 8, 2024
1 parent d0ba66c commit 3bc1d2f
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 27 deletions.
5 changes: 2 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const withSerwist = withSerwistInit({

export default withSerwist({
trailingSlash: true,
images: {
unoptimized: true,
},
images: { unoptimized: true },
reactStrictMode: false,
output: "export",
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
78 changes: 77 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TLProvider>
<div className="flex min-h-full flex-col bg-neutral-100">
Expand Down
3 changes: 0 additions & 3 deletions src/app/global.css

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down
8 changes: 8 additions & 0 deletions src/features/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ export function useStream<T extends keyof Channels>(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), {}))
}
35 changes: 35 additions & 0 deletions src/features/mfm/CustomEmoji.tsx
Original file line number Diff line number Diff line change
@@ -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}:` : <img src={url} alt={name} className="mfm-customEmoji" />

function FetchEmoji({ name, host }: { name: string; host: string }) {
const [cache, setCache] = useAtom(cacheAtom)
if (host in cache && name in cache[host]) return <EmojiImg name={name} url={cache[host][name]} />
const { url } = use(fetchEmoji(name, host))
setCache({
...cache,
[host]: { ...cache[host], [name]: url },
})
return <EmojiImg name={name} url={url} />
}

export default function CustomEmoji({ name }: CustomEmojiProps) {
const cache = useAtomValue(cacheAtom)
const { host } = useContext(CustomEmojiCtx)
if (!host) return <EmojiImg name={name} />
return (
<Suspense fallback={<EmojiImg name={name} url={cache[host]?.[name]} />}>
<FetchEmoji name={name} host={host} />
</Suspense>
)
}
14 changes: 14 additions & 0 deletions src/features/mfm/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
}))
}, [])
}
57 changes: 38 additions & 19 deletions src/features/note/NotePreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -17,8 +18,16 @@ type NotePreviewProps = {
renote?: entities.Note
}

const NotePreviewMemo = memo(NotePreview)
export default NotePreviewMemo
// なんかいい感じにできねーかな
function NotePreviewWithHost(props: NotePreviewProps) {
return (
<CustomEmojiCtx.Provider value={{ host: props.note.user.host }}>
<NotePreview {...props} />
</CustomEmojiCtx.Provider>
)
}

export default memo(NotePreviewWithHost)

// todo: 設定に応じて自動でリフレッシュ
function NotePreview({ note, renote }: NotePreviewProps) {
Expand All @@ -29,19 +38,7 @@ function NotePreview({ note, renote }: NotePreviewProps) {

return (
<div className="p-3">
{renote && (
<div className="mb-1 flex justify-between text-neutral-600">
<p className="ml-1 flex items-center gap-1 text-sm">
<Repeat2 size={16} />
<Link className="font-bold hover:underline" href={profileLink(renote.user)}>
{renote.user.name}
</Link>
</p>
<Link className="hover:underline" href={`/note?id=${renote.id}`}>
<TimeText dateTime={renote.createdAt} />
</Link>
</div>
)}
{renote && <RenoteBar renote={renote} />}
<div className="flex gap-1.5">
<Link
className="m-1 h-fit w-fit overflow-hidden rounded-[48px] shadow transition-all hover:rounded-md"
Expand All @@ -51,8 +48,8 @@ function NotePreview({ note, renote }: NotePreviewProps) {
<div className="flex w-full flex-col gap-0.5">
<div className="flex justify-between">
<div className="flex gap-1 font-bold">
<Link className="hover:underline" href={profileLink(note.user)}>
{note.user.name}
<Link className="mfm-plainCE hover:underline" href={profileLink(note.user)}>
<MfmSimple text={note.user.name ?? "" /* wtf */} />
</Link>
<p>
<span>@{note.user.username}</span>
Expand All @@ -63,7 +60,11 @@ function NotePreview({ note, renote }: NotePreviewProps) {
<TimeText dateTime={note.createdAt} />
</Link>
</div>
<p>{note.text}</p>
{note.text && (
<p>
<Mfm text={note.text} />
</p>
)}
{!!note.files.length && (
// todo: grid layout
<div className="grid w-1/2 grid-cols-2">
Expand All @@ -85,3 +86,21 @@ function NotePreview({ note, renote }: NotePreviewProps) {
</div>
)
}

function RenoteBar({ renote }: { renote: entities.Note }) {
return (
<CustomEmojiCtx.Provider value={{ host: renote.user.host }}>
<div className="mb-1 flex justify-between text-neutral-600">
<p className="ml-1 flex items-center gap-1 text-sm">
<Repeat2 size={16} />
<Link className="mfm-plainCE font-bold hover:underline" href={profileLink(renote.user)}>
<MfmSimple text={renote.user.name} />
</Link>
</p>
<Link className="hover:underline" href={`/note?id=${renote.id}`}>
<TimeText dateTime={renote.createdAt} />
</Link>
</div>
</CustomEmojiCtx.Provider>
)
}
12 changes: 12 additions & 0 deletions src/global.css
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 3bc1d2f

Please sign in to comment.