diff --git a/apps/tlon-web/src/app.tsx b/apps/tlon-web/src/app.tsx index eb365ce2dc..23c3f470c1 100644 --- a/apps/tlon-web/src/app.tsx +++ b/apps/tlon-web/src/app.tsx @@ -677,6 +677,23 @@ function RoutedApp() { const theme = useTheme(); const isDarkMode = useIsDark(); + useEffect(() => { + const onFocus = () => { + useLocalState.setState({ inFocus: true }); + }; + window.addEventListener('focus', onFocus); + + const onBlur = () => { + useLocalState.setState({ inFocus: false }); + }; + window.addEventListener('blur', onBlur); + + return () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + }; + }, []); + useEffect(() => { window.toggleDevTools = () => toggleDevTools(); }, []); diff --git a/apps/tlon-web/src/chat/ChatMessage/ChatMessage.tsx b/apps/tlon-web/src/chat/ChatMessage/ChatMessage.tsx index c94e8b9171..9f754869f9 100644 --- a/apps/tlon-web/src/chat/ChatMessage/ChatMessage.tsx +++ b/apps/tlon-web/src/chat/ChatMessage/ChatMessage.tsx @@ -65,6 +65,7 @@ import { useTrackedMessageStatus, } from '@/state/chat'; import { useRouteGroup } from '@/state/groups'; +import { useInFocus } from '@/state/local'; import ReactionDetails from '../ChatReactions/ReactionDetails'; import { @@ -219,6 +220,7 @@ const ChatMessage = React.memo< [isMessageHidden, isPostHidden] ); + const inFocus = useInFocus(); const { ref: viewRef, inView } = useInView({ threshold: 1, }); @@ -226,7 +228,7 @@ const ChatMessage = React.memo< useEffect(() => { const mainUnread = unreadDisplay === 'top' || unreadDisplay === 'top-with-thread'; - if (!inView || !mainUnread) { + if (!inFocus || !inView || !mainUnread) { return; } @@ -235,7 +237,14 @@ const ChatMessage = React.memo< } else { markReadChannel(); } - }, [inView, unreadDisplay, isDMOrMultiDM, markReadChannel, markDmRead]); + }, [ + inFocus, + inView, + unreadDisplay, + isDMOrMultiDM, + markReadChannel, + markDmRead, + ]); const cacheId = { author: window.our, diff --git a/apps/tlon-web/src/chat/ChatMessage/DeletedChatMessage.tsx b/apps/tlon-web/src/chat/ChatMessage/DeletedChatMessage.tsx index 954c57bfed..3a8a27d04e 100644 --- a/apps/tlon-web/src/chat/ChatMessage/DeletedChatMessage.tsx +++ b/apps/tlon-web/src/chat/ChatMessage/DeletedChatMessage.tsx @@ -11,6 +11,7 @@ import DateDivider from '@/chat/ChatMessage/DateDivider'; import { useMarkChannelRead } from '@/logic/channel'; import { useStickyUnread } from '@/logic/useStickyUnread'; import { useSourceActivity } from '@/state/activity'; +import { useInFocus } from '@/state/local'; export interface DeletedChatMessageProps { whom: string; @@ -61,17 +62,18 @@ const DeletedChatMessage = React.memo< ); const { markRead: markReadChannel } = useMarkChannelRead(`chat/${whom}`); + const inFocus = useInFocus(); const { ref: viewRef, inView } = useInView({ threshold: 1, }); useEffect(() => { - if (!inView || !isUnread) { + if (!inFocus || !inView || !isUnread) { return; } markReadChannel(); - }, [inView, isUnread, markReadChannel]); + }, [inFocus, inView, isUnread, markReadChannel]); return (
isMessageHidden || isPostHidden, [isMessageHidden, isPostHidden] ); + const inFocus = useInFocus(); const { ref: viewRef, inView } = useInView({ threshold: 1, }); useEffect(() => { // if no tracked unread we don't need to take any action - if (!inView || !isUnread) { + if (!inFocus || !inView || !isUnread) { return; } @@ -175,7 +177,15 @@ const ReplyMessage = React.memo< } else { markChannelRead(); } - }, [whom, inView, isUnread, isDMOrMultiDM, markChannelRead, markDmRead]); + }, [ + whom, + inFocus, + inView, + isUnread, + isDMOrMultiDM, + markChannelRead, + markDmRead, + ]); const msgStatus = useTrackedMessageStatus({ author: window.our, diff --git a/apps/tlon-web/src/state/activity.ts b/apps/tlon-web/src/state/activity.ts index 64e4da996c..3cbf49ea88 100644 --- a/apps/tlon-web/src/state/activity.ts +++ b/apps/tlon-web/src/state/activity.ts @@ -2,7 +2,6 @@ import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { Activity, ActivityAction, - ActivityBundle, ActivityDeleteUpdate, ActivityFeed, ActivityReadUpdate, @@ -16,6 +15,7 @@ import { Source, VolumeMap, VolumeSettings, + getKey, sourceToString, stripSourcePrefix, } from '@tloncorp/shared/dist/urbit/activity'; @@ -23,10 +23,12 @@ import _ from 'lodash'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import api from '@/api'; +import { useChatStore } from '@/chat/useChatStore'; import useReactQueryScry from '@/logic/useReactQueryScry'; import { createDevLogger } from '@/logic/utils'; import queryClient from '@/queryClient'; +import { useLocalState } from './local'; import { SidebarFilter } from './settings'; const actLogger = createDevLogger('activity', false); @@ -128,6 +130,22 @@ function activityVolumeUpdates(events: ActivityVolumeUpdate[]) { }, {} as VolumeSettings); } +function optimisticActivityUpdate(d: Activity, source: string): Activity { + const old = d[source]; + return { + ...d, + [source]: { + ...old, + unread: null, + count: Math.min(0, old.count - (old.unread?.count || 0)), + 'notify-count': + old.unread && old.unread.notify + ? Math.min(old['notify-count'] - old.unread.count) + : old['notify-count'], + }, + }; +} + function updateActivity({ main, threads, @@ -135,10 +153,18 @@ function updateActivity({ main: Activity; threads: Record; }) { + const { current, atBottom } = useChatStore.getState(); + const source = getKey(current); + const inFocus = useLocalState.getState().inFocus; + const filteredMain = + inFocus && atBottom && source in main + ? optimisticActivityUpdate(main, source) + : main; + console.log({ inFocus, source, atBottom, filteredMain }); queryClient.setQueryData(unreadsKey(), (d: Activity | undefined) => { return { ...d, - ...main, + ...filteredMain, }; }); @@ -304,19 +330,7 @@ export function useMarkReadMutation(recursive = false) { }; } - const old = d[source]; - return { - ...d, - [source]: { - ...old, - unread: null, - count: Math.min(0, old.count - (old.unread?.count || 0)), - 'notify-count': - old.unread && old.unread.notify - ? Math.min(old['notify-count'] - old.unread.count) - : old['notify-count'], - }, - }; + return optimisticActivityUpdate(d, source); }); return { current }; diff --git a/apps/tlon-web/src/state/local.ts b/apps/tlon-web/src/state/local.ts index 8f24d8a791..933c5201fe 100644 --- a/apps/tlon-web/src/state/local.ts +++ b/apps/tlon-web/src/state/local.ts @@ -22,6 +22,7 @@ interface LocalState { errorCount: number; airLockErrorCount: number; lastReconnect: number; + inFocus: boolean; onReconnect: (() => void) | null; logs: string[]; log: (msg: string) => void; @@ -44,6 +45,7 @@ export const useLocalState = create( lastReconnect: Date.now(), onReconnect: null, logs: [], + inFocus: true, log: (msg: string) => { set( produce((s) => { @@ -107,3 +109,8 @@ const selLast = (s: LocalState) => s.lastReconnect; export function useLastReconnect() { return useLocalState(selLast); } + +const selInFocus = (s: LocalState) => s.inFocus; +export function useInFocus() { + return useLocalState(selInFocus); +}