[Fix] Call get-membership-channel only once per path name

This commit is contained in:
MartinBraquet
2026-06-06 20:21:48 +02:00
parent baf006a53b
commit e85be4e029
6 changed files with 70 additions and 16 deletions

View File

@@ -1,12 +1,32 @@
import clsx from 'clsx'
import {PrivateMessageChannel} from 'common/supabase/private-messages'
import {PrivateUser} from 'common/user'
import {getNotificationDestinationsForUser} from 'common/user-notification-preferences'
import {usePathname} from 'next/navigation'
import {createContext, ReactNode, useContext} from 'react'
import {BiEnvelope, BiSolidEnvelope} from 'react-icons/bi'
import {Row} from 'web/components/layout/row'
import {useUnseenPrivateMessageChannels} from 'web/hooks/use-private-messages'
import {usePrivateUser} from 'web/hooks/use-user'
// Shared unseen-channels state so the desktop sidebar icon and the mobile
// bottom-nav icon (both mounted at once) consume a single fetch instead of each
// running their own `useUnseenPrivateMessageChannels`.
const UnseenMessageChannelsContext = createContext<PrivateMessageChannel[]>([])
export function UnseenMessageChannelsProvider(props: {children: ReactNode}) {
const {children} = props
const privateUser = usePrivateUser()
// The hook is always called (no conditional hooks); `enabled` gates the fetch
// so signed-out users don't hit the authed endpoint.
const {unseenChannels} = useUnseenPrivateMessageChannels(false, !!privateUser)
return (
<UnseenMessageChannelsContext.Provider value={unseenChannels}>
{children}
</UnseenMessageChannelsContext.Provider>
)
}
export function UnseenMessagesBubble(props: {className?: string}) {
const {className} = props
const privateUser = usePrivateUser()
@@ -50,7 +70,7 @@ function InternalUnseenMessagesBubble(props: {
}) {
const {privateUser, className, bubbleClassName} = props
const {unseenChannels} = useUnseenPrivateMessageChannels(false)
const unseenChannels = useContext(UnseenMessageChannelsContext)
const pathName = usePathname()
const {sendToBrowser} = getNotificationDestinationsForUser(privateUser, 'new_message')

View File

@@ -0,0 +1,27 @@
import {createContext, ReactNode, useContext} from 'react'
import {useSortedPrivateMessageMemberships} from 'web/hooks/use-private-messages'
import {useUser} from 'web/hooks/use-user'
type ChannelMembershipsValue = ReturnType<typeof useSortedPrivateMessageMemberships>
const PrivateMessageMembershipsContext = createContext<ChannelMembershipsValue>({
channels: undefined,
memberIdsByChannelId: undefined,
})
export const usePrivateMessageMembershipsContext = () =>
useContext(PrivateMessageMembershipsContext)
// Fetches the current user's channel memberships once and shares them via
// context so the many SendMessageButtons on a page (one per profile card) don't
// each call get-channel-memberships.
export function PrivateMessageMembershipsProvider(props: {children: ReactNode}) {
const {children} = props
const currentUser = useUser()
const channelMemberships = useSortedPrivateMessageMemberships(currentUser?.id)
return (
<PrivateMessageMembershipsContext.Provider value={channelMemberships}>
{children}
</PrivateMessageMembershipsContext.Provider>
)
}

View File

@@ -11,10 +11,10 @@ import {CommentInputTextArea} from 'web/components/comments/comment-input'
import {Col} from 'web/components/layout/col'
import {Modal, MODAL_CLASS} from 'web/components/layout/modal'
import {EmailVerificationPrompt} from 'web/components/messaging/email-verification-prompt'
import {usePrivateMessageMembershipsContext} from 'web/components/messaging/private-message-memberships-context'
import {useTextEditor} from 'web/components/widgets/editor'
import {Tooltip} from 'web/components/widgets/tooltip'
import {useFirebaseUser} from 'web/hooks/use-firebase-user'
import {useSortedPrivateMessageMemberships} from 'web/hooks/use-private-messages'
import {usePrivateUser, useUser} from 'web/hooks/use-user'
import {api} from 'web/lib/api'
import {firebaseLogin} from 'web/lib/firebase/users'
@@ -50,8 +50,7 @@ export const SendMessageButton = (props: {
const router = useRouter()
const privateUser = usePrivateUser()
const currentUser = useUser()
const channelMemberships = useSortedPrivateMessageMemberships(currentUser?.id)
const {memberIdsByChannelId} = channelMemberships
const {memberIdsByChannelId} = usePrivateMessageMembershipsContext()
const t = useT()
const [openComposeModal, setOpenComposeModal] = useState(false)

View File

@@ -12,10 +12,10 @@ const promiseCache: Record<string, Promise<any> | undefined> = {}
export const useAPIGetter = <P extends APIPath>(
path: P,
props: APIParams<P> | undefined,
ingoreDependencies?: string[],
ignoreDependencies?: string[],
) => {
const propsStringToTriggerRefresh = JSON.stringify(
deepCopyWithoutKeys(props, ingoreDependencies || []),
deepCopyWithoutKeys(props, ignoreDependencies || []),
)
// Key caching and in-flight dedup on the dependency-filtered props so that

View File

@@ -58,7 +58,7 @@ export function usePrivateMessages(channelId: number, limit: number, userId: str
return {messages, fetchMessages, setMessages}
}
export const useUnseenPrivateMessageChannels = (ignorePageSeenTime: boolean) => {
export const useUnseenPrivateMessageChannels = (ignorePageSeenTime: boolean, enabled = true) => {
const pathName = usePathname()
const lastSeenMessagesPageTime = useLastSeenMessagesPageTime() // ms for now
const [lastSeenChatTimeByChannelId, setLastSeenChatTimeByChannelId] = useState<
@@ -67,12 +67,14 @@ export const useUnseenPrivateMessageChannels = (ignorePageSeenTime: boolean) =>
const {data, refresh} = useAPIGetter(
'get-channel-memberships',
{
lastUpdatedTime: ignorePageSeenTime
? new Date(0).toISOString()
: millisToTs(lastSeenMessagesPageTime),
limit: 100,
},
enabled
? {
lastUpdatedTime: ignorePageSeenTime
? new Date(0).toISOString()
: millisToTs(lastSeenMessagesPageTime),
limit: 100,
}
: undefined,
['lastUpdatedTime'],
)
const {channels} = data ?? {

View File

@@ -21,6 +21,8 @@ import {useEffect, useState} from 'react'
import {AuthProvider, AuthUser} from 'web/components/auth-context'
import {ErrorBoundary} from 'web/components/error-boundary'
import {LiveRegionProvider} from 'web/components/live-region'
import {UnseenMessageChannelsProvider} from 'web/components/messaging/messages-icon'
import {PrivateMessageMembershipsProvider} from 'web/components/messaging/private-message-memberships-context'
import {ChoicesProvider} from 'web/hooks/use-choices'
import {useFontPreferenceManager} from 'web/hooks/use-font-preference'
import {useHasLoaded} from 'web/hooks/use-has-loaded'
@@ -237,9 +239,13 @@ function MyApp(props: AppProps<PageProps>) {
<ChoicesProvider>
<PinnedQuestionIdsProvider>
<HiddenProfilesProvider>
<WebPush />
<AndroidPush />
<Component {...pageProps} />
<UnseenMessageChannelsProvider>
<PrivateMessageMembershipsProvider>
<WebPush />
<AndroidPush />
<Component {...pageProps} />
</PrivateMessageMembershipsProvider>
</UnseenMessageChannelsProvider>
</HiddenProfilesProvider>
</PinnedQuestionIdsProvider>
</ChoicesProvider>