fix: realtime friend-request badge via connection-level info fallback

Server friend.request.* signals ride {info} frames addressed to the
recipient's own user id, but the SDK dispatcher resolves that topic via
its topic cache, where the me topic lives under the literal me -- the
lookup missed and the frame was dropped before any listener could see
it, so the 新朋友 badge and NewFriendsView list only updated after a
manual refresh or relogin. topics.ts now forwards frames whose topic is
not in the SDK cache (isTopicCached) through the connection-level
onInfoMessage hook to the same infoListeners used by topic-level
routing (no duplicate delivery), and AppShell invalidates the shared
['friend-requests'] query namespace on friend.request.* events, keeping
the pending badge live on any page. Zentao #3.
This commit is contained in:
Austin
2026-09-18 17:42:48 +08:00
parent b4675b70e1
commit f4a6ff36b4
2 changed files with 51 additions and 2 deletions
+19 -1
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Outlet, useNavigate } from 'react-router-dom';
import { CallRoot } from '@/features/calls/CallRoot';
@@ -8,6 +8,7 @@ import { useBootstrap } from '@/lib/api/hooks';
import { getMe } from '@/lib/api/profile';
import { updateTabIndicator } from '@/lib/notifications/tabIndicator';
import { useChatList } from '@/lib/tinode/hooks';
import { subscribeTopicInfo } from '@/lib/tinode/topics';
import { initCallSignaling } from '@/stores/calls';
import { useNotificationNavStore } from '@/stores/notificationNav';
@@ -35,6 +36,23 @@ export function AppShell() {
useEffect(() => {
initCallSignaling();
}, []);
const queryClient = useQueryClient();
// Friend-request realtime signals (Zentao #3): after a request is created/accepted/rejected the
// server pushes `{info what:"im" event:"friend.request.*"}` to the recipient's `me` topic
// (publishIMRelationEvent -> sessionStore.DeliverToUser). Nothing here used to listen for it --
// the SDK's _routeInfo hits its default case for what:"im" and only fires the onInfo chain -- so
// the 新朋友 badge/list stayed stale until a manual refresh or relogin. Subscribe once per app
// session (same shape as initCallSignaling above) and invalidate the shared ['friend-requests']
// query namespace: ContactsListColumn's pending badge (['friend-requests','inbound','pending'])
// and NewFriendsView's list (['friend-requests','all']) both refetch on invalidation. Listening
// here rather than in those views keeps the badge live no matter which page the user is on.
useEffect(() => {
return subscribeTopicInfo((_topicName, info) => {
if (info?.event?.startsWith('friend.request.')) {
queryClient.invalidateQueries({ queryKey: ['friend-requests'] });
}
});
}, [queryClient]);
// "sys" has its own authoritative unread source in entry_badges (bootstrap.ts) -- excluded here
// in case it ever also shows up as a regular Tinode subscription in `contacts`, which would
// otherwise double-count the same unread messages into this badge. "slf" gets no such exclusion: