forked from Ivasoft/mattermost-mobile
[Gekidou] refactor clean notifications (#6566)
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user