Skip to content

Commit

Permalink
[🐴] Unread messages badge (#3901)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
mozzius and estrattonbailey authored May 8, 2024
1 parent 0c41b31 commit 4fe5a86
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 8 deletions.
10 changes: 8 additions & 2 deletions src/state/messages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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(() => {
Expand All @@ -56,6 +60,8 @@ export function ChatProvider({
} else {
convo.background()
}

markAsRead({convoId})
}
}

Expand All @@ -64,7 +70,7 @@ export function ChatProvider({
return () => {
sub.remove()
}
}, [convo, isScreenFocused])
}, [convoId, convo, isScreenFocused, markAsRead])

return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
}
Expand Down
36 changes: 35 additions & 1 deletion src/state/queries/messages/conversation.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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,
})
},
})
}
107 changes: 105 additions & 2 deletions src/state/queries/messages/list-converations.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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,
),
})),
}
}
10 changes: 9 additions & 1 deletion src/view/shell/bottom-bar/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/view/shell/desktop/LeftNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -333,7 +335,7 @@ export function DesktopLeftNav() {
/>
<NavItem
href="/notifications"
count={numUnread}
count={numUnreadNotifications}
icon={
<BellIcon
strokeWidth={2}
Expand All @@ -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} />
Expand Down

0 comments on commit 4fe5a86

Please sign in to comment.