forked from Ivasoft/mattermost-mobile
[Gekidou] [MM-41837] Add verification for push proxy and related interface (#6192)
* Add verification for push proxy and related interface * Fix lint and extract i18n * Be specific about undefined equalities. * Fix test * Address feedback * Fix long server names styles * Fix tests and typo
This commit is contained in:
committed by
GitHub
parent
a80505496c
commit
7e80843092
@@ -6,10 +6,14 @@ import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
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 DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryChannelsById, getDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {prepareModels} from '@queries/servers/entry';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getPushVerificationStatus} from '@queries/servers/system';
|
||||
import {getNthLastChannelFromTeam} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
@@ -96,10 +100,59 @@ export async function appEntry(serverUrl: string, since = 0) {
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
return {error, userId: meData?.user?.id};
|
||||
}
|
||||
|
||||
export async function verifyPushProxy(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const ppVerification = await getPushVerificationStatus(database);
|
||||
if (ppVerification !== PUSH_PROXY_STATUS_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = await getDeviceToken(appDatabase);
|
||||
if (!deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.ping(deviceId);
|
||||
const canReceiveNotifications = response?.data?.CanReceiveNotifications;
|
||||
switch (canReceiveNotifications) {
|
||||
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
|
||||
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_NOT_AVAILABLE}], prepareRecordsOnly: false});
|
||||
return;
|
||||
case PUSH_PROXY_RESPONSE_UNKNOWN:
|
||||
return;
|
||||
default:
|
||||
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}], prepareRecordsOnly: false});
|
||||
}
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export async function upgradeEntry(serverUrl: string) {
|
||||
const dt = Date.now();
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
@@ -2,17 +2,40 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_VERIFIED, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {t} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getExpandedLinks} from '@queries/servers/system';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {ClientResponse} from '@mattermost/react-native-network-client';
|
||||
|
||||
export const doPing = async (serverUrl: string) => {
|
||||
async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
|
||||
if (!checkDeviceId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const serverDatabase = DatabaseManager.serverDatabases?.[serverUrl]?.database;
|
||||
if (serverDatabase) {
|
||||
const status = await getPushVerificationStatus(serverDatabase);
|
||||
if (status === PUSH_PROXY_STATUS_VERIFIED) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getDeviceToken(appDatabase);
|
||||
}
|
||||
|
||||
export const doPing = async (serverUrl: string, verifyPushProxy: boolean) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = await NetworkManager.createClient(serverUrl);
|
||||
@@ -30,9 +53,11 @@ export const doPing = async (serverUrl: string) => {
|
||||
defaultMessage: 'Cannot connect to the server.',
|
||||
};
|
||||
|
||||
const deviceId = await getDeviceIdForPing(serverUrl, verifyPushProxy);
|
||||
|
||||
let response: ClientResponse;
|
||||
try {
|
||||
response = await client.ping();
|
||||
response = await client.ping(deviceId);
|
||||
|
||||
if (response.code === 401) {
|
||||
// Don't invalidate the client since we want to eventually
|
||||
@@ -50,6 +75,17 @@ export const doPing = async (serverUrl: string) => {
|
||||
return {error: {intl: pingError}};
|
||||
}
|
||||
|
||||
if (verifyPushProxy) {
|
||||
let canReceiveNotifications = response?.data?.CanReceiveNotifications;
|
||||
|
||||
// Already verified or old server
|
||||
if (deviceId === undefined || canReceiveNotifications === null) {
|
||||
canReceiveNotifications = PUSH_PROXY_RESPONSE_VERIFIED;
|
||||
}
|
||||
|
||||
return {canReceiveNotifications, error: undefined};
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getCurrentUserId, getCommonSystemValues} from '@queries/servers/system';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
|
||||
import {loginEntry} from './entry';
|
||||
@@ -49,16 +50,28 @@ export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [];
|
||||
|
||||
// Set push proxy verification
|
||||
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
|
||||
if (ppVerification) {
|
||||
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
|
||||
}
|
||||
|
||||
// Start websocket
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
return operator.handleSystem({systems: [{
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
|
||||
value: 0,
|
||||
}],
|
||||
prepareRecordsOnly: false});
|
||||
});
|
||||
}
|
||||
|
||||
if (systems.length) {
|
||||
operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import ClientError from './error';
|
||||
|
||||
export interface ClientGeneralMix {
|
||||
getOpenGraphMetadata: (url: string) => Promise<any>;
|
||||
ping: () => Promise<any>;
|
||||
ping: (deviceId?: string) => Promise<any>;
|
||||
logClientError: (message: string, level?: string) => Promise<any>;
|
||||
getClientConfigOld: () => Promise<ClientConfig>;
|
||||
getClientLicenseOld: () => Promise<ClientLicense>;
|
||||
@@ -25,9 +25,13 @@ const ClientGeneral = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
ping = async () => {
|
||||
ping = async (deviceId?: string) => {
|
||||
let url = `${this.urlVersion}/system/ping?time=${Date.now()}`;
|
||||
if (deviceId) {
|
||||
url = `${url}&device_id=${deviceId}`;
|
||||
}
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/system/ping?time=${Date.now()}`,
|
||||
url,
|
||||
{method: 'get'},
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -54,6 +54,7 @@ export const SYSTEM_IDENTIFIERS = {
|
||||
DATA_RETENTION_POLICIES: 'dataRetentionPolicies',
|
||||
EXPANDED_LINKS: 'expandedLinks',
|
||||
LICENSE: 'license',
|
||||
PUSH_VERIFICATION_STATUS: 'pushVerificationStatus',
|
||||
RECENT_CUSTOM_STATUS: 'recentCustomStatus',
|
||||
RECENT_MENTIONS: 'recentMentions',
|
||||
RECENT_REACTIONS: 'recentReactions',
|
||||
|
||||
@@ -24,6 +24,7 @@ import Post from './post';
|
||||
import PostDraft from './post_draft';
|
||||
import Preferences from './preferences';
|
||||
import Profile from './profile';
|
||||
import PushProxy from './push_proxy';
|
||||
import Screens from './screens';
|
||||
import ServerErrors from './server_errors';
|
||||
import SnackBar from './snack_bar';
|
||||
@@ -56,6 +57,7 @@ export {
|
||||
PostDraft,
|
||||
Preferences,
|
||||
Profile,
|
||||
PushProxy,
|
||||
Screens,
|
||||
ServerErrors,
|
||||
SnackBar,
|
||||
|
||||
19
app/constants/push_proxy.ts
Normal file
19
app/constants/push_proxy.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const PUSH_PROXY_STATUS_VERIFIED = 'verified';
|
||||
export const PUSH_PROXY_STATUS_UNKNOWN = 'unknown';
|
||||
export const PUSH_PROXY_STATUS_NOT_AVAILABLE = 'not_available';
|
||||
|
||||
export const PUSH_PROXY_RESPONSE_VERIFIED = 'true';
|
||||
export const PUSH_PROXY_RESPONSE_UNKNOWN = 'unknown';
|
||||
export const PUSH_PROXY_RESPONSE_NOT_AVAILABLE = 'false';
|
||||
|
||||
export default {
|
||||
PUSH_PROXY_STATUS_VERIFIED,
|
||||
PUSH_PROXY_STATUS_UNKNOWN,
|
||||
PUSH_PROXY_STATUS_NOT_AVAILABLE,
|
||||
PUSH_PROXY_RESPONSE_VERIFIED,
|
||||
PUSH_PROXY_RESPONSE_UNKNOWN,
|
||||
PUSH_PROXY_RESPONSE_NOT_AVAILABLE,
|
||||
};
|
||||
@@ -9,9 +9,9 @@ import type Global from '@typings/database/models/app/global';
|
||||
|
||||
const {APP: {GLOBAL}} = MM_TABLES;
|
||||
|
||||
export const getDeviceToken = async (appDatabase: Database) => {
|
||||
export const getDeviceToken = async (appDatabase: Database): Promise<string> => {
|
||||
try {
|
||||
const tokens = await appDatabase.get(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN) as Global;
|
||||
const tokens = await appDatabase.get<Global>(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN);
|
||||
return tokens?.value || '';
|
||||
} catch {
|
||||
return '';
|
||||
|
||||
@@ -6,6 +6,7 @@ import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
@@ -72,7 +73,22 @@ export const getCurrentUserId = async (serverDatabase: Database): Promise<string
|
||||
export const observeCurrentUserId = (database: Database) => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.CURRENT_USER_ID).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: ''}))),
|
||||
).pipe(
|
||||
switchMap((model) => of$(model.value as string)),
|
||||
);
|
||||
};
|
||||
|
||||
export const getPushVerificationStatus = async (serverDatabase: Database): Promise<string> => {
|
||||
try {
|
||||
const status = await serverDatabase.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS);
|
||||
return status?.value || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const observePushVerificationStatus = (database: Database) => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: PUSH_PROXY_STATUS_UNKNOWN}))),
|
||||
switchMap((model) => of$(model.value as string)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -148,23 +148,32 @@ exports[`components/categories_list should render channels error 1`] = `
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
"paddingRight": 30,
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"paddingRight": 60,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.server_display_name"
|
||||
>
|
||||
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.server_display_name"
|
||||
>
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
@@ -340,12 +349,17 @@ exports[`components/categories_list should render team error 1`] = `
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"height": 40,
|
||||
"justifyContent": "space-between",
|
||||
},
|
||||
Object {
|
||||
"flex": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
|
||||
@@ -128,22 +128,31 @@ exports[`components/channel_list/header Channel List Header Component should mat
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
"paddingRight": 30,
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"paddingRight": 60,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.server_display_name"
|
||||
>
|
||||
|
||||
</Text>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.64)",
|
||||
"fontFamily": "OpenSans-SemiBold",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "600",
|
||||
"lineHeight": 16,
|
||||
}
|
||||
}
|
||||
testID="channel_list_header.server_display_name"
|
||||
>
|
||||
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import {renderWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import Header from './header';
|
||||
@@ -11,6 +12,7 @@ describe('components/channel_list/header', () => {
|
||||
it('Channel List Header Component should match snapshot', () => {
|
||||
const {toJSON} = renderWithIntl(
|
||||
<Header
|
||||
pushProxyStatus={PUSH_PROXY_STATUS_VERIFIED}
|
||||
canCreateChannels={true}
|
||||
canJoinChannels={true}
|
||||
displayName={'Test!'}
|
||||
|
||||
@@ -11,11 +11,13 @@ import {logout} from '@actions/remote/session';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import {useServerDisplayName, useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {alertPushProxyError, alertPushProxyUnknown} from '@utils/push_proxy';
|
||||
import {alertServerLogout} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
@@ -28,6 +30,7 @@ type Props = {
|
||||
displayName: string;
|
||||
iconPad?: boolean;
|
||||
onHeaderPress?: () => void;
|
||||
pushProxyStatus: string;
|
||||
}
|
||||
|
||||
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
@@ -37,7 +40,6 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
subHeadingStyles: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
paddingRight: 30,
|
||||
...typography('Heading', 50),
|
||||
},
|
||||
headerRow: {
|
||||
@@ -64,6 +66,14 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
color: changeOpacity(theme.sidebarText, 0.8),
|
||||
fontSize: 18,
|
||||
},
|
||||
pushAlert: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
subHeadingView: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 60,
|
||||
},
|
||||
noTeamHeadingStyles: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
@@ -78,7 +88,14 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
|
||||
const hitSlop: Insets = {top: 10, bottom: 30, left: 20, right: 20};
|
||||
|
||||
const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, iconPad, onHeaderPress}: Props) => {
|
||||
const ChannelListHeader = ({
|
||||
canCreateChannels,
|
||||
canJoinChannels,
|
||||
displayName,
|
||||
iconPad,
|
||||
onHeaderPress,
|
||||
pushProxyStatus,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const intl = useIntl();
|
||||
@@ -90,7 +107,6 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
|
||||
marginLeft: withTiming(marginLeft.value, {duration: 350}),
|
||||
}), []);
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
useEffect(() => {
|
||||
marginLeft.value = iconPad ? 44 : 0;
|
||||
}, [iconPad]);
|
||||
@@ -124,6 +140,14 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
|
||||
});
|
||||
}, [intl, insets, isTablet, theme]);
|
||||
|
||||
const onPushAlertPress = useCallback(() => {
|
||||
if (pushProxyStatus === PUSH_PROXY_STATUS_NOT_AVAILABLE) {
|
||||
alertPushProxyError(intl);
|
||||
} else {
|
||||
alertPushProxyUnknown(intl);
|
||||
}
|
||||
}, [pushProxyStatus, intl]);
|
||||
|
||||
const onLogoutPress = useCallback(() => {
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
}, []);
|
||||
@@ -168,20 +192,36 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.subHeadingStyles}
|
||||
testID='channel_list_header.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
<View style={styles.subHeadingView}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.subHeadingStyles}
|
||||
testID='channel_list_header.server_display_name'
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
{(pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED) && (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPushAlertPress}
|
||||
testID='channel_list_header.push_alert'
|
||||
type='opacity'
|
||||
>
|
||||
<CompassIcon
|
||||
name='alert-outline'
|
||||
color={theme.errorTextColor}
|
||||
size={14}
|
||||
style={styles.pushAlert}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
header = (
|
||||
<View style={styles.noTeamHeaderRow}>
|
||||
<View style={styles.noTeamHeaderRow}>
|
||||
<View style={[styles.noTeamHeaderRow, {flex: 1}]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
|
||||
@@ -8,6 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Permissions} from '@constants';
|
||||
import {observePermissionForTeam} from '@queries/servers/role';
|
||||
import {observePushVerificationStatus} from '@queries/servers/system';
|
||||
import {observeCurrentTeam} from '@queries/servers/team';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
|
||||
@@ -42,6 +43,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
displayName: team.pipe(
|
||||
switchMap((t) => of$(t?.displayName)),
|
||||
),
|
||||
pushProxyStatus: observePushVerificationStatus(database),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import {of as of$} from 'rxjs';
|
||||
import {catchError, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {GLOBAL_IDENTIFIERS, MM_TABLES} from '@constants/database';
|
||||
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {observePushVerificationStatus} from '@queries/servers/system';
|
||||
|
||||
import ServerItem from './server_item';
|
||||
|
||||
@@ -24,9 +27,12 @@ const enhance = withObservables(['highlight'], ({highlight, server}: {highlight:
|
||||
);
|
||||
}
|
||||
|
||||
const serverDatabase = DatabaseManager.serverDatabases[server.url]?.database;
|
||||
|
||||
return {
|
||||
server: server.observe(),
|
||||
tutorialWatched,
|
||||
pushProxyStatus: serverDatabase ? observePushVerificationStatus(serverDatabase) : of$(PUSH_PROXY_STATUS_UNKNOWN),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -18,11 +18,14 @@ import ServerIcon from '@components/server_icon';
|
||||
import TutorialHighlight from '@components/tutorial_highlight';
|
||||
import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left';
|
||||
import {Events} from '@constants';
|
||||
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 {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {alertPushProxyError, alertPushProxyUnknown} from '@utils/push_proxy';
|
||||
import {alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
@@ -39,6 +42,7 @@ type Props = {
|
||||
isActive: boolean;
|
||||
server: ServersModel;
|
||||
tutorialWatched: boolean;
|
||||
pushProxyStatus: string;
|
||||
}
|
||||
|
||||
type BadgeValues = {
|
||||
@@ -86,6 +90,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
name: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
flex: 1,
|
||||
},
|
||||
offline: {
|
||||
opacity: 0.5,
|
||||
@@ -105,6 +110,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
url: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 75, 'Regular'),
|
||||
marginRight: 7,
|
||||
},
|
||||
switching: {
|
||||
height: 40,
|
||||
@@ -117,9 +123,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
tutorialTablet: {
|
||||
top: -80,
|
||||
},
|
||||
nameView: {
|
||||
flexDirection: 'row',
|
||||
marginRight: 7,
|
||||
},
|
||||
pushAlert: {
|
||||
marginLeft: 7,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
pushAlertText: {
|
||||
color: theme.errorTextColor,
|
||||
...typography('Body', 75, 'Regular'),
|
||||
marginBottom: 12,
|
||||
},
|
||||
}));
|
||||
|
||||
const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
|
||||
const ServerItem = ({
|
||||
highlight,
|
||||
isActive,
|
||||
server,
|
||||
tutorialWatched,
|
||||
pushProxyStatus,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
@@ -201,12 +226,26 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
|
||||
const handleLogin = useCallback(async () => {
|
||||
swipeable.current?.close();
|
||||
setSwitching(true);
|
||||
const result = await doPing(server.url);
|
||||
const result = await doPing(server.url, true);
|
||||
if (result.error) {
|
||||
alertServerError(intl, result.error as ClientErrorProps);
|
||||
setSwitching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result.canReceiveNotifications) {
|
||||
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
|
||||
EphemeralStore.setPushProxyVerificationState(server.url, PUSH_PROXY_STATUS_NOT_AVAILABLE);
|
||||
alertPushProxyError(intl);
|
||||
break;
|
||||
case PUSH_PROXY_RESPONSE_UNKNOWN:
|
||||
EphemeralStore.setPushProxyVerificationState(server.url, PUSH_PROXY_STATUS_UNKNOWN);
|
||||
alertPushProxyUnknown(intl);
|
||||
break;
|
||||
default:
|
||||
EphemeralStore.setPushProxyVerificationState(server.url, PUSH_PROXY_STATUS_VERIFIED);
|
||||
}
|
||||
|
||||
const data = await fetchConfigAndLicense(server.url, true);
|
||||
if (data.error) {
|
||||
alertServerError(intl, data.error as ClientErrorProps);
|
||||
@@ -311,6 +350,16 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
|
||||
const serverItem = `server_list.server_item.${server.displayName.replace(/ /g, '_').toLocaleLowerCase()}`;
|
||||
const serverItemTestId = isActive ? `${serverItem}.active` : `${serverItem}.inactive`;
|
||||
|
||||
const pushAlertText = pushProxyStatus === PUSH_PROXY_STATUS_NOT_AVAILABLE ?
|
||||
intl.formatMessage({
|
||||
id: 'server_list.push_proxy_error',
|
||||
defaultMessage: 'Notifications cannot be received from this server because of its configuration. Contact your system admin.',
|
||||
}) :
|
||||
intl.formatMessage({
|
||||
id: 'server_list.push_proxy_unknown',
|
||||
defaultMessage: 'Notifications could not be received from this server because of its configuration. Log out and Log in again to retry.',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swipeable
|
||||
@@ -355,13 +404,23 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
|
||||
/>
|
||||
}
|
||||
<View style={styles.details}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.name}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
<View style={styles.nameView}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.name}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
{pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && (
|
||||
<CompassIcon
|
||||
name='alert-outline'
|
||||
color={theme.errorTextColor}
|
||||
size={14}
|
||||
style={styles.pushAlert}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
@@ -383,6 +442,12 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
|
||||
</RectButton>
|
||||
</View>
|
||||
</Swipeable>
|
||||
{pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && (
|
||||
<Text style={styles.pushAlertText}>
|
||||
{pushAlertText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{Boolean(database) && server.lastActiveAt > 0 &&
|
||||
<WebSocket
|
||||
database={database}
|
||||
|
||||
@@ -24,10 +24,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
marginTop: 20,
|
||||
marginHorizontal: 24,
|
||||
},
|
||||
text: {
|
||||
logoutText: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
displayNameText: {
|
||||
color: changeOpacity(theme.sidebarText, 0.64),
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
flex: 1,
|
||||
},
|
||||
logoutContainer: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
displayNameContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const MARGIN_WITH_SERVER_ICON = 66;
|
||||
@@ -53,8 +64,9 @@ function Header() {
|
||||
|
||||
let serverLabel = (
|
||||
<Text
|
||||
style={styles.text}
|
||||
style={styles.displayNameText}
|
||||
testID='select_team.server_display_name'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{serverDisplayName}
|
||||
</Text>
|
||||
@@ -65,6 +77,7 @@ function Header() {
|
||||
onPress={onLabelPress}
|
||||
type='opacity'
|
||||
testID='select_team.server_display_name.touchable'
|
||||
style={styles.displayNameContainer}
|
||||
>
|
||||
{serverLabel}
|
||||
</TouchableWithFeedback>
|
||||
@@ -80,9 +93,10 @@ function Header() {
|
||||
onPress={onLogoutPress}
|
||||
testID='select_team.logout.button'
|
||||
type='opacity'
|
||||
style={styles.logoutContainer}
|
||||
>
|
||||
<Text
|
||||
style={styles.text}
|
||||
style={styles.logoutText}
|
||||
testID='select_team.logout.text'
|
||||
>
|
||||
{intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})}
|
||||
|
||||
@@ -16,14 +16,17 @@ import LocalConfig from '@assets/config.json';
|
||||
import ClientError from '@client/rest/error';
|
||||
import AppVersion from '@components/app_version';
|
||||
import {Screens} from '@constants';
|
||||
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 DatabaseManager from '@database/manager';
|
||||
import {t} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {queryServerByDisplayName, queryServerByIdentifier} from '@queries/app/servers';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
|
||||
import {getErrorMessage} from '@utils/client_error';
|
||||
import {alertPushProxyError, alertPushProxyUnknown} from '@utils/push_proxy';
|
||||
import {loginOptions} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getServerUrlAfterRedirect, isValidUrl, sanitizeUrl} from '@utils/url';
|
||||
@@ -247,7 +250,7 @@ const Server = ({
|
||||
};
|
||||
|
||||
const serverUrl = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp);
|
||||
const result = await doPing(serverUrl);
|
||||
const result = await doPing(serverUrl, true);
|
||||
|
||||
if (canceled) {
|
||||
return;
|
||||
@@ -265,6 +268,19 @@ const Server = ({
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result.canReceiveNotifications) {
|
||||
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
|
||||
EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_NOT_AVAILABLE);
|
||||
alertPushProxyError(intl);
|
||||
break;
|
||||
case PUSH_PROXY_RESPONSE_UNKNOWN:
|
||||
EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_UNKNOWN);
|
||||
alertPushProxyUnknown(intl);
|
||||
break;
|
||||
default:
|
||||
EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_VERIFIED);
|
||||
}
|
||||
|
||||
const data = await fetchConfigAndLicense(serverUrl, true);
|
||||
if (data.error) {
|
||||
setButtonDisabled(true);
|
||||
|
||||
@@ -10,6 +10,8 @@ class EphemeralStore {
|
||||
creatingChannel = false;
|
||||
creatingDMorGMTeammates: string[] = [];
|
||||
|
||||
private pushProxyVerification: {[x: string]: string | undefined} = {};
|
||||
|
||||
// As of today, the server sends a duplicated event to add the user to the team.
|
||||
// If we do not handle this, this ends up showing some errors in the database, apart
|
||||
// of the extra computation time. We use this to track the events that are being handled
|
||||
@@ -146,6 +148,14 @@ class EphemeralStore {
|
||||
isAddingToTeam = (teamId: string) => {
|
||||
return this.addingTeam.has(teamId);
|
||||
};
|
||||
|
||||
setPushProxyVerificationState = (serverUrl: string, state: string) => {
|
||||
this.pushProxyVerification[serverUrl] = state;
|
||||
};
|
||||
|
||||
getPushProxyVerificationState = (serverUrl: string) => {
|
||||
return this.pushProxyVerification[serverUrl];
|
||||
};
|
||||
}
|
||||
|
||||
export default new EphemeralStore();
|
||||
|
||||
37
app/utils/push_proxy.ts
Normal file
37
app/utils/push_proxy.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
export function alertPushProxyError(intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'alert.push_proxy_error.title',
|
||||
defaultMessage: 'Notifications cannot be received from this server',
|
||||
}),
|
||||
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.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'alert.push_proxy.button', defaultMessage: 'Okay'}),
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
export function alertPushProxyUnknown(intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'alert.push_proxy_unknown.title',
|
||||
defaultMessage: 'Notifications could not be received from this server',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'alert.push_proxy_unknown.description',
|
||||
defaultMessage: 'This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.',
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'alert.push_proxy.button', defaultMessage: 'Okay'}),
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,11 @@
|
||||
"account.settings": "Settings",
|
||||
"account.user_status.title": "User Presence",
|
||||
"account.your_profile": "Your Profile",
|
||||
"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.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",
|
||||
"alert.push_proxy.button": "Okay",
|
||||
"alert.removed_from_team.description": "You have been removed from team {displayName}.",
|
||||
"alert.removed_from_team.title": "Removed from team",
|
||||
"api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.",
|
||||
@@ -531,6 +536,8 @@
|
||||
"select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.",
|
||||
"select_team.no_team.title": "No teams are available to join",
|
||||
"select_team.title": "Select a team",
|
||||
"server_list.push_proxy_error": "Notifications cannot be received from this server because of its configuration. Contact your system admin.",
|
||||
"server_list.push_proxy_unknown": "Notifications could not be received from this server because of its configuration. Log out and Log in again to retry.",
|
||||
"server.logout.alert_description": "All associated data will be removed",
|
||||
"server.logout.alert_title": "Are you sure you want to log out of {displayName}?",
|
||||
"server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed",
|
||||
|
||||
@@ -10,6 +10,8 @@ import nock from 'nock';
|
||||
|
||||
import Config from '@assets/config.json';
|
||||
import {Client} from '@client/rest';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
import {generateId} from '@utils/general';
|
||||
@@ -106,6 +108,11 @@ class TestHelper {
|
||||
await operator.batchRecords(systems);
|
||||
}
|
||||
|
||||
await operator.handleSystem({
|
||||
prepareRecordsOnly: false,
|
||||
systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}],
|
||||
});
|
||||
|
||||
return {database, operator};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user