[Gekidou] Manage user websocket events (#5862)

* Manage user websocket events

* Revert ChannelMembership -> MyChannelMembership

* Address feedback

* Update channel display name when the user changes

* Minor name and refactoring changes from feedback

* Address feedback

* Address feedback

* Add line breaks for readability
This commit is contained in:
Daniel Espino García 2022-01-24 12:12:06 +01:00 committed by GitHub
parent 55197b6125
commit d5228633b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 287 additions and 74 deletions

View file

@ -1,25 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {Model, Q} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {Navigation as NavigationConstants, Screens} from '@constants';
import {General, Navigation as NavigationConstants, Preferences, Screens} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {prepareDeleteChannel, queryAllMyChannelIds, queryChannelsById, queryMyChannel} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, queryCommonSystemValues, queryCurrentTeamId, setCurrentChannelId} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory, removeChannelFromTeamHistory} from '@queries/servers/team';
import {queryCurrentUser} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
import {isTablet} from '@utils/helpers';
import {displayGroupMessageName, displayUsername} from '@utils/user';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
export const switchToChannel = async (serverUrl: string, channelId: string, teamId?: string) => {
const {SERVER: {CHANNEL_MEMBERSHIP, USER}} = MM_TABLES;
export const switchToChannel = async (serverUrl: string, channelId: string, teamId?: string, prepareRecordsOnly = false) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
const models: Model[] = [];
try {
const dt = Date.now();
const isTabletDevice = await isTablet();
@ -29,7 +38,6 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team
if (member) {
const channel: ChannelModel = await member.channel.fetch();
const {operator} = DatabaseManager.serverDatabases[serverUrl];
const models = [];
const commonValues: PrepareCommonSystemValuesArgs = {currentChannelId: channelId};
if (isTabletDevice) {
// On tablet, the channel is being rendered, by setting the channel to empty first we speed up
@ -58,7 +66,7 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team
models.push(viewedAt);
}
if (models.length) {
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
}
@ -75,21 +83,22 @@ export const switchToChannel = async (serverUrl: string, channelId: string, team
return {error};
}
return {error: undefined};
return {models};
};
export const localRemoveUserFromChannel = async (serverUrl: string, channelId: string) => {
export const removeCurrentUserFromChannel = async (serverUrl: string, channelId: string, prepareRecordsOnly = false) => {
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
if (!serverDatabase) {
return;
return {error: `${serverUrl} database not found`};
}
const {operator, database} = serverDatabase;
const models: Model[] = [];
const myChannel = await queryMyChannel(database, channelId);
if (myChannel) {
const channel = await myChannel.channel.fetch() as ChannelModel;
const models = await prepareDeleteChannel(channel);
models.push(...await prepareDeleteChannel(channel));
let teamId = channel.teamId;
if (teamId) {
teamId = await queryCurrentTeamId(database);
@ -98,7 +107,7 @@ export const localRemoveUserFromChannel = async (serverUrl: string, channelId: s
if (system) {
models.push(...system);
}
if (models.length) {
if (models.length && !prepareRecordsOnly) {
try {
await operator.batchRecords(models);
} catch {
@ -107,9 +116,10 @@ export const localRemoveUserFromChannel = async (serverUrl: string, channelId: s
}
}
}
return {models};
};
export const localSetChannelDeleteAt = async (serverUrl: string, channelId: string, deleteAt: number) => {
export const setChannelDeleteAt = async (serverUrl: string, channelId: string, deleteAt: number) => {
const serverDatabase = DatabaseManager.serverDatabases[serverUrl];
if (!serverDatabase) {
return;
@ -196,3 +206,43 @@ export const resetMessageCount = async (serverUrl: string, channelId: string) =>
return {error};
}
};
export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], user: UserProfile, prepareRecordsOnly = false) {
const database = DatabaseManager.serverDatabases[serverUrl];
if (!database) {
return {};
}
const currentUser = await queryCurrentUser(database.database);
if (!currentUser) {
return {};
}
const {config, license} = await queryCommonSystemValues(database.database);
const preferences = await queryPreferencesByCategoryAndName(database.database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT);
const displaySettings = getTeammateNameDisplaySetting(preferences, config, license);
const models: Model[] = [];
channels?.forEach(async (channel) => {
let newDisplayName = '';
if (channel.type === General.DM_CHANNEL) {
newDisplayName = displayUsername(user, currentUser.locale, displaySettings);
} else {
const dbProfiles = await database.database.get<UserModel>(USER).query(Q.on(CHANNEL_MEMBERSHIP, Q.where('channel_id', channel.id))).fetch();
const newProfiles: Array<UserModel|UserProfile> = dbProfiles.filter((u) => u.id !== user.id);
newProfiles.push(user);
newDisplayName = displayGroupMessageName(newProfiles, currentUser.locale, displaySettings, currentUser.id);
}
if (channel.displayName !== newDisplayName) {
channel.prepareUpdate((c) => {
c.displayName = newDisplayName;
});
models.push(channel);
}
});
if (models.length && !prepareRecordsOnly) {
database.operator.batchRecords(models);
}
return {models};
}

View file

@ -42,7 +42,7 @@ export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
return fetchPostsForChannel(serverUrl, currentChannelId);
};
export const fetchPostsForChannel = async (serverUrl: string, channelId: string) => {
export const fetchPostsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
@ -65,42 +65,42 @@ export const fetchPostsForChannel = async (serverUrl: string, channelId: string)
// Here we should emit an event that fetching posts failed.
}
let authors: UserProfile[] = [];
if (data.posts?.length && data.order?.length) {
const models: Model[] = [];
try {
const {authors} = await fetchPostAuthors(serverUrl, data.posts, true);
if (authors?.length) {
const users = await operator.handleUsers({
users: authors,
prepareRecordsOnly: true,
});
if (users.length) {
models.push(...users);
}
}
const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true);
authors = fetchedAuthors || [];
} catch (error) {
// eslint-disable-next-line no-console
console.log('FETCH AUTHORS ERROR', error);
}
const postModels = await operator.handlePosts({
actionType,
order: data.order,
posts: data.posts,
previousPostId: data.previousPostId,
prepareRecordsOnly: true,
});
if (!fetchOnly) {
const models = [];
const postModels = await operator.handlePosts({
actionType,
order: data.order,
posts: data.posts,
previousPostId: data.previousPostId,
prepareRecordsOnly: true,
});
if (postModels) {
models.push(...postModels);
}
if (authors.length) {
const userModels = await operator.handleUsers({users: authors, prepareRecordsOnly: true});
if (userModels.length) {
models.push(...userModels);
}
}
if (postModels.length) {
models.push(...postModels);
}
if (models.length) {
await operator.batchRecords(models);
if (models.length) {
operator.batchRecords(models);
}
}
}
return {posts: data.posts};
return {posts: data.posts, order: data.order, authors, actionType, previousPostId: data.previousPostId};
};
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => {

View file

@ -360,7 +360,7 @@ export const updateAllUsersSinceLastDisconnect = async (serverUrl: string) => {
return {users: userUpdates};
};
export const updateUsersNoLongerVisible = async (serverUrl: string): Promise<{error?: unknown}> => {
export const updateUsersNoLongerVisible = async (serverUrl: string, prepareRecordsOnly = false): Promise<{error?: unknown; models?: Model[]}> => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
@ -373,12 +373,12 @@ export const updateUsersNoLongerVisible = async (serverUrl: string): Promise<{er
return {error: `${serverUrl} database not found`};
}
const models: Model[] = [];
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)) {
@ -386,7 +386,7 @@ export const updateUsersNoLongerVisible = async (serverUrl: string): Promise<{er
models.push(user);
}
}
if (models.length) {
if (models.length && !prepareRecordsOnly) {
serverDatabase.operator.batchRecords(models);
}
} catch (error) {
@ -394,7 +394,7 @@ export const updateUsersNoLongerVisible = async (serverUrl: string): Promise<{er
return {error};
}
return {};
return {models};
};
export const setStatus = async (serverUrl: string, status: UserStatus) => {

View file

@ -1,22 +1,89 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Model} from '@nozbe/watermelondb';
import {DeviceEventEmitter} from 'react-native';
import {localRemoveUserFromChannel, localSetChannelDeleteAt, switchToChannel} from '@actions/local/channel';
import {updateUsersNoLongerVisible} from '@actions/remote/user';
import {removeCurrentUserFromChannel, setChannelDeleteAt, switchToChannel} from '@actions/local/channel';
import {fetchMyChannel} from '@actions/remote/channel';
import {fetchPostsForChannel} from '@actions/remote/post';
import {fetchUsersByIds, 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 {deleteChannelMembership, prepareMyChannelsForTeam, queryChannelsById, queryCurrentChannel} from '@queries/servers/channel';
import {prepareCommonSystemValues, queryConfig, setCurrentChannelId} from '@queries/servers/system';
import {queryLastChannelFromTeam} from '@queries/servers/team';
import {queryCurrentUser} from '@queries/servers/user';
import {queryCurrentUser, queryUserById} 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) {
export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any) {
const database = DatabaseManager.serverDatabases[serverUrl];
if (!database) {
return;
}
const currentUser = await queryCurrentUser(database.database);
const {team_id: teamId, channel_id: channelId, user_id: userId} = msg.data;
const models: Model[] = [];
try {
const addedUser = queryUserById(database.database, userId);
if (!addedUser) {
// TODO Potential improvement https://mattermost.atlassian.net/browse/MM-40581
const {users} = await fetchUsersByIds(serverUrl, [userId], true);
if (users) {
models.push(...await database.operator.handleUsers({users, prepareRecordsOnly: true}));
}
}
if (userId === currentUser?.id) {
const {channels, memberships} = await fetchMyChannel(serverUrl, teamId, channelId, true);
if (channels && memberships) {
const prepare = await prepareMyChannelsForTeam(database.operator, teamId, channels, memberships);
if (prepare) {
const prepareModels = await Promise.all(prepare);
const flattenedModels = prepareModels.flat();
if (flattenedModels?.length > 0) {
models.push(...flattenedModels);
}
}
}
const {posts, order, authors, actionType, previousPostId} = await fetchPostsForChannel(serverUrl, channelId, true);
if (posts?.length && order && actionType) {
models.push(...await database.operator.handlePosts({
actionType,
order,
posts,
previousPostId,
prepareRecordsOnly: true,
}));
}
if (authors?.length) {
models.push(...await database.operator.handleUsers({users: authors, prepareRecordsOnly: true}));
}
database.operator.batchRecords(models);
} else {
const channels = await queryChannelsById(database.database, [channelId]);
if (channels?.[0]) {
models.push(...await database.operator.handleChannelMembership({
channelMemberships: [{channel_id: channelId, user_id: userId}],
prepareRecordsOnly: true,
}));
}
}
} catch {
// Do nothing
}
database.operator.batchRecords(models);
}
export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg: any) {
const database = DatabaseManager.serverDatabases[serverUrl];
if (!database) {
return;
@ -28,14 +95,25 @@ export async function handleUserRemovedEvent(serverUrl: string, msg: any) {
return;
}
if (user.id === msg.data.user_id) {
localRemoveUserFromChannel(serverUrl, msg.data.channel_id);
const userId = msg.data.user_id;
const channelId = msg.data.channel_id;
if (isGuest(user.roles)) {
updateUsersNoLongerVisible(serverUrl);
const models: Model[] = [];
if (user.isGuest) {
const {models: updateVisibleModels} = await updateUsersNoLongerVisible(serverUrl, true);
if (updateVisibleModels) {
models.push(...updateVisibleModels);
}
}
if (user.id === userId) {
const {models: removeUserModels} = await removeCurrentUserFromChannel(serverUrl, channelId, true);
if (removeUserModels) {
models.push(...removeUserModels);
}
if (channel && channel.id === msg.data.channel_id) {
if (channel && channel.id === channelId) {
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);
if (currentServer?.url === serverUrl) {
@ -46,16 +124,27 @@ export async function handleUserRemovedEvent(serverUrl: string, msg: any) {
if (await isTablet()) {
const channelToJumpTo = await queryLastChannelFromTeam(database.database, channel?.teamId);
if (channelToJumpTo) {
switchToChannel(serverUrl, channelToJumpTo);
} // TODO else jump to "join a channel" screen
const {models: switchChannelModels} = await switchToChannel(serverUrl, channelToJumpTo, '', true);
if (switchChannelModels) {
models.push(...switchChannelModels);
}
} // TODO else jump to "join a channel" screen https://mattermost.atlassian.net/browse/MM-41051
} else {
setCurrentChannelId(database.operator, '');
const currentChannelModels = await prepareCommonSystemValues(database.operator, {currentChannelId: ''});
if (currentChannelModels?.length) {
models.push(...currentChannelModels);
}
}
}
}
} else {
deleteChannelMembership(database.operator, msg.data.user_id, msg.data.channel_id);
const {models: deleteMemberModels} = await deleteChannelMembership(database.operator, userId, channelId, true);
if (deleteMemberModels) {
models.push(...deleteMemberModels);
}
}
database.operator.batchRecords(models);
}
export async function handleChannelDeletedEvent(serverUrl: string, msg: any) {
@ -72,14 +161,14 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: any) {
const config = await queryConfig(database.database);
await localSetChannelDeleteAt(serverUrl, msg.data.channel_id, msg.data.delete_at);
await setChannelDeleteAt(serverUrl, msg.data.channel_id, msg.data.delete_at);
if (isGuest(user.roles)) {
if (user.isGuest) {
updateUsersNoLongerVisible(serverUrl);
}
if (config?.ExperimentalViewArchivedChannels !== 'true') {
localRemoveUserFromChannel(serverUrl, msg.data.channel_id);
removeCurrentUserFromChannel(serverUrl, msg.data.channel_id);
if (currentChannel && currentChannel.id === msg.data.channel_id) {
const currentServer = await queryActiveServer(DatabaseManager.appDatabase!.database);

View file

@ -19,9 +19,10 @@ import {prepareMyChannelsForTeam} from '@queries/servers/channel';
import {queryCommonSystemValues, queryConfig, queryWebSocketLastDisconnected} from '@queries/servers/system';
import {queryCurrentUser} from '@queries/servers/user';
import {handleChannelDeletedEvent, handleUserRemovedEvent} from './channel';
import {handleChannelDeletedEvent, handleUserAddedToChannelEvent, handleUserRemovedFromChannelEvent} from './channel';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import {handleLeaveTeamEvent} from './teams';
import {handleUserUpdatedEvent} from './users';
import type {Model} from '@nozbe/watermelondb';
@ -122,7 +123,7 @@ async function doReconnect(serverUrl: string) {
const viewArchivedChannels = config?.ExperimentalViewArchivedChannels === 'true';
if (!stillMemberOfCurrentChannel) {
handleUserRemovedEvent(serverUrl, {data: {user_id: currentUserId, channel_id: currentChannelId}});
handleUserRemovedFromChannelEvent(serverUrl, {data: {user_id: currentUserId, channel_id: currentChannelId}});
} else if (!channelStillExist ||
(!viewArchivedChannels && channelStillExist.delete_at !== 0)
) {
@ -177,16 +178,14 @@ export async function handleEvent(serverUrl: string, msg: any) {
// return dispatch(handleTeamAddedEvent(msg));
case WebsocketEvents.USER_ADDED:
handleUserAddedToChannelEvent(serverUrl, msg);
break;
// return dispatch(handleUserAddedEvent(msg));
case WebsocketEvents.USER_REMOVED:
handleUserRemovedEvent(serverUrl, msg);
handleUserRemovedFromChannelEvent(serverUrl, msg);
break;
case WebsocketEvents.USER_UPDATED:
handleUserUpdatedEvent(serverUrl, msg);
break;
// return dispatch(handleUserUpdatedEvent(msg));
case WebsocketEvents.ROLE_ADDED:
break;

View file

@ -13,7 +13,6 @@ 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];
@ -31,7 +30,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: any) {
await removeUserFromTeam(serverUrl, msg.data.team_id);
fetchAllTeams(serverUrl);
if (isGuest(user.roles)) {
if (user.isGuest) {
updateUsersNoLongerVisible(serverUrl);
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {updateChannelsDisplayName} from '@actions/local/channel';
import {fetchMe} from '@actions/remote/user';
import {General} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {queryCurrentUser} from '@queries/servers/user';
import type ChannelModel from '@typings/database/models/servers/channel';
const {SERVER: {CHANNEL, CHANNEL_MEMBERSHIP}} = MM_TABLES;
export async function handleUserUpdatedEvent(serverUrl: string, msg: any) {
const database = DatabaseManager.serverDatabases[serverUrl];
if (!database) {
return;
}
const currentUser = await queryCurrentUser(database.database);
if (!currentUser) {
return;
}
const user: UserProfile = msg.data.user;
if (user.id === currentUser.id) {
if (user.update_at > (currentUser?.updateAt || 0)) {
// Need to request me to make sure we don't override with sanitized fields from the
// websocket event
// TODO Potential improvement https://mattermost.atlassian.net/browse/MM-40582
fetchMe(serverUrl, false);
// Update GMs display name if locale has changed
if (user.locale !== currentUser.locale) {
const channels = await database.database.get<ChannelModel>(CHANNEL).query(
Q.where('type', Q.eq(General.GM_CHANNEL))).fetch();
const {models} = await updateChannelsDisplayName(serverUrl, channels, user, true);
if (models?.length) {
database.operator.batchRecords(models);
}
}
}
} else {
database.operator.handleUsers({users: [user], prepareRecordsOnly: false});
const channels = await database.database.get<ChannelModel>(CHANNEL).query(
Q.where('type', Q.oneOf([General.DM_CHANNEL, General.GM_CHANNEL])),
Q.on(CHANNEL_MEMBERSHIP, Q.where('user_id', user.id))).fetch();
if (!channels?.length) {
return;
}
const {models} = await updateChannelsDisplayName(serverUrl, channels, user, true);
if (models?.length) {
database.operator.batchRecords(models);
}
}
}

View file

@ -77,7 +77,7 @@ export const isRecordGroupMembershipEqualToRaw = (record: GroupMembershipModel,
return raw.user_id === record.userId && raw.group_id === record.groupId;
};
export const isRecordChannelMembershipEqualToRaw = (record: ChannelMembershipModel, raw: ChannelMembership) => {
export const isRecordChannelMembershipEqualToRaw = (record: ChannelMembershipModel, raw: Pick<ChannelMembership, 'user_id' | 'channel_id'>) => {
return raw.user_id === record.userId && raw.channel_id === record.channelId;
};

View file

@ -71,6 +71,7 @@ const PostHandler = (superclass: any) => class extends superclass {
* @param {string[]} handlePosts.orders
* @param {RawPost[]} handlePosts.values
* @param {string | undefined} handlePosts.previousPostId
* @param {boolean | undefined} handlePosts.prepareRecordsOnly
* @returns {Promise<void>}
*/
handlePosts = async ({actionType, order, posts, previousPostId = '', prepareRecordsOnly = false}: HandlePostsArgs): Promise<Model[]> => {

View file

@ -214,14 +214,25 @@ export const queryCurrentChannel = async (database: Database) => {
return undefined;
};
export const deleteChannelMembership = async (operator: ServerDataOperator, userId: string, channelId: string) => {
export const deleteChannelMembership = async (operator: ServerDataOperator, userId: string, channelId: string, prepareRecordsOnly = false) => {
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);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models);
}
return {models};
} catch (error) {
return {error};
}
};
export const addChannelMembership = async (operator: ServerDataOperator, userId: string, channelId: string) => {
try {
await operator.handleChannelMembership({channelMemberships: [{channel_id: channelId, user_id: userId}], prepareRecordsOnly: false});
return {};
} catch (error) {
return {error};

View file

@ -34,7 +34,7 @@ export function displayUsername(user?: UserProfile | UserModel, locale?: string,
return name;
}
export function displayGroupMessageName(users: UserProfile[], locale?: string, teammateDisplayNameSetting?: string, excludeUserId?: string) {
export function displayGroupMessageName(users: Array<UserProfile | UserModel>, locale?: string, teammateDisplayNameSetting?: string, excludeUserId?: string) {
const names: string[] = [];
const sortUsernames = (a: string, b: string) => {
return a.localeCompare(b, locale || DEFAULT_LOCALE, {numeric: true});

View file

@ -5,6 +5,7 @@ type ChannelStats = {
channel_id: string;
guest_count: number;
member_count: number;
guest_count: number;
pinnedpost_count: number;
};
type ChannelNotifyProps = {

View file

@ -223,7 +223,7 @@ export type HandleGroupArgs = PrepareOnly & {
};
export type HandleChannelMembershipArgs = PrepareOnly & {
channelMemberships: ChannelMember[];
channelMemberships: Array<Pick<ChannelMembership, 'user_id' | 'channel_id'>>;
};
export type HandleGroupMembershipArgs = PrepareOnly & {

View file

@ -122,3 +122,4 @@ type RawValue =
| TeamSearchHistory
| TermsOfService
| UserProfile
| Pick<ChannelMembership, 'channel_id' | 'user_id'>