feat: add shared decks and multi-account Mastodon OAuth

This commit is contained in:
2026-09-24 16:52:55 +09:00
parent d2cbf4dbd3
commit c47f58f065
100 changed files with 9215 additions and 1027 deletions
+66 -36
View File
@@ -1,3 +1,4 @@
import { useState } from 'react'
import { Icon } from '#/components/icon'
import { PostText } from '#/components/post-text'
import type { ResearchPost } from '#/features/platforms/types'
@@ -11,8 +12,13 @@ const dateFormatter = new Intl.DateTimeFormat('ja-JP', {
})
export function ResearchPostCard({ post }: { post: ResearchPost }) {
const [revealed, setRevealed] = useState(false)
const concealed = Boolean(post.contentWarning || post.sensitive)
return (
<article className="research-post">
{post.boostedBy ? (
<p className="handle">{post.boostedBy.name} がブースト</p>
) : null}
<header>
{post.author.avatarUrl ? (
<img
@@ -35,43 +41,67 @@ export function ResearchPostCard({ post }: { post: ResearchPost }) {
</time>
) : null}
</header>
<p className="research-post-text">
<PostText text={post.text} />
</p>
{post.media?.map((media) =>
media.type === 'photo' ? (
<img
className="research-media"
key={media.url}
src={media.url}
alt={media.alt ?? ''}
loading="lazy"
/>
) : (
<a
key={media.url}
href={media.url}
target="_blank"
rel="noreferrer noopener"
{concealed ? (
<div>
<p>{post.contentWarning || 'センシティブなメディアを含む投稿'}</p>
<button
type="button"
aria-expanded={revealed}
onClick={() => setRevealed(!revealed)}
>
動画を開く
</a>
),
)}
{post.quotedPost ? (
<blockquote>
<strong>{post.quotedPost.author.name}</strong>
<p>
<PostText text={post.quotedPost.text} />
</p>
<a
href={post.quotedPost.url}
target="_blank"
rel="noreferrer noopener"
>
引用元を開く
</a>
</blockquote>
{revealed ? '内容を隠す' : '内容を表示'}
</button>
</div>
) : null}
{!concealed || revealed ? (
<>
{post.html !== undefined ? (
<div
className="research-post-text"
// biome-ignore lint/security/noDangerouslySetInnerHtml: Mastodon HTML is sanitized by the server allowlist before normalization.
dangerouslySetInnerHTML={{ __html: post.html }}
/>
) : (
<p className="research-post-text">
<PostText text={post.text} />
</p>
)}
{post.media?.map((media) =>
media.type === 'photo' ? (
<img
className="research-media"
key={media.url}
src={media.url}
alt={media.alt ?? ''}
loading="lazy"
/>
) : (
<a
key={media.url}
href={media.url}
target="_blank"
rel="noreferrer noopener"
>
動画を開く
</a>
),
)}
{post.quotedPost ? (
<blockquote>
<strong>{post.quotedPost.author.name}</strong>
<p>
<PostText text={post.quotedPost.text} />
</p>
<a
href={post.quotedPost.url}
target="_blank"
rel="noreferrer noopener"
>
引用元を開く
</a>
</blockquote>
) : null}
</>
) : null}
<footer>
<a href={post.url} target="_blank" rel="noreferrer noopener">