From 3ea065b84531ec6a5f06a4a2909775f076067774 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 9 Feb 2022 12:49:37 -0300 Subject: [PATCH] [Gekidou] Fix & small refactor to the app entry logic and WS reconnection (#5937) * Fix & small refactor to the app entry logic and WS reconnection * Remove not needed log in MainActivity * Replace async forEach with for await * extract getClient to its own block * replace double filter with reduce * fix select channel on WS reconnect and user no longer belongs to current team * feedback review on WS users actions * Add windowFocusChanged for Android only * on WS reconnection set team & channel if not member of current team * reduce suggestion * feedback review --- .../com/mattermost/rnbeta/MainActivity.java | 17 ++ .../rnbeta/MattermostManagedModule.java | 13 + app/actions/local/channel.ts | 37 +-- app/actions/remote/entry/app.ts | 13 +- app/actions/remote/entry/common.ts | 24 +- app/actions/remote/entry/login.ts | 2 +- app/actions/remote/entry/notification.ts | 14 +- app/actions/remote/group.ts | 5 +- app/actions/remote/role.ts | 6 +- app/actions/remote/team.ts | 5 +- app/actions/remote/user.ts | 53 ++-- app/actions/websocket/index.ts | 228 ++++++++++-------- app/actions/websocket/users.ts | 74 +++--- app/client/rest/base.ts | 3 +- app/client/rest/index.test.js | 4 +- app/init/global_event_handler.ts | 7 - app/init/websocket_manager.ts | 12 +- app/queries/servers/system.ts | 18 +- app/utils/supported_server/index.ts | 14 ++ 19 files changed, 349 insertions(+), 200 deletions(-) diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java index 78dd0d1dc7..a186dadd38 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java @@ -2,9 +2,14 @@ package com.mattermost.rnbeta; import android.os.Bundle; import androidx.annotation.Nullable; + import android.view.KeyEvent; import android.content.res.Configuration; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; import com.reactnativenavigation.NavigationActivity; import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule; @@ -29,6 +34,18 @@ public class MainActivity extends NavigationActivity { } } + @Override + // This can be removed once https://github.com/facebook/react-native/issues/32628 is solved + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + MattermostManagedModule instance = MattermostManagedModule.getInstance(); + if (instance != null) { + WritableMap data = Arguments.createMap(); + data.putString("appState", hasFocus ? "active" : "background"); + instance.sendEvent("windowFocusChanged", data); + } + } + /* https://mattermost.atlassian.net/browse/MM-10601 Required by react-native-hw-keyboard-event diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java index b74bde4d7e..74e17f972e 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -3,6 +3,7 @@ package com.mattermost.rnbeta; import android.app.Activity; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -10,19 +11,24 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; public class MattermostManagedModule extends ReactContextBaseJavaModule { private static MattermostManagedModule instance; + private ReactApplicationContext reactContext; private static final String TAG = MattermostManagedModule.class.getSimpleName(); private MattermostManagedModule(ReactApplicationContext reactContext) { super(reactContext); + this.reactContext = reactContext; } public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) { if (instance == null) { instance = new MattermostManagedModule(reactContext); + } else { + instance.reactContext = reactContext; } return instance; @@ -32,6 +38,13 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule { return instance; } + public void sendEvent(String eventName, + @Nullable WritableMap params) { + this.reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); + } + @Override @NonNull public String getName() { diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index f5b1425d6c..fc252bea75 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -15,7 +15,7 @@ import {addChannelToTeamHistory, addTeamToTeamHistory, removeChannelFromTeamHist import {queryCurrentUser} from '@queries/servers/user'; import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation'; import {isTablet} from '@utils/helpers'; -import {displayGroupMessageName, displayUsername} from '@utils/user'; +import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user'; import type ChannelModel from '@typings/database/models/servers/channel'; import type UserModel from '@typings/database/models/servers/user'; @@ -298,29 +298,38 @@ export const updateLastPostAt = async (serverUrl: string, channelId: string, las return {models}; }; -export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], user: UserProfile, prepareRecordsOnly = false) { - const database = DatabaseManager.serverDatabases[serverUrl]; - if (!database) { +export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], users: UserProfile[], prepareRecordsOnly = false) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return {}; } - const currentUser = await queryCurrentUser(database.database); + + const {database} = operator; + const currentUser = await queryCurrentUser(database); if (!currentUser) { return {}; } - const {config, license} = await queryCommonSystemValues(database.database); - const preferences = await queryPreferencesByCategoryAndName(database.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT); + const {config, license} = await queryCommonSystemValues(database); + const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT); const displaySettings = getTeammateNameDisplaySetting(preferences, config, license); const models: Model[] = []; - channels?.forEach(async (channel) => { + for await (const channel of channels) { let newDisplayName = ''; if (channel.type === General.DM_CHANNEL) { + const otherUserId = getUserIdFromChannelName(currentUser.id, channel.name); + const user = users.find((u) => u.id === otherUserId); newDisplayName = displayUsername(user, currentUser.locale, displaySettings); } else { - const dbProfiles = await database.database.get(USER).query(Q.on(CHANNEL_MEMBERSHIP, Q.where('channel_id', channel.id))).fetch(); - const newProfiles: Array = dbProfiles.filter((u) => u.id !== user.id); - newProfiles.push(user); - newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id); + const dbProfiles = await database.get(USER).query(Q.on(CHANNEL_MEMBERSHIP, Q.where('channel_id', channel.id))).fetch(); + const profileIds = dbProfiles.map((p) => p.id); + const gmUsers = users.filter((u) => profileIds.includes(u.id)); + if (gmUsers.length) { + const uIds = gmUsers.map((u) => u.id); + const newProfiles: Array = dbProfiles.filter((u) => !uIds.includes(u.id)); + newProfiles.push(...gmUsers); + newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id); + } } if (channel.displayName !== newDisplayName) { @@ -329,10 +338,10 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha }); models.push(channel); } - }); + } if (models.length && !prepareRecordsOnly) { - database.operator.batchRecords(models); + await operator.batchRecords(models); } return {models}; diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index 1b146ca96c..2f7e62ab43 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -7,7 +7,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; import {queryChannelsById, queryDefaultChannelForTeam} from '@queries/servers/channel'; import {prepareModels} from '@queries/servers/entry'; -import {prepareCommonSystemValues, queryCommonSystemValues, queryCurrentChannelId, queryCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {prepareCommonSystemValues, queryCommonSystemValues, queryCurrentChannelId, queryCurrentTeamId, queryWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {deleteMyTeams, queryTeamsById} from '@queries/servers/team'; import {queryCurrentUser} from '@queries/servers/user'; import {deleteV1Data} from '@utils/file'; @@ -24,7 +24,8 @@ export const appEntry = async (serverUrl: string) => { const tabletDevice = await isTablet(); const currentTeamId = await queryCurrentTeamId(database); - const fetchedData = await fetchAppEntryData(serverUrl, currentTeamId); + const lastDisconnectedAt = await queryWebSocketLastDisconnected(database); + const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, currentTeamId); const fetchedError = (fetchedData as AppEntryError).error; if (fetchedError) { @@ -32,6 +33,7 @@ export const appEntry = async (serverUrl: string) => { } const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData; + const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true); if (initialTeamId === currentTeamId) { let cId = await queryCurrentChannelId(database); @@ -67,14 +69,15 @@ export const appEntry = async (serverUrl: string) => { await deleteMyTeams(operator, removeTeams!); } - fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user); - let removeChannels; if (removeChannelIds?.length) { removeChannels = await queryChannelsById(database, removeChannelIds); } const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}); + if (rolesData.roles?.length) { + modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true})); + } const models = await Promise.all(modelPromises); if (models.length) { await operator.batchRecords(models.flat()); @@ -82,7 +85,7 @@ export const appEntry = async (serverUrl: string) => { const {id: currentUserId, locale: currentUserLocale} = meData.user || (await queryCurrentUser(database))!; const {config, license} = await queryCommonSystemValues(database); - deferredAppEntryActions(serverUrl, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); + deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); const error = teamData.error || chData?.error || prefData.error || meData.error; return {error, userId: meData?.user?.id}; diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 8966f4eced..b64afe7945 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -5,15 +5,16 @@ import {fetchMissingSidebarInfo, fetchMyChannelsForTeam, MyChannelsRequest} from import {fetchGroupsForTeam} from '@actions/remote/group'; import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post'; import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; +import {fetchConfigAndLicense} from '@actions/remote/systems'; import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team'; -import {fetchMe, MyUserRequest} from '@actions/remote/user'; +import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; import {General, Preferences} from '@constants'; import DatabaseManager from '@database/manager'; import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference'; import {selectDefaultTeam} from '@helpers/api/team'; import {DEFAULT_LOCALE} from '@i18n'; import {queryAllChannelsForTeam} from '@queries/servers/channel'; -import {queryConfig, queryWebSocketLastDisconnected} from '@queries/servers/system'; +import {queryConfig} from '@queries/servers/system'; import {queryAvailableTeamIds, queryMyTeams} from '@queries/servers/team'; import type ClientError from '@client/rest/error'; @@ -32,20 +33,21 @@ export type AppEntryError = { error?: Error | ClientError | string; } -export const fetchAppEntryData = async (serverUrl: string, initialTeamId: string): Promise => { +export const fetchAppEntryData = async (serverUrl: string, since: number, initialTeamId: string): Promise => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; } - const lastDisconnected = await queryWebSocketLastDisconnected(database); const includeDeletedChannels = true; const fetchOnly = true; + await fetchConfigAndLicense(serverUrl); + // Fetch in parallel teams / team membership / channels for current team / user preferences / user const promises: [Promise, Promise, Promise, Promise] = [ fetchMyTeams(serverUrl, fetchOnly), - initialTeamId ? fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, lastDisconnected, fetchOnly) : Promise.resolve(undefined), + initialTeamId ? fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly) : Promise.resolve(undefined), fetchMyPreferences(serverUrl, fetchOnly), fetchMe(serverUrl, fetchOnly), ]; @@ -63,7 +65,7 @@ export const fetchAppEntryData = async (serverUrl: string, initialTeamId: string const myTeams = teamData.teams!.filter((t) => teamMembers?.includes(t.id)); const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config.ExperimentalPrimaryTeam); if (defaultTeam?.id) { - chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, lastDisconnected, fetchOnly); + chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, since, fetchOnly); } } @@ -102,7 +104,7 @@ export const fetchAppEntryData = async (serverUrl: string, initialTeamId: string } const availableTeamIds = await queryAvailableTeamIds(database, initialTeamId, teamData.teams, prefData.preferences, meData.user?.locale); - const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, lastDisconnected, fetchOnly); + const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly); data = { ...data, @@ -156,7 +158,7 @@ export const fetchAlternateTeamData = async ( }; export const deferredAppEntryActions = async ( - serverUrl: string, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, + serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, initialTeamId?: string, initialChannelId?: string) => { // defer fetching posts for initial channel @@ -179,11 +181,13 @@ export const deferredAppEntryActions = async ( // defer groups for team if (initialTeamId) { - await fetchGroupsForTeam(serverUrl, initialTeamId); + fetchGroupsForTeam(serverUrl, initialTeamId, since); } // defer fetch channels and unread posts for other teams if (teamData.teams?.length && teamData.memberships?.length) { - fetchTeamsChannelsAndUnreadPosts(serverUrl, teamData.teams, teamData.memberships, initialTeamId); + await fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId); } + + updateAllUsersSince(serverUrl, since); }; diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index b5a76f8e7f..b4c2092d59 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -158,7 +158,7 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) const config = clData.config || {} as ClientConfig; const license = clData.license || {} as ClientLicense; - deferredAppEntryActions(serverUrl, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeam?.id, initialChannel?.id); + deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeam?.id, initialChannel?.id); const error = clData.error || prefData.error || teamData.error || chData?.error; return {error, time: Date.now() - dt, hasTeams: Boolean((myTeams?.length || 0) > 0 && !teamData.error)}; diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index 5e5a61beb3..fe94f8092f 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -10,7 +10,7 @@ import {Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {queryChannelsById, queryDefaultChannelForTeam, queryMyChannel} from '@queries/servers/channel'; import {prepareModels} from '@queries/servers/entry'; -import {queryCommonSystemValues, queryCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {queryCommonSystemValues, queryCurrentTeamId, queryWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {deleteMyTeams, queryMyTeamById, queryTeamsById} from '@queries/servers/team'; import {queryCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; @@ -30,6 +30,7 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not const isTabletDevice = await isTablet(); const {database} = operator; const currentTeamId = await queryCurrentTeamId(database); + const lastDisconnectedAt = await queryWebSocketLastDisconnected(database); const currentServerUrl = await DatabaseManager.getActiveServerUrl(); let isDirectChannel = false; @@ -55,7 +56,7 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not await switchToChannel(serverUrl, channelId, teamId); } - const fetchedData = await fetchAppEntryData(serverUrl, teamId); + const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId); const fetchedError = (fetchedData as AppEntryError).error; if (fetchedError) { @@ -63,6 +64,7 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not } const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData; + const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true); // There is a chance that after the above request returns // the user is no longer part of the team or channel @@ -122,14 +124,16 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not await deleteMyTeams(operator, removeTeams!); } - fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user); - let removeChannels; if (removeChannelIds?.length) { removeChannels = await queryChannelsById(operator.database, removeChannelIds); } const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}); + if (rolesData.roles?.length) { + modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true})); + } + const models = await Promise.all(modelPromises); if (models.length) { await operator.batchRecords(models.flat() as Model[]); @@ -138,7 +142,7 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not const {id: currentUserId, locale: currentUserLocale} = meData.user || (await queryCurrentUser(operator.database))!; const {config, license} = await queryCommonSystemValues(operator.database); - deferredAppEntryActions(serverUrl, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId); + deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId); const error = teamData.error || chData?.error || prefData.error || meData.error; return {error, userId: meData?.user?.id}; }; diff --git a/app/actions/remote/group.ts b/app/actions/remote/group.ts index 17e007c979..6b0436d7d5 100644 --- a/app/actions/remote/group.ts +++ b/app/actions/remote/group.ts @@ -5,12 +5,12 @@ import {Model} from '@nozbe/watermelondb'; import DatabaseManager from '@database/manager'; import NetworkManager from '@init/network_manager'; -import {queryCommonSystemValues, queryWebSocketLastDisconnected} from '@queries/servers/system'; +import {queryCommonSystemValues} from '@queries/servers/system'; import {queryTeamById} from '@queries/servers/team'; import {forceLogoutIfNecessary} from './session'; -export const fetchGroupsForTeam = async (serverUrl: string, teamId: string) => { +export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, since: number) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -59,7 +59,6 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string) => { } } } else { - const since = await queryWebSocketLastDisconnected(database); const [groupsAssociatedToChannelsInTeam, allGroups]: [{groups: Record}, Group[]] = await Promise.all([ client.getAllGroupsAssociatedToChannelsInTeam(teamId, true), client.getGroups(true, 0, 0, since), diff --git a/app/actions/remote/role.ts b/app/actions/remote/role.ts index 37eac702b3..c981fdbdc6 100644 --- a/app/actions/remote/role.ts +++ b/app/actions/remote/role.ts @@ -56,7 +56,7 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string } }; -export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], user?: UserProfile) => { +export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], user?: UserProfile, fetchOnly = false) => { const rolesToFetch = new Set(user?.roles.split(' ') || []); if (teamMembership?.length) { @@ -79,6 +79,8 @@ export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembers } if (rolesToFetch.size > 0) { - fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch)); + return fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), fetchOnly); } + + return {roles: []}; }; diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index a8508f1948..14ef9d59c8 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -9,7 +9,7 @@ import {Events} from '@constants'; import DatabaseManager from '@database/manager'; import NetworkManager from '@init/network_manager'; import {prepareMyChannelsForTeam, queryDefaultChannelForTeam} from '@queries/servers/channel'; -import {prepareCommonSystemValues, queryCurrentTeamId, queryWebSocketLastDisconnected} from '@queries/servers/system'; +import {prepareCommonSystemValues, queryCurrentTeamId} from '@queries/servers/system'; import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, queryNthLastChannelFromTeam, queryTeamsById, syncTeamTable} from '@queries/servers/team'; import {isTablet} from '@utils/helpers'; @@ -191,14 +191,13 @@ export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promi } }; -export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => { +export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since: number, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; } const myTeams = teams.filter((t) => memberships.find((m) => m.team_id === t.id && t.id !== excludeTeamId)); - const since = await queryWebSocketLastDisconnected(database); for await (const team of myTeams) { const {channels, memberships: members} = await fetchMyChannelsForTeam(serverUrl, team.id, since > 0, since, false, true); diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index ccb4d4e55d..6ce09b2aeb 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -3,19 +3,22 @@ import {Model, Q} from '@nozbe/watermelondb'; +import {updateChannelsDisplayName} from '@actions/local/channel'; import {updateRecentCustomStatuses, updateLocalUser} from '@actions/local/user'; import {fetchRolesIfNeeded} from '@actions/remote/role'; import {Database, General} from '@constants'; +import {MM_TABLES} from '@constants/database'; import DatabaseManager from '@database/manager'; import {debounce} from '@helpers/api/general'; import NetworkManager from '@init/network_manager'; -import {queryCurrentUserId, queryWebSocketLastDisconnected} from '@queries/servers/system'; +import {queryCurrentUserId} from '@queries/servers/system'; import {prepareUsers, queryAllUsers, queryCurrentUser, queryUsersById, queryUsersByUsername} from '@queries/servers/user'; import {forceLogoutIfNecessary} from './session'; import type {Client} from '@client/rest'; import type ClientError from '@client/rest/error'; +import type ChannelModel from '@typings/database/models/servers/channel'; import type UserModel from '@typings/database/models/servers/user'; export type MyUserRequest = { @@ -34,6 +37,8 @@ export type ProfilesInChannelRequest = { error?: unknown; } +const {SERVER: {CHANNEL}} = MM_TABLES; + export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise => { let client; try { @@ -332,31 +337,49 @@ export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernam } }; -export const updateAllUsersSinceLastDisconnect = async (serverUrl: string) => { - const database = DatabaseManager.serverDatabases[serverUrl]; - if (!database) { +export const updateAllUsersSince = async (serverUrl: string, since: number, fetchOnly = false) => { + if (!since) { + return {users: []}; + } + + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return {error: `${serverUrl} database not found`}; } - const lastDisconnectedAt = await queryWebSocketLastDisconnected(database.database); - - if (!lastDisconnectedAt) { - return {users: []}; + let client: Client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; } - const currentUserId = await queryCurrentUserId(database.database); - const users = await queryAllUsers(database.database); + + const currentUserId = await queryCurrentUserId(operator.database); + const users = await queryAllUsers(operator.database); const userIds = users.map((u) => u.id).filter((id) => id !== currentUserId); let userUpdates: UserProfile[] = []; try { - userUpdates = await NetworkManager.getClient(serverUrl).getProfilesByIds(userIds, {since: lastDisconnectedAt}); + userUpdates = await client.getProfilesByIds(userIds, {since}); + if (userUpdates.length && !fetchOnly) { + const modelsToBatch: Model[] = []; + const userModels = await operator.handleUsers({users: userUpdates, prepareRecordsOnly: true}); + modelsToBatch.push(...userModels); + const directChannels = await operator.database.get(CHANNEL). + query(Q.where('type', Q.oneOf([General.DM_CHANNEL, General.GM_CHANNEL]))). + fetch(); + const {models} = await updateChannelsDisplayName(serverUrl, directChannels, userUpdates, true); + if (models?.length) { + modelsToBatch.push(...models); + } + + if (modelsToBatch.length) { + await operator.batchRecords(modelsToBatch); + } + } } catch { // Do nothing } - if (userUpdates.length) { - database.operator.handleUsers({users: userUpdates, prepareRecordsOnly: false}); - } - return {users: userUpdates}; }; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index cf037974be..dd129b336b 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -1,23 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {DeviceEventEmitter} from 'react-native'; - -import {fetchMissingSidebarInfo, fetchMyChannelsForTeam} from '@actions/remote/channel'; -import {fetchPostsSince} from '@actions/remote/post'; -import {fetchMyPreferences} from '@actions/remote/preference'; +import {fetchMissingSidebarInfo, switchToChannelById} from '@actions/remote/channel'; +import {AppEntryData, AppEntryError, fetchAppEntryData} from '@actions/remote/entry/common'; +import {fetchGroupsForTeam} from '@actions/remote/group'; +import {fetchPostsForUnreadChannels, fetchPostsSince} from '@actions/remote/post'; import {fetchRoles} from '@actions/remote/role'; import {fetchConfigAndLicense} from '@actions/remote/systems'; -import {fetchAllTeams, fetchMyTeams} from '@actions/remote/team'; -import {fetchMe, updateAllUsersSinceLastDisconnect} from '@actions/remote/user'; +import {fetchAllTeams, fetchTeamsChannelsAndUnreadPosts} from '@actions/remote/team'; +import {updateAllUsersSince} from '@actions/remote/user'; import {General, WebsocketEvents} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; -import Events from '@constants/events'; import DatabaseManager from '@database/manager'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; -import {prepareMyChannelsForTeam} from '@queries/servers/channel'; -import {queryCommonSystemValues, queryConfig, queryWebSocketLastDisconnected} from '@queries/servers/system'; -import {queryCurrentUser} from '@queries/servers/user'; +import {queryChannelsById, queryDefaultChannelForTeam} from '@queries/servers/channel'; +import {prepareModels} from '@queries/servers/entry'; +import {queryCommonSystemValues, queryConfig, queryCurrentChannelId, queryWebSocketLastDisconnected, resetWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {deleteMyTeams, queryTeamsById} from '@queries/servers/team'; +import {isTablet} from '@utils/helpers'; import {handleChannelDeletedEvent, handleUserAddedToChannelEvent, handleUserRemovedFromChannelEvent} from './channel'; import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts'; @@ -27,21 +27,25 @@ import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRole import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent} from './teams'; import {handleUserUpdatedEvent, handleUserTypingEvent} from './users'; -import type {Model} from '@nozbe/watermelondb'; +// ESR: 5.37 +const alreadyConnected = new Set(); export async function handleFirstConnect(serverUrl: string) { - const database = DatabaseManager.serverDatabases[serverUrl]?.database; - if (!database) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return; } - const config = await queryConfig(database); - const lastDisconnect = await queryWebSocketLastDisconnected(database); - if (lastDisconnect && config.EnableReliableWebSockets !== 'true') { - doReconnect(serverUrl); + const config = await queryConfig(operator.database); + const lastDisconnect = await queryWebSocketLastDisconnected(operator.database); + + // ESR: 5.37 + if (lastDisconnect && config.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) { + handleReconnect(serverUrl); return; } - doFirstConnect(serverUrl); + alreadyConnected.add(serverUrl); + resetWebSocketLastDisconnected(operator); } export function handleReconnect(serverUrl: string) { @@ -64,90 +68,126 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) { }); } -function doFirstConnect(serverUrl: string) { - updateAllUsersSinceLastDisconnect(serverUrl); -} - async function doReconnect(serverUrl: string) { - const database = DatabaseManager.serverDatabases[serverUrl]; - if (!database) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return; } - const system = await queryCommonSystemValues(database.database); - const lastDisconnectedAt = await queryWebSocketLastDisconnected(database.database); + const {database} = operator; + const tabletDevice = await isTablet(); + const system = await queryCommonSystemValues(database); + const lastDisconnectedAt = await queryWebSocketLastDisconnected(database); - // TODO consider fetch only and batch all the results. - await fetchMe(serverUrl); - await fetchMyPreferences(serverUrl); - const {config} = await fetchConfigAndLicense(serverUrl); - const {memberships: teamMemberships, error: teamMembershipsError} = await fetchMyTeams(serverUrl); - const {currentChannelId, currentUserId, currentTeamId, license} = system; - const currentTeamMembership = teamMemberships?.find((tm) => tm.team_id === currentTeamId && tm.delete_at === 0); - - let channelMemberships: ChannelMembership[] | undefined; - if (currentTeamMembership) { - const {memberships, channels, error} = await fetchMyChannelsForTeam(serverUrl, currentTeamMembership.team_id, true, lastDisconnectedAt); - if (error) { - DeviceEventEmitter.emit(Events.TEAM_LOAD_ERROR, serverUrl, error); - return; - } - const currentUser = await queryCurrentUser(database.database); - const preferences = currentUser ? (await currentUser.preferences.fetch()) : []; - const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], system.config, license); - const directChannels = channels?.filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL); - if (directChannels?.length) { - await fetchMissingSidebarInfo(serverUrl, directChannels, currentUser?.locale, teammateDisplayNameSetting, currentUserId); - } - - const modelPromises: Array> = []; - const prepare = await prepareMyChannelsForTeam(database.operator, currentTeamMembership.team_id, channels!, memberships!); - if (prepare) { - modelPromises.push(...prepare); - } - if (modelPromises.length) { - const models = await Promise.all(modelPromises); - const flattenedModels = models.flat(); - if (flattenedModels?.length > 0) { - try { - await database.operator.batchRecords(flattenedModels); - } catch { - // eslint-disable-next-line no-console - console.log('FAILED TO BATCH CHANNELS'); - } - } - } - - channelMemberships = memberships; - - if (currentChannelId) { - const stillMemberOfCurrentChannel = memberships?.find((cm) => cm.channel_id === currentChannelId); - const channelStillExist = channels?.find((c) => c.id === currentChannelId); - const viewArchivedChannels = config?.ExperimentalViewArchivedChannels === 'true'; - - if (!stillMemberOfCurrentChannel) { - handleUserRemovedFromChannelEvent(serverUrl, {data: {user_id: currentUserId, channel_id: currentChannelId}}); - } else if (!channelStillExist || - (!viewArchivedChannels && channelStillExist.delete_at !== 0) - ) { - handleChannelDeletedEvent(serverUrl, {data: {user_id: currentUserId, channel_id: currentChannelId}} as WebSocketMessage); - } else { - // TODO Differentiate between post and thread, to fetch the thread posts - fetchPostsSince(serverUrl, currentChannelId, lastDisconnectedAt); - } - } - - // TODO Consider global thread screen to update global threads - } else if (!teamMembershipsError) { - handleLeaveTeamEvent(serverUrl, {data: {user_id: currentUserId, team_id: currentTeamId}} as WebSocketMessage); + resetWebSocketLastDisconnected(operator); + let {config, license} = await fetchConfigAndLicense(serverUrl); + if (!config) { + config = system.config; + } + + if (!license) { + license = system.license; + } + + const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, system.currentTeamId); + const fetchedError = (fetchedData as AppEntryError).error; + + if (fetchedError) { + return; + } + + const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData; + const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true); + + if (chData?.channels?.length) { + const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], config, license); + let directChannels: Channel[]; + [chData.channels, directChannels] = chData.channels.reduce(([others, direct], c: Channel) => { + if (c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL) { + direct.push(c); + } else { + others.push(c); + } + + return [others, direct]; + }, [[], []] as Channel[][]); + + if (directChannels.length) { + await fetchMissingSidebarInfo(serverUrl, directChannels, meData.user?.locale, teammateDisplayNameSetting, system.currentUserId, true); + chData.channels.push(...directChannels); + } + } + + // if no longer a member of the current team + if (initialTeamId !== system.currentTeamId) { + let cId = ''; + if (tabletDevice) { + if (!cId) { + const channel = await queryDefaultChannelForTeam(database, initialTeamId); + if (channel) { + cId = channel.id; + } + } + switchToChannelById(serverUrl, cId, initialTeamId); + } else { + setCurrentTeamAndChannelId(operator, initialTeamId, cId); + } + } + + let removeTeams; + if (removeTeamIds?.length) { + // Immediately delete myTeams so that the UI renders only teams the user is a member of. + removeTeams = await queryTeamsById(database, removeTeamIds); + await deleteMyTeams(operator, removeTeams!); + } + + let removeChannels; + if (removeChannelIds?.length) { + removeChannels = await queryChannelsById(database, removeChannelIds); + } + + const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}); + if (rolesData.roles?.length) { + modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true})); + } + + if (modelPromises.length) { + const models = await Promise.all(modelPromises); + const flattenedModels = models.flat(); + if (flattenedModels?.length > 0) { + try { + await operator.batchRecords(flattenedModels); + } catch { + // eslint-disable-next-line no-console + console.log('FAILED TO BATCH WS reconnection'); + } + } + } + + const currentChannelId = await queryCurrentChannelId(database); + if (currentChannelId) { + // https://mattermost.atlassian.net/browse/MM-40098 + fetchPostsSince(serverUrl, currentChannelId, lastDisconnectedAt); + + // defer fetching posts for unread channels on initial team + if (chData?.channels && chData.memberships) { + fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, currentChannelId); + } + } + + if (initialTeamId) { + fetchGroupsForTeam(serverUrl, initialTeamId, lastDisconnectedAt); + } + + // defer fetch channels and unread posts for other teams + if (teamData.teams?.length && teamData.memberships?.length) { + await fetchTeamsChannelsAndUnreadPosts(serverUrl, lastDisconnectedAt, teamData.teams, teamData.memberships, initialTeamId); } - fetchRoles(serverUrl, teamMemberships, channelMemberships); fetchAllTeams(serverUrl); + updateAllUsersSince(serverUrl, lastDisconnectedAt); - // TODO Fetch App bindings? - - updateAllUsersSinceLastDisconnect(serverUrl); + // https://mattermost.atlassian.net/browse/MM-41520 } export async function handleEvent(serverUrl: string, msg: WebSocketMessage) { diff --git a/app/actions/websocket/users.ts b/app/actions/websocket/users.ts index c89107599e..0d6da82bcd 100644 --- a/app/actions/websocket/users.ts +++ b/app/actions/websocket/users.ts @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Q} from '@nozbe/watermelondb'; +import {Model, Q} from '@nozbe/watermelondb'; import {DeviceEventEmitter} from 'react-native'; import {updateChannelsDisplayName} from '@actions/local/channel'; @@ -21,71 +21,87 @@ import type UserModel from '@typings/database/models/servers/user'; const {SERVER: {CHANNEL, CHANNEL_MEMBERSHIP}} = MM_TABLES; -export async function handleUserUpdatedEvent(serverUrl: string, msg: any) { - const database = DatabaseManager.serverDatabases[serverUrl]; - if (!database) { +export async function handleUserUpdatedEvent(serverUrl: string, msg: WebSocketMessage) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { return; } - const currentUser = await queryCurrentUser(database.database); + + const {database} = operator; + const currentUser = await queryCurrentUser(database); if (!currentUser) { return; } const user: UserProfile = msg.data.user; + const modelsToBatch: Model[] = []; + let userToSave = user; if (user.id === currentUser.id) { if (user.update_at > (currentUser?.updateAt || 0)) { - // Need to request me to make sure we don't override with sanitized fields from the - // websocket event - // TODO Potential improvement https://mattermost.atlassian.net/browse/MM-40582 - fetchMe(serverUrl, false); + // ESR: 6.5 + if (!user.notify_props || !Object.keys(user.notify_props).length) { + // Current user is sanitized so we fetch it from the server + // Need to request me to make sure we don't override with sanitized fields from the + // websocket event + const me = await fetchMe(serverUrl, true); + if (me.user) { + userToSave = me.user; + } + } // Update GMs display name if locale has changed if (user.locale !== currentUser.locale) { - const channels = await database.database.get(CHANNEL).query( + const channels = await database.get(CHANNEL).query( Q.where('type', Q.eq(General.GM_CHANNEL))).fetch(); - const {models} = await updateChannelsDisplayName(serverUrl, channels, user, true); - if (models?.length) { - database.operator.batchRecords(models); + if (channels.length) { + const {models} = await updateChannelsDisplayName(serverUrl, channels, [user], true); + if (models?.length) { + modelsToBatch.push(...models); + } } } } } else { - database.operator.handleUsers({users: [user], prepareRecordsOnly: false}); - - const channels = await database.database.get(CHANNEL).query( + const channels = await database.get(CHANNEL).query( Q.where('type', Q.oneOf([General.DM_CHANNEL, General.GM_CHANNEL])), Q.on(CHANNEL_MEMBERSHIP, Q.where('user_id', user.id))).fetch(); - if (!channels?.length) { - return; + if (channels.length) { + const {models} = await updateChannelsDisplayName(serverUrl, channels, [user], true); + if (models?.length) { + modelsToBatch.push(...models); + } } + } - const {models} = await updateChannelsDisplayName(serverUrl, channels, user, true); + const userModel = await operator.handleUsers({users: [userToSave], prepareRecordsOnly: true}); + modelsToBatch.push(...userModel); - if (models?.length) { - database.operator.batchRecords(models); - } + try { + await operator.batchRecords(modelsToBatch); + } catch { + // do nothing } } -export async function handleUserTypingEvent(serverUrl: string, msg: any) { +export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMessage) { const currentServerUrl = await DatabaseManager.getActiveServerUrl(); if (currentServerUrl === serverUrl) { - const database = DatabaseManager.serverDatabases[serverUrl]; + const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return; } - const {config, license} = await queryCommonSystemValues(database.database); + const {config, license} = await queryCommonSystemValues(database); - let user: UserModel | UserProfile | undefined = await queryUserById(database.database, msg.data.user_id); + let user: UserModel | UserProfile | undefined = await queryUserById(database, msg.data.user_id); if (!user) { const {users} = await fetchUsersByIds(serverUrl, [msg.data.user_id]); user = users?.[0]; } - const namePreference = await queryPreferencesByCategoryAndName(database.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT); - const teammateDisplayNameSetting = await getTeammateNameDisplaySetting(namePreference, config, license); - const currentUser = await queryCurrentUser(database.database); + const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT); + const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config, license); + const currentUser = await queryCurrentUser(database); const username = displayUsername(user, currentUser?.locale, teammateDisplayNameSetting); const data = { channelId: msg.broadcast.channel_id, diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index 52f542def8..f825d33f53 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -7,6 +7,7 @@ import {Events} from '@constants'; import {t} from '@i18n'; import {Analytics, create} from '@init/analytics'; import {setServerCredentials} from '@init/credentials'; +import {semverFromServerVersion} from '@utils/supported_server'; import * as ClientConstants from './constants'; import ClientError from './error'; @@ -234,7 +235,7 @@ export default class ClientBase { } const headers: ClientHeaders = response.headers || {}; - const serverVersion = headers[ClientConstants.HEADER_X_VERSION_ID] || headers[ClientConstants.HEADER_X_VERSION_ID.toLowerCase()]; + const serverVersion = semverFromServerVersion(headers[ClientConstants.HEADER_X_VERSION_ID] || headers[ClientConstants.HEADER_X_VERSION_ID.toLowerCase()]); const hasCacheControl = Boolean(headers[ClientConstants.HEADER_CACHE_CONTROL] || headers[ClientConstants.HEADER_CACHE_CONTROL.toLowerCase()]); if (serverVersion && !hasCacheControl && this.serverVersion !== serverVersion) { this.serverVersion = serverVersion; diff --git a/app/client/rest/index.test.js b/app/client/rest/index.test.js index b7def04ffa..6ab539c564 100644 --- a/app/client/rest/index.test.js +++ b/app/client/rest/index.test.js @@ -36,7 +36,7 @@ describe('Client', () => { await client.getMe(); - assert.equal(client.serverVersion, '5.0.0.5.0.0.abc123'); + assert.equal(client.serverVersion, '5.0.0'); assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true); assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), false); @@ -49,7 +49,7 @@ describe('Client', () => { await client.getMe(); - assert.equal(client.serverVersion, '5.3.0.5.3.0.abc123'); + assert.equal(client.serverVersion, '5.3.0'); assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true); assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), true); }); diff --git a/app/init/global_event_handler.ts b/app/init/global_event_handler.ts index 4e11ce8da0..44ab580d99 100644 --- a/app/init/global_event_handler.ts +++ b/app/init/global_event_handler.ts @@ -6,7 +6,6 @@ import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native'; import semver from 'semver'; import {selectAllMyChannelIds} from '@actions/local/channel'; -import {fetchConfigAndLicense} from '@actions/remote/systems'; import LocalConfig from '@assets/config.json'; import {Events, Sso} from '@constants'; import DatabaseManager from '@database/manager'; @@ -132,12 +131,6 @@ class GlobalEventHandler { {cancelable: false}, ); } - - const fetchTimeout = setTimeout(() => { - // Defer the call to avoid collision with other request writting to the db - fetchConfigAndLicense(serverUrl); - clearTimeout(fetchTimeout); - }, 3000); } }; diff --git a/app/init/websocket_manager.ts b/app/init/websocket_manager.ts index 4f97d08699..cf24488136 100644 --- a/app/init/websocket_manager.ts +++ b/app/init/websocket_manager.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import NetInfo, {NetInfoState} from '@react-native-community/netinfo'; -import {AppState, AppStateStatus} from 'react-native'; +import {AppState, AppStateStatus, DeviceEventEmitter, Platform} from 'react-native'; import {setCurrentUserStatusOffline} from '@actions/local/user'; import {fetchStatusByIds} from '@actions/remote/user'; @@ -10,7 +10,7 @@ import {handleClose, handleEvent, handleFirstConnect, handleReconnect} from '@ac import WebSocketClient from '@client/websocket'; import {General} from '@constants'; import DatabaseManager from '@database/manager'; -import {queryCurrentUserId, resetWebSocketLastDisconnected} from '@queries/servers/system'; +import {queryCurrentUserId} from '@queries/servers/system'; import {queryAllUsers} from '@queries/servers/user'; import type {ServerCredential} from '@typings/credentials'; @@ -34,7 +34,7 @@ class WebsocketManager { if (!operator) { return; } - await resetWebSocketLastDisconnected(operator); + try { this.createClient(serverUrl, token, 0); } catch (error) { @@ -46,6 +46,12 @@ class WebsocketManager { AppState.addEventListener('change', this.onAppStateChange); NetInfo.addEventListener(this.onNetStateChange); + + if (Platform.OS === 'android') { + DeviceEventEmitter.addListener('windowFocusChanged', ({appState}: {appState: AppStateStatus}) => { + this.onAppStateChange(appState); + }); + } }; public invalidateClient = (serverUrl: string) => { diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index f26479491b..230cc1782c 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -118,12 +118,18 @@ export const queryWebSocketLastDisconnected = async (serverDatabase: Database) = } }; -export const resetWebSocketLastDisconnected = (operator: ServerDataOperator) => { - return operator.handleSystem({systems: [{ - id: SYSTEM_IDENTIFIERS.WEBSOCKET, - value: 0, - }], - prepareRecordsOnly: false}); +export const resetWebSocketLastDisconnected = async (operator: ServerDataOperator, prepareRecordsOnly = false) => { + const lastDisconnectedAt = await queryWebSocketLastDisconnected(operator.database); + + if (lastDisconnectedAt) { + return operator.handleSystem({systems: [{ + id: SYSTEM_IDENTIFIERS.WEBSOCKET, + value: 0, + }], + prepareRecordsOnly}); + } + + return []; }; export const queryTeamHistory = async (serverDatabase: Database) => { diff --git a/app/utils/supported_server/index.ts b/app/utils/supported_server/index.ts index d315d02a1a..145cb6de19 100644 --- a/app/utils/supported_server/index.ts +++ b/app/utils/supported_server/index.ts @@ -14,6 +14,20 @@ export function unsupportedServer(isSystemAdmin: boolean, intl: IntlShape) { return unsupportedServerAlert(intl); } +export function semverFromServerVersion(value: string) { + if (!value || typeof value !== 'string') { + return undefined; + } + + const split = value.split('.'); + + const major = parseInt(split[0], 10); + const minor = parseInt(split[1] || '0', 10); + const patch = parseInt(split[2] || '0', 10); + + return `${major}.${minor}.${patch}`; +} + function unsupportedServerAdminAlert(intl: IntlShape) { const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});