forked from Ivasoft/mattermost-mobile
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:
committed by
GitHub
parent
98f25046af
commit
9f84ab79ce
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -52,6 +52,6 @@ export async function start() {
|
||||
|
||||
registerNavigationListeners();
|
||||
registerScreens();
|
||||
await initialLaunch();
|
||||
WebsocketManager.init(serverCredentials);
|
||||
await WebsocketManager.init(serverCredentials);
|
||||
initialLaunch();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -32,7 +32,6 @@ type ChannelProps = {
|
||||
channelsCount: number;
|
||||
isCRTEnabled: boolean;
|
||||
teamsCount: number;
|
||||
time?: number;
|
||||
isLicensed: boolean;
|
||||
showToS: boolean;
|
||||
launchType: LaunchType;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
12
app/utils/config.ts
Normal 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';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
index.ts
2
index.ts
@@ -57,5 +57,5 @@ if (Platform.OS === 'android') {
|
||||
|
||||
Navigation.events().registerAppLaunchedListener(async () => {
|
||||
await initialize();
|
||||
await start();
|
||||
start();
|
||||
});
|
||||
|
||||
2
types/api/session.d.ts
vendored
2
types/api/session.d.ts
vendored
@@ -15,7 +15,5 @@ interface Session {
|
||||
|
||||
interface LoginActionResponse {
|
||||
error?: ClientErrorProps | Error | string;
|
||||
hasTeams?: boolean;
|
||||
failed: boolean;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,5 @@ export interface LaunchProps {
|
||||
launchError?: Boolean;
|
||||
serverUrl?: string;
|
||||
displayName?: string;
|
||||
time?: number;
|
||||
coldStart?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user