From 3103090c33e2ccf90b653a27378e180b6915a3d0 Mon Sep 17 00:00:00 2001 From: syui Date: Wed, 25 Mar 2026 05:58:41 +0900 Subject: [PATCH] refact web --- src/commands/index.rs | 13 + src/web/components/browser.ts | 9 +- src/web/components/card.ts | 13 +- src/web/components/chat-list.ts | 187 +++++ src/web/components/chat-thread.ts | 143 ++++ src/web/components/chat.ts | 386 +-------- src/web/components/discussion.ts | 20 +- src/web/components/mode-tabs.ts | 9 +- src/web/components/note.ts | 18 +- src/web/components/posts.ts | 23 +- src/web/components/profile.ts | 9 +- src/web/components/rse.ts | 13 +- src/web/lib/api.ts | 446 +++++------ src/web/lib/markdown.ts | 11 +- src/web/lib/util.ts | 104 +++ src/web/main.ts | 1202 ++++++++++++++--------------- 16 files changed, 1241 insertions(+), 1365 deletions(-) create mode 100644 src/web/components/chat-list.ts create mode 100644 src/web/components/chat-thread.ts create mode 100644 src/web/lib/util.ts diff --git a/src/commands/index.rs b/src/commands/index.rs index 11b0ba2..d8bc5de 100644 --- a/src/commands/index.rs +++ b/src/commands/index.rs @@ -125,6 +125,19 @@ pub fn run(content_dir: &Path) -> Result<()> { total_updated += 1; } } + + // Generate list.json (all records in a single array) + let list_path = col_path.join("list.json"); + let mut records: Vec = Vec::new(); + for rkey in &rkeys { + let file_path = col_path.join(format!("{}.json", rkey)); + if let Ok(content) = fs::read_to_string(&file_path) { + if let Ok(json) = serde_json::from_str::(&content) { + records.push(json); + } + } + } + fs::write(&list_path, serde_json::to_string(&records)?)?; } } diff --git a/src/web/components/browser.ts b/src/web/components/browser.ts index d9452d7..fc1afc5 100644 --- a/src/web/components/browser.ts +++ b/src/web/components/browser.ts @@ -1,4 +1,5 @@ // AT-Browser: Server info and collection hierarchy +import { escapeHtml } from '../lib/util' // Group collections by service domain function groupCollectionsByService(collections: string[]): Map { @@ -241,11 +242,3 @@ function getRecordPreview(value: Record): string { return '' } -function escapeHtml(text: string): string { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} diff --git a/src/web/components/card.ts b/src/web/components/card.ts index 4a0877c..4d83d47 100644 --- a/src/web/components/card.ts +++ b/src/web/components/card.ts @@ -1,4 +1,5 @@ // Card display component for ai.syui.card.user collection +import { getLocalizedText } from '../lib/util' export interface UserCard { id: number @@ -29,18 +30,6 @@ export interface CardAdminData { card: CardAdminEntry[] } -// Get current language -function getLang(): string { - return localStorage.getItem('preferredLang') || 'ja' -} - -// Get localized text -function getLocalizedText(obj: { ja: string; en: string } | undefined): string { - if (!obj) return '' - const lang = getLang() - return obj[lang as 'ja' | 'en'] || obj.ja || obj.en || '' -} - // Get rarity class name function getRarityClass(card: UserCard): string { if (card.unique) return 'unique' diff --git a/src/web/components/chat-list.ts b/src/web/components/chat-list.ts new file mode 100644 index 0000000..12f114c --- /dev/null +++ b/src/web/components/chat-list.ts @@ -0,0 +1,187 @@ +import type { ChatMessage, Profile } from '../types' +import { escapeHtml, formatDateTime, getRkeyFromUri, getAuthorityFromUri } from '../lib/util' +import { buildAuthorMap, resolveAuthorDid, chatTypeSlug, getTranslatedContent } from './chat' + +export type { ChatCollectionEntry } from './chat' + +const formatChatTime = formatDateTime + +// Render chat threads list (conversations this user started) +export function renderChatThreadList( + messages: ChatMessage[], + userDid: string, + userHandle: string, + botDid: string, + botHandle: string, + userProfile?: Profile | null, + botProfile?: Profile | null, + pds?: string, + chatCollection: string = 'ai.syui.log.chat' +): string { + // Build set of all message URIs + const allUris = new Set(messages.map(m => m.uri)) + + // Find root messages by this user: + // 1. No root field (explicit start of conversation) + // 2. Or root points to non-existent message (orphaned, treat as root) + // For orphaned roots, only keep the oldest message per orphaned root URI + const orphanedRootFirstMsg = new Map() + const rootMessages: ChatMessage[] = [] + + for (const msg of messages) { + if (resolveAuthorDid(getAuthorityFromUri(msg.uri)) !== userDid) continue + + if (!msg.value.root) { + // No root = explicit conversation start + rootMessages.push(msg) + } else if (!allUris.has(msg.value.root)) { + // Orphaned root - keep only the oldest message per orphaned root + const existing = orphanedRootFirstMsg.get(msg.value.root) + if (!existing || new Date(msg.value.publishedAt) < new Date(existing.value.publishedAt)) { + orphanedRootFirstMsg.set(msg.value.root, msg) + } + } + } + + // Add orphaned root representatives + for (const msg of orphanedRootFirstMsg.values()) { + rootMessages.push(msg) + } + + if (rootMessages.length === 0) { + return '

