[Gekidou] refactor clean notifications (#6566)

This commit is contained in:
Elias Nahum
2022-08-19 16:29:15 -04:00
committed by GitHub
parent 25ae8fdb88
commit c2f5092678
42 changed files with 1217 additions and 933 deletions

View File

@@ -17,7 +17,6 @@ import {
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
@@ -176,8 +175,7 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
m.viewedAt = member.lastViewedAt;
m.lastViewedAt = Date.now();
});
const isCRTEnabled = await getIsCRTEnabled(database);
PushNotifications.cancelChannelNotifications(channelId, undefined, isCRTEnabled);
PushNotifications.removeChannelNotifications(serverUrl, channelId);
if (!prepareRecordsOnly) {
await operator.batchRecords([member]);
}

View File

@@ -111,6 +111,8 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI
const isCRTEnabled = await getIsCRTEnabled(database);
const isThreadNotification = isCRTEnabled && Boolean(rootId);
await operator.batchRecords(models);
let switchedToScreen = false;
let switchedToChannel = false;
if (myChannel && myTeam) {
@@ -129,15 +131,15 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI
// Make switch again to get the missing data and make sure the team is the correct one
switchedToScreen = true;
if (isThreadNotification) {
fetchAndSwitchToThread(serverUrl, rootId, true);
await fetchAndSwitchToThread(serverUrl, rootId, true);
} else {
switchedToChannel = true;
switchToChannelById(serverUrl, selectedChannelId, selectedTeamId);
await switchToChannelById(serverUrl, selectedChannelId, selectedTeamId);
}
} else if (selectedTeamId !== teamId || selectedChannelId !== channelId) {
// If in the end the selected team or channel is different than the one from the notification
// we switch again
setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
}
}
@@ -147,13 +149,19 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI
emitNotificationError('Channel');
}
await operator.batchRecords(models);
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
const {config, license} = await getCommonSystemValues(operator.database);
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, switchedToChannel ? selectedChannelId : undefined);
// Waiting for the screen to display fixes a race condition when fetching and storing data
if (switchedToChannel) {
await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL);
} else if (switchedToScreen && isThreadNotification) {
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
}
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
return {userId: currentUserId};
};

View File

@@ -5,6 +5,7 @@ import {Platform} from 'react-native';
import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
import {fetchDirectChannelsInfo, fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {fetchMyTeam} from '@actions/remote/team';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
@@ -73,6 +74,18 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
}
}
if (Platform.OS === 'android') {
// on Android we only fetched the post data on the native side
// when the RN context is not running, thus we need to fetch the
// data here as well
const isCRTEnabled = await getIsCRTEnabled(database);
const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
if (isThreadNotification) {
fetchPostThread(serverUrl, notification.payload!.root_id!);
} else {
fetchPostsForChannel(serverUrl, channelId);
}
}
return {};
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);

View File

@@ -54,7 +54,7 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string,
}
}
switchToThread(serverUrl, rootId, isFromNotification);
await switchToThread(serverUrl, rootId, isFromNotification);
return {};
};
@@ -186,7 +186,11 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
const isCRTEnabled = await getIsCRTEnabled(database);
const post = await getPostById(database, threadId);
if (post) {
PushNotifications.cancelChannelNotifications(post.channelId, threadId, isCRTEnabled);
if (isCRTEnabled) {
PushNotifications.removeThreadNotifications(serverUrl, threadId);
} else {
PushNotifications.removeChannelNotifications(serverUrl, post.channelId);
}
}
return {data};

View File

