forked from Ivasoft/mattermost-mobile
Gekidou websockets (#5671)
* Adapt websocket client * Add Websocket Manager * Address feedback * Start websockets on init and login, and invalidate on logout * Add temporal logging and bug fixing * Working WS * Add reconnect actions and periodic updates * Address feedback * Add missing change * Several improvements on websocket reconnect and channel handling * fix gekidou package-lock.json * update Podfile.lock * Address feedback * Address feedback * Address feedback * Fix update channel delete at * Catch errors on batchRecords * Update app/queries/servers/channel.ts Co-authored-by: Elias Nahum <nahumhbl@gmail.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e6f9b0e258
commit
d1e0c99c3d
@@ -10,9 +10,9 @@ import {addUserToTeam, fetchTeamByName, removeUserFromTeam} from '@actions/remot
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {prepareMyChannelsForTeam, queryMyChannel} from '@queries/servers/channel';
|
||||
import {queryCommonSystemValues, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, prepareMyTeams, queryMyTeamById, queryTeamById, queryTeamByName} from '@queries/servers/team';
|
||||
import {prepareDeleteChannel, prepareMyChannelsForTeam, queryChannelsById, queryMyChannel} from '@queries/servers/channel';
|
||||
import {queryCommonSystemValues, queryCurrentTeamId, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {addChannelToTeamHistory, prepareMyTeams, queryMyTeamById, queryTeamById, queryTeamByName, removeChannelFromTeamHistory} from '@queries/servers/team';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
@@ -221,3 +221,60 @@ export const switchToChannelByName = async (serverUrl: string, channelName: stri
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const localRemoveUserFromChannel = async (serverUrl: string, channelId: string) => {
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {operator, database} = serverDatabase;
|
||||
|
||||
const myChannel = await queryMyChannel(database, channelId);
|
||||
if (myChannel) {
|
||||
const channel = await myChannel.channel.fetch() as ChannelModel;
|
||||
const models = await prepareDeleteChannel(channel);
|
||||
let teamId = channel.teamId;
|
||||
if (teamId) {
|
||||
teamId = await queryCurrentTeamId(database);
|
||||
}
|
||||
const system = await removeChannelFromTeamHistory(operator, teamId, channel.id, true);
|
||||
if (system) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR REMOVE USER FROM CHANNEL');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const localSetChannelDeleteAt = async (serverUrl: string, channelId: string, deleteAt: number) => {
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {operator, database} = serverDatabase;
|
||||
|
||||
const channels = await queryChannelsById(database, [channelId]);
|
||||
if (!channels?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = channels[0];
|
||||
const model = channel.prepareUpdate((c) => {
|
||||
c.deleteAt = deleteAt;
|
||||
});
|
||||
|
||||
try {
|
||||
await operator.batchRecords([model]);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {NativeModules} from 'react-native';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import {Device} from '@constants';
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {fetchAllTeams} from '@actions/remote/team';
|
||||
import Events from '@constants/events';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryCurrentTeamId, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {queryLastChannelFromTeam} from '@queries/servers/team';
|
||||
import {prepareDeleteTeam, queryMyTeamById, removeTeamFromTeamHistory, queryLastChannelFromTeam, addTeamToTeamHistory} from '@queries/servers/team';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
export const handleTeamChange = async (serverUrl: string, teamId: string) => {
|
||||
const {operator, database} = DatabaseManager.serverDatabases[serverUrl];
|
||||
@@ -21,15 +23,50 @@ export const handleTeamChange = async (serverUrl: string, teamId: string) => {
|
||||
}
|
||||
|
||||
let channelId = '';
|
||||
if (Device.IS_TABLET) {
|
||||
const {isSplitView} = await isRunningInSplitView();
|
||||
if (!isSplitView) {
|
||||
channelId = await queryLastChannelFromTeam(database, teamId);
|
||||
if (channelId) {
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
}
|
||||
if (await isTablet()) {
|
||||
channelId = await queryLastChannelFromTeam(database, teamId);
|
||||
if (channelId) {
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentTeamAndChannelId(operator, teamId, channelId);
|
||||
addTeamToTeamHistory(operator, teamId);
|
||||
|
||||
const {channels, memberships, error} = await fetchMyChannelsForTeam(serverUrl, teamId);
|
||||
if (error) {
|
||||
DeviceEventEmitter.emit(Events.TEAM_LOAD_ERROR, serverUrl, error);
|
||||
}
|
||||
|
||||
if (channels?.length && memberships?.length) {
|
||||
fetchPostsForUnreadChannels(serverUrl, channels, memberships, channelId);
|
||||
}
|
||||
};
|
||||
|
||||
export const localRemoveUserFromTeam = async (serverUrl: string, teamId: string) => {
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {operator, database} = serverDatabase;
|
||||
|
||||
const myTeam = await queryMyTeamById(database, teamId);
|
||||
if (myTeam) {
|
||||
const team = await myTeam.team.fetch() as TeamModel;
|
||||
const models = await prepareDeleteTeam(team);
|
||||
const system = await removeTeamFromTeamHistory(operator, team.id, true);
|
||||
if (system) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length) {
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR REMOVE USER FROM TEAM');
|
||||
}
|
||||
}
|
||||
|
||||
fetchAllTeams(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,15 +2,41 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import General from '@constants/general';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryRecentCustomStatuses} from '@queries/servers/system';
|
||||
import {queryUserById} from '@queries/servers/user';
|
||||
import {queryCurrentUser, queryUserById} from '@queries/servers/user';
|
||||
|
||||
import {addRecentReaction} from './reactions';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export const setCurrentUserStatusOffline = async (serverUrl: string) => {
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return {error: `No database present for ${serverUrl}`};
|
||||
}
|
||||
|
||||
const {database, operator} = serverDatabase;
|
||||
|
||||
const user = await queryCurrentUser(database);
|
||||
if (!user) {
|
||||
return {error: `No current user for ${serverUrl}`};
|
||||
}
|
||||
|
||||
user.prepareStatus(General.OFFLINE);
|
||||
|
||||
try {
|
||||
await operator.batchRecords([user]);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR SET CURRENT USER STATUS OFFLINE');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateLocalCustomStatus = async (serverUrl: string, user: UserModel, customStatus?: UserCustomStatus) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -38,7 +64,13 @@ export const updateLocalCustomStatus = async (serverUrl: string, user: UserModel
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
try {
|
||||
await operator.batchRecords(models);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR UPDATING CUSTOM STATUS');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -84,7 +116,12 @@ export const updateUserPresence = async (serverUrl: string, userStatus: UserStat
|
||||
user.prepareUpdate((record) => {
|
||||
record.status = userStatus.status;
|
||||
});
|
||||
await operator.batchRecords([user]);
|
||||
try {
|
||||
await operator.batchRecords([user]);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANGES FOR UPDATE USER PRESENCE');
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -6,7 +6,9 @@ import {DeviceEventEmitter} from 'react-native';
|
||||
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@actions/local/timezone';
|
||||
import {General, Database} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import WebsocketManager from '@init/websocket_manager';
|
||||
import {queryDeviceToken} from '@queries/app/global';
|
||||
import {queryCurrentUserId, queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
@@ -43,6 +45,11 @@ export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
// Start websocket
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam, queryDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {queryWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {prepareMyTeams, queryMyTeamById} from '@queries/servers/team';
|
||||
import {prepareMyTeams, syncTeamTable} from '@queries/servers/team';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {fetchMyChannelsForTeam} from './channel';
|
||||
@@ -16,8 +17,6 @@ import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type TeamMembershipModel from '@typings/database/models/servers/team_membership';
|
||||
|
||||
export type MyTeamsRequest = {
|
||||
teams?: Team[];
|
||||
@@ -128,7 +127,7 @@ export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promi
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
await operator.handleTeam({prepareRecordsOnly: false, teams});
|
||||
syncTeamTable(operator, teams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,26 +197,8 @@ export const removeUserFromTeam = async (serverUrl: string, teamId: string, user
|
||||
try {
|
||||
await client.removeFromTeam(teamId, userId);
|
||||
|
||||
if (!fetchOnly && DatabaseManager.serverDatabases[serverUrl]) {
|
||||
const {operator, database} = DatabaseManager.serverDatabases[serverUrl];
|
||||
const myTeam = await queryMyTeamById(database, teamId);
|
||||
const models: Model[] = [];
|
||||
if (myTeam) {
|
||||
const team = await myTeam.team.fetch() as TeamModel;
|
||||
const members: TeamMembershipModel[] = await team.members.fetch();
|
||||
const member = members.find((m) => m.userId === userId);
|
||||
|
||||
myTeam.prepareDestroyPermanently();
|
||||
models.push(myTeam);
|
||||
if (member) {
|
||||
member.prepareDestroyPermanently();
|
||||
models.push(member);
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
if (!fetchOnly) {
|
||||
localRemoveUserFromTeam(serverUrl, teamId);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
// 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 {updateRecentCustomStatuses, updateUserPresence} from '@actions/local/user';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {Database, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import analytics from '@init/analytics';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryCurrentUserId} from '@queries/servers/system';
|
||||
import {prepareUsers, queryCurrentUser, queryUsersById, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {queryCurrentUserId, queryWebSocketLastDisconnected} 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 {LoadMeArgs} from '@typings/database/database';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export type MyUserRequest = {
|
||||
@@ -123,140 +120,6 @@ export const fetchProfilesPerChannels = async (serverUrl: string, channelIds: st
|
||||
}
|
||||
};
|
||||
|
||||
export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs) => {
|
||||
let currentUser = user;
|
||||
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
if (deviceToken) {
|
||||
await client.attachDevice(deviceToken);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
currentUser = await client.getMe();
|
||||
}
|
||||
} catch (e) {
|
||||
await forceLogoutIfNecessary(serverUrl, e as ClientError);
|
||||
return {
|
||||
error: e,
|
||||
currentUser: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const analyticsClient = analytics.create(serverUrl);
|
||||
analyticsClient.setUserId(currentUser.id);
|
||||
analyticsClient.setUserRoles(currentUser.roles);
|
||||
|
||||
//todo: Ask for a unified endpoint that will serve all those values in one go.( while ensuring backward-compatibility through fallbacks to previous code-path)
|
||||
const teamsRequest = client.getMyTeams();
|
||||
|
||||
// Goes into myTeam table
|
||||
const teamMembersRequest = client.getMyTeamMembers();
|
||||
|
||||
const preferencesRequest = client.getMyPreferences();
|
||||
const configRequest = client.getClientConfigOld();
|
||||
const licenseRequest = client.getClientLicenseOld();
|
||||
|
||||
const [
|
||||
teams,
|
||||
teamMemberships,
|
||||
preferences,
|
||||
config,
|
||||
license,
|
||||
] = await Promise.all([
|
||||
teamsRequest,
|
||||
teamMembersRequest,
|
||||
preferencesRequest,
|
||||
configRequest,
|
||||
licenseRequest,
|
||||
]);
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl].operator;
|
||||
const teamRecords = operator.handleTeam({prepareRecordsOnly: true, teams});
|
||||
const teamMembershipRecords = operator.handleTeamMemberships({prepareRecordsOnly: true, teamMemberships});
|
||||
|
||||
const myTeams = teamMemberships.map((tm) => {
|
||||
return {id: tm.team_id, roles: tm.roles ?? ''};
|
||||
});
|
||||
|
||||
const myTeamRecords = operator.handleMyTeam({
|
||||
prepareRecordsOnly: true,
|
||||
myTeams,
|
||||
});
|
||||
|
||||
const systemRecords = operator.handleSystem({
|
||||
systems: [
|
||||
{
|
||||
id: Database.SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
},
|
||||
{
|
||||
id: Database.SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
},
|
||||
{
|
||||
id: Database.SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: currentUser.id,
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const userRecords = operator.handleUsers({
|
||||
users: [currentUser],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const preferenceRecords = operator.handlePreferences({
|
||||
prepareRecordsOnly: true,
|
||||
preferences,
|
||||
});
|
||||
|
||||
let roles: string[] = [];
|
||||
for (const role of currentUser.roles.split(' ')) {
|
||||
roles = roles.concat(role);
|
||||
}
|
||||
|
||||
for (const teamMember of teamMemberships) {
|
||||
roles = roles.concat(teamMember.roles.split(' '));
|
||||
}
|
||||
|
||||
const rolesToLoad = new Set<string>(roles);
|
||||
|
||||
let rolesRecords: RoleModel[] = [];
|
||||
if (rolesToLoad.size > 0) {
|
||||
const rolesByName = await client.getRolesByNames(Array.from(rolesToLoad));
|
||||
|
||||
if (rolesByName?.length) {
|
||||
rolesRecords = await operator.handleRole({prepareRecordsOnly: true, roles: rolesByName});
|
||||
}
|
||||
}
|
||||
|
||||
const models = await Promise.all([teamRecords, teamMembershipRecords, myTeamRecords, systemRecords, preferenceRecords, rolesRecords, userRecords]);
|
||||
|
||||
const flattenedModels = models.flat();
|
||||
if (flattenedModels?.length > 0) {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
}
|
||||
} catch (e) {
|
||||
return {error: e, currentUser: undefined};
|
||||
}
|
||||
|
||||
return {currentUser, error: undefined};
|
||||
};
|
||||
|
||||
export const updateMe = async (serverUrl: string, user: UserModel) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -446,6 +309,70 @@ export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernam
|
||||
}
|
||||
};
|
||||
|
||||
export const updateAllUsersSinceLastDisconnect = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const lastDisconnectedAt = await queryWebSocketLastDisconnected(database.database);
|
||||
|
||||
if (!lastDisconnectedAt) {
|
||||
return {users: []};
|
||||
}
|
||||
const currentUserId = await queryCurrentUserId(database.database);
|
||||
const users = await queryAllUsers(database.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});
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (userUpdates.length) {
|
||||
database.operator.handleUsers({users: userUpdates, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
return {users: userUpdates};
|
||||
};
|
||||
|
||||
export const updateUsersNoLongerVisible = async (serverUrl: string): Promise<{error?: unknown}> => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!serverDatabase) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
const knownUsers = new Set(await client.getKnownUsers());
|
||||
const currentUserId = await queryCurrentUserId(serverDatabase.database);
|
||||
knownUsers.add(currentUserId);
|
||||
|
||||
const models: Model[] = [];
|
||||
const allUsers = await queryAllUsers(serverDatabase.database);
|
||||
for (const user of allUsers) {
|
||||
if (!knownUsers.has(user.id)) {
|
||||
user.prepareDestroyPermanently();
|
||||
models.push(user);
|
||||
}
|
||||
}
|
||||
if (models.length) {
|
||||
serverDatabase.operator.batchRecords(models);
|
||||
}
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
export const setStatus = async (serverUrl: string, status: UserStatus) => {
|
||||
let client: Client;
|
||||
try {
|
||||
|
||||
103
app/actions/websocket/channel.ts
Normal file
103
app/actions/websocket/channel.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {localRemoveUserFromChannel, localSetChannelDeleteAt, switchToChannel} from '@actions/local/channel';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import Events from '@constants/events';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryActiveServer} from '@queries/app/servers';
|
||||
import {deleteChannelMembership, queryCurrentChannel} from '@queries/servers/channel';
|
||||
import {queryConfig, setCurrentChannelId} from '@queries/servers/system';
|
||||
import {queryLastChannelFromTeam} from '@queries/servers/team';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {isGuest} from '@utils/user';
|
||||
|
||||
export async function handleUserRemovedEvent(serverUrl: string, msg: any) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = await queryCurrentChannel(database.database);
|
||||
const user = await queryCurrentUser(database.database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.id === msg.data.user_id) {
|
||||
localRemoveUserFromChannel(serverUrl, msg.data.channel_id);
|
||||
|
||||
if (isGuest(user.roles)) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
|
||||
if (channel && channel.id === msg.data.channel_id) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_CHANNEL);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (await isTablet()) {
|
||||
const channelToJumpTo = await queryLastChannelFromTeam(database.database, channel?.teamId);
|
||||
if (channelToJumpTo) {
|
||||
switchToChannel(serverUrl, channelToJumpTo);
|
||||
} // TODO else jump to "join a channel" screen
|
||||
} else {
|
||||
setCurrentChannelId(database.operator, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deleteChannelMembership(database.operator, msg.data.user_id, msg.data.channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleChannelDeletedEvent(serverUrl: string, msg: any) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChannel = await queryCurrentChannel(database.database);
|
||||
const user = await queryCurrentUser(database.database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await queryConfig(database.database);
|
||||
|
||||
await localSetChannelDeleteAt(serverUrl, msg.data.channel_id, msg.data.delete_at);
|
||||
|
||||
if (isGuest(user.roles)) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
|
||||
if (config?.ExperimentalViewArchivedChannels !== 'true') {
|
||||
localRemoveUserFromChannel(serverUrl, msg.data.channel_id);
|
||||
|
||||
if (currentChannel && currentChannel.id === msg.data.channel_id) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_DELETED);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
if (await isTablet()) {
|
||||
const channelToJumpTo = await queryLastChannelFromTeam(database.database, currentChannel?.teamId);
|
||||
if (channelToJumpTo) {
|
||||
switchToChannel(serverUrl, channelToJumpTo);
|
||||
} // TODO else jump to "join a channel" screen
|
||||
} else {
|
||||
setCurrentChannelId(database.operator, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
283
app/actions/websocket/index.ts
Normal file
283
app/actions/websocket/index.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchPostsSince} from '@actions/remote/post';
|
||||
import {fetchMyPreferences} from '@actions/remote/preference';
|
||||
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 Events from '@app/constants/events';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryCommonSystemValues, queryConfig, queryWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
|
||||
import {handleChannelDeletedEvent, handleUserRemovedEvent} from './channel';
|
||||
import {handleLeaveTeamEvent} from './teams';
|
||||
|
||||
export async function handleFirstConnect(serverUrl: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
const config = await queryConfig(database);
|
||||
const lastDisconnect = await queryWebSocketLastDisconnected(database);
|
||||
if (lastDisconnect && config.EnableReliableWebSockets !== 'true') {
|
||||
doReconnect(serverUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
doFirstConnect(serverUrl);
|
||||
}
|
||||
|
||||
export function handleReconnect(serverUrl: string) {
|
||||
doReconnect(serverUrl);
|
||||
}
|
||||
|
||||
export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
await operator.handleSystem({
|
||||
systems: [
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
|
||||
value: lastDisconnect.toString(10),
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
function doFirstConnect(serverUrl: string) {
|
||||
updateAllUsersSinceLastDisconnect(serverUrl);
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {currentUserId, currentTeamId, currentChannelId} = await queryCommonSystemValues(database.database);
|
||||
const lastDisconnectedAt = await queryWebSocketLastDisconnected(database.database);
|
||||
|
||||
// TODO consider fetch only and batch all the results.
|
||||
fetchMe(serverUrl);
|
||||
fetchMyPreferences(serverUrl);
|
||||
const {config} = await fetchConfigAndLicense(serverUrl);
|
||||
const {memberships: teamMemberships, error: teamMembershipsError} = await fetchMyTeams(serverUrl);
|
||||
|
||||
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, false, lastDisconnectedAt);
|
||||
if (error) {
|
||||
DeviceEventEmitter.emit(Events.TEAM_LOAD_ERROR, serverUrl, error);
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
handleUserRemovedEvent(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}});
|
||||
} 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}});
|
||||
}
|
||||
|
||||
fetchRoles(serverUrl, teamMemberships, channelMemberships);
|
||||
fetchAllTeams(serverUrl);
|
||||
|
||||
// TODO Fetch App bindings?
|
||||
|
||||
updateAllUsersSinceLastDisconnect(serverUrl);
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: any) {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
break;
|
||||
|
||||
//return dispatch(handleNewPostEvent(msg));
|
||||
case WebsocketEvents.POST_EDITED:
|
||||
break;
|
||||
|
||||
//return dispatch(handlePostEdited(msg));
|
||||
case WebsocketEvents.POST_DELETED:
|
||||
break;
|
||||
|
||||
// return dispatch(handlePostDeleted(msg));
|
||||
case WebsocketEvents.POST_UNREAD:
|
||||
break;
|
||||
|
||||
// return dispatch(handlePostUnread(msg));
|
||||
case WebsocketEvents.LEAVE_TEAM:
|
||||
handleLeaveTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.UPDATE_TEAM:
|
||||
break;
|
||||
|
||||
// return dispatch(handleUpdateTeamEvent(msg));
|
||||
case WebsocketEvents.ADDED_TO_TEAM:
|
||||
break;
|
||||
|
||||
// return dispatch(handleTeamAddedEvent(msg));
|
||||
case WebsocketEvents.USER_ADDED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleUserAddedEvent(msg));
|
||||
case WebsocketEvents.USER_REMOVED:
|
||||
handleUserRemovedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.USER_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleUserUpdatedEvent(msg));
|
||||
case WebsocketEvents.ROLE_ADDED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleRoleAddedEvent(msg));
|
||||
case WebsocketEvents.ROLE_REMOVED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleRoleRemovedEvent(msg));
|
||||
case WebsocketEvents.ROLE_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleRoleUpdatedEvent(msg));
|
||||
case WebsocketEvents.USER_ROLE_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleUserRoleUpdated(msg));
|
||||
case WebsocketEvents.MEMBERROLE_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleUpdateMemberRoleEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_CREATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelCreatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_DELETED:
|
||||
handleChannelDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CHANNEL_UNARCHIVED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelUnarchiveEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelUpdatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_CONVERTED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelConvertedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_VIEWED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelViewedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelMemberUpdatedEvent(msg));
|
||||
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleChannelSchemeUpdatedEvent(msg));
|
||||
case WebsocketEvents.DIRECT_ADDED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleDirectAddedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCE_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handlePreferenceChangedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCES_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handlePreferencesChangedEvent(msg));
|
||||
case WebsocketEvents.PREFERENCES_DELETED:
|
||||
break;
|
||||
|
||||
// return dispatch(handlePreferencesDeletedEvent(msg));
|
||||
case WebsocketEvents.STATUS_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleStatusChangedEvent(msg));
|
||||
case WebsocketEvents.TYPING:
|
||||
break;
|
||||
|
||||
// return dispatch(handleUserTypingEvent(msg));
|
||||
case WebsocketEvents.HELLO:
|
||||
break;
|
||||
|
||||
// handleHelloEvent(msg);
|
||||
// break;
|
||||
case WebsocketEvents.REACTION_ADDED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleReactionAddedEvent(msg));
|
||||
case WebsocketEvents.REACTION_REMOVED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleReactionRemovedEvent(msg));
|
||||
case WebsocketEvents.EMOJI_ADDED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleAddEmoji(msg));
|
||||
case WebsocketEvents.LICENSE_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleLicenseChangedEvent(msg));
|
||||
case WebsocketEvents.CONFIG_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleConfigChangedEvent(msg));
|
||||
case WebsocketEvents.OPEN_DIALOG:
|
||||
break;
|
||||
|
||||
// return dispatch(handleOpenDialogEvent(msg));
|
||||
case WebsocketEvents.RECEIVED_GROUP:
|
||||
break;
|
||||
|
||||
// return dispatch(handleGroupUpdatedEvent(msg));
|
||||
case WebsocketEvents.THREAD_UPDATED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleThreadUpdated(msg));
|
||||
case WebsocketEvents.THREAD_READ_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleThreadReadChanged(msg));
|
||||
case WebsocketEvents.THREAD_FOLLOW_CHANGED:
|
||||
break;
|
||||
|
||||
// return dispatch(handleThreadFollowChanged(msg));
|
||||
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS:
|
||||
break;
|
||||
|
||||
// return dispatch(handleRefreshAppsBindings());
|
||||
}
|
||||
}
|
||||
51
app/actions/websocket/teams.ts
Normal file
51
app/actions/websocket/teams.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {handleTeamChange, localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import Events from '@constants/events';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryActiveServer} from '@queries/app/servers';
|
||||
import {queryCurrentTeamId} from '@queries/servers/system';
|
||||
import {queryLastTeam} from '@queries/servers/team';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import {isGuest} from '@utils/user';
|
||||
|
||||
export async function handleLeaveTeamEvent(serverUrl: string, msg: any) {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTeamId = await queryCurrentTeamId(database.database);
|
||||
const user = await queryCurrentUser(database.database);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.id === msg.data.user_id) {
|
||||
localRemoveUserFromTeam(serverUrl, msg.data.team_id);
|
||||
|
||||
if (isGuest(user.roles)) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
|
||||
if (currentTeamId === msg.data.team_id) {
|
||||
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
|
||||
|
||||
if (currentServer?.url === serverUrl) {
|
||||
DeviceEventEmitter.emit(Events.LEAVE_TEAM);
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
const teamToJumpTo = await queryLastTeam(database.database);
|
||||
if (teamToJumpTo) {
|
||||
handleTeamChange(serverUrl, teamToJumpTo);
|
||||
} // TODO else jump to "join a team" screen
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export interface ClientTeamsMix {
|
||||
deleteTeam: (teamId: string) => Promise<any>;
|
||||
updateTeam: (team: Team) => Promise<Team>;
|
||||
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise<any>;
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise<Team[]>;
|
||||
getTeam: (teamId: string) => Promise<Team>;
|
||||
getTeamByName: (teamName: string) => Promise<Team>;
|
||||
getMyTeams: () => Promise<Team[]>;
|
||||
|
||||
@@ -1,220 +1,319 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import {getOrCreateWebSocketClient, WebSocketClientInterface} from '@mattermost/react-native-network-client';
|
||||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryCommonSystemValues} from '@queries/servers/system';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
|
||||
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
|
||||
class WebSocketClient {
|
||||
conn?: WebSocket;
|
||||
connectionUrl: string;
|
||||
token: string|null;
|
||||
sequence: number;
|
||||
connectFailCount: number;
|
||||
eventCallback?: Function;
|
||||
firstConnectCallback?: Function;
|
||||
reconnectCallback?: Function;
|
||||
errorCallback?: Function;
|
||||
closeCallback?: Function;
|
||||
connectingCallback?: Function;
|
||||
stop: boolean;
|
||||
connectionTimeout: any;
|
||||
export default class WebSocketClient {
|
||||
private conn?: WebSocketClientInterface;
|
||||
private connectionTimeout: any;
|
||||
private connectionId: string;
|
||||
private token: string;
|
||||
|
||||
constructor() {
|
||||
this.connectionUrl = '';
|
||||
this.token = null;
|
||||
this.sequence = 1;
|
||||
// responseSequence is the number to track a response sent
|
||||
// via the websocket. A response will always have the same sequence number
|
||||
// as the request.
|
||||
private responseSequence: number;
|
||||
|
||||
// serverSequence is the incrementing sequence number from the
|
||||
// server-sent event stream.
|
||||
private serverSequence: number;
|
||||
private connectFailCount: number;
|
||||
private eventCallback?: Function;
|
||||
private firstConnectCallback?: () => void;
|
||||
private missedEventsCallback?: () => void;
|
||||
private reconnectCallback?: () => void;
|
||||
private errorCallback?: Function;
|
||||
private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void;
|
||||
private connectingCallback?: () => void;
|
||||
private stop: boolean;
|
||||
private lastConnect: number;
|
||||
private lastDisconnect: number;
|
||||
|
||||
private serverUrl: string;
|
||||
|
||||
constructor(serverUrl: string, token: string, lastDisconnect = 0) {
|
||||
this.connectionId = '';
|
||||
this.token = token;
|
||||
this.responseSequence = 1;
|
||||
this.serverSequence = 0;
|
||||
this.connectFailCount = 0;
|
||||
this.stop = false;
|
||||
this.serverUrl = serverUrl;
|
||||
this.lastConnect = 0;
|
||||
this.lastDisconnect = lastDisconnect;
|
||||
}
|
||||
|
||||
initialize(token: string|null, opts = {}) {
|
||||
public async initialize(opts = {}) {
|
||||
const defaults = {
|
||||
forceConnection: true,
|
||||
connectionUrl: this.connectionUrl,
|
||||
};
|
||||
|
||||
const {connectionUrl, forceConnection, ...additionalOptions} = Object.assign({}, defaults, opts);
|
||||
const {forceConnection} = Object.assign({}, defaults, opts);
|
||||
|
||||
if (forceConnection) {
|
||||
this.stop = false;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.conn) {
|
||||
resolve(null);
|
||||
return;
|
||||
if (this.conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database = DatabaseManager.serverDatabases[this.serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const system = await queryCommonSystemValues(database);
|
||||
const connectionUrl = (system.config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
}
|
||||
|
||||
const regex = /^(?:https?|wss?):(?:\/\/)?[^/]*/;
|
||||
const captured = (regex).exec(connectionUrl);
|
||||
|
||||
let origin;
|
||||
if (captured) {
|
||||
origin = captured[0];
|
||||
} else {
|
||||
// If we're unable to set the origin header, the websocket won't connect, but the URL is likely malformed anyway
|
||||
const errorMessage = 'websocket failed to parse origin from ' + connectionUrl;
|
||||
console.warn(errorMessage); // eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
let url = connectionUrl;
|
||||
|
||||
const reliableWebSockets = system.config.EnableReliableWebSockets === 'true';
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
url = `${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}`;
|
||||
}
|
||||
|
||||
// Manually changing protocol since getOrCreateWebsocketClient does not accept http/s
|
||||
if (url.startsWith('https:')) {
|
||||
url = 'wss:' + url.substr('https:'.length);
|
||||
}
|
||||
|
||||
if (url.startsWith('http:')) {
|
||||
url = 'ws:' + url.substr('http:'.length);
|
||||
}
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + url); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
try {
|
||||
const {client} = await getOrCreateWebSocketClient(url, {headers: {origin}});
|
||||
this.conn = client;
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.conn!.onOpen(() => {
|
||||
this.lastConnect = Date.now();
|
||||
|
||||
// No need to reset sequence number here.
|
||||
if (!reliableWebSockets) {
|
||||
this.serverSequence = 0;
|
||||
}
|
||||
|
||||
if (connectionUrl == null) {
|
||||
console.log('websocket must have connection url'); //eslint-disable-line no-console
|
||||
reject(new Error('websocket must have connection url'));
|
||||
return;
|
||||
if (this.token) {
|
||||
// we check for the platform as a workaround until we fix on the server that further authentications
|
||||
// are ignored
|
||||
this.sendMessage('authentication_challenge', {token: this.token});
|
||||
}
|
||||
|
||||
if (this.connectFailCount > 0) {
|
||||
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
||||
if (!reliableWebSockets && this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
} else if (reliableWebSockets && this.serverSequence && this.missedEventsCallback) {
|
||||
this.missedEventsCallback();
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
this.firstConnectCallback();
|
||||
}
|
||||
|
||||
this.connectFailCount = 0;
|
||||
});
|
||||
|
||||
this.conn!.onClose(() => {
|
||||
const now = Date.now();
|
||||
if (this.lastDisconnect < this.lastConnect) {
|
||||
this.lastDisconnect = now;
|
||||
}
|
||||
|
||||
this.conn = undefined;
|
||||
this.responseSequence = 1;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
this.connectFailCount++;
|
||||
|
||||
if (this.closeCallback) {
|
||||
this.closeCallback(this.connectFailCount, this.lastDisconnect);
|
||||
}
|
||||
|
||||
const regex = /^(?:https?|wss?):(?:\/\/)?[^/]*/;
|
||||
const captured = (regex).exec(connectionUrl);
|
||||
|
||||
let origin;
|
||||
if (captured) {
|
||||
origin = captured[0];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// this is done cause for android having the port 80 or 443 will fail the connection
|
||||
// the websocket will append them
|
||||
const split = origin.split(':');
|
||||
const port = split[2];
|
||||
if (port === '80' || port === '443') {
|
||||
origin = `${split[0]}:${split[1]}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we're unable to set the origin header, the websocket won't connect, but the URL is likely malformed anyway
|
||||
const errorMessage = 'websocket failed to parse origin from ' + connectionUrl;
|
||||
console.warn(errorMessage); // eslint-disable-line no-console
|
||||
reject(new Error(errorMessage));
|
||||
if (this.stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.conn = new WebSocket(connectionUrl, [], {headers: {origin}, ...(additionalOptions || {})});
|
||||
this.connectionUrl = connectionUrl;
|
||||
this.token = token;
|
||||
let retryTime = MIN_WEBSOCKET_RETRY_TIME;
|
||||
|
||||
this.conn!.onopen = () => {
|
||||
if (token) {
|
||||
// we check for the platform as a workaround until we fix on the server that further authentications
|
||||
// are ignored
|
||||
this.sendMessage('authentication_challenge', {token});
|
||||
// If we've failed a bunch of connections then start backing off
|
||||
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
|
||||
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount;
|
||||
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
|
||||
retryTime = MAX_WEBSOCKET_RETRY_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.connectFailCount > 0) {
|
||||
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
||||
if (this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
|
||||
this.connectionTimeout = setTimeout(
|
||||
() => {
|
||||
if (this.stop) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
return;
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
this.firstConnectCallback();
|
||||
}
|
||||
|
||||
this.connectFailCount = 0;
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
this.conn!.onclose = () => {
|
||||
this.conn = undefined;
|
||||
this.sequence = 1;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
this.connectFailCount++;
|
||||
|
||||
if (this.closeCallback) {
|
||||
this.closeCallback(this.connectFailCount);
|
||||
}
|
||||
|
||||
let retryTime = MIN_WEBSOCKET_RETRY_TIME;
|
||||
|
||||
// If we've failed a bunch of connections then start backing off
|
||||
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
|
||||
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount;
|
||||
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
|
||||
retryTime = MAX_WEBSOCKET_RETRY_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
|
||||
this.connectionTimeout = setTimeout(
|
||||
() => {
|
||||
if (this.stop) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
return;
|
||||
}
|
||||
this.initialize(token, opts);
|
||||
},
|
||||
retryTime,
|
||||
);
|
||||
};
|
||||
|
||||
this.conn!.onerror = (evt: any) => {
|
||||
if (this.connectFailCount <= 1) {
|
||||
console.log('websocket error'); //eslint-disable-line no-console
|
||||
console.log(evt); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.errorCallback) {
|
||||
this.errorCallback(evt);
|
||||
}
|
||||
};
|
||||
|
||||
this.conn!.onmessage = (evt: any) => {
|
||||
const msg = JSON.parse(evt.data);
|
||||
if (msg.seq_reply) {
|
||||
if (msg.error) {
|
||||
console.warn(msg); //eslint-disable-line no-console
|
||||
}
|
||||
} else if (this.eventCallback) {
|
||||
this.eventCallback(msg);
|
||||
}
|
||||
};
|
||||
this.initialize(opts);
|
||||
},
|
||||
retryTime,
|
||||
);
|
||||
});
|
||||
|
||||
this.conn!.onError((evt: any) => {
|
||||
if (this.connectFailCount <= 1) {
|
||||
console.log('websocket error'); //eslint-disable-line no-console
|
||||
console.log(evt); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.errorCallback) {
|
||||
this.errorCallback(evt);
|
||||
}
|
||||
});
|
||||
|
||||
this.conn!.onMessage((evt: any) => {
|
||||
const msg = evt.message;
|
||||
|
||||
// This indicates a reply to a websocket request.
|
||||
// We ignore sequence number validation of message responses
|
||||
// and only focus on the purely server side event stream.
|
||||
if (msg.seq_reply) {
|
||||
if (msg.error) {
|
||||
console.warn(msg); //eslint-disable-line no-console
|
||||
}
|
||||
} else if (this.eventCallback) {
|
||||
if (reliableWebSockets) {
|
||||
// We check the hello packet, which is always the first packet in a stream.
|
||||
if (msg.event === WebsocketEvents.HELLO && this.reconnectCallback) {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log('got connection id ', msg.data.connection_id);
|
||||
|
||||
// If we already have a connectionId present, and server sends a different one,
|
||||
// that means it's either a long timeout, or server restart, or sequence number is not found.
|
||||
// Then we do the sync calls, and reset sequence number to 0.
|
||||
if (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log('long timeout, or server restart, or sequence number is not found.');
|
||||
this.reconnectCallback();
|
||||
this.serverSequence = 0;
|
||||
}
|
||||
|
||||
// If it's a fresh connection, we have to set the connectionId regardless.
|
||||
// And if it's an existing connection, setting it again is harmless, and keeps the code simple.
|
||||
this.connectionId = msg.data.connection_id;
|
||||
}
|
||||
|
||||
// Now we check for sequence number, and if it does not match,
|
||||
// we just disconnect and reconnect.
|
||||
if (msg.seq !== this.serverSequence) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence);
|
||||
|
||||
// We are not calling this.close() because we need to auto-restart.
|
||||
this.connectFailCount = 0;
|
||||
this.responseSequence = 1;
|
||||
this.conn?.close(); // Will auto-reconnect after MIN_WEBSOCKET_RETRY_TIME.
|
||||
return;
|
||||
}
|
||||
} else if (msg.seq !== this.serverSequence && this.reconnectCallback) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence);
|
||||
this.reconnectCallback();
|
||||
}
|
||||
|
||||
this.serverSequence = msg.seq + 1;
|
||||
this.eventCallback(msg);
|
||||
}
|
||||
});
|
||||
|
||||
this.conn.open();
|
||||
}
|
||||
|
||||
setConnectingCallback(callback: Function) {
|
||||
public setConnectingCallback(callback: () => void) {
|
||||
this.connectingCallback = callback;
|
||||
}
|
||||
|
||||
setEventCallback(callback: Function) {
|
||||
public setEventCallback(callback: Function) {
|
||||
this.eventCallback = callback;
|
||||
}
|
||||
|
||||
setFirstConnectCallback(callback: Function) {
|
||||
public setFirstConnectCallback(callback: () => void) {
|
||||
this.firstConnectCallback = callback;
|
||||
}
|
||||
|
||||
setReconnectCallback(callback: Function) {
|
||||
public setMissedEventsCallback(callback: () => void) {
|
||||
this.missedEventsCallback = callback;
|
||||
}
|
||||
|
||||
public setReconnectCallback(callback: () => void) {
|
||||
this.reconnectCallback = callback;
|
||||
}
|
||||
|
||||
setErrorCallback(callback: Function) {
|
||||
public setErrorCallback(callback: Function) {
|
||||
this.errorCallback = callback;
|
||||
}
|
||||
|
||||
setCloseCallback(callback: Function) {
|
||||
public setCloseCallback(callback: (connectFailCount: number, lastDisconnect: number) => void) {
|
||||
this.closeCallback = callback;
|
||||
}
|
||||
|
||||
close(stop = false) {
|
||||
public close(stop = false) {
|
||||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.sequence = 1;
|
||||
this.responseSequence = 1;
|
||||
|
||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||
this.conn.onclose = () => {}; //eslint-disable-line @typescript-eslint/no-empty-function
|
||||
this.conn.close();
|
||||
this.conn = undefined;
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(action: string, data: any) {
|
||||
public invalidate() {
|
||||
this.conn?.invalidate();
|
||||
this.conn = undefined;
|
||||
}
|
||||
|
||||
private sendMessage(action: string, data: any) {
|
||||
const msg = {
|
||||
action,
|
||||
seq: this.sequence++,
|
||||
seq: this.responseSequence++,
|
||||
data,
|
||||
};
|
||||
|
||||
@@ -226,22 +325,14 @@ class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
userTyping(channelId: string, parentId: string) {
|
||||
public sendUserTypingEvent(channelId: string, parentId: string) {
|
||||
this.sendMessage('user_typing', {
|
||||
channel_id: channelId,
|
||||
parent_id: parentId,
|
||||
});
|
||||
}
|
||||
|
||||
getStatuses() {
|
||||
this.sendMessage('get_statuses', null);
|
||||
}
|
||||
|
||||
getStatusesByIds(userIds: string[]) {
|
||||
this.sendMessage('get_statuses_by_ids', {
|
||||
user_ids: userIds,
|
||||
});
|
||||
public isConnected(): boolean {
|
||||
return this.conn?.readyState === WebSocket.OPEN; //|| (!this.stop && this.connectFailCount <= 2);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketClient();
|
||||
|
||||
@@ -57,6 +57,7 @@ export const SYSTEM_IDENTIFIERS = {
|
||||
INTEGRATION_TRIGGER_ID: 'IntegreationTriggerId',
|
||||
LICENSE: 'license',
|
||||
WEBSOCKET: 'WebSocket',
|
||||
TEAM_HISTORY: 'teamHistory',
|
||||
RECENT_CUSTOM_STATUS: 'recentCustomStatus',
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,8 @@ import keyMirror from '@utils/key_mirror';
|
||||
|
||||
export default keyMirror({
|
||||
ACCOUNT_SELECT_TABLET_VIEW: null,
|
||||
LEAVE_CHANNEL: null,
|
||||
LEAVE_TEAM: null,
|
||||
TEAM_LOAD_ERROR: null,
|
||||
CHANNEL_DELETED: null,
|
||||
});
|
||||
|
||||
@@ -43,5 +43,9 @@ const WebsocketEvents = {
|
||||
INCREASE_POST_VISIBILITY_BY_ONE: 'increase_post_visibility_by_one',
|
||||
MEMBERROLE_UPDATED: 'memberrole_updated',
|
||||
RECEIVED_GROUP: 'received_group',
|
||||
THREAD_UPDATED: 'thread_updated',
|
||||
THREAD_FOLLOW_CHANGED: 'thread_follow_changed',
|
||||
THREAD_READ_CHANGED: 'thread_read_changed',
|
||||
APPS_FRAMEWORK_REFRESH_BINDINGS: 'custom_com.mattermost.apps_refresh_bindings',
|
||||
};
|
||||
export default WebsocketEvents;
|
||||
|
||||
@@ -241,7 +241,7 @@ class DatabaseManager {
|
||||
if (database) {
|
||||
const server = await queryActiveServer(database);
|
||||
if (server?.url) {
|
||||
return this.serverDatabases[server.url].database;
|
||||
return this.serverDatabases[server.url]?.database;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import WebsocketManager from '@init/websocket_manager';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
import {LaunchType} from '@typings/launch';
|
||||
import {deleteFileCache} from '@utils/file';
|
||||
@@ -84,8 +85,8 @@ class GlobalEventHandler {
|
||||
onLogout = async (serverUrl: string) => {
|
||||
await removeServerCredentials(serverUrl);
|
||||
|
||||
// TODO WebSocket: invalidate WebSocket client
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
WebsocketManager.invalidateClient(serverUrl);
|
||||
await DatabaseManager.deleteServerDatabase(serverUrl);
|
||||
|
||||
const analyticsClient = analytics.get(serverUrl);
|
||||
|
||||
184
app/init/websocket_manager.ts
Normal file
184
app/init/websocket_manager.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import NetInfo, {NetInfoState} from '@react-native-community/netinfo';
|
||||
import {AppState, AppStateStatus} from 'react-native';
|
||||
|
||||
import {setCurrentUserStatusOffline} from '@actions/local/user';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {handleClose, handleEvent, handleFirstConnect, handleReconnect} from '@actions/websocket';
|
||||
import WebSocketClient from '@app/client/websocket';
|
||||
import {General} from '@app/constants';
|
||||
import {queryWebSocketLastDisconnected} from '@app/queries/servers/system';
|
||||
import {queryAllUsers} from '@app/queries/servers/user';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
||||
import type {ServerCredential} from '@typings/credentials';
|
||||
|
||||
class WebsocketManager {
|
||||
private clients: Record<string, WebSocketClient> = {};
|
||||
private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {};
|
||||
private previousAppState: AppStateStatus;
|
||||
private netConnected = false;
|
||||
|
||||
constructor() {
|
||||
this.previousAppState = AppState.currentState;
|
||||
}
|
||||
|
||||
public init = async (serverCredentials: ServerCredential[]) => {
|
||||
this.netConnected = Boolean((await NetInfo.fetch()).isConnected);
|
||||
await Promise.all(
|
||||
serverCredentials.map(
|
||||
async ({serverUrl, token}) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
const lastDisconnect = await queryWebSocketLastDisconnected(database);
|
||||
try {
|
||||
this.createClient(serverUrl, token, lastDisconnect);
|
||||
} catch (error) {
|
||||
console.log('WebsocketManager init error', error); //eslint-disable-line no-console
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
NetInfo.addEventListener(this.onNetStateChange);
|
||||
}
|
||||
|
||||
public invalidateClient = (serverUrl: string) => {
|
||||
this.clients[serverUrl]?.close();
|
||||
this.clients[serverUrl]?.invalidate();
|
||||
delete this.clients[serverUrl];
|
||||
}
|
||||
|
||||
public createClient = (serverUrl: string, bearerToken: string, storedLastDisconnect = 0) => {
|
||||
const client = new WebSocketClient(serverUrl, bearerToken, storedLastDisconnect);
|
||||
|
||||
client.setFirstConnectCallback(() => this.onFirstConnect(serverUrl));
|
||||
client.setEventCallback((evt: any) => handleEvent(serverUrl, evt));
|
||||
|
||||
//client.setMissedEventsCallback(() => {}) Nothing to do on missedEvents callback
|
||||
client.setReconnectCallback(() => this.onReconnect(serverUrl));
|
||||
client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect));
|
||||
|
||||
if (this.netConnected) {
|
||||
client.initialize();
|
||||
}
|
||||
this.clients[serverUrl] = client;
|
||||
|
||||
return this.clients[serverUrl];
|
||||
}
|
||||
|
||||
public closeAll = () => {
|
||||
for (const client of Object.values(this.clients)) {
|
||||
client.close(true);
|
||||
}
|
||||
}
|
||||
|
||||
public openAll = () => {
|
||||
for (const client of Object.values(this.clients)) {
|
||||
if (!client.isConnected()) {
|
||||
client.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isConnected = (serverUrl: string): boolean => {
|
||||
return this.clients[serverUrl]?.isConnected();
|
||||
}
|
||||
|
||||
private onFirstConnect = (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
handleFirstConnect(serverUrl);
|
||||
}
|
||||
|
||||
private onReconnect = (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
handleReconnect(serverUrl);
|
||||
}
|
||||
|
||||
private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => {
|
||||
if (connectFailCount <= 1) { // First fail
|
||||
await setCurrentUserStatusOffline(serverUrl);
|
||||
await handleClose(serverUrl, lastDisconnect);
|
||||
|
||||
this.stopPeriodicStatusUpdates(serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private startPeriodicStatusUpdates(serverUrl: string) {
|
||||
let currentId = this.statusUpdatesIntervalIDs[serverUrl];
|
||||
if (currentId != null) {
|
||||
clearInterval(currentId);
|
||||
}
|
||||
|
||||
const getStatusForUsers = async () => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const users = await queryAllUsers(database.database);
|
||||
|
||||
const userIds = users.map((u) => u.id);
|
||||
if (!userIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchStatusByIds(serverUrl, userIds);
|
||||
};
|
||||
|
||||
currentId = setInterval(getStatusForUsers, General.STATUS_INTERVAL);
|
||||
this.statusUpdatesIntervalIDs[serverUrl] = currentId;
|
||||
}
|
||||
|
||||
private stopPeriodicStatusUpdates(serverUrl: string) {
|
||||
const currentId = this.statusUpdatesIntervalIDs[serverUrl];
|
||||
if (currentId != null) {
|
||||
clearInterval(currentId);
|
||||
}
|
||||
|
||||
delete this.statusUpdatesIntervalIDs[serverUrl];
|
||||
}
|
||||
|
||||
private onAppStateChange = async (appState: AppStateStatus) => {
|
||||
if (appState === this.previousAppState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState !== 'active') {
|
||||
this.closeAll();
|
||||
this.previousAppState = appState;
|
||||
return;
|
||||
}
|
||||
|
||||
if (appState === 'active' && this.netConnected) { // Reopen the websockets only if there is connection
|
||||
this.openAll();
|
||||
this.previousAppState = appState;
|
||||
return;
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
}
|
||||
|
||||
private onNetStateChange = async (netState: NetInfoState) => {
|
||||
const newState = Boolean(netState.isConnected);
|
||||
if (this.netConnected === newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.netConnected = newState;
|
||||
|
||||
if (this.netConnected && this.previousAppState === 'active') { // Reopen the websockets only if the app is active
|
||||
this.openAll();
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeAll();
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebsocketManager();
|
||||
@@ -9,6 +9,7 @@ import {hasPermission} from '@utils/role';
|
||||
|
||||
import {prepareDeletePost} from './post';
|
||||
import {queryRoles} from './role';
|
||||
import {queryCurrentChannelId} from './system';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
@@ -16,7 +17,7 @@ import type ChannelInfoModel from '@typings/database/models/servers/channel_info
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const {SERVER: {CHANNEL, MY_CHANNEL}} = MM_TABLES;
|
||||
const {SERVER: {CHANNEL, MY_CHANNEL, CHANNEL_MEMBERSHIP}} = MM_TABLES;
|
||||
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[]) => {
|
||||
const allChannelsForTeam = await queryAllChannelsForTeam(operator.database, teamId);
|
||||
@@ -159,3 +160,29 @@ export const queryDefaultChannelForTeam = async (database: Database, teamId: str
|
||||
|
||||
return channel;
|
||||
};
|
||||
|
||||
export const queryCurrentChannel = async (database: Database) => {
|
||||
const currentChannelId = await queryCurrentChannelId(database);
|
||||
if (currentChannelId) {
|
||||
const channels = await queryChannelsById(database, [currentChannelId]);
|
||||
if (channels?.length) {
|
||||
return channels[0];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const deleteChannelMembership = async (operator: ServerDataOperator, userId: string, channelId: string) => {
|
||||
try {
|
||||
const channelMembership = await operator.database.get(CHANNEL_MEMBERSHIP).query(Q.where('user_id', Q.eq(userId)), Q.where('channel_id', Q.eq(channelId))).fetch();
|
||||
const models: Model[] = [];
|
||||
for (const membership of channelMembership) {
|
||||
models.push(membership.prepareDestroyPermanently());
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ type PrepareCommonSystemValuesArgs = {
|
||||
currentTeamId?: string;
|
||||
currentUserId?: string;
|
||||
license?: ClientLicense;
|
||||
teamHistory?: string;
|
||||
}
|
||||
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
@@ -117,6 +118,23 @@ export const queryWebSocketLastDisconnected = async (serverDatabase: Database) =
|
||||
}
|
||||
};
|
||||
|
||||
export const queryTeamHistory = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const teamHistory = await serverDatabase.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.TEAM_HISTORY);
|
||||
return (teamHistory.value) as string[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const patchTeamHistory = async (operator: ServerDataOperator, value: string[], prepareRecordsOnly = false) => {
|
||||
return operator.handleSystem({systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.TEAM_HISTORY,
|
||||
value: JSON.stringify(value),
|
||||
}],
|
||||
prepareRecordsOnly});
|
||||
};
|
||||
|
||||
export const prepareCommonSystemValues = (
|
||||
operator: ServerDataOperator, values: PrepareCommonSystemValuesArgs) => {
|
||||
try {
|
||||
|
||||
@@ -6,10 +6,11 @@ import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
|
||||
import {Database as DatabaseConstants, Preferences} from '@constants';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
|
||||
import {prepareDeleteChannel, queryDefaultChannelForTeam} from './channel';
|
||||
import {queryPreferencesByCategoryAndName} from './preference';
|
||||
import {queryConfig} from './system';
|
||||
import {patchTeamHistory, queryConfig, queryTeamHistory} from './system';
|
||||
import {queryCurrentUser} from './user';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
@@ -18,12 +19,16 @@ import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history';
|
||||
|
||||
const {MY_TEAM, TEAM, TEAM_CHANNEL_HISTORY} = DatabaseConstants.MM_TABLES.SERVER;
|
||||
const {MY_TEAM, TEAM, TEAM_CHANNEL_HISTORY, MY_CHANNEL} = DatabaseConstants.MM_TABLES.SERVER;
|
||||
|
||||
export const addChannelToTeamHistory = async (operator: ServerDataOperator, teamId: string, channelId: string, prepareRecordsOnly = false) => {
|
||||
let tch: TeamChannelHistory|undefined;
|
||||
|
||||
try {
|
||||
const myChannel = (await operator.database.get(MY_CHANNEL).find(channelId));
|
||||
if (!myChannel) {
|
||||
return [];
|
||||
}
|
||||
const teamChannelHistory = (await operator.database.get(TEAM_CHANNEL_HISTORY).find(teamId)) as TeamChannelHistoryModel;
|
||||
const channelIdSet = new Set(teamChannelHistory.channelIds);
|
||||
if (channelIdSet.has(channelId)) {
|
||||
@@ -65,6 +70,96 @@ export const queryLastChannelFromTeam = async (database: Database, teamId: strin
|
||||
return channelId;
|
||||
};
|
||||
|
||||
export const removeChannelFromTeamHistory = async (operator: ServerDataOperator, teamId: string, channelId: string, prepareRecordsOnly = false) => {
|
||||
let tch: TeamChannelHistory;
|
||||
|
||||
try {
|
||||
const teamChannelHistory = (await operator.database.get(TEAM_CHANNEL_HISTORY).find(teamId)) as TeamChannelHistoryModel;
|
||||
const channelIdSet = new Set(teamChannelHistory.channelIds);
|
||||
if (channelIdSet.has(channelId)) {
|
||||
channelIdSet.delete(channelId);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
const channelIds = Array.from(channelIdSet);
|
||||
tch = {
|
||||
id: teamId,
|
||||
channel_ids: channelIds,
|
||||
};
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return operator.handleTeamChannelHistory({teamChannelHistories: [tch], prepareRecordsOnly});
|
||||
};
|
||||
|
||||
export const addTeamToTeamHistory = async (operator: ServerDataOperator, teamId: string, prepareRecordsOnly = false) => {
|
||||
const teamHistory = (await queryTeamHistory(operator.database));
|
||||
const teamHistorySet = new Set(teamHistory);
|
||||
if (teamHistorySet.has(teamId)) {
|
||||
teamHistorySet.delete(teamId);
|
||||
}
|
||||
|
||||
const teamIds = Array.from(teamHistorySet);
|
||||
teamIds.unshift(teamId);
|
||||
return patchTeamHistory(operator, teamIds, prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const removeTeamFromTeamHistory = async (operator: ServerDataOperator, teamId: string, prepareRecordsOnly = false) => {
|
||||
const teamHistory = (await queryTeamHistory(operator.database));
|
||||
const teamHistorySet = new Set(teamHistory);
|
||||
if (!teamHistorySet.has(teamId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
teamHistorySet.delete(teamId);
|
||||
const teamIds = Array.from(teamHistorySet).slice(0, 5);
|
||||
|
||||
return patchTeamHistory(operator, teamIds, prepareRecordsOnly);
|
||||
};
|
||||
|
||||
export const queryLastTeam = async (database: Database) => {
|
||||
const teamHistory = (await queryTeamHistory(database));
|
||||
if (teamHistory.length > 0) {
|
||||
return teamHistory[0];
|
||||
}
|
||||
|
||||
return queryDefaultTeam(database);
|
||||
};
|
||||
|
||||
export const syncTeamTable = async (operator: ServerDataOperator, teams: Team[]) => {
|
||||
try {
|
||||
const notAvailable = await operator.database.get<TeamModel>(TEAM).query(Q.where('id', Q.notIn(teams.map((t) => t.id)))).fetch();
|
||||
const models = [];
|
||||
const deletions = await Promise.all(notAvailable.map((t) => prepareDeleteTeam(t)));
|
||||
for (const d of deletions) {
|
||||
models.push(...d);
|
||||
}
|
||||
models.push(...await operator.handleTeam({teams, prepareRecordsOnly: true}));
|
||||
await operator.batchRecords(models);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const queryDefaultTeam = async (database: Database) => {
|
||||
const user = await queryCurrentUser(database);
|
||||
const config = await queryConfig(database);
|
||||
const teamOrderPreferences = await queryPreferencesByCategoryAndName(database, Preferences.TEAMS_ORDER, '');
|
||||
let teamOrderPreference = '';
|
||||
if (teamOrderPreferences.length) {
|
||||
teamOrderPreference = teamOrderPreferences[0].value;
|
||||
}
|
||||
|
||||
const teamModels = await database.get<TeamModel>(TEAM).query(Q.on(MY_TEAM, Q.where('id', Q.notEq('')))).fetch();
|
||||
const teams = teamModels.map((t) => ({id: t.id, display_name: t.displayName, name: t.name} as Team));
|
||||
|
||||
const defaultTeam = selectDefaultTeam(teams, user?.locale || DEFAULT_LOCALE, teamOrderPreference, config.ExperimentalPrimaryTeam);
|
||||
return defaultTeam?.id;
|
||||
};
|
||||
|
||||
export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memberships: TeamMembership[]) => {
|
||||
try {
|
||||
const teamRecords = operator.handleTeam({prepareRecordsOnly: true, teams});
|
||||
@@ -85,13 +180,18 @@ export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memb
|
||||
};
|
||||
|
||||
export const deleteMyTeams = async (operator: ServerDataOperator, teams: TeamModel[]) => {
|
||||
const preparedModels: Model[] = [];
|
||||
for await (const team of teams) {
|
||||
const myTeam = await team.myTeam.fetch() as MyTeamModel;
|
||||
preparedModels.push(myTeam.prepareDestroyPermanently());
|
||||
}
|
||||
try {
|
||||
const preparedModels: Model[] = [];
|
||||
for await (const team of teams) {
|
||||
const myTeam = await team.myTeam.fetch() as MyTeamModel;
|
||||
preparedModels.push(myTeam.prepareDestroyPermanently());
|
||||
}
|
||||
|
||||
await operator.batchRecords(preparedModels);
|
||||
await operator.batchRecords(preparedModels);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareDeleteTeam = async (team: TeamModel): Promise<Model[]> => {
|
||||
|
||||
2
index.ts
2
index.ts
@@ -13,6 +13,7 @@ import {initialLaunch} from './app/init/launch';
|
||||
import ManagedApp from './app/init/managed_app';
|
||||
import NetworkManager from './app/init/network_manager';
|
||||
import PushNotifications from './app/init/push_notifications';
|
||||
import WebsocketManager from './app/init/websocket_manager';
|
||||
import {registerScreens} from './app/screens';
|
||||
import EphemeralStore from './app/store/ephemeral_store';
|
||||
import setFontFamily from './app/utils/font_family';
|
||||
@@ -57,6 +58,7 @@ Navigation.events().registerAppLaunchedListener(async () => {
|
||||
|
||||
await DatabaseManager.init(serverUrls);
|
||||
await NetworkManager.init(serverCredentials);
|
||||
await WebsocketManager.init(serverCredentials);
|
||||
PushNotifications.init();
|
||||
|
||||
initialLaunch();
|
||||
|
||||
@@ -29,7 +29,7 @@ target 'Mattermost' do
|
||||
pod 'Swime', '3.0.6'
|
||||
|
||||
# TODO: Remove this once upstream PR https://github.com/daltoniam/Starscream/pull/871 is merged
|
||||
pod 'Starscream', :git => 'https://github.com/mattermost/Starscream.git', :commit => '1b4b93708fb63d2665625a11e57461772a65364a'
|
||||
pod 'Starscream', :git => 'https://github.com/mattermost/Starscream.git', :commit => 'cb83dd247339ff6c155f0e749d6fe2cc145f5283'
|
||||
end
|
||||
|
||||
# Enables Flipper.
|
||||
|
||||
@@ -536,7 +536,7 @@ DEPENDENCIES:
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- "simdjson (from `../node_modules/@nozbe/simdjson`)"
|
||||
- Starscream (from `https://github.com/mattermost/Starscream.git`, commit `1b4b93708fb63d2665625a11e57461772a65364a`)
|
||||
- Starscream (from `https://github.com/mattermost/Starscream.git`, commit `cb83dd247339ff6c155f0e749d6fe2cc145f5283`)
|
||||
- Swime (= 3.0.6)
|
||||
- UMAppLoader (from `../node_modules/unimodules-app-loader/ios`)
|
||||
- "UMCore (from `../node_modules/@unimodules/core/ios`)"
|
||||
@@ -710,7 +710,7 @@ EXTERNAL SOURCES:
|
||||
simdjson:
|
||||
:path: "../node_modules/@nozbe/simdjson"
|
||||
Starscream:
|
||||
:commit: 1b4b93708fb63d2665625a11e57461772a65364a
|
||||
:commit: cb83dd247339ff6c155f0e749d6fe2cc145f5283
|
||||
:git: https://github.com/mattermost/Starscream.git
|
||||
UMAppLoader:
|
||||
:path: "../node_modules/unimodules-app-loader/ios"
|
||||
@@ -727,7 +727,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
Starscream:
|
||||
:commit: 1b4b93708fb63d2665625a11e57461772a65364a
|
||||
:commit: cb83dd247339ff6c155f0e749d6fe2cc145f5283
|
||||
:git: https://github.com/mattermost/Starscream.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
@@ -824,6 +824,6 @@ SPEC CHECKSUMS:
|
||||
Yoga: c11abbf5809216c91fcd62f5571078b83d9b6720
|
||||
YoutubePlayer-in-WKWebView: cfbf46da51d7370662a695a8f351e5fa1d3e1008
|
||||
|
||||
PODFILE CHECKSUM: d62332bb35b296f3ba5b63691519bc5b4dd1c289
|
||||
PODFILE CHECKSUM: b7ae70eab69dbc195f8f145338e347c9afbeb102
|
||||
|
||||
COCOAPODS: 1.10.2
|
||||
|
||||
1
types/api/config.d.ts
vendored
1
types/api/config.d.ts
vendored
@@ -76,6 +76,7 @@ interface ClientConfig {
|
||||
EnablePreviewFeatures: string;
|
||||
EnablePreviewModeBanner: string;
|
||||
EnablePublicLink: string;
|
||||
EnableReliableWebSockets: string;
|
||||
EnableSVGs: string;
|
||||
EnableSaml: string;
|
||||
EnableSignInWithEmail: string;
|
||||
|
||||
2
types/database/database.d.ts
vendored
2
types/database/database.d.ts
vendored
@@ -254,8 +254,6 @@ export type LoginArgs = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type LoadMeArgs = { user?: UserProfile; deviceToken?: string };
|
||||
|
||||
export type ServerUrlChangedArgs = {
|
||||
configRecord: System;
|
||||
licenseRecord: System;
|
||||
|
||||
2
types/database/raw_values.d.ts
vendored
2
types/database/raw_values.d.ts
vendored
@@ -70,7 +70,7 @@ type ReactionsPerPost = {
|
||||
|
||||
type IdValue = {
|
||||
id: string;
|
||||
value: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
type TeamChannelHistory = {
|
||||
|
||||
Reference in New Issue
Block a user