From da575c7cb1546e85c6285b31d1ef9262ed3a0b45 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Mon, 12 Jul 2021 22:13:54 +0200 Subject: [PATCH] 1.45 Release testing bug fixes (#5529) (#5536) * MM-36908 display unicode emoji in text field after selecting from autocomple * MM-36935 Fix android crash when ammending search in Sidebar Jump to * MM-36929 & MM-36928 fix notification badge resetting on new notification * MM-36920 Fix android push notification issues * MM-36922 Edit profile image * Fix crash when opening Android test notification (cherry picked from commit 78156ee75b80f86aa6986442c4c62d43df1eb800) Co-authored-by: Elias Nahum --- .../rnbeta/CustomPushNotification.java | 99 +++++++++++++++---- .../rnbeta/NotificationDismissService.java | 9 +- app/actions/views/edit_profile.js | 1 + app/components/attachment_button/index.js | 80 +++++++-------- .../emoji_suggestion/emoji_suggestion.js | 7 +- .../profile_picture/profile_picture.tsx | 6 +- .../filtered_list/filtered_list.js | 2 +- app/init/push_notifications.test.js | 9 +- app/init/push_notifications.ts | 23 ++--- .../notification_settings_mobile/index.js | 3 +- .../notification_settings_mobile.android.js | 10 +- 11 files changed, 161 insertions(+), 88 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java index 8164e4f54..9045cd886 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -2,6 +2,7 @@ package com.mattermost.rnbeta; import android.app.PendingIntent; import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; @@ -12,6 +13,7 @@ import androidx.core.app.NotificationManagerCompat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -23,12 +25,15 @@ import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_ import com.mattermost.react_native_interface.ResolvePromise; +import org.json.JSONArray; +import org.json.JSONObject; + public class CustomPushNotification extends PushNotification { + private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS"; private static final String PUSH_TYPE_MESSAGE = "message"; private static final String PUSH_TYPE_CLEAR = "clear"; private static final String PUSH_TYPE_SESSION = "session"; - - private final static Map> notificationsInChannel = new HashMap<>(); + private static final String NOTIFICATIONS_IN_CHANNEL = "notificationsInChannel"; public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) { super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper); @@ -37,12 +42,14 @@ public class CustomPushNotification extends PushNotification { public static void cancelNotification(Context context, String channelId, Integer notificationId) { if (!android.text.TextUtils.isEmpty(channelId)) { + Map> notificationsInChannel = loadNotificationsMap(context); List notifications = notificationsInChannel.get(channelId); if (notifications == null) { return; } notifications.remove(notificationId); + saveNotificationsMap(context, notificationsInChannel); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.cancel(notificationId); } @@ -50,12 +57,14 @@ public class CustomPushNotification extends PushNotification { public static void clearChannelNotifications(Context context, String channelId) { if (!android.text.TextUtils.isEmpty(channelId)) { + Map> notificationsInChannel = loadNotificationsMap(context); List notifications = notificationsInChannel.get(channelId); if (notifications == null) { return; } notificationsInChannel.remove(channelId); + saveNotificationsMap(context, notificationsInChannel); if (context != null) { for (final Integer notificationId : notifications) { @@ -67,8 +76,10 @@ public class CustomPushNotification extends PushNotification { } public static void clearAllNotifications(Context context) { - notificationsInChannel.clear(); if (context != null) { + Map> notificationsInChannel = loadNotificationsMap(context); + notificationsInChannel.clear(); + saveNotificationsMap(context, notificationsInChannel); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.cancelAll(); } @@ -106,22 +117,29 @@ public class CustomPushNotification extends PushNotification { }); } - if (channelId != null) { - synchronized (notificationsInChannel) { - List list = notificationsInChannel.get(channelId); - if (list == null) { - list = Collections.synchronizedList(new ArrayList(0)); - } - - list.add(0, notificationId); - notificationsInChannel.put(channelId, list); - } - } - switch (type) { case PUSH_TYPE_MESSAGE: case PUSH_TYPE_SESSION: - super.postNotification(notificationId); + if (!mAppLifecycleFacade.isAppVisible()) { + if (type.equals(PUSH_TYPE_MESSAGE)) { + if (channelId != null) { + Map> notificationsInChannel = loadNotificationsMap(mContext); + synchronized (notificationsInChannel) { + List list = notificationsInChannel.get(channelId); + if (list == null) { + list = Collections.synchronizedList(new ArrayList(0)); + } + + list.add(0, notificationId); + notificationsInChannel.put(channelId, list); + saveNotificationsMap(mContext, notificationsInChannel); + } + } + } + super.postNotification(notificationId); + } else { + notifyReceivedToJS(); + } break; case PUSH_TYPE_CLEAR: clearChannelNotifications(mContext, channelId); @@ -139,10 +157,18 @@ public class CustomPushNotification extends PushNotification { Bundle data = mNotificationProps.asBundle(); final String channelId = data.getString("channel_id"); - final Integer notificationId = data.getString("post_id").hashCode(); + final String postId = data.getString("post_id"); + Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID; + + if (postId != null) { + notificationId = postId.hashCode(); + } + if (channelId != null) { + Map> notificationsInChannel = loadNotificationsMap(mContext); List notifications = notificationsInChannel.get(channelId); notifications.remove(notificationId); + saveNotificationsMap(mContext, notificationsInChannel); clearChannelNotifications(mContext, channelId); } } @@ -160,4 +186,43 @@ public class CustomPushNotification extends PushNotification { private void notifyReceivedToJS() { mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); } + + private static void saveNotificationsMap(Context context, Map> inputMap) { + SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE); + if (pSharedPref != null && context != null) { + JSONObject json = new JSONObject(inputMap); + String jsonString = json.toString(); + SharedPreferences.Editor editor = pSharedPref.edit(); + editor.remove(NOTIFICATIONS_IN_CHANNEL).commit(); + editor.putString(NOTIFICATIONS_IN_CHANNEL, jsonString); + editor.commit(); + } + } + + private static Map> loadNotificationsMap(Context context) { + Map> outputMap = new HashMap<>(); + if (context != null) { + SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE); + try { + if (pSharedPref != null) { + String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString()); + JSONObject json = new JSONObject(jsonString); + Iterator keysItr = json.keys(); + while (keysItr.hasNext()) { + String key = keysItr.next(); + JSONArray array = json.getJSONArray(key); + List values = new ArrayList<>(); + for (int i = 0; i < array.length(); ++i) { + values.add(array.getInt(i)); + } + outputMap.put(key, values); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + return outputMap; + } } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java index 17a93ba80..d5e56eabb 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java @@ -18,7 +18,14 @@ public class NotificationDismissService extends IntentService { final Context context = getApplicationContext(); final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); final String channelId = bundle.getString("channel_id"); - final Integer notificationId = bundle.getString("post_id").hashCode(); + final String postId = bundle.getString("post_id"); + int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID; + if (postId != null) { + notificationId = postId.hashCode(); + } else if (channelId != null) { + notificationId = channelId.hashCode(); + } + CustomPushNotification.cancelNotification(context, channelId, notificationId); Log.i("ReactNative", "Dismiss notification"); } diff --git a/app/actions/views/edit_profile.js b/app/actions/views/edit_profile.js index c64267027..2b50a994b 100644 --- a/app/actions/views/edit_profile.js +++ b/app/actions/views/edit_profile.js @@ -28,6 +28,7 @@ export function setProfileImageUri(imageUri = '') { export function removeProfileImage(user) { return async (dispatch) => { const result = await dispatch(setDefaultProfileImage(user)); + dispatch(setProfileImageUri()); return result; }; } diff --git a/app/components/attachment_button/index.js b/app/components/attachment_button/index.js index 5b7e0d69b..0066b1792 100644 --- a/app/components/attachment_button/index.js +++ b/app/components/attachment_button/index.js @@ -15,7 +15,7 @@ import DeviceInfo from 'react-native-device-info'; import AndroidOpenSettings from 'react-native-android-open-settings'; import DocumentPicker from 'react-native-document-picker'; -import ImagePicker from 'react-native-image-picker'; +import {launchImageLibrary, launchCamera} from 'react-native-image-picker'; import Permissions from 'react-native-permissions'; import {showModalOverCurrentContext} from '@actions/navigation'; @@ -153,58 +153,35 @@ export default class AttachmentButton extends PureComponent { }; attachFileFromCamera = async (source, mediaType) => { - const {formatMessage} = this.context.intl; - const {title, text} = this.getPermissionDeniedMessage('camera', mediaType); const options = { quality: 0.8, videoQuality: 'high', noData: true, mediaType, - storageOptions: { - cameraRoll: true, - waitUntilSaved: true, - }, - permissionDenied: { - title, - text, - reTryTitle: formatMessage({ - id: 'mobile.permission_denied_retry', - defaultMessage: 'Settings', - }), - okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}), - }, + saveToPhotos: true, }; const hasCameraPermission = await this.hasPhotoPermission(source, mediaType); if (hasCameraPermission) { - ImagePicker.launchCamera(options, (response) => { + launchCamera(options, async (response) => { StatusBar.setHidden(false); emmProvider.inBackgroundSince = null; if (response.error || response.didCancel) { return; } - this.uploadFiles([response]); + const files = await this.getFilesFromResponse(response); + this.uploadFiles(files); }); } }; attachFileFromLibrary = async () => { - const {formatMessage} = this.context.intl; - const {title, text} = this.getPermissionDeniedMessage('photo'); const options = { quality: 0.8, - noData: true, - permissionDenied: { - title, - text, - reTryTitle: formatMessage({ - id: 'mobile.permission_denied_retry', - defaultMessage: 'Settings', - }), - okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}), - }, + mediaType: 'mixed', + includeBase64: false, }; if (Platform.OS === 'ios') { @@ -214,14 +191,15 @@ export default class AttachmentButton extends PureComponent { const hasPhotoPermission = await this.hasPhotoPermission('photo', 'photo'); if (hasPhotoPermission) { - ImagePicker.launchImageLibrary(options, (response) => { + launchImageLibrary(options, async (response) => { StatusBar.setHidden(false); emmProvider.inBackgroundSince = null; if (response.error || response.didCancel) { return; } - this.uploadFiles([response]); + const files = await this.getFilesFromResponse(response); + this.uploadFiles(files); }); } }; @@ -231,30 +209,20 @@ export default class AttachmentButton extends PureComponent { }; attachVideoFromLibraryAndroid = () => { - const {formatMessage} = this.context.intl; - const {title, text} = this.getPermissionDeniedMessage('video'); const options = { videoQuality: 'high', mediaType: 'video', noData: true, - permissionDenied: { - title, - text, - reTryTitle: formatMessage({ - id: 'mobile.permission_denied_retry', - defaultMessage: 'Settings', - }), - okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}), - }, }; - ImagePicker.launchImageLibrary(options, (response) => { + launchImageLibrary(options, async (response) => { emmProvider.inBackgroundSince = null; if (response.error || response.didCancel) { return; } - this.uploadFiles([response]); + const files = await this.getFilesFromResponse(response); + this.uploadFiles(files); }); }; @@ -286,6 +254,28 @@ export default class AttachmentButton extends PureComponent { } }; + getFilesFromResponse = async (response) => { + const files = []; + const file = response.assets[0]; + if (Platform.OS === 'android') { + // For android we need to retrieve the realPath in case the file being imported is from the cloud + const uri = (await ShareExtension.getFilePath(file.uri)).filePath; + const type = file.type || lookupMimeType(uri); + let fileName = file.fileName; + if (type.includes('video/')) { + fileName = uri.split('\\').pop().split('/').pop(); + } + + if (uri) { + files.push({...file, fileName, uri, type}); + } + } else { + files.push(file); + } + + return files; + } + hasPhotoPermission = async (source, mediaType = '') => { if (Platform.OS === 'ios') { const {formatMessage} = this.context.intl; diff --git a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js index da5e8135a..b14ac7d78 100644 --- a/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js +++ b/app/components/autocomplete/emoji_suggestion/emoji_suggestion.js @@ -13,7 +13,6 @@ import Fuse from 'fuse.js'; import Emoji from '@components/emoji'; import TouchableWithFeedback from '@components/touchable_with_feedback'; -import {BuiltInEmojis} from '@utils/emojis'; import {getEmojiByName, compareEmojis} from '@utils/emoji_utils'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; @@ -110,8 +109,8 @@ export default class EmojiSuggestion extends PureComponent { } const emojiData = getEmojiByName(emoji); - if (emojiData?.filename && !BuiltInEmojis.includes(emojiData.filename)) { - const codeArray = emojiData.filename.split('-'); + if (emojiData?.image && emojiData.category !== 'custom') { + const codeArray = emojiData.image.split('-'); const code = codeArray.reduce((acc, c) => { return acc + String.fromCodePoint(parseInt(c, 16)); }, ''); @@ -126,7 +125,7 @@ export default class EmojiSuggestion extends PureComponent { onChangeText(completedDraft); - if (Platform.OS === 'ios' && (!emojiData?.filename || BuiltInEmojis.includes(emojiData?.filename))) { + if (Platform.OS === 'ios' && (!emojiData?.filename || emojiData.category !== 'custom')) { // This is the second part of the hack were we replace the double : with just one // after the auto correct vanished setTimeout(() => { diff --git a/app/components/profile_picture/profile_picture.tsx b/app/components/profile_picture/profile_picture.tsx index a23af0cd3..15ec19a8e 100644 --- a/app/components/profile_picture/profile_picture.tsx +++ b/app/components/profile_picture/profile_picture.tsx @@ -118,13 +118,13 @@ const ProfilePicture = (props: ProfilePictureProps) => { }, [props.profileImageUri]); useDidUpdate(() => { - const {user} = props; + const {edit, user} = props; const url = user ? Client4.getProfilePictureUrl(user.id, user.last_picture_update) : undefined; - if (url !== pictureUrl) { + if (url !== pictureUrl && !edit) { setPictureUrl(url); } - }, [props.user]); + }, [props.user, props.edit]); let statusIcon; let statusStyle = props.statusStyle; diff --git a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js index 244c040d5..81cb37e47 100644 --- a/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js +++ b/app/components/sidebars/main/channels_list/filtered_list/filtered_list.js @@ -381,7 +381,7 @@ class FilteredList extends Component { { const deliveredNotifications = [{identifier: 1}, {identifier: 2}]; Notifications.setDeliveredNotifications(deliveredNotifications); - await PushNotification.clearChannelNotifications(); + await PushNotification.clearChannelNotifications('some other channel'); expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(deliveredNotifications.length); + + await PushNotification.clearChannelNotifications(); + expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0); }); it('clearChannelNotifications should set app badge number from redux store when set', async () => { @@ -90,7 +93,7 @@ describe('PushNotification', () => { const stateBadgeCount = 2 * deliveredNotifications.length; ViewSelectors.getBadgeCount = jest.fn().mockReturnValue(stateBadgeCount); - await PushNotification.clearChannelNotifications(); + await PushNotification.clearChannelNotifications('some other channel'); expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(stateBadgeCount); }); @@ -103,7 +106,7 @@ describe('PushNotification', () => { deliveredNotifications = []; Notifications.setDeliveredNotifications(deliveredNotifications); - await PushNotification.clearChannelNotifications(); + await PushNotification.clearChannelNotifications('some other channel'); expect(setBadgeCount).toHaveBeenCalledWith(0); }); }); diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 883185244..86a9c6e8b 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -88,6 +88,18 @@ class PushNotifications { //set the badge count to the total amount of notifications present in the not-center let badgeCount = notifications.length; + for (let i = 0; i < notifications.length; i++) { + const notification = notifications[i] as NotificationWithChannel; + if (notification.channel_id === channelId) { + ids.push(notification.identifier); + badgeCount--; + } + } + + if (ids.length) { + Notifications.ios.removeDeliveredNotifications(ids); + } + if (Store.redux) { const totalMentions = getBadgeCount(Store.redux.getState()); if (totalMentions > -1) { @@ -96,17 +108,6 @@ class PushNotifications { } } - for (let i = 0; i < notifications.length; i++) { - const notification = notifications[i] as NotificationWithChannel; - if (notification.channel_id === channelId) { - ids.push(notification.identifier); - } - } - - if (ids.length) { - Notifications.ios.removeDeliveredNotifications(ids); - } - if (Platform.OS === 'ios') { badgeCount = badgeCount <= 0 ? 0 : badgeCount; Notifications.ios.setBadgeCount(badgeCount); diff --git a/app/screens/settings/notification_settings_mobile/index.js b/app/screens/settings/notification_settings_mobile/index.js index 66899aa07..6cd50957d 100644 --- a/app/screens/settings/notification_settings_mobile/index.js +++ b/app/screens/settings/notification_settings_mobile/index.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {getConfig} from '@mm-redux/selectors/entities/general'; -import {getTheme} from '@mm-redux/selectors/entities/preferences'; +import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences'; import {getCurrentUser} from '@mm-redux/selectors/entities/users'; import {updateMe} from '@mm-redux/actions/users'; @@ -19,6 +19,7 @@ function mapStateToProps(state) { return { config, + teammateNameDisplay: getTeammateNameDisplaySetting(state), theme, updateMeRequest, currentUser, diff --git a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js index 4a0245d1c..5504ab262 100644 --- a/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js +++ b/app/screens/settings/notification_settings_mobile/notification_settings_mobile.android.js @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +/* eslint-disable max-lines */ + import React from 'react'; import {injectIntl} from 'react-intl'; import { @@ -14,13 +16,14 @@ import { } from 'react-native'; import deepEqual from 'deep-equal'; -import PropTypes from 'prop-types'; +import PropTypes, {string} from 'prop-types'; import FormattedText from '@components/formatted_text'; import RadioButtonGroup from '@components/radio_button'; import StatusBar from '@components/status_bar'; import PushNotifications from '@init/push_notifications'; import {RequestStatus} from '@mm-redux/constants'; +import {displayUsername} from '@mm-redux/utils/user_utils'; import SectionItem from '@screens/settings/section_item'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {getNotificationProps} from '@utils/notify_props'; @@ -31,6 +34,7 @@ import NotificationSettingsMobileBase from './notification_settings_mobile_base' class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { static propTypes = { ...NotificationSettingsMobileBase.propTypes, + teammateNameDisplay: string, updateMeRequest: PropTypes.object.isRequired, } @@ -694,13 +698,15 @@ class NotificationSettingsMobileAndroid extends NotificationSettingsMobileBase { }; sendTestNotification = () => { - const {intl} = this.props; + const {currentUser, intl, teammateNameDisplay} = this.props; PushNotifications.localNotification({ body: intl.formatMessage({ id: 'mobile.notification_settings_mobile.test_push', defaultMessage: 'This is a test push notification', }), + sender_name: displayUsername(currentUser, teammateNameDisplay), + sender_id: currentUser.id, userInfo: { local: true, test: true,