diff --git a/NOTICE.txt b/NOTICE.txt index 7a0630561..11ccab47a 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -667,6 +667,38 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- + +## @voximplant/react-native-foreground-service + +This product contains a modified version of '@voximplant/react-native-foreground-service' by Voximplant. + +A foreground service performs some operation that is noticeable to the user. + +* HOMEPAGE: + * https://github.com/voximplant/react-native-foreground-service + +* LICENSE: MIT License + +Copyright (c) 2019 Zingaya, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. --- diff --git a/android/app/build.gradle b/android/app/build.gradle index 349155214..668e84164 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 489 - versionName "2.9.0" + versionCode 490 + versionName "2.10.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts index 3de806fb8..fde5c8d11 100644 --- a/app/actions/app/global.ts +++ b/app/actions/app/global.ts @@ -77,3 +77,11 @@ export const removeLastViewedChannelIdAndServer = async () => { export const removeLastViewedThreadIdAndServer = async () => { return storeGlobal(GLOBAL_IDENTIFIERS.LAST_VIEWED_THREAD, null, false); }; + +export const storePushDisabledInServerAcknowledged = async (serverUrl: string) => { + return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, 'true', false); +}; + +export const removePushDisabledInServerAcknowledged = async (serverUrl: string) => { + return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, null, false); +}; diff --git a/app/components/tutorial_highlight/index.tsx b/app/components/tutorial_highlight/index.tsx index 182ad6e6e..2be624c19 100644 --- a/app/components/tutorial_highlight/index.tsx +++ b/app/components/tutorial_highlight/index.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {Modal, StyleSheet, useWindowDimensions, View} from 'react-native'; +import {Modal, useWindowDimensions} from 'react-native'; import HighlightItem from './item'; @@ -11,11 +11,10 @@ type Props = { itemBounds: TutorialItemBounds; itemBorderRadius?: number; onDismiss: () => void; - onLayout: () => void; onShow?: () => void; } -const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, onLayout, onShow}: Props) => { +const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, onShow}: Props) => { const {width, height} = useWindowDimensions(); const handleShowTutorial = useCallback(() => { @@ -26,19 +25,13 @@ const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, o return ( 0} transparent={true} animationType='fade' onDismiss={onDismiss} onRequestClose={onDismiss} testID='tutorial_highlight' > - - {itemBounds.endX > 0 && - } {children} ); diff --git a/app/components/user_item/user_item.tsx b/app/components/user_item/user_item.tsx index 0c2881bb4..a3f4a9e9a 100644 --- a/app/components/user_item/user_item.tsx +++ b/app/components/user_item/user_item.tsx @@ -32,6 +32,7 @@ type Props = { rightDecorator?: React.ReactNode; onUserPress?: (user: UserProfile | UserModel) => void; onUserLongPress?: (user: UserProfile | UserModel) => void; + onLayout?: () => void; disabled?: boolean; viewRef?: React.LegacyRef; padding?: number; @@ -102,6 +103,7 @@ const UserItem = ({ locale, teammateNameDisplay, rightDecorator, + onLayout, onUserPress, onUserLongPress, disabled = false, @@ -159,6 +161,7 @@ const UserItem = ({ onPress={onPress} onLongPress={onLongPress} disabled={!(onUserPress || onUserLongPress)} + onLayout={onLayout} > - - - - - - Long-press on an item to view a user's profile - - - - + visible={false} + /> { - startTutorial(); - }, []); + if (showTutorial) { + startTutorial(); + } + }, [showTutorial]); const icon = useMemo(() => { if (!selectable && !selected) { @@ -190,17 +192,19 @@ function UserListRow({ viewRef={viewRef} padding={20} includeMargin={includeMargin} + onLayout={onLayout} /> {showTutorial && + {Boolean(itemBounds.endX) && + } } diff --git a/app/constants/database.ts b/app/constants/database.ts index f14d22a62..d302c280a 100644 --- a/app/constants/database.ts +++ b/app/constants/database.ts @@ -82,6 +82,7 @@ export const GLOBAL_IDENTIFIERS = { ONBOARDING: 'onboarding', LAST_VIEWED_CHANNEL: 'lastViewedChannel', LAST_VIEWED_THREAD: 'lastViewedThread', + PUSH_DISABLED_ACK: 'pushDisabledAck', }; export enum OperationType { diff --git a/app/database/models/server/category.ts b/app/database/models/server/category.ts index 73b9f87fb..d7247fe22 100644 --- a/app/database/models/server/category.ts +++ b/app/database/models/server/category.ts @@ -120,7 +120,7 @@ export default class CategoryModel extends Model implements CategoryInterface { toCategoryWithChannels = async (): Promise => { const categoryChannels = await this.categoryChannels.fetch(); const orderedChannelIds = categoryChannels.sort((a, b) => { - return b.sortOrder - a.sortOrder; + return a.sortOrder - b.sortOrder; }).map((cc) => cc.channelId); return { diff --git a/app/managers/session_manager.ts b/app/managers/session_manager.ts index 0e21ca4a4..1ba1fbbb0 100644 --- a/app/managers/session_manager.ts +++ b/app/managers/session_manager.ts @@ -5,7 +5,7 @@ import CookieManager, {type Cookie} from '@react-native-cookies/cookies'; import {AppState, type AppStateStatus, DeviceEventEmitter, Platform} from 'react-native'; import FastImage from 'react-native-fast-image'; -import {storeOnboardingViewedValue} from '@actions/app/global'; +import {removePushDisabledInServerAcknowledged, storeOnboardingViewedValue} from '@actions/app/global'; import {cancelSessionNotification, logout, scheduleSessionNotification} from '@actions/remote/session'; import {Events, Launch} from '@constants'; import DatabaseManager from '@database/manager'; @@ -22,6 +22,7 @@ import {getThemeFromState} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import {deleteFileCache, deleteFileCacheByDir} from '@utils/file'; import {isMainActivity} from '@utils/helpers'; +import {urlSafeBase64Encode} from '@utils/security'; import {addNewServer} from '@utils/server'; import type {LaunchType} from '@typings/launch'; @@ -121,6 +122,7 @@ class SessionManager { WebsocketManager.invalidateClient(serverUrl); if (removeServer) { + await removePushDisabledInServerAcknowledged(urlSafeBase64Encode(serverUrl)); await DatabaseManager.destroyServerDatabase(serverUrl); } else { await DatabaseManager.deleteServerDatabase(serverUrl); diff --git a/app/queries/app/global.ts b/app/queries/app/global.ts index 9784c6124..e1ff99bb8 100644 --- a/app/queries/app/global.ts +++ b/app/queries/app/global.ts @@ -55,6 +55,22 @@ export const getDontAskForReview = async () => { return Boolean(records?.[0]?.value); }; +export const getPushDisabledInServerAcknowledged = async (serverDomainString: string) => { + const records = await queryGlobalValue(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverDomainString}`)?.fetch(); + return Boolean(records?.[0]?.value); +}; + +export const observePushDisabledInServerAcknowledged = (serverDomainString: string) => { + const query = queryGlobalValue(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverDomainString}`); + if (!query) { + return of$(false); + } + return query.observe().pipe( + switchMap((result) => (result.length ? result[0].observe() : of$(false))), + switchMap((v) => of$(Boolean(v))), + ); +}; + export const getFirstLaunch = async () => { const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.FIRST_LAUNCH)?.fetch(); if (!records?.[0]?.value) { diff --git a/app/screens/home/channel_list/categories_list/header/header.test.tsx b/app/screens/home/channel_list/categories_list/header/header.test.tsx index 2832e7f3d..7299f0c98 100644 --- a/app/screens/home/channel_list/categories_list/header/header.test.tsx +++ b/app/screens/home/channel_list/categories_list/header/header.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; -import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; +import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import {renderWithIntl} from '@test/intl-test-helper'; import Header from './header'; @@ -22,4 +22,18 @@ describe('components/channel_list/header', () => { expect(toJSON()).toMatchSnapshot(); }); + + it('Push notifications disabled show alert icon', () => { + const wrapper = renderWithIntl( +
, + ); + + expect(wrapper.getByTestId('channel_list_header.push_alert')).toBeTruthy(); + }); }); diff --git a/app/screens/home/channel_list/categories_list/header/header.tsx b/app/screens/home/channel_list/categories_list/header/header.tsx index 4cec3fde3..ca2840642 100644 --- a/app/screens/home/channel_list/categories_list/header/header.tsx +++ b/app/screens/home/channel_list/categories_list/header/header.tsx @@ -206,7 +206,7 @@ const ChannelListHeader = ({ > {serverDisplayName} - {(pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED) && ( + {pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && ( { - swipeable.current?.close(); - startTutorial(); - }, []); + if (showTutorial) { + swipeable.current?.close(); + startTutorial(); + } + }, [showTutorial]); const containerStyle = useMemo(() => { const style: StyleProp = [styles.container]; @@ -390,6 +392,7 @@ const ServerItem = ({ style={containerStyle} ref={viewRef} testID={serverItemTestId} + onLayout={onLayout} > + {Boolean(itemBounds.endX) && + } } diff --git a/app/utils/categories.ts b/app/utils/categories.ts index 7b2d3e604..91f9afadc 100644 --- a/app/utils/categories.ts +++ b/app/utils/categories.ts @@ -180,7 +180,9 @@ const sortChannelsByName = (notifyPropsPerChannel: Record>, locale: string) => { if (sorting === 'recent') { return channelsWithMyChannel.sort((cwmA, cwmB) => { - return cwmB.myChannel.lastPostAt - cwmA.myChannel.lastPostAt; + const a = Math.max(cwmA.myChannel.lastPostAt, cwmA.channel.createAt); + const b = Math.max(cwmB.myChannel.lastPostAt, cwmB.channel.createAt); + return b - a; }).map((cwm) => cwm.channel); } else if (sorting === 'manual') { return channelsWithMyChannel.sort((cwmA, cwmB) => { diff --git a/app/utils/push_proxy.ts b/app/utils/push_proxy.ts index e207c9841..ec84c91c3 100644 --- a/app/utils/push_proxy.ts +++ b/app/utils/push_proxy.ts @@ -3,16 +3,29 @@ import {Alert} from 'react-native'; +import {storePushDisabledInServerAcknowledged} from '@actions/app/global'; import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; +import {getPushDisabledInServerAcknowledged} from '@queries/app/global'; import EphemeralStore from '@store/ephemeral_store'; +import {urlSafeBase64Encode} from './security'; + import type {IntlShape} from 'react-intl'; -export function canReceiveNotifications(serverUrl: string, verification: string, intl: IntlShape) { +export function pushDisabledInServerAck(serverUrl: string) { + const extractedDomain = urlSafeBase64Encode(serverUrl); + return getPushDisabledInServerAcknowledged(extractedDomain); +} + +export async function canReceiveNotifications(serverUrl: string, verification: string, intl: IntlShape) { + const hasAckNotification = await pushDisabledInServerAck(serverUrl); + switch (verification) { case PUSH_PROXY_RESPONSE_NOT_AVAILABLE: EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_NOT_AVAILABLE); - alertPushProxyError(intl); + if (!hasAckNotification) { + alertPushProxyError(intl, serverUrl); + } break; case PUSH_PROXY_RESPONSE_UNKNOWN: EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_UNKNOWN); @@ -23,7 +36,14 @@ export function canReceiveNotifications(serverUrl: string, verification: string, } } -export function alertPushProxyError(intl: IntlShape) { +const handleAlertResponse = async (buttonIndex: number, serverUrl?: string) => { + if (buttonIndex === 0 && serverUrl) { + // User clicked "Okay" acknowledging that the push notifications are disabled on that server + await storePushDisabledInServerAcknowledged(urlSafeBase64Encode(serverUrl)); + } +}; + +export function alertPushProxyError(intl: IntlShape, serverUrl?: string) { Alert.alert( intl.formatMessage({ id: 'alert.push_proxy_error.title', @@ -31,10 +51,11 @@ export function alertPushProxyError(intl: IntlShape) { }), intl.formatMessage({ id: 'alert.push_proxy_error.description', - defaultMessage: 'Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.', + defaultMessage: 'Due to the configuration of this server, notifications cannot be received in the mobile app. Contact your system admin for more information.', }), [{ text: intl.formatMessage({id: 'alert.push_proxy.button', defaultMessage: 'Okay'}), + onPress: () => handleAlertResponse(0, serverUrl), }], ); } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 9434c16b5..7ca1f1df3 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -15,7 +15,7 @@ "account.your_profile": "Your Profile", "alert.channel_deleted.description": "The channel {displayName} has been archived.", "alert.channel_deleted.title": "Archived channel", - "alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", + "alert.push_proxy_error.description": "Due to the configuration of this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", "alert.push_proxy_error.title": "Notifications cannot be received from this server", "alert.push_proxy_unknown.description": "This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.", "alert.push_proxy_unknown.title": "Notifications could not be received from this server", diff --git a/assets/base/i18n/en_AU.json b/assets/base/i18n/en_AU.json index e6dbf6303..b0f133bc0 100644 --- a/assets/base/i18n/en_AU.json +++ b/assets/base/i18n/en_AU.json @@ -686,7 +686,7 @@ "alert.push_proxy.button": "Okay", "alert.push_proxy_unknown.title": "Notifications could not be received from this server", "alert.push_proxy_error.title": "Notifications cannot be received from this server", - "alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", + "alert.push_proxy_error.description": "Due to the configuration of this server, notifications cannot be received in the mobile app. Contact your system admin for more information.", "alert.channel_deleted.title": "Archived channel", "alert.channel_deleted.description": "The channel {displayName} has been archived.", "account.your_profile": "Your Profile", diff --git a/assets/base/i18n/zh_Hans.json b/assets/base/i18n/zh_Hans.json deleted file mode 100644 index 0967ef424..000000000 --- a/assets/base/i18n/zh_Hans.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/assets/base/i18n/zh_Hant.json b/assets/base/i18n/zh_Hant.json deleted file mode 100644 index 0967ef424..000000000 --- a/assets/base/i18n/zh_Hant.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 016a25e40..568c6f899 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -1929,7 +1929,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 489; + CURRENT_PROJECT_VERSION = 490; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1973,7 +1973,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 489; + CURRENT_PROJECT_VERSION = 490; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -2116,7 +2116,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 489; + CURRENT_PROJECT_VERSION = 490; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -2165,7 +2165,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 489; + CURRENT_PROJECT_VERSION = 490; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 41404b833..27de4b73f 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.9.0 + 2.10.0 CFBundleSignature ???? CFBundleURLTypes @@ -37,7 +37,7 @@ CFBundleVersion - 489 + 490 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 51abcdda1..006552425 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.9.0 + 2.10.0 CFBundleVersion - 489 + 490 UIAppFonts OpenSans-Bold.ttf diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index bef506837..bbe279808 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.9.0 + 2.10.0 CFBundleVersion - 489 + 490 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index df9d27bef..db9a2498a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mattermost-mobile", - "version": "2.9.0", + "version": "2.10.0", "lockfileVersion": 2, "requires": true, "packages": { @@ -28574,7 +28574,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@voximplant/react-native-foreground-service/-/react-native-foreground-service-3.0.2.tgz", "integrity": "sha512-ZIyccOAXPqznA1PAVAYlKZ+GI7kXYCuYgH+gmAkhPouyJbkgrSXaCJJzQ+uBkPr4FBa/PuC/yjzK8vf6tJREQA==", - "requires": {} + "requires": { + } }, "@webassemblyjs/ast": { "version": "1.11.1", diff --git a/package.json b/package.json index 077ac401a..16cabecc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mattermost-mobile", - "version": "2.9.0", + "version": "2.10.0", "description": "Mattermost Mobile with React Native", "repository": "git@github.com:mattermost/mattermost-mobile.git", "author": "Mattermost, Inc.",