mattermost-mobile-upstream/app/actions/remote/channel.ts
Ben Schumacher 307da95d9d
[MM-69457] Stop logging PII in exportable app logs (#9884)
* MM-69457: Stop logging PII in exportable app logs

App logs are written to rolling files users can export via "Report a
Problem" and the same args are forwarded as Sentry breadcrumbs. Several
call sites logged PII into both destinations.

Fix the known high-risk sites and prevent regressions:
- push token, incoming-call payload (callerName), channel display names,
  and raw websocket url/event are no longer logged
- wrap stray raw error/event logging with getFullErrorMessage()
- add a no-restricted-syntax ESLint rule flagging PII (names/emails/
  usernames) and credentials (tokens/passwords/secrets) in log* calls
- document Logging & PII guidance in CLAUDE.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAfpPfhVfZSv3veGDQfPYQ

* Update websocket error test for PII-safe logging

The error handler no longer logs the raw url/event; assert it logs
code/reason instead and verify the url is not logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAfpPfhVfZSv3veGDQfPYQ

* fix: address PR review feedback on PII-safe logging

- Add server URL to websocket error log for multi-server debuggability
- Redact device token to first 4 chars instead of omitting entirely
- Update websocket error test to match new log signature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address PR review feedback on PII-safe logging

- files.ts: add hasRejectionReason boolean to initial event log
- websocket/index.ts: replace this.url (full WS URL with connection_id)
  with this.serverUrl in all log calls; update tests to match
- calls_native.ts: split combined serverId/channelId guard into two
  separate if blocks for clearer per-field log messages
- push_notifications.ts: expand redacted token prefix from 4 to 16 chars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: revert websocket lifecycle logs back to full URL

The full WebSocket URL (with path and connection_id) provides more
diagnostic value than just the server base URL for lifecycle events
(connect, reconnect, close, missed events). Only the onError handler
keeps this.serverUrl, which was the original targeted fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* cleanup

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:19:04 +02:00

1586 lines
64 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, removeChannelFromManagedCategoryIfNeeded, handleConvertedGMCategories, storeCategories} from '@actions/local/category';
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeAllMyChannels, storeMyChannelsForTeam, switchToChannel, deletePostsForChannel} from '@actions/local/channel';
import {switchToGlobalDrafts} from '@actions/local/draft';
import {switchToGlobalThreads} from '@actions/local/thread';
import {loadCallForChannel} from '@calls/actions/calls';
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {privateChannelJoinPrompt} from '@helpers/api/channel';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import AppsManager from '@managers/apps_manager';
import NetworkManager from '@managers/network_manager';
import {getActiveServer} from '@queries/app/servers';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {canViewArchivedChannels, getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory, getTeamById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import {navigateToRoot} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {setTeamLoading} from '@store/team_load_store';
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
import {getFullErrorMessage} from '@utils/errors';
import {isTablet} from '@utils/helpers';
import {logDebug, logError, logInfo} from '@utils/log';
import {showMuteChannelSnackbar} from '@utils/snack_bar';
import {displayGroupMessageName, displayUsername} from '@utils/user';
import {addChannelToManagedCategoryIfNeeded, fetchCategories} from './category';
import {fetchChannelBookmarks} from './channel_bookmark';
import {fetchGroupsForChannelIfConstrained} from './groups';
import {fetchPostsForChannel} from './post';
import {openChannelIfNeeded, savePreference} from './preference';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import {addCurrentUserToTeam, fetchTeamById, fetchTeamByName, removeCurrentUserFromTeam} from './team';
import {fetchProfilesInChannel, fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type {IntlShape} from 'react-intl';
export type MyChannelsRequest = {
teamId?: string;
categories?: CategoryWithChannels[];
channels?: Channel[];
memberships?: ChannelMembership[];
error?: unknown;
}
export type ChannelMembersRequest = {
members?: ChannelMembership[];
error?: unknown;
}
export async function removeMemberFromChannel(serverUrl: string, channelId: string, userId: string) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
await client.removeFromChannel(userId, channelId);
await deleteChannelMembership(operator, userId, channelId);
return {};
} catch (error) {
logDebug('error on removeMemberFromChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchChannelMembersByIds(serverUrl: string, channelId: string, userIds: string[], fetchOnly = false): Promise<ChannelMembersRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const members = await client.getChannelMembersByIds(channelId, userIds);
if (!fetchOnly) {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
if (operator && members.length) {
const memberships = members.map((u) => ({
channel_id: channelId,
user_id: u.user_id,
scheme_admin: u.scheme_admin,
}));
await operator.handleChannelMembership({
channelMemberships: memberships,
prepareRecordsOnly: false,
});
}
}
return {members};
} catch (error) {
logDebug('error on fetchChannelMembersByIds', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function updateChannelMemberSchemeRoles(serverUrl: string, channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.updateChannelMemberSchemeRoles(channelId, userId, isSchemeUser, isSchemeAdmin);
if (!fetchOnly) {
return fetchMemberInChannel(serverUrl, channelId, userId);
}
return {};
} catch (error) {
logDebug('error on updateChannelMemberSchemeRoles', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchMemberInChannel(serverUrl: string, channelId: string, userId: string, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const member = await client.getMemberInChannel(channelId, userId);
if (!fetchOnly) {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
if (userId === currentUserId) {
const isCRTEnabled = await getIsCRTEnabled(database);
const channel = await client.getChannel(channelId);
await fetchRolesIfNeeded(serverUrl, member.roles.split(' '), false, false);
await operator.handleMyChannel({channels: [channel], myChannels: [member], isCRTEnabled, prepareRecordsOnly: false});
} else {
await operator.handleChannelMembership({channelMemberships: [member], prepareRecordsOnly: false});
}
}
return {member};
} catch (error) {
logDebug('error on getMemberInChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchChannelMemberships(serverUrl: string, channelId: string, options: GetUsersOptions, fetchOnly = false) {
const {users = []} = await fetchProfilesInChannel(serverUrl, channelId, undefined, options, fetchOnly);
const userIds = users.map((u) => u.id);
// MM-49896 https://mattermost.atlassian.net/browse/MM-49896
// We are not sure the getChannelMembers API returns the same members
// from getProfilesInChannel. This guarantees a 1:1 match of the
// user IDs
const {members = []} = await fetchChannelMembersByIds(serverUrl, channelId, userIds, true);
return {users, members};
}
export async function addMembersToChannel(serverUrl: string, channelId: string, userIds: string[], postRootId = '', fetchOnly = false) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const promises = userIds.map((id) => client.addToChannel(id, channelId, postRootId));
const channelMemberships: ChannelMembership[] = await Promise.all(promises);
const {users} = await fetchUsersByIds(serverUrl, userIds, true);
if (!fetchOnly) {
const modelPromises: Array<Promise<Model[]>> = [];
if (users?.length) {
modelPromises.push(operator.handleUsers({
users,
prepareRecordsOnly: true,
}));
}
modelPromises.push(operator.handleChannelMembership({
channelMemberships,
prepareRecordsOnly: true,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'addMembersToChannel');
}
return {channelMemberships};
} catch (error) {
logDebug('error on addMembersToChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchChannelByName(serverUrl: string, teamId: string, channelName: string, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await client.getChannelByName(teamId, channelName, true);
if (!fetchOnly) {
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
}
return {channel};
} catch (error) {
logDebug('error on fetchChannelByName', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function createChannel(serverUrl: string, displayName: string, purpose: string, header: string, type: ChannelType) {
try {
EphemeralStore.creatingChannel = true;
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {currentUserId, currentTeamId} = await getCommonSystemValues(database);
const name = generateChannelNameFromDisplayName(displayName);
const channel = {
creator_id: currentUserId,
team_id: currentTeamId,
display_name: displayName,
header,
name,
purpose,
type,
} as Channel;
const channelData = await client.createChannel(channel);
const member = await client.getChannelMember(channelData.id, currentUserId);
const models: Model[] = [];
const channelModels = await prepareMyChannelsForTeam(operator, channelData.team_id, [channelData], [member]);
if (channelModels.length) {
const resolvedModels = await Promise.all(channelModels);
models.push(...resolvedModels.flat());
}
const categoriesModels = await addChannelToDefaultCategory(serverUrl, channelData, true);
if (categoriesModels.models?.length) {
models.push(...categoriesModels.models);
}
if (models.length) {
await operator.batchRecords(models, 'createChannel');
}
fetchChannelStats(serverUrl, channelData.id, false);
EphemeralStore.creatingChannel = false;
return {channel: channelData};
} catch (error) {
logDebug('error on createChannel', getFullErrorMessage(error));
EphemeralStore.creatingChannel = false;
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function patchChannel(serverUrl: string, channelId: string, channelPatch: ChannelPatch) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channelData = await client.patchChannel(channelId, channelPatch);
const models = [];
const channelInfo = (await getChannelInfo(database, channelData.id));
if (channelInfo && (channelInfo.purpose !== channelData.purpose || channelInfo.header !== channelData.header)) {
channelInfo.prepareUpdate((v) => {
v.purpose = channelData.purpose;
v.header = channelData.header;
});
models.push(channelInfo);
}
const channel = await getChannelById(database, channelData.id);
if (channel && (channel.displayName !== channelData.display_name || channel.type !== channelData.type || channel.autotranslation !== channelData.autotranslation)) {
channel.prepareUpdate((v) => {
// DM and GM display names cannot be patched and are formatted client-side; do not overwrite
if (channelData.type !== General.DM_CHANNEL && channelData.type !== General.GM_CHANNEL) {
v.displayName = channelData.display_name;
}
v.type = channelData.type;
if (channelData.autotranslation !== undefined) {
v.autotranslation = channelData.autotranslation;
}
});
models.push(channel);
}
if (models?.length) {
await operator.batchRecords(models, 'patchChannel');
}
return {channel: channelData};
} catch (error) {
logDebug('error on patchChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function leaveChannel(serverUrl: string, channelId: string) {
try {
EphemeralStore.addLeavingChannel(channelId);
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const isTabletDevice = isTablet();
const user = await getCurrentUser(database);
const models: Model[] = [];
if (!user) {
return {error: 'current user not found'};
}
const channelBeforeLeave = await getChannelById(database, channelId);
await client.removeFromChannel(user.id, channelId);
if (channelBeforeLeave?.teamId) {
await removeChannelFromManagedCategoryIfNeeded(serverUrl, channelBeforeLeave.teamId, channelId);
}
if (user.isGuest) {
const {models: updateVisibleModels} = await updateUsersNoLongerVisible(serverUrl, true);
if (updateVisibleModels) {
models.push(...updateVisibleModels);
}
}
const {models: removeUserModels} = await removeCurrentUserFromChannel(serverUrl, channelId, true);
if (removeUserModels) {
models.push(...removeUserModels);
}
await operator.batchRecords(models, 'leaveChannel');
if (isTabletDevice) {
switchToLastChannel(serverUrl);
} else {
setCurrentChannelId(operator, '');
}
return {};
} catch (error) {
logDebug('error on leaveChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
} finally {
EphemeralStore.removeLeavingChannel(channelId);
}
}
export async function fetchChannelCreator(serverUrl: string, channelId: string, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const channel = await getChannelById(database, channelId);
if (channel && channel.creatorId) {
const user = await client.getUser(channel.creatorId);
if (!fetchOnly) {
const modelPromises: Array<Promise<Model[]>> = [];
if (user.id !== currentUserId) {
modelPromises.push(operator.handleUsers({
users: [user],
prepareRecordsOnly: true,
}));
}
modelPromises.push(operator.handleChannelMembership({
channelMemberships: [{channel_id: channelId, user_id: channel.creatorId}],
prepareRecordsOnly: true,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchChannelCreator');
}
return {user};
}
return {user: undefined};
} catch (error) {
logDebug('error on fetchChannelCreator', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const stats = await client.getChannelStats(channelId, groupLabel);
if (!fetchOnly) {
const channel = await getChannelById(database, channelId);
if (channel) {
const channelInfos: Array<Partial<ChannelInfo>> = [{
guest_count: stats.guest_count,
id: channelId,
member_count: stats.member_count,
pinned_post_count: stats.pinnedpost_count,
files_count: stats.files_count,
}];
await operator.handleChannelInfo({channelInfos, prepareRecordsOnly: false});
}
}
return {stats};
} catch (error) {
logDebug('error on fetchChannelStats', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchAllMyChannelsForAllTeams(serverUrl: string, since: number, isCRTEnabled: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel) {
try {
if (!fetchOnly) {
setTeamLoading(serverUrl, true);
}
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
async function fetchAllMemberships() {
const results: ChannelMembership[] = [];
async function fetchNextMembershipsPage(page: number) {
try {
const memberships = await client.getAllMyChannelMembersFromAllTeams(page, General.CHANNEL_MEMBERS_CHUNK_SIZE, groupLabel);
results.push(...memberships);
if (memberships.length < General.CHANNEL_MEMBERS_CHUNK_SIZE) {
return;
}
await fetchNextMembershipsPage(page + 1);
} catch (e) {
logError(`fetchAllMyChannelsForAllTeams error: Failed to fetch memberships for page ${page} `, e);
}
}
await fetchNextMembershipsPage(0);
return results;
}
const promises: [Promise<Channel[]>, Promise<ChannelMembership[]>] = [
client.getAllChannelsFromAllTeams(since, since > 0, groupLabel),
fetchAllMemberships(),
];
const [channels, memberships] = await Promise.all(promises);
const teamIdsSet = channels.reduce<Set<string>>((set, c) => {
if (c.team_id) {
set.add(c.team_id);
}
return set;
}, new Set());
const teamIds = Array.from(teamIdsSet);
const categoriesResults = await Promise.all(teamIds.map((tid) => fetchCategories(serverUrl, tid, false, true, groupLabel)));
const categories: CategoryWithChannels[] = categoriesResults.flatMap((r) => r.categories ?? []);
if (!fetchOnly) {
const {models: chModels} = await storeAllMyChannels(serverUrl, channels, memberships, isCRTEnabled, true);
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
const models = (chModels || []).concat(catModels || []);
if (models.length) {
await operator.batchRecords(models, 'fetchAllMyChannelsForAllTeams');
}
setTeamLoading(serverUrl, false);
}
return {channels, memberships, categories};
} catch (error) {
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
if (!fetchOnly) {
setTeamLoading(serverUrl, false);
}
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchMyChannelsForTeam(
serverUrl: string, teamId: string, includeDeleted = true,
since = 0, fetchOnly = false, excludeDirect = false,
isCRTEnabled?: boolean, groupLabel?: RequestGroupLabel,
): Promise<MyChannelsRequest> {
try {
if (!fetchOnly) {
setTeamLoading(serverUrl, true);
}
const client = NetworkManager.getClient(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const [allChannels, channelMemberships, categoriesResult] = await Promise.all([
client.getMyChannels(teamId, includeDeleted, since, groupLabel),
client.getMyChannelMembers(teamId, groupLabel),
fetchCategories(serverUrl, teamId, false, true, groupLabel),
]);
let channels = allChannels;
let memberships = channelMemberships;
if (excludeDirect) {
channels = channels.filter((c) => !isDMorGM(c));
}
const channelIds = new Set<string>(channels.map((c) => c.id));
const categories = categoriesResult.categories ?? [];
memberships = memberships.reduce((result: ChannelMembership[], m: ChannelMembership) => {
if (channelIds.has(m.channel_id)) {
result.push(m);
}
return result;
}, []);
if (!fetchOnly) {
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, teamId, channels, memberships, true, isCRTEnabled);
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
const models = (chModels || []).concat(catModels || []);
if (models.length) {
await operator.batchRecords(models, 'fetchMyChannelsForTeam');
}
setTeamLoading(serverUrl, false);
}
return {channels, memberships, categories, teamId};
} catch (error) {
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
if (!fetchOnly) {
setTeamLoading(serverUrl, false);
}
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyChannelsRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const [channel, member] = await Promise.all([
client.getChannel(channelId, groupLabel),
client.getChannelMember(channelId, 'me', groupLabel),
]);
if (!fetchOnly) {
await storeMyChannelsForTeam(serverUrl, channel.team_id || teamId, [channel], [member]);
}
return {
channels: [channel],
memberships: [member],
};
} catch (error) {
logDebug('error on fetchMyChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchMissingDirectChannelsInfo(
serverUrl: string, directChannels: Channel[], locale?: string,
teammateDisplayNameSetting?: string, currentUserId?: string,
fetchOnly = false, groupLabel?: RequestGroupLabel,
) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
const channelIds = new Set(directChannels.map((c) => c.id));
const storedChannels = await queryChannelsById(database, Array.from(channelIds)).fetch();
const storedChannelsMap = new Map(storedChannels.map((c) => [c.id, c]));
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name && !storedChannelsMap.get(c.id)?.displayName) {
dmWithoutDisplayName.add(c.id);
}
continue;
}
gms.push(c);
}
const currentUser = await getCurrentUser(database);
// let's filter those channels that we already have the users
const membersCount = await getMembersCountByChannelsId(database, dmIds);
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 && dmWithoutDisplayName.has(id));
const results = await Promise.all([
profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false, groupLabel) : Promise.resolve({data: undefined}),
fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false, groupLabel),
]);
const profileRequests = results.flat();
for (const result of profileRequests) {
result.data?.forEach((data) => {
if (data.users?.length) {
users.push(...data.users);
if (data.users.length > 1) {
displayNameByChannel[data.channelId] = displayGroupMessageName(data.users, locale, teammateDisplayNameSetting, currentUserId);
} else {
displayNameByChannel[data.channelId] = displayUsername(data.users[0], locale, teammateDisplayNameSetting, false);
}
}
});
directChannels.forEach((c) => {
const displayName = displayNameByChannel[c.id];
if (displayName) {
c.display_name = displayName;
c.fake = true;
updatedChannels.add(c);
}
});
if (currentUserId) {
const ownDirectChannel = dms.find((dm) => dm.name === getDirectChannelName(currentUserId, currentUserId));
if (ownDirectChannel) {
ownDirectChannel.display_name = displayUsername(currentUser, locale, teammateDisplayNameSetting, false);
ownDirectChannel.fake = true;
updatedChannels.add(ownDirectChannel);
}
}
}
const updatedChannelsArray = Array.from(updatedChannels);
if (!fetchOnly) {
const modelPromises: Array<Promise<Model[]>> = [];
if (updatedChannelsArray.length) {
modelPromises.push(operator.handleChannel({channels: updatedChannelsArray, prepareRecordsOnly: true}));
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchMissingDirectChannelInfo');
}
return {directChannels: updatedChannelsArray, users};
} catch (error) {
logDebug('error on fetchMissingDirectChannelsInfo', getFullErrorMessage(error));
return {error};
}
}
export async function fetchDirectChannelsInfo(serverUrl: string, directChannels: ChannelModel[]) {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
const preferences = await queryDisplayNamePreferences(database).fetch();
const config = await getConfig(database);
const license = await getLicense(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
const currentUser = await getCurrentUser(database);
const channels = directChannels.map((d) => d.toApi());
return fetchMissingDirectChannelsInfo(serverUrl, channels, currentUser?.locale, teammateDisplayNameSetting, currentUser?.id);
}
export async function joinChannel(serverUrl: string, teamId: string, channelId?: string, channelName?: string, fetchOnly = false) {
let member: ChannelMembership | undefined;
let channel: Channel | undefined;
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
if (channelId) {
EphemeralStore.addJoiningChannel(channelId);
member = await client.addToChannel(userId, channelId);
channel = await client.getChannel(channelId);
} else if (channelName) {
channel = await client.getChannelByName(teamId, channelName, true);
EphemeralStore.addJoiningChannel(channel.id);
if (isDMorGM(channel)) {
member = await client.getChannelMember(channel.id, userId);
} else {
member = await client.addToChannel(userId, channel.id);
}
}
if (channel && member && !fetchOnly) {
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
const modelPromises: Array<Promise<Model[]>> = await prepareMyChannelsForTeam(operator, teamId, [channel], [member]);
if (modelPromises.length) {
const models = await Promise.all(modelPromises);
const flattenedModels: Model[] = models.flat();
const categoriesModels = await addChannelToDefaultCategory(serverUrl, channel, true);
if (categoriesModels.models?.length) {
flattenedModels.push(...categoriesModels.models);
}
if (flattenedModels?.length > 0) {
try {
await operator.batchRecords(flattenedModels, 'joinChannel');
} catch {
logError('FAILED TO BATCH CHANNELS');
}
}
}
await addChannelToManagedCategoryIfNeeded(serverUrl, channel);
}
} catch (error) {
logDebug('error on joinChannel', getFullErrorMessage(error));
if (channelId || channel?.id) {
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
}
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
if (channelId || channel?.id) {
loadCallForChannel(serverUrl, channelId || channel!.id);
EphemeralStore.removeJoiningChannel(channelId || channel!.id);
}
return {channel, member};
}
export async function joinChannelIfNeeded(serverUrl: string, channelId: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const myChannel = await getMyChannel(database, channelId);
if (myChannel) {
return {};
}
return joinChannel(serverUrl, '', channelId);
} catch (error) {
return {error};
}
}
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false, groupLabel?: RequestGroupLabel) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.viewMyChannel(channelId, undefined, groupLabel);
if (updateLocal) {
await markChannelAsViewed(serverUrl, channelId, true);
}
return {};
} catch (error) {
logDebug('error on markChannelAsRead', getFullErrorMessage(error));
return {error};
}
}
export async function unsetActiveChannelOnServer(serverUrl: string) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.viewMyChannel('');
return {};
} catch (error) {
logDebug('error on markChannelAsRead', getFullErrorMessage(error));
return {error};
}
}
export async function joinIfNeededAndSwitchToChannel(
serverUrl: string,
channelInfo: {id?: string; name?: string},
teamInfo: {id?: string; name?: string} | undefined,
errorHandler: (intl: IntlShape) => void,
intl: IntlShape,
) {
const onError = (joinedTeam: boolean, teamIdToRemove?: string) => {
errorHandler(intl);
if (joinedTeam && teamIdToRemove) {
removeCurrentUserFromTeam(serverUrl, teamIdToRemove, false);
}
};
let joinedTeam = false;
let teamId = teamInfo?.id || '';
let channelId = channelInfo?.id || '';
const channelName = channelInfo?.name || '';
const teamName = teamInfo?.name || '';
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
if (teamName === DeepLink.Redirect) {
teamId = await getCurrentTeamId(database);
} else {
const team = teamId ? await getTeamById(database, teamId) : await getTeamByName(database, teamName);
const isTeamMember = team ? Boolean(await getMyTeamById(database, team.id)) : false;
if (!teamId) {
teamId = team?.id || '';
}
if (!isTeamMember) {
const fetchRequest = teamId ? await fetchTeamById(serverUrl, teamId) : await fetchTeamByName(serverUrl, teamName);
if (!fetchRequest.team) {
onError(joinedTeam);
return {error: fetchRequest.error || 'no team received'};
}
const {error} = await addCurrentUserToTeam(serverUrl, fetchRequest.team.id);
if (error) {
onError(joinedTeam);
return {error};
}
teamId = fetchRequest.team.id;
joinedTeam = true;
}
}
const channel = channelId ? await getChannelById(database, channelId) : await getChannelByName(database, teamId, channelName);
const isChannelMember = channel ? await getMyChannel(database, channel.id) : false;
if (!channelId) {
channelId = channel?.id || '';
}
if (!isChannelMember) {
const fetchRequest = channelId ? await fetchChannelById(serverUrl, channelId) : await fetchChannelByName(serverUrl, teamId, channelName, true);
if (!fetchRequest.channel) {
onError(joinedTeam, teamId);
return {error: fetchRequest.error || 'cannot fetch channel'};
}
if (fetchRequest.channel.type === General.PRIVATE_CHANNEL) {
const {join} = await privateChannelJoinPrompt(fetchRequest.channel.display_name, intl);
if (!join) {
onError(joinedTeam, teamId);
return {error: 'Refused to join Private channel'};
}
}
logInfo('joining channel', fetchRequest.channel.id, 'type', fetchRequest.channel.type);
const joinRequest = await joinChannel(serverUrl, teamId, channelId, channelName, false);
if (!joinRequest.channel) {
onError(joinedTeam, teamId);
return {error: joinRequest.error || 'no channel returned from join'};
}
channelId = fetchRequest.channel.id;
}
await switchToChannelById(serverUrl, channelId, teamId);
return {};
} catch (error) {
onError(joinedTeam, teamId);
return {error};
}
}
export async function createDirectChannel(serverUrl: string, userId: string, displayName = '') {
try {
EphemeralStore.creatingDMorGMTeammates = [userId];
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const currentUser = await getCurrentUser(database);
if (!currentUser) {
return {error: 'Cannot get the current user'};
}
const channelName = getDirectChannelName(currentUser.id, userId);
const channel = await getChannelByName(database, '', channelName);
if (channel) {
openChannelIfNeeded(serverUrl, channel.id);
return {data: channel.toApi()};
}
const created = await client.createDirectChannel([userId, currentUser.id]);
const profiles: UserProfile[] = [];
if (displayName) {
created.display_name = displayName;
} else {
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const license = await getLicense(database);
const config = await getConfig(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
created.display_name = (directChannels?.length && directChannels?.[0].display_name) || created.display_name;
if (users?.length) {
profiles.push(...users);
}
}
const member = {
channel_id: created.id,
user_id: currentUser.id,
roles: `${General.CHANNEL_USER_ROLE}`,
last_viewed_at: 0,
msg_count: 0,
mention_count: 0,
msg_count_root: 0,
mention_count_root: 0,
notify_props: {desktop: 'default' as const, mark_unread: 'all' as const},
last_update_at: created.create_at,
};
const models = [];
const preferences = [
{user_id: currentUser.id, category: Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, name: userId, value: 'true'},
{user_id: currentUser.id, category: Preferences.CATEGORIES.CHANNEL_OPEN_TIME, name: created.id, value: new Date().getTime().toString()},
];
const preferenceModels = await savePreference(serverUrl, preferences, true);
if (preferenceModels.preferences?.length) {
models.push(...preferenceModels.preferences);
}
const channelPromises = await prepareMyChannelsForTeam(operator, '', [created], [member, {...member, user_id: userId}]);
if (channelPromises.length) {
const channelModels = await Promise.all(channelPromises);
const flattenedChannelModels = channelModels.flat();
if (flattenedChannelModels.length) {
models.push(...flattenedChannelModels);
}
const categoryModels = await addChannelToDefaultCategory(serverUrl, created, true);
if (categoryModels.models?.length) {
models.push(...categoryModels.models);
}
}
if (profiles.length) {
const userModels = await operator.handleUsers({users: profiles, prepareRecordsOnly: true});
models.push(...userModels);
}
await operator.batchRecords(models, 'createDirectChannel');
EphemeralStore.creatingDMorGMTeammates = [];
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
return {data: created};
} catch (error) {
logDebug('error on createDirectChannel', getFullErrorMessage(error));
EphemeralStore.creatingDMorGMTeammates = [];
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
try {
const client = NetworkManager.getClient(serverUrl);
const channels = await client.getChannels(teamId, page, perPage);
return {channels};
} catch (error) {
logDebug('error on fetchChannels', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function makeDirectChannel(serverUrl: string, userId: string, displayName = '', shouldSwitchToChannel = true) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const channelName = getDirectChannelName(userId, currentUserId);
let channel: Channel|ChannelModel|undefined = await getChannelByName(database, '', channelName);
let result: {data?: Channel|ChannelModel; error?: any};
if (channel) {
result = {data: channel};
} else {
result = await createDirectChannel(serverUrl, userId, displayName);
channel = result.data;
}
if (channel && shouldSwitchToChannel) {
switchToChannelById(serverUrl, channel.id);
}
return result;
} catch (error) {
return {error};
}
}
export async function fetchArchivedChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
try {
const client = NetworkManager.getClient(serverUrl);
const channels = await client.getArchivedChannels(teamId, page, perPage);
return {channels};
} catch (error) {
logDebug('error on fetchArchivedChannels', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function createGroupChannel(serverUrl: string, userIds: string[]) {
try {
EphemeralStore.creatingDMorGMTeammates = userIds;
const client = NetworkManager.getClient(serverUrl);
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUser = await getCurrentUser(database);
if (!currentUser) {
return {error: 'Cannot get the current user'};
}
const created = await client.createGroupChannel(userIds);
// Check the channel previous existency: if the channel already have
// posts is because it existed before.
if (created.total_msg_count > 0) {
openChannelIfNeeded(serverUrl, created.id);
EphemeralStore.creatingChannel = false;
return {data: created};
}
const displayNamePreferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const license = await getLicense(database);
const config = await getConfig(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(displayNamePreferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
const member = {
channel_id: created.id,
user_id: '',
roles: `${General.CHANNEL_USER_ROLE}`,
last_viewed_at: 0,
msg_count: 0,
mention_count: 0,
msg_count_root: 0,
mention_count_root: 0,
notify_props: {desktop: 'default' as const, mark_unread: 'all' as const},
last_update_at: created.create_at,
};
const members = userIds.map((id) => {
return {...member, user_id: id};
});
if (directChannels?.length) {
const channelPromises = await prepareMyChannelsForTeam(operator, '', directChannels, members);
if (channelPromises.length) {
const channelModels = await Promise.all(channelPromises);
const models: Model[] = channelModels.flat();
const userModels = await operator.handleUsers({users, prepareRecordsOnly: true});
const categoryModels = await addChannelToDefaultCategory(serverUrl, created, true);
if (categoryModels.models?.length) {
models.push(...categoryModels.models);
}
const preferences = [
{user_id: currentUser.id, category: Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, name: created.id, value: 'true'},
{user_id: currentUser.id, category: Preferences.CATEGORIES.CHANNEL_OPEN_TIME, name: created.id, value: new Date().getTime().toString()},
];
const preferenceModels = await savePreference(serverUrl, preferences, true);
if (preferenceModels.preferences?.length) {
models.push(...preferenceModels.preferences);
}
models.push(...userModels);
await operator.batchRecords(models, 'createGroupChannel');
}
}
EphemeralStore.creatingDMorGMTeammates = [];
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
return {data: created};
} catch (error) {
logDebug('error on createGroupChannel', getFullErrorMessage(error));
EphemeralStore.creatingDMorGMTeammates = [];
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchSharedChannels(serverUrl: string, teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE) {
try {
const client = NetworkManager.getClient(serverUrl);
const channels = await client.getSharedChannels(teamId, page, perPage);
return {channels};
} catch (error) {
logDebug('error on fetchSharedChannels', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchRemoteClusters(serverUrl: string) {
try {
const client = NetworkManager.getClient(serverUrl);
const remoteClusters = await client.getRemoteClusters({
excludePlugins: true,
onlyConfirmed: true,
});
return {remoteClusters};
} catch (error) {
logDebug('error on fetchRemoteClusters', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function fetchChannelSharedRemotes(serverUrl: string, channelId: string) {
try {
const client = NetworkManager.getClient(serverUrl);
const remotes = await client.getChannelSharedRemotes(channelId);
return {remotes};
} catch (error) {
logDebug('error on fetchChannelSharedRemotes', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function shareChannelWithRemote(serverUrl: string, channelId: string, remoteId: string) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.shareChannelWithRemote(channelId, remoteId);
return {};
} catch (error) {
logDebug('error on shareChannelWithRemote', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function unshareChannelFromRemote(serverUrl: string, channelId: string, remoteId: string) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.unshareChannelFromRemote(channelId, remoteId);
return {};
} catch (error) {
logDebug('error on unshareChannelFromRemote', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export async function makeGroupChannel(serverUrl: string, userIds: string[], shouldSwitchToChannel = true) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentUserId = await getCurrentUserId(database);
const result = await createGroupChannel(serverUrl, [currentUserId, ...userIds]);
const channel = result.data;
if (channel && shouldSwitchToChannel) {
switchToChannelById(serverUrl, channel.id);
}
return result;
} catch (error) {
return {error};
}
}
export async function getChannelMemberCountsByGroup(serverUrl: string, channelId: string, includeTimezones: boolean) {
try {
const client = NetworkManager.getClient(serverUrl);
const channelMemberCountsByGroup = await client.getChannelMemberCountsByGroup(channelId, includeTimezones);
return {channelMemberCountsByGroup};
} catch (error) {
logDebug('error on getChannelMemberCountsByGroup', getFullErrorMessage(error));
return {error};
}
}
export async function getChannelTimezones(serverUrl: string, channelId: string) {
try {
const client = NetworkManager.getClient(serverUrl);
const channelTimezones = await client.getChannelTimezones(channelId);
return {channelTimezones};
} catch (error) {
logDebug('error on getChannelTimezones', getFullErrorMessage(error));
return {error};
}
}
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, groupLabel?: RequestGroupLabel) {
if (channelId === Screens.GLOBAL_THREADS) {
return switchToGlobalThreads(serverUrl, teamId);
} else if (channelId === Screens.GLOBAL_DRAFTS) {
return switchToGlobalDrafts(serverUrl, teamId);
}
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
DeviceEventEmitter.emit(Events.BLUR_AND_DISMISS_KEYBOARD);
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, true);
fetchPostsForChannel(serverUrl, channelId, false, false, groupLabel);
fetchChannelBookmarks(serverUrl, channelId, false, groupLabel);
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
openChannelIfNeeded(serverUrl, channelId, groupLabel);
markChannelAsRead(serverUrl, channelId, false, groupLabel);
fetchChannelStats(serverUrl, channelId, false, groupLabel);
fetchGroupsForChannelIfConstrained(serverUrl, channelId, false, groupLabel);
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, false);
if (await AppsManager.isAppsEnabled(serverUrl)) {
AppsManager.fetchBindings(serverUrl, channelId, false, groupLabel);
}
return {};
}
export async function switchToPenultimateChannel(serverUrl: string, teamId?: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const teamIdToUse = teamId || await getCurrentTeamId(database);
const channelId = await getNthLastChannelFromTeam(database, teamIdToUse, 1);
return switchToChannelById(serverUrl, channelId);
} catch (error) {
return {error};
}
}
export async function switchToLastChannel(serverUrl: string, teamId?: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const teamIdToUse = teamId || await getCurrentTeamId(database);
const channelId = await getNthLastChannelFromTeam(database, teamIdToUse);
return switchToChannelById(serverUrl, channelId);
} catch (error) {
return {error};
}
}
export async function searchChannels(serverUrl: string, term: string, teamId: string, isSearch = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const autoCompleteFunc = isSearch ? client.autocompleteChannelsForSearch : client.autocompleteChannels;
const channels = await autoCompleteFunc(teamId, term);
return {channels};
} catch (error) {
logDebug('error on searchChannels', getFullErrorMessage(error));
return {error};
}
}
export async function fetchChannelById(serverUrl: string, id: string) {
try {
const client = NetworkManager.getClient(serverUrl);
const channel = await client.getChannel(id);
return {channel};
} catch (error) {
logDebug('error on fetchChannelById', getFullErrorMessage(error));
return {error};
}
}
export async function searchAllChannels(serverUrl: string, term: string, archivedOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const myTeamIds = await queryMyTeams(database).fetchIds();
const channels = await client.searchAllChannels(term, myTeamIds, archivedOnly);
return {channels};
} catch (error) {
logDebug('error on searchAllChannels', getFullErrorMessage(error));
return {error};
}
}
export const updateChannelNotifyProps = async (serverUrl: string, channelId: string, props: Partial<ChannelNotifyProps>) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
const notifyProps = {...props, channel_id: channelId, user_id: userId} as ChannelNotifyProps & {channel_id: string; user_id: string};
await client.updateChannelNotifyProps(notifyProps);
return {
notifyProps,
};
} catch (error) {
logDebug('error on updateChannelNotifyProps', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const setChannelAutotranslation = async (serverUrl: string, channelId: string, enabled: boolean) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channelData = await client.setChannelAutotranslation(channelId, enabled);
const models = [];
const channel = await getChannelById(database, channelData.id);
const autotranslationDisabled = channel && channel.autotranslation && !channelData.autotranslation;
if (channel && channel.autotranslation !== channelData.autotranslation) {
channel.prepareUpdate((v) => {
v.autotranslation = channelData.autotranslation ?? false;
});
models.push(channel);
}
if (models?.length) {
await operator.batchRecords(models, 'setChannelAutotranslation');
}
// Delete posts when autotranslation setting changes
if (autotranslationDisabled) {
await deletePostsForChannel(serverUrl, channelData.id);
}
return {channel: channelData};
} catch (error) {
logDebug('error on setChannelAutotranslation', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const setMyChannelAutotranslation = async (serverUrl: string, channelId: string, enabled: boolean) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
await client.setMyChannelAutotranslation(channelId, enabled);
const models = [];
const myChannel = await getMyChannel(database, channelId);
const autotranslationChanged = myChannel && myChannel.autotranslationDisabled !== !enabled;
if (myChannel && autotranslationChanged) {
myChannel.prepareUpdate((v) => {
v.autotranslationDisabled = !enabled;
});
models.push(myChannel);
}
if (models?.length) {
await operator.batchRecords(models, 'setMyChannelAutotranslation');
}
// Delete posts when autotranslation setting changes
if (autotranslationChanged) {
await deletePostsForChannel(serverUrl, channelId);
}
return {data: true};
} catch (error) {
logDebug('error on setMyChannelAutotranslation', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const toggleMuteChannel = async (serverUrl: string, channelId: string, showSnackBar = false) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channelSettings = await queryMyChannelSettingsByIds(database, [channelId]).fetch();
const myChannelSetting = channelSettings?.[0];
const mark_unread = myChannelSetting.notifyProps?.mark_unread === 'mention' ? 'all' : 'mention';
const notifyProps: Partial<ChannelNotifyProps> = {...myChannelSetting.notifyProps, mark_unread};
await updateChannelNotifyProps(serverUrl, channelId, notifyProps);
await database.write(async () => {
await myChannelSetting.update((c) => {
c.notifyProps = notifyProps;
});
});
if (showSnackBar) {
const onUndo = () => toggleMuteChannel(serverUrl, channelId, false);
showMuteChannelSnackbar(mark_unread === 'mention', onUndo);
}
return {
notifyProps,
};
} catch (error) {
return {error};
}
};
export const archiveChannel = async (serverUrl: string, channelId: string) => {
try {
EphemeralStore.addArchivingChannel(channelId);
const client = NetworkManager.getClient(serverUrl);
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channelBeforeArchive = await getChannelById(database, channelId);
await client.deleteChannel(channelId);
if (await canViewArchivedChannels(database)) {
await setChannelDeleteAt(serverUrl, channelId, Date.now());
} else {
removeCurrentUserFromChannel(serverUrl, channelId);
}
if (channelBeforeArchive?.teamId) {
await removeChannelFromManagedCategoryIfNeeded(serverUrl, channelBeforeArchive.teamId, channelId);
}
return {};
} catch (error) {
logDebug('error on archiveChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
} finally {
EphemeralStore.removeArchivingChannel(channelId);
}
};
export const unarchiveChannel = async (serverUrl: string, channelId: string) => {
try {
EphemeralStore.addArchivingChannel(channelId);
const client = NetworkManager.getClient(serverUrl);
await client.unarchiveChannel(channelId);
await setChannelDeleteAt(serverUrl, channelId, 0);
return {};
} catch (error) {
logDebug('error on unarchiveChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
} finally {
EphemeralStore.removeArchivingChannel(channelId);
}
};
export const convertChannelToPrivate = async (serverUrl: string, channelId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
if (channel) {
EphemeralStore.addConvertingChannel(channelId);
}
await client.convertChannelToPrivate(channelId);
if (channel) {
channel.prepareUpdate((c) => {
c.type = General.PRIVATE_CHANNEL;
});
await operator.batchRecords([channel], 'convertChannelToPrivate');
}
return {};
} catch (error) {
logDebug('error on convertChannelToPrivate', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
} finally {
EphemeralStore.removeConvertingChannel(channelId);
}
};
export const handleKickFromChannel = async (serverUrl: string, channelId: string, event: string = Events.LEAVE_CHANNEL) => {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentChannelId = await getCurrentChannelId(database);
if (currentChannelId !== channelId) {
return {};
}
const currentServer = await getActiveServer();
if (currentServer?.url === serverUrl) {
const channel = await getChannelById(database, channelId);
if (channel) {
DeviceEventEmitter.emit(event, channel.displayName);
await navigateToRoot();
}
}
const tabletDevice = isTablet();
if (tabletDevice) {
const teamId = await getCurrentTeamId(database);
await removeChannelFromTeamHistory(operator, teamId, channelId);
const newChannelId = await getNthLastChannelFromTeam(database, teamId, 0, channelId);
if (newChannelId) {
if (currentServer?.url === serverUrl) {
if (newChannelId === Screens.GLOBAL_THREADS) {
await switchToGlobalThreads(serverUrl, teamId, false);
} else if (newChannelId === Screens.GLOBAL_DRAFTS) {
await switchToGlobalDrafts(serverUrl, teamId);
} else {
await switchToChannelById(serverUrl, newChannelId, teamId, true);
}
} else {
await setCurrentTeamAndChannelId(operator, teamId, channelId);
}
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
} else {
await setCurrentChannelId(operator, '');
}
return {};
} catch (error) {
logDebug('cannot kick user from channel', error);
return {error};
}
};
export const fetchGroupMessageMembersCommonTeams = async (serverUrl: string, channelId: string) => {
try {
const client = NetworkManager.getClient(serverUrl);
const teams = await client.getGroupMessageMembersCommonTeams(channelId);
return {teams};
} catch (error) {
logDebug('error on getGroupMessageMembersCommonTeams', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export async function switchToChannelByName(serverUrl: string, channelName: string, teamName?: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let teamId = '';
if (teamName) {
const team = await getTeamByName(database, teamName);
teamId = team?.id || '';
}
if (!teamId) {
teamId = await getCurrentTeamId(database) || '';
}
let channelId = '';
const channel = await getChannelByName(database, teamId, channelName);
if (channel) {
channelId = channel.id;
} else {
const fetchResult = await fetchChannelByName(serverUrl, teamId, channelName, true);
if (fetchResult.channel) {
channelId = fetchResult.channel.id;
}
}
if (channelId) {
await switchToChannelById(serverUrl, channelId, teamId);
}
return {};
} catch (error) {
logDebug('error on switchToChannelByName', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
}
export const convertGroupMessageToPrivateChannel = async (serverUrl: string, channelId: string, targetTeamId: string, displayName: string) => {
try {
const name = generateChannelNameFromDisplayName(displayName);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const existingChannel = await getChannelById(database, channelId);
if (existingChannel) {
EphemeralStore.addConvertingChannel(channelId);
}
const client = NetworkManager.getClient(serverUrl);
const updatedChannel = await client.convertGroupMessageToPrivateChannel(channelId, targetTeamId, displayName, name);
if (existingChannel) {
existingChannel.prepareUpdate((channel) => {
channel.type = General.PRIVATE_CHANNEL;
channel.displayName = displayName;
channel.name = name;
channel.teamId = targetTeamId;
});
const models: Model[] = [existingChannel];
const {models: categoryUpdateModels} = await handleConvertedGMCategories(serverUrl, channelId, targetTeamId, true);
if (categoryUpdateModels) {
models.push(...categoryUpdateModels);
}
await operator.batchRecords(models, 'convertGroupMessageToPrivateChannel');
}
return {updatedChannel};
} catch (error) {
logError('error on convertGroupMessageToPrivateChannel', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
return {error};
} finally {
EphemeralStore.removeConvertingChannel(channelId);
}
};