Files
mattermost-mobile/app/actions/local/channel.ts
Daniel Espino García 980c31f40f Remove watermelondb limitation on updating an already updated model (#7067)
* Remove watermelondb limitation on updating an already updated model

* Add logic to handle different prepare states and improve logging

* fix tests

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-02-03 17:11:57 +02:00

460 lines
18 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceEventEmitter} from 'react-native';
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {extractChannelDisplayName} from '@helpers/database';
import PushNotifications from '@init/push_notifications';
import {
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {isTablet} from '@utils/helpers';
import {logError, logInfo} from '@utils/log';
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
export async function switchToChannel(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let models: Model[] = [];
const dt = Date.now();
const isTabletDevice = await isTablet();
const system = await getCommonSystemValues(database);
const member = await getMyChannel(database, channelId);
EphemeralStore.addSwitchingToChannel(channelId);
if (member) {
const channel = await member.channel.fetch();
if (channel) {
if (!channel.teamId && teamId) {
const team = await getTeamById(database, teamId);
if (!team) {
return {error: `team with id ${teamId} not found`};
}
}
const toTeamId = channel.teamId || teamId || system.currentTeamId;
if (isTabletDevice && system.currentChannelId !== channelId) {
// On tablet, the channel is being rendered, by setting the channel to empty first we speed up
// the switch by ~3x
await setCurrentChannelId(operator, '');
}
const modelPromises: Array<Promise<Model[]>> = [];
if (system.currentTeamId !== toTeamId) {
modelPromises.push(addTeamToTeamHistory(operator, toTeamId, true));
}
const commonValues: PrepareCommonSystemValuesArgs = {
lastUnreadChannelId: member.isUnread && !skipLastUnread ? channelId : '',
};
if ((system.currentTeamId !== toTeamId) || (system.currentChannelId !== channelId)) {
commonValues.currentChannelId = system.currentChannelId === channelId ? undefined : channelId;
commonValues.currentTeamId = system.currentTeamId === toTeamId ? undefined : toTeamId;
}
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
if (system.currentChannelId !== channelId || system.currentTeamId !== toTeamId) {
modelPromises.push(addChannelToTeamHistory(operator, toTeamId, channelId, true));
}
models = (await Promise.all(modelPromises)).flat();
const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, false, true);
if (viewedAt) {
models.push(viewedAt);
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'switchToChannel');
}
if (isTabletDevice) {
dismissAllModalsAndPopToRoot();
DeviceEventEmitter.emit(NavigationConstants.NAVIGATION_HOME, Screens.CHANNEL);
} else {
dismissAllModalsAndPopToScreen(Screens.CHANNEL, '', undefined, {topBar: {visible: false}});
}
logInfo('channel switch to', channel?.displayName, channelId, (Date.now() - dt), 'ms');
}
}
return {models};
} catch (error) {
logError('Failed to switch to channelId', channelId, 'teamId', teamId, 'error', error);
return {error};
}
}
export async function removeCurrentUserFromChannel(serverUrl: string, channelId: string, prepareRecordsOnly = false) {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const models: Model[] = [];
const myChannel = await getMyChannel(database, channelId);
if (myChannel) {
const channel = await myChannel.channel.fetch();
if (!channel) {
throw new Error('myChannel present but no channel on the database');
}
models.push(...await prepareDeleteChannel(channel));
let teamId = channel.teamId;
if (teamId) {
teamId = await getCurrentTeamId(database);
}
// We update the history ASAP to avoid clashes with channel switch.
await removeChannelFromTeamHistory(operator, teamId, channel.id, false);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'removeCurrentUserFromChannel');
}
}
return {models};
} catch (error) {
logError('failed to removeCurrentUserFromChannel', error);
return {error};
}
}
export async function setChannelDeleteAt(serverUrl: string, channelId: string, deleteAt: number) {
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
if (!channel) {
return;
}
const model = channel.prepareUpdate((c) => {
c.deleteAt = deleteAt;
});
await operator.batchRecords([model], 'setChannelDeleteAt');
} catch (error) {
logError('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT', error);
}
}
export async function selectAllMyChannelIds(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
return queryAllMyChannel(database).fetchIds();
} catch {
return [];
}
}
export async function markChannelAsViewed(serverUrl: string, channelId: string, onlyCounts = false, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const member = await getMyChannel(database, channelId);
if (!member) {
return {error: 'not a member'};
}
member.prepareUpdate((m) => {
m.isUnread = false;
m.mentionsCount = 0;
m.manuallyUnread = false;
if (!onlyCounts) {
m.viewedAt = member.lastViewedAt;
m.lastViewedAt = Date.now();
}
});
PushNotifications.removeChannelNotifications(serverUrl, channelId);
if (!prepareRecordsOnly) {
await operator.batchRecords([member], 'markChannelAsViewed');
}
return {member};
} catch (error) {
logError('Failed markChannelAsViewed', error);
return {error};
}
}
export async function markChannelAsUnread(serverUrl: string, channelId: string, messageCount: number, mentionsCount: number, lastViewed: number, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const member = await getMyChannel(database, channelId);
if (!member) {
return {error: 'not a member'};
}
member.prepareUpdate((m) => {
m.viewedAt = lastViewed - 1;
m.lastViewedAt = lastViewed - 1;
m.messageCount = messageCount;
m.mentionsCount = mentionsCount;
m.manuallyUnread = true;
m.isUnread = true;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([member], 'markChannelAsUnread');
}
return {member};
} catch (error) {
logError('Failed markChannelAsUnread', error);
return {error};
}
}
export async function resetMessageCount(serverUrl: string, channelId: string) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const member = await getMyChannel(database, channelId);
if (!member) {
return {error: 'not a member'};
}
member.prepareUpdate((m) => {
m.messageCount = 0;
});
await operator.batchRecords([member], 'resetMessageCount');
return member;
} catch (error) {
logError('Failed resetMessageCount', error);
return {error};
}
}
export async function storeMyChannelsForTeam(serverUrl: string, teamId: string, channels: Channel[], memberships: ChannelMembership[], prepareRecordsOnly = false, isCRTEnabled?: boolean) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const modelPromises: Array<Promise<Model[]>> = [
...await prepareMyChannelsForTeam(operator, teamId, channels, memberships, isCRTEnabled),
];
const models = await Promise.all(modelPromises);
if (!models.length) {
return {models: []};
}
const flattenedModels = models.flat();
if (prepareRecordsOnly) {
return {models: flattenedModels};
}
if (flattenedModels.length) {
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
}
return {models: flattenedModels};
} catch (error) {
logError('Failed storeMyChannelsForTeam', error);
return {error};
}
}
export async function updateMyChannelFromWebsocket(serverUrl: string, channelMember: ChannelMembership, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const member = await getMyChannel(database, channelMember.channel_id);
if (member) {
member.prepareUpdate((m) => {
m.roles = channelMember.roles;
});
if (!prepareRecordsOnly) {
operator.batchRecords([member], 'updateMyChannelFromWebsocket');
}
}
return {model: member};
} catch (error) {
logError('Failed updateMyChannelFromWebsocket', error);
return {error};
}
}
export async function updateChannelInfoFromChannel(serverUrl: string, channel: Channel, prepareRecordsOnly = false) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const newInfo = await operator.handleChannelInfo({channelInfos: [{
header: channel.header,
purpose: channel.purpose,
id: channel.id,
}],
prepareRecordsOnly: true});
if (!prepareRecordsOnly) {
operator.batchRecords(newInfo, 'updateChannelInfoFromChannel');
}
return {model: newInfo};
} catch (error) {
logError('Failed updateChannelInfoFromChannel', error);
return {error};
}
}
export async function updateLastPostAt(serverUrl: string, channelId: string, lastPostAt: number, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const myChannel = await getMyChannel(database, channelId);
if (!myChannel) {
return {error: 'not a member'};
}
if (lastPostAt > myChannel.lastPostAt) {
myChannel.resetPreparedState();
myChannel.prepareUpdate((m) => {
m.lastPostAt = lastPostAt;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([myChannel], 'updateLastPostAt');
}
return {member: myChannel};
}
return {member: undefined};
} catch (error) {
logError('Failed updateLastPostAt', error);
return {error};
}
}
export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId: string, lastFetchedAt: number, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const myChannel = await getMyChannel(database, channelId);
if (!myChannel) {
return {error: 'not a member'};
}
if (lastFetchedAt > myChannel.lastFetchedAt) {
myChannel.resetPreparedState();
myChannel.prepareUpdate((m) => {
m.lastFetchedAt = lastFetchedAt;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([myChannel], 'updateMyChannelLastFetchedAt');
}
return {member: myChannel};
}
return {member: undefined};
} catch (error) {
logError('Failed updateLastFetchedAt', error);
return {error};
}
}
type User = UserProfile | UserModel;
export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], users: User[], prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUser = await getCurrentUser(database);
if (!currentUser) {
return {};
}
const license = await getLicense(database);
const config = await getConfig(database);
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const models: Model[] = [];
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, false);
} else {
const dbProfiles = await queryUsersOnChannel(database, channel.id).fetch();
const profileIds = new Set(dbProfiles.map((p) => p.id));
const gmUsers = users.filter((u) => profileIds.has(u.id));
if (gmUsers.length) {
const uIds = new Set(gmUsers.map((u) => u.id));
const newProfiles: Array<UserModel|UserProfile> = dbProfiles.filter((u) => !uIds.has(u.id));
newProfiles.push(...gmUsers);
newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id);
}
}
if (newDisplayName && channel.displayName !== newDisplayName) {
channel.prepareUpdate((c) => {
c.displayName = extractChannelDisplayName({
type: c.type,
display_name: newDisplayName,
fake: true,
}, c);
});
models.push(channel);
}
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'updateChannelsDisplayName');
}
return {models};
} catch (error) {
logError('Failed updateChannelsDisplayName', error);
return {error};
}
}
export async function showUnreadChannelsOnly(serverUrl: string, onlyUnreads: boolean) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
return operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.ONLY_UNREADS,
value: JSON.stringify(onlyUnreads),
}],
prepareRecordsOnly: false,
});
} catch (error) {
logError('Failed showUnreadChannelsOnly', error);
return {error};
}
}
export const updateDmGmDisplayName = async (serverUrl: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
if (!currentUserId) {
return {error: 'The current user id could not be retrieved from the database'};
}
const channels = await queryUserChannelsByTypes(database, currentUserId, ['G', 'D']).fetch();
const userIds = channels.reduce((acc: string[], ch) => {
if (ch.type !== General.DM_CHANNEL) {
return acc;
}
const uid = getUserIdFromChannelName(currentUserId, ch.name);
acc.push(uid);
return acc;
}, []);
const users = await queryUsersById(database, userIds).fetch();
await updateChannelsDisplayName(serverUrl, channels, users, false);
return {channels};
} catch (error) {
logError('Failed updateDmGmDisplayName', error);
return {error};
}
};