Only call app entry on websocket reconnect (#7065)

* Only call app entry on websocket reconnect

* Handle notification on its own entry and run app entry on websocket initialization

* Fix notification entry issues

* Fix login entry and add retry on entry failure

* feedback review

* Put back handleEntryAfterLoadNavigation before the batching

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Daniel Espino García
2023-02-23 10:11:34 +01:00
committed by GitHub
parent 98f25046af
commit 9f84ab79ce
29 changed files with 269 additions and 500 deletions

View File

@@ -1,87 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {dataRetentionCleanup, setLastServerVersionCheck} from '@actions/local/systems';
import {setLastServerVersionCheck} from '@actions/local/systems';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import DatabaseManager from '@database/manager';
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import {setTeamLoading} from '@store/team_load_store';
import WebsocketManager from '@managers/websocket_manager';
import {prepareCommonSystemValues} from '@queries/servers/system';
import {deleteV1Data} from '@utils/file';
import {isTablet} from '@utils/helpers';
import {logInfo} from '@utils/log';
import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
import {deferredAppEntryActions, entry} from './gql_common';
import {verifyPushProxy} from './common';
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
export async function appEntry(serverUrl: string, since = 0) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
if (!since) {
registerDeviceToken(serverUrl);
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
await setLastServerVersionCheck(serverUrl, true);
}
}
// Run data retention cleanup
await dataRetentionCleanup(serverUrl);
// clear lastUnreadChannelId
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
if (removeLastUnreadChannelId) {
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
}
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);
const currentChannelId = await getCurrentChannelId(database);
const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since;
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
return {error: entryData.error};
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
if (isUpgrade && meData?.user) {
const isTabletDevice = await isTablet();
const me = await prepareCommonSystemValues(operator, {
currentUserId: meData.user.id,
currentTeamId: initialTeamId,
currentChannelId: isTabletDevice ? initialChannelId : undefined,
});
if (me?.length) {
await operator.batchRecords(me, 'appEntry - upgrade store me');
}
}
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models, 'appEntry');
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
const config = await getConfig(database);
const license = await getLicense(database);
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
if (!since) {
// Load data from other servers
syncOtherServers(serverUrl);
}
WebsocketManager.openAll();
verifyPushProxy(serverUrl);
return {userId: currentUserId};
return {};
}
export async function upgradeEntry(serverUrl: string) {
@@ -89,7 +40,7 @@ export async function upgradeEntry(serverUrl: string) {
try {
const configAndLicense = await fetchConfigAndLicense(serverUrl, false);
const entryData = await appEntry(serverUrl, 0, true);
const entryData = await appEntry(serverUrl, 0);
const error = configAndLicense.error || entryData.error;
if (!error) {

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
@@ -11,8 +10,7 @@ import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
import {gqlAllChannels} from '@client/graphQL/entry';
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
import {General, Preferences, 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';
@@ -22,16 +20,13 @@ import {selectDefaultTeam} from '@helpers/api/team';
import {DEFAULT_LOCALE} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {getAllServers} from '@queries/app/servers';
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
import {getHasCRTChanged} from '@queries/servers/preference';
import {getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getConfig, getCurrentChannelId, getCurrentTeamId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import NavigationStore from '@store/navigation_store';
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
import {isTablet} from '@utils/helpers';
import {logDebug} from '@utils/log';
import {processIsCRTEnabled} from '@utils/thread';
@@ -112,6 +107,12 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?:
}
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData;
const chError = chData?.error as ClientError | undefined;
if (chError?.status_code === 403) {
// if the user does not have appropriate permissions, which means the user those not belong to the team,
// we set it as there is no errors, so that the teams and others can be properly handled
chData!.error = undefined;
}
const error = teamData.error || chData?.error || prefData.error || meData.error;
if (error) {
return {error};
@@ -372,107 +373,6 @@ export const registerDeviceToken = async (serverUrl: string) => {
return {error: undefined};
};
export const syncOtherServers = async (serverUrl: string) => {
const servers = await getAllServers();
for (const server of servers) {
if (server.url !== serverUrl && server.lastActiveAt > 0) {
registerDeviceToken(server.url);
syncAllChannelMembersAndThreads(server.url).then(() => {
dataRetentionCleanup(server.url);
});
autoUpdateTimezone(server.url);
}
}
};
const syncAllChannelMembersAndThreads = async (serverUrl: string) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const config = await getConfig(database);
if (config?.FeatureFlagGraphQL === 'true') {
const error = await graphQLSyncAllChannelMembers(serverUrl);
if (error) {
logDebug('failed graphQL, falling back to rest', error);
restSyncAllChannelMembers(serverUrl);
}
} else {
restSyncAllChannelMembers(serverUrl);
}
};
const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return 'Server database not found';
}
const {database} = operator;
const response = await gqlAllChannels(serverUrl);
if ('error' in response) {
return response.error;
}
if (response.errors) {
return response.errors[0].message;
}
const userId = await getCurrentUserId(database);
const channels = getMemberChannelsFromGQLQuery(response.data);
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
if (channels && memberships) {
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
const models = (await Promise.all(modelPromises)).flat();
if (models.length) {
await operator.batchRecords(models, 'graphQLSyncAllChannelMembers');
}
}
const isCRTEnabled = await getIsCRTEnabled(database);
if (isCRTEnabled) {
const myTeams = await queryMyTeams(operator.database).fetch();
for await (const myTeam of myTeams) {
// need to await here since GM/DM threads in different teams overlap
await syncTeamThreads(serverUrl, myTeam.id);
}
}
return '';
};
const restSyncAllChannelMembers = async (serverUrl: string) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
return;
}
try {
const myTeams = await client.getMyTeams();
const preferences = await client.getMyPreferences();
const config = await client.getClientConfigOld();
let excludeDirect = false;
for await (const myTeam of myTeams) {
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
excludeDirect = true;
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
// need to await here since GM/DM threads in different teams overlap
await syncTeamThreads(serverUrl, myTeam.id);
}
}
} catch {
// Do nothing
}
};
export async function verifyPushProxy(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
@@ -537,7 +437,14 @@ export async function handleEntryAfterLoadNavigation(
const isThreadsMounted = mountedScreens.includes(Screens.THREAD);
const tabletDevice = await isTablet();
if (currentTeamIdAfterLoad !== currentTeamId) {
if (!currentTeamIdAfterLoad) {
// First load or no team
if (tabletDevice) {
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
} else {
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
}
} else if (currentTeamIdAfterLoad !== currentTeamId) {
// Switched teams while loading
if (!teamMembers.find((t) => t.team_id === currentTeamIdAfterLoad && t.delete_at === 0)) {
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);

View File

@@ -1,81 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {switchToChannelById} from '@actions/remote/channel';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {deferredAppEntryActions, entry} from './gql_common';
import type {Client} from '@client/rest';
import {getServerCredentials} from '@init/credentials';
import WebsocketManager from '@managers/websocket_manager';
type AfterLoginArgs = {
serverUrl: string;
user: UserProfile;
deviceToken?: string;
}
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> {
const dt = Date.now();
export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?: any}> {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
if (deviceToken) {
try {
client.attachDevice(deviceToken);
} catch {
// do nothing, the token could've failed to attach to the session but is not a blocker
}
}
try {
const clData = await fetchConfigAndLicense(serverUrl, false);
if (clData.error) {
return {error: clData.error};
}
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, '', '');
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
return {error: entryData.error};
const credentials = await getServerCredentials(serverUrl);
if (credentials?.token) {
WebsocketManager.createClient(serverUrl, credentials.token);
await WebsocketManager.initializeClient(serverUrl);
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
const isTabletDevice = await isTablet();
let switchToChannel = false;
if (initialChannelId && isTabletDevice) {
switchToChannel = true;
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
} else {
setCurrentTeamAndChannelId(operator, initialTeamId, '');
}
await operator.batchRecords(models, 'loginEntry');
setTeamLoading(serverUrl, false);
const config = clData.config || {} as ClientConfig;
const license = clData.license || {} as ClientLicense;
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
return {};
} catch (error) {
return {error};
}

View File

@@ -1,47 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {switchToChannelById} from '@actions/remote/channel';
import {fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
import {fetchPostById} from '@actions/remote/post';
import {fetchMyTeam} from '@actions/remote/team';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {Screens} from '@constants';
import {getDefaultThemeByAppearance} from '@context/theme';
import DatabaseManager from '@database/manager';
import WebsocketManager from '@managers/websocket_manager';
import {getMyChannel} from '@queries/servers/channel';
import {getPostById} from '@queries/servers/post';
import {queryThemePreferences} from '@queries/servers/preference';
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getCurrentTeamId} from '@queries/servers/system';
import {getMyTeamById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {emitNotificationError} from '@utils/notification';
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
import {syncOtherServers} from './common';
import {deferredAppEntryActions, entry} from './gql_common';
import type ClientError from '@client/rest/error';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type MyTeamModel from '@typings/database/models/servers/my_team';
import type PostModel from '@typings/database/models/servers/post';
export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) {
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
// We only reach this point if we have a channel Id in the notification payload
const channelId = notification.payload!.channel_id!;
const rootId = notification.payload!.root_id!;
const channelId = notification.channel_id!;
const rootId = notification.root_id!;
const {database} = operator;
const currentTeamId = await getCurrentTeamId(database);
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
let isDirectChannel = false;
let teamId = notification.payload?.team_id;
let teamId = notification.team_id;
if (!teamId) {
// If the notification payload does not have a teamId we assume is a DM/GM
isDirectChannel = true;
teamId = currentTeamId;
}
@@ -61,91 +58,62 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
updateThemeIfNeeded(theme, true);
}
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
// To make the switch faster we determine if we already have the team & channel
const myChannel = await getMyChannel(database, channelId);
const myTeam = await getMyTeamById(database, teamId);
let myChannel: MyChannelModel | ChannelMembership | undefined = await getMyChannel(database, channelId);
let myTeam: MyTeamModel | TeamMembership | undefined = await getMyTeamById(database, teamId);
if (!myTeam) {
const resp = await fetchMyTeam(serverUrl, teamId);
if (resp.error) {
if ((resp.error as ClientError).status_code === 403) {
emitNotificationError('Team');
} else {
emitNotificationError('Connection');
}
} else {
myTeam = resp.memberships?.[0];
}
}
if (!myChannel) {
const resp = await fetchMyChannel(serverUrl, teamId, channelId);
if (resp.error) {
if ((resp.error as ClientError).status_code === 403) {
emitNotificationError('Channel');
} else {
emitNotificationError('Connection');
}
} else {
myChannel = resp.memberships?.[0];
}
}
const isCRTEnabled = await getIsCRTEnabled(database);
const isThreadNotification = isCRTEnabled && Boolean(rootId);
let switchedToScreen = false;
let switchedToChannel = false;
if (myChannel && myTeam) {
if (isThreadNotification) {
await fetchAndSwitchToThread(serverUrl, rootId, true);
} else {
switchedToChannel = true;
await switchToChannelById(serverUrl, channelId, teamId);
}
switchedToScreen = true;
}
let post: PostModel | Post | undefined = await getPostById(database, rootId);
if (!post) {
const resp = await fetchPostById(serverUrl, rootId);
post = resp.post;
}
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, teamId, channelId);
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
return {error: entryData.error};
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
const actualRootId = post && ('root_id' in post ? post.root_id : post.rootId);
// There is a chance that after the above request returns
// the user is no longer part of the team or channel
// that triggered the notification (rare but possible)
let selectedTeamId = teamId;
let selectedChannelId = channelId;
if (initialTeamId !== teamId) {
// We are no longer a part of the team that the notification belongs to
// Immediately set the new team as the current team in the database so that the UI
// renders the correct team.
selectedTeamId = initialTeamId;
if (!isDirectChannel) {
selectedChannelId = initialChannelId;
}
}
if (!switchedToScreen) {
const isTabletDevice = await isTablet();
if (isTabletDevice || (channelId === selectedChannelId)) {
// Make switch again to get the missing data and make sure the team is the correct one
switchedToScreen = true;
if (isThreadNotification) {
if (actualRootId) {
await fetchAndSwitchToThread(serverUrl, actualRootId, true);
} else if (post) {
await fetchAndSwitchToThread(serverUrl, rootId, true);
} else {
switchedToChannel = true;
await switchToChannelById(serverUrl, channelId, teamId);
emitNotificationError('Post');
}
} else if (teamId !== selectedTeamId || channelId !== selectedChannelId) {
// If in the end the selected team or channel is different than the one from the notification
// we switch again
await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
} else {
await switchToChannelById(serverUrl, channelId, teamId);
}
}
if (teamId !== selectedTeamId) {
emitNotificationError('Team');
} else if (channelId !== selectedChannelId) {
emitNotificationError('Channel');
}
WebsocketManager.openAll();
// Waiting for the screen to display fixes a race condition when fetching and storing data
if (switchedToChannel) {
await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL);
} else if (switchedToScreen && isThreadNotification) {
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
}
await operator.batchRecords(models, 'pushNotificationEntry');
setTeamLoading(serverUrl, false);
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
const config = await getConfig(database);
const license = await getLicense(database);
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
syncOtherServers(serverUrl);
return {userId: currentUserId};
return {};
}

View File

@@ -137,7 +137,7 @@ export const backgroundNotification = async (serverUrl: string, notification: No
serverUrl, teamId,
channel ? [channel] : [],
myChannel ? [myChannel] : [],
true, isCRTEnabled,
false, isCRTEnabled,
);
if (data.categoryChannels?.length && channel) {

View File

@@ -7,16 +7,14 @@ import {DeviceEventEmitter, Platform} from 'react-native';
import {Database, Events} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getServerCredentials} from '@init/credentials';
import PushNotifications from '@init/push_notifications';
import NetworkManager from '@managers/network_manager';
import WebsocketManager from '@managers/websocket_manager';
import {getDeviceToken} from '@queries/app/global';
import {getServerDisplayName} from '@queries/app/servers';
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUserId, getExpiredSession} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {logWarning, logError} from '@utils/log';
import {logWarning, logError, logDebug} from '@utils/log';
import {scheduleExpiredNotification} from '@utils/notification';
import {getCSRFFromCookie} from '@utils/security';
@@ -27,47 +25,25 @@ import type {LoginArgs} from '@typings/database/database';
const HTTP_UNAUTHORIZED = 401;
export const completeLogin = async (serverUrl: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) => {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(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});
}
if (systems.length) {
await operator.handleSystem({systems, prepareRecordsOnly: false});
}
} catch (error) {
logDebug('error setting the push proxy verification state on login', error);
}
const {database} = operator;
const license = await getLicense(database);
const config = await getConfig(database);
if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) {
return null;
}
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);
systems.push({
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
value: 0,
});
}
if (systems.length) {
operator.handleSystem({systems, prepareRecordsOnly: false});
}
return null;
};
export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientErrorProps) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
@@ -151,11 +127,12 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
}
try {
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
completeLogin(serverUrl);
return {error: error as ClientError, failed: false, hasTeams, time};
await addPushProxyVerificationStateFromLogin(serverUrl);
const {error} = await loginEntry({serverUrl});
await DatabaseManager.setActiveServerDatabase(serverUrl);
return {error: error as ClientError, failed: false};
} catch (error) {
return {error: error as ClientError, failed: false, time: 0};
return {error: error as ClientError, failed: false};
}
};
@@ -251,7 +228,6 @@ export const sendPasswordResetEmail = async (serverUrl: string, email: string) =
};
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
let deviceToken;
let user;
const database = DatabaseManager.appDatabase?.database;
@@ -279,7 +255,6 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
displayName: serverDisplayName,
},
});
deviceToken = await getDeviceToken();
user = await client.getMe();
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
await server?.operator.handleSystem({
@@ -294,11 +269,12 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
}
try {
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
completeLogin(serverUrl);
return {error: error as ClientError, failed: false, hasTeams, time};
await addPushProxyVerificationStateFromLogin(serverUrl);
const {error} = await loginEntry({serverUrl});
await DatabaseManager.setActiveServerDatabase(serverUrl);
return {error: error as ClientError, failed: false};
} catch (error) {
return {error: error as ClientError, failed: false, time: 0};
return {error: error as ClientError, failed: false};
}
};

