feat(search): focus and highlight found message in chat
- store focused message id per chat - scroll to target message and highlight it after search selection - clear focus automatically after short timeout
This commit is contained in:
@@ -28,6 +28,8 @@ export function MessageList() {
|
||||
const loadingMoreByChat = useChatStore((s) => s.loadingMoreByChat);
|
||||
const loadMoreMessages = useChatStore((s) => s.loadMoreMessages);
|
||||
const unreadBoundaryByChat = useChatStore((s) => s.unreadBoundaryByChat);
|
||||
const focusedMessageIdByChat = useChatStore((s) => s.focusedMessageIdByChat);
|
||||
const setFocusedMessage = useChatStore((s) => s.setFocusedMessage);
|
||||
const chats = useChatStore((s) => s.chats);
|
||||
const setReplyToMessage = useChatStore((s) => s.setReplyToMessage);
|
||||
const updateChatPinnedMessage = useChatStore((s) => s.updateChatPinnedMessage);
|
||||
@@ -64,6 +66,7 @@ export function MessageList() {
|
||||
}, [chats, forwardQuery]);
|
||||
const unreadBoundaryCount = activeChatId ? (unreadBoundaryByChat[activeChatId] ?? 0) : 0;
|
||||
const unreadBoundaryIndex = unreadBoundaryCount > 0 ? Math.max(0, messages.length - unreadBoundaryCount) : -1;
|
||||
const focusedMessageId = activeChatId ? (focusedMessageIdByChat[activeChatId] ?? null) : null;
|
||||
const hasMore = Boolean(activeChatId && hasMoreByChat[activeChatId]);
|
||||
const isLoadingMore = Boolean(activeChatId && loadingMoreByChat[activeChatId]);
|
||||
const selectedMessages = useMemo(
|
||||
@@ -104,6 +107,19 @@ export function MessageList() {
|
||||
return () => window.clearInterval(interval);
|
||||
}, [pendingDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeChatId || !focusedMessageId) {
|
||||
return;
|
||||
}
|
||||
const element = document.getElementById(`message-${focusedMessageId}`);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
const timer = window.setTimeout(() => setFocusedMessage(activeChatId, null), 2500);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [activeChatId, focusedMessageId, messages.length, setFocusedMessage]);
|
||||
|
||||
if (!activeChatId) {
|
||||
return <div className="flex h-full items-center justify-center text-slate-300/80">Select a chat</div>;
|
||||
}
|
||||
@@ -287,9 +303,10 @@ export function MessageList() {
|
||||
) : null}
|
||||
<div className={`mb-2 flex ${own ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
id={`message-${message.id}`}
|
||||
className={`max-w-[86%] rounded-2xl px-3 py-2 shadow-sm md:max-w-[72%] ${
|
||||
own ? "rounded-br-md bg-sky-500/90 text-slate-950" : "rounded-bl-md border border-slate-700/60 bg-slate-900/80 text-slate-100"
|
||||
} ${isSelected ? "ring-2 ring-sky-400/80" : ""}`}
|
||||
} ${isSelected ? "ring-2 ring-sky-400/80" : ""} ${focusedMessageId === message.id ? "ring-2 ring-amber-300" : ""}`}
|
||||
onClick={() => {
|
||||
if (selectedIds.size > 0) {
|
||||
toggleSelected(message.id);
|
||||
|
||||
@@ -17,6 +17,7 @@ export function ChatsPage() {
|
||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||
const chats = useChatStore((s) => s.chats);
|
||||
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
|
||||
const setFocusedMessage = useChatStore((s) => s.setFocusedMessage);
|
||||
const loadMessages = useChatStore((s) => s.loadMessages);
|
||||
const activeChat = chats.find((chat) => chat.id === activeChatId);
|
||||
const isReadOnlyChannel = Boolean(activeChat && activeChat.type === "channel" && activeChat.my_role === "member");
|
||||
@@ -147,6 +148,7 @@ export function ChatsPage() {
|
||||
key={`search-msg-${message.id}`}
|
||||
onClick={() => {
|
||||
setActiveChatId(message.chat_id);
|
||||
setFocusedMessage(message.chat_id, message.id);
|
||||
setSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ interface ChatState {
|
||||
typingByChat: Record<number, number[]>;
|
||||
replyToByChat: Record<number, Message | null>;
|
||||
unreadBoundaryByChat: Record<number, number>;
|
||||
focusedMessageIdByChat: Record<number, number | null>;
|
||||
loadChats: (query?: string) => Promise<void>;
|
||||
setActiveChatId: (chatId: number | null) => void;
|
||||
loadMessages: (chatId: number) => Promise<void>;
|
||||
@@ -44,6 +45,7 @@ interface ChatState {
|
||||
applyPresenceEvent: (chatId: number, userId: number, isOnline: boolean, lastSeenAt?: string) => void;
|
||||
setDraft: (chatId: number, text: string) => void;
|
||||
clearDraft: (chatId: number) => void;
|
||||
setFocusedMessage: (chatId: number, messageId: number | null) => void;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>((set, get) => ({
|
||||
@@ -56,6 +58,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
typingByChat: {},
|
||||
replyToByChat: {},
|
||||
unreadBoundaryByChat: {},
|
||||
focusedMessageIdByChat: {},
|
||||
loadChats: async (query) => {
|
||||
const chats = await getChats(query);
|
||||
const currentActive = get().activeChatId;
|
||||
@@ -342,5 +345,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
const next = { ...state.draftsByChat };
|
||||
delete next[chatId];
|
||||
return { draftsByChat: next };
|
||||
})
|
||||
}),
|
||||
setFocusedMessage: (chatId, messageId) =>
|
||||
set((state) => ({
|
||||
focusedMessageIdByChat: {
|
||||
...state.focusedMessageIdByChat,
|
||||
[chatId]: messageId
|
||||
}
|
||||
}))
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user