No chat threads yet.

' + } + + const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) + + // Sort by publishedAt (newest first) + const sorted = [...rootMessages].sort((a, b) => + new Date(b.value.publishedAt).getTime() - new Date(a.value.publishedAt).getTime() + ) + + const items = sorted.map(msg => { + const authorDid = resolveAuthorDid(getAuthorityFromUri(msg.uri)) + const time = formatChatTime(msg.value.publishedAt) + const rkey = getRkeyFromUri(msg.uri) + const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } + + const avatarHtml = author.avatarUrl + ? `@${escapeHtml(author.handle)}` + : `
` + + // Truncate content for preview (use translated content, show first 3 lines) + const displayContent = getTranslatedContent(msg) + const lines = displayContent.split('\n').slice(0, 3) + const preview = lines.join('\n') + + return ` + +
+ ${avatarHtml} +
+
+
+ @${escapeHtml(author.handle)} + ${chatTypeSlug(chatCollection)} + ${time} +
+
${escapeHtml(preview)}
+
+
+ ` + }).join('') + + return `
${items}
` +} + +// Render unified chat list page with filter buttons +export function renderChatListPage( + collections: import('./chat').ChatCollectionEntry[], + userDid: string, + userHandle: string, + botDid: string, + botHandle: string, + userProfile?: Profile | null, + botProfile?: Profile | null, + pds?: string +): string { + // Merge all collections into a single list + const allMessages: ChatMessage[] = [] + const messageCollectionMap = new Map() + + for (const c of collections) { + for (const msg of c.messages) { + allMessages.push(msg) + messageCollectionMap.set(msg.uri, c.collection) + } + } + + // Build author map with first collection's params (all share same users) + const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) + + // Find root messages (same logic as renderChatThreadList) + const allUris = new Set(allMessages.map(m => m.uri)) + const orphanedRootFirstMsg = new Map() + const rootMessages: ChatMessage[] = [] + + for (const msg of allMessages) { + if (resolveAuthorDid(getAuthorityFromUri(msg.uri)) !== userDid) continue + + if (!msg.value.root) { + rootMessages.push(msg) + } else if (!allUris.has(msg.value.root)) { + const existing = orphanedRootFirstMsg.get(msg.value.root) + if (!existing || new Date(msg.value.publishedAt) < new Date(existing.value.publishedAt)) { + orphanedRootFirstMsg.set(msg.value.root, msg) + } + } + } + + for (const msg of orphanedRootFirstMsg.values()) { + rootMessages.push(msg) + } + + if (rootMessages.length === 0) { + return '

No chat threads yet.

' + } + + // Sort by publishedAt (newest first) + const sorted = [...rootMessages].sort((a, b) => + new Date(b.value.publishedAt).getTime() - new Date(a.value.publishedAt).getTime() + ) + + const items = sorted.map(msg => { + const authorDid = resolveAuthorDid(getAuthorityFromUri(msg.uri)) + const time = formatChatTime(msg.value.publishedAt) + const rkey = getRkeyFromUri(msg.uri) + const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } + const collection = messageCollectionMap.get(msg.uri) || 'ai.syui.log.chat' + const slug = chatTypeSlug(collection) + + const avatarHtml = author.avatarUrl + ? `@${escapeHtml(author.handle)}` + : `
` + + const displayContent = getTranslatedContent(msg) + const lines = displayContent.split('\n').slice(0, 3) + const preview = lines.join('\n') + + return ` + +
+ ${avatarHtml} +
+
+
+ @${escapeHtml(author.handle)} + ${slug} + ${time} +
+
${escapeHtml(preview)}
+
+
+ ` + }).join('') + + return `
${items}
` +} diff --git a/src/web/components/chat-thread.ts b/src/web/components/chat-thread.ts new file mode 100644 index 0000000..9372dfd --- /dev/null +++ b/src/web/components/chat-thread.ts @@ -0,0 +1,143 @@ +import type { ChatMessage, Profile } from '../types' +import { renderMarkdown } from '../lib/markdown' +import { escapeHtml, formatDateTime, getRkeyFromUri, getAuthorityFromUri } from '../lib/util' +import { buildAuthorMap, resolveAuthorDid, chatTypeSlug, getTranslatedContent } from './chat' + +const formatChatTime = formatDateTime + +// Render single chat thread (full conversation) +export function renderChatThread( + messages: ChatMessage[], + rootRkey: string, + userDid: string, + userHandle: string, + botDid: string, + botHandle: string, + userProfile?: Profile | null, + botProfile?: Profile | null, + pds?: string, + chatCollection: string = 'ai.syui.log.chat', + loggedInDid?: string | null +): string { + // Find root message + const rootUri = `at://${userDid}/${chatCollection}/${rootRkey}` + const rootMsg = messages.find(m => m.uri === rootUri) + + if (!rootMsg) { + return '

