diff --git a/web/components/messaging/messages-icon.tsx b/web/components/messaging/messages-icon.tsx index bbbedaf3..3833f457 100644 --- a/web/components/messaging/messages-icon.tsx +++ b/web/components/messaging/messages-icon.tsx @@ -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([]) + +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 ( + + {children} + + ) +} + 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') diff --git a/web/components/messaging/private-message-memberships-context.tsx b/web/components/messaging/private-message-memberships-context.tsx new file mode 100644 index 00000000..7213d5c2 --- /dev/null +++ b/web/components/messaging/private-message-memberships-context.tsx @@ -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 + +const PrivateMessageMembershipsContext = createContext({ + 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 ( + + {children} + + ) +} diff --git a/web/components/messaging/send-message-button.tsx b/web/components/messaging/send-message-button.tsx index 1d333c29..c40d2a56 100644 --- a/web/components/messaging/send-message-button.tsx +++ b/web/components/messaging/send-message-button.tsx @@ -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) diff --git a/web/hooks/use-api-getter.ts b/web/hooks/use-api-getter.ts index 024cacbd..afadf84f 100644 --- a/web/hooks/use-api-getter.ts +++ b/web/hooks/use-api-getter.ts @@ -12,10 +12,10 @@ const promiseCache: Record | undefined> = {} export const useAPIGetter =

( path: P, props: APIParams

| 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 diff --git a/web/hooks/use-private-messages.ts b/web/hooks/use-private-messages.ts index e6ed6425..f5b38619 100644 --- a/web/hooks/use-private-messages.ts +++ b/web/hooks/use-private-messages.ts @@ -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 ?? { diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index a3671532..d0989588 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -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) { - - - + + + + + + +