From ebfec37e4722a4c4438f2ff4e5695fa3bd7cd514 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 8 Jun 2026 03:06:12 -0500 Subject: [PATCH] 's' --- .env.local | 7 + app/[locale]/chat/[id]/page.tsx | 243 ++++++++++++++++++ app/[locale]/chat/page.tsx | 159 ++++++++++++ app/[locale]/dating/components/LikesPanel.tsx | 54 ++-- app/[locale]/dating/components/MatchModal.tsx | 5 +- app/[locale]/dating/page.tsx | 17 +- app/[locale]/dating/types.ts | 9 +- app/[locale]/dating/utils.ts | 2 + app/[locale]/messages/page.tsx | 6 + app/components/SiteMenu.tsx | 1 + app/lib/chat.ts | 81 ++++++ backend/__pycache__/init_db.cpython-312.pyc | Bin 19497 -> 19960 bytes backend/__pycache__/main.cpython-312.pyc | Bin 137820 -> 150098 bytes backend/__pycache__/settings.cpython-312.pyc | Bin 13131 -> 14910 bytes backend/dm.py | 228 ++++++++++++++++ backend/init_db.py | 17 ++ backend/main.py | 197 +++++++++++++- messages/en.json | 22 +- messages/zh.json | 22 +- scripts/ssh-build-frontend-prod.py | 44 ++++ scripts/ssh-check-docker-mirror.py | 39 +++ scripts/ssh-check-ntfy.py | 40 +++ scripts/ssh-fix-deploy.py | 40 +++ scripts/ssh-fix-lounge.py | 47 ++++ scripts/ssh-git-pull-prod.py | 4 +- scripts/ssh-ntfy-auth.py | 38 +++ scripts/ssh-patch-join-page.py | 32 +++ scripts/ssh-probe-server.py | 41 +++ scripts/ssh-restore-lounge.py | 131 ++++++++++ scripts/ssh-setup-ntfy-acl.py | 58 +++++ scripts/ssh-setup-ntfy-acl2.py | 57 ++++ scripts/ssh-setup-ntfy-acl3.py | 63 +++++ scripts/ssh-sync-ntfy-backend.py | 52 ++++ scripts/ssh-verify-prod.py | 28 ++ 34 files changed, 1753 insertions(+), 31 deletions(-) create mode 100644 app/[locale]/chat/[id]/page.tsx create mode 100644 app/[locale]/chat/page.tsx create mode 100644 app/lib/chat.ts create mode 100644 backend/dm.py create mode 100644 scripts/ssh-build-frontend-prod.py create mode 100644 scripts/ssh-check-docker-mirror.py create mode 100644 scripts/ssh-check-ntfy.py create mode 100644 scripts/ssh-fix-deploy.py create mode 100644 scripts/ssh-fix-lounge.py create mode 100644 scripts/ssh-ntfy-auth.py create mode 100644 scripts/ssh-patch-join-page.py create mode 100644 scripts/ssh-probe-server.py create mode 100644 scripts/ssh-restore-lounge.py create mode 100644 scripts/ssh-setup-ntfy-acl.py create mode 100644 scripts/ssh-setup-ntfy-acl2.py create mode 100644 scripts/ssh-setup-ntfy-acl3.py create mode 100644 scripts/ssh-sync-ntfy-backend.py create mode 100644 scripts/ssh-verify-prod.py diff --git a/.env.local b/.env.local index 459e423..28d0b93 100644 --- a/.env.local +++ b/.env.local @@ -44,5 +44,12 @@ PAYMENT_API_URL=http://127.0.0.1:8000 NEXT_PUBLIC_MIROTALK_URL=https://mirotalk.nomadro.cn NEXT_PUBLIC_LOUNGE_URL=https://lounge.nomadro.cn +NEXT_PUBLIC_LISTMONK_URL=https://listmonk.nomadro.cn +LISTMONK_API_URL=https://listmonk.nomadro.cn + +NEXT_PUBLIC_NTFY_URL=https://ntfy.nomadro.cn +NTFY_URL=https://ntfy.nomadro.cn +NTFY_ENABLED=true + # 本地调试可自动完成支付(无需真实回调) DEV_AUTO_PAY=true diff --git a/app/[locale]/chat/[id]/page.tsx b/app/[locale]/chat/[id]/page.tsx new file mode 100644 index 0000000..85ecb4f --- /dev/null +++ b/app/[locale]/chat/[id]/page.tsx @@ -0,0 +1,243 @@ +"use client"; + +import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; +import { Link } from "@/i18n/navigation"; +import { useLocale, useTranslation } from "@/i18n/navigation"; +import { useParams } from "next/navigation"; +import Footer from "@/app/components/Footer"; +import { fetchAuthMe } from "@/app/lib/api-client"; +import { + fetchConversation, + fetchMessages, + sendChatMessage, + type ChatMessage, + type ConversationItem, +} from "@/app/lib/chat"; +import { mediaUrl } from "@/app/lib/media"; + +const POLL_MS = 4000; + +function formatMessageTime(value?: string, locale = "zh"): string { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + return date.toLocaleTimeString(locale === "zh" ? "zh-CN" : "en-US", { + hour: "2-digit", + minute: "2-digit", + }); +} + +export default function ChatThreadPage() { + const params = useParams(); + const conversationId = String(params?.id || ""); + const { t } = useTranslation("chat"); + const locale = useLocale(); + + const [conversation, setConversation] = useState(null); + const [messages, setMessages] = useState([]); + const [draft, setDraft] = useState(""); + const [loading, setLoading] = useState(true); + const [sending, setSending] = useState(false); + const [error, setError] = useState(""); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const bottomRef = useRef(null); + const listRef = useRef(null); + + const scrollToBottom = useCallback(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, []); + + const loadThread = useCallback(async () => { + if (!conversationId) return; + try { + const [meta, items] = await Promise.all([ + fetchConversation(conversationId), + fetchMessages(conversationId), + ]); + setConversation(meta); + setMessages(items); + setError(""); + } catch (err) { + setError(err instanceof Error ? err.message : t("loadError")); + } finally { + setLoading(false); + } + }, [conversationId, t]); + + useEffect(() => { + fetchAuthMe().then((data) => setIsLoggedIn(Boolean(data?.user?.id))); + }, []); + + useEffect(() => { + loadThread(); + }, [loadThread]); + + useEffect(() => { + if (!conversationId || !isLoggedIn) return undefined; + const timer = window.setInterval(() => { + fetchMessages(conversationId) + .then((items) => setMessages(items)) + .catch(() => undefined); + }, POLL_MS); + return () => window.clearInterval(timer); + }, [conversationId, isLoggedIn]); + + useEffect(() => { + scrollToBottom(); + }, [messages, scrollToBottom]); + + const handleSend = async (event: FormEvent) => { + event.preventDefault(); + const body = draft.trim(); + if (!body || sending || !conversationId) return; + setSending(true); + setError(""); + try { + const message = await sendChatMessage(conversationId, body); + setMessages((prev) => [...prev, message]); + setDraft(""); + setConversation((prev) => + prev + ? { + ...prev, + lastMessagePreview: body.replace(/\n/g, " ").slice(0, 180), + lastMessageAt: message.created || prev.lastMessageAt, + } + : prev + ); + } catch (err) { + setError(err instanceof Error ? err.message : t("sendError")); + } finally { + setSending(false); + } + }; + + const peerName = conversation?.peer?.name || (locale === "zh" ? "成员" : "Member"); + + return ( +
+
+
+ + ← {t("back")} + + {conversation?.peer ? ( +
+ {peerName} +
+

{peerName}

+ {conversation.peer.location ? ( +

📍 {conversation.peer.location}

+ ) : null} +
+
+ ) : ( +

{t("title")}

+ )} +
+
+ +
+ {!isLoggedIn ? ( +
+
+

{t("loginRequired")}

+ + {t("loginAction")} + +
+
+ ) : loading ? ( +
+
+
+ ) : error && !messages.length ? ( +
+
+

{error}

+ + {t("back")} + +
+
+ ) : ( +
+ {!messages.length ? ( +
+

{t("emptyThread")}

+
+ ) : ( + messages.map((message) => ( +
+
+

{message.body}

+

+ {formatMessageTime(message.created, locale)} +

+
+
+ )) + )} +
+
+ )} + + {isLoggedIn && !loading ? ( +
+ {error && messages.length ? ( +

{error}

+ ) : null} +
+