Chat thread not found.

' + } + + // Find all messages in this thread + // 1. The root message itself + // 2. Messages with root === rootUri (direct children) + // 3. If this is an orphaned root (root points to non-existent), find siblings with same original root + const originalRoot = rootMsg.value.root + const allUris = new Set(messages.map(m => m.uri)) + const isOrphanedRoot = originalRoot && !allUris.has(originalRoot) + + const threadMessages = messages.filter(msg => { + // Include the root message itself + if (msg.uri === rootUri) return true + // Include messages that point to this as root + if (msg.value.root === rootUri) return true + // If orphaned, include messages with the same original root + if (isOrphanedRoot && msg.value.root === originalRoot) return true + return false + }) + + if (threadMessages.length === 0) { + return '

No messages in this thread.

' + } + + const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) + + // Sort by publishedAt + const sorted = [...threadMessages].sort((a, b) => + new Date(a.value.publishedAt).getTime() - new Date(b.value.publishedAt).getTime() + ) + + const items = sorted.map(msg => { + const authorDid = resolveAuthorDid(getAuthorityFromUri(msg.uri)) + const time = formatChatTime(msg.value.publishedAt) + const rkey = getRkeyFromUri(msg.uri) + const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } + + const avatarHtml = author.avatarUrl + ? `@${escapeHtml(author.handle)}` + : `
` + + const displayContent = getTranslatedContent(msg) + const content = renderMarkdown(displayContent) + const recordLink = `/@${author.handle}/at/collection/${chatCollection}/${rkey}` + const canEdit = loggedInDid && authorDid === loggedInDid + const editLink = `/@${userHandle}/at/chat/${chatTypeSlug(chatCollection)}/${rkey}/edit` + + return ` + + ` + }).join('') + + return `
${items}
` +} + +// Render chat thread page +export function renderChatThreadPage( + messages: ChatMessage[], + rootRkey: string, + userDid: string, + userHandle: string, + botDid: string, + botHandle: string, + userProfile?: Profile | null, + botProfile?: Profile | null, + pds?: string, + chatCollection: string = 'ai.syui.log.chat', + loggedInDid?: string | null +): string { + const thread = renderChatThread(messages, rootRkey, userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds, chatCollection, loggedInDid) + return `
${thread}
` +} + +// Render chat edit form +export function renderChatEditForm( + message: ChatMessage, + collection: string, + userHandle: string +): string { + const rkey = message.uri.split('/').pop() || '' + const content = message.value.content.text + + return ` +
+

