🐛(frontend) refresh thread view on new messages (#453)

If the message count of the selected thread has changed the view should be
refreshed to display the new message.
This commit is contained in:
Jean-Baptiste PENRATH
2025-12-11 23:54:13 +01:00
committed by GitHub
parent b1eb39c641
commit bba7662691
5 changed files with 26 additions and 15 deletions
@@ -38,7 +38,7 @@ export const ThreadMessage = forwardRef<HTMLElement, ThreadMessageProps>(
const replyFormRef = useRef<HTMLDivElement>(null);
const threadViewContext = useThreadViewContext()
const isMessageReady = threadViewContext.isMessageReady(message.id);
const [isMessageBodyLoaded, setIsMessageBodyLoaded] = useState(false);
const [isMessageBodyLoaded, setIsMessageBodyLoaded] = useState(isMessageReady);
const [isFolded, setIsFolded] = useState(!isLatest && !message.is_unread && !draftMessage?.is_draft);
const [replyFormMode, setReplyFormMode] = useState<MessageFormMode | null>(getReplyFormMode)
const previousReplyFormMode = usePrevious<MessageFormMode | null>(replyFormMode);
@@ -97,10 +97,6 @@ export const ThreadMessage = forwardRef<HTMLElement, ThreadMessageProps>(
setReplyFormMode(getReplyFormMode())
}, [message, draftMessage])
useEffect(() => {
setIsFolded(!isLatest && !message.is_unread && !draftMessage?.is_draft);
}, [isLatest, draftMessage?.is_draft])
// Smooth scroll to the reply form when it is opened by the user
useEffect(() => {
if (!threadViewContext.isReady) return;
@@ -42,7 +42,7 @@ const ThreadViewComponent = ({ messages, mailboxId, thread, showTrashedMessages,
const rootRef = useRef<HTMLDivElement>(null);
const { markAsRead } = useRead();
const isAISummaryEnabled = useFeatureFlag(FEATURE_KEYS.AI_SUMMARY);
const { isReady, reset } = useThreadViewContext();
const { isReady, reset, hasBeenInitialized, setHasBeenInitialized } = useThreadViewContext();
// Refs for all unread messages
const unreadRefs = useRef<Record<string, HTMLElement | null>>({});
// Find all unread message IDs
@@ -95,7 +95,7 @@ const ThreadViewComponent = ({ messages, mailboxId, thread, showTrashedMessages,
}, [isReady, unreadMessageIds.join(","), thread.id]);
useEffect(() => {
if (isReady) {
if (isReady && !hasBeenInitialized) {
let messageToScroll = latestMessage?.id;
let selector = `#thread-message-${messageToScroll}`;
if (draftMessageIds.length > 0) {
@@ -109,15 +109,15 @@ const ThreadViewComponent = ({ messages, mailboxId, thread, showTrashedMessages,
const el = document.querySelector<HTMLElement>(selector);
if (el) {
rootRef.current?.scrollTo({ top: el.offsetTop - 225, behavior: 'instant' });
setHasBeenInitialized(true);
}
}
}, [isReady]);
useEffect(() => {
useEffect(() => () => {
reset();
}, [thread.id]);
return (
<div className={clsx("thread-view", { "thread-view--talk": isThreadSender })} ref={rootRef}>
<div className="thread-view__sticky-container" ref={stickyContainerRef}>
@@ -9,6 +9,8 @@ type ThreadViewContextType = {
isMessageReady: (messageId: string) => boolean | undefined;
setMessageReadiness: (messageId: string, isReady: boolean) => void;
reset: (messageId?: string) => void;
hasBeenInitialized: boolean;
setHasBeenInitialized: (hasBeenInitialized: boolean) => void;
}
const ThreadViewContext = createContext<ThreadViewContextType | undefined>(undefined);
@@ -19,6 +21,7 @@ const ThreadViewContext = createContext<ThreadViewContextType | undefined>(undef
*/
const ThreadViewProvider = ({ messageIds, children }: ThreadViewProviderProps) => {
const [messagesReadiness, setMessagesReadiness] = useState(new Map(messageIds.map((id) => [id, false])));
const [hasBeenInitialized, setHasBeenInitialized] = useState(false);
const isReady = useMemo(() => {
return Array.from(messagesReadiness.values()).every((isReady) => isReady === true);
@@ -48,6 +51,7 @@ const ThreadViewProvider = ({ messageIds, children }: ThreadViewProviderProps) =
setMessageReadiness(messageId, false);
} else {
setMessagesReadiness(new Map(messageIds.map((id) => [id, false])));
setHasBeenInitialized(false);
}
}
@@ -56,8 +60,10 @@ const ThreadViewProvider = ({ messageIds, children }: ThreadViewProviderProps) =
isMessageReady,
setMessageReadiness,
reset,
hasBeenInitialized,
setHasBeenInitialized,
messagesReadiness,
}), [isReady, setMessageReadiness, isMessageReady, reset, messagesReadiness]);
}), [isReady, setMessageReadiness, isMessageReady, reset, messagesReadiness, hasBeenInitialized, setHasBeenInitialized]);
@@ -122,7 +122,7 @@ export const MailboxProvider = ({ children }: PropsWithChildren) => {
?? mailboxQuery.data.data[mailboxQuery.data.data.length - 1]
}, [router.query.mailboxId, mailboxQuery.data])
const previousUnreadMessagesCount = usePrevious(selectedMailbox?.count_unread_messages || 0);
const previousUnreadMessagesCount = usePrevious(selectedMailbox?.count_unread_messages);
const threadQueryKey = useMemo(() => {
const queryKey = ['threads', selectedMailbox?.id];
if (searchParams.get('search')) {
@@ -167,6 +167,7 @@ export const MailboxProvider = ({ children }: PropsWithChildren) => {
const threadId = router.query.threadId;
return threadsQuery.data?.pages.flatMap((page) => page.data.results).find((thread) => thread.id === threadId) ?? null;
}, [router.query.threadId, flattenThreads])
const previousSelectedThreadMessagesCount = usePrevious(selectedThread?.messages.length);
const messagesQuery = useMessagesList({
query: {
@@ -335,13 +336,21 @@ export const MailboxProvider = ({ children }: PropsWithChildren) => {
// Invalidate the threads query to refresh the threads list when the unread messages count changes
useEffect(() => {
if (!selectedMailbox) return;
if ((previousUnreadMessagesCount ?? 0) !== (selectedMailbox.count_unread_messages)) {
if (!selectedMailbox || previousUnreadMessagesCount === undefined) return;
if (previousUnreadMessagesCount !== selectedMailbox.count_unread_messages) {
invalidateThreadsStats();
queryClient.invalidateQueries({ queryKey: ['threads', selectedMailbox?.id] });
}
}, [selectedMailbox?.count_unread_messages]);
// Invalidate the thread messages query to refresh the thread messages when there is a new message
useEffect(() => {
if (!selectedThread || previousSelectedThreadMessagesCount === undefined) return;
if (previousSelectedThreadMessagesCount < (selectedThread?.messages.length ?? 0)) {
invalidateThreadMessages();
}
}, [selectedThread?.messages.length]);
useEffect(() => {
if (searchParams.get('search') !== previousSearchParams?.get('search')) {
resetSearchQueryDebounced();
+2 -2
View File
@@ -5,7 +5,7 @@ import { useEffect, useRef } from 'react';
* @param value The value to track
* @returns The previous value of the tracked variable
*/
function usePrevious<T>(value: T): T | undefined {
function usePrevious<T>(value: T): T {
const ref = useRef<T>(value);
useEffect(() => {
@@ -15,4 +15,4 @@ function usePrevious<T>(value: T): T | undefined {
return ref.current;
}
export default usePrevious;
export default usePrevious;