From 4992a96891c2cd71d8ae6c5d55ee8b455d9e3ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 10 May 2024 09:21:32 +0200 Subject: [PATCH] Avoid race condition setting the badge number when the app is in the background (#7925) --- app/screens/home/tab_bar/home.tsx | 167 ++++++++++-------- .../NotificationService.swift | 2 +- 2 files changed, 96 insertions(+), 73 deletions(-) diff --git a/app/screens/home/tab_bar/home.tsx b/app/screens/home/tab_bar/home.tsx index cd636fbdb..ef94cee09 100644 --- a/app/screens/home/tab_bar/home.tsx +++ b/app/screens/home/tab_bar/home.tsx @@ -2,9 +2,10 @@ // See LICENSE.txt for license information. import React, {useEffect, useState} from 'react'; -import {Platform, StyleSheet, View} from 'react-native'; +import {DeviceEventEmitter, Platform, StyleSheet, View} from 'react-native'; import {Notifications} from 'react-native-notifications'; +import useDidUpdate from '@app/hooks/did_update'; import Badge from '@components/badge'; import CompassIcon from '@components/compass_icon'; import {BOTTOM_TAB_ICON_SIZE} from '@constants/view'; @@ -23,6 +24,8 @@ type Props = { theme: Theme; } +const HOME_TOTAL_MENTIONS_EVENT = 'home_total_mentions_event'; + const subscriptions: Map = new Map(); const style = StyleSheet.create({ @@ -41,81 +44,101 @@ const style = StyleSheet.create({ }, }); +const getTotalMentionsAndUnread = () => { + let unread = false; + let mentions = 0; + subscriptions.forEach((value) => { + unread = unread || value.unread; + mentions += value.mentions; + }); + return {unread, mentions}; +}; + +const updateBadge = () => { + if (Platform.OS === 'ios') { + const {mentions} = getTotalMentionsAndUnread(); + NativeNotification.getDeliveredNotifications().then((delivered) => { + if (mentions === 0 && delivered.length > 0) { + logDebug('Not updating badge count, since we have no mentions in the database, and the number of notifications in the notification center is', delivered.length); + return; + } + + logDebug('Setting the badge count based on database values to', mentions); + Notifications.ios.setBadgeCount(mentions); + }); + } +}; + +const unreadsSubscription = (serverUrl: string, {myChannels, settings, threadMentionCount}: UnreadObserverArgs) => { + const unreads = subscriptions.get(serverUrl); + if (unreads) { + let mentions = 0; + let unread = false; + for (const myChannel of myChannels) { + const isMuted = settings?.[myChannel.id]?.mark_unread === 'mention'; + mentions += isMuted ? 0 : myChannel.mentionsCount; + unread = unread || (myChannel.isUnread && !isMuted); + } + + unreads.mentions = mentions + threadMentionCount; + unreads.unread = unread; + subscriptions.set(serverUrl, unreads); + DeviceEventEmitter.emit(HOME_TOTAL_MENTIONS_EVENT); + } +}; + +const serversObserver = async (servers: ServersModel[]) => { + // unsubscribe mentions from servers that were removed + const allUrls = new Set(servers.map((s) => s.url)); + const subscriptionsToRemove = [...subscriptions].filter(([key]) => !allUrls.has(key)); + let hasRemovedServers = false; + for (const [key, map] of subscriptionsToRemove) { + map.subscription?.unsubscribe(); + subscriptions.delete(key); + hasRemovedServers = true; + } + + for (const server of servers) { + const {lastActiveAt, url} = server; + if (lastActiveAt && !subscriptions.has(url)) { + const unreads: UnreadSubscription = { + mentions: 0, + unread: false, + }; + subscriptions.set(url, unreads); + unreads.subscription = subscribeUnreadAndMentionsByServer(url, unreadsSubscription); + } else if (!lastActiveAt && subscriptions.has(url)) { + subscriptions.get(url)?.subscription?.unsubscribe(); + subscriptions.delete(url); + hasRemovedServers = true; + } + } + if (hasRemovedServers) { + DeviceEventEmitter.emit(HOME_TOTAL_MENTIONS_EVENT); + } +}; + const Home = ({isFocused, theme}: Props) => { const [total, setTotal] = useState({mentions: 0, unread: false}); const appState = useAppState(); - const updateTotal = () => { - let unread = false; - let mentions = 0; - subscriptions.forEach((value) => { - unread = unread || value.unread; - mentions += value.mentions; - }); - setTotal({mentions, unread}); - - if (Platform.OS === 'ios') { - NativeNotification.getDeliveredNotifications().then((delivered) => { - if (mentions === 0 && delivered.length > 0) { - logDebug('Not updating badge count, since we have no mentions in the database, and the number of notifications in the notification center is', delivered.length); - return; - } - - logDebug('Setting the badge count based on database values to', mentions); - Notifications.ios.setBadgeCount(mentions); - }); - } - }; - - const unreadsSubscription = (serverUrl: string, {myChannels, settings, threadMentionCount}: UnreadObserverArgs) => { - const unreads = subscriptions.get(serverUrl); - if (unreads) { - let mentions = 0; - let unread = false; - for (const myChannel of myChannels) { - const isMuted = settings?.[myChannel.id]?.mark_unread === 'mention'; - mentions += isMuted ? 0 : myChannel.mentionsCount; - unread = unread || (myChannel.isUnread && !isMuted); - } - - unreads.mentions = mentions + threadMentionCount; - unreads.unread = unread; - subscriptions.set(serverUrl, unreads); - updateTotal(); - } - }; - - const serversObserver = async (servers: ServersModel[]) => { - // unsubscribe mentions from servers that were removed - const allUrls = new Set(servers.map((s) => s.url)); - const subscriptionsToRemove = [...subscriptions].filter(([key]) => !allUrls.has(key)); - for (const [key, map] of subscriptionsToRemove) { - map.subscription?.unsubscribe(); - subscriptions.delete(key); - updateTotal(); - } - - for (const server of servers) { - const {lastActiveAt, url} = server; - if (lastActiveAt && !subscriptions.has(url)) { - const unreads: UnreadSubscription = { - mentions: 0, - unread: false, - }; - subscriptions.set(url, unreads); - unreads.subscription = subscribeUnreadAndMentionsByServer(url, unreadsSubscription); - } else if (!lastActiveAt && subscriptions.has(url)) { - subscriptions.get(url)?.subscription?.unsubscribe(); - subscriptions.delete(url); - updateTotal(); - } - } - }; - useEffect(() => { + const totalMentionsEventListener = () => { + setTotal((prev) => { + const newTotal = getTotalMentionsAndUnread(); + if (prev.mentions === newTotal.mentions && prev.unread === newTotal.unread) { + return prev; + } + return newTotal; + }); + }; + const totalSubscription = DeviceEventEmitter.addListener(HOME_TOTAL_MENTIONS_EVENT, totalMentionsEventListener); + const subscription = subscribeAllServers(serversObserver); return () => { + totalSubscription.remove(); + subscription?.unsubscribe(); subscriptions.forEach((unreads) => { unreads.subscription?.unsubscribe(); @@ -124,11 +147,11 @@ const Home = ({isFocused, theme}: Props) => { }; }, []); - useEffect(() => { - if (appState === 'background') { - updateTotal(); + useDidUpdate(() => { + if (appState !== 'active') { + updateBadge(); } - }, [appState]); + }, [total, appState !== 'active']); let unreadStyle; if (total.mentions) { diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 22942dd40..adb78e616 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -30,7 +30,7 @@ class NotificationService: UNNotificationServiceExtension { self?.sendMessageIntent() }) } else { - bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber + bestAttemptContent.badge = nil os_log(OSLogType.default, "Mattermost Notifications: app in use, no data processed. Will call sendMessageIntent") self?.sendMessageIntent() }