Edit Chat Message

+
+ + +
+
+
+ ` +} diff --git a/src/web/components/chat.ts b/src/web/components/chat.ts index 4bbd179..1db1923 100644 --- a/src/web/components/chat.ts +++ b/src/web/components/chat.ts @@ -1,65 +1,28 @@ import type { ChatMessage, Profile } from '../types' -import { renderMarkdown } from '../lib/markdown' -import { getCurrentLang } from './mode-tabs' - -// Get translated content for a chat message -function getTranslatedContent(msg: ChatMessage): string { - const currentLang = getCurrentLang() - const originalLang = msg.value.langs?.[0] || 'ja' - const translations = msg.value.translations - - if (translations && currentLang !== originalLang && translations[currentLang]) { - const translated = translations[currentLang].content - if (typeof translated === 'string') return translated - if (translated && typeof translated === 'object' && 'text' in translated) return (translated as { text: string }).text - return msg.value.content.text - } - return msg.value.content.text -} - -// Escape HTML to prevent XSS -function escapeHtml(text: string): string { - const div = document.createElement('div') - div.textContent = text - return div.innerHTML -} - -// Format date/time for chat -function formatChatTime(dateStr: string): string { - const d = new Date(dateStr) - const month = String(d.getMonth() + 1).padStart(2, '0') - const day = String(d.getDate()).padStart(2, '0') - const hour = String(d.getHours()).padStart(2, '0') - const min = String(d.getMinutes()).padStart(2, '0') - return `${month}/${day} ${hour}:${min}` -} - -// Extract rkey from AT URI -function getRkeyFromUri(uri: string): string { - return uri.split('/').pop() || '' -} - -// Extract authority from AT URI (at://did:plc:xxx/collection/rkey → did:plc:xxx) -function getAuthorityFromUri(uri: string): string { - return uri.replace('at://', '').split('/')[0] -} +import { getTranslatedContent as getTranslation } from '../lib/util' // Profile info for authors -interface AuthorInfo { +export interface AuthorInfo { did: string handle: string avatarUrl?: string } +// Chat collection entry for unified list +export interface ChatCollectionEntry { + collection: string + messages: ChatMessage[] +} + // Handle/DID resolver: maps both handle and DID to DID let handleToDidMap = new Map() -function resolveAuthorDid(authority: string): string { +export function resolveAuthorDid(authority: string): string { return handleToDidMap.get(authority) || authority } // Build author info map -function buildAuthorMap( +export function buildAuthorMap( userDid: string, userHandle: string, botDid: string, @@ -97,330 +60,17 @@ function buildAuthorMap( } // Map collection name to chat type slug -function chatTypeSlug(chatCollection: string): string { +export function chatTypeSlug(chatCollection: string): string { if (chatCollection === 'ai.syui.ue.chat') return 'ue' return 'log' } -// Render chat threads list (conversations this user started) -export function renderChatThreadList( - messages: ChatMessage[], - userDid: string, - userHandle: string, - botDid: string, - botHandle: string, - userProfile?: Profile | null, - botProfile?: Profile | null, - pds?: string, - chatCollection: string = 'ai.syui.log.chat' -): string { - // Build set of all message URIs - const allUris = new Set(messages.map(m => m.uri)) - - // Find root messages by this user: - // 1. No root field (explicit start of conversation) - // 2. Or root points to non-existent message (orphaned, treat as root) - // For orphaned roots, only keep the oldest message per orphaned root URI - const orphanedRootFirstMsg = new Map() - const rootMessages: ChatMessage[] = [] - - for (const msg of messages) { - if (resolveAuthorDid(getAuthorityFromUri(msg.uri)) !== userDid) continue - - if (!msg.value.root) { - // No root = explicit conversation start - rootMessages.push(msg) - } else if (!allUris.has(msg.value.root)) { - // Orphaned root - keep only the oldest message per orphaned root - const existing = orphanedRootFirstMsg.get(msg.value.root) - if (!existing || new Date(msg.value.publishedAt) < new Date(existing.value.publishedAt)) { - orphanedRootFirstMsg.set(msg.value.root, msg) - } - } - } - - // Add orphaned root representatives - for (const msg of orphanedRootFirstMsg.values()) { - rootMessages.push(msg) - } - - if (rootMessages.length === 0) { - return '

No chat threads yet.

' - } - - const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) - - // Sort by publishedAt (newest first) - const sorted = [...rootMessages].sort((a, b) => - new Date(b.value.publishedAt).getTime() - new Date(a.value.publishedAt).getTime() - ) - - const items = sorted.map(msg => { - const authorDid = resolveAuthorDid(getAuthorityFromUri(msg.uri)) - const time = formatChatTime(msg.value.publishedAt) - const rkey = getRkeyFromUri(msg.uri) - const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } - - const avatarHtml = author.avatarUrl - ? `@${escapeHtml(author.handle)}` - : `
` - - // Truncate content for preview (use translated content, show first 3 lines) - const displayContent = getTranslatedContent(msg) - const lines = displayContent.split('\n').slice(0, 3) - const preview = lines.join('\n') - - return ` - -
- ${avatarHtml} -
-
-
- @${escapeHtml(author.handle)} - ${chatTypeSlug(chatCollection)} - ${time} -
-
${escapeHtml(preview)}
-
-
- ` - }).join('') - - return `
${items}
` +// Get translated content for a chat message +export function getTranslatedContent(msg: ChatMessage): string { + const originalLang = msg.value.langs?.[0] || 'ja' + return getTranslation(msg.value.content.text, msg.value.translations, originalLang) } -// Render single chat thread (full conversation) -export function renderChatThread( - messages: ChatMessage[], - rootRkey: string, - userDid: string, - userHandle: string, - botDid: string, - botHandle: string, - userProfile?: Profile | null, - botProfile?: Profile | null, - pds?: string, - chatCollection: string = 'ai.syui.log.chat', - loggedInDid?: string | null -): string { - // Find root message - const rootUri = `at://${userDid}/${chatCollection}/${rootRkey}` - const rootMsg = messages.find(m => m.uri === rootUri) - - if (!rootMsg) { - return '