View File

@@ -2,11 +2,12 @@
// See LICENSE.txt for license information.
import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {markChannelAsRead} from '@actions/remote/channel';
import {handleEntryAfterLoadNavigation} from '@actions/remote/entry/common';
import {handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common';
import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {fetchStatusByIds} from '@actions/remote/user';
import {autoUpdateTimezone} from '@actions/remote/user';
import {loadConfigAndCalls} from '@calls/actions/calls';
import {
handleCallChannelDisabled,
@@ -32,17 +33,15 @@ import {Screens, WebsocketEvents} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import AppsManager from '@managers/apps_manager';
import {getCurrentChannel} from '@queries/servers/channel';
import {getLastPostInThread} from '@queries/servers/post';
import {
getConfig,
getCurrentChannelId,
getCurrentUserId,
getCurrentTeamId,
getLicense,
getWebSocketLastDisconnected,
resetWebSocketLastDisconnected,
} from '@queries/servers/system';
import {getCurrentTeam} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
@@ -72,36 +71,14 @@ import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent,
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
// ESR: 5.37
const alreadyConnected = new Set<string>();
export async function handleFirstConnect(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const config = await getConfig(database);
const lastDisconnect = await getWebSocketLastDisconnected(database);
// ESR: 5.37
if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) {
await handleReconnect(serverUrl);
return;
}
alreadyConnected.add(serverUrl);
resetWebSocketLastDisconnected(operator);
fetchStatusByIds(serverUrl, ['me']);
if (isSupportedServerCalls(config?.Version)) {
const currentUserId = await getCurrentUserId(database);
loadConfigAndCalls(serverUrl, currentUserId);
}
registerDeviceToken(serverUrl);
autoUpdateTimezone(serverUrl);
return doReconnect(serverUrl);
}
export async function handleReconnect(serverUrl: string) {
await doReconnect(serverUrl);
return doReconnect(serverUrl);
}
export async function handleClose(serverUrl: string, lastDisconnect: number) {
@@ -123,12 +100,12 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) {
async function doReconnect(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
return new Error('cannot find server database');
}
const appDatabase = DatabaseManager.appDatabase?.database;
if (!appDatabase) {
return;
return new Error('cannot find app database');
}
const {database} = operator;
@@ -136,21 +113,23 @@ async function doReconnect(serverUrl: string) {
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
resetWebSocketLastDisconnected(operator);
const currentTeam = await getCurrentTeam(database);
const currentChannel = await getCurrentChannel(database);
const currentTeamId = await getCurrentTeamId(database);
const currentChannelId = await getCurrentChannelId(database);
setTeamLoading(serverUrl, true);
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastDisconnectedAt);
if ('error' in entryData) {
setTeamLoading(serverUrl, false);
return;
return entryData.error;
}
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId);
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models, 'doReconnect');
if (models?.length) {
await operator.batchRecords(models, 'doReconnect');
}
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);
@@ -166,7 +145,10 @@ async function doReconnect(serverUrl: string) {
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
dataRetentionCleanup(serverUrl);
AppsManager.refreshAppBindings(serverUrl);
return undefined;
}
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {

View File

@@ -2,8 +2,8 @@
// See LICENSE.txt for license information.
import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread';
import {getCurrentTeamId} from '@app/queries/servers/system';
import DatabaseManager from '@database/manager';
import {getCurrentTeamId} from '@queries/servers/system';
import EphemeralStore from '@store/ephemeral_store';
export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {

View File

@@ -7,16 +7,18 @@ import {Platform} from 'react-native';
import {WebsocketEvents} from '@constants';
import DatabaseManager from '@database/manager';
import {getConfig} from '@queries/servers/system';
import {hasReliableWebsocket} from '@utils/config';
import {toMilliseconds} from '@utils/datetime';
import {logError, logInfo, logWarning} from '@utils/log';
const MAX_WEBSOCKET_FAILS = 7;
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
const WEBSOCKET_TIMEOUT = toMilliseconds({seconds: 30});
const MIN_WEBSOCKET_RETRY_TIME = toMilliseconds({seconds: 3});
const MAX_WEBSOCKET_RETRY_TIME = toMilliseconds({minutes: 5});
export default class WebSocketClient {
private conn?: WebSocketClientInterface;
private connectionTimeout: any;
private connectionTimeout: NodeJS.Timeout | undefined;
private connectionId: string;
private token: string;
@@ -43,6 +45,7 @@ export default class WebSocketClient {
private url = '';
private serverUrl: string;
private hasReliablyReconnect = false;
constructor(serverUrl: string, token: string, lastDisconnect = 0) {
this.connectionId = '';
@@ -98,7 +101,7 @@ export default class WebSocketClient {
this.url = connectionUrl;
const reliableWebSockets = config.EnableReliableWebSockets === 'true';
const reliableWebSockets = hasReliableWebsocket(config);
if (reliableWebSockets) {
// Add connection id, and last_sequence_number to the query param.
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
@@ -125,7 +128,7 @@ export default class WebSocketClient {
// iOS is using he underlying cookieJar
headers.Authorization = `Bearer ${this.token}`;
}
const {client} = await getOrCreateWebSocketClient(this.url, {headers});
const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT});
this.conn = client;
} catch (error) {
return;
@@ -154,6 +157,7 @@ export default class WebSocketClient {
if (this.serverSequence && this.missedEventsCallback) {
this.missedEventsCallback();
}
this.hasReliablyReconnect = true;
}
} else if (this.firstConnectCallback) {
logInfo('websocket connected to', this.url);
@@ -171,6 +175,7 @@ export default class WebSocketClient {
this.conn = undefined;
this.responseSequence = 1;
this.hasReliablyReconnect = false;
if (this.connectFailCount === 0) {
logInfo('websocket closed', this.url);
@@ -203,7 +208,9 @@ export default class WebSocketClient {
this.connectionTimeout = setTimeout(
() => {
if (this.stop) {
clearTimeout(this.connectionTimeout);
if (this.connectionTimeout) {
clearTimeout(this.connectionTimeout);
}
return;
}
this.initialize(opts);
@@ -214,6 +221,7 @@ export default class WebSocketClient {
this.conn!.onError((evt: any) => {
if (evt.url === this.url) {
this.hasReliablyReconnect = false;
if (this.connectFailCount <= 1) {
logError('websocket error', this.url);
logError('WEBSOCKET ERROR EVENT', evt);
@@ -243,11 +251,17 @@ export default class WebSocketClient {
// If we already have a connectionId present, and server sends a different one,
// that means it's either a long timeout, or server restart, or sequence number is not found.
// If the server is not available the first time we try to connect, we won't have a connection id
// but still we need to sync.
// Then we do the sync calls, and reset sequence number to 0.
if (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) {
logInfo(this.url, 'long timeout, or server restart, or sequence number is not found.');
if (
(this.connectionId !== '' && this.connectionId !== msg.data.connection_id) ||
(this.hasReliablyReconnect && this.connectionId === '')
) {
logInfo(this.url, 'long timeout, or server restart, or sequence number is not found, or first connect after failure.');
this.reconnectCallback();
this.serverSequence = 0;
this.hasReliablyReconnect = false;
}
// If it's a fresh connection, we have to set the connectionId regardless.
@@ -315,6 +329,7 @@ export default class WebSocketClient {
this.stop = stop;
this.connectFailCount = 0;
this.responseSequence = 1;
this.hasReliablyReconnect = false;
if (this.conn && this.conn.readyState === WebSocketReadyState.OPEN) {
this.conn.close();

View File

@@ -4,12 +4,12 @@
import React, {useCallback, useMemo} from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
import {useEmojiSkinTone} from '@app/hooks/emoji_category_bar';
import {preventDoubleTap} from '@app/utils/tap';
import Emoji from '@components/emoji';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useEmojiSkinTone} from '@hooks/emoji_category_bar';
import {skinCodes} from '@utils/emoji';
import {isValidNamedEmoji} from '@utils/emoji/helpers';
import {preventDoubleTap} from '@utils/tap';
type Props = {
name: string;

View File

@@ -52,6 +52,6 @@ export async function start() {
registerNavigationListeners();
registerScreens();
await initialLaunch();
WebsocketManager.init(serverCredentials);
await WebsocketManager.init(serverCredentials);
initialLaunch();
}

View File

@@ -27,8 +27,7 @@ const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, Pu
export const initialLaunch = async () => {
const deepLinkUrl = await Linking.getInitialURL();
if (deepLinkUrl) {
await launchAppFromDeepLink(deepLinkUrl, true);
return;
return launchAppFromDeepLink(deepLinkUrl, true);
}
const notification = await Notifications.getInitialNotification();
@@ -43,11 +42,10 @@ export const initialLaunch = async () => {
tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null;
}
if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) {
await launchAppFromNotification(convertToNotificationData(notification!), true);
return;
return launchAppFromNotification(convertToNotificationData(notification!), true);
}
await launchApp({launchType: Launch.Normal, coldStart: true});
return launchApp({launchType: Launch.Normal, coldStart: notification ? tapped : true});
};
const launchAppFromDeepLink = async (deepLinkUrl: string, coldStart = false) => {
@@ -162,14 +160,17 @@ const launchToHome = async (props: LaunchProps) => {
const extra = props.extra as NotificationWithData;
openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
if (openPushNotification) {
pushNotificationEntry(props.serverUrl!, extra);
} else {
appEntry(props.serverUrl!);
await resetToHome(props);
return pushNotificationEntry(props.serverUrl!, extra.payload!);
}
appEntry(props.serverUrl!);
break;
}
case Launch.Normal:
appEntry(props.serverUrl!);
if (props.coldStart) {
appEntry(props.serverUrl!);
}
break;
}
@@ -202,16 +203,21 @@ export const getLaunchPropsFromNotification = async (notification: NotificationW
const {payload} = notification;
launchProps.extra = notification;
let serverUrl: string | undefined;
if (payload?.server_url) {
launchProps.serverUrl = payload.server_url;
} else if (payload?.server_id) {
const serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id);
if (serverUrl) {
launchProps.serverUrl = serverUrl;
} else {
launchProps.launchError = true;
try {
if (payload?.server_url) {
DatabaseManager.getServerDatabaseAndOperator(payload.server_url);
serverUrl = payload.server_url;
} else if (payload?.server_id) {
serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id);
}
} catch {
launchProps.launchError = true;
}
if (serverUrl) {
launchProps.serverUrl = serverUrl;
} else {
launchProps.launchError = true;
}

View File

@@ -33,6 +33,7 @@ class WebsocketManager {
private previousActiveState: boolean;
private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {};
private backgroundIntervalId: number | undefined;
private firstConnectionSynced: Record<string, boolean> = {};
constructor() {
this.previousActiveState = AppState.currentState === 'active';
@@ -40,21 +41,15 @@ class WebsocketManager {
public init = async (serverCredentials: ServerCredential[]) => {
this.netConnected = Boolean((await NetInfo.fetch()).isConnected);
await Promise.all(
serverCredentials.map(
async ({serverUrl, token}) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
try {
this.createClient(serverUrl, token, 0);
} catch (error) {
logError('WebsocketManager init error', error);
}
},
),
serverCredentials.forEach(
({serverUrl, token}) => {
try {
DatabaseManager.getServerDatabaseAndOperator(serverUrl);
this.createClient(serverUrl, token, 0);
} catch (error) {
logError('WebsocketManager init error', error);
}
},
);
AppState.addEventListener('change', this.onAppStateChange);
@@ -68,6 +63,7 @@ class WebsocketManager {
this.connectionTimerIDs[serverUrl].cancel();
}
delete this.clients[serverUrl];
delete this.firstConnectionSynced[serverUrl];
this.getConnectedSubject(serverUrl).next('not_connected');
delete this.connectedSubjects[serverUrl];
@@ -84,9 +80,6 @@ class WebsocketManager {
client.setReliableReconnectCallback(() => this.onReliableReconnect(serverUrl));
client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect));
if (this.netConnected && ['unknown', 'active'].includes(AppState.currentState)) {
client.initialize();
}
this.clients[serverUrl] = client;
return this.clients[serverUrl];
@@ -143,25 +136,34 @@ class WebsocketManager {
}
};
private initializeClient = (serverUrl: string) => {
public initializeClient = async (serverUrl: string) => {
const client: WebSocketClient = this.clients[serverUrl];
if (!client?.isConnected()) {
client.initialize();
}
this.connectionTimerIDs[serverUrl]?.cancel();
delete this.connectionTimerIDs[serverUrl];
if (!client?.isConnected()) {
client.initialize();
if (!this.firstConnectionSynced[serverUrl]) {
const error = await handleFirstConnect(serverUrl);
if (error) {
client.close(false);
}
this.firstConnectionSynced[serverUrl] = true;
}
}
};
private onFirstConnect = (serverUrl: string) => {
this.startPeriodicStatusUpdates(serverUrl);
this.getConnectedSubject(serverUrl).next('connected');
handleFirstConnect(serverUrl);
};
private onReconnect = async (serverUrl: string) => {
this.startPeriodicStatusUpdates(serverUrl);
this.getConnectedSubject(serverUrl).next('connected');
await handleReconnect(serverUrl);
const error = await handleReconnect(serverUrl);
if (error) {
this.getClient(serverUrl)?.close(false);
}
};
private onReliableReconnect = async (serverUrl: string) => {

View File

@@ -18,7 +18,6 @@ import {Navigation} from 'react-native-navigation';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {RTCView} from 'react-native-webrtc';
import {appEntry} from '@actions/remote/entry';
import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions';
import {startCallRecording, stopCallRecording} from '@calls/actions/calls';
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
@@ -41,6 +40,7 @@ import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import WebsocketManager from '@managers/websocket_manager';
import {
bottomSheet,
dismissAllModalsAndPopToScreen,
@@ -357,7 +357,7 @@ const CallScreen = ({
await popTopScreen(Screens.THREAD);
}
await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl);
await appEntry(currentCall.serverUrl, Date.now());
WebsocketManager.initializeClient(currentCall.serverUrl);
await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]);

View File

@@ -5,11 +5,11 @@ import BottomSheetM, {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheet
import React, {ReactNode, useCallback, useEffect, useMemo, useRef} from 'react';
import {DeviceEventEmitter, Handle, InteractionManager, Keyboard, StyleProp, View, ViewStyle} from 'react-native';
import useNavButtonPressed from '@app/hooks/navigation_button_pressed';
import {Events} from '@constants';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {dismissModal} from '@screens/navigation';
import {hapticFeedback} from '@utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';

View File

@@ -6,7 +6,6 @@ import {useIntl} from 'react-intl';
import {Keyboard, Platform, Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {bottomSheetSnapPoint} from '@app/utils/helpers';
import {CHANNEL_ACTIONS_OPTIONS_HEIGHT} from '@components/channel_actions/channel_actions';
import CompassIcon from '@components/compass_icon';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
@@ -19,6 +18,7 @@ import {useIsTablet} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
import {bottomSheet, popTopScreen, showModal} from '@screens/navigation';
import {isTypeDMorGM} from '@utils/channel';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';

View File

@@ -32,7 +32,6 @@ type ChannelProps = {
channelsCount: number;
isCRTEnabled: boolean;
teamsCount: number;
time?: number;
isLicensed: boolean;
showToS: boolean;
launchType: LaunchType;

View File

@@ -9,7 +9,6 @@ import Swipeable from 'react-native-gesture-handler/Swipeable';
import {Navigation} from 'react-native-navigation';
import {storeMultiServerTutorial} from '@actions/app/global';
import {appEntry} from '@actions/remote/entry';
import {doPing} from '@actions/remote/general';
import {logout} from '@actions/remote/session';
import {fetchConfigAndLicense} from '@actions/remote/systems';
@@ -24,6 +23,7 @@ import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads';
import {useIsTablet} from '@hooks/device';
import WebsocketManager from '@managers/websocket_manager';
import {getServerByIdentifier} from '@queries/app/servers';
import {dismissBottomSheet} from '@screens/navigation';
import {canReceiveNotifications} from '@utils/push_proxy';
@@ -296,7 +296,7 @@ const ServerItem = ({
await dismissBottomSheet();
Navigation.updateProps(Screens.HOME, {extra: undefined});
DatabaseManager.setActiveServerDatabase(server.url);
await appEntry(server.url, Date.now());
WebsocketManager.initializeClient(server.url);
return;
}

View File

@@ -36,7 +36,6 @@ enableFreeze(true);
type HomeProps = LaunchProps & {
componentId: string;
time?: number;
};
const Tab = createBottomTabNavigator();
@@ -46,7 +45,7 @@ export default function HomeScreen(props: HomeProps) {
const intl = useIntl();
useEffect(() => {
const listener = DeviceEventEmitter.addListener(Events.NOTIFICATION_ERROR, (value: 'Team' | 'Channel') => {
const listener = DeviceEventEmitter.addListener(Events.NOTIFICATION_ERROR, (value: 'Team' | 'Channel' | 'Post' | 'Connection') => {
notificationError(intl, value);
});

View File

@@ -8,14 +8,14 @@ import {Keyboard, TextInput, TouchableOpacity, View} from 'react-native';
import Button from 'react-native-button';
import {login} from '@actions/remote/session';
import CompassIcon from '@app/components/compass_icon';
import ClientError from '@client/rest/error';
import CompassIcon from '@components/compass_icon';
import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
import {t} from '@i18n';
import {goToScreen, loginAnimationOptions, resetToHome, resetToTeams} from '@screens/navigation';
import {goToScreen, loginAnimationOptions, resetToHome} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {isServerError} from '@utils/errors';
import {preventDoubleTap} from '@utils/tap';
@@ -99,17 +99,13 @@ const LoginForm = ({config, extra, serverDisplayName, launchError, launchType, l
const signIn = async () => {
const result: LoginActionResponse = await login(serverUrl!, {serverDisplayName, loginId: loginId.toLowerCase(), password, config, license});
if (checkLoginResponse(result)) {
if (!result.hasTeams && !result.error) {
resetToTeams();
return;
}
goToHome(result.time || 0, result.error as never);
goToHome(result.error as never);
}
};
const goToHome = (time: number, loginError?: never) => {
const goToHome = (loginError?: never) => {
const hasError = launchError || Boolean(loginError);
resetToHome({extra, launchError: hasError, launchType, serverUrl, time});
resetToHome({extra, launchError: hasError, launchType, serverUrl});
};
const checkLoginResponse = (data: LoginActionResponse) => {

View File

@@ -19,7 +19,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import Background from '@screens/background';
import {popTopScreen, resetToTeams} from '@screens/navigation';
import {popTopScreen} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -32,7 +32,7 @@ import type {AvailableScreens} from '@typings/screens/navigation';
type MFAProps = {
componentId: AvailableScreens;
config: Partial<ClientConfig>;
goToHome: (time: number, error?: never) => void;
goToHome: (error?: never) => void;
license: Partial<ClientLicense>;
loginId: string;
password: string;
@@ -156,11 +156,7 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
setError(result.error.message);
return;
}
if (!result.hasTeams && !result.error) {
resetToTeams();
return;
}
goToHome(result.time || 0, result.error as never);
goToHome(result.error as never);
}), [token]);
const transform = useAnimatedStyle(() => {

View File

@@ -15,7 +15,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import NetworkManager from '@managers/network_manager';
import Background from '@screens/background';
import {dismissModal, popTopScreen, resetToHome, resetToTeams} from '@screens/navigation';
import {dismissModal, popTopScreen, resetToHome} from '@screens/navigation';
import {logWarning} from '@utils/log';
import SSOWithRedirectURL from './sso_with_redirect_url';
@@ -105,16 +105,12 @@ const SSO = ({
onLoadEndError(result.error);
return;
}
if (!result.hasTeams && !result.error) {
resetToTeams();
return;
}
goToHome(result.time || 0, result.error as never);
goToHome(result.error as never);
};
const goToHome = (time: number, error?: never) => {
const goToHome = (error?: never) => {
const hasError = launchError || Boolean(error);
resetToHome({extra, launchError: hasError, launchType, serverUrl, time});
resetToHome({extra, launchError: hasError, launchType, serverUrl});
};
const dismiss = () => {

12
app/utils/config.ts Normal file
View File

@@ -0,0 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {isMinimumServerVersion} from './helpers';
export function hasReliableWebsocket(config: ClientConfig) {
if (isMinimumServerVersion(config.Version, 6, 5)) {
return true;
}
return config.EnableReliableWebSockets === 'true';
}

View File

@@ -5,13 +5,13 @@ import {createIntl, IntlShape} from 'react-intl';
import urlParse from 'url-parse';
import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel';
import {appEntry} from '@actions/remote/entry';
import {showPermalink} from '@actions/remote/permalink';
import {fetchUsersByUsernames} from '@actions/remote/user';
import {DeepLink, Launch, Screens} from '@constants';
import {getDefaultThemeByAppearance} from '@context/theme';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
import WebsocketManager from '@managers/websocket_manager';
import {getActiveServerUrl} from '@queries/app/servers';
import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
@@ -48,7 +48,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
if (existingServerUrl !== currentServerUrl && NavigationStore.getVisibleScreen()) {
await dismissAllModalsAndPopToRoot();
DatabaseManager.setActiveServerDatabase(existingServerUrl);
appEntry(existingServerUrl, Date.now());
WebsocketManager.initializeClient(existingServerUrl);
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
}

View File

@@ -47,7 +47,7 @@ export const convertToNotificationData = (notification: Notification, tapped = t
return notificationData;
};
export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel') => {
export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel' | 'Connection' | 'Post') => {
const title = intl.formatMessage({id: 'notification.message_not_found', defaultMessage: 'Message not found'});
let message;
switch (type) {
@@ -63,13 +63,25 @@ export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel') =>
defaultMessage: 'This message belongs to a team where you are not a member.',
});
break;
case 'Post':
message = intl.formatMessage({
id: 'notification.no_post',
defaultMessage: 'The message has not been found.',
});
break;
case 'Connection':
message = intl.formatMessage({
id: 'notification.no_connection',
defaultMessage: 'The server is unreachable and we were not able to retrieve the notification channel / team.',
});
break;
}
Alert.alert(title, message);
popToRoot();
};
export const emitNotificationError = (type: 'Team' | 'Channel') => {
export const emitNotificationError = (type: 'Team' | 'Channel' | 'Post' | 'Connection') => {
const req = setTimeout(() => {
DeviceEventEmitter.emit(Events.NOTIFICATION_ERROR, type);
clearTimeout(req);

View File

@@ -340,6 +340,7 @@
"invite.send_invite": "Send",
"invite.sendInvitationsTo": "Send invitations to…",
"invite.shareLink": "Share link",
"invite.summary.back": "Go back",
"invite.summary.done": "Done",
"invite.summary.email_invite": "An invitation email has been sent",
"invite.summary.error": "{invitationsCount, plural, one {Invitation} other {Invitations}} could not be sent successfully",
@@ -535,7 +536,6 @@
"mobile.login_options.saml": "SAML",
"mobile.login_options.select_option": "Select a login option below.",
"mobile.login_options.separator_text": "or log in with",
"mobile.login_options.sso_continue": "Continue with",
"mobile.manage_members.admin": "Admin",
"mobile.manage_members.cancel": "Cancel",
"mobile.manage_members.change_role.error": "An error occurred while trying to update the role. Please check your connection and try again.",
@@ -726,6 +726,8 @@
"notification_settings.threads_start": "Threads that I start",
"notification_settings.threads_start_participate": "Threads that I start or participate in",
"notification.message_not_found": "Message not found",
"notification.no_connection": "The server is unreachable and we were not able to retrieve the notification channel / team.",
"notification.no_post": "The message has not been found.",
"notification.not_channel_member": "This message belongs to a channel where you are not a member.",
"notification.not_team_member": "This message belongs to a team where you are not a member.",
"onboarding.calls": "Start secure audio calls instantly",

View File

@@ -57,5 +57,5 @@ if (Platform.OS === 'android') {
Navigation.events().registerAppLaunchedListener(async () => {
await initialize();
await start();
start();
});

View File

@@ -15,7 +15,5 @@ interface Session {
interface LoginActionResponse {
error?: ClientErrorProps | Error | string;
hasTeams?: boolean;
failed: boolean;
time?: number;
}

View File

@@ -44,6 +44,5 @@ export interface LaunchProps {
launchError?: Boolean;
serverUrl?: string;
displayName?: string;
time?: number;
coldStart?: boolean;
}