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.",