@@ -35,12 +35,13 @@ export const subscribeServerUnreadAndMentions = (serverUrl: string, observer: Un
let subscription: Subscription|undefined;
if (server?.database) {
subscription = server.database.get<MyChannelModel>(MY_CHANNEL).
subscription = server.database.
get<MyChannelModel>(MY_CHANNEL).
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['is_unread', 'mentions_count']).
pipe(
combineLatestWith(observeAllMyChannelNotifyProps(server.database)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, false)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, true)),
map$(([[myChannels, settings], {unreads, mentions}]) => ({myChannels, settings, threadUnreads: unreads, threadMentionCount: mentions})),
).
subscribe(observer);
@@ -59,7 +60,7 @@ export const subscribeMentionsByServer = (serverUrl: string, observer: ServerUnr
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['mentions_count']).
pipe(
combineLatestWith(observeThreadMentionCount(server.database, undefined, false)),
combineLatestWith(observeThreadMentionCount(server.database, undefined, true)),
map$(([myChannels, threadMentionCount]) => ({myChannels, threadMentionCount})),
).
subscribe(observer.bind(undefined, serverUrl));
@@ -78,7 +79,7 @@ export const subscribeUnreadAndMentionsByServer = (serverUrl: string, observer:
observeWithColumns(['mentions_count', 'is_unread']).
pipe(
combineLatestWith(observeAllMyChannelNotifyProps(server.database)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, false)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, true)),
map$(([[myChannels, settings], {unreads, mentions}]) => ({myChannels, settings, threadUnreads: unreads, threadMentionCount: mentions})),
).
subscribe(observer.bind(undefined, serverUrl));

View File

