Unify Is Read Only logic to avoid code repetition and improve redability (#8720)

This commit is contained in:
Daniel Espino García 2025-04-06 17:50:19 +02:00 committed by GitHub
parent 250732d3fc
commit fefa164a06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 461 additions and 260 deletions

View file

@ -28,7 +28,7 @@ import {getHasCRTChanged} from '@queries/servers/preference';
import {getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId, getConfigValue} from '@queries/servers/system';
import {getTeamChannelHistory} from '@queries/servers/team';
import NavigationStore from '@store/navigation_store';
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
import {isDefaultChannel, isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
import {getFullErrorMessage} from '@utils/errors';
import {isMinimumServerVersion, isTablet} from '@utils/helpers';
import {logDebug, logError} from '@utils/log';
@ -227,7 +227,7 @@ export async function entryInitialChannelId(database: Database, requestedChannel
}
// Check if we are member of the default channel.
const defaultChannel = channels?.find((c) => c.name === General.DEFAULT_CHANNEL && c.team_id === initialTeamId);
const defaultChannel = channels?.find((c) => isDefaultChannel(c) && c.team_id === initialTeamId);
const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannel && membershipIds.has(defaultChannel.id));
if (iAmMemberOfTheTeamDefaultChannel) {
return defaultChannel!.id;

View file

@ -5,9 +5,9 @@ import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentUser} from '@queries/servers/user';
import {isDefaultChannel} from '@utils/channel';
import LeaveChannelLabel from './leave_channel_label';
@ -23,8 +23,8 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps
const canLeave = channel.pipe(
combineLatestWith(currentUser),
switchMap(([ch, u]) => {
const isDefaultChannel = ch?.name === General.DEFAULT_CHANNEL;
return of$(!isDefaultChannel || (isDefaultChannel && u?.isGuest));
const isDC = isDefaultChannel(ch);
return of$(!isDC || (isDC && u?.isGuest));
}),
);

View file

@ -7,12 +7,12 @@ import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {observeChannel, observeIsReadOnlyChannel} from '@queries/servers/channel';
import {queryDraft, observeFirstDraft} from '@queries/servers/drafts';
import {observePermissionForChannel} from '@queries/servers/role';
import {observeConfigBooleanValue, observeCurrentChannelId} from '@queries/servers/system';
import {observeCurrentChannelId} from '@queries/servers/system';
import {observeCurrentUser, observeUser} from '@queries/servers/user';
import {isSystemAdmin, getUserIdFromChannelName} from '@utils/user';
import {getUserIdFromChannelName} from '@utils/user';
import PostDraft from './post_draft';
@ -49,10 +49,7 @@ const enhanced = withObservables(['channelId', 'rootId', 'channelIsArchived'], (
const canPost = combineLatest([channel, currentUser]).pipe(switchMap(([c, u]) => (c && u ? observePermissionForChannel(database, c, u, Permissions.CREATE_POST, true) : of$(true))));
const channelIsArchived = channel.pipe(switchMap((c) => (ownProps.channelIsArchived ? of$(true) : of$(c?.deleteAt !== 0))));
const experimentalTownSquareIsReadOnly = observeConfigBooleanValue(database, 'ExperimentalTownSquareIsReadOnly');
const channelIsReadOnly = combineLatest([currentUser, channel, experimentalTownSquareIsReadOnly]).pipe(
switchMap(([u, c, readOnly]) => of$(c?.name === General.DEFAULT_CHANNEL && !isSystemAdmin(u?.roles || '') && readOnly)),
);
const channelIsReadOnly = observeIsReadOnlyChannel(database, ownProps.channelId);
const deactivatedChannel = combineLatest([currentUser, channel]).pipe(
switchMap(([u, c]) => {

View file

@ -3,15 +3,14 @@
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {combineLatest, of as of$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {Permissions} from '@constants';
import {observeChannel, observeIsReadOnlyChannel} from '@queries/servers/channel';
import {observeReactionsForPost} from '@queries/servers/reaction';
import {observePermissionForPost} from '@queries/servers/role';
import {observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
import {isSystemAdmin} from '@utils/user';
import Reactions from './reactions';
@ -28,9 +27,13 @@ const withReactions = withObservables(['post'], ({database, post}: WithReactions
switchMap((id) => observeUser(database, id)),
);
const channel = observeChannel(database, post.channelId);
const experimentalTownSquareIsReadOnly = observeConfigBooleanValue(database, 'ExperimentalTownSquareIsReadOnly');
const disabled = combineLatest([currentUser, channel, experimentalTownSquareIsReadOnly]).pipe(
map(([u, c, readOnly]) => ((c && c.deleteAt > 0) || (c?.name === General.DEFAULT_CHANNEL && !isSystemAdmin(u?.roles || '') && readOnly))),
const disabled = combineLatest([channel]).pipe(
switchMap(([c]) => {
if (c && c.deleteAt > 0) {
return of$(true);
}
return observeIsReadOnlyChannel(database, post.channelId);
}),
);
const canAddReaction = currentUser.pipe(switchMap((u) => observePermissionForPost(database, post, u, Permissions.ADD_REACTION, true)));

View file

@ -0,0 +1,374 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb';
import {General} from '@constants';
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import TestHelper from '@test/test_helper';
import {
observeIsReadOnlyChannel,
observeMyChannelUnreads,
prepareAllMyChannels,
} from './channel';
describe('observeMyChannelUnreads', () => {
const teamId = 'team_id';
const userId = 'user_id';
const serverUrl = 'baseHandler.test.com';
let database: Database;
let operator: ServerDataOperator;
jest.restoreAllMocks();
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
});
afterEach(async () => {
await DatabaseManager.deleteServerDatabase(serverUrl);
});
it('should return true when there are unread channels that are not muted', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'all' as const};
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).toHaveBeenCalledWith(true);
subscriptionNext.mockClear();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should return false when all unread channels are muted', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'mention' as const};
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).not.toHaveBeenCalled();
});
it('missing notify props are considered unmuted', async () => {
const subscriptionNext = jest.fn();
const notify_props = {};
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).toHaveBeenCalledWith(true);
subscriptionNext.mockClear();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should not retrigger the subscription when there are changes in other teams', async () => {
const subscriptionNext = jest.fn();
const otherTeamId = 'other_team_id';
const notify_props = {mark_unread: 'all' as const};
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: otherTeamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: otherTeamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: otherTeamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
expect(subscriptionNext).not.toHaveBeenCalled();
});
});
describe('observeIsReadOnlyChannel', () => {
const userId = 'user_id';
const serverUrl = 'baseHandler.test.com';
let database: Database;
let operator: ServerDataOperator;
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
await operator.handleSystem({
systems: [{id: 'currentUserId', value: userId}],
prepareRecordsOnly: false,
});
});
afterEach(async () => {
await DatabaseManager.deleteServerDatabase(serverUrl);
});
it('should return true for town square when user is not admin and setting is enabled', async () => {
const subscriptionNext = jest.fn();
const channelId = 'channel_id';
// Set up initial data
const models = (await Promise.all([
operator.handleChannel({
channels: [TestHelper.fakeChannel({id: channelId, name: General.DEFAULT_CHANNEL})],
prepareRecordsOnly: true,
}),
operator.handleUsers({
users: [TestHelper.fakeUser({id: userId, roles: ''})],
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
// Set up config
await operator.handleConfigs({
configs: [{id: 'ExperimentalTownSquareIsReadOnly', value: 'true'}],
configsToDelete: [],
prepareRecordsOnly: false,
});
const result = observeIsReadOnlyChannel(database, channelId);
result.subscribe({next: subscriptionNext});
expect(subscriptionNext).toHaveBeenCalledWith(true);
});
it('should return false for non-town-square channels', async () => {
const subscriptionNext = jest.fn();
const channelId = 'channel_id';
// Set up initial data
const models = (await Promise.all([
operator.handleChannel({
channels: [TestHelper.fakeChannel({id: channelId, name: 'other-channel'})],
prepareRecordsOnly: true,
}),
operator.handleUsers({
users: [TestHelper.fakeUser({id: userId, roles: ''})],
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
// Set up config
await operator.handleConfigs({
configs: [{id: 'ExperimentalTownSquareIsReadOnly', value: 'true'}],
configsToDelete: [],
prepareRecordsOnly: false,
});
const result = observeIsReadOnlyChannel(database, channelId);
result.subscribe({next: subscriptionNext});
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should return false when experimental setting is disabled', async () => {
const subscriptionNext = jest.fn();
const channelId = 'channel_id';
// Set up initial data
const models = (await Promise.all([
operator.handleChannel({
channels: [TestHelper.fakeChannel({id: channelId, name: General.DEFAULT_CHANNEL})],
prepareRecordsOnly: true,
}),
operator.handleUsers({
users: [TestHelper.fakeUser({id: userId, roles: ''})],
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
// Set up config
await operator.handleConfigs({
configs: [{id: 'ExperimentalTownSquareIsReadOnly', value: 'false'}],
configsToDelete: [],
prepareRecordsOnly: false,
});
const result = observeIsReadOnlyChannel(database, channelId);
result.subscribe({next: subscriptionNext});
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should return false when user is system admin', async () => {
const subscriptionNext = jest.fn();
const channelId = 'channel_id';
// Set up initial data
const models = (await Promise.all([
operator.handleChannel({
channels: [TestHelper.fakeChannel({id: channelId, name: General.DEFAULT_CHANNEL})],
prepareRecordsOnly: true,
}),
operator.handleUsers({
users: [TestHelper.fakeUser({id: userId, roles: 'system_admin'})],
prepareRecordsOnly: true,
}),
])).flat();
await operator.batchRecords(models, 'test');
// Set up config
await operator.handleConfigs({
configs: [{id: 'ExperimentalTownSquareIsReadOnly', value: 'true'}],
configsToDelete: [],
prepareRecordsOnly: false,
});
const result = observeIsReadOnlyChannel(database, channelId);
result.subscribe({next: subscriptionNext});
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should return false when channel is undefined', async () => {
const subscriptionNext = jest.fn();
const channelId = 'nonexistent_channel';
// Set up initial data
const models = await operator.handleUsers({
users: [TestHelper.fakeUser({id: userId, roles: ''})],
prepareRecordsOnly: true,
});
await operator.batchRecords(models, 'test');
// Set up config
await operator.handleConfigs({
configs: [{id: 'ExperimentalTownSquareIsReadOnly', value: 'true'}],
configsToDelete: [],
prepareRecordsOnly: false,
});
const result = observeIsReadOnlyChannel(database, channelId);
result.subscribe({next: subscriptionNext});
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
});

View file

@ -8,7 +8,6 @@ import {of as of$} from 'rxjs';
import {General, Permissions} from '@constants';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import ServerDataOperator from '@database/operator/server_data_operator';
import TestHelper from '@test/test_helper';
import {hasPermission} from '@utils/role';
@ -63,8 +62,6 @@ import {prepareChannels,
queryChannelMembers,
queryChannelsForAutocomplete,
observeChannelMembers,
observeMyChannelUnreads,
prepareAllMyChannels,
} from './channel';
import {queryRoles} from './role';
import {getCurrentChannelId, observeCurrentChannelId, observeCurrentUserId} from './system';
@ -1562,216 +1559,3 @@ describe('Channel Observations', () => {
});
});
});
describe('observeMyChannelUnreads', () => {
const teamId = 'team_id';
const userId = 'user_id';
const serverUrl = 'baseHandler.test.com';
const waitTime = 50;
let database: Database;
let operator: ServerDataOperator;
jest.restoreAllMocks();
beforeEach(async () => {
await DatabaseManager.init([serverUrl]);
const serverDatabaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
database = serverDatabaseAndOperator.database;
operator = serverDatabaseAndOperator.operator;
});
afterEach(async () => {
await DatabaseManager.deleteServerDatabase(serverUrl);
});
it('should return true when there are unread channels that are not muted', async () => {
const subscriptionNext = jest.fn();
const notify_props = TestHelper.fakeChannelNotifyProps({mark_unread: 'all'});
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
TestHelper.wait(waitTime);
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).toHaveBeenCalledWith(true);
subscriptionNext.mockClear();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should return false when all unread channels are muted', async () => {
const subscriptionNext = jest.fn();
const notify_props = TestHelper.fakeChannelNotifyProps({mark_unread: General.MENTION});
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
TestHelper.wait(waitTime);
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
it('missing notify props are considered unmuted', async () => {
const subscriptionNext = jest.fn();
const notify_props = {};
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
TestHelper.wait(waitTime);
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).toHaveBeenCalledWith(true);
subscriptionNext.mockClear();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: teamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).toHaveBeenCalledWith(false);
});
it('should not retrigger the subscription when there are changes in other teams', async () => {
const subscriptionNext = jest.fn();
const otherTeamId = 'other_team_id';
const notify_props = TestHelper.fakeChannelNotifyProps({mark_unread: General.MENTION});
const result = observeMyChannelUnreads(database, teamId);
result.subscribe({next: subscriptionNext});
TestHelper.wait(waitTime);
// Subscription always returns the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
let models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: otherTeamId, total_msg_count: 20})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
// No change
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: otherTeamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 20})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
models = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'channel1', team_id: otherTeamId, total_msg_count: 30})],
[TestHelper.fakeMyChannel({channel_id: 'channel1', user_id: userId, notify_props, msg_count: 30})],
false,
)))).flat();
await operator.batchRecords(models, 'test');
TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
});

View file

@ -4,18 +4,20 @@
/* eslint-disable max-lines */
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
import {of as of$, Observable} from 'rxjs';
import {of as of$, Observable, combineLatest} from 'rxjs';
import {map as map$, switchMap, distinctUntilChanged, combineLatestWith} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {MM_TABLES} from '@constants/database';
import {sanitizeLikeString} from '@helpers/database';
import {isDefaultChannel} from '@utils/channel';
import {hasPermission} from '@utils/role';
import {isSystemAdmin} from '@utils/user';
import {prepareDeletePost} from './post';
import {queryRoles} from './role';
import {observeCurrentChannelId, getCurrentChannelId, observeCurrentUserId} from './system';
import {observeTeammateNameDisplay} from './user';
import {observeCurrentChannelId, getCurrentChannelId, observeCurrentUserId, observeConfigBooleanValue} from './system';
import {observeCurrentUser, observeTeammateNameDisplay} from './user';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Clause} from '@nozbe/watermelondb/QueryDescription';
@ -340,7 +342,7 @@ export const getDefaultChannelForTeam = async (database: Database, teamId: strin
Q.sortBy('display_name', Q.asc),
).fetch();
const defaultChannel = myChannels.find((c) => c.name === General.DEFAULT_CHANNEL);
const defaultChannel = myChannels.find((c) => isDefaultChannel(c));
const myFirstTeamChannel = myChannels[0];
if (defaultChannel || canIJoinPublicChannelsInTeam) {
@ -751,3 +753,12 @@ export const queryChannelMembers = (database: Database, channelId: string) => {
export const observeChannelMembers = (database: Database, channelId: string) => {
return queryChannelMembers(database, channelId).observe();
};
export const observeIsReadOnlyChannel = (database: Database, channelId: string) => {
const channel = observeChannel(database, channelId);
const experimentalTownSquareIsReadOnly = observeConfigBooleanValue(database, 'ExperimentalTownSquareIsReadOnly');
const user = observeCurrentUser(database);
return combineLatest([channel, user, experimentalTownSquareIsReadOnly]).pipe(
switchMap(([c, u, readOnly]) => of$(isDefaultChannel(c) && !isSystemAdmin(u?.roles || '') && readOnly)),
);
};

View file

@ -6,7 +6,7 @@ import {of as of$, combineLatest} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {Database as DatabaseConstants, General, Permissions} from '@constants';
import {isDMorGM} from '@utils/channel';
import {isDefaultChannel, isDMorGM} from '@utils/channel';
import {hasPermission} from '@utils/role';
import {observeChannel, observeMyChannelRoles} from './channel';
@ -91,7 +91,7 @@ export function observePermissionForPost(database: Database, post: PostModel, us
export function observeCanManageChannelMembers(database: Database, channelId: string, user: UserModel) {
return observeChannel(database, channelId).pipe(
switchMap((c) => {
if (!c || c.deleteAt !== 0 || isDMorGM(c) || c.name === General.DEFAULT_CHANNEL) {
if (!c || c.deleteAt !== 0 || isDMorGM(c) || isDefaultChannel(c)) {
return of$(false);
}

View file

@ -9,6 +9,7 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useDidUpdate from '@hooks/did_update';
import EphemeralStore from '@store/ephemeral_store';
import {isDefaultChannel} from '@utils/channel';
import DirectChannel from './direct_channel';
import PublicOrPrivateChannel from './public_or_private_channel';
@ -42,7 +43,7 @@ const Intro = ({channel, roles}: Props) => {
return null;
}
if (channel.type === General.OPEN_CHANNEL && channel.name === General.DEFAULT_CHANNEL) {
if (channel.type === General.OPEN_CHANNEL && isDefaultChannel(channel)) {
return (
<TownSquare
channelId={channel.id}

View file

@ -11,6 +11,7 @@ import {observePermissionForChannel, observePermissionForTeam} from '@queries/se
import {observeConfigBooleanValue} from '@queries/servers/system';
import {observeCurrentTeam} from '@queries/servers/team';
import {observeCurrentUser} from '@queries/servers/user';
import {isDefaultChannel} from '@utils/channel';
import Archive from './archive';
@ -30,8 +31,8 @@ const enhanced = withObservables(['channelId', 'type'], ({channelId, database, t
const canLeave = channel.pipe(
combineLatestWith(currentUser),
switchMap(([ch, u]) => {
const isDefaultChannel = ch?.name === General.DEFAULT_CHANNEL;
return of$(!isDefaultChannel || (isDefaultChannel && u?.isGuest));
const isDC = isDefaultChannel(ch);
return of$(!isDC || (isDC && u?.isGuest));
}),
);

View file

@ -5,10 +5,11 @@ import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {of as of$} from 'rxjs';
import {combineLatestWith, switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {Permissions} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {observePermissionForChannel} from '@queries/servers/role';
import {observeCurrentUser} from '@queries/servers/user';
import {isDefaultChannel} from '@utils/channel';
import ConvertPrivate from './convert_private';
@ -24,7 +25,7 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: Props) =
const canConvert = channel.pipe(
combineLatestWith(currentUser),
switchMap(([ch, u]) => {
if (!ch || !u || ch.name === General.DEFAULT_CHANNEL) {
if (!ch || !u || isDefaultChannel(ch)) {
return of$(false);
}

View file

@ -5,22 +5,21 @@ import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {combineLatest, of as of$, Observable} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {General, Permissions, Post, Screens} from '@constants';
import {Permissions, Post, Screens} from '@constants';
import {AppBindingLocations} from '@constants/apps';
import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
import AppsManager from '@managers/apps_manager';
import {observeChannel} from '@queries/servers/channel';
import {observeChannel, observeIsReadOnlyChannel} from '@queries/servers/channel';
import {observePost, observePostSaved} from '@queries/servers/post';
import {observeReactionsForPost} from '@queries/servers/reaction';
import {observePermissionForChannel, observePermissionForPost} from '@queries/servers/role';
import {observeConfigBooleanValue, observeConfigIntValue, observeConfigValue, observeLicense} from '@queries/servers/system';
import {observeConfigIntValue, observeConfigValue, observeLicense} from '@queries/servers/system';
import {observeIsCRTEnabled, observeThreadById} from '@queries/servers/thread';
import {observeCurrentUser} from '@queries/servers/user';
import {toMilliseconds} from '@utils/datetime';
import {isMinimumServerVersion} from '@utils/helpers';
import {isFromWebhook, isSystemMessage} from '@utils/post';
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
import {isSystemAdmin} from '@utils/user';
import PostOptions from './post_options';
@ -92,10 +91,7 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, sour
return observePermissionForPost(database, post, u, isOwner ? Permissions.DELETE_POST : Permissions.DELETE_OTHERS_POSTS, false);
}));
const experimentalTownSquareIsReadOnly = observeConfigBooleanValue(database, 'ExperimentalTownSquareIsReadOnly');
const channelIsReadOnly = combineLatest([currentUser, channel, experimentalTownSquareIsReadOnly]).pipe(switchMap(([u, c, readOnly]) => {
return of$(c?.name === General.DEFAULT_CHANNEL && (u && !isSystemAdmin(u.roles)) && readOnly);
}));
const channelIsReadOnly = observeIsReadOnlyChannel(database, post.channelId);
const isUnderMaxAllowedReactions = observeReactionsForPost(database, post.id).pipe(
// eslint-disable-next-line max-nested-callbacks

View file

@ -12,6 +12,7 @@ import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {observeCanManageChannelMembers, observePermissionForChannel} from '@queries/servers/role';
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
import {observeTeammateNameDisplay, observeCurrentUser, observeUser, observeUserIsChannelAdmin, observeUserIsTeamAdmin} from '@queries/servers/user';
import {isDefaultChannel} from '@utils/channel';
import {isSystemAdmin} from '@utils/user';
import UserProfile from './user_profile';
@ -30,8 +31,8 @@ const enhanced = withObservables([], ({channelId, database, userId}: EnhancedPro
const user = observeUser(database, userId);
const teammateDisplayName = observeTeammateNameDisplay(database);
const isChannelAdmin = channelId ? observeUserIsChannelAdmin(database, userId, channelId) : of$(false);
const isDefaultChannel = channel ? channel.pipe(
switchMap((c) => of$(c?.name === General.DEFAULT_CHANNEL)),
const isDC = channel ? channel.pipe(
switchMap((c) => of$(isDefaultChannel(c))),
) : of$(false);
const isDirectMessage = channelId ? channel.pipe(
switchMap((c) => of$(c?.type === General.DM_CHANNEL)),
@ -60,7 +61,7 @@ const enhanced = withObservables([], ({channelId, database, userId}: EnhancedPro
enablePostUsernameOverride,
isChannelAdmin,
isCustomStatusEnabled,
isDefaultChannel,
isDefaultChannel: isDC,
isDirectMessage,
isMilitaryTime,
isSystemAdmin: systemAdmin,

View file

@ -6,7 +6,20 @@ import {createIntl} from 'react-intl';
import {Channel, General, Permissions} from '@constants';
import {hasPermission} from '@utils/role';
import {compareNotifyProps, filterChannelsMatchingTerm, generateChannelNameFromDisplayName, getDirectChannelName, isArchived, isDMorGM, isTypeDMorGM, selectDefaultChannelForTeam, sortChannelsByDisplayName, sortChannelsModelByDisplayName, validateDisplayName} from '.';
import {
compareNotifyProps,
filterChannelsMatchingTerm,
generateChannelNameFromDisplayName,
getDirectChannelName,
isArchived,
isDefaultChannel,
isDMorGM,
isTypeDMorGM,
selectDefaultChannelForTeam,
sortChannelsByDisplayName,
sortChannelsModelByDisplayName,
validateDisplayName,
} from '.';
import type ChannelModel from '@typings/database/models/servers/channel';
@ -244,3 +257,18 @@ describe('filterChannelsMatchingTerm', () => {
});
});
describe('isDefaultChannel', () => {
it('should return true if channel is the default channel', () => {
const channel = {name: General.DEFAULT_CHANNEL} as Channel; // using casting instead of TestHelper to avoid circular dependency
expect(isDefaultChannel(channel)).toBe(true);
});
it('should return false if channel is not the default channel', () => {
const channel = {name: 'random'} as Channel; // using casting instead of TestHelper to avoid circular dependency
expect(isDefaultChannel(channel)).toBe(false);
});
it('should return false if channel is undefined', () => {
expect(isDefaultChannel(undefined)).toBe(false);
});
});

View file

@ -45,7 +45,7 @@ export function selectDefaultChannelForTeam<T extends Channel|ChannelModel>(chan
if (roles) {
canIJoinPublicChannelsInTeam = hasPermission(roles, Permissions.JOIN_PUBLIC_CHANNELS);
}
const defaultChannel = channels?.find((c) => c.name === General.DEFAULT_CHANNEL);
const defaultChannel = channels?.find(isDefaultChannel);
const membershipIds = new Set(memberships.map((m) => m.channel_id));
const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannel && membershipIds.has(defaultChannel.id));
const myFirstTeamChannel = channels?.filter((c) =>
@ -158,3 +158,7 @@ export function filterChannelsMatchingTerm(channels: Channel[], term: string): C
displayName.startsWith(lowercasedTerm);
});
}
export function isDefaultChannel(channel: Channel | ChannelModel | undefined): boolean {
return channel?.name === General.DEFAULT_CHANNEL;
}