Fix DMs in sidebar (#7468)

* Fix DMs in sidebar

* Properly handle view, show and open preferences

* Refactor openChannel to keep similar logic together
This commit is contained in:
Daniel Espino García 2023-08-02 11:51:33 +02:00 committed by GitHub
parent d74826064b
commit 8a32588507
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 56 deletions

View file

@ -33,7 +33,7 @@ import {displayGroupMessageName, displayUsername} from '@utils/user';
import {fetchGroupsForChannelIfConstrained} from './groups';
import {fetchPostsForChannel} from './post';
import {setDirectChannelVisible} from './preference';
import {openChannelIfNeeded, savePreference} from './preference';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
@ -764,6 +764,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
const channelName = getDirectChannelName(currentUser.id, userId);
const channel = await getChannelByName(database, '', channelName);
if (channel) {
openChannelIfNeeded(serverUrl, channel.id);
return {data: channel.toApi()};
}
@ -798,6 +799,16 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
};
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);
@ -893,14 +904,15 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
// 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 preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const displayNamePreferences = 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 teammateDisplayNameSetting = getTeammateNameDisplaySetting(displayNamePreferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const {directChannels, users} = await fetchMissingDirectChannelsInfo(serverUrl, [created], currentUser.locale, teammateDisplayNameSetting, currentUser.id, true);
const member = {
@ -931,6 +943,15 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
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);
operator.batchRecords(models, 'createGroupChannel');
}
@ -1012,7 +1033,7 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
fetchPostsForChannel(serverUrl, channelId);
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
setDirectChannelVisible(serverUrl, channelId);
openChannelIfNeeded(serverUrl, channelId);
markChannelAsRead(serverUrl, channelId);
fetchChannelStats(serverUrl, channelId);
fetchGroupsForChannelIfConstrained(serverUrl, channelId);

View file

