import React from 'react'; import { IcNavBack, IcMoreVert, IcSend, IcReply, IcCopy, IcShare, IcDelete, IcAdd } from '../res/icons'; import { useLocation } from 'react-router-dom'; import { useRedditStore } from '../state'; import { useShallow } from 'zustand/react/shallow'; import { useRedditGestures } from '../hooks/useRedditGestures'; import { formatChatDateShort, formatChatMessageTime, getChatDayKey } from '../utils/chatTime'; import { getUserAvatar } from '../utils/userIdentity'; import { KeyboardService } from '@/os/keyboard'; import { ClipboardService } from '@/os/clipboard'; type ChatMessage = { id: string; from: 'me' | 'them'; body: string; created_utc: number; }; const pickAvatar = (usernameLike: string): string | undefined => getUserAvatar(usernameLike); function getUsernameFromPath(pathname: string): string | null { const m = pathname.match(/^\/chat\/([^/?#]+)/); if (!m) return null; const u = decodeURIComponent(m[1]); if (!u || u === 'new') return null; return u; } export const ChatThreadPage: React.FC = () => { const { chatThreads, chatReplies, user } = useRedditStore(useShallow((s) => ({ chatThreads: s.chatThreads, chatReplies: s.chatReplies, user: s.user, }))); const storeSeedChatThread = useRedditStore((s) => s.seedChatThread); const storeSendChatMessage = useRedditStore((s) => s.sendChatMessage); const storeDeleteChatMessage = useRedditStore((s) => s.deleteChatMessage); const { bindBack, bindTap, bindLongPress } = useRedditGestures(); const location = useLocation(); const username = getUsernameFromPath(location.pathname); const [draft, setDraft] = React.useState(''); const [longPressMenu, setLongPressMenu] = React.useState(null); const [confirmDelete, setConfirmDelete] = React.useState(null); const inputRef = React.useRef(null); const messagesEndRef = React.useRef(null); const scrollToBottom = React.useCallback((instant = false) => { messagesEndRef.current?.scrollIntoView({ behavior: instant ? 'auto' : 'smooth' }); }, []); const thread = React.useMemo(() => { if (!username) return []; const list = chatThreads[username]; return Array.isArray(list) ? (list as ChatMessage[]) : []; }, [chatThreads, username]); // Seed initial message for the demo chats, if empty. React.useEffect(() => { if (!username) return; const existing = chatThreads[username]; if (Array.isArray(existing) && existing.length > 0) return; const seedBody = username === 'Objective-Skill-2591' ? "well,it's so funny" : 'hello'; storeSeedChatThread(username, seedBody); }, [storeSeedChatThread, chatThreads, username]); // 消息变化时滚到底部 React.useLayoutEffect(() => { scrollToBottom(true); }, [thread]); // 键盘弹出时瞬间滚到底部,避免最新消息被遮挡 React.useEffect(() => { let wasVisible = false; return KeyboardService.subscribe(() => { const nowVisible = KeyboardService.isVisible(); if (nowVisible && !wasVisible) { setTimeout(() => scrollToBottom(true), 50); } wasVisible = nowVisible; }); }, []); const canSend = draft.trim().length > 0 && !!username; const closeLongPressMenu = React.useCallback(() => setLongPressMenu(null), []); const copyText = React.useCallback((text: string) => { ClipboardService.copyText(String(text ?? '')); }, []); const deleteMessage = React.useCallback((messageId: string) => { if (!username) return; storeDeleteChatMessage(username, messageId); }, [storeDeleteChatMessage, username]); const send = React.useCallback(() => { if (!username) return; const body = draft.trim(); if (!body) return; storeSendChatMessage(username, body); setDraft(''); requestAnimationFrame(() => { if (inputRef.current) { inputRef.current.style.height = 'auto'; inputRef.current.style.overflow = 'hidden'; inputRef.current.focus(); } }); }, [draft, storeSendChatMessage, username]); const getThreadInfo = React.useCallback( (messageId: string): { count: number; lastFrom: 'me' | 'them' } => { if (!username) return { count: 0, lastFrom: 'me' }; const k = `${username}:${messageId}`; const list = chatReplies?.[k]; if (!Array.isArray(list) || list.length === 0) return { count: 0, lastFrom: 'me' }; const last = list[list.length - 1] as ChatMessage; return { count: list.length, lastFrom: last?.from ?? 'me' }; }, [chatReplies, username], ); if (!username) { return (
Invalid chat.
); } const avatarSrc = pickAvatar(username); const meName = user.username || 'Embarrassed_Fee8630'; const myAvatarSrc = user.avatar || pickAvatar(meName); return (
{/* Top bar */}
(e.currentTarget.style.display = 'none')} />
{username}
{/* Profile header like screenshot */}
(e.currentTarget.style.display = 'none')} />
{username}
531 karma • redditor for 1y 10m
{/* Message list (stick to bottom when few) */}
{thread.map((m, idx) => { const currentKey = getChatDayKey(m.created_utc); const prevKey = idx > 0 ? getChatDayKey(thread[idx - 1].created_utc) : null; const showDay = idx === 0 || currentKey !== prevKey; const threadInfo = getThreadInfo(m.id); const replyCount = threadInfo.count; const openThreadProps = bindTap('chatThread.message.thread.open', { params: { username, messageId: m.id }, }); return ( {showDay && (
{formatChatDateShort(m.created_utc)}
)}
setLongPressMenu(m), }, )} >
(e.currentTarget.style.display = 'none')} />
{m.from === 'me' ? meName : username}{' '} {formatChatMessageTime(m.created_utc)}
{m.body}
{replyCount > 0 && ( )}
); })}
{/* Input bar */}