[🐴] Unread messages badge (#3901)
* add badge * move stringify logic to hook * add mutation hooks * optimistic mark convo as read * don't count muted chats * Integrate new context * Integrate mark unread mutation * Remove unused edit --------- Co-authored-by: Eric Bailey <git@esb.lol>zio/stable
parent
0c41b3188a
commit
4fe5a869c3
|
@ -6,6 +6,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||||
import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo'
|
import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo'
|
||||||
import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
|
import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
|
||||||
import {MessagesEventBusProvider} from '#/state/messages/events'
|
import {MessagesEventBusProvider} from '#/state/messages/events'
|
||||||
|
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
|
||||||
|
@ -37,15 +38,18 @@ export function ChatProvider({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
|
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
|
||||||
|
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
convo.resume()
|
convo.resume()
|
||||||
|
markAsRead({convoId})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
convo.background()
|
convo.background()
|
||||||
|
markAsRead({convoId})
|
||||||
}
|
}
|
||||||
}, [convo]),
|
}, [convo, convoId, markAsRead]),
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -56,6 +60,8 @@ export function ChatProvider({
|
||||||
} else {
|
} else {
|
||||||
convo.background()
|
convo.background()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markAsRead({convoId})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +70,7 @@ export function ChatProvider({
|
||||||
return () => {
|
return () => {
|
||||||
sub.remove()
|
sub.remove()
|
||||||
}
|
}
|
||||||
}, [convo, isScreenFocused])
|
}, [convoId, convo, isScreenFocused, markAsRead])
|
||||||
|
|
||||||
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
|
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {BskyAgent} from '@atproto-labs/api'
|
import {BskyAgent} from '@atproto-labs/api'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-converations'
|
||||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
import {useHeaders} from './temp-headers'
|
import {useHeaders} from './temp-headers'
|
||||||
|
|
||||||
|
@ -23,3 +24,36 @@ export function useConvoQuery(convoId: string) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMarkAsReadMutation() {
|
||||||
|
const headers = useHeaders()
|
||||||
|
const {serviceUrl} = useDmServiceUrlStorage()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
convoId,
|
||||||
|
messageId,
|
||||||
|
}: {
|
||||||
|
convoId: string
|
||||||
|
messageId?: string
|
||||||
|
}) => {
|
||||||
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
|
await agent.api.chat.bsky.convo.updateRead(
|
||||||
|
{
|
||||||
|
convoId,
|
||||||
|
messageId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encoding: 'application/json',
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ListConvosQueryKey,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import {BskyAgent} from '@atproto-labs/api'
|
import {useCallback, useMemo} from 'react'
|
||||||
import {useInfiniteQuery} from '@tanstack/react-query'
|
import {
|
||||||
|
BskyAgent,
|
||||||
|
ChatBskyConvoDefs,
|
||||||
|
ChatBskyConvoListConvos,
|
||||||
|
} from '@atproto-labs/api'
|
||||||
|
import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
import {useHeaders} from './temp-headers'
|
import {useHeaders} from './temp-headers'
|
||||||
|
|
||||||
|
@ -27,3 +33,100 @@ export function useListConvos({refetchInterval}: {refetchInterval: number}) {
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUnreadMessageCount() {
|
||||||
|
const {currentConvoId} = useCurrentConvoId()
|
||||||
|
const convos = useListConvos({
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count =
|
||||||
|
convos.data?.pages
|
||||||
|
.flatMap(page => page.convos)
|
||||||
|
.filter(convo => convo.id !== currentConvoId)
|
||||||
|
.reduce((acc, convo) => {
|
||||||
|
return acc + (!convo.muted && convo.unreadCount > 0 ? 1 : 0)
|
||||||
|
}, 0) ?? 0
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
numUnread: count > 0 ? (count > 30 ? '30+' : String(count)) : undefined,
|
||||||
|
}
|
||||||
|
}, [count])
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConvoListQueryData = {
|
||||||
|
pageParams: Array<string | undefined>
|
||||||
|
pages: Array<ChatBskyConvoListConvos.OutputSchema>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOnDeleteMessage() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(chatId: string, messageId: string) => {
|
||||||
|
queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => {
|
||||||
|
return optimisticUpdate(chatId, old, convo =>
|
||||||
|
messageId === convo.lastMessage?.id
|
||||||
|
? {
|
||||||
|
...convo,
|
||||||
|
lastMessage: {
|
||||||
|
$type: 'chat.bsky.convo.defs#deletedMessageView',
|
||||||
|
id: messageId,
|
||||||
|
rev: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: convo,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[queryClient],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOnNewMessage() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(chatId: string, message: ChatBskyConvoDefs.MessageView) => {
|
||||||
|
queryClient.setQueryData(RQKEY, (old: ConvoListQueryData) => {
|
||||||
|
return optimisticUpdate(chatId, old, convo => ({
|
||||||
|
...convo,
|
||||||
|
lastMessage: message,
|
||||||
|
unreadCount: convo.unreadCount + 1,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({queryKey: RQKEY})
|
||||||
|
},
|
||||||
|
[queryClient],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOnCreateConvo() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
queryClient.invalidateQueries({queryKey: RQKEY})
|
||||||
|
}, [queryClient])
|
||||||
|
}
|
||||||
|
|
||||||
|
function optimisticUpdate(
|
||||||
|
chatId: string,
|
||||||
|
old: ConvoListQueryData,
|
||||||
|
updateFn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
|
||||||
|
) {
|
||||||
|
if (!old) {
|
||||||
|
return old
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
convos: page.convos.map(convo =>
|
||||||
|
chatId === convo.id ? updateFn(convo) : convo,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {getTabState, TabState} from '#/lib/routes/helpers'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {emitSoftReset} from '#/state/events'
|
import {emitSoftReset} from '#/state/events'
|
||||||
|
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
|
||||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
@ -68,6 +69,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
||||||
isAtMessages,
|
isAtMessages,
|
||||||
} = useNavigationTabState()
|
} = useNavigationTabState()
|
||||||
const numUnreadNotifications = useUnreadNotifications()
|
const numUnreadNotifications = useUnreadNotifications()
|
||||||
|
const numUnreadMessages = useUnreadMessageCount()
|
||||||
const {footerMinimalShellTransform} = useMinimalShellMode()
|
const {footerMinimalShellTransform} = useMinimalShellMode()
|
||||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
|
@ -257,9 +259,15 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onPress={onPressMessages}
|
onPress={onPressMessages}
|
||||||
|
notificationCount={numUnreadMessages.numUnread}
|
||||||
|
accessible={true}
|
||||||
accessibilityRole="tab"
|
accessibilityRole="tab"
|
||||||
accessibilityLabel={_(msg`Messages`)}
|
accessibilityLabel={_(msg`Messages`)}
|
||||||
accessibilityHint=""
|
accessibilityHint={
|
||||||
|
numUnreadMessages.count > 0
|
||||||
|
? `${numUnreadMessages.numUnread} unread`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Btn
|
<Btn
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||||
import {emitSoftReset} from '#/state/events'
|
import {emitSoftReset} from '#/state/events'
|
||||||
import {useFetchHandle} from '#/state/queries/handle'
|
import {useFetchHandle} from '#/state/queries/handle'
|
||||||
|
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
|
||||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
@ -274,7 +275,8 @@ export function DesktopLeftNav() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||||
const numUnread = useUnreadNotifications()
|
const numUnreadNotifications = useUnreadNotifications()
|
||||||
|
const numUnreadMessages = useUnreadMessageCount()
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
|
|
||||||
if (!hasSession && !isDesktop) {
|
if (!hasSession && !isDesktop) {
|
||||||
|
@ -333,7 +335,7 @@ export function DesktopLeftNav() {
|
||||||
/>
|
/>
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/notifications"
|
href="/notifications"
|
||||||
count={numUnread}
|
count={numUnreadNotifications}
|
||||||
icon={
|
icon={
|
||||||
<BellIcon
|
<BellIcon
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
@ -353,6 +355,7 @@ export function DesktopLeftNav() {
|
||||||
{gate('dms') && (
|
{gate('dms') && (
|
||||||
<NavItem
|
<NavItem
|
||||||
href="/messages"
|
href="/messages"
|
||||||
|
count={numUnreadMessages.numUnread}
|
||||||
icon={<Envelope style={pal.text} width={isDesktop ? 26 : 30} />}
|
icon={<Envelope style={pal.text} width={isDesktop ? 26 : 30} />}
|
||||||
iconFilled={
|
iconFilled={
|
||||||
<EnvelopeFilled style={pal.text} width={isDesktop ? 26 : 30} />
|
<EnvelopeFilled style={pal.text} width={isDesktop ? 26 : 30} />
|
||||||
|
|
Loading…
Reference in New Issue