[🐴] 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 {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
|
||||
import {MessagesEventBusProvider} from '#/state/messages/events'
|
||||
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||
|
||||
|
@ -37,15 +38,18 @@ export function ChatProvider({
|
|||
}),
|
||||
)
|
||||
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
|
||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
convo.resume()
|
||||
markAsRead({convoId})
|
||||
|
||||
return () => {
|
||||
convo.background()
|
||||
markAsRead({convoId})
|
||||
}
|
||||
}, [convo]),
|
||||
}, [convo, convoId, markAsRead]),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -56,6 +60,8 @@ export function ChatProvider({
|
|||
} else {
|
||||
convo.background()
|
||||
}
|
||||
|
||||
markAsRead({convoId})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +70,7 @@ export function ChatProvider({
|
|||
return () => {
|
||||
sub.remove()
|
||||
}
|
||||
}, [convo, isScreenFocused])
|
||||
}, [convoId, convo, isScreenFocused, markAsRead])
|
||||
|
||||
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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 {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 {useInfiniteQuery} from '@tanstack/react-query'
|
||||
import {useCallback, useMemo} from 'react'
|
||||
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 {useHeaders} from './temp-headers'
|
||||
|
||||
|
@ -27,3 +33,100 @@ export function useListConvos({refetchInterval}: {refetchInterval: number}) {
|
|||
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 {s} from '#/lib/styles'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
|
@ -68,6 +69,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||
isAtMessages,
|
||||
} = useNavigationTabState()
|
||||
const numUnreadNotifications = useUnreadNotifications()
|
||||
const numUnreadMessages = useUnreadMessageCount()
|
||||
const {footerMinimalShellTransform} = useMinimalShellMode()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||
|
@ -257,9 +259,15 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||
)
|
||||
}
|
||||
onPress={onPressMessages}
|
||||
notificationCount={numUnreadMessages.numUnread}
|
||||
accessible={true}
|
||||
accessibilityRole="tab"
|
||||
accessibilityLabel={_(msg`Messages`)}
|
||||
accessibilityHint=""
|
||||
accessibilityHint={
|
||||
numUnreadMessages.count > 0
|
||||
? `${numUnreadMessages.numUnread} unread`
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Btn
|
||||
|
|
|
@ -16,6 +16,7 @@ import {useGate} from '#/lib/statsig/statsig'
|
|||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useFetchHandle} from '#/state/queries/handle'
|
||||
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
|
@ -274,7 +275,8 @@ export function DesktopLeftNav() {
|
|||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||
const numUnread = useUnreadNotifications()
|
||||
const numUnreadNotifications = useUnreadNotifications()
|
||||
const numUnreadMessages = useUnreadMessageCount()
|
||||
const gate = useGate()
|
||||
|
||||
if (!hasSession && !isDesktop) {
|
||||
|
@ -333,7 +335,7 @@ export function DesktopLeftNav() {
|
|||
/>
|
||||
<NavItem
|
||||
href="/notifications"
|
||||
count={numUnread}
|
||||
count={numUnreadNotifications}
|
||||
icon={
|
||||
<BellIcon
|
||||
strokeWidth={2}
|
||||
|
@ -353,6 +355,7 @@ export function DesktopLeftNav() {
|
|||
{gate('dms') && (
|
||||
<NavItem
|
||||
href="/messages"
|
||||
count={numUnreadMessages.numUnread}
|
||||
icon={<Envelope style={pal.text} width={isDesktop ? 26 : 30} />}
|
||||
iconFilled={
|
||||
<EnvelopeFilled style={pal.text} width={isDesktop ? 26 : 30} />
|
||||
|
|
Loading…
Reference in New Issue