diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index ac59246f76db..af571a2c861a 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,7 +4,7 @@ on: release: types: [published] pull_request: - branches: "**" + branches: '**' push: branches: - develop @@ -36,7 +36,7 @@ jobs: - name: Use Node.js 14.18.3 uses: actions/setup-node@v3 with: - node-version: "14.18.3" + node-version: '14.18.3' - uses: actions/checkout@v3 @@ -175,8 +175,8 @@ jobs: strategy: matrix: - node-version: ["14.18.3"] - mongodb-version: ["3.6", "4.0", "4.2", "4.4", "5.0"] + node-version: ['14.18.3'] + mongodb-version: ['3.6', '4.0', '4.2', '4.4', '5.0'] steps: - name: Launch MongoDB @@ -247,7 +247,7 @@ jobs: - name: E2E Test API env: - TEST_MODE: "true" + TEST_MODE: 'true' MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://localhost:27017/local run: | @@ -258,7 +258,7 @@ jobs: - name: E2E Test UI (Legacy - Cypress) env: - TEST_MODE: "true" + TEST_MODE: 'true' MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://localhost:27017/local run: | @@ -269,7 +269,7 @@ jobs: - name: E2E Test UI env: - TEST_MODE: "true" + TEST_MODE: 'true' MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://localhost:27017/local run: | @@ -305,8 +305,8 @@ jobs: strategy: matrix: - node-version: ["14.18.3"] - mongodb-version: ["4.4"] + node-version: ['14.18.3'] + mongodb-version: ['4.4'] steps: - name: Launch MongoDB @@ -372,12 +372,12 @@ jobs: - name: E2E Test API env: - TEST_MODE: "true" + TEST_MODE: 'true' MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://localhost:27017/local ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} TRANSPORTER: nats://localhost:4222 - SKIP_PROCESS_EVENT_REGISTRATION: "true" + SKIP_PROCESS_EVENT_REGISTRATION: 'true' run: | echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc Xvfb -screen 0 1024x768x24 :99 & @@ -388,7 +388,7 @@ jobs: - name: E2E Test UI (Legacy - Cypress) env: - TEST_MODE: "true" + TEST_MODE: 'true' MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://localhost:27017/local ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} @@ -396,7 +396,7 @@ jobs: CYPRESS_BASE_URL: http://localhost:4000 CYPRESS_TEST_API_URL: http://localhost:4000 OVERWRITE_SETTING_Site_Url: http://localhost:4000 - SKIP_PROCESS_EVENT_REGISTRATION: "true" + SKIP_PROCESS_EVENT_REGISTRATION: 'true' run: | echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc Xvfb -screen 0 1024x768x24 :99 & @@ -412,7 +412,7 @@ jobs: - name: E2E Test UI env: - TEST_MODE: "true" + TEST_MODE: 'true' MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://localhost:27017/local ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} @@ -420,7 +420,7 @@ jobs: CYPRESS_BASE_URL: http://localhost:4000 CYPRESS_TEST_API_URL: http://localhost:4000 OVERWRITE_SETTING_Site_Url: http://localhost:4000 - SKIP_PROCESS_EVENT_REGISTRATION: "true" + SKIP_PROCESS_EVENT_REGISTRATION: 'true' run: | echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc Xvfb -screen 0 1024x768x24 :99 & @@ -469,7 +469,7 @@ jobs: strategy: matrix: - release: ["official", "preview"] + release: ['official', 'preview'] steps: - uses: actions/checkout@v3 @@ -519,7 +519,7 @@ jobs: - name: Use Node.js 14.18.3 uses: actions/setup-node@v3 with: - node-version: "14.18.3" + node-version: '14.18.3' - name: Install Meteor run: | @@ -601,8 +601,7 @@ jobs: strategy: matrix: - service: - ["ddp-streamer"] + service: ['ddp-streamer'] steps: - uses: actions/checkout@v3 @@ -610,7 +609,19 @@ jobs: - name: Use Node.js 14.18.3 uses: actions/setup-node@v3 with: - node-version: "14.18.3" + node-version: '14.18.3' + + - uses: c-hive/gha-yarn-cache@v2 + - name: Cache turbo + id: cache-turbo + uses: actions/cache@v2 + with: + path: | + ./node_modules/.turbo + key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turbo- + ${{ runner.os }}- - name: Build Docker images env: @@ -670,7 +681,7 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: "us-east-1" + AWS_DEFAULT_REGION: 'us-east-1' GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} REDHAT_REGISTRY_PID: ${{ secrets.REDHAT_REGISTRY_PID }} REDHAT_REGISTRY_KEY: ${{ secrets.REDHAT_REGISTRY_KEY }} @@ -738,10 +749,10 @@ jobs: strategy: matrix: # this is current a mix of variants and different images - release: ["official", "preview", "alpine"] + release: ['official', 'preview', 'alpine'] env: - IMAGE_NAME: "rocketchat/rocket.chat" + IMAGE_NAME: 'rocketchat/rocket.chat' steps: - uses: actions/checkout@v3 @@ -844,8 +855,7 @@ jobs: strategy: matrix: - service: - ["account", "authorization", "ddp-streamer", "presence", "stream-hub"] + service: ['account', 'authorization', 'ddp-streamer', 'presence', 'stream-hub'] steps: - uses: actions/checkout@v3 @@ -853,7 +863,18 @@ jobs: - name: Use Node.js 14.18.3 uses: actions/setup-node@v3 with: - node-version: "14.18.3" + node-version: '14.18.3' + - uses: c-hive/gha-yarn-cache@v2 + - name: Cache turbo + id: cache-turbo + uses: actions/cache@v2 + with: + path: | + ./node_modules/.turbo + key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turbo- + ${{ runner.os }}- - name: Login to DockerHub uses: docker/login-action@v1 diff --git a/apps/meteor/app/api/server/v1/channels.js b/apps/meteor/app/api/server/v1/channels.js index ce2590fcce3e..b5137b487223 100644 --- a/apps/meteor/app/api/server/v1/channels.js +++ b/apps/meteor/app/api/server/v1/channels.js @@ -516,7 +516,7 @@ API.v1.addRoute( checkedArchived: false, }); - if (findResult.broadcast && !hasPermission(this.userId, 'view-broadcast-member-list')) { + if (findResult.broadcast && !hasPermission(this.userId, 'view-broadcast-member-list', findResult._id)) { return API.v1.unauthorized(); } diff --git a/apps/meteor/app/authorization/client/hasRole.ts b/apps/meteor/app/authorization/client/hasRole.ts index 027bff0be080..25995aeb3773 100644 --- a/apps/meteor/app/authorization/client/hasRole.ts +++ b/apps/meteor/app/authorization/client/hasRole.ts @@ -2,18 +2,18 @@ import type { IUser, IRole, IRoom } from '@rocket.chat/core-typings'; import { Roles } from '../../models/client'; -export const hasRole = (userId: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): boolean => { +export const hasRole = (userId: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id'], ignoreSubscriptions = false): boolean => { if (Array.isArray(roleId)) { throw new Error('error-invalid-arguments'); } - return Roles.isUserInRoles(userId, [roleId], scope); + return Roles.isUserInRoles(userId, [roleId], scope, ignoreSubscriptions); }; -export const hasAnyRole = (userId: IUser['_id'], roleIds: IRole['_id'][], scope?: IRoom['_id']): boolean => { +export const hasAnyRole = (userId: IUser['_id'], roleIds: IRole['_id'][], scope?: IRoom['_id'], ignoreSubscriptions = false): boolean => { if (!Array.isArray(roleIds)) { throw new Error('error-invalid-arguments'); } - return Roles.isUserInRoles(userId, roleIds, scope); + return Roles.isUserInRoles(userId, roleIds, scope, ignoreSubscriptions); }; diff --git a/apps/meteor/app/federation-v2/server/bridge.ts b/apps/meteor/app/federation-v2/server/bridge.ts index 21457fc56101..289f7e9033d4 100644 --- a/apps/meteor/app/federation-v2/server/bridge.ts +++ b/apps/meteor/app/federation-v2/server/bridge.ts @@ -1,10 +1,12 @@ -import { Bridge as MatrixBridge, AppServiceRegistration } from '@rocket.chat/forked-matrix-appservice-bridge'; +import type { Bridge as MatrixBridge } from '@rocket.chat/forked-matrix-appservice-bridge'; import { settings } from '../../settings/server'; -import { IMatrixEvent } from './definitions/IMatrixEvent'; -import { MatrixEventType } from './definitions/MatrixEventType'; +import { Settings } from '../../models/server/raw'; +import type { IMatrixEvent } from './definitions/IMatrixEvent'; +import type { MatrixEventType } from './definitions/MatrixEventType'; import { addToQueue } from './queue'; import { getRegistrationInfo } from './config'; +import { bridgeLogger } from './logger'; class Bridge { private bridgeInstance: MatrixBridge; @@ -14,12 +16,17 @@ class Bridge { public async start(): Promise { try { await this.stop(); - } finally { - this.createInstance(); + await this.createInstance(); + if (!this.isRunning) { await this.bridgeInstance.run(this.getBridgePort()); this.isRunning = true; } + } catch (e) { + bridgeLogger.error('Failed to initialize the matrix-appservice-bridge.', e); + + bridgeLogger.error('Disabling Matrix Bridge. Please resolve error and try again'); + Settings.updateValueById('Federation_Matrix_enabled', false); } } @@ -28,7 +35,7 @@ class Bridge { return; } // the http server can take some minutes to shutdown and this promise to be resolved - await this.bridgeInstance.close(); + await this.bridgeInstance?.close(); this.isRunning = false; } @@ -36,7 +43,12 @@ class Bridge { return this.bridgeInstance; } - private createInstance(): void { + private async createInstance(): Promise { + bridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge'); + + // Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails + const { Bridge: MatrixBridge, AppServiceRegistration } = await import('@rocket.chat/forked-matrix-appservice-bridge'); + this.bridgeInstance = new MatrixBridge({ homeserverUrl: settings.get('Federation_Matrix_homeserver_url'), domain: settings.get('Federation_Matrix_homeserver_domain'), diff --git a/apps/meteor/app/federation-v2/server/matrix-client/user.ts b/apps/meteor/app/federation-v2/server/matrix-client/user.ts index 01a9174baba9..28a5d5a4a318 100644 --- a/apps/meteor/app/federation-v2/server/matrix-client/user.ts +++ b/apps/meteor/app/federation-v2/server/matrix-client/user.ts @@ -1,4 +1,4 @@ -import { MatrixProfileInfo } from '@rocket.chat/forked-matrix-bot-sdk'; +import type { MatrixProfileInfo } from '@rocket.chat/forked-matrix-bot-sdk'; import { IUser } from '@rocket.chat/core-typings'; import { matrixBridge } from '../bridge'; diff --git a/apps/meteor/app/importer-csv/server/importer.js b/apps/meteor/app/importer-csv/server/importer.js index ce92a08c235d..d4d3d014dbfa 100644 --- a/apps/meteor/app/importer-csv/server/importer.js +++ b/apps/meteor/app/importer-csv/server/importer.js @@ -1,7 +1,7 @@ import { Random } from 'meteor/random'; import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import { Users } from '../../models/server'; +import { Users, Settings as SettingsRaw } from '../../models/server'; export class CsvImporter extends Base { constructor(info, importRecord) { @@ -119,7 +119,8 @@ export class CsvImporter extends Base { }); } - super.updateRecord({ 'count.users': parsedUsers.length }); + SettingsRaw.incrementValueById('CSV_Importer_Count', usersCount); + super.updateRecord({ 'count.users': usersCount }); return increaseProgressCount(); } diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js b/apps/meteor/app/importer-hipchat-enterprise/server/importer.js index c2c05c34032f..0e1fed9db759 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/importer.js @@ -5,6 +5,7 @@ import fs from 'fs'; import { Meteor } from 'meteor/meteor'; import { Base, ProgressStep } from '../../importer/server'; +import { Settings as SettingsRaw } from '../../models/server'; export class HipChatEnterpriseImporter extends Base { constructor(info, importRecord) { @@ -52,6 +53,7 @@ export class HipChatEnterpriseImporter extends Base { this.converter.addUser(newUser); } + SettingsRaw.incrementValueById('Hipchat_Enterprise_Importer_Count', count); super.updateRecord({ 'count.users': count }); super.addCountToTotal(count); } diff --git a/apps/meteor/app/importer-slack-users/server/importer.js b/apps/meteor/app/importer-slack-users/server/importer.js index 08c2992c766c..5fcf3924499b 100644 --- a/apps/meteor/app/importer-slack-users/server/importer.js +++ b/apps/meteor/app/importer-slack-users/server/importer.js @@ -5,6 +5,7 @@ import { Random } from 'meteor/random'; import { RawImports, Base, ProgressStep, Selection, SelectionUser } from '../../importer/server'; import { RocketChatFile } from '../../file'; import { Users } from '../../models'; +import { Settings as SettingsRaw } from '../../models/server'; export class SlackUsersImporter extends Base { constructor(info, importRecord) { @@ -164,6 +165,7 @@ export class SlackUsersImporter extends Base { }); } + SettingsRaw.incrementValueById('Slack_Users_Importer_Count', this.users.users.length); super.updateProgress(ProgressStep.FINISHING); super.updateProgress(ProgressStep.DONE); } catch (e) { diff --git a/apps/meteor/app/importer-slack/server/importer.js b/apps/meteor/app/importer-slack/server/importer.js index e2270576fcc5..8a9caa1775fe 100644 --- a/apps/meteor/app/importer-slack/server/importer.js +++ b/apps/meteor/app/importer-slack/server/importer.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import { Messages, ImportData } from '../../models/server'; +import { Messages, ImportData, Settings as SettingsRaw } from '../../models/server'; import { settings } from '../../settings/server'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; @@ -155,6 +155,7 @@ export class SlackImporter extends Base { } this.converter.addUser(newUser); + SettingsRaw.incrementValueById('Slack_Importer_Count'); } } diff --git a/apps/meteor/app/importer/server/classes/ImporterBase.js b/apps/meteor/app/importer/server/classes/ImporterBase.js index 2024338dde56..fd4e7a9972c8 100644 --- a/apps/meteor/app/importer/server/classes/ImporterBase.js +++ b/apps/meteor/app/importer/server/classes/ImporterBase.js @@ -265,9 +265,6 @@ export class Base { this.oldSettings.FileUpload_MediaTypeBlackList = Settings.findOneById('FileUpload_MediaTypeBlackList').value; Settings.updateValueById('FileUpload_MediaTypeBlackList', ''); - - this.oldSettings.UI_Allow_room_names_with_special_chars = Settings.findOneById('UI_Allow_room_names_with_special_chars').value; - Settings.updateValueById('UI_Allow_room_names_with_special_chars', true); break; case ProgressStep.DONE: case ProgressStep.ERROR: @@ -277,7 +274,6 @@ export class Base { Settings.updateValueById('FileUpload_MaxFileSize', this.oldSettings.FileUpload_MaxFileSize); Settings.updateValueById('FileUpload_MediaTypeWhiteList', this.oldSettings.FileUpload_MediaTypeWhiteList); Settings.updateValueById('FileUpload_MediaTypeBlackList', this.oldSettings.FileUpload_MediaTypeBlackList); - Settings.updateValueById('UI_Allow_room_names_with_special_chars', this.oldSettings.UI_Allow_room_names_with_special_chars); break; } diff --git a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js b/apps/meteor/app/lib/server/methods/sendInvitationEmail.js index f5927e8c5a0e..89cbdce9eb97 100644 --- a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js +++ b/apps/meteor/app/lib/server/methods/sendInvitationEmail.js @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import * as Mailer from '../../../mailer'; import { hasPermission } from '../../../authorization'; import { settings } from '../../../settings'; +import { Settings as SettingsRaw } from '../../../models/server'; let html = ''; Meteor.startup(() => { @@ -37,7 +38,7 @@ Meteor.methods({ return validEmails.filter((email) => { try { - return Mailer.send({ + const mailerResult = Mailer.send({ to: email, from: settings.get('From_Email'), subject, @@ -46,6 +47,9 @@ Meteor.methods({ email, }, }); + + SettingsRaw.incrementValueById('Invitation_Email_Count'); + return mailerResult; } catch ({ message }) { throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { method: 'sendInvitationEmail', diff --git a/apps/meteor/app/lib/server/startup/email.ts b/apps/meteor/app/lib/server/startup/email.ts index b5e1d2f938c0..470599a41d52 100644 --- a/apps/meteor/app/lib/server/startup/email.ts +++ b/apps/meteor/app/lib/server/startup/email.ts @@ -474,6 +474,11 @@ settingsRegistry.addGroup('Email', function () { ); }); + this.add('Invitation_Email_Count', 0, { + type: 'int', + hidden: true, + }); + this.section('Forgot_password_section', function () { this.add('Forgot_Password_Email_Subject', '{Forgot_Password_Email_Subject}', { type: 'string', diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index b81c4e52d237..8683597ef833 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -180,6 +180,26 @@ settingsRegistry.addGroup('Accounts', function () { type: 'string', hidden: true, }); + this.add('Manual_Entry_User_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('CSV_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('Hipchat_Enterprise_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('Slack_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('Slack_Users_Importer_Count', 0, { + type: 'int', + hidden: true, + }); this.add('Accounts_UseDefaultBlockedDomainsList', true, { type: 'boolean', }); diff --git a/apps/meteor/app/models/client/models/Roles.js b/apps/meteor/app/models/client/models/Roles.js index 95f4381950cf..d195e413cb85 100644 --- a/apps/meteor/app/models/client/models/Roles.js +++ b/apps/meteor/app/models/client/models/Roles.js @@ -20,12 +20,13 @@ const Roles = Object.assign(new Mongo.Collection(null), { * @param {string} userId * @param {IRole['_id'][]} roles the list of role ids * @param {IRoom['_id']} scope the value for the role scope (room id) + * @param {boolean} ignoreSubscriptions ignore the subscription role scope */ - isUserInRoles(userId, roles, scope) { + isUserInRoles(userId, roles, scope, ignoreSubscriptions = false) { roles = [].concat(roles); return roles.some((roleId) => { const role = this.findOne(roleId); - const roleScope = (role && role.scope) || 'Users'; + const roleScope = ignoreSubscriptions ? 'Users' : (role && role.scope) || 'Users'; const model = Models[roleScope]; return model && model.isUserInRole && model.isUserInRole(userId, roleId, scope); }); diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index bc8932afe570..593fdab626da 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -26,6 +26,7 @@ export class Rooms extends Base { // field used for DMs only this.tryEnsureIndex({ uids: 1 }, { sparse: true }); this.tryEnsureIndex({ createdOTR: 1 }, { sparse: true }); + this.tryEnsureIndex({ encrypted: 1 }, { sparse: true }); // used on statistics this.tryEnsureIndex( { diff --git a/apps/meteor/app/models/server/models/Settings.js b/apps/meteor/app/models/server/models/Settings.js index 19d7059e76a7..c2ef98d35476 100644 --- a/apps/meteor/app/models/server/models/Settings.js +++ b/apps/meteor/app/models/server/models/Settings.js @@ -145,7 +145,7 @@ export class Settings extends Base { return this.update(query, update); } - incrementValueById(_id) { + incrementValueById(_id, value = 1) { const query = { blocked: { $ne: true }, _id, @@ -153,7 +153,7 @@ export class Settings extends Base { const update = { $inc: { - value: 1, + value, }, }; diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index 0a794af7e32f..3e8eb705d3d3 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -60,6 +60,9 @@ export class Users extends Base { this.tryEnsureIndex({ extension: 1 }, { sparse: true, unique: true }); this.tryEnsureIndex({ language: 1 }, { sparse: true }); + this.tryEnsureIndex({ 'active': 1, 'services.email2fa.enabled': 1 }, { sparse: true }); // used by statistics + this.tryEnsureIndex({ 'active': 1, 'services.totp.enabled': 1 }, { sparse: true }); // used by statistics + const collectionObj = this.model.rawCollection(); this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } diff --git a/apps/meteor/app/models/server/raw/Invites.ts b/apps/meteor/app/models/server/raw/Invites.ts index 2f3e9d835a92..ef55e009ca8e 100644 --- a/apps/meteor/app/models/server/raw/Invites.ts +++ b/apps/meteor/app/models/server/raw/Invites.ts @@ -27,4 +27,12 @@ export class InvitesRaw extends BaseRaw { }, ); } + + async countUses(): Promise { + const [result] = await this.col + .aggregate<{ totalUses: number } | undefined>([{ $group: { _id: null, totalUses: { $sum: '$uses' } } }]) + .toArray(); + + return result?.totalUses || 0; + } } diff --git a/apps/meteor/app/models/server/raw/Messages.js b/apps/meteor/app/models/server/raw/Messages.js index ce0343ded065..7f84cac25420 100644 --- a/apps/meteor/app/models/server/raw/Messages.js +++ b/apps/meteor/app/models/server/raw/Messages.js @@ -188,4 +188,46 @@ export class MessagesRaw extends BaseRaw { options, ); } + + async countRoomsWithStarredMessages(options) { + const [queryResult] = await this.col + .aggregate( + [{ $match: { 'starred._id': { $exists: true } } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], + options, + ) + .toArray(); + + return queryResult?.total || 0; + } + + async countRoomsWithPinnedMessages(options) { + const [queryResult] = await this.col + .aggregate([{ $match: { pinned: true } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], options) + .toArray(); + + return queryResult?.total || 0; + } + + async countE2EEMessages(options) { + return this.find({ t: 'e2e' }, options).count(); + } + + findPinned(options) { + const query = { + t: { $ne: 'rm' }, + _hidden: { $ne: true }, + pinned: true, + }; + + return this.find(query, options); + } + + findStarred(options) { + const query = { + '_hidden': { $ne: true }, + 'starred._id': { $exists: true }, + }; + + return this.find(query, options); + } } diff --git a/apps/meteor/app/models/server/raw/Rooms.js b/apps/meteor/app/models/server/raw/Rooms.js index a7e338db5f68..1f147c87966f 100644 --- a/apps/meteor/app/models/server/raw/Rooms.js +++ b/apps/meteor/app/models/server/raw/Rooms.js @@ -460,4 +460,21 @@ export class RoomsRaw extends BaseRaw { }, ]); } + + findByE2E(options) { + return this.find( + { + encrypted: true, + }, + options, + ); + } + + findRoomsInsideTeams(autoJoin = false) { + return this.find({ + teamId: { $exists: true }, + teamMain: { $exists: false }, + ...(autoJoin && { teamDefault: true }), + }); + } } diff --git a/apps/meteor/app/models/server/raw/Users.js b/apps/meteor/app/models/server/raw/Users.js index 29dfd1c3a49f..c6b4f0809d22 100644 --- a/apps/meteor/app/models/server/raw/Users.js +++ b/apps/meteor/app/models/server/raw/Users.js @@ -999,4 +999,20 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } + + findActiveUsersTOTPEnable(options) { + const query = { + 'active': true, + 'services.totp.enabled': true, + }; + return this.find(query, options); + } + + findActiveUsersEmail2faEnable(options) { + const query = { + 'active': true, + 'services.email2fa.enabled': true, + }; + return this.find(query, options); + } } diff --git a/apps/meteor/app/settings/server/startup.ts b/apps/meteor/app/settings/server/startup.ts index 191f831b4513..41d748578e60 100644 --- a/apps/meteor/app/settings/server/startup.ts +++ b/apps/meteor/app/settings/server/startup.ts @@ -5,9 +5,6 @@ import { ICachedSettings } from './CachedSettings'; export function initializeSettings({ SettingsModel, settings }: { SettingsModel: Settings; settings: ICachedSettings }): void { SettingsModel.find().forEach((record: ISetting) => { - if (record._id.startsWith('Prometheus')) { - console.log('store cache', record); - } settings.set(record); }); diff --git a/apps/meteor/app/spotify/lib/spotify.js b/apps/meteor/app/spotify/lib/spotify.js deleted file mode 100644 index 9ff6aa3037dc..000000000000 --- a/apps/meteor/app/spotify/lib/spotify.js +++ /dev/null @@ -1,38 +0,0 @@ -const process = (message, source, callback) => { - if (!source?.trim()) { - return; - } - - const msgParts = source.split(/(```\w*[\n ]?[\s\S]*?```+?)|(`(?:[^`]+)`)/); - for (let index = 0; index < msgParts.length; index++) { - const part = msgParts[index]; - if (!/(?:```(\w*)[\n ]?([\s\S]*?)```+?)|(?:`(?:[^`]+)`)/.test(part)) { - callback(message, msgParts, index, part); - } - } -}; - -export const createSpotifyBeforeSaveMessageHandler = () => (message) => { - const urls = Array.isArray(message.urls) ? message.urls : []; - - let changed = false; - - process(message, message.msg, (message, msgParts, index, part) => { - const re = /(?:^|\s)spotify:([^:\s]+):([^:\s]+)(?::([^:\s]+))?(?::(\S+))?(?:\s|$)/g; - - let match; - while ((match = re.exec(part)) != null) { - const data = match.slice(1).filter(Boolean); - const path = data.map((value) => encodeURI(value)).join('/'); - const url = `https://open.spotify.com/${path}`; - urls.push({ url, source: `spotify:${data.join(':')}` }); - changed = true; - } - }); - - if (changed) { - message.urls = urls; - } - - return message; -}; diff --git a/apps/meteor/app/spotify/lib/spotify.ts b/apps/meteor/app/spotify/lib/spotify.ts new file mode 100644 index 000000000000..718be19dcb90 --- /dev/null +++ b/apps/meteor/app/spotify/lib/spotify.ts @@ -0,0 +1,46 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +const process = ( + message: IMessage, + source: string, + callback: (msg: IMessage, msgParts: string[], index: number, part: string) => void, +): void => { + if (!source?.trim()) { + return; + } + + const msgParts = source.split(/(```\w*[\n ]?[\s\S]*?```+?)|(`(?:[^`]+)`)/); + for (let index = 0; index < msgParts.length; index++) { + const part = msgParts[index]; + if (!/(?:```(\w*)[\n ]?([\s\S]*?)```+?)|(?:`(?:[^`]+)`)/.test(part)) { + callback(message, msgParts, index, part); + } + } +}; + +export const createSpotifyBeforeSaveMessageHandler = + (): ((msg: IMessage) => IMessage) => + (message: IMessage): IMessage => { + const urls = Array.isArray(message.urls) ? message.urls : []; + + let changed = false; + + process(message, message.msg, (_message: IMessage, _msgParts: string[], _index: number, part: string) => { + const re = /(?:^|\s)spotify:([^:\s]+):([^:\s]+)(?::([^:\s]+))?(?::(\S+))?(?:\s|$)/g; + + let match; + while ((match = re.exec(part)) != null) { + const data = match.slice(1).filter(Boolean); + const path = data.map((value) => encodeURI(value)).join('/'); + const url = `https://open.spotify.com/${path}`; + urls.push({ url, source: `spotify:${data.join(':')}`, meta: {} }); + changed = true; + } + }); + + if (changed) { + message.urls = urls; + } + + return message; + }; diff --git a/apps/meteor/app/spotify/server/index.js b/apps/meteor/app/spotify/server/index.ts similarity index 100% rename from apps/meteor/app/spotify/server/index.js rename to apps/meteor/app/spotify/server/index.ts diff --git a/apps/meteor/app/statistics/server/lib/getImporterStatistics.ts b/apps/meteor/app/statistics/server/lib/getImporterStatistics.ts new file mode 100644 index 000000000000..0f8fc5beccc5 --- /dev/null +++ b/apps/meteor/app/statistics/server/lib/getImporterStatistics.ts @@ -0,0 +1,10 @@ +import { settings } from '../../../settings/server'; + +export function getImporterStatistics(): Record { + return { + totalCSVImportedUsers: settings.get('CSV_Importer_Count'), + totalHipchatEnterpriseImportedUsers: settings.get('Hipchat_Enterprise_Importer_Count'), + totalSlackImportedUsers: settings.get('Slack_Importer_Count'), + totalSlackUsersImportedUsers: settings.get('Slack_Users_Importer_Count'), + }; +} diff --git a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts index b41cbb2d8385..b0aec1438019 100644 --- a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts @@ -1,6 +1,11 @@ +import { MongoInternals } from 'meteor/mongo'; + +import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; + function getCustomOAuthServices(): Record< string, { @@ -9,6 +14,8 @@ function getCustomOAuthServices(): Record< users: number; } > { + const readPreference = readSecondaryPreferred(db); + const customOauth = settings.getByRegexp(/Accounts_OAuth_Custom-[^-]+$/im); return Object.fromEntries( Object.entries(customOauth).map(([key, value]) => { @@ -18,7 +25,7 @@ function getCustomOAuthServices(): Record< { enabled: Boolean(value), mergeRoles: settings.get(`Accounts_OAuth_Custom-${name}-merge_roles`), - users: Users.countActiveUsersByService(name), + users: Users.countActiveUsersByService(name, { readPreference }), }, ]; }), @@ -26,9 +33,11 @@ function getCustomOAuthServices(): Record< } export function getServicesStatistics(): Record { + const readPreference = readSecondaryPreferred(db); + return { ldap: { - users: Users.countActiveUsersByService('ldap'), + users: Users.countActiveUsersByService('ldap', { readPreference }), enabled: settings.get('LDAP_Enable'), loginFallback: settings.get('LDAP_Login_Fallback'), encryption: settings.get('LDAP_Encryption'), @@ -53,7 +62,7 @@ export function getServicesStatistics(): Record { }, saml: { enabled: settings.get('SAML_Custom_Default'), - users: Users.countActiveUsersByService('saml'), + users: Users.countActiveUsersByService('saml', { readPreference }), signatureValidationType: settings.get('SAML_Custom_Default_signature_validation_type'), generateUsername: settings.get('SAML_Custom_Default_generate_username'), updateSubscriptionsOnLogin: settings.get('SAML_Custom_Default_channels_update'), @@ -61,66 +70,66 @@ export function getServicesStatistics(): Record { }, cas: { enabled: settings.get('CAS_enabled'), - users: Users.countActiveUsersByService('cas'), + users: Users.countActiveUsersByService('cas', { readPreference }), allowUserCreation: settings.get('CAS_Creation_User_Enabled'), alwaysSyncUserData: settings.get('CAS_Sync_User_Data_Enabled'), }, oauth: { apple: { enabled: settings.get('Accounts_OAuth_Apple'), - users: Users.countActiveUsersByService('apple'), + users: Users.countActiveUsersByService('apple', { readPreference }), }, dolphin: { enabled: settings.get('Accounts_OAuth_Dolphin'), - users: Users.countActiveUsersByService('dolphin'), + users: Users.countActiveUsersByService('dolphin', { readPreference }), }, drupal: { enabled: settings.get('Accounts_OAuth_Drupal'), - users: Users.countActiveUsersByService('drupal'), + users: Users.countActiveUsersByService('drupal', { readPreference }), }, facebook: { enabled: settings.get('Accounts_OAuth_Facebook'), - users: Users.countActiveUsersByService('facebook'), + users: Users.countActiveUsersByService('facebook', { readPreference }), }, github: { enabled: settings.get('Accounts_OAuth_Github'), - users: Users.countActiveUsersByService('github'), + users: Users.countActiveUsersByService('github', { readPreference }), }, githubEnterprise: { enabled: settings.get('Accounts_OAuth_GitHub_Enterprise'), - users: Users.countActiveUsersByService('github_enterprise'), + users: Users.countActiveUsersByService('github_enterprise', { readPreference }), }, gitlab: { enabled: settings.get('Accounts_OAuth_Gitlab'), - users: Users.countActiveUsersByService('gitlab'), + users: Users.countActiveUsersByService('gitlab', { readPreference }), }, google: { enabled: settings.get('Accounts_OAuth_Google'), - users: Users.countActiveUsersByService('google'), + users: Users.countActiveUsersByService('google', { readPreference }), }, linkedin: { enabled: settings.get('Accounts_OAuth_Linkedin'), - users: Users.countActiveUsersByService('linkedin'), + users: Users.countActiveUsersByService('linkedin', { readPreference }), }, meteor: { enabled: settings.get('Accounts_OAuth_Meteor'), - users: Users.countActiveUsersByService('meteor'), + users: Users.countActiveUsersByService('meteor', { readPreference }), }, nextcloud: { enabled: settings.get('Accounts_OAuth_Nextcloud'), - users: Users.countActiveUsersByService('nextcloud'), + users: Users.countActiveUsersByService('nextcloud', { readPreference }), }, tokenpass: { enabled: settings.get('Accounts_OAuth_Tokenpass'), - users: Users.countActiveUsersByService('tokenpass'), + users: Users.countActiveUsersByService('tokenpass', { readPreference }), }, twitter: { enabled: settings.get('Accounts_OAuth_Twitter'), - users: Users.countActiveUsersByService('twitter'), + users: Users.countActiveUsersByService('twitter', { readPreference }), }, wordpress: { enabled: settings.get('Accounts_OAuth_Wordpress'), - users: Users.countActiveUsersByService('wordpress'), + users: Users.countActiveUsersByService('wordpress', { readPreference }), }, custom: getCustomOAuthServices(), }, diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 9471d1d1c1eb..051456c55ae7 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -18,6 +18,7 @@ import { Statistics, Sessions, Integrations, + Invites, Uploads, LivechatDepartment, EmailInbox, @@ -27,9 +28,10 @@ import { } from '../../../models/server/raw'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { getAppsStatistics } from './getAppsStatistics'; +import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; -import { Analytics } from '../../../../server/sdk'; +import { Analytics, Team } from '../../../../server/sdk'; import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics'; const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; @@ -395,6 +397,7 @@ export const statistics = { statistics.apps = getAppsStatistics(); statistics.services = getServicesStatistics(); + statistics.importer = getImporterStatistics(); // If getSettingsStatistics() returns an error, save as empty object. statsPms.push( @@ -448,6 +451,12 @@ export const statistics = { }), ); + statsPms.push( + Team.getStatistics().then((result) => { + statistics.teams = result; + }), + ); + statsPms.push(Analytics.resetSeatRequestCount()); statistics.dashboardCount = settings.get('Engagement_Dashboard_Load_Count'); @@ -457,6 +466,21 @@ export const statistics = { statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); statistics.totalOTRRooms = Rooms.findByCreatedOTR().count(); statistics.totalOTR = settings.get('OTR_Count'); + statistics.totalRoomsWithStarred = await MessagesRaw.countRoomsWithStarredMessages({ readPreference }); + statistics.totalRoomsWithPinned = await MessagesRaw.countRoomsWithPinnedMessages({ readPreference }); + statistics.totalUserTOTP = await UsersRaw.findActiveUsersTOTPEnable({ readPreference }).count(); + statistics.totalUserEmail2fa = await UsersRaw.findActiveUsersEmail2faEnable({ readPreference }).count(); + statistics.totalPinned = await MessagesRaw.findPinned({ readPreference }).count(); + statistics.totalStarred = await MessagesRaw.findStarred({ readPreference }).count(); + statistics.totalLinkInvitation = await Invites.find().count(); + statistics.totalLinkInvitationUses = await Invites.countUses(); + statistics.totalEmailInvitation = settings.get('Invitation_Email_Count'); + statistics.totalE2ERooms = await RoomsRaw.findByE2E({ readPreference }).count(); + statistics.logoChange = Object.keys(settings.get('Assets_logo')).includes('url'); + statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== 'Home'; + statistics.showHomeButton = settings.get('Layout_Show_Home_Button'); + statistics.totalEncryptedMessages = await MessagesRaw.countE2EEMessages({ readPreference }); + statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count'); await Promise.all(statsPms).catch(log); diff --git a/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts b/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts index 9df566e7aca9..7410fa2deeca 100644 --- a/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts +++ b/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts @@ -5,6 +5,7 @@ import { APIClient } from '../../utils/client'; import * as TabBar from './actionButtons/tabbar'; import * as MessageAction from './actionButtons/messageAction'; import * as MessageBox from './actionButtons/messageBox'; +import * as DropdownAction from './actionButtons/dropdownAction'; let registeredButtons: Array = []; @@ -19,6 +20,9 @@ export const addButton = (button: IUIActionButton): void => { case UIActionButtonContext.MESSAGE_BOX_ACTION: MessageBox.onAdded(button); break; + case UIActionButtonContext.USER_DROPDOWN_ACTION: + DropdownAction.onAdded(button); + break; } registeredButtons.push(Object.freeze(button)); @@ -35,6 +39,9 @@ export const removeButton = (button: IUIActionButton): void => { case UIActionButtonContext.MESSAGE_BOX_ACTION: MessageBox.onRemoved(button); break; + case UIActionButtonContext.USER_DROPDOWN_ACTION: + DropdownAction.onRemoved(button); + break; } }; diff --git a/apps/meteor/app/ui-message/client/actionButtons/dropdownAction.ts b/apps/meteor/app/ui-message/client/actionButtons/dropdownAction.ts new file mode 100644 index 000000000000..f6b8ebb4a840 --- /dev/null +++ b/apps/meteor/app/ui-message/client/actionButtons/dropdownAction.ts @@ -0,0 +1,28 @@ +import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; + +import { AccountBox } from '../../../ui-utils/client/lib/AccountBox'; + +export const onAdded = async (button: IUIActionButton): Promise => { + const { appId, actionId, labelI18n, context } = button; + await AccountBox.addItem({ + ...button, + name: button.labelI18n, + appId, + actionId, + labelI18n, + context, + isAppButtonItem: true, + }); +}; +export const onRemoved = async (button: IUIActionButton): Promise => { + const { appId, actionId, labelI18n, context } = button; + AccountBox.deleteItem({ + ...button, + name: button.labelI18n, + appId, + actionId, + labelI18n, + context, + isAppButtonItem: true, + }); +}; diff --git a/apps/meteor/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts b/apps/meteor/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts index 0ad1879659f7..f693a87a2e0a 100644 --- a/apps/meteor/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts +++ b/apps/meteor/app/ui-message/client/actionButtons/lib/applyButtonFilters.ts @@ -16,15 +16,17 @@ import { import { hasAtLeastOnePermission, hasPermission, hasRole, hasAnyRole } from '../../../../authorization/client'; -export const applyAuthFilter = (button: IUIActionButton, room?: IRoom): boolean => { +export const applyAuthFilter = (button: IUIActionButton, room?: IRoom, ignoreSubscriptions = false): boolean => { const { hasAllPermissions, hasOnePermission, hasAllRoles, hasOneRole } = button.when || {}; const userId = Meteor.userId(); const hasAllPermissionsResult = hasAllPermissions ? hasPermission(hasAllPermissions) : true; const hasOnePermissionResult = hasOnePermission ? hasAtLeastOnePermission(hasOnePermission) : true; - const hasAllRolesResult = hasAllRoles ? !!userId && hasAllRoles.every((role) => hasRole(userId, role, room?._id)) : true; - const hasOneRoleResult = hasOneRole ? !!userId && hasAnyRole(userId, hasOneRole, room?._id) : true; + const hasAllRolesResult = hasAllRoles + ? !!userId && hasAllRoles.every((role) => hasRole(userId, role, room?._id, ignoreSubscriptions)) + : true; + const hasOneRoleResult = hasOneRole ? !!userId && hasAnyRole(userId, hasOneRole, room?._id, ignoreSubscriptions) : true; return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult; }; @@ -49,3 +51,7 @@ export const applyRoomFilter = (button: IUIActionButton, room: IRoom): boolean = export const applyButtonFilters = (button: IUIActionButton, room?: IRoom): boolean => { return applyAuthFilter(button, room) && (!room || applyRoomFilter(button, room)); }; + +export const applyDropdownActionButtonFilters = (button: IUIActionButton): boolean => { + return applyAuthFilter(button, undefined, true); +}; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.html b/apps/meteor/app/ui-message/client/messageBox/messageBox.html index 032dbdab8afb..2bf1ffe57a03 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.html @@ -35,7 +35,7 @@ {{#if canSend}} {{> messageBoxAudioMessage rid=rid tmid=tmid}} - + {{#if actions}} {{> icon block="rc-input__icon-svg" icon="plus"}} diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html index 2fb54e27ac55..806ea08ca380 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxAudioMessage.html @@ -11,7 +11,7 @@
{{> icon block="rc-input__icon-svg" icon="checkmark-circled"}}
-
+
{{> icon block="rc-input__icon-svg" icon="mic"}}
diff --git a/apps/meteor/app/ui-utils/client/lib/AccountBox.d.ts b/apps/meteor/app/ui-utils/client/lib/AccountBox.d.ts deleted file mode 100644 index 30790cd88564..000000000000 --- a/apps/meteor/app/ui-utils/client/lib/AccountBox.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ComponentProps } from 'react'; -import { Option } from '@rocket.chat/fuselage'; -import type { IUser } from '@rocket.chat/core-typings'; -import { TranslationKey } from '@rocket.chat/ui-contexts'; - -export declare const AccountBox: { - setStatus: (status: IUser['status'], statusText?: IUser['statusText']) => void; - getItems: () => Array<{ - condition: () => boolean; - name: TranslationKey; - icon: ComponentProps['icon']; - sideNav: string; - href: string; - }>; -}; diff --git a/apps/meteor/app/ui-utils/client/lib/AccountBox.js b/apps/meteor/app/ui-utils/client/lib/AccountBox.js deleted file mode 100644 index 8f3940ad1b7a..000000000000 --- a/apps/meteor/app/ui-utils/client/lib/AccountBox.js +++ /dev/null @@ -1,103 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Session } from 'meteor/session'; -import _ from 'underscore'; - -import { appLayout } from '../../../../client/lib/appLayout'; -import { SideNav } from './SideNav'; - -export const AccountBox = (function () { - let status = 0; - const items = new ReactiveVar([]); - function setStatus(status, statusText) { - return Meteor.call('setUserStatus', status, statusText); - } - function open() { - if (SideNav.flexStatus()) { - SideNav.closeFlex(); - return; - } - status = 1; - } - function close() { - status = 0; - } - function toggle() { - if (status) { - return close(); - } - return open(); - } - function openFlex() { - status = 0; - } - - /* - * @param newOption: - * name: Button label - * icon: Button icon - * class: Class of the item - * permissions: Which permissions a user should have (all of them) to see this item - */ - function addItem(newItem) { - return Tracker.nonreactive(function () { - const actual = items.get(); - actual.push(newItem); - return items.set(actual); - }); - } - function checkCondition(item) { - return item.condition == null || item.condition(); - } - function getItems() { - return _.filter(items.get(), function (item) { - if (checkCondition(item)) { - return true; - } - }); - } - function addRoute(newRoute, router, wait = () => {}) { - if (router == null) { - router = FlowRouter; - } - const container = newRoute.customContainer ? 'pageCustomContainer' : 'pageContainer'; - const routeConfig = { - center: container, - pageTemplate: newRoute.pageTemplate, - }; - if (newRoute.i18nPageTitle != null) { - routeConfig.i18nPageTitle = newRoute.i18nPageTitle; - } - if (newRoute.pageTitle != null) { - routeConfig.pageTitle = newRoute.pageTitle; - } - return router.route(newRoute.path, { - name: newRoute.name, - async action() { - await wait(); - Session.set('openedRoom'); - appLayout.renderMainLayout(routeConfig); - }, - triggersEnter: [ - function () { - if (newRoute.sideNav != null) { - SideNav.setFlex(newRoute.sideNav); - SideNav.openFlex(); - } - }, - ], - }); - } - return { - setStatus, - toggle, - open, - close, - openFlex, - addRoute, - addItem, - getItems, - }; -})(); diff --git a/apps/meteor/app/ui-utils/client/lib/AccountBox.ts b/apps/meteor/app/ui-utils/client/lib/AccountBox.ts new file mode 100644 index 000000000000..dddf7636a361 --- /dev/null +++ b/apps/meteor/app/ui-utils/client/lib/AccountBox.ts @@ -0,0 +1,114 @@ +import { IUActionButtonWhen, IUIActionButton } from '@rocket.chat/apps-engine/definition/ui/IUIActionButtonDescriptor'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; +import { Meteor } from 'meteor/meteor'; +import { FlowRouter, Router } from 'meteor/kadira:flow-router'; +import { Session } from 'meteor/session'; + +import { SideNav } from './SideNav'; +import { appLayout } from '../../../../client/lib/appLayout'; +import { applyDropdownActionButtonFilters } from '../../../ui-message/client/actionButtons/lib/applyButtonFilters'; + +export interface IAccountBoxItem extends Omit { + name: string; + icon?: string; + href?: string; + sideNav?: string; + isAppButtonItem?: boolean; + subItems?: [IAccountBoxItem]; + when?: Omit; +} + +export class AccountBoxBase { + private items = new ReactiveVar([]); + + private status = 0; + + public setStatus(status: number, statusText: string): any { + return Meteor.call('setUserStatus', status, statusText); + } + + public open(): void { + if (SideNav.flexStatus()) { + SideNav.closeFlex(); + return; + } + this.status = 1; + } + + public close(): void { + this.status = 0; + } + + public toggle(): Window | null | void { + if (this.status) { + return this.close(); + } + return this.open(); + } + + public openFlex(): void { + this.status = 0; + } + + public async addItem(newItem: IAccountBoxItem): Promise { + Tracker.nonreactive(() => { + const actual = this.items.get(); + actual.push(newItem as never); + this.items.set(actual); + }); + } + + public async deleteItem(item: IAccountBoxItem): Promise { + Tracker.nonreactive(() => { + const actual = this.items.get(); + const itemIndex = actual.findIndex((actualItem: IAccountBoxItem) => actualItem.appId === item.appId); + actual.splice(itemIndex, 1); + this.items.set(actual); + }); + } + + public getItems(): IAccountBoxItem[] { + return this.items.get().filter((item: IAccountBoxItem) => applyDropdownActionButtonFilters(item)); + } + + public addRoute(newRoute: any, router: any, wait = async (): Promise => null): Router { + if (router == null) { + router = FlowRouter; + } + const container = newRoute.customContainer ? 'pageCustomContainer' : 'pageContainer'; + const routeConfig = { + center: container, + pageTemplate: newRoute.pageTemplate, + i18nPageTitle: '', + pageTitle: '', + }; + + if (newRoute.i18nPageTitle != null) { + routeConfig.i18nPageTitle = newRoute.i18nPageTitle; + } + + if (newRoute.pageTitle != null) { + routeConfig.pageTitle = newRoute.pageTitle; + } + + return router.route(newRoute.path, { + name: newRoute.name, + async action() { + await wait(); + Session.set('openedRoom', null); + appLayout.renderMainLayout(routeConfig); + }, + triggersEnter: [ + (): void => { + if (newRoute.sideNav != null) { + SideNav.setFlex(newRoute.sideNav); + SideNav.openFlex(); + } + }, + ], + }); + } +} + +export const AccountBox = new AccountBoxBase(); diff --git a/apps/meteor/app/utils/lib/getValidRoomName.js b/apps/meteor/app/utils/lib/getValidRoomName.js index 32c791c018f5..9085005fbe68 100644 --- a/apps/meteor/app/utils/lib/getValidRoomName.js +++ b/apps/meteor/app/utils/lib/getValidRoomName.js @@ -10,7 +10,7 @@ export const getValidRoomName = (displayName, rid = '', options = {}) => { let slugifiedName = displayName; if (settings.get('UI_Allow_room_names_with_special_chars')) { - const cleanName = limax(displayName); + const cleanName = limax(displayName, { maintainCase: true }); if (options.allowDuplicates !== true) { const room = Rooms.findOneByDisplayName(displayName); if (room && room._id !== rid) { diff --git a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 9f51e36b56eb..ce67c59afa30 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -1,4 +1,4 @@ -import { IRoom } from '@rocket.chat/core-typings'; +import { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -10,7 +10,7 @@ import { useFileInput } from '../../hooks/useFileInput'; import RoomAvatar from './RoomAvatar'; type RoomAvatarEditorProps = { - room: IRoom; + room: Pick; roomAvatar?: string; onChangeAvatar: (url: string | null) => void; }; diff --git a/apps/meteor/client/lib/rooms/roomCoordinator.ts b/apps/meteor/client/lib/rooms/roomCoordinator.ts index 96297baf069f..0a861577f50a 100644 --- a/apps/meteor/client/lib/rooms/roomCoordinator.ts +++ b/apps/meteor/client/lib/rooms/roomCoordinator.ts @@ -38,7 +38,7 @@ class RoomCoordinatorClient extends RoomCoordinator { getAvatarPath(_room): string { return ''; }, - getIcon(_room: Partial): string | undefined { + getIcon(_room: Partial): IRoomTypeConfig['icon'] { return this.config.icon; }, getUserStatus(_roomId: string): string | undefined { @@ -92,7 +92,7 @@ class RoomCoordinatorClient extends RoomCoordinator { openRoom(type, name, render); } - getIcon(room: Partial): string | undefined { + getIcon(room: Partial): IRoomTypeConfig['icon'] { return room?.t && this.getRoomDirectives(room.t)?.getIcon(room); } diff --git a/apps/meteor/client/sidebar/header/UserDropdown.tsx b/apps/meteor/client/sidebar/header/UserDropdown.tsx index 3d78f7f81cb8..a56c63d6abba 100644 --- a/apps/meteor/client/sidebar/header/UserDropdown.tsx +++ b/apps/meteor/client/sidebar/header/UserDropdown.tsx @@ -6,7 +6,9 @@ import { useLayout, useRoute, useLogout, useSetting, useAtLeastOnePermission, us import { FlowRouter } from 'meteor/kadira:flow-router'; import React, { ReactElement } from 'react'; +import { triggerActionButtonAction } from '../../../app/ui-message/client/ActionManager'; import { AccountBox, SideNav } from '../../../app/ui-utils/client'; +import { IAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; import { userStatus } from '../../../app/user-status/client'; import { callbacks } from '../../../lib/callbacks'; import MarkdownText from '../../components/MarkdownText'; @@ -40,7 +42,7 @@ const isDefaultStatus = (id: string): boolean => (Object.values(UserStatusEnum) const isDefaultStatusName = (_name: string, id: string): _name is UserStatusEnum => isDefaultStatus(id); const setStatus = (status: typeof userStatus.list['']): void => { - AccountBox.setStatus(status.statusType, !isDefaultStatus(status.id) ? status.name : ''); + AccountBox.setStatus(status.statusType as unknown as number, !isDefaultStatus(status.id) ? status.name : ''); callbacks.run('userStatusManuallySet', status); }; @@ -103,6 +105,8 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { const accountBoxItems = useReactiveValue(getItems); + const appBoxItems = (): IAccountBoxItem[] => accountBoxItems.filter((item) => item.isAppButtonItem); + return ( @@ -171,20 +175,55 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { <> {showAdmin && } - {accountBoxItems.map((item, i) => { + {accountBoxItems + .filter((item) => !item.isAppButtonItem) + .map((item, i) => { + const action = (): void => { + if (item.href) { + FlowRouter.go(item.href); + onClose(); + } + if (item.sideNav) { + SideNav.setFlex(item.sideNav); + SideNav.openFlex(); + onClose(); + } + }; + + return ( + + ); + })} + + )} + + {appBoxItems().length > 0 && ( + <> + + + {t('Apps')} + + {appBoxItems().map((item, key) => { const action = (): void => { - if (item.href) { - FlowRouter.go(item.href); - onClose(); - } - if (item.sideNav) { - SideNav.setFlex(item.sideNav); - SideNav.openFlex(); - onClose(); - } + triggerActionButtonAction({ + rid: '', + mid: '', + actionId: item.actionId, + appId: item.appId, + payload: { context: item.context }, + }); }; - - return ; + return ( + // We use the type assertion to any in the `label` property as i18n strings that come from apps are not known in compile time + <> +