diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 793da32fa3..f53e6bd866 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -27,6 +27,7 @@ import {showMuteChannelSnackbar} from '@utils/snack_bar'; import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; import {displayGroupMessageName, displayUsername} from '@utils/user'; +import {fetchGroupsForChannelIfConstrained} from './groups'; import {fetchPostsForChannel} from './post'; import {setDirectChannelVisible} from './preference'; import {fetchRolesIfNeeded} from './role'; @@ -1109,6 +1110,7 @@ export async function switchToChannelById(serverUrl: string, channelId: string, setDirectChannelVisible(serverUrl, channelId); markChannelAsRead(serverUrl, channelId); fetchChannelStats(serverUrl, channelId); + fetchGroupsForChannelIfConstrained(serverUrl, channelId); DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, false); diff --git a/app/actions/remote/groups.ts b/app/actions/remote/groups.ts index 7501475c97..f18729a737 100644 --- a/app/actions/remote/groups.ts +++ b/app/actions/remote/groups.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {getChannelById} from '@app/queries/servers/channel'; import {Client} from '@client/rest'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; @@ -48,7 +49,16 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string const client = NetworkManager.getClient(serverUrl); const response = await client.getAllGroupsAssociatedToChannel(channelId); - return operator.handleGroups({groups: response.groups, prepareRecordsOnly: fetchOnly}); + const [groups, groupChannels] = await Promise.all([ + operator.handleGroups({groups: response.groups, prepareRecordsOnly: true}), + operator.handleGroupChannelsForChannel({groups: response.groups, channelId, prepareRecordsOnly: true}), + ]); + + if (!fetchOnly) { + await operator.batchRecords([...groups, ...groupChannels]); + } + + return {groups, groupChannels}; } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); return {error}; @@ -141,3 +151,18 @@ export const fetchGroupsForTeamIfConstrained = async (serverUrl: string, teamId: return {error}; } }; + +export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false) => { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const channel = await getChannelById(database, channelId); + + if (channel?.isGroupConstrained) { + return fetchGroupsForChannel(serverUrl, channelId, fetchOnly); + } + + return {}; + } catch (error) { + return {error}; + } +}; diff --git a/app/database/operator/server_data_operator/handlers/group.ts b/app/database/operator/server_data_operator/handlers/group.ts index 9fd30204f8..6ceab3d804 100644 --- a/app/database/operator/server_data_operator/handlers/group.ts +++ b/app/database/operator/server_data_operator/handlers/group.ts @@ -2,21 +2,23 @@ // See LICENSE.txt for license information. import {MM_TABLES} from '@constants/database'; -import {transformGroupMembershipRecord, transformGroupRecord, transformGroupTeamRecord} from '@database/operator/server_data_operator/transformers/group'; +import {transformGroupChannelRecord, transformGroupMembershipRecord, transformGroupRecord, transformGroupTeamRecord} from '@database/operator/server_data_operator/transformers/group'; import {getUniqueRawsBy} from '@database/operator/utils/general'; -import {queryGroupMembershipForMember, queryGroupTeamForTeam} from '@queries/servers/group'; +import {queryGroupChannelForChannel, queryGroupMembershipForMember, queryGroupTeamForTeam} from '@queries/servers/group'; import {generateGroupAssociationId} from '@utils/groups'; import {logWarning} from '@utils/log'; -import type {HandleGroupArgs, HandleGroupMembershipForMemberArgs, HandleGroupTeamsForTeamArgs} from '@typings/database/database'; +import type {HandleGroupArgs, HandleGroupChannelsForChannelArgs, HandleGroupMembershipForMemberArgs, HandleGroupTeamsForTeamArgs} from '@typings/database/database'; import type GroupModel from '@typings/database/models/servers/group'; +import type GroupChannelModel from '@typings/database/models/servers/group_channel'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; import type GroupTeamModel from '@typings/database/models/servers/group_team'; -const {GROUP, GROUP_MEMBERSHIP, GROUP_TEAM} = MM_TABLES.SERVER; +const {GROUP, GROUP_CHANNEL, GROUP_MEMBERSHIP, GROUP_TEAM} = MM_TABLES.SERVER; export interface GroupHandlerMix { handleGroups: ({groups, prepareRecordsOnly}: HandleGroupArgs) => Promise; + handleGroupChannelsForChannel: ({channelId, groups, prepareRecordsOnly}: HandleGroupChannelsForChannelArgs) => Promise; handleGroupMembershipsForMember: ({userId, groups, prepareRecordsOnly}: HandleGroupMembershipForMemberArgs) => Promise; handleGroupTeamsForTeam: ({teamId, groups, prepareRecordsOnly}: HandleGroupTeamsForTeamArgs) => Promise; } @@ -48,6 +50,63 @@ const GroupHandler = (superclass: any) => class extends superclass implements Gr }); }; + /** + * handleGroupChannelsForChannel: Handler responsible for the Create/Update operations occurring on the GroupChannel table from the 'Server' schema + * + * @param {HandleGroupChannelsForChannelArgs} + * @returns {Promise} + */ + handleGroupChannelsForChannel = async ({channelId, groups, prepareRecordsOnly = true}: HandleGroupChannelsForChannelArgs): Promise => { + // Get existing group channels + const existingGroupChannels = await queryGroupChannelForChannel(this.database, channelId).fetch(); + + let records: GroupChannelModel[] = []; + let rawValues: GroupChannel[] = []; + + // Nothing to add or remove + if (!groups?.length && !existingGroupChannels.length) { + return records; + } else if (!groups?.length && existingGroupChannels.length) { // No groups - remove all existing ones + records = existingGroupChannels.map((gt) => gt.prepareDestroyPermanently()); + } else if (groups?.length && !existingGroupChannels.length) { // No existing groups - add all new ones + rawValues = groups.map((g) => ({id: generateGroupAssociationId(g.id, channelId), channel_id: channelId, group_id: g.id})); + } else if (groups?.length && existingGroupChannels.length) { // If both, we only want to save new ones and delete one's no longer in groups + const groupsSet: {[key: string]: GroupChannel} = {}; + + for (const g of groups) { + groupsSet[g.id] = {id: generateGroupAssociationId(g.id, channelId), channel_id: channelId, group_id: g.id}; + } + + for (const gt of existingGroupChannels) { + // Check if existingGroups overlaps with groups + if (groupsSet[gt.groupId]) { + // If there is an existing group already, we don't need to add it + delete groupsSet[gt.groupId]; + } else { + // No group? Remove existing one + records.push(gt.prepareDestroyPermanently()); + } + } + + rawValues.push(...Object.values(groupsSet)); + } + + records.push(...(await this.handleRecords({ + fieldName: 'id', + transformer: transformGroupChannelRecord, + rawValues, + tableName: GROUP_CHANNEL, + prepareRecordsOnly: true, + }))); + + // Batch update if there are records + if (records.length && !prepareRecordsOnly) { + await this.batchRecords(records); + } + + return records; + }; + /** * handleGroupMembershipsForMember: Handler responsible for the Create/Update operations occurring on the GroupMembership table from the 'Server' schema * diff --git a/app/database/operator/server_data_operator/transformers/group.ts b/app/database/operator/server_data_operator/transformers/group.ts index 6888a1fa65..a1c0ed6ddc 100644 --- a/app/database/operator/server_data_operator/transformers/group.ts +++ b/app/database/operator/server_data_operator/transformers/group.ts @@ -9,11 +9,13 @@ import {generateGroupAssociationId} from '@utils/groups'; import type {TransformerArgs} from '@typings/database/database'; import type GroupModel from '@typings/database/models/servers/group'; +import type GroupChannelModel from '@typings/database/models/servers/group_channel'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; import type GroupTeamModel from '@typings/database/models/servers/group_team'; const { GROUP, + GROUP_CHANNEL, GROUP_MEMBERSHIP, GROUP_TEAM, } = MM_TABLES.SERVER; @@ -49,6 +51,32 @@ export const transformGroupRecord = ({action, database, value}: TransformerArgs) }) as Promise; }; +/** + * transformGroupChannelRecord: Prepares a record of the SERVER database 'GroupChannel' table for update or create actions. + * @param {TransformerArgs} operator + * @param {Database} operator.database + * @param {RecordPair} operator.value + * @returns {Promise} + */ +export const transformGroupChannelRecord = ({action, database, value}: TransformerArgs): Promise => { + const raw = value.raw as GroupChannel; + + // id of group comes from server response + const fieldsMapper = (model: GroupChannelModel) => { + model._raw.id = raw.id || generateGroupAssociationId(raw.group_id, raw.channel_id); + model.groupId = raw.group_id; + model.channelId = raw.channel_id; + }; + + return prepareBaseRecord({ + action, + database, + tableName: GROUP_CHANNEL, + value, + fieldsMapper, + }) as Promise; +}; + /** * transformGroupMembershipRecord: Prepares a record of the SERVER database 'GroupMembership' table for update or create actions. * @param {TransformerArgs} operator diff --git a/app/queries/servers/group.ts b/app/queries/servers/group.ts index 5867041065..e6a162afcf 100644 --- a/app/queries/servers/group.ts +++ b/app/queries/servers/group.ts @@ -6,6 +6,7 @@ import {Database, Q} from '@nozbe/watermelondb'; import {MM_TABLES} from '@constants/database'; import type GroupModel from '@typings/database/models/servers/group'; +import type GroupChannelModel from '@typings/database/models/servers/group_channel'; import type GroupMembershipModel from '@typings/database/models/servers/group_membership'; import type GroupTeamModel from '@typings/database/models/servers/group_team'; @@ -37,6 +38,12 @@ export const queryGroupsByNameInChannel = (database: Database, name: string, cha ); }; +export const queryGroupChannelForChannel = (database: Database, channelId: string) => { + return database.collections.get(GROUP_CHANNEL).query( + Q.where('channel_id', channelId), + ); +}; + export const queryGroupMembershipForMember = (database: Database, userId: string) => { return database.collections.get(GROUP_MEMBERSHIP).query( Q.where('user_id', userId), diff --git a/types/api/groups.d.ts b/types/api/groups.d.ts index 9b88f2cd27..6c03cbace9 100644 --- a/types/api/groups.d.ts +++ b/types/api/groups.d.ts @@ -22,19 +22,9 @@ type GroupTeam = { } type GroupChannel = { + id?: string; channel_id: string; - channel_display_name: string; - channel_type: string; - team_id: string; - team_display_name: string; - team_type: string; group_id: string; - auto_add: boolean; - member_count?: number; - timezone_count?: number; - create_at: number; - delete_at: number; - update_at: number; } type GroupMembership = { diff --git a/types/database/database.d.ts b/types/database/database.d.ts index 000cb6b2cc..599e606c74 100644 --- a/types/database/database.d.ts +++ b/types/database/database.d.ts @@ -222,6 +222,11 @@ export type HandleGroupArgs = PrepareOnly & { groups?: Group[]; }; +export type HandleGroupChannelsForChannelArgs = PrepareOnly & { + channelId: string; + groups?: Group[]; +} + export type HandleGroupMembershipForMemberArgs = PrepareOnly & { userId: string; groups?: Group[];