@ -6,18 +6,22 @@ import {DeviceEventEmitter} from 'react-native';
import {handleReconnect} from '@actions/websocket';
import {Events, General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {getPreferenceAsBool} from '@helpers/api/preference';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {queryAllUnreadDMsAndGMsIds, getChannelById} from '@queries/servers/channel';
import {truncateCrtRelatedTables} from '@queries/servers/entry';
import {querySavedPostsPreferences} from '@queries/servers/preference';
import {queryPreferencesByCategoryAndName, querySavedPostsPreferences} from '@queries/servers/preference';
import {getCurrentUserId} from '@queries/servers/system';
import EphemeralStore from '@store/ephemeral_store';
import {isDMorGM} from '@utils/channel';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {getUserIdFromChannelName} from '@utils/user';
import {forceLogoutIfNecessary} from './session';
import type ChannelModel from '@typings/database/models/servers/channel';
export type MyPreferencesRequest = {
preferences?: PreferenceType[];
error?: unknown;
@ -79,19 +83,23 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
}
};
export const savePreference = async (serverUrl: string, preferences: PreferenceType[]) => {
export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false) => {
try {
if (!preferences.length) {
return {preferences: []};
}
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
client.savePreferences(userId, preferences);
await operator.handlePreferences({
const preferenceModels = await operator.handlePreferences({
preferences,
prepareRecordsOnly: false,
prepareRecordsOnly,
});
return {preferences};
return {preferences: preferenceModels};
} catch (error) {
logDebug('error on savePreference', getFullErrorMessage(error));
forceLogoutIfNecessary(serverUrl, error);
@ -128,6 +136,70 @@ export const deleteSavedPost = async (serverUrl: string, postId: string) => {
}
};
export const openChannelIfNeeded = async (serverUrl: string, channelId: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channel = await getChannelById(database, channelId);
if (!channel || !isDMorGM(channel)) {
return {};
}
const res = await openChannels(serverUrl, [channel]);
return res;
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
export const openAllUnreadChannels = async (serverUrl: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const channels = await queryAllUnreadDMsAndGMsIds(database).fetch();
const res = await openChannels(serverUrl, channels);
return res;
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
return {error};
}
};
const openChannels = async (serverUrl: string, channels: ChannelModel[]) => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const userId = await getCurrentUserId(database);
const {DIRECT_CHANNEL_SHOW, GROUP_CHANNEL_SHOW} = Preferences.CATEGORIES;
const directChannelShowPreferences = await queryPreferencesByCategoryAndName(database, DIRECT_CHANNEL_SHOW).fetch();
const groupChannelShowPreferences = await queryPreferencesByCategoryAndName(database, GROUP_CHANNEL_SHOW).fetch();
const showPreferences = directChannelShowPreferences.concat(groupChannelShowPreferences);
const prefs: PreferenceType[] = [];
for (const channel of channels) {
const category = channel.type === General.DM_CHANNEL ? DIRECT_CHANNEL_SHOW : GROUP_CHANNEL_SHOW;
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channel.id;
const visible = getPreferenceAsBool(showPreferences, category, name, false);
if (visible) {
continue;
}
prefs.push(
{
user_id: userId,
category,
name,
value: 'true',
},
{
user_id: userId,
category: Preferences.CATEGORIES.CHANNEL_OPEN_TIME,
name: channel.id,
value: Date.now().toString(),
},
);
}
return savePreference(serverUrl, prefs);
};
export const setDirectChannelVisible = async (serverUrl: string, channelId: string, visible = true) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);

View file

@ -360,6 +360,48 @@ export async function fetchStatusByIds(serverUrl: string, userIds: string[], fet
}
}
let usersByIdBatch: {
serverUrl: string;
userIds: Set<string>;
timeout?: NodeJS.Timeout;
} | undefined;
const TIME_TO_BATCH = 500;
const processBatch = () => {
if (!usersByIdBatch) {
return;
}
if (usersByIdBatch.timeout) {
clearTimeout(usersByIdBatch.timeout);
}
if (usersByIdBatch.userIds.size) {
fetchUsersByIds(usersByIdBatch.serverUrl, Array.from(usersByIdBatch.userIds));
}
usersByIdBatch = undefined;
};
export const fetchUserByIdBatched = async (serverUrl: string, userId: string) => {
if (serverUrl !== usersByIdBatch?.serverUrl) {
processBatch();
}
if (!usersByIdBatch) {
usersByIdBatch = {
serverUrl,
userIds: new Set(),
};
}
if (usersByIdBatch.timeout) {
clearTimeout(usersByIdBatch.timeout);
}
usersByIdBatch.userIds.add(userId);
usersByIdBatch.timeout = setTimeout(processBatch, TIME_TO_BATCH);
};
export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false) => {
if (!userIds.length) {
return {users: [], existingUsers: []};

View file

@ -7,6 +7,7 @@ import {markChannelAsRead} from '@actions/remote/channel';
import {handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common';
import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {openAllUnreadChannels} from '@actions/remote/preference';
import {autoUpdateTimezone} from '@actions/remote/user';
import {loadConfigAndCalls} from '@calls/actions/calls';
import {
@ -152,6 +153,8 @@ async function doReconnect(serverUrl: string) {
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
openAllUnreadChannels(serverUrl);
dataRetentionCleanup(serverUrl);
AppsManager.refreshAppBindings(serverUrl);

View file

@ -8,6 +8,7 @@ import {addPostAcknowledgement, markPostAsDeleted, removePostAcknowledgement} fr
import {createThreadFromNewPost, updateThread} from '@actions/local/thread';
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
import {openChannelIfNeeded} from '@actions/remote/preference';
import {fetchThread} from '@actions/remote/thread';
import {fetchMissingProfilesByIds} from '@actions/remote/user';
import {ActionType, Events, Screens} from '@constants';
@ -157,6 +158,8 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
models.push(unreadAt);
}
}
openChannelIfNeeded(serverUrl, post.channel_id);
}
let actionType: string = ActionType.POSTS.RECEIVED_NEW;

View file

@ -1,10 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useEffect} from 'react';
import {fetchUserByIdBatched} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import ProfilePicture from '@components/profile_picture';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -12,6 +14,7 @@ import type UserModel from '@typings/database/models/servers/user';
import type {StyleProp, ViewStyle} from 'react-native';
type Props = {
authorId: string;
author?: UserModel;
isOnCenterBg?: boolean;
style: StyleProp<ViewStyle>;
@ -35,10 +38,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const DmAvatar = ({author, isOnCenterBg, style, size}: Props) => {
const DmAvatar = ({
authorId,
author,
isOnCenterBg,
style,
size,
}: Props) => {
const theme = useTheme();
const serverUrl = useServerUrl();
const styles = getStyleSheet(theme);
useEffect(() => {
if (authorId && !author) {
fetchUserByIdBatched(serverUrl, authorId);
}
}, []);
if (author?.deleteAt) {
return (
<CompassIcon

View file

@ -28,6 +28,7 @@ const enhance = withObservables(['channelName'], ({channelName, database}: {chan
);
return {
authorId,
author,
};
});

View file

@ -11,7 +11,6 @@ import {General, Permissions} from '@constants';
import {MM_TABLES} from '@constants/database';
import {sanitizeLikeString} from '@helpers/database';
import {hasPermission} from '@utils/role';
import {getUserIdFromChannelName} from '@utils/user';
import {prepareDeletePost} from './post';
import {queryRoles} from './role';
@ -201,6 +200,16 @@ export const queryAllMyChannelsForTeam = (database: Database, teamId: string) =>
);
};
export const queryAllUnreadDMsAndGMsIds = (database: Database) => {
return database.get<ChannelModel>(CHANNEL).query(
Q.on(MY_CHANNEL, Q.or(
Q.where('mentions_count', Q.gt(0)),
Q.where('message_count', Q.gt(0)),
)),
Q.where('type', Q.oneOf([General.GM_CHANNEL, General.DM_CHANNEL])),
);
};
export const getMyChannel = async (database: Database, channelId: string) => {
try {
const member = await database.get<MyChannelModel>(MY_CHANNEL).find(channelId);
@ -463,38 +472,6 @@ export const queryMyChannelUnreads = (database: Database, currentTeamId: string)
);
};
export const observeArchivedDirectChannels = (database: Database, currentUserId: string) => {
const deactivated = database.get<UserModel>(USER).query(
Q.where('delete_at', Q.gt(0)),
).observe();
return deactivated.pipe(
switchMap((users) => {
const usersMap = new Map(users.map((u) => [u.id, u]));
return database.get<ChannelModel>(CHANNEL).query(
Q.on(
CHANNEL_MEMBERSHIP,
Q.and(
Q.where('user_id', Q.notEq(currentUserId)),
Q.where('user_id', Q.oneOf(Array.from(usersMap.keys()))),
),
),
Q.where('type', 'D'),
).observe().pipe(
switchMap((channels) => {
// eslint-disable-next-line max-nested-callbacks
return of$(new Map(channels.map((c) => {
const teammateId = getUserIdFromChannelName(currentUserId, c.name);
const user = usersMap.get(teammateId);
return [c.id, user];
})));
}),
);
}),
);
};
export function observeMyChannelMentionCount(database: Database, teamId?: string, columns = ['mentions_count', 'is_unread']): Observable<number> {
const conditions: Q.Where[] = [
Q.where('delete_at', Q.eq(0)),

View file

@ -125,3 +125,13 @@ export const observeUserIsChannelAdmin = (database: Database, userId: string, ch
distinctUntilChanged(),
);
};
export const observeDeactivatedUsers = (database: Database) => {
return database.get<UserModel>(USER).query(
Q.where('delete_at', Q.gt(0)),
).observe().pipe(
switchMap((users) => {
return of$(new Map(users.map((u) => [u.id, u])));
}),
);
};

View file

@ -9,9 +9,10 @@ import {switchMap, combineLatestWith, distinctUntilChanged} from 'rxjs/operators
import {Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories';
import {getSidebarPreferenceAsBool} from '@helpers/api/preference';
import {observeArchivedDirectChannels, observeNotifyPropsByChannels} from '@queries/servers/channel';
import {observeNotifyPropsByChannels} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName, querySidebarPreferences} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system';
import {observeDeactivatedUsers} from '@queries/servers/user';
import {type ChannelWithMyChannel, filterArchivedChannels, filterAutoclosedDMs, filterManuallyClosedDms, getUnreadIds, sortChannels} from '@utils/categories';
import CategoryBody from './category_body';
@ -106,15 +107,15 @@ const enhanced = withObservables([], ({category, currentUserId, database, isTabl
distinctUntilChanged(),
);
const deactivated = (category.type === DMS_CATEGORY) ? observeArchivedDirectChannels(database, currentUserId) : of$(undefined);
const deactivated = (category.type === DMS_CATEGORY) ? observeDeactivatedUsers(database) : of$(undefined);
const sortedChannels = channelsWithMyChannel.pipe(
combineLatestWith(categorySorting, currentChannelId, lastUnreadId, notifyPropsPerChannel, manuallyClosedPrefs, autoclosePrefs, deactivated, limit),
switchMap(([cwms, sorting, channelId, unreadId, notifyProps, manuallyClosedDms, autoclose, deactivatedDMS, maxDms]) => {
switchMap(([cwms, sorting, channelId, unreadId, notifyProps, manuallyClosedDms, autoclose, deactivatedUsers, maxDms]) => {
let channelsW = cwms;
channelsW = filterArchivedChannels(channelsW, channelId);
channelsW = filterManuallyClosedDms(channelsW, notifyProps, manuallyClosedDms, currentUserId, unreadId);
channelsW = filterAutoclosedDMs(category.type, maxDms, channelId, channelsW, autoclose, notifyProps, deactivatedDMS, unreadId);
channelsW = filterAutoclosedDMs(category.type, maxDms, currentUserId, channelId, channelsW, autoclose, notifyProps, deactivatedUsers, unreadId);
return of$(sortChannels(sorting, channelsW, notifyProps, locale));
}),

View file

@ -32,10 +32,10 @@ export const filterArchivedChannels = (channelsWithMyChannel: ChannelWithMyChann
};
export const filterAutoclosedDMs = (
categoryType: CategoryType, limit: number, currentChannelId: string,
categoryType: CategoryType, limit: number, currentUserId: string, currentChannelId: string,
channelsWithMyChannel: ChannelWithMyChannel[], preferences: PreferenceModel[],
notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>,
deactivatedDMs?: Map<string, UserModel | undefined >,
deactivatedUsers?: Map<string, UserModel | undefined >,
lastUnreadChannelId?: string,
) => {
if (categoryType !== DMS_CATEGORY) {
@ -71,16 +71,22 @@ export const filterAutoclosedDMs = (
return true;
}
const lastViewedAt = getLastViewedAt(cwm);
// DMs with deactivated users will be visible if you're currently viewing them and they were opened
// since the user was deactivated
if (channel.type === General.DM_CHANNEL) {
const lastViewedAt = getLastViewedAt(cwm);
const teammate = deactivatedDMs?.get(channel.id);
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
const teammate = deactivatedUsers?.get(teammateId);
if (teammate && teammate.deleteAt > lastViewedAt) {
return false;
}
}
if (isDMorGM(channel) && !lastViewedAt) {
return false;
}
return true;
});
@ -138,9 +144,6 @@ export const filterManuallyClosedDms = (
if (!isDMorGM(channel)) {
return true;
} else if (!myChannel.lastPostAt) {
// If the direct channel does not have posts we hide it
return false;
}
if (isUnreadChannel(myChannel, notifyPropsPerChannel[myChannel.id], lastUnreadChannelId)) {