Chat thread not found.

' - } - - // Find all messages in this thread - // 1. The root message itself - // 2. Messages with root === rootUri (direct children) - // 3. If this is an orphaned root (root points to non-existent), find siblings with same original root - const originalRoot = rootMsg.value.root - const allUris = new Set(messages.map(m => m.uri)) - const isOrphanedRoot = originalRoot && !allUris.has(originalRoot) - - const threadMessages = messages.filter(msg => { - // Include the root message itself - if (msg.uri === rootUri) return true - // Include messages that point to this as root - if (msg.value.root === rootUri) return true - // If orphaned, include messages with the same original root - if (isOrphanedRoot && msg.value.root === originalRoot) return true - return false - }) - - if (threadMessages.length === 0) { - return '

No messages in this thread.

' - } - - const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) - - // Sort by publishedAt - const sorted = [...threadMessages].sort((a, b) => - new Date(a.value.publishedAt).getTime() - new Date(b.value.publishedAt).getTime() - ) - - const items = sorted.map(msg => { - const authorDid = resolveAuthorDid(getAuthorityFromUri(msg.uri)) - const time = formatChatTime(msg.value.publishedAt) - const rkey = getRkeyFromUri(msg.uri) - const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } - - const avatarHtml = author.avatarUrl - ? `@${escapeHtml(author.handle)}` - : `
` - - const displayContent = getTranslatedContent(msg) - const content = renderMarkdown(displayContent) - const recordLink = `/@${author.handle}/at/collection/${chatCollection}/${rkey}` - const canEdit = loggedInDid && authorDid === loggedInDid - const editLink = `/@${userHandle}/at/chat/${chatTypeSlug(chatCollection)}/${rkey}/edit` - - return ` - - ` - }).join('') - - return `
${items}
` -} - -// Chat collection entry for unified list -export interface ChatCollectionEntry { - collection: string - messages: ChatMessage[] -} - -// Render unified chat list page with filter buttons -export function renderChatListPage( - collections: ChatCollectionEntry[], - userDid: string, - userHandle: string, - botDid: string, - botHandle: string, - userProfile?: Profile | null, - botProfile?: Profile | null, - pds?: string -): string { - // Merge all collections into a single list - const allMessages: ChatMessage[] = [] - const messageCollectionMap = new Map() - - for (const c of collections) { - for (const msg of c.messages) { - allMessages.push(msg) - messageCollectionMap.set(msg.uri, c.collection) - } - } - - // Build author map with first collection's params (all share same users) - const authors = buildAuthorMap(userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds) - - // Find root messages (same logic as renderChatThreadList) - const allUris = new Set(allMessages.map(m => m.uri)) - const orphanedRootFirstMsg = new Map() - const rootMessages: ChatMessage[] = [] - - for (const msg of allMessages) { - if (resolveAuthorDid(getAuthorityFromUri(msg.uri)) !== userDid) continue - - if (!msg.value.root) { - rootMessages.push(msg) - } else if (!allUris.has(msg.value.root)) { - const existing = orphanedRootFirstMsg.get(msg.value.root) - if (!existing || new Date(msg.value.publishedAt) < new Date(existing.value.publishedAt)) { - orphanedRootFirstMsg.set(msg.value.root, msg) - } - } - } - - for (const msg of orphanedRootFirstMsg.values()) { - rootMessages.push(msg) - } - - if (rootMessages.length === 0) { - return '

No chat threads yet.

' - } - - // Sort by publishedAt (newest first) - const sorted = [...rootMessages].sort((a, b) => - new Date(b.value.publishedAt).getTime() - new Date(a.value.publishedAt).getTime() - ) - - const items = sorted.map(msg => { - const authorDid = resolveAuthorDid(getAuthorityFromUri(msg.uri)) - const time = formatChatTime(msg.value.publishedAt) - const rkey = getRkeyFromUri(msg.uri) - const author = authors.get(authorDid) || { did: authorDid, handle: authorDid.slice(0, 20) + '...' } - const collection = messageCollectionMap.get(msg.uri) || 'ai.syui.log.chat' - const slug = chatTypeSlug(collection) - - const avatarHtml = author.avatarUrl - ? `@${escapeHtml(author.handle)}` - : `
` - - const displayContent = getTranslatedContent(msg) - const lines = displayContent.split('\n').slice(0, 3) - const preview = lines.join('\n') - - return ` - -
- ${avatarHtml} -
-
-
- @${escapeHtml(author.handle)} - ${slug} - ${time} -
-
${escapeHtml(preview)}
-
-
- ` - }).join('') - - return `
${items}
` -} - -// Render chat thread page -export function renderChatThreadPage( - messages: ChatMessage[], - rootRkey: string, - userDid: string, - userHandle: string, - botDid: string, - botHandle: string, - userProfile?: Profile | null, - botProfile?: Profile | null, - pds?: string, - chatCollection: string = 'ai.syui.log.chat', - loggedInDid?: string | null -): string { - const thread = renderChatThread(messages, rootRkey, userDid, userHandle, botDid, botHandle, userProfile, botProfile, pds, chatCollection, loggedInDid) - return `
${thread}
` -} - -// Render chat edit form -export function renderChatEditForm( - message: ChatMessage, - collection: string, - userHandle: string -): string { - const rkey = message.uri.split('/').pop() || '' - const content = message.value.content.text - - return ` -
-

