diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index a9b285b074a8..8ecc9a96331c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -454,9 +454,6 @@ jobs: - name: yarn build run: yarn build - - name: Unit Test - run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - - name: Restore build uses: actions/download-artifact@v2 with: diff --git a/.vscode/settings.json b/.vscode/settings.json index 47310bec0703..04c7bdc33b1a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,5 @@ } ], "typescript.tsdk": "./node_modules/typescript/lib", - "cSpell.words": ["katex", "livechat", "omnichannel", "photoswipe", "tmid"] + "cSpell.words": ["katex", "listbox", "livechat", "omnichannel", "photoswipe", "searchbox", "tmid"] } diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 63e36de0e438..853b8e4ac498 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -45,7 +45,6 @@ rocketchat:livechat rocketchat:streamer rocketchat:version -konecty:multiple-instances-status konecty:user-presence dispatch:run-as-user diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 7eadce0be117..e88a81075a4e 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -60,7 +60,6 @@ jparker:crypto-md5@0.1.1 jparker:gravatar@0.5.1 jquery@3.0.0 kadira:flow-router@2.12.1 -konecty:multiple-instances-status@1.1.0 konecty:user-presence@2.6.3 launch-screen@1.3.0 littledata:synced-cron@1.5.1 diff --git a/apps/meteor/.mocharc.client.js b/apps/meteor/.mocharc.client.js index 07cde62887ca..50ef8e213933 100644 --- a/apps/meteor/.mocharc.client.js +++ b/apps/meteor/.mocharc.client.js @@ -28,6 +28,8 @@ module.exports = { './tests/setup/hoistedReact.ts', './tests/setup/cleanupTestingLibrary.ts', ], + reporter: 'dot', + timeout: 5000, exit: false, slow: 200, spec: [ diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 2cfd4a70111c..0849c4c98241 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -27,6 +27,7 @@ import './v1/import'; import './v1/ldap'; import './v1/misc'; import './v1/permissions'; +import './v1/presence'; import './v1/push'; import './v1/roles'; import './v1/rooms.js'; diff --git a/apps/meteor/app/api/server/lib/messages.ts b/apps/meteor/app/api/server/lib/messages.ts index 71f9f540c6bf..a4f28f9680f7 100644 --- a/apps/meteor/app/api/server/lib/messages.ts +++ b/apps/meteor/app/api/server/lib/messages.ts @@ -3,7 +3,6 @@ import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Rooms, Messages, Users } from '@rocket.chat/models'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { getValue } from '../../../settings/server/raw'; export async function findMentionedMessages({ uid, @@ -83,73 +82,6 @@ export async function findStarredMessages({ }; } -export async function findSnippetedMessageById({ uid, messageId }: { uid: string; messageId: string }): Promise { - if (!(await getValue('Message_AllowSnippeting'))) { - throw new Error('error-not-allowed'); - } - - if (!uid) { - throw new Error('invalid-user'); - } - - const snippet = await Messages.findOne({ _id: messageId, snippeted: true }); - - if (!snippet) { - throw new Error('invalid-message'); - } - - const room = await Rooms.findOneById(snippet.rid); - - if (!room) { - throw new Error('invalid-message'); - } - - if (!(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - - return snippet; -} - -export async function findSnippetedMessages({ - uid, - roomId, - pagination: { offset, count, sort }, -}: { - uid: string; - roomId: string; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}): Promise<{ - messages: IMessage[]; - count: number; - offset: number; - total: number; -}> { - if (!(await getValue('Message_AllowSnippeting'))) { - throw new Error('error-not-allowed'); - } - const room = await Rooms.findOneById(roomId); - - if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - - const { cursor, totalCount } = Messages.findSnippetedByRoom(roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - messages, - count: messages.length, - offset, - total, - }; -} - export async function findDiscussionsFromRoom({ uid, roomId, diff --git a/apps/meteor/app/api/server/v1/chat.js b/apps/meteor/app/api/server/v1/chat.js index 73f4b6931ddc..ab623563eee7 100644 --- a/apps/meteor/app/api/server/v1/chat.js +++ b/apps/meteor/app/api/server/v1/chat.js @@ -14,13 +14,7 @@ import Rooms from '../../../models/server/models/Rooms'; import Users from '../../../models/server/models/Users'; import Subscriptions from '../../../models/server/models/Subscriptions'; import { settings } from '../../../settings/server'; -import { - findMentionedMessages, - findStarredMessages, - findSnippetedMessageById, - findSnippetedMessages, - findDiscussionsFromRoom, -} from '../lib/messages'; +import { findMentionedMessages, findStarredMessages, findDiscussionsFromRoom } from '../lib/messages'; API.v1.addRoute( 'chat.delete', @@ -786,55 +780,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'chat.getSnippetedMessageById', - { authRequired: true }, - { - get() { - const { messageId } = this.queryParams; - - if (!messageId) { - throw new Meteor.Error('error-invalid-params', 'The required "messageId" query param is missing.'); - } - const message = Promise.await( - findSnippetedMessageById({ - uid: this.userId, - messageId, - }), - ); - return API.v1.success(message); - }, - }, -); - -API.v1.addRoute( - 'chat.getSnippetedMessages', - { authRequired: true }, - { - get() { - const { roomId } = this.queryParams; - const { sort } = this.parseJsonQuery(); - const { offset, count } = this.getPaginationItems(); - - if (!roomId) { - throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); - } - const messages = Promise.await( - findSnippetedMessages({ - uid: this.userId, - roomId, - pagination: { - offset, - count, - sort, - }, - }), - ); - return API.v1.success(messages); - }, - }, -); - API.v1.addRoute( 'chat.getDiscussions', { authRequired: true }, diff --git a/apps/meteor/app/api/server/v1/invites.ts b/apps/meteor/app/api/server/v1/invites.ts index bf4f64be32b7..5f4f484075cd 100644 --- a/apps/meteor/app/api/server/v1/invites.ts +++ b/apps/meteor/app/api/server/v1/invites.ts @@ -1,6 +1,11 @@ /* eslint-disable react-hooks/rules-of-hooks */ import type { IInvite } from '@rocket.chat/core-typings'; -import { isFindOrCreateInviteParams, isUseInviteTokenProps, isValidateInviteTokenProps } from '@rocket.chat/rest-typings'; +import { + isFindOrCreateInviteParams, + isUseInviteTokenProps, + isValidateInviteTokenProps, + isSendInvitationEmailParams, +} from '@rocket.chat/rest-typings'; import { API } from '../api'; import { findOrCreateInvite } from '../../../invites/server/functions/findOrCreateInvite'; @@ -8,6 +13,7 @@ import { removeInvite } from '../../../invites/server/functions/removeInvite'; import { listInvites } from '../../../invites/server/functions/listInvites'; import { useInviteToken } from '../../../invites/server/functions/useInviteToken'; import { validateInviteToken } from '../../../invites/server/functions/validateInviteToken'; +import { sendInvitationEmail } from '../../../invites/server/functions/sendInvitationEmail'; API.v1.addRoute( 'listInvites', @@ -82,3 +88,21 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'sendInvitationEmail', + { + authRequired: true, + validateParams: isSendInvitationEmailParams, + }, + { + async post() { + const { emails } = this.bodyParams; + try { + return API.v1.success({ success: Boolean(await sendInvitationEmail(this.userId, emails)) }); + } catch (e: any) { + return API.v1.failure({ error: e.message }); + } + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/presence.ts b/apps/meteor/app/api/server/v1/presence.ts new file mode 100644 index 000000000000..019137569f61 --- /dev/null +++ b/apps/meteor/app/api/server/v1/presence.ts @@ -0,0 +1,27 @@ +import { Presence } from '@rocket.chat/core-services'; + +import { API } from '../api'; + +API.v1.addRoute( + 'presence.getConnections', + { authRequired: true, permissionsRequired: ['manage-user-status'] }, + { + async get() { + const result = await Presence.getConnectionCount(); + + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'presence.enableBroadcast', + { authRequired: true, permissionsRequired: ['manage-user-status'], twoFactorRequired: true }, + { + async post() { + await Presence.toggleBroadcast(true); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 2c33af9297af..b8fd4479a705 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -1,12 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { check, Match } from 'meteor/check'; -import { - isRoleAddUserToRoleProps, - isRoleCreateProps, - isRoleDeleteProps, - isRoleRemoveUserFromRoleProps, - isRoleUpdateProps, -} from '@rocket.chat/rest-typings'; +import { isRoleAddUserToRoleProps, isRoleDeleteProps, isRoleRemoveUserFromRoleProps } from '@rocket.chat/rest-typings'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; import { api } from '@rocket.chat/core-services'; @@ -19,8 +13,6 @@ import { settings } from '../../../settings/server/index'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { updateRole } from '../../../../server/lib/roles/updateRole'; -import { insertRole } from '../../../../server/lib/roles/insertRole'; API.v1.addRoute( 'roles.list', @@ -58,48 +50,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'roles.create', - { authRequired: true }, - { - async post() { - if (!isRoleCreateProps(this.bodyParams)) { - throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); - } - - const userId = Meteor.userId(); - - if (!userId || !(await hasPermissionAsync(userId, 'access-permissions'))) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); - } - - const { name, scope, description, mandatory2fa } = this.bodyParams; - - if (await Roles.findOneByIdOrName(name)) { - throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); - } - - const roleData = { - description: description || '', - ...(mandatory2fa !== undefined && { mandatory2fa }), - name, - scope: scope || 'Users', - protected: false, - }; - - const options = { - broadcastUpdate: settings.get('UI_DisplayRoles'), - }; - - const role = insertRole(roleData, options); - - return API.v1.success({ - role, - }); - }, - }, -); - API.v1.addRoute( 'roles.addUserToRole', { authRequired: true }, @@ -190,42 +140,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'roles.update', - { authRequired: true }, - { - async post() { - if (!isRoleUpdateProps(this.bodyParams)) { - throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); - } - - if (!(await hasPermissionAsync(this.userId, 'access-permissions'))) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); - } - - const { roleId, name, scope, description, mandatory2fa } = this.bodyParams; - - const roleData = { - description: description || '', - ...(mandatory2fa !== undefined && { mandatory2fa }), - name, - scope: scope || 'Users', - protected: false, - }; - - const options = { - broadcastUpdate: settings.get('UI_DisplayRoles'), - }; - - const role = updateRole(roleId, roleData, options); - - return API.v1.success({ - role, - }); - }, - }, -); - API.v1.addRoute( 'roles.delete', { authRequired: true }, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index dd3b69e84989..e376554664f7 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -775,6 +775,14 @@ API.v1.addRoute( { authRequired: true }, { get() { + // if presence broadcast is disabled, return an empty array (all users are "offline") + if (settings.get('Presence_broadcast_disabled')) { + return API.v1.success({ + users: [], + full: true, + }); + } + const { from, ids } = this.queryParams; const options = { diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index b239ab7866f7..9185b567b1cd 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -1,9 +1,9 @@ import { Random } from 'meteor/random'; import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; -import type { IUserCreationOptions, IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; import { Subscriptions, Users as UsersRaw } from '@rocket.chat/models'; -import { setUserAvatar, checkUsernameAvailability, deleteUser } from '../../../lib/server/functions'; +import { setUserAvatar, checkUsernameAvailability, deleteUser, getUserCreatedByApp } from '../../../lib/server/functions'; import { Users } from '../../../models/server'; import type { AppServerOrchestrator } from '../orchestrator'; @@ -33,6 +33,24 @@ export class AppUserBridge extends UserBridge { return this.orch.getConverters()?.get('users').convertToApp(user); } + /** + * Deletes all bot or app users created by the App. + * @param appId the App's ID. + * @param type the type of the user to be deleted. + * @returns true if any user was deleted, false otherwise. + */ + protected async deleteUsersCreatedByApp(appId: string, type: UserType.APP | UserType.BOT): Promise { + this.orch.debugLog(`The App ${appId} is deleting all bot users`); + + const appUsers = await getUserCreatedByApp(appId, type); + if (appUsers.length) { + this.orch.debugLog(`The App ${appId} is deleting ${appUsers.length} users`); + await Promise.all(appUsers.map((appUser) => deleteUser(appUser._id))); + return true; + } + return false; + } + protected async create(userDescriptor: Partial, appId: string, options?: IUserCreationOptions): Promise { this.orch.debugLog(`The App ${appId} is requesting to create a new user.`); const user = this.orch.getConverters()?.get('users').convertToRocketChat(userDescriptor); @@ -46,6 +64,7 @@ export class AppUserBridge extends UserBridge { } switch (user.type) { + case 'bot': case 'app': if (!checkUsernameAvailability(user.username)) { throw new Error(`The username "${user.username}" is already being used. Rename or remove the user using it to install this App`); diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 631f6a03f7a5..1dd482a951d3 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -4,7 +4,7 @@ import { Settings } from '@rocket.chat/models'; import { API } from '../../../api/server'; import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; -import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server'; +import { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils'; import { Users } from '../../../models/server'; @@ -148,7 +148,7 @@ export class AppsRestApi { return API.v1.failure({ error: 'Invalid purchase type' }); } - const token = await getUserCloudAccessToken(this.getLoggedInUser()._id, true, 'marketplace:purchase', false); + const token = await getWorkspaceAccessTokenWithScope('marketplace:purchase'); if (!token) { return API.v1.failure({ error: 'Unauthorized' }); } @@ -160,7 +160,7 @@ export class AppsRestApi { return API.v1.success({ url: `${baseUrl}/apps/${this.queryParams.appId}/${ this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : subscribeRoute - }?workspaceId=${workspaceId}&token=${token}&seats=${seats}`, + }?workspaceId=${workspaceId}&token=${token.token}&seats=${seats}`, }); } diff --git a/apps/meteor/app/apps/server/storage/logs-storage.js b/apps/meteor/app/apps/server/storage/logs-storage.js index 005a69e70c8f..057b1c575c17 100644 --- a/apps/meteor/app/apps/server/storage/logs-storage.js +++ b/apps/meteor/app/apps/server/storage/logs-storage.js @@ -1,6 +1,6 @@ import { AppConsole } from '@rocket.chat/apps-engine/server/logging'; import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; +import { InstanceStatus } from '@rocket.chat/instance-status'; export class AppRealLogsStorage extends AppLogStorage { constructor(model) { diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 8ffb33958c26..8c1786853dee 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -31,11 +31,15 @@ export const canDeleteMessageAsync = async (uid: string, { u, rid, ts }: { u: IU if (!allowed) { return false; } - const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes'); + const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete'); - if (blockDeleteInMinutes) { - const timeElapsedForMessage = elapsedTime(ts); - return timeElapsedForMessage <= blockDeleteInMinutes; + if (!bypassBlockTimeLimit) { + const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes'); + + if (blockDeleteInMinutes) { + const timeElapsedForMessage = elapsedTime(ts); + return timeElapsedForMessage <= blockDeleteInMinutes; + } } const room = await Rooms.findOneById(rid, { fields: { ro: 1, unmuted: 1 } }); diff --git a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts index 48ee4745742c..33ca8d1ec8e0 100644 --- a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts +++ b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts @@ -152,7 +152,6 @@ export const upsertPermissions = async (): Promise => { { _id: 'view-livechat-installation', roles: ['livechat-manager', 'admin'] }, { _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] }, { _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-facebook', roles: ['livechat-manager', 'admin'] }, { _id: 'view-livechat-business-hours', roles: ['livechat-manager', 'livechat-monitor', 'admin'], @@ -210,7 +209,6 @@ export const upsertPermissions = async (): Promise => { { _id: 'manage-sounds', roles: ['admin'] }, { _id: 'access-mailer', roles: ['admin'] }, { _id: 'pin-message', roles: ['owner', 'moderator', 'admin'] }, - { _id: 'snippet-message', roles: ['owner', 'moderator', 'admin'] }, { _id: 'mobile-upload-file', roles: ['user', 'admin'] }, { _id: 'send-mail', roles: ['admin'] }, { _id: 'view-federation-data', roles: ['admin'] }, @@ -225,6 +223,7 @@ export const upsertPermissions = async (): Promise => { { _id: 'view-import-operations', roles: ['admin'] }, { _id: 'clear-oembed-cache', roles: ['admin'] }, { _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] }, ]; for await (const permission of permissions) { diff --git a/apps/meteor/app/authorization/server/index.js b/apps/meteor/app/authorization/server/index.js index b43cbeabc51d..d334474f86b7 100644 --- a/apps/meteor/app/authorization/server/index.js +++ b/apps/meteor/app/authorization/server/index.js @@ -10,7 +10,6 @@ import './methods/addUserToRole'; import './methods/deleteRole'; import './methods/removeRoleFromPermission'; import './methods/removeUserFromRole'; -import './methods/saveRole'; import './streamer/permissions'; export { diff --git a/apps/meteor/app/authorization/server/methods/saveRole.ts b/apps/meteor/app/authorization/server/methods/saveRole.ts deleted file mode 100644 index 9eb19298f7f7..000000000000 --- a/apps/meteor/app/authorization/server/methods/saveRole.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { isRoleCreateProps } from '@rocket.chat/rest-typings'; -import { Roles } from '@rocket.chat/models'; - -import { settings } from '../../../settings/server'; -import { hasPermission } from '../functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { updateRoleAsync } from '../../../../server/lib/roles/updateRole'; -import { insertRoleAsync } from '../../../../server/lib/roles/insertRole'; - -Meteor.methods({ - async 'authorization:saveRole'(roleData: Record) { - methodDeprecationLogger.warn('authorization:saveRole will be deprecated in future versions of Rocket.Chat'); - const userId = Meteor.userId(); - - if (!isRoleCreateProps(roleData)) { - throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.', { - method: 'authorization:saveRole', - }); - } - - if (!userId || !hasPermission(userId, 'access-permissions')) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { - method: 'authorization:saveRole', - action: 'Accessing_permissions', - }); - } - - const role = { - description: roleData.description || '', - ...(roleData.mandatory2fa !== undefined && { mandatory2fa: roleData.mandatory2fa }), - name: roleData.name, - scope: roleData.scope || 'Users', - protected: false, - }; - - const existingRole = await Roles.findOneByName(roleData.name, { projection: { _id: 1 } }); - const options = { - broadcastUpdate: settings.get('UI_DisplayRoles'), - }; - - if (existingRole) { - return updateRoleAsync(existingRole._id, role, options); - } - - return insertRoleAsync(role); - }, -}); diff --git a/apps/meteor/app/autolinker/client/client.js b/apps/meteor/app/autolinker/client/client.js deleted file mode 100644 index a3e6d77d3e21..000000000000 --- a/apps/meteor/app/autolinker/client/client.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import Autolinker from 'autolinker'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -export const createAutolinkerMessageRenderer = - ({ phone, ...config }) => - (message) => { - if (!message.html?.trim()) { - return message; - } - - let msgParts; - let regexTokens; - if (message.tokens && message.tokens.length) { - regexTokens = new RegExp(`(${(message.tokens || []).map(({ token }) => escapeRegExp(token))})`, 'g'); - msgParts = message.html.split(regexTokens); - } else { - msgParts = [message.html]; - } - - message.html = msgParts - .map((msgPart) => { - if (regexTokens && regexTokens.test(msgPart)) { - return msgPart; - } - - const muttableConfig = { - ...config, - phone: false, - stripTrailingSlash: false, - replaceFn: (match) => { - const token = `=!=${Random.id()}=!=`; - const tag = match.buildTag(); - - if (~match.matchedText.indexOf(Meteor.absoluteUrl())) { - tag.setAttr('target', ''); - } - - message.tokens = message.tokens ?? []; - message.tokens.push({ - token, - text: tag.toAnchorString(), - }); - return token; - }, - }; - - const autolinkerMsg = Autolinker.link(msgPart, muttableConfig); - - muttableConfig.phone = phone; - - return phone ? Autolinker.link(autolinkerMsg, muttableConfig) : autolinkerMsg; - }) - .join(''); - - return message; - }; diff --git a/apps/meteor/app/autolinker/client/index.js b/apps/meteor/app/autolinker/client/index.js deleted file mode 100644 index 4dbc40111253..000000000000 --- a/apps/meteor/app/autolinker/client/index.js +++ /dev/null @@ -1 +0,0 @@ -export { createAutolinkerMessageRenderer } from './client'; diff --git a/apps/meteor/app/autolinker/server/index.js b/apps/meteor/app/autolinker/server/index.js deleted file mode 100644 index 97097791afdc..000000000000 --- a/apps/meteor/app/autolinker/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './settings'; diff --git a/apps/meteor/app/autolinker/server/settings.ts b/apps/meteor/app/autolinker/server/settings.ts deleted file mode 100644 index 898e5b59a476..000000000000 --- a/apps/meteor/app/autolinker/server/settings.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settingsRegistry } from '../../settings/server'; - -Meteor.startup(function () { - const enableQuery = { - _id: 'AutoLinker', - value: true, - }; - - settingsRegistry.add('AutoLinker', true, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - i18nLabel: 'Enabled', - alert: 'This_is_a_deprecated_feature_alert', - }); - - settingsRegistry.add('AutoLinker_StripPrefix', false, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - i18nDescription: 'AutoLinker_StripPrefix_Description', - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); - settingsRegistry.add('AutoLinker_Urls_Scheme', true, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); - settingsRegistry.add('AutoLinker_Urls_www', true, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); - settingsRegistry.add('AutoLinker_Urls_TLD', true, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); - settingsRegistry.add('AutoLinker_UrlsRegExp', '(://|www\\.).+', { - type: 'string', - group: 'Message', - section: 'AutoLinker', - public: true, - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); - settingsRegistry.add('AutoLinker_Email', true, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); - settingsRegistry.add('AutoLinker_Phone', true, { - type: 'boolean', - group: 'Message', - section: 'AutoLinker', - public: true, - i18nDescription: 'AutoLinker_Phone_Description', - enableQuery, - alert: 'This_is_a_deprecated_feature_alert', - }); -}); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 1d926fe5bb77..9ade9beaa0ef 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -165,7 +165,7 @@ export abstract class AutoTranslate { tokenizeURLs(message: IMessage): IMessage { let count = message.tokens?.length || 0; - const schemes = settings.get('Markdown_SupportSchemesForLink')?.split(',').join('|'); + const schemes = 'http,https'; // Support ![alt text](http://image url) and [text](http://link) message.msg = message.msg.replace( diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index a30fc190546b..79db197033dc 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -30,6 +30,7 @@ type WorkspaceRegistrationData = { licenseVersion: number; enterpriseReady: boolean; setupComplete: boolean; + connectionDisable: boolean; npsEnabled: SettingValue; }; @@ -49,7 +50,7 @@ export async function buildWorkspaceRegistrationData { - if (!settings.get('Discussion_enabled')) { - return messageBox.actions.remove('Create_new', /start-discussion/); - } - messageBox.actions.add('Create_new', 'Discussion', { - id: 'start-discussion', - icon: 'discussion', - condition: () => { - const room = Rooms.findOne(Session.get('openedRoom')); - if (!room) { - return false; - } - return (hasPermission('start-discussion') || hasPermission('start-discussion-other-user')) && !isRoomFederated(room); - }, - action(data) { - imperativeModal.open({ - component: CreateDiscussion, - props: { - defaultParentRoom: data.prid || data.rid, - onClose: imperativeModal.close, - }, - }); - }, - }); - }); -}); diff --git a/apps/meteor/app/discussion/client/index.js b/apps/meteor/app/discussion/client/index.js index 25f06f1a7bb4..f45e6458ae7a 100644 --- a/apps/meteor/app/discussion/client/index.js +++ b/apps/meteor/app/discussion/client/index.js @@ -1,5 +1,4 @@ // Other UI extensions import './lib/messageTypes/discussionMessage'; import './createDiscussionMessageAction'; -import './discussionFromMessageBox'; import './tabBar'; diff --git a/apps/meteor/app/file-upload/server/lib/proxy.js b/apps/meteor/app/file-upload/server/lib/proxy.js index 53be1e285a99..23331ff3f705 100644 --- a/apps/meteor/app/file-upload/server/lib/proxy.js +++ b/apps/meteor/app/file-upload/server/lib/proxy.js @@ -4,7 +4,8 @@ import URL from 'url'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import { UploadFS } from 'meteor/jalik:ufs'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; +import { InstanceStatus } from '@rocket.chat/instance-status'; +import { InstanceStatus as InstanceStatusModel } from '@rocket.chat/models'; import { Logger } from '../../../logger'; import { isDocker } from '../../../utils'; @@ -63,7 +64,7 @@ WebApp.connectHandlers.stack.unshift({ } // Proxy to other instance - const instance = InstanceStatus.getCollection().findOne({ _id: file.instanceId }); + const instance = Promise.await(InstanceStatusModel.findOneById(file.instanceId)); if (instance == null) { res.writeHead(404); diff --git a/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts b/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts new file mode 100644 index 000000000000..ee666d4c8ba5 --- /dev/null +++ b/apps/meteor/app/invites/server/functions/sendInvitationEmail.ts @@ -0,0 +1,65 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { Settings } from '@rocket.chat/models'; + +import * as Mailer from '../../../mailer'; +import { hasPermission } from '../../../authorization/server'; +import { settings } from '../../../settings/server'; + +let html = ''; +Meteor.startup(() => { + Mailer.getTemplate('Invitation_Email', (value) => { + html = value; + }); +}); + +export const sendInvitationEmail = async (userId: string, emails: string[]) => { + check(emails, [String]); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'sendInvitationEmail', + }); + } + if (!hasPermission(userId, 'bulk-register-user')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'sendInvitationEmail', + }); + } + const validEmails = emails.filter(Mailer.checkAddressFormat); + + if (!validEmails || validEmails.length === 0) { + throw new Meteor.Error('error-email-send-failed', 'No valid email addresses', { + method: 'sendInvitationEmail', + }); + } + + const subject = settings.get('Invitation_Subject'); + + if (!subject) { + throw new Meteor.Error('error-email-send-failed', 'No subject', { + method: 'sendInvitationEmail', + }); + } + + return validEmails.filter((email) => { + try { + const mailerResult = Mailer.send({ + to: email, + from: settings.get('From_Email'), + subject, + html, + data: { + email, + }, + }); + + Settings.incrementValueById('Invitation_Email_Count'); + return mailerResult; + } catch ({ message }) { + throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { + method: 'sendInvitationEmail', + message, + }); + } + }); +}; diff --git a/apps/meteor/app/issuelinks/client/client.js b/apps/meteor/app/issuelinks/client/client.js deleted file mode 100644 index 0a149ba1dfb6..000000000000 --- a/apps/meteor/app/issuelinks/client/client.js +++ /dev/null @@ -1,14 +0,0 @@ -export const createIssueLinksMessageRenderer = - ({ template }) => - (message) => { - if (!message.html?.trim()) { - return message; - } - - message.html = message.html.replace(/(?:^|\s|\n)(#[0-9]+)\b/g, (match, issueNumber) => { - const url = template.replace('%s', issueNumber.substring(1)); - return match.replace(issueNumber, `${issueNumber}`); - }); - - return message; - }; diff --git a/apps/meteor/app/issuelinks/client/index.js b/apps/meteor/app/issuelinks/client/index.js deleted file mode 100644 index bd72fbfb085e..000000000000 --- a/apps/meteor/app/issuelinks/client/index.js +++ /dev/null @@ -1 +0,0 @@ -export { createIssueLinksMessageRenderer } from './client'; diff --git a/apps/meteor/app/issuelinks/server/index.js b/apps/meteor/app/issuelinks/server/index.js deleted file mode 100644 index 97097791afdc..000000000000 --- a/apps/meteor/app/issuelinks/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './settings'; diff --git a/apps/meteor/app/issuelinks/server/settings.ts b/apps/meteor/app/issuelinks/server/settings.ts deleted file mode 100644 index 57d07cf6fa54..000000000000 --- a/apps/meteor/app/issuelinks/server/settings.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { settingsRegistry } from '../../settings/server'; - -settingsRegistry.add('IssueLinks_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'IssueLinks_Incompatible', - group: 'Message', - section: 'Issue_Links', - public: true, - alert: 'This_is_a_deprecated_feature_alert', -}); - -settingsRegistry.add('IssueLinks_Template', '', { - type: 'string', - i18nLabel: 'IssueLinks_LinkTemplate', - i18nDescription: 'IssueLinks_LinkTemplate_Description', - group: 'Message', - section: 'Issue_Links', - public: true, - alert: 'This_is_a_deprecated_feature_alert', -}); diff --git a/apps/meteor/app/lib/server/functions/getUserCreatedByApp.ts b/apps/meteor/app/lib/server/functions/getUserCreatedByApp.ts new file mode 100644 index 000000000000..7073f9ccd541 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/getUserCreatedByApp.ts @@ -0,0 +1,13 @@ +import type { FindOptions } from 'mongodb'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import type { UserType } from '@rocket.chat/apps-engine/definition/users'; + +export async function getUserCreatedByApp( + appId: string, + type: UserType.BOT | UserType.APP, + options?: FindOptions, +): Promise> { + const users = await Users.find({ appId, type }, options).toArray(); + return users ?? []; +} diff --git a/apps/meteor/app/lib/server/functions/index.ts b/apps/meteor/app/lib/server/functions/index.ts index 614cc8ac0b00..857644be5f84 100644 --- a/apps/meteor/app/lib/server/functions/index.ts +++ b/apps/meteor/app/lib/server/functions/index.ts @@ -12,6 +12,7 @@ export { deleteRoom } from './deleteRoom'; export { deleteUser } from './deleteUser'; export { getRoomByNameOrIdWithOptionToJoin } from './getRoomByNameOrIdWithOptionToJoin'; export { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; +export { getUserCreatedByApp } from './getUserCreatedByApp'; export { generateUsernameSuggestion } from './getUsernameSuggestion'; export { insertMessage } from './insertMessage'; export { isTheLastMessage } from './isTheLastMessage'; diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 3fe64be17d9e..0dd28d080a57 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { settings } from '../../../settings/server'; import { Messages, Rooms } from '../../../models/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -34,12 +33,6 @@ export function loadMessageHistory({ fields: {}, }; - if (!settings.get('Message_ShowEditedStatus')) { - options.fields = { - editedAt: 0, - }; - } - const records = end != null ? Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes(rid, end, hiddenMessageTypes, options, showThreadMessages).fetch() diff --git a/apps/meteor/app/lib/server/index.js b/apps/meteor/app/lib/server/index.js index a67ce3fb2f63..3bd54d5808b9 100644 --- a/apps/meteor/app/lib/server/index.js +++ b/apps/meteor/app/lib/server/index.js @@ -41,7 +41,6 @@ import './methods/removeOAuthService'; import './methods/restartServer'; import './methods/saveSetting'; import './methods/saveSettings'; -import './methods/sendInvitationEmail'; import './methods/sendMessage'; import './methods/sendSMTPTestEmail'; import './methods/setAdminStatus'; diff --git a/apps/meteor/app/lib/server/lib/debug.js b/apps/meteor/app/lib/server/lib/debug.js index 649da8d9d1bf..f8215c8b9054 100644 --- a/apps/meteor/app/lib/server/lib/debug.js +++ b/apps/meteor/app/lib/server/lib/debug.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; +import { InstanceStatus } from '@rocket.chat/instance-status'; import _ from 'underscore'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 948508dd5a3a..19eae5aa5477 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -4,7 +4,6 @@ import _ from 'underscore'; import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { Subscriptions, Messages, Rooms } from '../../../models/server'; -import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -59,10 +58,6 @@ Meteor.methods({ limit: count, }; - if (!settings.get('Message_ShowEditedStatus')) { - options.fields = { editedAt: 0 }; - } - const records = _.isUndefined(oldest) ? Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes( rid, diff --git a/apps/meteor/app/lib/server/methods/sendInvitationEmail.ts b/apps/meteor/app/lib/server/methods/sendInvitationEmail.ts deleted file mode 100644 index 23f575a96c02..000000000000 --- a/apps/meteor/app/lib/server/methods/sendInvitationEmail.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import { Settings } from '@rocket.chat/models'; - -import * as Mailer from '../../../mailer'; -import { hasPermission } from '../../../authorization/server'; -import { settings } from '../../../settings/server'; - -let html = ''; -Meteor.startup(() => { - Mailer.getTemplate('Invitation_Email', (value) => { - html = value; - }); -}); - -Meteor.methods({ - async sendInvitationEmail(emails) { - check(emails, [String]); - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'sendInvitationEmail', - }); - } - if (!hasPermission(uid, 'bulk-register-user')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'sendInvitationEmail', - }); - } - const validEmails = emails.filter(Mailer.checkAddressFormat); - - if (!validEmails || validEmails.length === 0) { - throw new Meteor.Error('error-email-send-failed', 'No valid email addresses', { - method: 'sendInvitationEmail', - }); - } - - const subject = settings.get('Invitation_Subject'); - - if (!subject) { - throw new Meteor.Error('error-email-send-failed', 'No subject', { - method: 'sendInvitationEmail', - }); - } - - return validEmails.filter((email) => { - try { - const mailerResult = Mailer.send({ - to: email, - from: settings.get('From_Email'), - subject, - html, - data: { - email, - }, - }); - - Settings.incrementValueById('Invitation_Email_Count'); - return mailerResult; - } catch ({ message }) { - throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { - method: 'sendInvitationEmail', - message, - }); - } - }); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js b/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js index da8648a2bce5..7f7cce26450f 100644 --- a/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js +++ b/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js @@ -31,7 +31,7 @@ Meteor.methods({ }); } return { - message: 'Your_mail_was_sent_to_s', + message: 'Sending_your_mail_to_s', params: [user.emails[0].address], }; }, diff --git a/apps/meteor/app/lib/server/methods/updateMessage.js b/apps/meteor/app/lib/server/methods/updateMessage.js index d3c11f06e53f..ce0eb6ebdc97 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.js +++ b/apps/meteor/app/lib/server/methods/updateMessage.js @@ -49,7 +49,9 @@ Meteor.methods({ } const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'); - if (Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) { + const bypassBlockTimeLimit = hasPermission(Meteor.userId(), 'bypass-time-limit-edit-and-delete'); + + if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) { let currentTsDiff; let msgTs; diff --git a/apps/meteor/app/lib/server/startup/email.ts b/apps/meteor/app/lib/server/startup/email.ts index bac864aa52b3..d45af06306d4 100644 --- a/apps/meteor/app/lib/server/startup/email.ts +++ b/apps/meteor/app/lib/server/startup/email.ts @@ -396,6 +396,13 @@ settingsRegistry.addGroup('Email', function () { return this.add('SMTP_Test_Button', 'sendSMTPTestEmail', { type: 'action', actionText: 'Send_a_test_mail_to_my_user', + enableQuery: { + _id: 'SMTP_Host', + value: { + $exists: true, + $ne: '', + }, + }, }); }); diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 4e45541acdce..9d371b73d0b0 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -82,10 +82,6 @@ settingsRegistry.addGroup('Accounts', function () { type: 'int', public: true, }); - this.add('Accounts_ShowFormLogin', true, { - type: 'boolean', - public: true, - }); this.add('Accounts_EmailOrUsernamePlaceholder', '', { type: 'string', public: true, @@ -471,26 +467,6 @@ settingsRegistry.addGroup('Accounts', function () { public: true, i18nLabel: 'Enter_Behaviour', }); - - this.add('Accounts_Default_User_Preferences_messageViewMode', 0, { - type: 'select', - values: [ - { - key: 0, - i18nLabel: 'Normal', - }, - { - key: 1, - i18nLabel: 'Cozy', - }, - { - key: 2, - i18nLabel: 'Compact', - }, - ], - public: true, - i18nLabel: 'MessageBox_view_mode', - }); this.add('Accounts_Default_User_Preferences_emailNotificationMode', 'mentions', { type: 'select', values: [ @@ -548,13 +524,6 @@ settingsRegistry.addGroup('Accounts', function () { public: true, i18nLabel: 'Notifications_Sound_Volume', }); - - this.add('Accounts_Default_User_Preferences_useLegacyMessageTemplate', false, { - type: 'boolean', - public: true, - i18nLabel: 'Use_Legacy_Message_Template', - alert: 'This_is_a_deprecated_feature_alert', - }); }); this.section('Avatar', function () { @@ -1159,13 +1128,6 @@ settingsRegistry.addGroup('General', function () { settingsRegistry.addGroup('Message', function () { this.section('Message_Attachments', function () { - this.add('Message_Attachments_GroupAttach', false, { - type: 'boolean', - public: true, - i18nDescription: 'Message_Attachments_GroupAttachDescription', - alert: 'This_is_a_deprecated_feature_alert', - }); - this.add('Message_Attachments_Thumbnails_Enabled', true, { type: 'boolean', public: true, @@ -1240,11 +1202,6 @@ settingsRegistry.addGroup('Message', function () { this.add('Message_AlwaysSearchRegExp', false, { type: 'boolean', }); - this.add('Message_ShowEditedStatus', true, { - type: 'boolean', - public: true, - alert: 'This_is_a_deprecated_feature_alert', - }); this.add('Message_ShowDeletedStatus', false, { type: 'boolean', public: true, @@ -1277,14 +1234,6 @@ settingsRegistry.addGroup('Message', function () { type: 'boolean', public: true, }); - /** - * @deprecated - */ - this.add('Message_ShowFormattingTips', true, { - type: 'boolean', - public: true, - alert: 'This_is_a_deprecated_feature_alert', - }); this.add('Message_GroupingPeriod', 300, { type: 'int', public: true, @@ -1312,13 +1261,6 @@ settingsRegistry.addGroup('Message', function () { i18nLabel: 'clear_cache_now', }); // TODO: deprecate this setting in favor of App - this.add('API_EmbedDisabledFor', '', { - type: 'string', - public: true, - i18nDescription: 'API_EmbedDisabledFor_Description', - alert: 'This_is_a_deprecated_feature_alert', - }); - // TODO: deprecate this setting in favor of App this.add('API_EmbedIgnoredHosts', 'localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16', { type: 'string', i18nDescription: 'API_EmbedIgnoredHosts_Description', @@ -1573,6 +1515,46 @@ settingsRegistry.addGroup('Push', function () { }); settingsRegistry.addGroup('Layout', function () { + this.section('Login', function () { + this.add('Layout_Login_Hide_Logo', false, { + type: 'boolean', + public: true, + enterprise: true, + invalidValue: false, + }); + this.add('Layout_Login_Hide_Title', false, { + type: 'boolean', + public: true, + enterprise: true, + invalidValue: false, + }); + this.add('Layout_Login_Hide_Powered_By', false, { + type: 'boolean', + public: true, + enterprise: true, + invalidValue: false, + }); + this.add('Layout_Login_Template', 'horizontal-template', { + type: 'select', + values: [ + { + key: 'vertical-template', + i18nLabel: 'Layout_Login_Template_Vertical', + }, + { + key: 'horizontal-template', + i18nLabel: 'Layout_Login_Template_Horizontal', + }, + ], + public: true, + enterprise: true, + invalidValue: 'horizontal-template', + }); + this.add('Accounts_ShowFormLogin', true, { + type: 'boolean', + public: true, + }); + }); this.section('Content', function () { this.add('Layout_Home_Title', 'Home', { type: 'string', @@ -3204,10 +3186,20 @@ settingsRegistry.addGroup('Troubleshoot', function () { type: 'boolean', alert: 'Troubleshoot_Disable_Notifications_Alert', }); + + // this settings will let clients know in case presence has been disabled + this.add('Presence_broadcast_disabled', false, { + type: 'boolean', + public: true, + blocked: true, + }); + this.add('Troubleshoot_Disable_Presence_Broadcast', false, { type: 'boolean', alert: 'Troubleshoot_Disable_Presence_Broadcast_Alert', + enableQuery: { _id: 'Presence_broadcast_disabled', value: false }, }); + this.add('Troubleshoot_Disable_Instance_Broadcast', false, { type: 'boolean', alert: 'Troubleshoot_Disable_Instance_Broadcast_Alert', diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 3a76c7ce06fa..cc32d2909f21 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -12,6 +12,7 @@ import { findDepartmentsBetweenIds, findDepartmentAgents, } from '../../../server/api/lib/departments'; +import { DepartmentHelper } from '../../../server/lib/Departments'; API.v1.addRoute( 'livechat/department', @@ -133,15 +134,14 @@ API.v1.addRoute( return API.v1.failure(); }, - delete() { + async delete() { check(this.urlParams, { _id: String, }); - if (Livechat.removeDepartment(this.urlParams._id)) { - return API.v1.success(); - } - return API.v1.failure(); + await DepartmentHelper.removeDepartment(this.urlParams._id); + + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/imports/server/rest/facebook.ts b/apps/meteor/app/livechat/imports/server/rest/facebook.ts deleted file mode 100644 index 2e03507fa37c..000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/facebook.ts +++ /dev/null @@ -1,119 +0,0 @@ -import crypto from 'crypto'; - -import { isPOSTLivechatFacebookParams } from '@rocket.chat/rest-typings'; -import { Random } from 'meteor/random'; -import { LivechatVisitors } from '@rocket.chat/models'; -import type { ILivechatVisitor } from '@rocket.chat/core-typings'; - -import { API } from '../../../../api/server'; -import { LivechatRooms } from '../../../../models/server'; -import { settings } from '../../../../settings/server'; -import { Livechat } from '../../../server/lib/Livechat'; - -type SentMessage = { - message: { - _id: string; - rid?: string; - token?: string; - msg?: string; - }; - roomInfo: { - facebook: { - page: string; - }; - }; - guest?: ILivechatVisitor | null; -}; - -/** - * @api {post} /livechat/facebook Send Facebook message - * @apiName Facebook - * @apiGroup Livechat - * - * @apiParam {String} mid Facebook message id - * @apiParam {String} page Facebook pages id - * @apiParam {String} token Facebook user's token - * @apiParam {String} first_name Facebook user's first name - * @apiParam {String} last_name Facebook user's last name - * @apiParam {String} [text] Facebook message text - * @apiParam {String} [attachments] Facebook message attachments - */ -API.v1.addRoute( - 'livechat/facebook', - { validateParams: isPOSTLivechatFacebookParams }, - { - async post() { - if (!this.bodyParams.text && !this.bodyParams.attachments) { - return API.v1.failure('Invalid request'); - } - - if (!this.request.headers['x-hub-signature']) { - return API.v1.unauthorized(); - } - - if (!settings.get('Livechat_Facebook_Enabled')) { - return API.v1.failure('Facebook integration is disabled'); - } - - // validate if request come from omni - const signature = crypto - .createHmac('sha1', settings.get('Livechat_Facebook_API_Secret')) - .update(JSON.stringify(this.request.body)) - .digest('hex'); - if (this.request.headers['x-hub-signature'] !== `sha1=${signature}`) { - return API.v1.unauthorized(); - } - - const sendMessage: SentMessage = { - message: { - _id: this.bodyParams.mid, - msg: this.bodyParams.text, - }, - roomInfo: { - facebook: { - page: this.bodyParams.page, - }, - }, - }; - let visitor = await LivechatVisitors.getVisitorByToken(this.bodyParams.token, {}); - if (visitor) { - const rooms = LivechatRooms.findOpenByVisitorToken(visitor.token).fetch(); - if (rooms && rooms.length > 0) { - sendMessage.message.rid = rooms[0]._id; - } else { - sendMessage.message.rid = Random.id(); - } - sendMessage.message.token = visitor.token; - } else { - sendMessage.message.rid = Random.id(); - sendMessage.message.token = this.bodyParams.token; - - const userId = await Livechat.registerGuest({ - token: sendMessage.message.token, - name: `${this.bodyParams.first_name} ${this.bodyParams.last_name}`, - // TODO: type livechat big file :( - id: undefined, - email: undefined, - phone: undefined, - department: undefined, - username: undefined, - connectionData: undefined, - }); - - visitor = await LivechatVisitors.findOneById(userId); - } - - sendMessage.guest = visitor; - - try { - return API.v1.success({ - // @ts-expect-error - Typings on Livechat.sendMessage are wrong - message: await Livechat.sendMessage(sendMessage), - }); - } catch (err) { - Livechat.logger.error({ msg: 'Error using Facebook ->', err }); - return API.v1.failure(err); - } - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api.ts b/apps/meteor/app/livechat/server/api.ts index eaf38ff18ebd..865b4308b941 100644 --- a/apps/meteor/app/livechat/server/api.ts +++ b/apps/meteor/app/livechat/server/api.ts @@ -1,6 +1,5 @@ import '../imports/server/rest/agent'; import '../imports/server/rest/departments'; -import '../imports/server/rest/facebook'; import '../imports/server/rest/sms.js'; import '../imports/server/rest/users'; import '../imports/server/rest/upload'; diff --git a/apps/meteor/app/livechat/server/config.ts b/apps/meteor/app/livechat/server/config.ts index 6fedc78bb649..a815b0e21068 100644 --- a/apps/meteor/app/livechat/server/config.ts +++ b/apps/meteor/app/livechat/server/config.ts @@ -449,29 +449,6 @@ Meteor.startup(function () { i18nLabel: 'Channel_name', }); - this.add('Livechat_Facebook_Enabled', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Facebook', - enableQuery: omnichannelEnabledQuery, - }); - - this.add('Livechat_Facebook_API_Key', '', { - type: 'string', - group: 'Omnichannel', - section: 'Facebook', - i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours', - enableQuery: omnichannelEnabledQuery, - }); - - this.add('Livechat_Facebook_API_Secret', '', { - type: 'string', - group: 'Omnichannel', - section: 'Facebook', - i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours', - enableQuery: omnichannelEnabledQuery, - }); - this.add('Livechat_Routing_Method', 'Auto_Selection', { type: 'select', group: 'Omnichannel', diff --git a/apps/meteor/app/livechat/server/hooks/sendToFacebook.js b/apps/meteor/app/livechat/server/hooks/sendToFacebook.js deleted file mode 100644 index 2b77c18e5b56..000000000000 --- a/apps/meteor/app/livechat/server/hooks/sendToFacebook.js +++ /dev/null @@ -1,49 +0,0 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; - -import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings/server'; -import OmniChannel from '../lib/OmniChannel'; -import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; - -callbacks.add( - 'afterSaveMessage', - function (message, room) { - // skips this callback if the message was edited - if (message.editedAt) { - return message; - } - - // only send the sms by SMS if it is a livechat room with SMS set to true - if (!(isOmnichannelRoom(room) && room.facebook && room.v && room.v.token)) { - return message; - } - - if (!settings.get('Livechat_Facebook_Enabled') || !settings.get('Livechat_Facebook_API_Key')) { - return message; - } - - // if the message has a token, it was sent from the visitor, so ignore it - if (message.token) { - return message; - } - - // if the message has a type means it is a special message (like the closing comment), so skips - if (message.t) { - return message; - } - - if (message.file) { - message = Promise.await(normalizeMessageFileUpload(message)); - } - - OmniChannel.reply({ - page: room.facebook.page.id, - token: room.v.token, - text: message.msg, - }); - - return message; - }, - callbacks.priority.LOW, - 'sendMessageToFacebook', -); diff --git a/apps/meteor/app/livechat/server/index.js b/apps/meteor/app/livechat/server/index.js index b8d507f74940..c64dd5b181f5 100644 --- a/apps/meteor/app/livechat/server/index.js +++ b/apps/meteor/app/livechat/server/index.js @@ -10,7 +10,6 @@ import './hooks/offlineMessage'; import './hooks/offlineMessageToChannel'; import './hooks/saveAnalyticsData'; import './hooks/sendToCRM'; -import './hooks/sendToFacebook'; import './hooks/processRoomAbandonment'; import './hooks/saveLastVisitorMessageTs'; import './hooks/markRoomNotResponded'; @@ -24,7 +23,6 @@ import './methods/changeLivechatStatus'; import './methods/closeByVisitor'; import './methods/closeRoom'; import './methods/discardTranscript'; -import './methods/facebook'; import './methods/getCustomFields'; import './methods/getAgentData'; import './methods/getAgentOverviewData'; diff --git a/apps/meteor/app/livechat/server/lib/Departments.ts b/apps/meteor/app/livechat/server/lib/Departments.ts new file mode 100644 index 000000000000..ec9e66093470 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/Departments.ts @@ -0,0 +1,56 @@ +import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { Logger } from '../../../logger/server'; + +class DepartmentHelperClass { + logger = new Logger('Omnichannel:DepartmentHelper'); + + async removeDepartment(departmentId: string) { + this.logger.debug(`Removing department: ${departmentId}`); + + const department = await LivechatDepartment.findOneById(departmentId); + if (!department) { + this.logger.debug(`Department not found: ${departmentId}`); + throw new Error('error-department-not-found'); + } + + const { _id } = department; + + const ret = await LivechatDepartment.removeById(_id); + if (ret.acknowledged !== true) { + this.logger.error(`Department record not removed: ${_id}. Result from db: ${ret}`); + throw new Error('error-failed-to-delete-department'); + } + this.logger.debug(`Department record removed: ${_id}`); + + const agentsIds: string[] = await LivechatDepartmentAgents.findAgentsByDepartmentId(department._id) + .cursor.map((agent) => agent.agentId) + .toArray(); + + this.logger.debug( + `Performing post-department-removal actions: ${_id}. Removing department agents, unsetting fallback department and removing department from rooms`, + ); + + const promiseResponses = await Promise.allSettled([ + LivechatDepartmentAgents.removeByDepartmentId(_id), + LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id), + LivechatRooms.bulkRemoveDepartmentAndUnitsFromRooms(_id), + ]); + promiseResponses.forEach((response, index) => { + if (response.status === 'rejected') { + this.logger.error(`Error while performing post-department-removal actions: ${_id}. Action No: ${index}. Error:`, response.reason); + } + }); + + this.logger.debug(`Post-department-removal actions completed: ${_id}. Notifying callbacks with department and agentsIds`); + + Meteor.defer(() => { + callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); + }); + + return ret; + } +} + +export const DepartmentHelper = new DepartmentHelperClass(); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 47ae2de29551..6d32b0c2e834 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1129,30 +1129,6 @@ export const Livechat = { return true; }, - removeDepartment(_id) { - check(_id, String); - - const department = LivechatDepartment.findOneById(_id, { fields: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - const ret = LivechatDepartment.removeById(_id); - const agentsIds = LivechatDepartmentAgents.findByDepartmentId(_id) - .fetch() - .map((agent) => agent.agentId); - LivechatDepartmentAgents.removeByDepartmentId(_id); - LivechatDepartment.unsetFallbackDepartmentByDepartmentId(_id); - if (ret) { - Meteor.defer(() => { - callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds }); - }); - } - return ret; - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; diff --git a/apps/meteor/app/livechat/server/lib/OmniChannel.js b/apps/meteor/app/livechat/server/lib/OmniChannel.js deleted file mode 100644 index 0a4651114890..000000000000 --- a/apps/meteor/app/livechat/server/lib/OmniChannel.js +++ /dev/null @@ -1,70 +0,0 @@ -import { HTTP } from 'meteor/http'; - -import { settings } from '../../../settings/server'; - -const gatewayURL = 'https://omni.rocket.chat'; - -export default { - enable() { - const result = HTTP.call('POST', `${gatewayURL}/facebook/enable`, { - headers: { - 'authorization': `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, - 'content-type': 'application/json', - }, - data: { - url: settings.get('Site_Url'), - }, - }); - return result.data; - }, - - disable() { - const result = HTTP.call('DELETE', `${gatewayURL}/facebook/enable`, { - headers: { - 'authorization': `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, - 'content-type': 'application/json', - }, - }); - return result.data; - }, - - listPages() { - const result = HTTP.call('GET', `${gatewayURL}/facebook/pages`, { - headers: { - authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, - }, - }); - return result.data; - }, - - subscribe(pageId) { - const result = HTTP.call('POST', `${gatewayURL}/facebook/page/${pageId}/subscribe`, { - headers: { - authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, - }, - }); - return result.data; - }, - - unsubscribe(pageId) { - const result = HTTP.call('DELETE', `${gatewayURL}/facebook/page/${pageId}/subscribe`, { - headers: { - authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, - }, - }); - return result.data; - }, - - reply({ page, token, text }) { - return HTTP.call('POST', `${gatewayURL}/facebook/reply`, { - headers: { - authorization: `Bearer ${settings.get('Livechat_Facebook_API_Key')}`, - }, - data: { - page, - token, - text, - }, - }); - }, -}; diff --git a/apps/meteor/app/livechat/server/methods/facebook.js b/apps/meteor/app/livechat/server/methods/facebook.js deleted file mode 100644 index 66131e4c5c74..000000000000 --- a/apps/meteor/app/livechat/server/methods/facebook.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Settings } from '@rocket.chat/models'; - -import { hasPermission } from '../../../authorization'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { settings } from '../../../settings/server'; -import OmniChannel from '../lib/OmniChannel'; - -Meteor.methods({ - 'livechat:facebook'(options) { - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:addAgent' }); - } - - try { - switch (options.action) { - case 'initialState': { - return { - enabled: settings.get('Livechat_Facebook_Enabled'), - hasToken: !!settings.get('Livechat_Facebook_API_Key'), - }; - } - - case 'enable': { - const result = OmniChannel.enable(); - - if (!result.success) { - return result; - } - - return Settings.updateValueById('Livechat_Facebook_Enabled', true); - } - - case 'disable': { - OmniChannel.disable(); - - return Settings.updateValueById('Livechat_Facebook_Enabled', false); - } - - case 'list-pages': { - return OmniChannel.listPages(); - } - - case 'subscribe': { - return OmniChannel.subscribe(options.page); - } - - case 'unsubscribe': { - return OmniChannel.unsubscribe(options.page); - } - } - } catch (err) { - if (err.response && err.response.data && err.response.data.error) { - if (err.response.data.error.error) { - throw new Meteor.Error(err.response.data.error.error, err.response.data.error.message); - } - if (err.response.data.error.response) { - throw new Meteor.Error('integration-error', err.response.data.error.response.error.message); - } - if (err.response.data.error.message) { - throw new Meteor.Error('integration-error', err.response.data.error.message); - } - } - SystemLogger.error({ msg: 'Error contacting omni.rocket.chat:', err }); - throw new Meteor.Error('integration-error', err.error); - } - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/removeDepartment.js b/apps/meteor/app/livechat/server/methods/removeDepartment.js index 226fb1153376..c4d64ee25c12 100644 --- a/apps/meteor/app/livechat/server/methods/removeDepartment.js +++ b/apps/meteor/app/livechat/server/methods/removeDepartment.js @@ -1,19 +1,22 @@ import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { Livechat } from '../lib/Livechat'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { DepartmentHelper } from '../lib/Departments'; Meteor.methods({ 'livechat:removeDepartment'(_id) { methodDeprecationLogger.warn('livechat:removeDepartment will be deprecated in future versions of Rocket.Chat'); + check(_id, String); + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-departments')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeDepartment', }); } - return Livechat.removeDepartment(_id); + return DepartmentHelper.removeDepartment(_id); }, }); diff --git a/apps/meteor/app/markdown/lib/markdown.js b/apps/meteor/app/markdown/lib/markdown.js index 7ee511d403e4..8f55cf17635c 100644 --- a/apps/meteor/app/markdown/lib/markdown.js +++ b/apps/meteor/app/markdown/lib/markdown.js @@ -5,15 +5,12 @@ import { Meteor } from 'meteor/meteor'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { marked } from './parser/marked/marked'; import { original } from './parser/original/original'; import { filtered } from './parser/filtered/filtered'; import { code } from './parser/original/code'; -import { settings } from '../../settings'; const parsers = { original, - marked, filtered, }; @@ -33,29 +30,11 @@ class MarkdownClass { } parseMessageNotEscaped(message) { - const parser = settings.get('Markdown_Parser'); - - if (parser === 'disabled') { - return message; - } - const options = { - supportSchemesForLink: settings.get('Markdown_SupportSchemesForLink'), - headers: settings.get('Markdown_Headers'), rootUrl: Meteor.absoluteUrl(), - marked: { - gfm: settings.get('Markdown_Marked_GFM'), - tables: settings.get('Markdown_Marked_Tables'), - breaks: settings.get('Markdown_Marked_Breaks'), - pedantic: settings.get('Markdown_Marked_Pedantic'), - smartLists: settings.get('Markdown_Marked_SmartLists'), - smartypants: settings.get('Markdown_Marked_Smartypants'), - }, }; - const parse = typeof parsers[parser] === 'function' ? parsers[parser] : parsers.original; - - return parse(message, options); + return parsers.original(message, options); } mountTokensBackRecursively(message, tokenList, useHtml = true) { @@ -91,9 +70,7 @@ class MarkdownClass { } filterMarkdownFromMessage(message) { - return parsers.filtered(message, { - supportSchemesForLink: settings.get('Markdown_SupportSchemesForLink'), - }); + return parsers.filtered(message); } } @@ -101,20 +78,14 @@ export const Markdown = new MarkdownClass(); export const filterMarkdown = (message) => Markdown.filterMarkdownFromMessage(message); -export const createMarkdownMessageRenderer = ({ parser, ...options }) => { - if (!parser || parser === 'disabled') { - return (message) => message; - } - - const parse = typeof parsers[parser] === 'function' ? parsers[parser] : parsers.original; - +export const createMarkdownMessageRenderer = ({ ...options }) => { return (message) => { if (!message?.html?.trim()) { return message; } - return parse(message, options); + return parsers.original(message, options); }; }; -export const createMarkdownNotificationRenderer = (options) => (message) => parsers.filtered(message, options); +export const createMarkdownNotificationRenderer = () => (message) => parsers.filtered(message); diff --git a/apps/meteor/app/markdown/lib/parser/marked/marked.js b/apps/meteor/app/markdown/lib/parser/marked/marked.js deleted file mode 100644 index a1aff30618c4..000000000000 --- a/apps/meteor/app/markdown/lib/parser/marked/marked.js +++ /dev/null @@ -1,106 +0,0 @@ -import { Random } from 'meteor/random'; -import _ from 'underscore'; -import { marked as _marked } from 'marked'; -import createDOMPurify from 'dompurify'; -import { unescapeHTML, escapeHTML } from '@rocket.chat/string-helpers'; - -import hljs, { register } from '../../hljs'; -import { getGlobalWindow } from '../../getGlobalWindow'; - -const renderer = new _marked.Renderer(); - -let msg = null; - -renderer.code = function (code, lang, escaped) { - if (this.options.highlight) { - const out = this.options.highlight(code, lang); - if (out != null && out !== code) { - escaped = true; - code = out; - } - } - - let text = null; - - if (!lang) { - text = `
${escaped ? code : escapeHTML(code)}
`; - } else { - text = `
${escaped ? code : escapeHTML(code)}
`; - } - - if (_.isString(msg)) { - return text; - } - - const token = `=!=${Random.id()}=!=`; - msg.tokens.push({ - highlight: true, - token, - text, - }); - - return token; -}; - -renderer.codespan = function (text) { - text = `${text}`; - if (_.isString(msg)) { - return text; - } - - const token = `=!=${Random.id()}=!=`; - msg.tokens.push({ - token, - text, - }); - - return token; -}; - -renderer.blockquote = function (quote) { - return `
${quote}
`; -}; - -const linkRenderer = renderer.link; -renderer.link = function (href, title, text) { - const html = linkRenderer.call(renderer, href, title, text); - return html.replace(/^ { - msg = message; - - if (!message.tokens) { - message.tokens = []; - } - - message.html = _marked.parse(unescapeHTML(message.html), { - gfm, - tables, - breaks, - pedantic, - smartLists, - smartypants, - renderer, - highlight, - }); - - const window = getGlobalWindow(); - const DomPurify = createDOMPurify(window); - message.html = DomPurify.sanitize(message.html, { ADD_ATTR: ['target'], FORBID_ATTR: ['style'], FORBID_TAGS: ['style'] }); - - return message; -}; diff --git a/apps/meteor/app/markdown/server/index.js b/apps/meteor/app/markdown/server/index.js index 9b4f3084c58b..8cf4e3078e48 100644 --- a/apps/meteor/app/markdown/server/index.js +++ b/apps/meteor/app/markdown/server/index.js @@ -2,35 +2,20 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { callbacks } from '../../../lib/callbacks'; -import { settings } from '../../settings/server'; import { createMarkdownMessageRenderer, createMarkdownNotificationRenderer } from '../lib/markdown'; -import './settings'; export { Markdown } from '../lib/markdown'; Meteor.startup(() => { Tracker.autorun(() => { const options = { - parser: settings.get('Markdown_Parser'), - supportSchemesForLink: settings.get('Markdown_SupportSchemesForLink'), - headers: settings.get('Markdown_Headers'), rootUrl: Meteor.absoluteUrl(), - marked: { - gfm: settings.get('Markdown_Marked_GFM'), - tables: settings.get('Markdown_Marked_Tables'), - breaks: settings.get('Markdown_Marked_Breaks'), - pedantic: settings.get('Markdown_Marked_Pedantic'), - smartLists: settings.get('Markdown_Marked_SmartLists'), - smartypants: settings.get('Markdown_Marked_Smartypants'), - }, }; const renderMessage = createMarkdownMessageRenderer(options); callbacks.add('renderMessage', renderMessage, callbacks.priority.HIGH, 'markdown'); }); - const renderNotification = createMarkdownNotificationRenderer({ - supportSchemesForLink: settings.get('Markdown_SupportSchemesForLink'), - }); + const renderNotification = createMarkdownNotificationRenderer(); callbacks.add('renderNotification', renderNotification, callbacks.priority.HIGH, 'filter-markdown'); }); diff --git a/apps/meteor/app/markdown/server/settings.ts b/apps/meteor/app/markdown/server/settings.ts deleted file mode 100644 index 34e878186714..000000000000 --- a/apps/meteor/app/markdown/server/settings.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { settingsRegistry } from '../../settings/server'; - -settingsRegistry.add('Markdown_Parser', 'original', { - type: 'select', - values: [ - { - key: 'disabled', - i18nLabel: 'Disabled', - }, - { - key: 'original', - i18nLabel: 'Original', - }, - { - key: 'marked', - i18nLabel: 'Marked', - }, - ], - group: 'Message', - section: 'Markdown', - public: true, - alert: 'This_is_a_deprecated_feature_alert', -}); - -const enableQueryOriginal = { _id: 'Markdown_Parser', value: 'original' }; -settingsRegistry.add('Markdown_Headers', false, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryOriginal, - alert: 'This_is_a_deprecated_feature_alert', -}); -settingsRegistry.add('Markdown_SupportSchemesForLink', 'http,https', { - type: 'string', - group: 'Message', - section: 'Markdown', - public: true, - i18nDescription: 'Markdown_SupportSchemesForLink_Description', - enableQuery: enableQueryOriginal, - alert: 'This_is_a_deprecated_feature_alert', -}); - -const enableQueryMarked = { _id: 'Markdown_Parser', value: 'marked' }; -settingsRegistry.add('Markdown_Marked_GFM', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - alert: 'This_is_a_deprecated_feature_alert', -}); -settingsRegistry.add('Markdown_Marked_Tables', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - alert: 'This_is_a_deprecated_feature_alert', -}); -settingsRegistry.add('Markdown_Marked_Breaks', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - alert: 'This_is_a_deprecated_feature_alert', -}); -settingsRegistry.add('Markdown_Marked_Pedantic', false, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - alert: 'This_is_a_deprecated_feature_alert', - enableQuery: [ - { - _id: 'Markdown_Parser', - value: 'marked', - }, - { - _id: 'Markdown_Marked_GFM', - value: false, - }, - ], -}); -settingsRegistry.add('Markdown_Marked_SmartLists', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - alert: 'This_is_a_deprecated_feature_alert', -}); -settingsRegistry.add('Markdown_Marked_Smartypants', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - alert: 'This_is_a_deprecated_feature_alert', -}); diff --git a/apps/meteor/app/message-snippet/client/index.js b/apps/meteor/app/message-snippet/client/index.js deleted file mode 100644 index d5b477773204..000000000000 --- a/apps/meteor/app/message-snippet/client/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import './messageType'; -import './snippetMessage'; -import './page/snippetPage.html'; -import './page/snippetPage'; -import './tabBar/tabBar'; -import './tabBar/views/snippetedMessages.html'; -import './tabBar/views/snippetedMessages'; -import './page/stylesheets/snippetPage.css'; diff --git a/apps/meteor/app/message-snippet/client/lib/collections.js b/apps/meteor/app/message-snippet/client/lib/collections.js deleted file mode 100644 index ff80211fc3a3..000000000000 --- a/apps/meteor/app/message-snippet/client/lib/collections.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const SnippetedMessages = new Mongo.Collection('rocketchat_snippeted_message'); diff --git a/apps/meteor/app/message-snippet/client/messageType.js b/apps/meteor/app/message-snippet/client/messageType.js deleted file mode 100644 index ccc2075553cc..000000000000 --- a/apps/meteor/app/message-snippet/client/messageType.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -import { MessageTypes } from '../../ui-utils'; - -Meteor.startup(function () { - MessageTypes.registerType({ - id: 'message_snippeted', - system: true, - message: 'Snippeted_a_message', - data(message) { - const snippetLink = `${escapeHTML( - message.snippetName, - )}`; - return { snippetLink }; - }, - }); -}); diff --git a/apps/meteor/app/message-snippet/client/page/snippetPage.html b/apps/meteor/app/message-snippet/client/page/snippetPage.html deleted file mode 100644 index 139c9ae9210a..000000000000 --- a/apps/meteor/app/message-snippet/client/page/snippetPage.html +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/apps/meteor/app/message-snippet/client/page/snippetPage.js b/apps/meteor/app/message-snippet/client/page/snippetPage.js deleted file mode 100644 index 0cd662305e7d..000000000000 --- a/apps/meteor/app/message-snippet/client/page/snippetPage.js +++ /dev/null @@ -1,44 +0,0 @@ -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; -import moment from 'moment'; - -import { settings } from '../../../settings'; -import { Markdown } from '../../../markdown/client'; -import { APIClient } from '../../../utils/client'; -import { formatTime } from '../../../../client/lib/utils/formatTime'; - -Template.snippetPage.helpers({ - snippet() { - return Template.instance().message.get(); - }, - snippetContent() { - const message = Template.instance().message.get(); - if (message === undefined) { - return null; - } - message.html = message.msg; - const markdown = Markdown.parse(message); - return markdown.tokens[0].text; - }, - date() { - const snippet = Template.instance().message.get(); - if (snippet !== undefined) { - return moment(snippet.ts).format(settings.get('Message_DateFormat')); - } - }, - time() { - const snippet = Template.instance().message.get(); - if (snippet !== undefined) { - return formatTime(snippet.ts); - } - }, -}); - -Template.snippetPage.onCreated(async function () { - const snippetId = FlowRouter.getParam('snippetId'); - this.message = new ReactiveVar({}); - - const { message } = await APIClient.get('/v1/chat.getSnippetedMessageById', { messageId: snippetId }); - this.message.set(message); -}); diff --git a/apps/meteor/app/message-snippet/client/page/stylesheets/snippetPage.css b/apps/meteor/app/message-snippet/client/page/stylesheets/snippetPage.css deleted file mode 100644 index 1b3462721efd..000000000000 --- a/apps/meteor/app/message-snippet/client/page/stylesheets/snippetPage.css +++ /dev/null @@ -1,62 +0,0 @@ -:root { - --snippet-page-left-border-size: 30px; - --snippet-page-top-border-size: 20px; - --snippet-informations-height: 40px; - --snippet-page-right-border-size: 30px; -} - -.snippet-page { - overflow-x: hidden; - overflow-y: auto; - - width: auto; - height: 100%; - padding-top: var(--snippet-page-top-border-size); - padding-right: var(--snippet-page-right-border-size); - padding-left: var(--snippet-page-left-border-size); - - & pre code, - & h1, - & span { - user-select: all; - } - - & .snippet-informations { - display: inline-block; - - width: 100%; - height: 60px; - - & .avatar { - display: block; - float: left; - clear: left; - - margin-right: 10px; - } - - & .username { - display: block; - - width: 100%; - } - - & .snippet-filename { - display: block; - - width: 100%; - - font-weight: bold; - } - } - - & .info { - color: darkgrey; - - font-style: italic; - } - - & .download-button { - float: right; - } -} diff --git a/apps/meteor/app/message-snippet/client/snippetMessage.js b/apps/meteor/app/message-snippet/client/snippetMessage.js deleted file mode 100644 index 3c8a35d6cfdc..000000000000 --- a/apps/meteor/app/message-snippet/client/snippetMessage.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; -import { ChatMessage, Subscriptions } from '../../models/client'; - -Meteor.methods({ - snippetMessage(message) { - if (typeof Meteor.userId() === 'undefined' || Meteor.userId() === null) { - return false; - } - if ( - typeof settings.get('Message_AllowSnippeting') === 'undefined' || - settings.get('Message_AllowSnippeting') === null || - settings.get('Message_AllowSnippeting') === false - ) { - return false; - } - - const subscription = Subscriptions.findOne({ 'rid': message.rid, 'u._id': Meteor.userId() }); - - if (subscription === undefined) { - return false; - } - ChatMessage.update( - { - _id: message._id, - }, - { - $set: { - snippeted: true, - }, - }, - ); - }, -}); diff --git a/apps/meteor/app/message-snippet/client/tabBar/tabBar.ts b/apps/meteor/app/message-snippet/client/tabBar/tabBar.ts deleted file mode 100644 index 6e36a47833a8..000000000000 --- a/apps/meteor/app/message-snippet/client/tabBar/tabBar.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useMemo } from 'react'; -import { useSetting } from '@rocket.chat/ui-contexts'; - -import { addAction } from '../../../../client/views/room/lib/Toolbox'; - -addAction('snippeted-messages', () => { - const snippetingEnabled = useSetting('Message_AllowSnippeting'); - return useMemo( - () => - snippetingEnabled - ? { - groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], - id: 'snippeted-messages', - title: 'snippet-message', - icon: 'code', - template: 'snippetedMessages', - order: 20, - } - : null, - [snippetingEnabled], - ); -}); diff --git a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.html b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.html deleted file mode 100644 index 2c047fbb9d34..000000000000 --- a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.html +++ /dev/null @@ -1,24 +0,0 @@ - diff --git a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js deleted file mode 100644 index 48da295d07bc..000000000000 --- a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js +++ /dev/null @@ -1,80 +0,0 @@ -import _ from 'underscore'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; -import { Mongo } from 'meteor/mongo'; - -import { createMessageContext } from '../../../../ui-utils/client/lib/messageContext'; -import { APIClient } from '../../../../utils/client'; -import { Messages } from '../../../../models/client'; -import { upsertMessageBulk } from '../../../../ui-utils/client/lib/RoomHistoryManager'; - -const LIMIT_DEFAULT = 50; - -Template.snippetedMessages.helpers({ - hasMessages() { - return Template.instance().messages.find().count(); - }, - messages() { - const instance = Template.instance(); - return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); - }, - hasMore() { - return Template.instance().hasMore.get(); - }, - messageContext: createMessageContext, -}); - -Template.snippetedMessages.onCreated(function () { - this.rid = this.data.rid; - this.hasMore = new ReactiveVar(true); - this.messages = new Mongo.Collection(null); - this.limit = new ReactiveVar(LIMIT_DEFAULT); - - this.autorun(() => { - const query = { - _hidden: { $ne: true }, - snippeted: true, - rid: this.rid, - }; - - this.cursor && this.cursor.stop(); - - this.limit.set(LIMIT_DEFAULT); - - this.cursor = Messages.find(query).observe({ - added: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - changed: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - removed: ({ _id }) => { - this.messages.remove({ _id }); - }, - }); - }); - - this.autorun(async () => { - const limit = this.limit.get(); - const { messages, total } = await APIClient.get('/v1/chat.getSnippetedMessages', { - roomId: this.rid, - count: limit, - }); - - upsertMessageBulk({ msgs: messages }, this.messages); - - this.hasMore.set(total > limit); - }); -}); - -Template.snippetedMessages.onDestroyed(function () { - this.cursor.stop(); -}); - -Template.snippetedMessages.events({ - 'scroll .js-list': _.throttle(function (e, instance) { - if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get()) { - return instance.limit.set(instance.limit.get() + 50); - } - }, 200), -}); diff --git a/apps/meteor/app/message-snippet/server/index.js b/apps/meteor/app/message-snippet/server/index.js deleted file mode 100644 index 74e7fa9524c2..000000000000 --- a/apps/meteor/app/message-snippet/server/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import './startup/settings'; -import './methods/snippetMessage'; -import './requests'; diff --git a/apps/meteor/app/message-snippet/server/methods/snippetMessage.js b/apps/meteor/app/message-snippet/server/methods/snippetMessage.js deleted file mode 100644 index 987c3102c71d..000000000000 --- a/apps/meteor/app/message-snippet/server/methods/snippetMessage.js +++ /dev/null @@ -1,55 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Subscriptions, Messages, Users, Rooms } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { callbacks } from '../../../../lib/callbacks'; -import { isTheLastMessage } from '../../../lib'; - -Meteor.methods({ - snippetMessage(message, filename) { - if (Meteor.userId() == null) { - // noinspection JSUnresolvedFunction - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'snippetMessage' }); - } - - const room = Rooms.findOne({ _id: message.rid }); - - if (typeof room === 'undefined' || room === null) { - return false; - } - - const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId(), { - fields: { _id: 1 }, - }); - if (!subscription) { - return false; - } - - const me = Users.findOneById(Meteor.userId()); - - // If we keep history of edits, insert a new message to store history information - if (settings.get('Message_KeepHistory')) { - Messages.cloneAndSaveAsHistoryById(message._id, me); - } - - message.snippeted = true; - message.snippetedAt = Date.now; - message.snippetedBy = { - _id: Meteor.userId(), - username: me.username, - }; - - message = callbacks.run('beforeSaveMessage', message); - - // Create the SnippetMessage - Messages.setSnippetedByIdAndUserId(message, filename, message.snippetedBy, message.snippeted, Date.now, filename); - if (isTheLastMessage(room, message)) { - Rooms.setLastMessageSnippeted(room._id, message, filename, message.snippetedBy, message.snippeted, Date.now, filename); - } - - Messages.createWithTypeRoomIdMessageAndUser('message_snippeted', message.rid, '', me, { - snippetId: message._id, - snippetName: filename, - }); - }, -}); diff --git a/apps/meteor/app/message-snippet/server/requests.js b/apps/meteor/app/message-snippet/server/requests.js deleted file mode 100644 index 63f74bf59a00..000000000000 --- a/apps/meteor/app/message-snippet/server/requests.js +++ /dev/null @@ -1,63 +0,0 @@ -import { WebApp } from 'meteor/webapp'; -import { Cookies } from 'meteor/ostrio:cookies'; - -import { Users, Rooms, Messages } from '../../models/server'; - -WebApp.connectHandlers.use('/snippet/download', function (req, res) { - let rawCookies; - let token; - let uid; - const cookie = new Cookies(); - - if (req.headers && req.headers.cookie !== null) { - rawCookies = req.headers.cookie; - } - - if (rawCookies !== null) { - uid = cookie.get('rc_uid', rawCookies); - } - - if (rawCookies !== null) { - token = cookie.get('rc_token', rawCookies); - } - - if (uid === null) { - uid = req.query.rc_uid; - token = req.query.rc_token; - } - - const user = Users.findOneByIdAndLoginToken(uid, token); - - if (!(uid && token && user)) { - res.writeHead(403); - res.end(); - return false; - } - const match = /^\/([^\/]+)\/(.*)/.exec(req.url); - - if (match[1]) { - const snippet = Messages.findOne({ - _id: match[1], - snippeted: true, - }); - const room = Rooms.findOne({ _id: snippet.rid, usernames: { $in: [user.username] } }); - if (room === undefined) { - res.writeHead(403); - res.end(); - return false; - } - - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(snippet.snippetName)}`); - res.setHeader('Content-Type', 'application/octet-stream'); - - // Removing the ``` contained in the msg. - const snippetContent = snippet.msg.substr(3, snippet.msg.length - 6); - res.setHeader('Content-Length', snippetContent.length); - res.write(snippetContent); - res.end(); - return; - } - - res.writeHead(404); - res.end(); -}); diff --git a/apps/meteor/app/message-snippet/server/startup/settings.ts b/apps/meteor/app/message-snippet/server/startup/settings.ts deleted file mode 100644 index eb20c713cb5c..000000000000 --- a/apps/meteor/app/message-snippet/server/startup/settings.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { settingsRegistry } from '../../../settings/server'; - -settingsRegistry.add('Message_AllowSnippeting', false, { - type: 'boolean', - public: true, - group: 'Message', - alert: 'This_is_a_deprecated_feature_alert', -}); diff --git a/apps/meteor/app/models/server/models/LivechatDepartment.js b/apps/meteor/app/models/server/models/LivechatDepartment.js index 4f8a22fa04b0..4bed4d175dfd 100644 --- a/apps/meteor/app/models/server/models/LivechatDepartment.js +++ b/apps/meteor/app/models/server/models/LivechatDepartment.js @@ -152,18 +152,6 @@ export class LivechatDepartment extends Base { return this.find(query, options); } - - unsetFallbackDepartmentByDepartmentId(_id) { - return this.update( - { fallbackForwardDepartment: _id }, - { - $unset: { - fallbackForwardDepartment: 1, - }, - }, - { multi: true }, - ); - } } export default new LivechatDepartment(); diff --git a/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js b/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js index d3e9111b33f5..5ec1494a70fd 100644 --- a/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js +++ b/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js @@ -52,10 +52,6 @@ export class LivechatDepartmentAgents extends Base { this.remove({ departmentId, agentId }); } - removeByDepartmentId(departmentId) { - this.remove({ departmentId }); - } - getNextAgentForDepartment(departmentId, ignoreAgentId, extraQuery) { const agents = this.findByDepartmentId(departmentId).fetch(); diff --git a/apps/meteor/app/models/server/models/Messages.js b/apps/meteor/app/models/server/models/Messages.js index e570c67ed779..5b02eac244d8 100644 --- a/apps/meteor/app/models/server/models/Messages.js +++ b/apps/meteor/app/models/server/models/Messages.js @@ -21,7 +21,6 @@ export class Messages extends Base { this.tryEnsureIndex({ 'file._id': 1 }, { sparse: true }); this.tryEnsureIndex({ 'mentions.username': 1 }, { sparse: true }); this.tryEnsureIndex({ pinned: 1 }, { sparse: true }); - this.tryEnsureIndex({ snippeted: 1 }, { sparse: true }); this.tryEnsureIndex({ location: '2dsphere' }); this.tryEnsureIndex({ slackTs: 1, slackBotId: 1 }, { sparse: true }); this.tryEnsureIndex({ unread: 1 }, { sparse: true }); @@ -618,30 +617,6 @@ export class Messages extends Base { return this.update(query, update); } - setSnippetedByIdAndUserId(message, snippetName, snippetedBy, snippeted, snippetedAt) { - if (snippeted == null) { - snippeted = true; - } - if (snippetedAt == null) { - snippetedAt = 0; - } - const query = { _id: message._id }; - - const msg = `\`\`\`${message.msg}\`\`\``; - - const update = { - $set: { - msg, - snippeted, - snippetedAt: snippetedAt || new Date(), - snippetedBy, - snippetName, - }, - }; - - return this.update(query, update); - } - setUrlsById(_id, urls) { const query = { _id }; diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index 2aee72624316..1314a7e65e5b 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -125,24 +125,6 @@ export class Rooms extends Base { return this.update(query, update); } - setLastMessageSnippeted(roomId, message, snippetName, snippetedBy, snippeted, snippetedAt) { - const query = { _id: roomId }; - - const msg = `\`\`\`${message.msg}\`\`\``; - - const update = { - $set: { - 'lastMessage.msg': msg, - 'lastMessage.snippeted': snippeted, - 'lastMessage.snippetedAt': snippetedAt || new Date(), - 'lastMessage.snippetedBy': snippetedBy, - 'lastMessage.snippetName': snippetName, - }, - }; - - return this.update(query, update); - } - setLastMessagePinned(roomId, pinnedBy, pinned, pinnedAt) { const query = { _id: roomId }; diff --git a/apps/meteor/app/notifications/client/lib/Presence.ts b/apps/meteor/app/notifications/client/lib/Presence.ts index 2c222d28d643..29e0709c0ac9 100644 --- a/apps/meteor/app/notifications/client/lib/Presence.ts +++ b/apps/meteor/app/notifications/client/lib/Presence.ts @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Presence, STATUS_MAP } from '../../../../client/lib/presence'; // TODO implement API on Streamer to be able to listen to all streamed data -// this is a hacky way to listen to all streamed data from user-presense Streamer +// this is a hacky way to listen to all streamed data from user-presence Streamer (Meteor as any).StreamerCentral.on('stream-user-presence', (uid: string, args: unknown) => { if (!Array.isArray(args)) { throw new Error('Presence event must be an array'); diff --git a/apps/meteor/app/search/client/provider/result.js b/apps/meteor/app/search/client/provider/result.js index 804c808ce05e..a733b862fc55 100644 --- a/apps/meteor/app/search/client/provider/result.js +++ b/apps/meteor/app/search/client/provider/result.js @@ -31,6 +31,7 @@ Meteor.startup(function () { name: Rooms.findOne({ _id: message.rid }).name, }, { + ...FlowRouter.current().queryParams, jump: message._id, }, ); diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index b563e37757a8..ba042a930bb8 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -168,8 +168,8 @@ export class CachedCollection extends Emitter<{ changed: T; re return; } - const { _id, ...data } = newRecord; - this.collection.direct.upsert({ _id } as Mongo.Selector, { $set: data } as Mongo.Modifier); + const { _id } = newRecord; + this.collection.direct.upsert({ _id } as Mongo.Selector, newRecord); this.emit('changed', newRecord as any); // TODO: investigate why this is needed if (hasUpdatedAt(newRecord) && newRecord._updatedAt > this.updatedAt) { @@ -232,8 +232,8 @@ export class CachedCollection extends Emitter<{ changed: T; re if (action === 'removed') { this.collection.remove(newRecord._id); } else { - const { _id, ...data } = newRecord; - this.collection.direct.upsert({ _id } as Mongo.Selector, { $set: data } as Mongo.Modifier); + const { _id } = newRecord; + this.collection.direct.upsert({ _id } as Mongo.Selector, newRecord); } this.save(); }); @@ -276,8 +276,8 @@ export class CachedCollection extends Emitter<{ changed: T; re const actionTime = newRecord._updatedAt; changes.push({ action: () => { - const { _id, ...data } = newRecord; - this.collection.direct.upsert({ _id } as Mongo.Selector, { $set: data } as Mongo.Modifier); + const { _id } = newRecord; + this.collection.direct.upsert({ _id } as Mongo.Selector, newRecord); if (actionTime > this.updatedAt) { this.updatedAt = actionTime; } diff --git a/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts b/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts index eb47ecfcdf9b..2ad405bcf9c5 100644 --- a/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts +++ b/apps/meteor/app/ui-message/client/actionButtons/messageBox.ts @@ -11,11 +11,9 @@ import { Utilities } from '../../../apps/lib/misc/Utilities'; const getIdForActionButton = ({ appId, actionId }: IUIActionButton): string => `${appId}/${actionId}`; -const APP_GROUP = 'Create_new'; - export const onAdded = (button: IUIActionButton): void => // eslint-disable-next-line no-void - void messageBox.actions.add(APP_GROUP, t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)) as TranslationKey, { + void messageBox.actions.add('Apps', t(Utilities.getI18nKeyForApp(button.labelI18n, button.appId)) as TranslationKey, { id: getIdForActionButton(button), // icon: button.icon || '', condition() { @@ -33,4 +31,4 @@ export const onAdded = (button: IUIActionButton): void => export const onRemoved = (button: IUIActionButton): void => // eslint-disable-next-line no-void - void messageBox.actions.remove(APP_GROUP, new RegExp(getIdForActionButton(button))); + void messageBox.actions.remove('Apps', new RegExp(getIdForActionButton(button))); diff --git a/apps/meteor/app/ui-message/client/message.html b/apps/meteor/app/ui-message/client/message.html index f1ce01f8e126..f9fbb46ed268 100644 --- a/apps/meteor/app/ui-message/client/message.html +++ b/apps/meteor/app/ui-message/client/message.html @@ -97,9 +97,6 @@ {{/if}}
- {{#if isSnippet}} -
{{_ "Snippet_name"}}: {{snippetName}}
- {{/if}} {{#if msg.blocks}}
{{> Blocks blocks=msg.blocks rid=msg.rid mid=msg._id}} diff --git a/apps/meteor/app/ui-message/client/message.js b/apps/meteor/app/ui-message/client/message.js index f0bffebb9a52..38eed20870fb 100644 --- a/apps/meteor/app/ui-message/client/message.js +++ b/apps/meteor/app/ui-message/client/message.js @@ -309,15 +309,6 @@ Template.message.helpers({ return false; } - // check if oembed is disabled for message's sender - if ( - (settings.API_EmbedDisabledFor || '') - .split(',') - .map((username) => username.trim()) - .includes(msg.u && msg.u.username) - ) { - return false; - } return true; }, reactions() { @@ -446,10 +437,6 @@ Template.message.helpers({ messageActions() { return Template.instance().actions.get(); }, - isSnippet() { - const { msg } = this; - return msg.actionContext === 'snippeted'; - }, isThreadReply() { const { groupable, diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index e1e0298f353f..33d93f40a661 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -5,7 +5,6 @@ import $ from 'jquery'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; -import './messageBoxActions'; import type { FormattingButton } from './messageBoxFormatting'; import { formattingButtons } from './messageBoxFormatting'; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index 6b22ca91a94d..703e83e3a67b 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -1,5 +1,3 @@ -import './messageBoxActions'; - const lastFocusedInput: HTMLTextAreaElement | undefined = undefined; export const refocusComposer = () => { diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts deleted file mode 100644 index d9402f550239..000000000000 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxActions.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import { isRoomFederated } from '@rocket.chat/core-typings'; - -import { messageBox } from '../../../ui-utils/client'; -import { settings } from '../../../settings/client'; -import { imperativeModal } from '../../../../client/lib/imperativeModal'; -import ShareLocationModal from '../../../../client/views/room/ShareLocation/ShareLocationModal'; -import { Rooms } from '../../../models/client'; - -messageBox.actions.add('Create_new', 'Video_message', { - id: 'video-message', - icon: 'video', - condition: () => - navigator.mediaDevices && - window.MediaRecorder && - settings.get('FileUpload_Enabled') && - settings.get('Message_VideoRecorderEnabled') && - (!settings.get('FileUpload_MediaTypeBlackList') || !settings.get('FileUpload_MediaTypeBlackList').match(/video\/webm|video\/\*/i)) && - (!settings.get('FileUpload_MediaTypeWhiteList') || settings.get('FileUpload_MediaTypeWhiteList').match(/video\/webm|video\/\*/i)) && - window.MediaRecorder.isTypeSupported('video/webm; codecs=vp8,opus'), - action: ({ chat }) => { - if (!chat?.composer?.recordingVideo.get()) { - chat?.composer?.setRecordingVideo(true); - } - }, -}); - -messageBox.actions.add('Add_files_from', 'Computer', { - id: 'file-upload', - icon: 'computer', - condition: () => settings.get('FileUpload_Enabled'), - action({ event, chat }) { - event.preventDefault(); - const $input = $(document.createElement('input')); - $input.css('display', 'none'); - $input.attr({ - id: 'fileupload-input', - type: 'file', - multiple: 'multiple', - }); - - $(document.body).append($input); - - $input.one('change', async function (e) { - const { mime } = await import('../../../utils/lib/mimeTypes'); - const filesToUpload = Array.from(e.target.files ?? []).map((file) => { - Object.defineProperty(file, 'type', { - value: mime.lookup(file.name), - }); - return file; - }); - - chat?.flows.uploadFiles(filesToUpload); - $input.remove(); - }); - - $input.click(); - - // Simple hack for iOS aka codegueira - if (navigator.userAgent.match(/(iPad|iPhone|iPod)/g)) { - $input.click(); - } - }, -}); - -const canGetGeolocation = new ReactiveVar(false); - -messageBox.actions.add('Share', 'My_location', { - id: 'share-location', - icon: 'map-pin', - condition: () => { - const room = Rooms.findOne(Session.get('openedRoom')); - if (!room) { - return false; - } - return canGetGeolocation.get() && !isRoomFederated(room); - }, - async action({ rid, tmid }) { - imperativeModal.open({ component: ShareLocationModal, props: { rid, tmid, onClose: imperativeModal.close } }); - }, -}); - -Meteor.startup(() => { - Tracker.autorun(() => { - const isMapViewEnabled = settings.get('MapView_Enabled') === true; - const isGeolocationCurrentPositionSupported = Boolean(navigator.geolocation?.getCurrentPosition); - const googleMapsApiKey = settings.get('MapView_GMapsAPIKey'); - canGetGeolocation.set(isMapViewEnabled && isGeolocationCurrentPositionSupported && googleMapsApiKey && googleMapsApiKey.length); - }); -}); diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts index 4078a1c2e6a1..b92d23e76c5b 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts @@ -2,7 +2,6 @@ import type { Icon } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; -import { Markdown } from '../../../markdown/client'; import { settings } from '../../../settings/client'; export type FormattingButton = @@ -13,13 +12,13 @@ export type FormattingButton = // text?: () => string | undefined; command?: string; link?: string; - condition: () => boolean; + condition?: () => boolean; } | { label: TranslationKey; text: () => string | undefined; link: string; - condition: () => boolean; + condition?: () => boolean; }; export const formattingButtons: ReadonlyArray = [ @@ -28,45 +27,27 @@ export const formattingButtons: ReadonlyArray = [ icon: 'bold', pattern: '*{{text}}*', command: 'b', - condition: () => Markdown && settings.get('Markdown_Parser') === 'original', - }, - { - label: 'bold', - icon: 'bold', - pattern: '**{{text}}**', - command: 'b', - condition: () => Markdown && settings.get('Markdown_Parser') === 'marked', }, { label: 'italic', icon: 'italic', pattern: '_{{text}}_', command: 'i', - condition: () => Markdown && settings.get('Markdown_Parser') !== 'disabled', }, { label: 'strike', icon: 'strike', pattern: '~{{text}}~', - condition: () => Markdown && settings.get('Markdown_Parser') === 'original', - }, - { - label: 'strike', - icon: 'strike', - pattern: '~~{{text}}~~', - condition: () => Markdown && settings.get('Markdown_Parser') === 'marked', }, { label: 'inline_code', icon: 'code', pattern: '`{{text}}`', - condition: () => Markdown && settings.get('Markdown_Parser') !== 'disabled', }, { label: 'multi_line', icon: 'multiline', pattern: '```\n{{text}}\n``` ', - condition: () => Markdown && settings.get('Markdown_Parser') !== 'disabled', }, { label: 'KaTeX' as TranslationKey, diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 960d886e5fa2..e7a40881a891 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -59,7 +59,7 @@ export async function upsertMessage( ); } - return direct.upsert({ _id }, { $set: messageToUpsert }); + return direct.upsert({ _id }, msg); } export function upsertMessageBulk( diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 86ce779378e5..94fdb37fd5ae 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -153,14 +153,16 @@ Meteor.startup(async function () { if (isRoomFederated(room)) { return message.u._id === Meteor.userId(); } - const hasPermission = hasAtLeastOnePermission('edit-message', message.rid); + const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid); const isEditAllowed = settings.Message_AllowEditing; const editOwn = message.u && message.u._id === Meteor.userId(); - if (!(hasPermission || (isEditAllowed && editOwn))) { + if (!(canEditMessage || (isEditAllowed && editOwn))) { return false; } const blockEditInMinutes = settings.Message_AllowEditing_BlockEditInMinutes; - if (blockEditInMinutes) { + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + + if (!bypassBlockTimeLimit && blockEditInMinutes) { let msgTs; if (message.ts != null) { msgTs = moment(message.ts); diff --git a/apps/meteor/app/ui-utils/client/lib/messageContext.ts b/apps/meteor/app/ui-utils/client/lib/messageContext.ts index c5ad7e0d2fc0..14f519d7d348 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageContext.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageContext.ts @@ -14,7 +14,6 @@ import type { CommonRoomTemplateInstance } from '../../../ui/client/views/app/li const fields = { 'name': 1, 'username': 1, - 'settings.preferences.useLegacyMessageTemplate': 1, 'settings.preferences.autoImageLoad': 1, 'settings.preferences.saveMobileBandwidth': 1, 'settings.preferences.collapseMediaByDefault': 1, @@ -51,7 +50,6 @@ export const createMessageContext = ({ ), translateLanguage = AutoTranslate.getLanguage(rid), autoImageLoad = getUserPreference(user, 'autoImageLoad'), - useLegacyMessageTemplate = getUserPreference(user, 'useLegacyMessageTemplate'), saveMobileBandwidth = Meteor.Device.isPhone() && getUserPreference(user, 'saveMobileBandwidth'), collapseMediaByDefault = getUserPreference(user, 'collapseMediaByDefault'), showreply = true, @@ -70,12 +68,8 @@ export const createMessageContext = ({ // eslint-disable-next-line @typescript-eslint/naming-convention Message_AllowEditing_BlockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'), // eslint-disable-next-line @typescript-eslint/naming-convention - Message_ShowEditedStatus = settings.get('Message_ShowEditedStatus'), - // eslint-disable-next-line @typescript-eslint/naming-convention API_Embed = settings.get('API_Embed'), // eslint-disable-next-line @typescript-eslint/naming-convention - API_EmbedDisabledFor = settings.get('API_EmbedDisabledFor'), - // eslint-disable-next-line @typescript-eslint/naming-convention Message_GroupingPeriod = settings.get('Message_GroupingPeriod') * 1000, }: { uid?: IUser['_id'] | null; @@ -87,7 +81,6 @@ export const createMessageContext = ({ embeddedLayout?: boolean; translateLanguage?: unknown; autoImageLoad?: unknown; - useLegacyMessageTemplate?: unknown; saveMobileBandwidth?: unknown; collapseMediaByDefault?: unknown; showreply?: unknown; @@ -100,9 +93,7 @@ export const createMessageContext = ({ AutoTranslate_Enabled?: unknown; Message_AllowEditing?: unknown; Message_AllowEditing_BlockEditInMinutes?: unknown; - Message_ShowEditedStatus?: unknown; API_Embed?: unknown; - API_EmbedDisabledFor?: unknown; Message_GroupingPeriod?: unknown; } = {}) => { return { @@ -112,7 +103,6 @@ export const createMessageContext = ({ settings: { translateLanguage, autoImageLoad, - useLegacyMessageTemplate, saveMobileBandwidth, collapseMediaByDefault, showreply, @@ -125,9 +115,7 @@ export const createMessageContext = ({ AutoTranslate_Enabled, Message_AllowEditing, Message_AllowEditing_BlockEditInMinutes, - Message_ShowEditedStatus, API_Embed, - API_EmbedDisabledFor, Message_GroupingPeriod, }, } as const; diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 1b19db534e8f..b018f83b8fc8 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -1,12 +1,11 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { UIEvent } from 'react'; -import { createUploadsAPI } from '../../../../client/lib/chats/uploads'; import { setHighlightMessage, clearHighlightMessage, } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; import type { ChatAPI, ComposerAPI, DataAPI, UploadsAPI } from '../../../../client/lib/chats/ChatAPI'; -import { createDataAPI } from '../../../../client/lib/chats/data'; import { uploadFiles } from '../../../../client/lib/chats/flows/uploadFiles'; import { processSlashCommand } from '../../../../client/lib/chats/flows/processSlashCommand'; import { requestMessageDeletion } from '../../../../client/lib/chats/flows/requestMessageDeletion'; @@ -16,8 +15,35 @@ import { processSetReaction } from '../../../../client/lib/chats/flows/processSe import { sendMessage } from '../../../../client/lib/chats/flows/sendMessage'; import { UserAction } from '..'; import { replyBroadcast } from '../../../../client/lib/chats/flows/replyBroadcast'; +import { createDataAPI } from '../../../../client/lib/chats/data'; +import { createUploadsAPI } from '../../../../client/lib/chats/uploads'; + +type DeepWritable = T extends (...args: any) => any + ? T + : { + -readonly [P in keyof T]: DeepWritable; + }; export class ChatMessages implements ChatAPI { + public composer: ComposerAPI | undefined; + + public setComposerAPI = (composer: ComposerAPI): void => { + this.composer?.release(); + this.composer = composer; + }; + + public data: DataAPI; + + public uploads: UploadsAPI; + + public userCard: { open(username: string): (event: UIEvent) => void; close(): void }; + + public action: { + start(action: 'typing'): void; + stop(action: 'typing' | 'recording' | 'uploading' | 'playing'): void; + performContinuously(action: 'recording' | 'uploading' | 'playing'): void; + }; + private currentEditingMID?: string; public messageEditing: ChatAPI['messageEditing'] = { @@ -82,17 +108,39 @@ export class ChatMessages implements ChatAPI { }, }; - public composer: ComposerAPI | undefined; + public flows: DeepWritable; - public readonly data: DataAPI; + public constructor( + private params: { + rid: IRoom['_id']; + tmid?: IMessage['_id']; + }, + ) { + const { rid, tmid } = params; + this.data = createDataAPI({ rid, tmid }); + this.uploads = createUploadsAPI({ rid, tmid }); - public readonly uploads: UploadsAPI; + const unimplemented = () => { + throw new Error('Flow is not implemented'); + }; - public readonly flows: ChatAPI['flows']; + this.userCard = { + open: unimplemented, + close: unimplemented, + }; + + this.action = { + start: async (action: 'typing') => { + UserAction.start(params.rid, `user-${action}`, { tmid: params.tmid }); + }, + performContinuously: async (action: 'recording' | 'uploading' | 'playing') => { + UserAction.performContinuously(params.rid, `user-${action}`, { tmid: params.tmid }); + }, + stop: async (action: 'typing' | 'recording' | 'uploading' | 'playing') => { + UserAction.stop(params.rid, `user-${action}`, { tmid: params.tmid }); + }, + }; - public constructor(private params: { rid: IRoom['_id']; tmid?: IMessage['_id'] }) { - this.data = createDataAPI({ rid: params.rid, tmid: params.tmid }); - this.uploads = createUploadsAPI({ rid: params.rid, tmid: params.tmid }); this.flows = { uploadFiles: uploadFiles.bind(null, this), sendMessage: sendMessage.bind(this, this), @@ -102,26 +150,9 @@ export class ChatMessages implements ChatAPI { processSetReaction: processSetReaction.bind(null, this), requestMessageDeletion: requestMessageDeletion.bind(this, this), replyBroadcast: replyBroadcast.bind(null, this), - - action: { - start: async (action: 'typing') => { - UserAction.start(params.rid, `user-${action}`, { tmid: params.tmid }); - }, - performContinuously: async (action: 'recording' | 'uploading' | 'playing') => { - UserAction.performContinuously(params.rid, `user-${action}`, { tmid: params.tmid }); - }, - stop: async (action: 'typing' | 'recording' | 'uploading' | 'playing') => { - UserAction.stop(params.rid, `user-${action}`, { tmid: params.tmid }); - }, - }, }; } - public setComposerAPI(composer: ComposerAPI): void { - this.composer?.release(); - this.composer = composer; - } - public get currentEditing() { if (!this.composer || !this.currentEditingMID) { return undefined; @@ -172,8 +203,7 @@ export class ChatMessages implements ChatAPI { }; } - private async release() { - this.composer?.release(); + public async release() { if (this.currentEditing) { if (!this.params.tmid) { await this.currentEditing.cancel(); @@ -181,35 +211,4 @@ export class ChatMessages implements ChatAPI { this.composer?.clear(); } } - - private static refs = new Map(); - - private static getID({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): string { - return `${rid}${tmid ? `-${tmid}` : ''}`; - } - - public static hold({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }) { - const id = this.getID({ rid, tmid }); - - const ref = this.refs.get(id) ?? { instance: new ChatMessages({ rid, tmid }), count: 0 }; - ref.count++; - this.refs.set(id, ref); - - return ref.instance; - } - - public static release({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }) { - const id = this.getID({ rid, tmid }); - - const ref = this.refs.get(id); - if (!ref) { - return; - } - - ref.count--; - if (ref.count === 0) { - this.refs.delete(id); - ref.instance.release(); - } - } } diff --git a/apps/meteor/app/webdav/client/index.js b/apps/meteor/app/webdav/client/index.js index 8aa0226584fd..5d110d20ce25 100644 --- a/apps/meteor/app/webdav/client/index.js +++ b/apps/meteor/app/webdav/client/index.js @@ -9,7 +9,6 @@ Meteor.startup(() => { return; } c.stop(); - import('./startup/messageBoxActions'); import('./startup/sync'); import('./actionButton'); }); diff --git a/apps/meteor/app/webdav/client/startup/messageBoxActions.js b/apps/meteor/app/webdav/client/startup/messageBoxActions.js deleted file mode 100644 index e17c1d340a99..000000000000 --- a/apps/meteor/app/webdav/client/startup/messageBoxActions.js +++ /dev/null @@ -1,55 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { settings } from '../../../settings/client'; -import { messageBox } from '../../../ui-utils/client'; -import { WebdavAccounts } from '../../../models/client'; -import { imperativeModal } from '../../../../client/lib/imperativeModal'; -import { getWebdavServerName } from '../../../../client/lib/getWebdavServerName'; -import AddWebdavAccountModal from '../../../../client/views/room/webdav/AddWebdavAccountModal'; -import WebdavFilePickerModal from '../../../../client/views/room/webdav/WebdavFilePickerModal'; - -messageBox.actions.add('WebDAV', 'Add Server', { - id: 'add-webdav', - icon: 'plus', - condition: () => settings.get('Webdav_Integration_Enabled'), - action() { - imperativeModal.open({ - component: AddWebdavAccountModal, - props: { onClose: imperativeModal.close, onConfirm: imperativeModal.close }, - }); - }, -}); - -Meteor.startup(function () { - Tracker.autorun(() => { - const accounts = WebdavAccounts.find(); - - if (accounts.count() === 0) { - return messageBox.actions.remove('WebDAV', /webdav-upload-/gi); - } - - accounts.forEach((account) => { - const name = getWebdavServerName({ name: account.name, serverURL: account.serverURL, username: account.username }); - - messageBox.actions.add('WebDAV', name, { - id: `webdav-upload-${account._id.toLowerCase()}`, - icon: 'cloud-plus', - condition: () => settings.get('Webdav_Integration_Enabled'), - action({ chat }) { - imperativeModal.open({ - component: WebdavFilePickerModal, - props: { - onUpload: async (file, description) => - chat.uploads.send(file, { - description, - }), - onClose: imperativeModal.close, - account, - }, - }); - }, - }); - }); - }); -}); diff --git a/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx b/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx index 8ded03a3e999..74d751de4f9d 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx @@ -5,6 +5,7 @@ import proxyquire from 'proxyquire'; import type { ReactNode } from 'react'; import React from 'react'; +import ModalContextMock from '../../../tests/mocks/client/ModalContextMock'; import RouterContextMock from '../../../tests/mocks/client/RouterContextMock'; import type * as AdministrationModelListModule from './AdministrationModelList'; @@ -29,13 +30,16 @@ describe('AdministrationModelList', () => { '../../../app/authorization/client': { userHasAllPermission: () => true, }, + '@tanstack/react-query': { + useQuery: () => '', + }, ...stubs, }).default; }; it('should render administration', async () => { const AdministrationModelList = loadMock(); - render( null} />); + render( null} />, { wrapper: ModalContextMock }); expect(screen.getByText('Administration')).to.exist; expect(screen.getByText('Workspace')).to.exist; @@ -44,7 +48,7 @@ describe('AdministrationModelList', () => { it('should not render workspace', async () => { const AdministrationModelList = loadMock(); - render( null} />); + render( null} />, { wrapper: ModalContextMock }); expect(screen.getByText('Administration')).to.exist; expect(screen.queryByText('Workspace')).to.not.exist; @@ -56,7 +60,11 @@ describe('AdministrationModelList', () => { const handleDismiss = spy(); const ProvidersMock = ({ children }: { children: ReactNode }) => { - return {children}; + return ( + + {children} + + ); }; it('should go to admin info', async () => { @@ -117,6 +125,7 @@ describe('AdministrationModelList', () => { showWorkspace={false} onDismiss={handleDismiss} />, + { wrapper: ProvidersMock }, ); const button = screen.getByText('Admin Item'); @@ -136,6 +145,7 @@ describe('AdministrationModelList', () => { showWorkspace={false} onDismiss={handleDismiss} />, + { wrapper: ProvidersMock }, ); const button = screen.getByText('Admin Item'); diff --git a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx index f047cde8f03f..e72c12e4c7f2 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx @@ -1,5 +1,6 @@ import { OptionTitle } from '@rocket.chat/fuselage'; -import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; +import { useTranslation, useRoute, useMethod, useSetModal } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import { FlowRouter } from 'meteor/kadira:flow-router'; import type { FC } from 'react'; import React from 'react'; @@ -7,6 +8,7 @@ import React from 'react'; import { userHasAllPermission } from '../../../app/authorization/client'; import type { AccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; import { getUpgradeTabLabel, isFullyFeature } from '../../../lib/upgradeTab'; +import RegisterWorkspaceModal from '../../views/admin/cloud/modals/RegisterWorkspaceModal'; import { useUpgradeTabParams } from '../../views/hooks/useUpgradeTabParams'; import Emoji from '../Emoji'; import ListItem from '../Sidebar/ListItem'; @@ -25,10 +27,21 @@ const AdministrationModelList: FC = ({ accountBoxI const shouldShowEmoji = isFullyFeature(tabType); const label = getUpgradeTabLabel(tabType); const hasInfoPermission = userHasAllPermission(INFO_PERMISSIONS); + const setModal = useSetModal(); + + const checkCloudRegisterStatus = useMethod('cloud:checkRegisterStatus'); + const result = useQuery(['admin/cloud/register-status'], async () => checkCloudRegisterStatus()); + const { workspaceRegistered, connectToCloud } = result.data || {}; + + const handleRegisterWorkspaceClick = (): void => { + const handleModalClose = (): void => setModal(null); + setModal(); + }; const infoRoute = useRoute('admin-info'); const adminRoute = useRoute('admin-index'); const upgradeRoute = useRoute('upgrade'); + const cloudRoute = useRoute('cloud'); const showUpgradeItem = !isLoading && tabType; return ( @@ -49,6 +62,18 @@ const AdministrationModelList: FC = ({ accountBoxI }} /> )} + { + if (workspaceRegistered) { + cloudRoute.push({ context: '/' }); + onDismiss(); + return; + } + handleRegisterWorkspaceClick(); + }} + /> {showWorkspace && ( void; haveAll?: boolean; haveNoAgentsSelectedOption?: boolean; }; + const AutoCompleteAgent = ({ value, + error, + placeholder, onChange, haveAll = false, haveNoAgentsSelectedOption = false, @@ -46,6 +51,8 @@ const AutoCompleteAgent = ({ return ( void; defaultParentRoom?: IRoom['_id']; nameSuggestion?: string; @@ -44,23 +44,14 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug const canCreate = (parentRoom || defaultParentRoom) && name; - const createDiscussion = useEndpointAction('POST', '/v1/rooms.createDiscussion'); + const createDiscussion = useEndpoint('POST', '/v1/rooms.createDiscussion'); - const create = useMutableCallback(async (): Promise => { - try { - const result = await createDiscussion({ - prid: defaultParentRoom || parentRoom, - t_name: name, - users: usernames, - reply: encrypted ? undefined : firstMessage, - ...(parentMessageId && { pmid: parentMessageId }), - }); - - goToRoomById(result.discussion._id); + const createDiscussionMutation = useMutation({ + mutationFn: createDiscussion, + onSuccess: ({ discussion }) => { + goToRoomById(discussion._id); onClose(); - } catch (error) { - console.warn(error); - } + }, }); const onChangeUsers = useMutableCallback((value, action) => { @@ -140,7 +131,19 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug - diff --git a/apps/meteor/client/components/GenericModal.tsx b/apps/meteor/client/components/GenericModal.tsx index b03f83f95a54..187d73444920 100644 --- a/apps/meteor/client/components/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal.tsx @@ -17,10 +17,11 @@ type GenericModalProps = RequiredModalProps & { title?: string | ReactElement; icon?: ComponentProps['name'] | ReactElement | null; confirmDisabled?: boolean; + tagline?: ReactNode; onCancel?: () => void; onClose?: () => void; onConfirm: () => void; -}; +} & Omit, 'title'>; const iconMap: Record['name']> = { danger: 'modal-warning', @@ -68,6 +69,7 @@ const GenericModal: FC = ({ onConfirm, dontAskAgain, confirmDisabled, + tagline, ...props }) => { const t = useTranslation(); @@ -77,6 +79,7 @@ const GenericModal: FC = ({ {renderIcon(icon, variant)} + {tagline && {tagline}} {title ?? t('Are_you_sure')} diff --git a/apps/meteor/client/components/GenericTable/GenericTable.tsx b/apps/meteor/client/components/GenericTable/GenericTable.tsx index e1e3321309e9..327372373be8 100644 --- a/apps/meteor/client/components/GenericTable/GenericTable.tsx +++ b/apps/meteor/client/components/GenericTable/GenericTable.tsx @@ -1,7 +1,7 @@ import { Pagination, Tile } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactNode, ReactElement, Key, Ref } from 'react'; +import type { ReactNode, ReactElement, Key, Ref, RefAttributes } from 'react'; import React, { useState, useEffect, forwardRef, useMemo } from 'react'; import flattenChildren from 'react-keyed-flatten-children'; @@ -105,6 +105,8 @@ const GenericTable = forwardRef(function GenericTable< )} ); -}); +}) as void }, TResultProps extends { _id?: Key } | object>( + props: GenericTableProps & RefAttributes, +) => ReactElement | null; export default GenericTable; diff --git a/apps/meteor/client/components/GenericTable/NoResults.tsx b/apps/meteor/client/components/GenericTable/NoResults.tsx index f2a1cdf6f41c..556292dfb531 100644 --- a/apps/meteor/client/components/GenericTable/NoResults.tsx +++ b/apps/meteor/client/components/GenericTable/NoResults.tsx @@ -11,7 +11,7 @@ type NoResultsProps = { }; const NoResults: FC = ({ icon, title, description, buttonTitle, buttonAction }) => ( - + > = ({ let markedOptions: marked.MarkedOptions; - const schemes = useSetting('Markdown_SupportSchemesForLink') as string; + const schemes = 'http,https'; switch (variant) { case 'inline': diff --git a/apps/meteor/client/components/Omnichannel/Tags.tsx b/apps/meteor/client/components/Omnichannel/Tags.tsx index f0c18d5b5026..53a91ecbc0d3 100644 --- a/apps/meteor/client/components/Omnichannel/Tags.tsx +++ b/apps/meteor/client/components/Omnichannel/Tags.tsx @@ -1,11 +1,10 @@ import { Field, TextInput, Chip, Button } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ChangeEvent, ReactElement } from 'react'; import React, { useState } from 'react'; -import { AsyncStatePhase } from '../../hooks/useAsyncState'; -import { useEndpointData } from '../../hooks/useEndpointData'; import { useFormsSubscription } from '../../views/omnichannel/additionalForms'; import { FormSkeleton } from './Skeleton'; @@ -23,13 +22,16 @@ const Tags = ({ const t = useTranslation(); const forms = useFormsSubscription() as any; - const { value: tagsResult, phase: stateTags } = useEndpointData('/v1/livechat/tags'); - // TODO: Refactor the formsSubscription to use components instead of hooks (since the only thing the hook does is return a component) const { useCurrentChatTags } = forms; // Conditional hook was required since the whole formSubscription uses hooks in an incorrect manner const EETagsComponent = useCurrentChatTags?.(); + const getTags = useEndpoint('GET', '/v1/livechat/tags'); + const { data: tagsResult, isInitialLoading } = useQuery(['/v1/livechat/tags'], () => getTags({ text: '' }), { + enabled: Boolean(EETagsComponent), + }); + const dispatchToastMessage = useToastMessageDispatch(); const [tagValue, handleTagValue] = useState(''); @@ -61,7 +63,7 @@ const Tags = ({ handleTagValue(''); }); - if ([stateTags].includes(AsyncStatePhase.LOADING)) { + if (isInitialLoading) { return ; } diff --git a/apps/meteor/client/components/Page/Page.tsx b/apps/meteor/client/components/Page/Page.tsx index 8951a0fd4da2..f3cabcce81a6 100644 --- a/apps/meteor/client/components/Page/Page.tsx +++ b/apps/meteor/client/components/Page/Page.tsx @@ -22,6 +22,7 @@ const Page = ({ background = 'light', ...props }: PageProps): ReactElement => { overflow='hidden' aria-labelledby='PageHeader-title' bg={background} + color='default' {...props} /> diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 80286a1a2b85..03edf0be79d9 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -1,8 +1,9 @@ import { AutoComplete, Option, Box } from '@rocket.chat/fuselage'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement, ComponentProps } from 'react'; import React, { memo, useMemo, useState } from 'react'; -import { useEndpointData } from '../../hooks/useEndpointData'; import RoomAvatar from '../avatar/RoomAvatar'; import Avatar from './Avatar'; @@ -20,14 +21,21 @@ type RoomAutoCompleteProps = Omit, 'value /* @deprecated */ const RoomAutoComplete = (props: RoomAutoCompleteProps): ReactElement => { const [filter, setFilter] = useState(''); - const { value: data } = useEndpointData('/v1/rooms.autocomplete.channelAndPrivate', { params: useMemo(() => query(filter), [filter]) }); + const autocomplete = useEndpoint('GET', '/v1/rooms.autocomplete.channelAndPrivate'); + + const result = useQuery(['rooms.autocomplete.channelAndPrivate', filter], () => autocomplete(query(filter)), { + keepPreviousData: true, + }); + const options = useMemo( () => - data?.items.map(({ name, _id, avatarETag, t }) => ({ - value: _id, - label: { name, avatarETag, type: t }, - })) || [], - [data], + result.isSuccess + ? result.data.items.map(({ name, _id, avatarETag, t }) => ({ + value: _id, + label: { name, avatarETag, type: t }, + })) + : [], + [result.data?.items, result.isSuccess], ) as unknown as { value: string; label: string }[]; return ( diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index e54ab6ae5731..140236f812b6 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -4,6 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ComponentProps, MouseEvent } from 'react'; import React, { forwardRef } from 'react'; +import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; import MarkdownText from '../MarkdownText'; import * as Status from '../UserStatus'; import UserAvatar from '../avatar/UserAvatar'; @@ -65,6 +66,7 @@ const UserCard = forwardRef(function UserCard( ref, ) { const t = useTranslation(); + const isLayoutEmbedded = useEmbeddedLayout(); return ( @@ -111,7 +113,7 @@ const UserCard = forwardRef(function UserCard( {typeof bio === 'string' ? : bio} )} - {!isLoading && open && {t('See_full_profile')}} + {!isLoading && open && !isLayoutEmbedded && {t('See_full_profile')}} {onClose && ( diff --git a/apps/meteor/client/components/VerticalBar/VerticalBar.tsx b/apps/meteor/client/components/VerticalBar/VerticalBar.tsx index 5792997e71f4..af000dbf6e07 100644 --- a/apps/meteor/client/components/VerticalBar/VerticalBar.tsx +++ b/apps/meteor/client/components/VerticalBar/VerticalBar.tsx @@ -3,13 +3,13 @@ import { useLayoutSizes, useLayoutContextualBarPosition } from '@rocket.chat/ui- import type { FC, ComponentProps } from 'react'; import React, { memo } from 'react'; -const VerticalBar: FC> = ({ children, ...props }) => { +const VerticalBar: FC> = ({ children, bg = 'room', ...props }) => { const sizes = useLayoutSizes(); const position = useLayoutContextualBarPosition(); return ( { event.stopPropagation(); - openUserCard(username)(event); + chat?.userCard.open(username)(event); }; }, - [openUserCard], + [chat?.userCard], ); const resolveChannelMention = useCallback((mention: string) => channels?.find(({ name }) => name === mention), [channels]); diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index 1038df59d3a7..70d1539d7d6d 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -13,10 +13,10 @@ import React, { memo } from 'react'; import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../hooks/useFormatTime'; -import { useUserCard } from '../../hooks/useUserCard'; import { useUserData } from '../../hooks/useUserData'; import { getUserDisplayName } from '../../lib/getUserDisplayName'; import type { UserPresence } from '../../lib/presence'; +import { useChat } from '../../views/room/contexts/ChatContext'; import StatusIndicators from './StatusIndicators'; import MessageRoles from './header/MessageRoles'; import { useMessageRoles } from './header/hooks/useMessageRoles'; @@ -28,7 +28,6 @@ type MessageHeaderProps = { const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { const t = useTranslation(); - const { open: openUserCard } = useUserCard(); const formatTime = useFormatTime(); const formatDateAndTime = useFormatDateAndTime(); @@ -42,6 +41,8 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { const roles = useMessageRoles(message.u._id, message.rid, showRoles); const shouldShowRolesList = roles.length > 0; + const chat = useChat(); + return ( @@ -49,8 +50,11 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { {...(!showUsername && { 'data-qa-type': 'username' })} title={!showUsername && !usernameAndRealNameAreSame ? `@${user.username}` : undefined} data-username={user.username} - onClick={user.username !== undefined ? openUserCard(user.username) : undefined} - style={{ cursor: 'pointer' }} + {...(user.username !== undefined && + chat?.userCard && { + onClick: chat?.userCard.open(message.u.username), + style: { cursor: 'pointer' }, + })} > {message.alias || getUserDisplayName(user.name, user.username, showRealName)} @@ -60,8 +64,11 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { @{user.username} diff --git a/apps/meteor/client/components/message/ToolboxHolder.tsx b/apps/meteor/client/components/message/ToolboxHolder.tsx index 295fe798fc3b..0f6c02f87002 100644 --- a/apps/meteor/client/components/message/ToolboxHolder.tsx +++ b/apps/meteor/client/components/message/ToolboxHolder.tsx @@ -1,9 +1,11 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { MessageToolboxWrapper } from '@rocket.chat/fuselage'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useRef } from 'react'; import type { MessageActionContext } from '../../../app/ui-utils/client/lib/MessageAction'; +import { useChat } from '../../views/room/contexts/ChatContext'; import { useIsVisible } from '../../views/room/hooks/useIsVisible'; import Toolbox from './toolbox/Toolbox'; @@ -17,7 +19,29 @@ export const ToolboxHolder = ({ message, context }: ToolboxHolderProps): ReactEl const [visible] = useIsVisible(ref); - return {visible && }; + const chat = useChat(); + + const depsQueryResult = useQuery(['toolbox', message._id, context], async () => { + const room = await chat?.data.findRoom(); + const subscription = await chat?.data.findSubscription(); + return { + room, + subscription, + }; + }); + + return ( + + {visible && depsQueryResult.isSuccess && depsQueryResult.data.room && ( + + )} + + ); }; export default memo(ToolboxHolder); diff --git a/apps/meteor/client/components/message/content/ThreadMetrics.tsx b/apps/meteor/client/components/message/content/ThreadMetrics.tsx index 296de2dfc6a9..ffadd21447eb 100644 --- a/apps/meteor/client/components/message/content/ThreadMetrics.tsx +++ b/apps/meteor/client/components/message/content/ThreadMetrics.tsx @@ -40,14 +40,14 @@ const ThreadMetrics = ({ unread, mention, all, rid, mid, counter, participants, }); const handleFollow = useCallback(() => { - toggleFollowingThreadMutation.mutate({ tmid: mid, follow: !following }); - }, [following, mid, toggleFollowingThreadMutation]); + toggleFollowingThreadMutation.mutate({ rid, tmid: mid, follow: !following }); + }, [following, rid, mid, toggleFollowingThreadMutation]); return (
- goToThread(mid)}> + goToThread({ rid, tmid: mid })}> {t('Reply')} diff --git a/apps/meteor/client/components/message/hooks/useMessageNormalization.ts b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts similarity index 68% rename from apps/meteor/client/components/message/hooks/useMessageNormalization.ts rename to apps/meteor/client/components/message/hooks/useNormalizedMessage.ts index a4ed5d6c52b3..d5a823e3ff95 100644 --- a/apps/meteor/client/components/message/hooks/useMessageNormalization.ts +++ b/apps/meteor/client/components/message/hooks/useNormalizedMessage.ts @@ -6,12 +6,12 @@ import type { MessageWithMdEnforced } from '../../../lib/parseMessageTextToAstMa import { parseMessageTextToAstMarkdown, removePossibleNullMessageValues } from '../../../lib/parseMessageTextToAstMarkdown'; import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import { useKatex } from '../../../views/room/MessageList/hooks/useKatex'; -import { useRoomSubscription } from '../../../views/room/contexts/RoomContext'; +import { useSubscriptionFromMessageQuery } from './useSubscriptionFromMessageQuery'; -export const useMessageNormalization = (): ((message: TMessage) => MessageWithMdEnforced) => { +export const useNormalizedMessage = (message: TMessage): MessageWithMdEnforced => { const { katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled } = useKatex(); - const subscription = useRoomSubscription(); + const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; const autoTranslateOptions = useAutoTranslate(subscription); const showColors = useSetting('HexColorPreview_Enabled'); @@ -27,7 +27,6 @@ export const useMessageNormalization = (): ((message: }), }; - return (message: TTMessage): MessageWithMdEnforced => - parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); - }, [showColors, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, autoTranslateOptions]); + return parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); + }, [showColors, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, message, autoTranslateOptions]); }; diff --git a/apps/meteor/client/components/message/hooks/useSubscriptionFromMessageQuery.ts b/apps/meteor/client/components/message/hooks/useSubscriptionFromMessageQuery.ts new file mode 100644 index 000000000000..195032210e8d --- /dev/null +++ b/apps/meteor/client/components/message/hooks/useSubscriptionFromMessageQuery.ts @@ -0,0 +1,12 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useQuery } from '@tanstack/react-query'; + +import { useChat } from '../../../views/room/contexts/ChatContext'; + +export const useSubscriptionFromMessageQuery = (message: IMessage) => { + const chat = useChat(); + + return useQuery(['messages', message._id, 'subscription'], async () => { + return chat?.data.getSubscriptionFromMessage(message) ?? null; + }); +}; diff --git a/apps/meteor/client/components/message/list/MessageListSkeleton.tsx b/apps/meteor/client/components/message/list/MessageListSkeleton.tsx new file mode 100644 index 000000000000..7c780fae5946 --- /dev/null +++ b/apps/meteor/client/components/message/list/MessageListSkeleton.tsx @@ -0,0 +1,38 @@ +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import React, { memo, useMemo } from 'react'; + +const availablePercentualWidths = [47, 68, 75, 82]; + +type MessageListSkeletonProps = { + messageCount?: number; +}; + +const MessageListSkeleton = ({ messageCount = 2 }: MessageListSkeletonProps): ReactElement => { + const widths = useMemo( + () => + Array.from( + { length: messageCount }, + () => `${availablePercentualWidths[Math.floor(Math.random() * availablePercentualWidths.length)]}%`, + ), + [messageCount], + ); + + return ( + + {widths.map((width, index) => ( + + + + + + + + + + ))} + + ); +}; + +export default memo(MessageListSkeleton); diff --git a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx index f438e91d3d8b..6cd26687e730 100644 --- a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx @@ -4,6 +4,7 @@ import type { ComponentProps, UIEvent, ReactElement } from 'react'; import React, { useState, Fragment, useRef } from 'react'; import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; +import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; import ToolboxDropdown from './ToolboxDropdown'; type MessageActionConfigOption = Omit & { @@ -19,6 +20,7 @@ export const MessageActionMenu = ({ options, ...props }: MessageActionMenuProps) const t = useTranslation(); const [visible, setVisible] = useState(false); + const isLayoutEmbedded = useEmbeddedLayout(); const groupOptions = options .map(({ color, ...option }) => ({ @@ -28,7 +30,8 @@ export const MessageActionMenu = ({ options, ...props }: MessageActionMenuProps) .reduce((acc, option) => { const group = option.variant ? option.variant : ''; acc[group] = acc[group] || []; - acc[group].push(option); + if (!(isLayoutEmbedded && option.id === 'reply-directly')) acc[group].push(option); + return acc; }, {} as { [key: string]: MessageActionConfigOption[] }) as { [key: string]: MessageActionConfigOption[]; diff --git a/apps/meteor/client/components/message/toolbox/Toolbox.tsx b/apps/meteor/client/components/message/toolbox/Toolbox.tsx index c4787818c225..1a9a329faf92 100644 --- a/apps/meteor/client/components/message/toolbox/Toolbox.tsx +++ b/apps/meteor/client/components/message/toolbox/Toolbox.tsx @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, ITranslatedMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket.chat/core-typings'; import { isThreadMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { MessageToolbox, MessageToolboxItem } from '@rocket.chat/fuselage'; import { useUser, useSettings, useTranslation } from '@rocket.chat/ui-contexts'; @@ -11,7 +11,6 @@ import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction import { useIsSelecting } from '../../../views/room/MessageList/contexts/SelectedMessagesContext'; import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import { useChat } from '../../../views/room/contexts/ChatContext'; -import { useRoom, useRoomSubscription } from '../../../views/room/contexts/RoomContext'; import { useToolboxContext } from '../../../views/room/contexts/ToolboxContext'; import MessageActionMenu from './MessageActionMenu'; @@ -38,14 +37,13 @@ const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActi type ToolboxProps = { message: IMessage & Partial; messageContext?: MessageActionContext; + room: IRoom; + subscription?: ISubscription; }; -const Toolbox = ({ message, messageContext }: ToolboxProps): ReactElement | null => { +const Toolbox = ({ message, messageContext, room, subscription }: ToolboxProps): ReactElement | null => { const t = useTranslation(); - const room = useRoom(); - const subscription = useRoomSubscription(); - const settings = useSettings(); const user = useUser(); diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 783479f7a1a4..f3071b9195cf 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -6,7 +6,6 @@ import type { ReactElement } from 'react'; import React, { memo } from 'react'; import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; -import { useUserCard } from '../../../hooks/useUserCard'; import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext'; import { useIsSelecting, @@ -14,6 +13,7 @@ import { useIsSelectedMessage, useCountSelected, } from '../../../views/room/MessageList/contexts/SelectedMessagesContext'; +import { useChat } from '../../../views/room/contexts/ChatContext'; import UserAvatar from '../../avatar/UserAvatar'; import IgnoredContent from '../IgnoredContent'; import MessageHeader from '../MessageHeader'; @@ -22,19 +22,21 @@ import ToolboxHolder from '../ToolboxHolder'; import RoomMessageContent from './room/RoomMessageContent'; type RoomMessageProps = { - message: IMessage; + message: IMessage & { ignored?: boolean }; sequential: boolean; unread: boolean; mention: boolean; all: boolean; context?: MessageActionContext; + ignoredUser?: boolean; }; -const RoomMessage = ({ message, sequential, all, mention, unread, context }: RoomMessageProps): ReactElement => { +const RoomMessage = ({ message, sequential, all, mention, unread, context, ignoredUser }: RoomMessageProps): ReactElement => { const uid = useUserId(); const editing = useIsMessageHighlight(message._id); - const [ignored, toggleIgnoring] = useToggle((message as { ignored?: boolean }).ignored ?? false); - const { open: openUserCard } = useUserCard(); + const [displayIgnoredMessage, toggleDisplayIgnoredMessage] = useToggle(false); + const ignored = (ignoredUser || message.ignored) && !displayIgnoredMessage; + const chat = useChat(); const selecting = useIsSelecting(); const toggleSelected = useToggleSelect(message._id); @@ -63,9 +65,11 @@ const RoomMessage = ({ message, sequential, all, mention, unread, context }: Roo )} {selecting && } @@ -76,7 +80,7 @@ const RoomMessage = ({ message, sequential, all, mention, unread, context }: Roo {!sequential && } {ignored ? ( - + ) : ( )} diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index 4c067715be05..6dfa2af11118 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -19,7 +19,6 @@ import React, { memo } from 'react'; import { MessageTypes } from '../../../../app/ui-utils/client'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../../hooks/useFormatTime'; -import { useUserCard } from '../../../hooks/useUserCard'; import { useUserData } from '../../../hooks/useUserData'; import { getUserDisplayName } from '../../../lib/getUserDisplayName'; import type { UserPresence } from '../../../lib/presence'; @@ -29,6 +28,7 @@ import { useIsSelectedMessage, useCountSelected, } from '../../../views/room/MessageList/contexts/SelectedMessagesContext'; +import { useChat } from '../../../views/room/contexts/ChatContext'; import UserAvatar from '../../avatar/UserAvatar'; import Attachments from '../content/Attachments'; import MessageActions from '../content/MessageActions'; @@ -42,7 +42,7 @@ const SystemMessage = ({ message }: SystemMessageProps): ReactElement => { const t = useTranslation(); const formatTime = useFormatTime(); const formatDateAndTime = useFormatDateAndTime(); - const { open: openUserCard } = useUserCard(); + const chat = useChat(); const showRealName = useMessageListShowRealName(); const user: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; @@ -72,8 +72,11 @@ const SystemMessage = ({ message }: SystemMessageProps): ReactElement => { {getUserDisplayName(user.name, user.username, showRealName)} @@ -82,8 +85,11 @@ const SystemMessage = ({ message }: SystemMessageProps): ReactElement => { {' '} @{user.username} diff --git a/apps/meteor/client/components/message/variants/ThreadMessage.tsx b/apps/meteor/client/components/message/variants/ThreadMessage.tsx index 4f4dfdfec8b4..399e387fb5aa 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessage.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessage.tsx @@ -5,8 +5,8 @@ import { useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; -import { useUserCard } from '../../../hooks/useUserCard'; import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext'; +import { useChat } from '../../../views/room/contexts/ChatContext'; import UserAvatar from '../../avatar/UserAvatar'; import IgnoredContent from '../IgnoredContent'; import MessageHeader from '../MessageHeader'; @@ -24,7 +24,7 @@ const ThreadMessage = ({ message, sequential, unread }: ThreadMessageProps): Rea const uid = useUserId(); const editing = useIsMessageHighlight(message._id); const [ignored, toggleIgnoring] = useToggle((message as { ignored?: boolean }).ignored); - const { open: openUserCard } = useUserCard(); + const chat = useChat(); return ( )} {sequential && } diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx index 84981a19dbba..9c3d112a6056 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx @@ -58,7 +58,11 @@ const ThreadMessagePreview = ({ message, sequential, ...props }: ThreadMessagePr {!sequential && ( goToThread(message.tmid, parentMessage.data?._id) : undefined} + onClick={ + !isSelecting && parentMessage.isSuccess + ? () => goToThread({ rid: message.rid, tmid: message.tmid, jump: parentMessage.data?._id }) + : undefined + } > @@ -87,7 +91,7 @@ const ThreadMessagePreview = ({ message, sequential, ...props }: ThreadMessagePr )} - goToThread(message.tmid, message._id) : undefined}> + goToThread({ rid: message.rid, tmid: message.tmid, jump: message._id }) : undefined}> {!isSelecting && } {isSelecting && } diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.spec.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.spec.tsx index 4788f5dae01b..84f0013b73e4 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.spec.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.spec.tsx @@ -5,6 +5,7 @@ import proxyquire from 'proxyquire'; import type { ReactNode } from 'react'; import React, { useMemo } from 'react'; +import FakeChatProvider from '../../../../../tests/mocks/client/FakeChatProvider'; import FakeRoomProvider from '../../../../../tests/mocks/client/FakeRoomProvider'; import RouterContextMock from '../../../../../tests/mocks/client/RouterContextMock'; import { createFakeMessageWithMd } from '../../../../../tests/mocks/data'; @@ -30,19 +31,21 @@ describe('RoomMessageContent', () => { - ({ - queryUserData: () => ({ - subscribe: () => () => undefined, - get: () => undefined, + + ({ + queryUserData: () => ({ + subscribe: () => () => undefined, + get: () => undefined, + }), }), - }), - [], - )} - > - {children} - + [], + )} + > + {children} + + diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 15de13bc5ca0..7e7ff9075866 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -3,11 +3,11 @@ import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useMemo, memo } from 'react'; +import React, { memo } from 'react'; import { useUserData } from '../../../../hooks/useUserData'; import type { UserPresence } from '../../../../lib/presence'; -import { useRoomSubscription } from '../../../../views/room/contexts/RoomContext'; +import { useChat } from '../../../../views/room/contexts/ChatContext'; import MessageContentBody from '../../MessageContentBody'; import ReadReceiptIndicator from '../../ReadReceiptIndicator'; import Attachments from '../../content/Attachments'; @@ -19,8 +19,9 @@ import Reactions from '../../content/Reactions'; import ThreadMetrics from '../../content/ThreadMetrics'; import UiKitSurface from '../../content/UiKitSurface'; import UrlPreviews from '../../content/UrlPreviews'; -import { useMessageNormalization } from '../../hooks/useMessageNormalization'; +import { useNormalizedMessage } from '../../hooks/useNormalizedMessage'; import { useOembedLayout } from '../../hooks/useOembedLayout'; +import { useSubscriptionFromMessageQuery } from '../../hooks/useSubscriptionFromMessageQuery'; type RoomMessageContentProps = { message: IMessage; @@ -32,15 +33,15 @@ type RoomMessageContentProps = { const RoomMessageContent = ({ message, unread, all, mention }: RoomMessageContentProps): ReactElement => { const encrypted = isE2EEMessage(message); const { enabled: oembedEnabled } = useOembedLayout(); - const broadcast = useRoomSubscription()?.broadcast ?? false; + const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; + const broadcast = subscription?.broadcast ?? false; const uid = useUserId(); const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); - + const chat = useChat(); const t = useTranslation(); - const normalizeMessage = useMessageNormalization(); - const normalizedMessage = useMemo(() => normalizeMessage(message), [message, normalizeMessage]); + const normalizedMessage = useNormalizedMessage(message); return ( <> @@ -76,7 +77,7 @@ const RoomMessageContent = ({ message, unread, all, mention }: RoomMessageConten {normalizedMessage.reactions && Object.keys(normalizedMessage.reactions).length && } - {isThreadMainMessage(normalizedMessage) && ( + {chat && isThreadMainMessage(normalizedMessage) && ( -1)} diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 15a2e883be6f..d2e5b0f0f26c 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -3,11 +3,10 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useUserId, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useMemo, memo } from 'react'; +import React, { memo } from 'react'; import { useUserData } from '../../../../hooks/useUserData'; import type { UserPresence } from '../../../../lib/presence'; -import { useRoomSubscription } from '../../../../views/room/contexts/RoomContext'; import MessageContentBody from '../../MessageContentBody'; import ReadReceiptIndicator from '../../ReadReceiptIndicator'; import Attachments from '../../content/Attachments'; @@ -17,8 +16,9 @@ import MessageActions from '../../content/MessageActions'; import Reactions from '../../content/Reactions'; import UiKitSurface from '../../content/UiKitSurface'; import UrlPreviews from '../../content/UrlPreviews'; -import { useMessageNormalization } from '../../hooks/useMessageNormalization'; +import { useNormalizedMessage } from '../../hooks/useNormalizedMessage'; import { useOembedLayout } from '../../hooks/useOembedLayout'; +import { useSubscriptionFromMessageQuery } from '../../hooks/useSubscriptionFromMessageQuery'; type ThreadMessageContentProps = { message: IThreadMessage | IThreadMainMessage; @@ -27,15 +27,15 @@ type ThreadMessageContentProps = { const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElement => { const encrypted = isE2EEMessage(message); const { enabled: oembedEnabled } = useOembedLayout(); - const broadcast = useRoomSubscription()?.broadcast ?? false; + const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; + const broadcast = subscription?.broadcast ?? false; const uid = useUserId(); const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); const t = useTranslation(); - const normalizeMessage = useMessageNormalization(); - const normalizedMessage = useMemo(() => normalizeMessage(message), [message, normalizeMessage]); + const normalizedMessage = useNormalizedMessage(message); return ( <> diff --git a/apps/meteor/client/hooks/usePresence.ts b/apps/meteor/client/hooks/usePresence.ts index ae152023d88c..bfc89a21a442 100644 --- a/apps/meteor/client/hooks/usePresence.ts +++ b/apps/meteor/client/hooks/usePresence.ts @@ -5,6 +5,7 @@ import type { UserPresence } from '../lib/presence'; import { Presence } from '../lib/presence'; /** + * @deprecated * Hook to fetch and subscribe users presence * * @param uid - User Id diff --git a/apps/meteor/client/hooks/useUserData.ts b/apps/meteor/client/hooks/useUserData.ts index 3faeefe34775..391738e28c55 100644 --- a/apps/meteor/client/hooks/useUserData.ts +++ b/apps/meteor/client/hooks/useUserData.ts @@ -13,19 +13,6 @@ import type { UserPresence } from '../lib/presence'; */ export const useUserData = (uid: string): UserPresence | undefined => { const userPresence = useContext(UserPresenceContext); - // const subscription = useCallback( - // (callback: () => void): (() => void) => { - // Presence.listen(uid, callback); - // return (): void => { - // Presence.stop(uid, callback); - // }; - // }, - // [uid], - // ); - - // const getSnapshot = (): UserPresence | undefined => Presence.store.get(uid); - - // return useSyncExternalStore(subscription, getSnapshot); const { subscribe, get } = useMemo( () => userPresence?.queryUserData(uid) ?? { subscribe: () => () => undefined, get: () => undefined }, diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index 3dc430e1eda6..3843b7fcd9e2 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -29,7 +29,6 @@ import '../app/logger/client'; import '../app/markdown/client'; import '../app/message-attachments/client'; import '../app/message-mark-as-unread/client'; -import '../app/message-snippet/client'; import '../app/nextcloud/client'; import '../app/oauth2-server-config/client'; import '../app/oembed/client'; diff --git a/apps/meteor/client/lib/VideoConfManager.ts b/apps/meteor/client/lib/VideoConfManager.ts index 257bcfd01847..8d41129c99e7 100644 --- a/apps/meteor/client/lib/VideoConfManager.ts +++ b/apps/meteor/client/lib/VideoConfManager.ts @@ -22,10 +22,10 @@ export type DirectCallParams = { uid: IUser['_id']; rid: IRoom['_id']; callId: string; + + // #ToDo: The attributes below should not be part of DirectCallParams - they are used by local events only, never notification events. dismissed?: boolean; acceptTimeout?: ReturnType | undefined; - // TODO: improve this, nowadays there is not possible check if the video call has finished, but ist a nice improvement - // state: 'incoming' | 'outgoing' | 'connected' | 'disconnected' | 'dismissed'; }; type IncomingDirectCall = DirectCallParams & { timeout: number }; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index d5271cdefa1e..20caee431a28 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,19 +1,10 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { UIEvent } from 'react'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; import type { Subscribable } from '../../definitions/Subscribable'; import type { Upload } from './Upload'; -export type UserActionAPI = { - readonly action: { - get(): { - action: 'typing' | 'recording' | 'uploading' | 'playing'; - users: string[]; - }[]; - subscribe(callback: () => void): () => void; - }; -}; - export type ComposerAPI = { release(): void; readonly text: string; @@ -82,6 +73,10 @@ export type DataAPI = { markRoomAsRead(): Promise; findDiscussionByID(drid: IRoom['_id']): Promise; getDiscussionByID(drid: IRoom['_id']): Promise; + findSubscription(): Promise; + getSubscription(): Promise; + findSubscriptionFromMessage(message: IMessage): Promise; + getSubscriptionFromMessage(message: IMessage): Promise; }; export type UploadsAPI = { @@ -111,6 +106,18 @@ export type ChatAPI = { cancel(): Promise; } | undefined; + + readonly userCard: { + open(username: string): (event: UIEvent) => void; + close(): void; + }; + + readonly action: { + start(action: 'typing'): void; + stop(action: 'typing' | 'recording' | 'uploading' | 'playing'): void; + performContinuously(action: 'recording' | 'uploading' | 'playing'): void; + }; + readonly flows: { readonly uploadFiles: (files: readonly File[]) => Promise; readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean }) => Promise; @@ -120,11 +127,5 @@ export type ChatAPI = { readonly processSetReaction: (message: Pick) => Promise; readonly requestMessageDeletion: (message: IMessage) => Promise; readonly replyBroadcast: (message: IMessage) => Promise; - - readonly action: { - start(action: 'typing'): void; - stop(action: 'typing' | 'recording' | 'uploading' | 'playing'): void; - performContinuously(action: 'recording' | 'uploading' | 'playing'): void; - }; }; }; diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts index f393252f1354..aa619f601e17 100644 --- a/apps/meteor/client/lib/chats/data.ts +++ b/apps/meteor/client/lib/chats/data.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import moment from 'moment'; import { hasAtLeastOnePermission, hasPermission } from '../../../app/authorization/client'; @@ -86,17 +86,19 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return false; } - const hasPermission = hasAtLeastOnePermission('edit-message', message.rid); + const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid); const editAllowed = (settings.get('Message_AllowEditing') as boolean | undefined) ?? false; const editOwn = message?.u && message.u._id === Meteor.userId(); - if (!hasPermission && (!editAllowed || !editOwn)) { + if (!canEditMessage && (!editAllowed || !editOwn)) { return false; } const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes') as number | undefined; + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + const elapsedMinutes = moment().diff(message.ts, 'minutes'); - if (elapsedMinutes && blockEditInMinutes && elapsedMinutes > blockEditInMinutes) { + if (!bypassBlockTimeLimit && elapsedMinutes && blockEditInMinutes && elapsedMinutes > blockEditInMinutes) { return false; } @@ -206,8 +208,9 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage } const blockDeleteInMinutes = settings.get('Message_AllowDeleting_BlockDeleteInMinutes') as number | undefined; + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); const elapsedMinutes = moment().diff(message.ts, 'minutes'); - const onTimeForDelete = !blockDeleteInMinutes || !elapsedMinutes || elapsedMinutes <= blockDeleteInMinutes; + const onTimeForDelete = bypassBlockTimeLimit || !blockDeleteInMinutes || !elapsedMinutes || elapsedMinutes <= blockDeleteInMinutes; return deleteAllowed && onTimeForDelete; }; @@ -263,6 +266,33 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage return discussion; }; + const createStrictGetter = Promise>( + find: TFind, + errorMessage: string, + ): ((...args: Parameters) => Promise>, undefined>>) => { + return async (...args) => { + const result = await find(...args); + + if (!result) { + throw new Error(errorMessage); + } + + return result; + }; + }; + + const findSubscription = async (): Promise => { + return ChatSubscription.findOne({ rid }, { reactive: false }); + }; + + const getSubscription = createStrictGetter(findSubscription, 'Subscription not found'); + + const findSubscriptionFromMessage = async (message: IMessage): Promise => { + return ChatSubscription.findOne({ rid: message.rid }, { reactive: false }); + }; + + const getSubscriptionFromMessage = createStrictGetter(findSubscriptionFromMessage, 'Subscription not found'); + return { composeMessage, findMessageByID, @@ -290,5 +320,9 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage markRoomAsRead, findDiscussionByID, getDiscussionByID, + findSubscription, + getSubscription, + findSubscriptionFromMessage, + getSubscriptionFromMessage, }; }; diff --git a/apps/meteor/client/lib/presence.ts b/apps/meteor/client/lib/presence.ts index b81b7e8195f7..0e7a8495cc6c 100644 --- a/apps/meteor/client/lib/presence.ts +++ b/apps/meteor/client/lib/presence.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { APIClient } from '../../app/utils/client'; -export const STATUS_MAP = [UserStatus.OFFLINE, UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY]; +export const STATUS_MAP = [UserStatus.OFFLINE, UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY, UserStatus.DISABLED]; type InternalEvents = { remove: IUser['_id']; @@ -35,7 +35,7 @@ const uids = new Set(); const update: EventHandlerOf = (update) => { if (update?._id) { - store.set(update._id, { ...store.get(update._id), ...update }); + store.set(update._id, { ...store.get(update._id), ...update, ...(status === 'disabled' && { status: UserStatus.DISABLED }) }); uids.delete(update._id); } }; @@ -175,7 +175,16 @@ const get = async (uid: UserPresence['_id']): Promise listen(uid, callback); }); +let status = 'enabled'; + +const setStatus = (newStatus: 'enabled' | 'disabled'): void => { + status = newStatus; + reset(); +}; + export const Presence = { + setStatus, + status, listen, stop, reset, diff --git a/apps/meteor/client/lib/utils/jumpToMessage.ts b/apps/meteor/client/lib/utils/jumpToMessage.ts index 4ade65112439..f80f6a224cb2 100644 --- a/apps/meteor/client/lib/utils/jumpToMessage.ts +++ b/apps/meteor/client/lib/utils/jumpToMessage.ts @@ -20,6 +20,7 @@ export const jumpToMessage = (message: IMessage) => { name: ChatRoom.findOne({ _id: message.rid })?.name ?? '', }, { + ...FlowRouter.current().queryParams, jump: message._id, }, ); diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index 36f0a29eabc3..08cb64cadffe 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -1,5 +1,4 @@ -import '../ee/definition/rest'; -import '../ee/definition/methods'; +import '../ee/definition'; import '../definition/methods'; import '../ee/client/ecdh'; import './polyfills'; diff --git a/apps/meteor/client/methods/updateMessage.ts b/apps/meteor/client/methods/updateMessage.ts index 0f77b000e3ff..c03c188c2a25 100644 --- a/apps/meteor/client/methods/updateMessage.ts +++ b/apps/meteor/client/methods/updateMessage.ts @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import moment from 'moment'; import _ from 'underscore'; -import { hasAtLeastOnePermission } from '../../app/authorization/client'; +import { hasAtLeastOnePermission, hasPermission } from '../../app/authorization/client'; import { ChatMessage } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { t } from '../../app/utils/client'; @@ -23,7 +23,7 @@ Meteor.methods({ if (!originalMessage) { return; } - const hasPermission = hasAtLeastOnePermission('edit-message', message.rid); + const canEditMessage = hasAtLeastOnePermission('edit-message', message.rid); const editAllowed = settings.get('Message_AllowEditing'); let editOwn = false; @@ -42,7 +42,7 @@ Meteor.methods({ return false; } - if (!(hasPermission || (editAllowed && editOwn))) { + if (!(canEditMessage || (editAllowed && editOwn))) { dispatchToastMessage({ type: 'error', message: t('error-action-not-allowed', { action: t('Message_editing') }), @@ -51,7 +51,9 @@ Meteor.methods({ } const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'); - if (_.isNumber(blockEditInMinutes) && blockEditInMinutes !== 0) { + const bypassBlockTimeLimit = hasPermission('bypass-time-limit-edit-and-delete'); + + if (!bypassBlockTimeLimit && _.isNumber(blockEditInMinutes) && blockEditInMinutes !== 0) { if (originalMessage.ts) { const msgTs = moment(originalMessage.ts); if (msgTs) { diff --git a/apps/meteor/client/providers/TooltipProvider.tsx b/apps/meteor/client/providers/TooltipProvider.tsx index 4fb6abaae6b6..ffc39cf53fc1 100644 --- a/apps/meteor/client/providers/TooltipProvider.tsx +++ b/apps/meteor/client/providers/TooltipProvider.tsx @@ -41,7 +41,6 @@ const TooltipProvider: FC = ({ children }) => { } anchor.setAttribute('data-title', title); anchor.setAttribute('data-tooltip', title); - anchor.removeAttribute('title'); lastAnchor.current = anchor; setTooltip(); }, 300); diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index 8272f6acd7bb..24190bb44d58 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -17,7 +17,7 @@ type TranslationNamespace = Extract exten : never : never; -const namespacesDefault = ['onboarding', 'registration'] as TranslationNamespace[]; +const namespacesDefault = ['onboarding', 'registration', 'cloud'] as TranslationNamespace[]; const parseToJSON = (customTranslations: string) => { try { diff --git a/apps/meteor/client/providers/UserPresenceProvider.tsx b/apps/meteor/client/providers/UserPresenceProvider.tsx index a0f2c23cc246..2032b5adaa53 100644 --- a/apps/meteor/client/providers/UserPresenceProvider.tsx +++ b/apps/meteor/client/providers/UserPresenceProvider.tsx @@ -1,5 +1,6 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect } from 'react'; import { UserPresenceContext } from '../contexts/UserPresenceContext'; import { Presence } from '../lib/presence'; @@ -9,6 +10,12 @@ type UserPresenceProviderProps = { }; const UserPresenceProvider = ({ children }: UserPresenceProviderProps): ReactElement => { + const usePresenceDisabled = useSetting('Presence_broadcast_disabled'); + + useEffect(() => { + Presence.setStatus(usePresenceDisabled ? 'disabled' : 'enabled'); + }, [usePresenceDisabled]); + return ( { const sidebarViewMode = useUserPreference('sidebarViewMode'); const sidebarHideAvatar = !useUserPreference('sidebarDisplayAvatar'); const { isMobile, sidebar } = useLayout(); + const [bannerDismissed, setBannerDismissed] = useSessionStorage('presence_cap_notifier', false); + const presenceDisabled = useSetting('Presence_broadcast_disabled'); + + const sideBarBackground = css` + background-color: ${Palette.surface['surface-tint']}; + `; const sideBarStyle = css` position: relative; @@ -23,7 +31,6 @@ const Sidebar = () => { height: 100%; user-select: none; transition: transform 0.3s; - background-color: var(--sidebar-background); &.opened { box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 15px 1px; @@ -89,11 +96,13 @@ const Sidebar = () => { 'rcx-sidebar--main', `rcx-sidebar rcx-sidebar--${sidebarViewMode}`, sidebarHideAvatar && 'rcx-sidebar--hide-avatar', + sideBarBackground, ].filter(Boolean)} role='navigation' data-qa-opened={sidebar.isCollapsed ? 'false' : 'true'} > + {presenceDisabled && !bannerDismissed && setBannerDismissed(true)} />} diff --git a/apps/meteor/client/sidebar/header/UserDropdown.tsx b/apps/meteor/client/sidebar/header/UserDropdown.tsx index 1cbdebc2a945..2347e76038de 100644 --- a/apps/meteor/client/sidebar/header/UserDropdown.tsx +++ b/apps/meteor/client/sidebar/header/UserDropdown.tsx @@ -12,7 +12,8 @@ import { RadioButton, } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useLayout, useRoute, useLogout, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useLayout, useRoute, useLogout, useSetting, useTranslation, useSetModal } from '@rocket.chat/ui-contexts'; import { useThemeMode } from '@rocket.chat/ui-theming/src/hooks/useThemeMode'; import type { ReactElement } from 'react'; import React from 'react'; @@ -20,6 +21,7 @@ import React from 'react'; import { AccountBox } from '../../../app/ui-utils/client'; import { userStatus } from '../../../app/user-status/client'; import { callbacks } from '../../../lib/callbacks'; +import GenericModal from '../../components/GenericModal'; import MarkdownText from '../../components/MarkdownText'; import { UserStatus } from '../../components/UserStatus'; import UserAvatar from '../../components/avatar/UserAvatar'; @@ -38,7 +40,7 @@ const setStatus = (status: typeof userStatus.list['']): void => { const translateStatusName = (t: ReturnType, status: typeof userStatus.list['']): string => { if (isDefaultStatusName(status.name, status.id)) { - return t(status.name); + return t(status.name as TranslationKey); } return status.name; @@ -52,9 +54,18 @@ type UserDropdownProps = { const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { const t = useTranslation(); const accountRoute = useRoute('account-index'); + const userStatusRoute = useRoute('user-status'); const logout = useLogout(); const { isMobile } = useLayout(); + const presenceDisabled = useSetting('Presence_broadcast_disabled'); + const setModal = useSetModal(); + const closeModal = useMutableCallback(() => setModal()); + const handleGoToSettings = useMutableCallback(() => { + userStatusRoute.push({}); + closeModal(); + onClose(); + }); const [selectedTheme, setTheme] = useThemeMode(); const { username, avatarETag, status, statusText } = user; @@ -103,7 +114,7 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { @@ -111,6 +122,32 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { {t('Status')} + {presenceDisabled && ( + + {t('User_status_disabled')} + + setModal( + , + ) + } + > + {t('Learn_more')} + + + )} {Object.values(userStatus.list) .filter(filterInvisibleStatus) .map((status, i) => { @@ -120,6 +157,7 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => { return ( ); })} - + {t('Theme')} diff --git a/apps/meteor/client/sidebar/header/index.tsx b/apps/meteor/client/sidebar/header/index.tsx index 87292356a269..79790341a24e 100644 --- a/apps/meteor/client/sidebar/header/index.tsx +++ b/apps/meteor/client/sidebar/header/index.tsx @@ -27,7 +27,7 @@ const HeaderWithData = (): ReactElement => { - + {user && ( <> diff --git a/apps/meteor/client/sidebar/search/SearchList.tsx b/apps/meteor/client/sidebar/search/SearchList.tsx index 733835d221f5..807a7efe06ec 100644 --- a/apps/meteor/client/sidebar/search/SearchList.tsx +++ b/apps/meteor/client/sidebar/search/SearchList.tsx @@ -301,20 +301,21 @@ const SearchList = forwardRef(function SearchList({ onClose }: SearchListProps, top: 0; `} ref={ref} + role='search' > - + } /> diff --git a/apps/meteor/client/sidebar/sections/StatusDisabledSection.tsx b/apps/meteor/client/sidebar/sections/StatusDisabledSection.tsx new file mode 100644 index 000000000000..3103a176e622 --- /dev/null +++ b/apps/meteor/client/sidebar/sections/StatusDisabledSection.tsx @@ -0,0 +1,42 @@ +import { SidebarBanner } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useRoute, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import GenericModal from '../../components/GenericModal'; + +const StatusDisabledSection = ({ onDismiss }: { onDismiss: () => void }) => { + const t = useTranslation(); + const userStatusRoute = useRoute('user-status'); + const setModal = useSetModal(); + const closeModal = useMutableCallback(() => setModal()); + const handleGoToSettings = useMutableCallback(() => { + userStatusRoute.push({}); + closeModal(); + }); + + return ( + + setModal( + , + ) + } + /> + ); +}; + +export default StatusDisabledSection; diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx index 0411c082f4da..497e0143bfaa 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelCallToggleReady.tsx @@ -47,22 +47,16 @@ export const OmnichannelCallToggleReady = ({ ...props }): ReactElement => { return registered ? 'phone' : 'phone-disabled'; }; - const getColor = (): 'warning' | 'success' | undefined => { - if (networkStatus === 'offline') { - return 'warning'; - } - return registered ? 'success' : undefined; - }; - return ( ); diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx index 9cf1a02411fa..bf5867df254c 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx @@ -25,7 +25,7 @@ export const OmnichannelLivechatToggle = (props: Omit diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index 2e2cb1d5d11f..c03f1e6cd464 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -54,49 +54,53 @@ Meteor.startup(() => { observable = Subscriptions.find().observe({ changed: async (sub: ISubscription) => { - if (!sub.encrypted && !sub.E2EKey) { - e2e.removeInstanceByRoomId(sub.rid); - return; - } - - const e2eRoom = await e2e.getInstanceByRoomId(sub.rid); - if (!e2eRoom) { - return; - } - - if (sub.E2ESuggestedKey) { - if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { - e2e.acceptSuggestedKey(sub.rid); - } else { - console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); - e2e.rejectSuggestedKey(sub.rid); + Meteor.defer(async () => { + if (!sub.encrypted && !sub.E2EKey) { + e2e.removeInstanceByRoomId(sub.rid); + return; } - } - sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + const e2eRoom = await e2e.getInstanceByRoomId(sub.rid); + if (!e2eRoom) { + return; + } + + if (sub.E2ESuggestedKey) { + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + e2e.acceptSuggestedKey(sub.rid); + } else { + console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + e2e.rejectSuggestedKey(sub.rid); + } + } + + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); - // Cover private groups and direct messages - if (!e2eRoom.isSupportedRoomType(sub.t)) { - e2eRoom.disable(); - return; - } + // Cover private groups and direct messages + if (!e2eRoom.isSupportedRoomType(sub.t)) { + e2eRoom.disable(); + return; + } - if (sub.E2EKey && e2eRoom.isWaitingKeys()) { - e2eRoom.keyReceived(); - return; - } + if (sub.E2EKey && e2eRoom.isWaitingKeys()) { + e2eRoom.keyReceived(); + return; + } - if (!e2eRoom.isReady()) { - return; - } + if (!e2eRoom.isReady()) { + return; + } - e2eRoom.decryptSubscription(); + e2eRoom.decryptSubscription(); + }); }, added: async (sub: ISubscription) => { - if (!sub.encrypted && !sub.E2EKey) { - return; - } - return e2e.getInstanceByRoomId(sub.rid); + Meteor.defer(async () => { + if (!sub.encrypted && !sub.E2EKey) { + return; + } + return e2e.getInstanceByRoomId(sub.rid); + }); }, removed: (sub: ISubscription) => { e2e.removeInstanceByRoomId(sub.rid); diff --git a/apps/meteor/client/startup/renderMessage/autolinker.ts b/apps/meteor/client/startup/renderMessage/autolinker.ts deleted file mode 100644 index 2429b0a1a9e7..000000000000 --- a/apps/meteor/client/startup/renderMessage/autolinker.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { settings } from '../../../app/settings/client'; -import { callbacks } from '../../../lib/callbacks'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const isEnabled = settings.get('AutoLinker') === true; - - if (!isEnabled) { - callbacks.remove('renderMessage', 'autolinker'); - return; - } - - const options = { - stripPrefix: settings.get('AutoLinker_StripPrefix'), - urls: { - schemeMatches: settings.get('AutoLinker_Urls_Scheme'), - wwwMatches: settings.get('AutoLinker_Urls_www'), - tldMatches: settings.get('AutoLinker_Urls_TLD'), - }, - email: settings.get('AutoLinker_Email'), - phone: settings.get('AutoLinker_Phone'), - }; - - import('../../../app/autolinker/client').then(({ createAutolinkerMessageRenderer }) => { - const renderMessage = createAutolinkerMessageRenderer(options); - callbacks.remove('renderMessage', 'autolinker'); - callbacks.add('renderMessage', renderMessage, callbacks.priority.MEDIUM, 'autolinker'); - }); - }); -}); diff --git a/apps/meteor/client/startup/renderMessage/index.ts b/apps/meteor/client/startup/renderMessage/index.ts index 09eeca80dd1e..ec329d84b530 100644 --- a/apps/meteor/client/startup/renderMessage/index.ts +++ b/apps/meteor/client/startup/renderMessage/index.ts @@ -1,9 +1,6 @@ -import './autolinker'; import './autotranslate'; import './emoji'; import './hexcolor'; import './highlightWords'; -import './issuelink'; import './katex'; -import './markdown'; import './mentionsMessage'; diff --git a/apps/meteor/client/startup/renderMessage/issuelink.ts b/apps/meteor/client/startup/renderMessage/issuelink.ts deleted file mode 100644 index ba96e8253397..000000000000 --- a/apps/meteor/client/startup/renderMessage/issuelink.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { settings } from '../../../app/settings/client'; -import { callbacks } from '../../../lib/callbacks'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const isEnabled = settings.get('IssueLinks_Enabled'); - - if (!isEnabled) { - callbacks.remove('renderMessage', 'issuelink'); - return; - } - - const options = { - template: settings.get('IssueLinks_Template'), - }; - - import('../../../app/issuelinks/client').then(({ createIssueLinksMessageRenderer }) => { - const renderMessage = createIssueLinksMessageRenderer(options); - callbacks.remove('renderMessage', 'issuelink'); - callbacks.add('renderMessage', renderMessage, callbacks.priority.MEDIUM, 'issuelink'); - }); - }); -}); diff --git a/apps/meteor/client/startup/renderMessage/markdown.ts b/apps/meteor/client/startup/renderMessage/markdown.ts deleted file mode 100644 index d13e4f3b9cda..000000000000 --- a/apps/meteor/client/startup/renderMessage/markdown.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { settings } from '../../../app/settings/client'; -import { callbacks } from '../../../lib/callbacks'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const options = { - parser: settings.get('Markdown_Parser'), - supportSchemesForLink: settings.get('Markdown_SupportSchemesForLink'), - headers: settings.get('Markdown_Headers'), - rootUrl: Meteor.absoluteUrl(), - marked: { - gfm: settings.get('Markdown_Marked_GFM'), - tables: settings.get('Markdown_Marked_Tables'), - breaks: settings.get('Markdown_Marked_Breaks'), - pedantic: settings.get('Markdown_Marked_Pedantic'), - smartLists: settings.get('Markdown_Marked_SmartLists'), - smartypants: settings.get('Markdown_Marked_Smartypants'), - }, - }; - - import('../../../app/markdown/client').then(({ createMarkdownMessageRenderer }) => { - const renderMessage = createMarkdownMessageRenderer(options); - callbacks.remove('renderMessage', 'markdown'); - callbacks.add('renderMessage', renderMessage, callbacks.priority.HIGH, 'markdown'); - }); - }); -}); diff --git a/apps/meteor/client/startup/renderNotification/markdown.ts b/apps/meteor/client/startup/renderNotification/markdown.ts index 8c83f75776d0..1d8ca18164e8 100644 --- a/apps/meteor/client/startup/renderNotification/markdown.ts +++ b/apps/meteor/client/startup/renderNotification/markdown.ts @@ -1,15 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../../app/settings/client'; import { callbacks } from '../../../lib/callbacks'; Meteor.startup(() => { - const options = { - supportSchemesForLink: settings.get('Markdown_SupportSchemesForLink'), - }; - import('../../../app/markdown/client').then(({ createMarkdownNotificationRenderer }) => { - const renderNotification = createMarkdownNotificationRenderer(options); + const renderNotification = createMarkdownNotificationRenderer(); callbacks.remove('renderNotification', 'filter-markdown'); callbacks.add('renderNotification', renderNotification, callbacks.priority.HIGH, 'filter-markdown'); }); diff --git a/apps/meteor/client/startup/routes.tsx b/apps/meteor/client/startup/routes.tsx index 9a12675ca871..334c787b7d55 100644 --- a/apps/meteor/client/startup/routes.tsx +++ b/apps/meteor/client/startup/routes.tsx @@ -235,17 +235,6 @@ FlowRouter.route('/reset-password/:token', { }, }); -FlowRouter.route('/snippet/:snippetId/:snippetName', { - name: 'snippetView', - action() { - appLayout.render( - - - , - ); - }, -}); - FlowRouter.route('/oauth/authorize', { name: 'oauth/authorize', action() { diff --git a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx index b4228b1f86b4..115b32ebc57b 100644 --- a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx +++ b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx @@ -31,7 +31,6 @@ type CurrentData = { pushNotifications: string; enableAutoAway: boolean; highlights: string; - messageViewMode: number; hideUsernames: boolean; hideRoles: boolean; displayAvatars: boolean; diff --git a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx index b53249b40a95..7c2410deb9fb 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesGlobalSection.tsx @@ -1,5 +1,5 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { Accordion, Field, FieldGroup, MultiSelect, ToggleSwitch, Callout } from '@rocket.chat/fuselage'; +import { Accordion, Field, FieldGroup, MultiSelect } from '@rocket.chat/fuselage'; import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; @@ -11,7 +11,6 @@ const PreferencesGlobalSection = ({ onChange, commitRef, ...props }: FormSection const t = useTranslation(); const userDontAskAgainList = useUserPreference<{ action: string; label: string }[]>('dontAskAgainList'); - const userLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate'); const options = useMemo( () => (userDontAskAgainList || []).map(({ action, label }) => [action, label]) as SelectOption[], @@ -23,17 +22,15 @@ const PreferencesGlobalSection = ({ onChange, commitRef, ...props }: FormSection const { values, handlers, commit } = useForm( { dontAskAgainList: selectedOptions, - useLegacyMessageTemplate: userLegacyMessageTemplate, }, onChange, ); - const { dontAskAgainList, useLegacyMessageTemplate } = values as { + const { dontAskAgainList } = values as { dontAskAgainList: string[]; - useLegacyMessageTemplate: boolean; }; - const { handleDontAskAgainList, handleUseLegacyMessageTemplate } = handlers; + const { handleDontAskAgainList } = handlers; commitRef.current.global = commit; @@ -51,13 +48,6 @@ const PreferencesGlobalSection = ({ onChange, commitRef, ...props }: FormSection /> - - {t('Use_Legacy_Message_Template')} - - - - - {t('This_is_a_deprecated_feature_alert')} ); diff --git a/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx index 4d7358bba32b..a9b2996e3195 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx @@ -21,7 +21,6 @@ type Values = { displayAvatars: boolean; clockMode: 0 | 1 | 2; sendOnEnter: 'normal' | 'alternative' | 'desktop'; - messageViewMode: 0 | 1 | 2; }; const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSectionProps): ReactElement => { @@ -42,7 +41,6 @@ const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSecti hideFlexTab: useUserPreference('hideFlexTab'), clockMode: useUserPreference('clockMode') ?? 0, sendOnEnter: useUserPreference('sendOnEnter'), - messageViewMode: useUserPreference('messageViewMode'), displayAvatars: useUserPreference('displayAvatars'), }; @@ -62,7 +60,6 @@ const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSecti displayAvatars, clockMode, sendOnEnter, - messageViewMode, } = values as Values; const { @@ -79,7 +76,6 @@ const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSecti handleDisplayAvatars, handleClockMode, handleSendOnEnter, - handleMessageViewMode, } = handlers; const alsoSendThreadMessageToChannelOptions = useMemo( @@ -109,15 +105,6 @@ const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSecti [t], ); - const messageViewModeOptions = useMemo( - (): SelectOption[] => [ - [0 as any, t('Normal')], // TO DO: update SelectOption type to accept number as first item - [1, t('Cozy')], - [2, t('Compact')], - ], - [t], - ); - commitRef.current.messages = commit; // TODO: Weird behaviour when saving clock mode, and then changing it. @@ -275,18 +262,6 @@ const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSecti ), [handleSendOnEnter, sendOnEnter, sendOnEnterOptions, t], )} - {useMemo( - () => ( - - {t('View_mode')} - - } + render={({ field }): ReactElement => ( + + + ); +}; + +export default FileUploadAction; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/actions/ShareLocationAction.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/actions/ShareLocationAction.tsx new file mode 100644 index 000000000000..0867c1dcbc5a --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/actions/ShareLocationAction.tsx @@ -0,0 +1,33 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; +import { useSetting, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import ShareLocationModal from '../../../../../ShareLocation/ShareLocationModal'; + +const ShareLocationAction = ({ room, tmid }: { room: IRoom; tmid?: string }) => { + const t = useTranslation(); + const setModal = useSetModal(); + + const isMapViewEnabled = useSetting('MapView_Enabled') === true; + const isGeolocationCurrentPositionSupported = Boolean(navigator.geolocation?.getCurrentPosition); + const googleMapsApiKey = useSetting('MapView_GMapsAPIKey') as string; + const canGetGeolocation = isMapViewEnabled && isGeolocationCurrentPositionSupported && googleMapsApiKey && googleMapsApiKey.length; + + const handleShareLocation = () => setModal( setModal(null)} />); + + const allowGeolocation = room && canGetGeolocation && !isRoomFederated(room); + + return ( + <> + {t('Share')} + + + ); +}; + +export default ShareLocationAction; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/actions/VideoMessageAction.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/actions/VideoMessageAction.tsx new file mode 100644 index 000000000000..893de8a05b83 --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/actions/VideoMessageAction.tsx @@ -0,0 +1,53 @@ +import { MessageComposerAction } from '@rocket.chat/ui-composer'; +import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import type { ChatAPI } from '../../../../../../../lib/chats/ChatAPI'; +import { useChat } from '../../../../../contexts/ChatContext'; + +type VideoMessageActionProps = { + isRecording: boolean; + canSend: boolean; + chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React +}; + +const VideoMessageAction = ({ chatContext, isRecording, canSend }: VideoMessageActionProps) => { + const t = useTranslation(); + const fileUploadEnabled = useSetting('FileUpload_Enabled'); + const messageVideoRecorderEnabled = useSetting('Message_VideoRecorderEnabled'); + const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; + const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; + + const chat = useChat() ?? chatContext; + + const handleOpenVideoMessage = () => { + if (!chat?.composer?.recordingVideo.get()) { + chat?.composer?.setRecordingVideo(true); + } + }; + + const enableVideoMessage = + navigator.mediaDevices && + window.MediaRecorder && + fileUploadEnabled && + messageVideoRecorderEnabled && + (!fileUploadMediaTypeBlackList || !fileUploadMediaTypeBlackList.match(/video\/webm|video\/\*/i)) && + (!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/video\/webm|video\/\*/i)) && + window.MediaRecorder.isTypeSupported('video/webm; codecs=vp8,opus'); + + if (!enableVideoMessage) { + return null; + } + + return ( + + ); +}; + +export default VideoMessageAction; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/actions/WebdavAction.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/actions/WebdavAction.tsx new file mode 100644 index 000000000000..6b07cb26de4d --- /dev/null +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/actions/WebdavAction.tsx @@ -0,0 +1,56 @@ +import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; +import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; +import { useTranslation, useSetting, useSetModal } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { WebdavAccounts } from '../../../../../../../../app/models/client'; +import { useReactiveValue } from '../../../../../../../hooks/useReactiveValue'; +import type { ChatAPI } from '../../../../../../../lib/chats/ChatAPI'; +import { useChat } from '../../../../../contexts/ChatContext'; +import AddWebdavAccountModal from '../../../../../webdav/AddWebdavAccountModal'; +import WebdavFilePickerModal from '../../../../../webdav/WebdavFilePickerModal'; + +const getWebdavAccounts = (): IWebdavAccountIntegration[] => WebdavAccounts.find().fetch(); + +const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { + const t = useTranslation(); + const setModal = useSetModal(); + const webDavAccounts = useReactiveValue(getWebdavAccounts); + + const webDavEnabled = useSetting('Webdav_Integration_Enabled'); + + const handleCreateWebDav = () => setModal( setModal(null)} onConfirm={() => setModal(null)} />); + + const chat = useChat() ?? chatContext; + + const handleUpload = async (file: File, description?: string) => + chat?.uploads.send(file, { + description, + }); + + const handleOpenWebdav = (account: IWebdavAccountIntegration) => + setModal( setModal(null)} />); + + return ( + <> + + {webDavEnabled && + webDavAccounts.length > 0 && + webDavAccounts.map((account) => ( + + ))} + + ); +}; + +export default WebdavAction; diff --git a/apps/meteor/client/views/room/components/body/useRoomMessageContext.ts b/apps/meteor/client/views/room/components/body/useRoomMessageContext.ts index a5f01705adf4..77c7aba178be 100644 --- a/apps/meteor/client/views/room/components/body/useRoomMessageContext.ts +++ b/apps/meteor/client/views/room/components/body/useRoomMessageContext.ts @@ -16,7 +16,6 @@ export const useRoomMessageContext = (room: IRoom) => { const { isMobile: mobile } = useLayout(); const translateLanguage = useReactiveValue(useCallback(() => AutoTranslate.getLanguage(rid), [rid])); const autoImageLoad = useUserPreference('autoImageLoad'); - const useLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate'); const saveMobileBandwidth = useUserPreference('saveMobileBandwidth'); const collapseMediaByDefault = useUserPreference('collapseMediaByDefault'); const hasPermissionDeleteMessage = usePermission('delete-message', rid); @@ -28,9 +27,7 @@ export const useRoomMessageContext = (room: IRoom) => { const autoTranslateEnabled = useSetting('AutoTranslate_Enabled'); const allowEditing = useSetting('Message_AllowEditing'); const blockEditInMinutes = useSetting('Message_AllowEditing_BlockEditInMinutes'); - const showEditedStatus = useSetting('Message_ShowEditedStatus'); const embed = useSetting('API_Embed'); - const embedDisabledFor = useSetting('API_EmbedDisabledFor'); const groupingPeriod = useSetting('Message_GroupingPeriod') as number; return useMemo( @@ -43,7 +40,6 @@ export const useRoomMessageContext = (room: IRoom) => { subscription, translateLanguage, autoImageLoad, - useLegacyMessageTemplate, saveMobileBandwidth: mobile && saveMobileBandwidth, collapseMediaByDefault, showreply: true, @@ -56,9 +52,7 @@ export const useRoomMessageContext = (room: IRoom) => { AutoTranslate_Enabled: autoTranslateEnabled, Message_AllowEditing: allowEditing, Message_AllowEditing_BlockEditInMinutes: blockEditInMinutes, - Message_ShowEditedStatus: showEditedStatus, API_Embed: embed, - API_EmbedDisabledFor: embedDisabledFor, Message_GroupingPeriod: groupingPeriod * 1000, }), [ @@ -70,7 +64,6 @@ export const useRoomMessageContext = (room: IRoom) => { collapseMediaByDefault, displayRoles, embed, - embedDisabledFor, groupingPeriod, hasPermissionDeleteMessage, hasPermissionDeleteOwnMessage, @@ -79,11 +72,9 @@ export const useRoomMessageContext = (room: IRoom) => { rid, room, saveMobileBandwidth, - showEditedStatus, subscription, translateLanguage, uid, - useLegacyMessageTemplate, useRealName, user, ], diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.js b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.js index 9b426f3bb5fe..187f74d95f4a 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.js +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/hooks/useMessageDeletionIsAllowed.js @@ -8,6 +8,7 @@ export const useMessageDeletionIsAllowed = (rid, uid) => { const deletionIsEnabled = useSetting('Message_AllowDeleting'); const userHasPermissonToDeleteAny = usePermission('delete-message', rid); const userHasPermissonToDeleteOwn = usePermission('delete-own-message'); + const bypassBlockTimeLimit = usePermission('bypass-time-limit-edit-and-delete'); const blockDeleteInMinutes = useSetting('Message_AllowDeleting_BlockDeleteInMinutes'); const isDeletionAllowed = (() => { @@ -24,7 +25,7 @@ export const useMessageDeletionIsAllowed = (rid, uid) => { } const checkTimeframe = - blockDeleteInMinutes !== 0 + !bypassBlockTimeLimit && blockDeleteInMinutes !== 0 ? ({ ts }) => { if (!ts) { return false; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx b/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx index 170be18dd0b6..32e5aecdd3c7 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx @@ -62,7 +62,13 @@ const Thread: VFC = ({ tmid }) => { }; const handleToggleFollowing = () => { - toggleFollowingMutation.mutate({ tmid, follow: !following }); + const rid = mainMessageQueryResult.data?.rid; + + if (!rid) { + return; + } + + toggleFollowingMutation.mutate({ rid, tmid, follow: !following }); }; const handleClose = () => { diff --git a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx index fe2a56b4d319..9e9605756a2e 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx @@ -107,9 +107,9 @@ const ThreadList: VFC = () => { const goToThread = useGoToThread({ replace: true }); const handleThreadClick = useCallback( (tmid: IMessage['_id']) => { - goToThread(tmid); + goToThread({ rid, tmid }); }, - [goToThread], + [rid, goToThread], ); return ( diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx index b2d0f1920374..f2d61153708a 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -15,7 +15,6 @@ import { useFileUploadDropTarget } from '../../../components/body/useFileUploadD import { useChat } from '../../../contexts/ChatContext'; import { useRoom, useRoomSubscription } from '../../../contexts/RoomContext'; import { useTabBarClose } from '../../../contexts/ToolboxContext'; -import LegacyThreadMessageList from './LegacyThreadMessageList'; import ThreadMessageList from './ThreadMessageList'; type ThreadChatProps = { @@ -105,18 +104,12 @@ const ThreadChat: VFC = ({ mainMessage }) => { const sendToChannelID = useUniqueId(); const t = useTranslation(); - const useLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate') ?? false; - return ( - {useLegacyMessageTemplate ? ( - - ) : ( - - )} + , 'mutationFn'>, ): UseMutationResult => { - const room = useRoom(); const followMessage = useEndpoint('POST', '/v1/chat.followMessage'); const unfollowMessage = useEndpoint('POST', '/v1/chat.unfollowMessage'); @@ -33,7 +31,7 @@ export const useToggleFollowingThreadMutation = ( { ...options, onSuccess: async (data, variables, context) => { - await queryClient.invalidateQueries(['rooms', room._id, 'threads']); + await queryClient.invalidateQueries(['rooms', variables.rid, 'threads']); return options?.onSuccess?.(data, variables, context); }, }, diff --git a/apps/meteor/client/views/room/hooks/useGoToThread.ts b/apps/meteor/client/views/room/hooks/useGoToThread.ts index 61c3251f45d5..29a88ef61db1 100644 --- a/apps/meteor/client/views/room/hooks/useGoToThread.ts +++ b/apps/meteor/client/views/room/hooks/useGoToThread.ts @@ -1,14 +1,12 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useCurrentRoute, useRoute } from '@rocket.chat/ui-contexts'; -import { useRoom } from '../contexts/RoomContext'; - -export const useGoToThread = ({ replace = false }: { replace?: boolean } = {}): (( - tmid: IMessage['_id'], - jump?: IMessage['_id'], -) => void) => { - const room = useRoom(); +export const useGoToThread = ({ replace = false }: { replace?: boolean } = {}): ((params: { + rid: IRoom['_id']; + tmid: IMessage['_id']; + jump?: IMessage['_id']; +}) => void) => { const [routeName, params, queryParams] = useCurrentRoute(); if (!routeName) { @@ -19,7 +17,7 @@ export const useGoToThread = ({ replace = false }: { replace?: boolean } = {}): const go = replace ? roomRoute.replace : roomRoute.push; // TODO: remove params recycling - return useMutableCallback((tmid, jump) => { - go({ rid: room._id, ...params, tab: 'thread', context: tmid }, { ...queryParams, ...(jump && { jump }) }); + return useMutableCallback(({ rid, tmid, jump }) => { + go({ rid, ...params, tab: 'thread', context: tmid }, { ...queryParams, ...(jump && { jump }) }); }); }; diff --git a/apps/meteor/client/views/room/hooks/useGoToThreadList.ts b/apps/meteor/client/views/room/hooks/useGoToThreadList.ts index 4dd4071dabf2..b0677118d035 100644 --- a/apps/meteor/client/views/room/hooks/useGoToThreadList.ts +++ b/apps/meteor/client/views/room/hooks/useGoToThreadList.ts @@ -5,7 +5,7 @@ import { useRoom } from '../contexts/RoomContext'; export const useGoToThreadList = ({ replace = false }: { replace?: boolean } = {}): (() => void) => { const room = useRoom(); - const [routeName, { context, ...params } = { context: '' }] = useCurrentRoute(); + const [routeName, { context, ...params } = { context: '' }, queryParams] = useCurrentRoute(); if (!routeName) { throw new Error('Route name is not defined'); @@ -14,6 +14,6 @@ export const useGoToThreadList = ({ replace = false }: { replace?: boolean } = { const roomRoute = useRoute(routeName); const go = replace ? roomRoute.replace : roomRoute.push; return useMutableCallback(() => { - go({ rid: room._id, ...params, tab: 'thread' }); + go({ rid: room._id, ...params, tab: 'thread' }, queryParams); }); }; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index 5b7be9e36432..13d59c85daa4 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -1,6 +1,7 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { useMemo } from 'react'; +import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout'; import type { Action } from '../../../hooks/useActionSpread'; import { useBlockUserAction } from './actions/useBlockUserAction'; import { useCallAction } from './actions/useCallAction'; @@ -28,10 +29,11 @@ export const useUserInfoActions = ( const muteUserOption = useMuteUserAction(user, rid); const removeUserOption = useRemoveUserAction(user, rid, reload); const callOption = useCallAction(user); + const isLayoutEmbedded = useEmbeddedLayout(); return useMemo( () => ({ - ...(openDirectMessageOption && { openDirectMessage: openDirectMessageOption }), + ...(openDirectMessageOption && !isLayoutEmbedded && { openDirectMessage: openDirectMessageOption }), ...(callOption && { call: callOption }), ...(changeOwnerOption && { changeOwner: changeOwnerOption }), ...(changeLeaderOption && { changeLeader: changeLeaderOption }), @@ -51,6 +53,7 @@ export const useUserInfoActions = ( removeUserOption, callOption, blockUserOption, + isLayoutEmbedded, ], ); }; diff --git a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx index 7ff193c8dc2a..162c8dd3071b 100644 --- a/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx +++ b/apps/meteor/client/views/room/modals/ReactionListModal/ReactionListModal.tsx @@ -5,7 +5,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import GenericModal from '../../../../components/GenericModal'; -import { useUserCard } from '../../../../hooks/useUserCard'; +import { useChat } from '../../contexts/ChatContext'; import Reactions from './Reactions'; type ReactionListProps = { @@ -16,7 +16,7 @@ type ReactionListProps = { const ReactionList = ({ reactions, onClose }: ReactionListProps): ReactElement => { const t = useTranslation(); - const { open: openUserCard } = useUserCard(); + const chat = useChat(); const onClick = useMutableCallback((e) => { const { username } = e.currentTarget.dataset; @@ -25,7 +25,7 @@ const ReactionList = ({ reactions, onClose }: ReactionListProps): ReactElement = return; } - openUserCard(username)(e); + chat?.userCard.open(username)(e); }); return ( diff --git a/apps/meteor/client/views/room/providers/ChatProvider.tsx b/apps/meteor/client/views/room/providers/ChatProvider.tsx index 6191b4ae55c9..5b17e3e881a6 100644 --- a/apps/meteor/client/views/room/providers/ChatProvider.tsx +++ b/apps/meteor/client/views/room/providers/ChatProvider.tsx @@ -1,9 +1,9 @@ import type { ReactElement, ReactNode } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import React from 'react'; -import { ChatMessages } from '../../../../app/ui/client/lib/ChatMessages'; import { ChatContext } from '../contexts/ChatContext'; import { useRoom } from '../contexts/RoomContext'; +import { useChatMessagesInstance } from './hooks/useChatMessagesInstance'; type ChatProviderProps = { children: ReactNode; @@ -12,17 +12,7 @@ type ChatProviderProps = { const ChatProvider = ({ children, tmid }: ChatProviderProps): ReactElement => { const { _id: rid } = useRoom(); - - const chatMessages = useMemo(() => ChatMessages.hold({ rid, tmid }), [rid, tmid]); - - useEffect( - () => (): void => { - ChatMessages.release({ rid, tmid }); - }, - [rid, tmid], - ); - - const value = useMemo(() => chatMessages, [chatMessages]); + const value = useChatMessagesInstance({ rid, tmid }); return {children}; }; diff --git a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx index da814af11939..7021005ffaf4 100644 --- a/apps/meteor/client/views/room/providers/ToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/ToolboxProvider.tsx @@ -21,7 +21,7 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom }); const { listen, actions } = useToolboxActions(room); - const [routeName, params] = useCurrentRoute(); + const [routeName, params, queryStringParams] = useCurrentRoute(); const router = useRoute(routeName || ''); const tab = params?.tab; @@ -33,11 +33,14 @@ const ToolboxProvider = ({ children, room }: { children: ReactNode; room: IRoom ); const close = useMutableCallback(() => { - router.push({ - ...params, - tab: '', - context: '', - }); + router.push( + { + ...params, + tab: '', + context: '', + }, + queryStringParams, + ); }); const open = useMutableCallback((actionId: string, context?: string) => { diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts new file mode 100644 index 000000000000..7b15a507ac49 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts @@ -0,0 +1,18 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; + +import { ChatMessages } from '../../../../../app/ui/client/lib/ChatMessages'; +import type { ChatAPI } from '../../../../lib/chats/ChatAPI'; +import { useInstance } from './useInstance'; +import { useUserCard } from './useUserCard'; + +export function useChatMessagesInstance({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): ChatAPI { + const chatMessages = useInstance(() => { + const instance = new ChatMessages({ rid, tmid }); + + return [instance, () => instance.release()]; + }, [rid, tmid]); + + chatMessages.userCard = useUserCard(); + + return chatMessages; +} diff --git a/apps/meteor/client/views/room/providers/hooks/useDepsMatch.ts b/apps/meteor/client/views/room/providers/hooks/useDepsMatch.ts new file mode 100644 index 000000000000..0fed52b67779 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useDepsMatch.ts @@ -0,0 +1,14 @@ +import { useRef } from 'react'; + +const depsMatch = (a: unknown[], b: unknown[]): boolean => a.every((value, index) => Object.is(value, b[index])); + +export const useDepsMatch = (deps: unknown[]): boolean => { + const prevDepsRef = useRef(deps); + const { current: prevDeps } = prevDepsRef; + + const match = depsMatch(prevDeps, deps); + + prevDepsRef.current = deps; + + return match; +}; diff --git a/apps/meteor/client/views/room/providers/hooks/useInstance.ts b/apps/meteor/client/views/room/providers/hooks/useInstance.ts new file mode 100644 index 000000000000..cbe05b39d039 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useInstance.ts @@ -0,0 +1,23 @@ +import { useRef, useEffect } from 'react'; + +import { useDepsMatch } from './useDepsMatch'; + +export function useInstance(factory: () => [instance: T, release?: () => void], deps: unknown[]): T { + const ref = useRef<[instance: T, release?: () => void]>(); + + useEffect( + () => () => { + ref.current?.[1]?.(); + }, + [], + ); + + const depsMatch = useDepsMatch(deps); + + if (!ref.current || !depsMatch) { + ref.current?.[1]?.(); + ref.current = factory(); + } + + return ref.current[0]; +} diff --git a/apps/meteor/client/hooks/useUserCard.ts b/apps/meteor/client/views/room/providers/hooks/useUserCard.ts similarity index 74% rename from apps/meteor/client/hooks/useUserCard.ts rename to apps/meteor/client/views/room/providers/hooks/useUserCard.ts index 5cc4b1b0a1ca..c4a108864502 100644 --- a/apps/meteor/client/hooks/useUserCard.ts +++ b/apps/meteor/client/views/room/providers/hooks/useUserCard.ts @@ -1,9 +1,9 @@ import type { UIEvent } from 'react'; import { useCallback, useEffect } from 'react'; -import { openUserCard, closeUserCard } from '../../app/ui/client/lib/userCard'; -import { useRoom } from '../views/room/contexts/RoomContext'; -import { useTabBarOpenUserInfo } from '../views/room/contexts/ToolboxContext'; +import { openUserCard, closeUserCard } from '../../../../../app/ui/client/lib/userCard'; +import { useRoom } from '../../contexts/RoomContext'; +import { useTabBarOpenUserInfo } from '../../contexts/ToolboxContext'; export const useUserCard = () => { useEffect(() => { diff --git a/apps/meteor/client/views/root/PageLoading.tsx b/apps/meteor/client/views/root/PageLoading.tsx index bd2b9e1a754a..cb95a546929d 100644 --- a/apps/meteor/client/views/root/PageLoading.tsx +++ b/apps/meteor/client/views/root/PageLoading.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import React from 'react'; const PageLoading: FC = () => ( -
+
diff --git a/apps/meteor/definition/externals/meteor/kadira-flow-router.d.ts b/apps/meteor/definition/externals/meteor/kadira-flow-router.d.ts index b7c5f57a6cd8..eb62ed467a5b 100644 --- a/apps/meteor/definition/externals/meteor/kadira-flow-router.d.ts +++ b/apps/meteor/definition/externals/meteor/kadira-flow-router.d.ts @@ -10,7 +10,7 @@ declare module 'meteor/kadira:flow-router' { }; export type RouteOptions = { - name: string; + name?: string; action?: (this: Route, params?: Record, queryParams?: Record) => void; subscriptions?: (this: Route, params?: Record, queryParams?: Record) => void; triggersEnter?: ((context: Context, redirect: (pathDef: string) => void, stop: () => void) => void)[]; @@ -74,7 +74,7 @@ declare module 'meteor/kadira:flow-router' { parent: Group | undefined; - route(pathDef: string, options?: RouteOptions, group?: Group): Route; + route(pathDef: string, options: RouteOptions, group?: Group): Route; group(options?: GroupOptions): Group; @@ -97,7 +97,7 @@ declare module 'meteor/kadira:flow-router' { class Router { constructor(); - route(pathDef: string, options: RouteOptions, group?: Group): void; + route(pathDef: string, options: RouteOptions, group?: Group): Route; group(options: GroupOptions): Group; @@ -136,6 +136,14 @@ declare module 'meteor/kadira:flow-router' { getQueryParam(key: string): string; watchPathChange(): void; + + _initialized: boolean; + + _routes: Route[]; + + _routesMap: Record; + + _updateCallbacks(): void; } export const FlowRouter: Router & { diff --git a/apps/meteor/definition/externals/meteor/konecty-multiple-instances-status.d.ts b/apps/meteor/definition/externals/meteor/konecty-multiple-instances-status.d.ts deleted file mode 100644 index b64056618399..000000000000 --- a/apps/meteor/definition/externals/meteor/konecty-multiple-instances-status.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'meteor/konecty:multiple-instances-status' { - namespace InstanceStatus { - function id(): string; - - function registerInstance(name: string, instance: Record): void; - } -} diff --git a/apps/meteor/definition/externals/meteor/templating.d.ts b/apps/meteor/definition/externals/meteor/templating.d.ts index 5ec2bb975589..5319c649f839 100644 --- a/apps/meteor/definition/externals/meteor/templating.d.ts +++ b/apps/meteor/definition/externals/meteor/templating.d.ts @@ -38,8 +38,6 @@ declare module 'meteor/templating' { liveStreamBroadcast: Blaze.Template>; liveStreamTab: Blaze.Template>; liveStreamView: Blaze.Template>; - snippetPage: Blaze.Template>; - snippetedMessages: Blaze.Template>; inputAutocomplete: Blaze.Template>; textareaAutocomplete: Blaze.Template>; _autocompleteContainer: Blaze.Template>; @@ -108,7 +106,6 @@ declare module 'meteor/templating' { rc_modal: Blaze.Template>; popout: Blaze.Template>; popover: Blaze.Template>; - audit: Blaze.Template>; messagePopupCannedResponse: Blaze.Template>; instance(): TemplateStatic[TTemplateName] extends Blaze.Template ? I : never; diff --git a/apps/meteor/definition/methods/omnichannel.ts b/apps/meteor/definition/methods/omnichannel.ts index 3a2f1a30c225..fb3540981559 100644 --- a/apps/meteor/definition/methods/omnichannel.ts +++ b/apps/meteor/definition/methods/omnichannel.ts @@ -7,21 +7,6 @@ declare module '@rocket.chat/ui-contexts' { 'livechat:addMonitor': (...args: any[]) => any; 'livechat:closeRoom': (...args: any[]) => any; 'livechat:discardTranscript': (...args: any[]) => any; - - // TODO: chapter day backend - enhance/deprecate - 'livechat:facebook': - | ((...args: [{ action: 'initialState' }]) => { - enabled: boolean; - hasToken: boolean; - }) - | ((...args: [{ action: 'list-pages' }]) => { - name: string; - subscribed: boolean; - id: string; - }[]) - | ((...args: [{ action: 'subscribe' | 'unsubscribe'; page: string }]) => void) - | ((...args: [{ action: 'enable' }]) => { url: string } | undefined) - | ((...args: [{ action: 'disable' }]) => void); 'livechat:getAgentOverviewData': (...args: any[]) => any; 'livechat:getAnalyticsChartData': (...args: any[]) => any; 'livechat:getAnalyticsOverviewData': (...args: any[]) => any; diff --git a/apps/meteor/ee/app/auditing/client/index.css b/apps/meteor/ee/app/auditing/client/index.css deleted file mode 100644 index 4c7c46f93da2..000000000000 --- a/apps/meteor/ee/app/auditing/client/index.css +++ /dev/null @@ -1,95 +0,0 @@ -.main-content { - display: flex; - flex-direction: column; -} - -.rc-audit-empty { - padding: 1.5rem; - - text-align: center; -} - -.rc-select { - margin: 0.5rem 0; -} - -.rc-audit-container { - overflow-y: scroll; - flex: 1 1 auto; -} - -.rc-audit { - display: flex; - flex-direction: column; - flex: 1 1 100%; -} - -.rc-audit-form { - display: flex; - - flex: 0 0 auto; - - padding: var(--header-padding); - - align-items: center; - flex-wrap: wrap; - justify-content: flex-start; -} - -.rc-input__element[type='date']::-webkit-inner-spin-button { - display: none; - -webkit-appearance: none; -} - -.rc-audit-date { - flex-grow: 0; -} - -.rc-audit-filter-td { - width: 200px; -} - -.rc-audit-results-td { - width: 70px; -} - -.rc-audit-created-td { - width: 200px; - - text-align: center; -} - -.rc-audit-user-td { - display: flex; - - min-width: 180px; - - font-size: 16px; - align-items: center; -} - -.rc-audit-user__avatar { - flex: 0 0 auto; - - width: 48px; - height: 48px; -} - -@media (max-width: 501px) { - .rc-audit-results-td, - .rc-audit-user__avatar { - display: none; - } -} - -.rc-audit-user__username { - padding: 0 0.5rem; -} - -.rc-audit .message .read-receipt, -.rc-audit .message .message-actions, -.rc-audit .message .actionLinks, -.rc-audit .message .rc-button-broadcast, -.rc-audit .message .reactions { - display: none; -} diff --git a/apps/meteor/ee/app/auditing/client/index.js b/apps/meteor/ee/app/auditing/client/index.js deleted file mode 100644 index bab254951d84..000000000000 --- a/apps/meteor/ee/app/auditing/client/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import { hasLicense } from '../../license/client'; - -hasLicense('auditing') - .then((enabled) => { - if (!enabled) { - return; - } - require('./templates'); - require('./index.css'); - }) - .catch((error) => { - console.error('Error checking license.', error); - }); diff --git a/apps/meteor/ee/app/auditing/client/templates/audit/audit.html b/apps/meteor/ee/app/auditing/client/templates/audit/audit.html deleted file mode 100644 index 24c8d609151b..000000000000 --- a/apps/meteor/ee/app/auditing/client/templates/audit/audit.html +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/apps/meteor/ee/app/auditing/client/templates/audit/audit.js b/apps/meteor/ee/app/auditing/client/templates/audit/audit.js deleted file mode 100644 index c8895279ecde..000000000000 --- a/apps/meteor/ee/app/auditing/client/templates/audit/audit.js +++ /dev/null @@ -1,82 +0,0 @@ -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { hasAllPermission } from '../../../../../../app/authorization/client'; -import { createMessageContext } from '../../../../../../app/ui-utils/client/lib/messageContext'; -import { call } from '../../utils.js'; - -import './audit.html'; - -const loadMessages = async function ({ rid, users, startDate, endDate = new Date(), msg, type, visitor, agent }) { - this.messages = this.messages || new ReactiveVar([]); - this.loading = this.loading || new ReactiveVar(true); - try { - this.loading.set(true); - const messages = - type === 'l' - ? await call('auditGetOmnichannelMessages', { - rid, - users, - startDate, - endDate, - msg, - type, - visitor, - agent, - }) - : await call('auditGetMessages', { - rid, - users, - startDate, - endDate, - msg, - type, - visitor, - agent, - }); - this.messagesContext.set({ - ...createMessageContext({ rid }), - messages, - }); - } catch (e) { - this.messagesContext.set({}); - } finally { - this.loading.set(false); - } -}; - -Template.audit.helpers({ - isLoading() { - return Template.instance().loading.get(); - }, - messageContext() { - return Template.instance().messagesContext.get(); - }, - hasResults() { - return Template.instance().hasResults.get(); - }, -}); - -Template.audit.onCreated(async function () { - this.messagesContext = new ReactiveVar({}); - this.loading = new ReactiveVar(false); - this.hasResults = new ReactiveVar(false); - - if (!hasAllPermission('can-audit')) { - return FlowRouter.go('/home'); - } - - this.autorun(() => { - const messagesContext = this.messagesContext.get(); - - this.hasResults.set(messagesContext && messagesContext.messages && messagesContext.messages.length > 0); - }); - - this.loadMessages = loadMessages.bind(this); - - const { visitor, agent, users, rid } = this.data; - if (rid || users.length || agent || visitor) { - await this.loadMessages(this.data); - } -}); diff --git a/apps/meteor/ee/app/auditing/client/templates/index.js b/apps/meteor/ee/app/auditing/client/templates/index.js deleted file mode 100644 index 8e4cbfbcf7e6..000000000000 --- a/apps/meteor/ee/app/auditing/client/templates/index.js +++ /dev/null @@ -1 +0,0 @@ -import './audit/audit.js'; diff --git a/apps/meteor/ee/app/auditing/client/utils.js b/apps/meteor/ee/app/auditing/client/utils.js deleted file mode 100644 index 23e2a7377396..000000000000 --- a/apps/meteor/ee/app/auditing/client/utils.js +++ /dev/null @@ -1,22 +0,0 @@ -export { callWithErrorHandling as call } from '../../../../client/lib/utils/callWithErrorHandling'; - -export const convertDate = (date) => { - const [y, m, d] = date.split('-'); - return new Date(y, m - 1, d); -}; - -export const scrollTo = function scrollTo(element, to, duration) { - if (duration <= 0) { - return; - } - const difference = to - element.scrollTop; - const perTick = (difference / duration) * 10; - - setTimeout(function () { - element.scrollTop += perTick; - if (element.scrollTop === to) { - return; - } - scrollTo(element, to, duration - 10); - }, 10); -}; diff --git a/apps/meteor/ee/app/auditing/server/index.ts b/apps/meteor/ee/app/auditing/server/index.ts deleted file mode 100644 index 420f9106a3aa..000000000000 --- a/apps/meteor/ee/app/auditing/server/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint no-multi-spaces: 0 */ -import { Meteor } from 'meteor/meteor'; -import { Permissions } from '@rocket.chat/models'; - -import { onLicense } from '../../license/server'; -import { createOrUpdateProtectedRole } from '../../../../server/lib/roles/createOrUpdateProtectedRole'; - -onLicense('auditing', () => { - require('./methods'); - - Meteor.startup(function () { - const permissions = [ - { _id: 'can-audit', roles: ['admin', 'auditor'] }, - { _id: 'can-audit-log', roles: ['admin', 'auditor-log'] }, - ]; - - const defaultRoles = [ - { name: 'auditor', scope: 'Users' }, - { name: 'auditor-log', scope: 'Users' }, - ] as const; - - permissions.forEach((permission) => { - Permissions.create(permission._id, permission.roles); - }); - - defaultRoles.forEach((role) => createOrUpdateProtectedRole(role.name, role)); - }); -}); diff --git a/apps/meteor/ee/app/license/definitions/ILicense.ts b/apps/meteor/ee/app/license/definition/ILicense.ts similarity index 100% rename from apps/meteor/ee/app/license/definitions/ILicense.ts rename to apps/meteor/ee/app/license/definition/ILicense.ts diff --git a/apps/meteor/ee/app/license/definitions/ILicenseTag.ts b/apps/meteor/ee/app/license/definition/ILicenseTag.ts similarity index 100% rename from apps/meteor/ee/app/license/definitions/ILicenseTag.ts rename to apps/meteor/ee/app/license/definition/ILicenseTag.ts diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index ca8d59cfdbb9..dc41bab376d7 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -5,8 +5,8 @@ import type { BundleFeature } from './bundles'; import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; import decrypt from './decrypt'; import { getTagColor } from './getTagColor'; -import type { ILicense } from '../definitions/ILicense'; -import type { ILicenseTag } from '../definitions/ILicenseTag'; +import type { ILicense } from '../definition/ILicense'; +import type { ILicenseTag } from '../definition/ILicenseTag'; const EnterpriseLicenses = new EventEmitter(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js deleted file mode 100644 index 8a17be5f27fd..000000000000 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js +++ /dev/null @@ -1,16 +0,0 @@ -import { callbacks } from '../../../../../lib/callbacks'; -import { LivechatDepartment } from '../../../../../app/models/server'; - -callbacks.add( - 'livechat.afterRemoveDepartment', - (options = {}) => { - const { department } = options; - if (!department) { - return options; - } - LivechatDepartment.removeDepartmentFromForwardListById(department._id); - return options; - }, - callbacks.priority.HIGH, - 'livechat-after-remove-department', -); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts new file mode 100644 index 000000000000..6f4adbf557d6 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts @@ -0,0 +1,30 @@ +import type { ILivechatAgent, ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; +import { LivechatDepartment } from '@rocket.chat/models'; + +import { callbacks } from '../../../../../lib/callbacks'; +import { cbLogger } from '../lib/logger'; + +const afterRemoveDepartment = async (options: { department: ILivechatDepartmentRecord; agentsId: ILivechatAgent['_id'][] }) => { + cbLogger.debug(`Performing post-department-removal actions in EE: ${options?.department?._id}. Removing department from forward list`); + if (!options || !options.department) { + cbLogger.warn('No department found in options', options); + return options; + } + + const { department } = options; + + cbLogger.debug(`Removing department from forward list: ${department._id}`); + await LivechatDepartment.removeDepartmentFromForwardListById(department._id); + cbLogger.debug(`Removed department from forward list: ${department._id}`); + + cbLogger.debug(`Post-department-removal actions completed in EE: ${department._id}`); + + return options; +}; + +callbacks.add( + 'livechat.afterRemoveDepartment', + (options) => Promise.await(afterRemoveDepartment(options)), + callbacks.priority.HIGH, + 'livechat-after-remove-department', +); diff --git a/apps/meteor/ee/app/models/server/models/LivechatDepartment.js b/apps/meteor/ee/app/models/server/models/LivechatDepartment.js index cfa218c9db51..163ae5e72d1d 100644 --- a/apps/meteor/ee/app/models/server/models/LivechatDepartment.js +++ b/apps/meteor/ee/app/models/server/models/LivechatDepartment.js @@ -55,8 +55,4 @@ overwriteClassOnLicense('livechat-enterprise', LivechatDepartment, { }, }); -LivechatDepartment.prototype.removeDepartmentFromForwardListById = function (_id) { - return this.update({ departmentsAllowedToForward: _id }, { $pull: { departmentsAllowedToForward: _id } }, { multi: true }); -}; - export default LivechatDepartment; diff --git a/apps/meteor/ee/client/audit/AuditLogPage.tsx b/apps/meteor/ee/client/audit/AuditLogPage.tsx deleted file mode 100644 index d0b0552d3c2d..000000000000 --- a/apps/meteor/ee/client/audit/AuditLogPage.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Field } from '@rocket.chat/fuselage'; -import { useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import type { ReactElement } from 'react'; -import React, { useState } from 'react'; - -import Page from '../../../client/components/Page'; -import AuditLogTable from './AuditLogTable'; -import DateRangePicker from './DateRangePicker'; - -const AuditLogPage = (): ReactElement => { - const t = useTranslation(); - - const [dateRange, setDateRange] = useState({ - start: '', - end: '', - }); - - const { start, end } = dateRange; - - const getAudits = useMethod('auditGetAuditions'); - - const result = useQuery(['audits', { start, end }], async () => getAudits({ startDate: new Date(start), endDate: new Date(end) })); - - return ( - - - - - {t('Date')} - - - - - - - - ); -}; - -export default AuditLogPage; diff --git a/apps/meteor/ee/client/audit/AuditLogTable.js b/apps/meteor/ee/client/audit/AuditLogTable.js deleted file mode 100644 index 6932cc164168..000000000000 --- a/apps/meteor/ee/client/audit/AuditLogTable.js +++ /dev/null @@ -1,33 +0,0 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GenericTable from '../../../client/components/GenericTable'; -import { useFormatDate } from '../../../client/hooks/useFormatDate'; -import { useFormatDateAndTime } from '../../../client/hooks/useFormatDateAndTime'; -import UserRow from './UserRow'; - -function AuditLogTable({ data }) { - const t = useTranslation(); - - const formatDateAndTime = useFormatDateAndTime(); - const formatDate = useFormatDate(); - - return ( - - {t('Username')} - {t('Looked_for')} - {t('When')} - {t('Results')} - {t('Filters_applied')} - - } - results={data} - > - {(props) => } - - ); -} - -export default AuditLogTable; diff --git a/apps/meteor/ee/client/audit/AuditPage.js b/apps/meteor/ee/client/audit/AuditPage.js deleted file mode 100644 index 1c4aa31807bc..000000000000 --- a/apps/meteor/ee/client/audit/AuditPage.js +++ /dev/null @@ -1,117 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useRef, useState } from 'react'; - -import { useForm } from '../../../client/hooks/useForm'; -import { AuditPageBase } from './AuditPageBase'; - -const initialValues = { - msg: '', - type: '', - dateRange: { - start: '', - end: '', - }, - visitor: '', - agent: 'all', - rid: '', - users: [], -}; - -const AuditPage = () => { - const t = useTranslation(); - - const { values, handlers } = useForm(initialValues); - const setData = useRef(() => {}); - - const [errors, setErrors] = useState({}); - - const { - msg, - type, - dateRange: { start: startDate, end: endDate }, - visitor, - agent, - users, - rid, - } = values; - - const { handleMsg, handleType, handleVisitor, handleAgent, handleUsers, handleRid, handleDateRange } = handlers; - - const onChangeUsers = useMutableCallback((value, action) => { - if (!action) { - if (users.includes(value)) { - return; - } - return handleUsers([...users, value]); - } - handleUsers(users.filter((current) => current !== value)); - }); - - const apply = useMutableCallback((eventStats) => { - if (!rid && type === '') { - return setErrors({ - rid: t('The_field_is_required', t('Channel_name')), - }); - } - - if (users.length < 2 && type === 'd') { - return setErrors({ - users: t('Select_at_least_two_users'), - }); - } - - if (type === 'l') { - const errors = {}; - - if (agent === '') { - errors.agent = t('The_field_is_required', t('Agent')); - } - - if (visitor === '') { - errors.visitor = t('The_field_is_required', t('Visitor')); - } - - if (errors.visitor || errors.agent) { - return setErrors(errors); - } - } - - setErrors({}); - eventStats(); - setData.current({ - msg, - type, - startDate: new Date(startDate), - endDate: new Date(`${endDate}T23:59:00`), - visitor, - agent, - users, - rid, - }); - }); - - return ( - - ); -}; - -export default AuditPage; diff --git a/apps/meteor/ee/client/audit/AuditPage.stories.tsx b/apps/meteor/ee/client/audit/AuditPage.stories.tsx deleted file mode 100644 index 0cdd8f7c9792..000000000000 --- a/apps/meteor/ee/client/audit/AuditPage.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; - -import AuditPage from './AuditPage'; - -export default { - title: 'Enterprise/Auditing/AuditPage', - component: AuditPage, - parameters: { - layout: 'fullscreen', - controls: { hideNoControlsWarning: true }, - }, -} as ComponentMeta; - -export const Default: ComponentStory = () => ; -Default.storyName = 'AuditPage'; diff --git a/apps/meteor/ee/client/audit/AuditPageBase.js b/apps/meteor/ee/client/audit/AuditPageBase.js deleted file mode 100644 index 992bb07f6079..000000000000 --- a/apps/meteor/ee/client/audit/AuditPageBase.js +++ /dev/null @@ -1,114 +0,0 @@ -import { Box, Field, TextInput, ButtonGroup, Button, Margins, Tabs, Flex } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import Page from '../../../client/components/Page'; -import { useEndpointAction } from '../../../client/hooks/useEndpointAction'; -import DateRangePicker from './DateRangePicker'; -import Result from './Result'; -import ChannelTab from './Tabs/ChannelTab'; -import DirectTab from './Tabs/DirectTab'; -import UsersTab from './Tabs/UsersTab'; -import VisitorsTab from './Tabs/VisitorsTab'; - -// TODO: create more stories for the tabs -export const AuditPageBase = ({ - type, - handleType, - msg, - handleMsg, - handleDateRange, - errors, - rid, - handleRid, - users, - handleUsers, - onChangeUsers, - visitor, - handleVisitor, - agent, - handleAgent, - apply, - setData, -}) => { - const t = useTranslation(); - - const useHandleType = (type) => - useMutableCallback(() => { - handleVisitor(''); - handleAgent(); - handleRid(''); - handleUsers([]); - handleType(type); - }); - - const eventStats = useEndpointAction('POST', '/v1/statistics.telemetry'); - - return ( - - - - - {t('Rooms')} - - - {t('Users')} - - - {t('Direct_Messages')} - - - {t('Omnichannel')} - - - - - - - - - {t('Message')} - - - - - - {t('Date')} - - - - - - - - - - {type === '' && } - {type === 'u' && } - {type === 'd' && } - {type === 'l' && ( - - )} - - - - - - {setData && } - - - - ); -}; diff --git a/apps/meteor/ee/client/audit/DateRangePicker.tsx b/apps/meteor/ee/client/audit/DateRangePicker.tsx deleted file mode 100644 index 985ea9d94d6a..000000000000 --- a/apps/meteor/ee/client/audit/DateRangePicker.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Box, InputBox, Menu, Margins } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentProps } from 'react'; -import React, { useState, useMemo, useEffect } from 'react'; - -const date = new Date(); - -const formatToDateInput = (date: Date): string => date.toISOString().slice(0, 10); - -const todayDate = formatToDateInput(date); - -const getMonthRange = (monthsToSubtractFromToday: number): { start: string; end: string } => { - const date = new Date(); - return { - start: formatToDateInput(new Date(date.getFullYear(), date.getMonth() - monthsToSubtractFromToday, 1)), - end: formatToDateInput(new Date(date.getFullYear(), date.getMonth() - monthsToSubtractFromToday + 1, 0)), - }; -}; - -const getWeekRange = (daysToSubtractFromStart: number, daysToSubtractFromEnd: number): { start: string; end: string } => { - const date = new Date(); - return { - start: formatToDateInput(new Date(date.getFullYear(), date.getMonth(), date.getDate() - daysToSubtractFromStart)), - end: formatToDateInput(new Date(date.getFullYear(), date.getMonth(), date.getDate() - daysToSubtractFromEnd)), - }; -}; - -type DateRangePickerProps = Omit, 'onChange'> & { - onChange?: (dateRange: { start: string; end: string }) => void; -}; - -const DateRangePicker = ({ onChange, ...props }: DateRangePickerProps): ReactElement => { - const t = useTranslation(); - const [range, setRange] = useState({ start: '', end: '' }); - - const { start, end } = range; - - const handleStart = useMutableCallback(({ currentTarget }) => { - const rangeObj = { - start: currentTarget.value, - end: range.end, - }; - setRange(rangeObj); - onChange?.(rangeObj); - }); - - const handleEnd = useMutableCallback(({ currentTarget }) => { - const rangeObj = { - end: currentTarget.value, - start: range.start, - }; - setRange(rangeObj); - onChange?.(rangeObj); - }); - - const handleRange = useMutableCallback((range) => { - setRange(range); - onChange?.(range); - }); - - useEffect(() => { - handleRange({ - start: todayDate, - end: todayDate, - }); - }, [handleRange]); - - const options = useMemo( - () => ({ - today: { - icon: 'history', - label: t('Today'), - action: (): void => { - handleRange(getWeekRange(0, 0)); - }, - }, - yesterday: { - icon: 'history', - label: t('Yesterday'), - action: (): void => { - handleRange(getWeekRange(1, 1)); - }, - }, - thisWeek: { - icon: 'history', - label: t('This_week'), - action: (): void => { - handleRange(getWeekRange(7, 0)); - }, - }, - previousWeek: { - icon: 'history', - label: t('Previous_week'), - action: (): void => { - handleRange(getWeekRange(14, 7)); - }, - }, - thisMonth: { - icon: 'history', - label: t('This_month'), - action: (): void => { - handleRange(getMonthRange(0)); - }, - }, - lastMonth: { - icon: 'history', - label: t('Previous_month'), - action: (): void => { - handleRange(getMonthRange(1)); - }, - }, - }), - [handleRange, t], - ); - - return ( - - - - - - - - ); -}; - -export default DateRangePicker; diff --git a/apps/meteor/ee/client/audit/FilterDisplay.js b/apps/meteor/ee/client/audit/FilterDisplay.js deleted file mode 100644 index 96b593077e5c..000000000000 --- a/apps/meteor/ee/client/audit/FilterDisplay.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React from 'react'; - -const FilterDisplay = ({ users, room, startDate, endDate, t }) => ( - - {users ? `@${users[0]} : @${users[1]}` : `#${room}`} - - {startDate} {t('to')} {endDate} - - -); - -export default FilterDisplay; diff --git a/apps/meteor/ee/client/audit/Result.js b/apps/meteor/ee/client/audit/Result.js deleted file mode 100644 index a71a27a4f55d..000000000000 --- a/apps/meteor/ee/client/audit/Result.js +++ /dev/null @@ -1,44 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useStableArray } from '@rocket.chat/fuselage-hooks'; -import { Blaze } from 'meteor/blaze'; -import { Template } from 'meteor/templating'; -import React, { useEffect, useState, useRef, memo } from 'react'; - -import '../../app/auditing/client/templates/audit/audit.html'; - -const Result = memo(({ setDataRef }) => { - const ref = useRef(); - - const [data, setData] = useState({}); - - const { msg, type, startDate, endDate, visitor, agent, users = [], rid } = data; - - const stableUsers = useStableArray(users); - - setDataRef.current = setData; - - useEffect(() => { - const view = Blaze.renderWithData( - Template.audit, - { - msg, - type, - startDate, - endDate, - visitor, - agent, - users: stableUsers, - rid, - }, - ref.current, - ); - - return () => Blaze.remove(view); - }, [agent, endDate, msg, rid, startDate, type, stableUsers, visitor]); - - return ; -}); - -Result.displayName = 'Result'; - -export default Result; diff --git a/apps/meteor/ee/client/audit/RoomAutoComplete/Avatar.js b/apps/meteor/ee/client/audit/RoomAutoComplete/Avatar.js deleted file mode 100644 index 0f19a2788bdf..000000000000 --- a/apps/meteor/ee/client/audit/RoomAutoComplete/Avatar.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Options } from '@rocket.chat/fuselage'; -import React from 'react'; - -import RoomAvatar from '../../../../client/components/avatar/RoomAvatar'; - -const Avatar = ({ value, type, avatarETag, ...props }) => ( - -); - -export default Avatar; diff --git a/apps/meteor/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js b/apps/meteor/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js deleted file mode 100644 index edb96d1b0686..000000000000 --- a/apps/meteor/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js +++ /dev/null @@ -1,37 +0,0 @@ -import { AutoComplete, Option } from '@rocket.chat/fuselage'; -import React, { memo, useMemo, useState } from 'react'; - -import { useEndpointData } from '../../../../client/hooks/useEndpointData'; -import Avatar from './Avatar'; - -const query = (name = '') => ({ selector: JSON.stringify({ name }) }); - -const RoomAutoComplete = (props) => { - const [filter, setFilter] = useState(''); - const { value: data } = useEndpointData('/v1/rooms.autocomplete.adminRooms', { params: useMemo(() => query(filter), [filter]) }); - const options = useMemo( - () => - (data && - data.items.map(({ name, _id, fname, avatarETag, t }) => ({ - value: _id, - label: { name: fname || name, avatarETag, type: t }, - }))) || - [], - [data], - ); - - return ( -