From a45accd2de28bc1456f5376859a6645ea021c7d4 Mon Sep 17 00:00:00 2001 From: oneao Date: Fri, 26 Jun 2026 22:18:09 +0800 Subject: [PATCH] 20260626221809 --- .../scripts/__pycache__/core.cpython-312.pyc | Bin 0 -> 11774 bytes .../__pycache__/design_system.cpython-312.pyc | Bin 0 -> 57984 bytes code/app/.workbuddy/memory/2026-06-26.md | 12 + code/app/app-go/internal/ws/handler.go | 7 +- code/app/app-rn/app.json | 4 +- .../src/app/(main)/(tabs)/chat/index.tsx | 328 ++++++++++++++---- .../app/app-rn/src/app/(main)/tools/index.tsx | 9 +- code/app/app-rn/src/components/DateTime.tsx | 5 +- .../app-rn/src/components/DateTimeRange.tsx | 8 +- .../app-rn/src/components/layout/tabbar.tsx | 7 +- .../app/app-rn/src/components/layout/view.tsx | 6 +- code/app/app-rn/src/global.css | 12 +- 12 files changed, 306 insertions(+), 92 deletions(-) create mode 100644 code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc create mode 100644 code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc create mode 100644 code/app/.workbuddy/memory/2026-06-26.md diff --git a/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc b/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a4c0c5afdc25609512c8df66f8322a73a7a1094 GIT binary patch literal 11774 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 F{{&mG&~pF) literal 0 HcmV?d00001 diff --git a/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc b/code/app/.codex/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..531501117fa98f7e048a9a3676f05536417a578f GIT binary patch literal 57984 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 - - {users.slice(0, 6).map((u, i) => ( + + {users.slice(0, 6).map((u) => ( router.navigate(`/family/member/${u.user_id}` as any)} > - 0 && "-ml-2")}> + {u.avatar ? ( ) : ( - + {u.nickname?.charAt(0) || "?"} )} @@ -50,7 +51,7 @@ function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: () ))} {users.length > 6 && ( - + +{users.length - 6} )} @@ -133,7 +134,7 @@ const ChatMessageItem = memo(function ChatMessageItem({ isSelf ? "bg-accent rounded-br-md" : "bg-white rounded-bl-md shadow-sm", )} > - + {item.content} @@ -155,7 +156,7 @@ const ChatMessageItem = memo(function ChatMessageItem({ {item.msg_type === ChatMsgType.Video && item.attachments?.length && ( - ▶ + {item.attachments[0].duration ? ( @@ -173,16 +174,16 @@ const ChatMessageItem = memo(function ChatMessageItem({ )} > - 📎 + {item.attachments[0].file_name} - + {item.attachments[0].file_size ? (item.attachments[0].file_size / 1024).toFixed(0) + " KB" : ""} @@ -195,7 +196,7 @@ const ChatMessageItem = memo(function ChatMessageItem({ 来自 {item.source_table} - + {(() => { try { const d = JSON.parse(item.source_data); @@ -212,7 +213,7 @@ const ChatMessageItem = memo(function ChatMessageItem({ {item.content ? ( - {item.content} + {item.content} ) : null} @@ -274,15 +275,142 @@ export default function ChatPage() { const [inputText, setInputText] = useState(""); const [adminRole, setAdminRole] = useState(0); const listRef = useRef(null); + const opacity = useRef(new Animated.Value(0)).current; + const revealed = useRef(false); - // 键盘弹起时自动滚到底部 - useEffect(() => { - const show = Keyboard.addListener("keyboardWillShow", () => { - setTimeout(() => listRef.current?.scrollToEnd?.({ animated: true }), 100); - }); - return () => show.remove(); + // ---- optimistic send ---- + type PendingMsg = { localId: string; content: string; status: "sending" | "failed"; createTime: string }; + const [pending, setPending] = useState([]); + const pendingTimers = useRef>>({}); + + const removePending = useCallback((localId: string) => { + setPending((prev) => prev.filter((p) => p.localId !== localId)); + const t = pendingTimers.current[localId]; + if (t) { clearTimeout(t); delete pendingTimers.current[localId]; } }, []); + // Watch ws.messages — when a new message from self appears, resolve matching pending + const resolvedRef = useRef>(new Set()); + useEffect(() => { + for (const m of ws.messages) { + if (m.user_id !== userId) continue; + const key = `${m.user_id}:${m.content}`; + if (resolvedRef.current.has(key)) continue; + resolvedRef.current.add(key); + const match = pending.find((p) => p.content === m.content && p.status === "sending"); + if (match) { + removePending(match.localId); + toast.success("发送成功", { id: match.localId }); + } + } + }, [ws.messages, userId, pending, removePending]); + + // ---- toast: online / offline ---- + const onlineIdsRef = useRef>(new Set()); + useEffect(() => { + const curr = new Set(ws.onlineUsers.map((u) => u.user_id)); + const prev = onlineIdsRef.current; + // joined + for (const id of curr) { + if (!prev.has(id) && prev.size > 0) { + const u = ws.onlineUsers.find((x) => x.user_id === id); + if (u && u.user_id !== userId) toast(`${u.nickname} 上线了`); + } + } + // left + for (const id of prev) { + if (!curr.has(id)) { + // look up nickname from members (offline event only gives user_id) + const m = members[id]; + const name = m?.nickname || `用户${id}`; + toast(`${name} 下线了`); + } + } + onlineIdsRef.current = curr; + }, [ws.onlineUsers, userId, members]); + + // ---- toast: new message from others ---- + const seenMsgIds = useRef>(new Set()); + // Sync history IDs into seen set so we don't toast on initial load + useEffect(() => { + for (const m of history) seenMsgIds.current.add(m.id); + }, [history]); + // Watch ws.messages for new arrivals from others + useEffect(() => { + if (loading) return; // skip during initial load + for (const m of ws.messages) { + if (seenMsgIds.current.has(m.id)) continue; + seenMsgIds.current.add(m.id); + if (m.user_id !== userId) { + const name = m.nickname || members[m.user_id]?.nickname || "用户"; + const preview = m.content.length > 20 ? m.content.slice(0, 20) + "…" : m.content; + toast(`${name}: ${preview}`); + } + } + }, [ws.messages, userId, members, loading]); + + // Merge pending messages into the display list + const allMessages = useMemo(() => { + const ids = new Set(history.map((m) => m.id)); + const merged: (ChatMessage & { _pending?: PendingMsg })[] = [...history]; + for (const m of ws.messages) { + if (!ids.has(m.id)) { merged.push(m); ids.add(m.id); } + } + // Append pending (sending/failed) at the end + for (const p of pending) { + merged.push({ + id: -(Date.now() % 100000), // negative ID for local-only items + family_id: familyId ?? 0, + user_id: userId, + nickname: "", + avatar: "", + content: p.content, + msg_type: ChatMsgType.Text, + source_table: null, + source_id: null, + source_data: null, + attachments: null, + recalled: 0, + deleted: 0, + create_time: p.createTime, + _pending: p, + } as any); + } + return merged; + }, [history, ws.messages, pending, familyId, userId]); + + // Non-inverted: scrollToEnd = latest messages + const scrollToBottom = useCallback( + (animated = true) => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + listRef.current?.scrollToEnd?.({ animated }); + }); + }); + }, + [], + ); + + const revealAtBottom = useCallback(() => { + requestAnimationFrame(() => { + listRef.current?.scrollToEnd?.({ animated: false }); + requestAnimationFrame(() => { + listRef.current?.scrollToEnd?.({ animated: false }); + opacity.setValue(1); + }); + }); + }, [opacity]); + + // Keyboard appears → double-tap scroll: first to catch the new viewport, + // second as a safety net after FlatList internal measurements settle. + useEffect(() => { + const sub = Keyboard.addListener("keyboardDidShow", () => { + setTimeout(() => scrollToBottom(true), 200); + setTimeout(() => scrollToBottom(true), 400); + }); + return () => sub.remove(); + }, [scrollToBottom]); + // 成员映射 useEffect(() => { if (!familyId) return; @@ -364,27 +492,54 @@ export default function ChatPage() { }); }, [loadingMore, hasMore, page, loadHistory, runLoadMore]); - // 合并消息 (旧→新) - const allMessages = (() => { - const ids = new Set(history.map((m) => m.id)); - const merged = [...history]; - for (const m of ws.messages) { - if (!ids.has(m.id)) { - merged.push(m); - ids.add(m.id); - } - } - return merged; - })(); - - // 发送 - const handleSend = useCallback(() => { + // Send the current message draft (optimistic). + function handleSend() { const text = inputText.trim(); if (!text) return; setInputText(""); Keyboard.dismiss(); + + const localId = `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; + const now = new Date().toISOString(); + + // Show immediately in the list + setPending((prev) => [...prev, { localId, content: text, status: "sending", createTime: now }]); + toast.loading("正在发送", { id: localId }); + + // Send via WebSocket ws.send({ type: "send", content: text }); - }, [inputText, ws]); + + // 10-second timeout → mark as failed + pendingTimers.current[localId] = setTimeout(() => { + setPending((prev) => prev.map((p) => (p.localId === localId ? { ...p, status: "failed" } : p))); + toast.error("发送失败", { id: localId }); + }, 10_000); + } + + // Retry a failed message + function handleRetry(p: PendingMsg) { + setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "sending" } : x))); + toast.loading("正在发送", { id: p.localId }); + ws.send({ type: "send", content: p.content }); + pendingTimers.current[p.localId] = setTimeout(() => { + setPending((prev) => prev.map((x) => (x.localId === p.localId ? { ...x, status: "failed" } : x))); + toast.error("发送失败", { id: p.localId }); + }, 10_000); + } + + // Auto-scroll when a new message arrives (WebSocket push) + const msgCountRef = useRef(0); + useEffect(() => { + if (msgCountRef.current === 0 && allMessages.length > 0) { + // Initial data just arrived — seed count, don't scroll + msgCountRef.current = allMessages.length; + return; + } + if (allMessages.length > msgCountRef.current) { + scrollToBottom(true); + } + msgCountRef.current = allMessages.length; + }, [allMessages.length, scrollToBottom]); if (loading) { return ( @@ -400,9 +555,10 @@ export default function ChatPage() { behavior="padding" keyboardVerticalOffset={-60} > - - + + } footer={ - - + + + scrollToBottom(true)} + onSubmitEditing={handleSend} + returnKeyType="send" + multiline + maxLength={800} + textAlignVertical="center" + /> + - + @@ -435,6 +599,7 @@ export default function ChatPage() { } data={allMessages} keyExtractor={(item) => String(item.id)} + contentContainerStyle={{ paddingBottom: 12 }} onScroll={({ nativeEvent }) => { if (nativeEvent.contentOffset.y < 30 && hasMore && !loadingMore) { handleLoadMore(); @@ -442,8 +607,12 @@ export default function ChatPage() { }} scrollEventThrottle={200} onContentSizeChange={() => { - if (loadingMore) return; - listRef.current?.scrollToEnd({ animated: false }); + if (!revealed.current) { + revealed.current = true; + revealAtBottom(); + return; + } + if (!loadingMore) scrollToBottom(false); }} ListHeaderComponent={ loadingMore ? ( @@ -457,18 +626,45 @@ export default function ChatPage() { 暂无消息,开始聊天吧 } - renderItem={({ item }) => ( - - )} + renderItem={({ item }: { item: any }) => { + const pending = item._pending as PendingMsg | undefined; + if (pending) { + return ( + + {pending.status === "sending" ? ( + + + + ) : ( + handleRetry(pending)}> + + + + + + )} + + + {pending.content} + + + + ); + } + return ( + + ); + }} /> + ); } diff --git a/code/app/app-rn/src/app/(main)/tools/index.tsx b/code/app/app-rn/src/app/(main)/tools/index.tsx index 585a0090..72a6228b 100644 --- a/code/app/app-rn/src/app/(main)/tools/index.tsx +++ b/code/app/app-rn/src/app/(main)/tools/index.tsx @@ -1,5 +1,5 @@ import LayoutView from "@/components/layout/view"; -import { SearchField, CloseButton, PressableFeedback } from "heroui-native"; +import { SearchField, CloseButton, PressableFeedback, useThemeColor } from "heroui-native"; import { toast } from "sonner-native"; import { View, Text } from "react-native"; import { Stack, useRouter } from "expo-router"; @@ -14,6 +14,7 @@ import { Heart } from "lucide-react-native"; export default function Tools() { const router = useRouter(); const safeAreaInsets = useSafeAreaInsets(); + const accentColor = useThemeColor("accent"); const scrollRef = useRef(null); const headerPositions = useRef>({}); @@ -120,7 +121,7 @@ export default function Tools() { {/* 收藏分组特殊标记 */} {item.title === "收藏" && ( - + )} {/* 普通分组标记 */} @@ -175,8 +176,8 @@ export default function Tools() { > diff --git a/code/app/app-rn/src/components/DateTime.tsx b/code/app/app-rn/src/components/DateTime.tsx index 807fdd88..59f24a7b 100644 --- a/code/app/app-rn/src/components/DateTime.tsx +++ b/code/app/app-rn/src/components/DateTime.tsx @@ -1,6 +1,6 @@ import DateTimePicker from "react-native-dates-picker"; import dayjs from "dayjs"; -import { BottomSheet, Button, Spinner } from "heroui-native"; +import { BottomSheet, Button, Spinner, useThemeColor } from "heroui-native"; import { useState, useEffect, useCallback, useRef } from "react"; import { View } from "react-native"; import { CheckIcon } from "lucide-react-native"; @@ -49,6 +49,7 @@ export default function DateTime({ format, }: DatePickerProps) { const finalFormat = format ?? defaultFormatMap[mode]; + const accentColor = useThemeColor("accent"); const [open, setOpen] = useState(false); const [tempDate, setTempDate] = useState(dayjs()); @@ -100,7 +101,7 @@ export default function DateTime({ date={tempDate} initialView={modeViewMap[mode]} timePicker={mode === "time" || mode === "datetime"} - selectedItemColor="#0d9488" + selectedItemColor={accentColor} onChange={({ date }) => { if (date) setTempDate(dayjs(date)); }} diff --git a/code/app/app-rn/src/components/DateTimeRange.tsx b/code/app/app-rn/src/components/DateTimeRange.tsx index c54b884f..3e01a135 100644 --- a/code/app/app-rn/src/components/DateTimeRange.tsx +++ b/code/app/app-rn/src/components/DateTimeRange.tsx @@ -1,6 +1,6 @@ import DateTimePicker, { DateType } from "react-native-dates-picker"; import dayjs from "dayjs"; -import { BottomSheet, Button, Spinner } from "heroui-native"; +import { BottomSheet, Button, Spinner, useThemeColor } from "heroui-native"; import { View } from "react-native"; import { useState, useEffect, useCallback, useRef } from "react"; import { CheckIcon } from "lucide-react-native"; @@ -21,6 +21,8 @@ export default function DateTimeRange({ onClose, onConfirm, }: DateRangePickerProps) { + const accentColor = useThemeColor("accent"); + const accentSoftColor = useThemeColor("accent-soft"); const [open, setOpen] = useState(false); const [range, setRange] = useState<{ startDate: DateType; @@ -75,8 +77,8 @@ export default function DateTimeRange({ mode="range" startDate={range.startDate} endDate={range.endDate} - selectedItemColor="#0d9488" - selectedRangeBackgroundColor="rgba(13,148,136,0.15)" + selectedItemColor={accentColor} + selectedRangeBackgroundColor={accentSoftColor} onChange={(params) => setRange(params)} /> diff --git a/code/app/app-rn/src/components/layout/tabbar.tsx b/code/app/app-rn/src/components/layout/tabbar.tsx index 0dc1978b..e6b69d3d 100644 --- a/code/app/app-rn/src/components/layout/tabbar.tsx +++ b/code/app/app-rn/src/components/layout/tabbar.tsx @@ -7,13 +7,13 @@ import Animated, { withTiming, } from "react-native-reanimated"; import { useEffect } from "react"; +import { useThemeColor } from "heroui-native"; // ── 视觉常量 ────────────────────────────────────── const ICON_SIZE = 20; const LABEL_SIZE = 11; const BAR_HEIGHT = 56; const BAR_RADIUS = 16; -const COLOR_ACTIVE = "#10B981"; const COLOR_IDLE = "#9CA3AF"; const BAR_BG = "#FFFFFF"; @@ -62,6 +62,7 @@ function TabItem({ onPress: () => void; }) { const Icon = tab.icon; + const activeColor = useThemeColor("accent"); const scale = useSharedValue(1); useEffect(() => { @@ -86,7 +87,7 @@ function TabItem({ {tab.title ? ( @@ -94,7 +95,7 @@ function TabItem({ style={[ s.label, { - color: active ? COLOR_ACTIVE : COLOR_IDLE, + color: active ? activeColor : COLOR_IDLE, fontWeight: active ? "600" : "400", }, ]} diff --git a/code/app/app-rn/src/components/layout/view.tsx b/code/app/app-rn/src/components/layout/view.tsx index b516fce0..409cbbc5 100644 --- a/code/app/app-rn/src/components/layout/view.tsx +++ b/code/app/app-rn/src/components/layout/view.tsx @@ -8,7 +8,6 @@ import { ViewStyle, } from "react-native"; import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; - import { ScrollShadow } from "heroui-native"; import { LinearGradient } from "expo-linear-gradient"; import { LegendList } from "@legendapp/list"; @@ -134,14 +133,13 @@ LayoutView.FlatList = forwardRef( paddingTop: padding.paddingTop, paddingHorizontal: padding.paddingHorizontal, }} + className="z-[999]" > {header} ) : null} - - - + {footer ? (