[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
This commit is contained in:
Elias Nahum
2022-02-09 12:49:37 -03:00
committed by GitHub
parent 92e069f00c
commit 3ea065b845
19 changed files with 349 additions and 200 deletions

View File

@@ -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

View File

@@ -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() {

View File

@@ -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<UserModel>(USER).query(Q.on(CHANNEL_MEMBERSHIP, Q.where('channel_id', channel.id))).fetch();
const newProfiles: Array<UserModel|UserProfile> = dbProfiles.filter((u) => u.id !== user.id);
newProfiles.push(user);
newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id);
const dbProfiles = await database.get<UserModel>(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<UserModel|UserProfile> = 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};

View File

@@ -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};

View File

@@ -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<AppEntryData | AppEntryError> => {
export const fetchAppEntryData = async (serverUrl: string, since: number, initialTeamId: string): Promise<AppEntryData | AppEntryError> => {
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<MyTeamsRequest>, Promise<MyChannelsRequest | undefined>, Promise<MyPreferencesRequest>, Promise<MyUserRequest>] = [
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);
};

View File

@@ -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)};

View File

@@ -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};
};

View File

@@ -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<string, Group[]>}, Group[]] = await Promise.all([
client.getAllGroupsAssociatedToChannelsInTeam(teamId, true),
client.getGroups(true, 0, 0, since),

View File

@@ -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<string>(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: []};
};

View File

@@ -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);

View File

@@ -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<MyUserRequest> => {
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<ChannelModel>(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};
};

View File

@@ -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<string>();
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<Promise<Model[]>> = [];
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) {

View File

@@ -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<ChannelModel>(CHANNEL).query(
const channels = await database.get<ChannelModel>(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<ChannelModel>(CHANNEL).query(
const channels = await database.get<ChannelModel>(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,

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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);
}
};

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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'});