Edit Chat Message

-
- - -
-
-
- ` -} +// Re-export from split modules +export { renderChatThreadList, renderChatListPage } from './chat-list' +export { renderChatThread, renderChatThreadPage, renderChatEditForm } from './chat-thread' diff --git a/src/web/components/discussion.ts b/src/web/components/discussion.ts index e1aacdc..589fe3b 100644 --- a/src/web/components/discussion.ts +++ b/src/web/components/discussion.ts @@ -1,24 +1,8 @@ import { searchPostsForUrl, getCurrentNetwork, type SearchPost } from '../lib/api' +import { escapeHtml, formatDateJa } from '../lib/util' const DISCUSSION_POST_LIMIT = 10 -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} - -function formatDate(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('ja-JP', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) -} - function getPostUrl(uri: string, appUrl: string): string { // at://did:plc:xxx/app.bsky.feed.post/rkey -> {appUrl}/profile/did:plc:xxx/post/rkey const parts = uri.replace('at://', '').split('/') @@ -100,7 +84,7 @@ export async function loadDiscussionPosts(container: HTMLElement, postUrl: strin ${escapeHtml(displayName)} @${escapeHtml(handle)} - ${formatDate(createdAt)} + ${formatDateJa(createdAt)}
${escapeHtml(truncatedText)}
diff --git a/src/web/components/mode-tabs.ts b/src/web/components/mode-tabs.ts index 29a84f5..5732733 100644 --- a/src/web/components/mode-tabs.ts +++ b/src/web/components/mode-tabs.ts @@ -33,12 +33,11 @@ export function renderModeTabs(handle: string, activeTab: 'blog' | 'browser' | ' tabs += `chat` } - // Note tab only for local user (admin) with note posts - if (isLocalUser && hasNotes) { - tabs += `note` - } - if (isLoggedIn()) { + // Note tab only for logged-in admin with note posts + if (isLocalUser && hasNotes) { + tabs += `note` + } tabs += `post` tabs += `link` } diff --git a/src/web/components/note.ts b/src/web/components/note.ts index 044da8f..f51cdb1 100644 --- a/src/web/components/note.ts +++ b/src/web/components/note.ts @@ -1,5 +1,6 @@ import { renderMarkdown } from '../lib/markdown' import type { Post } from '../types' +import { escapeHtml, escapeAttr, formatDateJa } from '../lib/util' // Note post has extra fields for member content interface NotePost extends Post { @@ -42,12 +43,10 @@ export function renderNoteListPage(posts: NotePost[], handle: string): string { export function renderNoteDetailPage( post: NotePost, _handle: string, - localOnly: boolean + isOwner: boolean ): string { const rkey = post.uri.split('/').pop() || '' - const date = new Date(post.value.publishedAt).toLocaleDateString('ja-JP', { - year: 'numeric', month: '2-digit', day: '2-digit' - }) + const date = formatDateJa(post.value.publishedAt) const freeText = post.value.content?.text || '' const memberText = post.value.member?.text || '' const bonusText = post.value.member?.bonus || '' @@ -59,7 +58,7 @@ export function renderNoteDetailPage( let html = '' // Action buttons at top - if (localOnly) { + if (isOwner) { html += `
@@ -103,7 +102,7 @@ export function renderNoteDetailPage( html += `
` // Edit form (below content) - if (localOnly) { + if (isOwner) { html += `