From baeeb2bfedff4063043a4905febbd47bff7f8f52 Mon Sep 17 00:00:00 2001 From: haorwen Date: Sat, 27 Dec 2025 18:08:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD=20(#285)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add MessageSearch component for message filtering in chat * chore: bump version to v0.9.41 --- package.json | 2 +- src/components/MessageSearch/index.tsx | 115 ++++++++++++++++++ src/routes/chat/ChannelChat/index.tsx | 11 +- src/routes/chat/DMChat/index.tsx | 13 +- .../chat/Layout/VirtualMessageFeed/index.tsx | 23 +++- src/routes/chat/Layout/index.tsx | 8 +- 6 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 src/components/MessageSearch/index.tsx diff --git a/package.json b/package.json index 6cdd929d..e2b7a7be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vocechat-web", - "version": "0.9.40", + "version": "0.9.41", "homepage": "https://voce.chat", "dependencies": { "@metamask/onboarding": "^1.0.1", diff --git a/src/components/MessageSearch/index.tsx b/src/components/MessageSearch/index.tsx new file mode 100644 index 00000000..200d07b5 --- /dev/null +++ b/src/components/MessageSearch/index.tsx @@ -0,0 +1,115 @@ +import { useState, useMemo } from "react"; +import { useAppSelector } from "@/app/store"; +import { shallowEqual } from "react-redux"; +import { ChatContext } from "@/types/common"; +import dayjs from "dayjs"; +import Avatar from "@/components/Avatar"; +import IconSearch from "@/assets/icons/search.svg"; +import IconClose from "@/assets/icons/close.circle.svg"; + +interface Props { + context: ChatContext; + id: number; + onLocate: (mid: number) => void; +} + +export default function MessageSearch({ context, id, onLocate }: Props) { + const [query, setQuery] = useState(""); + const [visible, setVisible] = useState(false); + + const mids = useAppSelector( + (store) => (context === "channel" ? store.channelMessage[id] : store.userMessage.byId[id]) || [], + shallowEqual + ); + const messageData = useAppSelector((store) => store.message, shallowEqual); + const usersData = useAppSelector((store) => store.users.byId, shallowEqual); + + const searchResults = useMemo(() => { + if (!query.trim()) return []; + + const lowerQuery = query.toLowerCase(); + return mids + .map((mid) => messageData[mid]) + .filter((msg) => { + if (!msg || !msg.content) return false; + if (msg.content_type === "text/plain" || msg.content_type === "text/markdown") { + const content = typeof msg.content === "string" ? msg.content : ""; + return content.toLowerCase().includes(lowerQuery); + } + return false; + }) + .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) + .slice(0, 50); + }, [query, mids, messageData]); + + const handleLocate = (mid: number) => { + onLocate(mid); + setVisible(false); + setQuery(""); + }; + + return ( +
+ + + {visible && ( +
+
+
+ setQuery(e.target.value)} + placeholder="搜索消息..." + className="flex-1 px-3 py-2 bg-gray-50 dark:bg-gray-700 rounded outline-none text-sm" + autoFocus + /> + +
+
+ +
+ {query && searchResults.length === 0 && ( +
未找到匹配的消息
+ )} + {searchResults.map((msg) => { + const user = usersData[msg.from_uid || 0]; + return ( +
handleLocate(msg.mid)} + className="p-3 flex items-start gap-2 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700" + > + +
+
+ + {user?.name} + + + {dayjs(msg.created_at).isSame(dayjs(), 'day') + ? dayjs(msg.created_at).format("HH:mm") + : dayjs(msg.created_at).format("MM-DD HH:mm")} + +
+
+ {typeof msg.content === "string" ? msg.content : ""} +
+
+
+ ); + })} +
+
+ )} +
+ ); +} + diff --git a/src/routes/chat/ChannelChat/index.tsx b/src/routes/chat/ChannelChat/index.tsx index 95abd8dd..9a693950 100644 --- a/src/routes/chat/ChannelChat/index.tsx +++ b/src/routes/chat/ChannelChat/index.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect } from "react"; +import { memo, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { shallowEqual, useDispatch } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; @@ -10,11 +10,13 @@ import { useAppSelector } from "@/app/store"; import ChannelIcon from "@/components/ChannelIcon"; import GoBackNav from "@/components/GoBackNav"; import Tooltip from "@/components/Tooltip"; +import MessageSearch from "@/components/MessageSearch"; import IconFav from "@/assets/icons/bookmark.svg"; import IconPeople from "@/assets/icons/people.svg"; import IconPin from "@/assets/icons/pin.svg"; import FavList from "../FavList"; import Layout from "../Layout"; +import { VirtualMessageFeedHandle } from "../Layout/VirtualMessageFeed"; import VoiceChat from "../VoiceChat"; import Dashboard from "../VoiceChat/Dashboard"; import Members from "./Members"; @@ -32,6 +34,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { const { pathname } = useLocation(); const navigate = useNavigate(); const dispatch = useDispatch(); + const feedRef = useRef(null); const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); const visibleAside = useAppSelector((store) => store.footprint.channelAsides[cid], shallowEqual); const userIds = useAppSelector((store) => store.users.ids, shallowEqual); @@ -58,6 +61,10 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { ); }; + const handleLocate = (mid: number) => { + feedRef.current?.scrollToMessage(mid); + }; + if (!data) return null; const { name, description, is_public, members = [], owner } = data; const memberIds = is_public ? userIds : members.slice(0).sort((n) => (n == owner ? -1 : 0)); @@ -71,6 +78,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { to={cid} context="channel" dropFiles={dropFiles} + feedRef={feedRef} aside={