@@ -20,7 +20,6 @@ import {backgroundNotification, openNotification} from '@actions/remote/notifica
import {markThreadAsRead} from '@actions/remote/thread';
import {Device, Events, Navigation, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getTotalMentionsForServer} from '@database/subscription/unreads';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import NativeNotifications from '@notifications';
import {queryServerName} from '@queries/app/servers';
@@ -52,60 +51,6 @@ class PushNotifications {
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
}
cancelAllLocalNotifications = () => {
Notifications.cancelAllLocalNotifications();
};
cancelChannelNotifications = async (channelId: string, rootId?: string, isCRTEnabled?: boolean) => {
const notifications = await NativeNotifications.getDeliveredNotifications();
this.cancelNotificationsForChannel(notifications, channelId, rootId, isCRTEnabled);
};
cancelChannelsNotifications = async (channelIds: string[]) => {
const notifications = await NativeNotifications.getDeliveredNotifications();
for (const channelId of channelIds) {
this.cancelNotificationsForChannel(notifications, channelId);
}
};
cancelNotificationsForChannel = (notifications: NotificationWithChannel[], channelId: string, rootId?: string, isCRTEnabled?: boolean) => {
if (Platform.OS === 'android') {
NativeNotifications.removeDeliveredNotifications(channelId, rootId, isCRTEnabled);
} else {
const ids: string[] = [];
const clearThreads = Boolean(rootId);
for (const notification of notifications) {
if (notification.channel_id === channelId) {
let doesNotificationMatch = true;
if (clearThreads) {
doesNotificationMatch = notification.thread === rootId;
} else if (isCRTEnabled) {
// Do not match when CRT is enabled BUT post is not a root post
doesNotificationMatch = !notification.root_id;
}
if (doesNotificationMatch) {
ids.push(notification.identifier);
}
}
}
if (ids.length) {
NativeNotifications.removeDeliveredNotifications(ids);
}
let badgeCount = notifications.length - ids.length;
const serversUrl = Object.keys(DatabaseManager.serverDatabases);
const mentionPromises = serversUrl.map((url) => getTotalMentionsForServer(url));
Promise.all(mentionPromises).then((result) => {
badgeCount += result.reduce((acc, count) => (acc + count), 0);
badgeCount = badgeCount <= 0 ? 0 : badgeCount;
Notifications.ios.setBadgeCount(badgeCount);
});
}
};
createReplyCategory = () => {
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
@@ -288,6 +233,18 @@ class PushNotifications {
return null;
};
removeChannelNotifications = async (serverUrl: string, channelId: string) => {
NativeNotifications.removeChannelNotifications(serverUrl, channelId);
};
removeServerNotifications = (serverUrl: string) => {
NativeNotifications.removeServerNotifications(serverUrl);
};
removeThreadNotifications = async (serverUrl: string, threadId: string) => {
NativeNotifications.removeThreadNotifications(serverUrl, threadId);
};
requestNotificationReplyPermissions = () => {
if (Platform.OS === 'ios') {
const replyCategory = this.createReplyCategory();

View File

@@ -5,7 +5,6 @@ import CookieManager, {Cookie} from '@react-native-cookies/cookies';
import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native';
import semver from 'semver';
import {selectAllMyChannelIds} from '@actions/local/channel';
import LocalConfig from '@assets/config.json';
import {Events, Sso, Launch} from '@constants';
import DatabaseManager from '@database/manager';
@@ -92,8 +91,7 @@ class GlobalEventHandler {
onLogout = async ({serverUrl, removeServer}: LogoutCallbackArg) => {
await removeServerCredentials(serverUrl);
const channelIds = await selectAllMyChannelIds(serverUrl);
PushNotifications.cancelChannelsNotifications(channelIds);
PushNotifications.removeServerNotifications(serverUrl);
NetworkManager.invalidateClient(serverUrl);
WebsocketManager.invalidateClient(serverUrl);

View File

@@ -1,5 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// @ts-expect-error platform specific
export {default} from './notifications';
import {NativeModules} from 'react-native';
const {Notifications} = NativeModules;
const nativeNotification: NativeNotification = {
getDeliveredNotifications: Notifications.getDeliveredNotifications,
removeChannelNotifications: Notifications.removeChannelNotifications,
removeThreadNotifications: Notifications.removeThreadNotifications,
removeServerNotifications: Notifications.removeServerNotifications,
};
export default nativeNotification;

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {NativeModules, PermissionsAndroid} from 'react-native';
const {NotificationPreferences} = NativeModules;
const defaultPreferences: NativeNotificationPreferences = {
sounds: [],
shouldBlink: false,
shouldVibrate: true,
};
const nativeNotification: NativeNotification = {
getDeliveredNotifications: NotificationPreferences.getDeliveredNotifications,
getPreferences: async () => {
try {
const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE);
let granted;
if (!hasPermission) {
granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
);
}
if (hasPermission || granted === PermissionsAndroid.RESULTS.GRANTED) {
return await NotificationPreferences.getPreferences();
}
return defaultPreferences;
} catch (error) {
return defaultPreferences;
}
},
play: NotificationPreferences.previewSound,
removeDeliveredNotifications: NotificationPreferences.removeDeliveredNotifications,
setNotificationSound: NotificationPreferences.setNotificationSound,
setShouldBlink: NotificationPreferences.setShouldBlink,
setShouldVibrate: NotificationPreferences.setShouldVibrate,
};
export default nativeNotification;

View File

@@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Notifications} from 'react-native-notifications';
import {emptyFunction} from '@utils/general';
const nativeNotification: NativeNotification = {
getDeliveredNotifications: async () => Notifications.ios.getDeliveredNotifications(),
getPreferences: async () => null,
play: (soundUri: string) => emptyFunction(soundUri),
removeDeliveredNotifications: async (ids: string[]) => Notifications.ios.removeDeliveredNotifications(ids),
setNotificationSound: () => emptyFunction(),
setShouldBlink: (shouldBlink: boolean) => emptyFunction(shouldBlink),
setShouldVibrate: (shouldVibrate: boolean) => emptyFunction(shouldVibrate),
};
export default nativeNotification;

View File

@@ -2,13 +2,15 @@
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import {Platform, StyleSheet, View} from 'react-native';
import {Notifications} from 'react-native-notifications';
import Badge from '@components/badge';
import CompassIcon from '@components/compass_icon';
import {BOTTOM_TAB_ICON_SIZE} from '@constants/view';
import {subscribeAllServers} from '@database/subscription/servers';
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
import NativeNotification from '@notifications';
import {changeOpacity} from '@utils/theme';
import type ServersModel from '@typings/database/models/app/servers';
@@ -48,6 +50,16 @@ const Home = ({isFocused, theme}: Props) => {
mentions += value.mentions;
});
setTotal({mentions, unread});
if (Platform.OS === 'ios') {
NativeNotification.getDeliveredNotifications().then((delivered) => {
if (mentions === 0 && delivered.length > 0) {
return;
}
Notifications.ios.setBadgeCount(mentions);
});
}
};
const unreadsSubscription = (serverUrl: string, {myChannels, settings, threadMentionCount}: UnreadObserverArgs) => {