diff --git a/.changeset/calm-penguins-do.md b/.changeset/calm-penguins-do.md new file mode 100644 index 000000000000..6a83fae9030d --- /dev/null +++ b/.changeset/calm-penguins-do.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Now we are considering channels with auto-join inside teams on user creation diff --git a/.changeset/cuddly-rocks-fix.md b/.changeset/cuddly-rocks-fix.md new file mode 100644 index 000000000000..ced0904df59b --- /dev/null +++ b/.changeset/cuddly-rocks-fix.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Deprecate `insertOrUpdateUser` Meteor method diff --git a/.changeset/funny-cooks-sneeze.md b/.changeset/funny-cooks-sneeze.md new file mode 100644 index 000000000000..c7b019a3d92b --- /dev/null +++ b/.changeset/funny-cooks-sneeze.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed room owner specified on room import not being inserted as a room member or owner. diff --git a/.changeset/khaki-oranges-wink.md b/.changeset/khaki-oranges-wink.md new file mode 100644 index 000000000000..d03418158399 --- /dev/null +++ b/.changeset/khaki-oranges-wink.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Added a new formatter shortcut to add hyperlinks to a message diff --git a/.changeset/nice-ducks-shout.md b/.changeset/nice-ducks-shout.md new file mode 100644 index 000000000000..fc8ce46dcace --- /dev/null +++ b/.changeset/nice-ducks-shout.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed auto-availability of reactivated livechat agents; they now stay 'Not Available' until manually set to 'Available' diff --git a/.changeset/nine-ads-hide.md b/.changeset/nine-ads-hide.md new file mode 100644 index 000000000000..b09886787279 --- /dev/null +++ b/.changeset/nine-ads-hide.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +feat: show date on message's scroll diff --git a/.changeset/quick-cheetahs-help.md b/.changeset/quick-cheetahs-help.md new file mode 100644 index 000000000000..12005d73ec1a --- /dev/null +++ b/.changeset/quick-cheetahs-help.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/web-ui-registration": patch +--- + +fixed the login page crashing when receiving unexpected errors diff --git a/.changeset/wicked-months-laugh.md b/.changeset/wicked-months-laugh.md new file mode 100644 index 000000000000..684b4180a257 --- /dev/null +++ b/.changeset/wicked-months-laugh.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix users presence stuck as online after connecting using mobile apps diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index 72a5b882b18a..dc5a0f57e003 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -1,13 +1,11 @@ -import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import type { IAppServerOrchestrator, AppStatus } from '@rocket.chat/apps'; import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps-engine/server/bridges/AppActivationBridge'; import { Users } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppActivationBridge extends ActivationBridge { // eslint-disable-next-line no-empty-function - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index a5767f11b977..46bb70e3339a 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -1,3 +1,4 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import type { IApiRequest, IApiEndpoint, IApi } from '@rocket.chat/apps-engine/definition/api'; import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges/ApiBridge'; @@ -7,7 +8,6 @@ import express from 'express'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; const apiServer = express(); @@ -24,8 +24,7 @@ interface IRequestWithPrivateHash extends Request { export class AppApisBridge extends ApiBridge { appRouters: Map; - // eslint-disable-next-line no-empty-function - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); this.appRouters = new Map(); diff --git a/apps/meteor/app/apps/server/bridges/cloud.ts b/apps/meteor/app/apps/server/bridges/cloud.ts index a0675c115f01..30ca897240f8 100644 --- a/apps/meteor/app/apps/server/bridges/cloud.ts +++ b/apps/meteor/app/apps/server/bridges/cloud.ts @@ -1,11 +1,11 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; import { CloudWorkspaceBridge } from '@rocket.chat/apps-engine/server/bridges/CloudWorkspaceBridge'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; export class AppCloudBridge extends CloudWorkspaceBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index ee7e73e16eb5..5e018c51de89 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -1,3 +1,4 @@ +import type { IAppServerOrchestrator, IAppsRoom, IAppsUser } from '@rocket.chat/apps'; import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; import { CommandBridge } from '@rocket.chat/apps-engine/server/bridges/CommandBridge'; @@ -5,14 +6,13 @@ import type { IMessage, RequiredField, SlashCommand, SlashCommandCallbackParams import { Meteor } from 'meteor/meteor'; import { Utilities } from '../../../../ee/lib/misc/Utilities'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { parseParameters } from '../../../../lib/utils/parseParameters'; import { slashCommands } from '../../../utils/server/slashCommand'; export class AppCommandsBridge extends CommandBridge { disabledCommands: Map; - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); this.disabledCommands = new Map(); } @@ -44,7 +44,7 @@ export class AppCommandsBridge extends CommandBridge { slashCommands.commands[cmd] = this.disabledCommands.get(cmd) as (typeof slashCommands.commands)[string]; this.disabledCommands.delete(cmd); - this.orch.getNotifier().commandUpdated(cmd); + void this.orch.getNotifier().commandUpdated(cmd); } protected async disableCommand(command: string, appId: string): Promise { @@ -69,7 +69,7 @@ export class AppCommandsBridge extends CommandBridge { this.disabledCommands.set(cmd, commandObj); delete slashCommands.commands[cmd]; - this.orch.getNotifier().commandDisabled(cmd); + void this.orch.getNotifier().commandDisabled(cmd); } // command: { command, paramsExample, i18nDescription, executor: function } @@ -95,7 +95,7 @@ export class AppCommandsBridge extends CommandBridge { ) as (typeof slashCommands.commands)[string]['previewCallback']; slashCommands.commands[cmd] = item; - this.orch.getNotifier().commandUpdated(cmd); + void this.orch.getNotifier().commandUpdated(cmd); } protected async registerCommand(command: ISlashCommand, appId: string): Promise { @@ -118,7 +118,7 @@ export class AppCommandsBridge extends CommandBridge { } as SlashCommand; slashCommands.commands[command.command.toLowerCase()] = item; - this.orch.getNotifier().commandAdded(command.command.toLowerCase()); + void this.orch.getNotifier().commandAdded(command.command.toLowerCase()); } protected async unregisterCommand(command: string, appId: string): Promise { @@ -132,7 +132,7 @@ export class AppCommandsBridge extends CommandBridge { this.disabledCommands.delete(cmd); delete slashCommands.commands[cmd]; - this.orch.getNotifier().commandRemoved(cmd); + void this.orch.getNotifier().commandRemoved(cmd); } private _verifyCommand(command: ISlashCommand): void { @@ -162,14 +162,15 @@ export class AppCommandsBridge extends CommandBridge { } private async _appCommandExecutor({ command, message, params, triggerId, userId }: SlashCommandCallbackParams): Promise { - const user = await this.orch.getConverters()?.get('users').convertById(userId); - const room = await this.orch.getConverters()?.get('rooms').convertById(message.rid); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const user: IAppsUser | undefined = await this.orch.getConverters()?.get('users').convertById(userId); + const room: IAppsRoom | undefined = await this.orch.getConverters()?.get('rooms').convertById(message.rid); const threadId = message.tmid; const parameters = parseParameters(params); const context = new SlashCommandContext( - Object.freeze(user), - Object.freeze(room), + Object.freeze(user as IAppsUser), + Object.freeze(room as IAppsRoom), Object.freeze(parameters) as string[], threadId, triggerId, @@ -183,12 +184,19 @@ export class AppCommandsBridge extends CommandBridge { parameters: any, message: RequiredField, 'rid'>, ): Promise { - const user = await this.orch.getConverters()?.get('users').convertById(Meteor.userId()); - const room = await this.orch.getConverters()?.get('rooms').convertById(message.rid); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const uid = Meteor.userId() as string; + const user: IAppsUser | undefined = await this.orch.getConverters()?.get('users').convertById(uid); + const room: IAppsRoom | undefined = await this.orch.getConverters()?.get('rooms').convertById(message.rid); const threadId = message.tmid; const params = parseParameters(parameters); - const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params) as string[], threadId); + const context = new SlashCommandContext( + Object.freeze(user as IAppsUser), + Object.freeze(room as IAppsRoom), + Object.freeze(params) as string[], + threadId, + ); return this.orch.getManager()?.getCommandManager().getPreviews(command, context); } @@ -199,14 +207,16 @@ export class AppCommandsBridge extends CommandBridge { preview: ISlashCommandPreviewItem, triggerId: string, ): Promise { - const user = await this.orch.getConverters()?.get('users').convertById(Meteor.userId()); - const room = await this.orch.getConverters()?.get('rooms').convertById(message.rid); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const uid = Meteor.userId() as string; + const user: IAppsUser | undefined = await this.orch.getConverters()?.get('users').convertById(uid); + const room: IAppsRoom | undefined = await this.orch.getConverters()?.get('rooms').convertById(message.rid); const threadId = message.tmid; const params = parseParameters(parameters); const context = new SlashCommandContext( - Object.freeze(user), - Object.freeze(room), + Object.freeze(user as IAppsUser), + Object.freeze(room as IAppsRoom), Object.freeze(params) as string[], threadId, triggerId, diff --git a/apps/meteor/app/apps/server/bridges/details.ts b/apps/meteor/app/apps/server/bridges/details.ts index 50709917183a..3930cdd451cc 100644 --- a/apps/meteor/app/apps/server/bridges/details.ts +++ b/apps/meteor/app/apps/server/bridges/details.ts @@ -1,18 +1,19 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { AppDetailChangesBridge as DetailChangesBridge } from '@rocket.chat/apps-engine/server/bridges/AppDetailChangesBridge'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppDetailChangesBridge extends DetailChangesBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } protected onAppSettingsChange(appId: string, setting: ISetting): void { + const logFailure = () => console.warn('failed to notify about the setting change.', appId); + try { - this.orch.getNotifier().appSettingsChange(appId, setting); + this.orch.getNotifier().appSettingsChange(appId, setting).catch(logFailure); } catch (e) { - console.warn('failed to notify about the setting change.', appId); + logFailure(); } } } diff --git a/apps/meteor/app/apps/server/bridges/environmental.ts b/apps/meteor/app/apps/server/bridges/environmental.ts index 43b34674e95f..705a27186dee 100644 --- a/apps/meteor/app/apps/server/bridges/environmental.ts +++ b/apps/meteor/app/apps/server/bridges/environmental.ts @@ -1,11 +1,10 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { EnvironmentalVariableBridge } from '@rocket.chat/apps-engine/server/bridges/EnvironmentalVariableBridge'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppEnvironmentalVariableBridge extends EnvironmentalVariableBridge { allowed: Array; - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); this.allowed = ['NODE_ENV', 'ROOT_URL', 'INSTANCE_IP']; } diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index a4b09e848c90..1535a18823c5 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -1,10 +1,9 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors'; import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps-engine/server/bridges'; import { HttpBridge } from '@rocket.chat/apps-engine/server/bridges/HttpBridge'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - const isGetOrHead = (method: string): boolean => ['GET', 'HEAD'].includes(method.toUpperCase()); // Previously, there was no timeout for HTTP requests. @@ -13,7 +12,7 @@ const isGetOrHead = (method: string): boolean => ['GET', 'HEAD'].includes(method const DEFAULT_TIMEOUT = 3 * 60 * 1000; export class AppHttpBridge extends HttpBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/internal.ts b/apps/meteor/app/apps/server/bridges/internal.ts index b41ffbb2889e..c5cd9a3f1a60 100644 --- a/apps/meteor/app/apps/server/bridges/internal.ts +++ b/apps/meteor/app/apps/server/bridges/internal.ts @@ -1,14 +1,13 @@ -import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; +import type { IAppServerOrchestrator, IAppsSetting } from '@rocket.chat/apps'; import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/InternalBridge'; -import type { ISubscription } from '@rocket.chat/core-typings'; +import type { ISetting, ISubscription } from '@rocket.chat/core-typings'; import { Settings, Subscriptions } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { isTruthy } from '../../../../lib/isTruthy'; import { deasyncPromise } from '../../../../server/deasync/deasync'; export class AppInternalBridge extends InternalBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } @@ -37,9 +36,13 @@ export class AppInternalBridge extends InternalBridge { return records.map((s: ISubscription) => s.u.username).filter(isTruthy); } - protected async getWorkspacePublicKey(): Promise { - const publicKeySetting = await Settings.findOneById('Cloud_Workspace_PublicKey'); + protected async getWorkspacePublicKey(): Promise { + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const publicKeySetting: ISetting | null = await Settings.findOneById('Cloud_Workspace_PublicKey'); - return this.orch.getConverters()?.get('settings').convertToApp(publicKeySetting); + return this.orch + .getConverters() + ?.get('settings') + .convertToApp(publicKeySetting as ISetting); } } diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 2c5eaa841cdd..bf175d1d1426 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -1,28 +1,22 @@ +import type { IAppServerOrchestrator, IAppsLivechatMessage } from '@rocket.chat/apps'; import type { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; -import type { - ILivechatMessage, - IVisitor, - ILivechatRoom, - ILivechatTransferData, - IDepartment, -} from '@rocket.chat/apps-engine/definition/livechat'; +import type { IVisitor, ILivechatRoom, ILivechatTransferData, IDepartment } from '@rocket.chat/apps-engine/definition/livechat'; import type { IMessage as IAppsEngineMesage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/LivechatBridge'; -import type { SelectedAgent } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; import { getRoom } from '../../../livechat/server/api/lib/livechat'; -import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; +import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { settings } from '../../../settings/server'; export class AppLivechatBridge extends LivechatBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } @@ -36,16 +30,21 @@ export class AppLivechatBridge extends LivechatBridge { return LivechatTyped.online(departmentId); } - protected async createMessage(message: ILivechatMessage, appId: string): Promise { + protected async createMessage(message: IAppsLivechatMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is creating a new message.`); if (!message.token) { throw new Error('Invalid token for livechat message'); } + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const guest = this.orch.getConverters().get('visitors').convertAppVisitor(message.visitor); + const appMessage = (await this.orch.getConverters().get('messages').convertAppMessage(message)) as IMessage | undefined; + const livechatMessage = appMessage as ILivechatMessage | undefined; + const msg = await LivechatTyped.sendMessage({ - guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(message.visitor), - message: await this.orch.getConverters()?.get('messages').convertAppMessage(message), + guest: guest as ILivechatVisitor, + message: livechatMessage as ILivechatMessage, agent: undefined, roomInfo: { source: { @@ -59,13 +58,16 @@ export class AppLivechatBridge extends LivechatBridge { return msg._id; } - protected async getMessageById(messageId: string, appId: string): Promise { + protected async getMessageById(messageId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the message: "${messageId}"`); - return this.orch.getConverters()?.get('messages').convertById(messageId); + const message = await this.orch.getConverters().get('messages').convertById(messageId); + + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + return message as IAppsLivechatMessage; } - protected async updateMessage(message: ILivechatMessage, appId: string): Promise { + protected async updateMessage(message: IAppsLivechatMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is updating a message.`); const data = { @@ -114,7 +116,8 @@ export class AppLivechatBridge extends LivechatBridge { extraParams: undefined, }); - return this.orch.getConverters()?.get('rooms').convertRoom(result.room); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + return this.orch.getConverters()?.get('rooms').convertRoom(result.room) as Promise; } protected async closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise { @@ -152,7 +155,8 @@ export class AppLivechatBridge extends LivechatBridge { result = await LivechatRooms.findOpenByVisitorToken(visitor.token, {}, extraQuery).toArray(); } - return Promise.all((result as unknown as ILivechatRoom[]).map((room) => this.orch.getConverters()?.get('rooms').convertRoom(room))); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + return Promise.all(result.map((room) => this.orch.getConverters()?.get('rooms').convertRoom(room) as Promise)); } protected async createVisitor(visitor: IVisitor, appId: string): Promise { @@ -208,8 +212,9 @@ export class AppLivechatBridge extends LivechatBridge { userId = transferredTo._id; } + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. return LivechatTyped.transfer( - await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom), + (await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom)) as IOmnichannelRoom, this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), { userId, departmentId, transferredBy, transferredTo }, ); @@ -275,7 +280,8 @@ export class AppLivechatBridge extends LivechatBridge { this.orch.debugLog(`The App ${appId} is looking for livechat departments.`); const converter = this.orch.getConverters()?.get('departments'); - const boundConverter = converter.convertDepartment.bind(converter); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const boundConverter = converter.convertDepartment.bind(converter) as (_: ILivechatDepartment) => Promise; return Promise.all((await LivechatDepartment.findEnabledWithAgents().toArray()).map(boundConverter)); } diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 311e3aaca1e1..18a68220998f 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -1,39 +1,41 @@ -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IAppServerOrchestrator, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; -import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; import { api } from '@rocket.chat/core-services'; +import type { IMessage } from '@rocket.chat/core-typings'; import { Users, Subscriptions, Messages } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; import notifications from '../../../notifications/server/lib/Notifications'; export class AppMessageBridge extends MessageBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } - protected async create(message: IMessage, appId: string): Promise { + protected async create(message: IAppsMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is creating a new message.`); - const convertedMessage = await this.orch.getConverters()?.get('messages').convertAppMessage(message); - - const sentMessage = await executeSendMessage(convertedMessage.u._id, convertedMessage); + const convertedMessage: IMessage | undefined = await this.orch.getConverters()?.get('messages').convertAppMessage(message); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const definedMessage = convertedMessage as IMessage; + const sentMessage = await executeSendMessage(definedMessage.u._id, definedMessage); return sentMessage._id; } - protected async getById(messageId: string, appId: string): Promise { + protected async getById(messageId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the message: "${messageId}"`); - return this.orch.getConverters()?.get('messages').convertById(messageId); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const message: IAppsMessage | undefined = await this.orch.getConverters()?.get('messages').convertById(messageId); + return message as IAppsMessage; } - protected async update(message: IMessage, appId: string): Promise { + protected async update(message: IAppsMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is updating a message.`); if (!message.editor) { @@ -44,17 +46,18 @@ export class AppMessageBridge extends MessageBridge { throw new Error('A message must exist to update.'); } - const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const msg: IMessage | undefined = await this.orch.getConverters()?.get('messages').convertAppMessage(message); const editor = await Users.findOneById(message.editor.id); if (!editor) { throw new Error('Invalid editor assigned to the message for the update.'); } - await updateMessage(msg, editor); + await updateMessage(msg as IMessage, editor); } - protected async delete(message: IMessage, user: IUser, appId: string): Promise { + protected async delete(message: IAppsMessage, user: IAppsUser, appId: string): Promise { this.orch.debugLog(`The App ${appId} is deleting a message.`); if (!message.id) { @@ -64,10 +67,10 @@ export class AppMessageBridge extends MessageBridge { const convertedMsg = await this.orch.getConverters()?.get('messages').convertAppMessage(message); const convertedUser = (await Users.findOneById(user.id)) || this.orch.getConverters()?.get('users').convertToRocketChat(user); - await deleteMessage(convertedMsg, convertedUser); + await deleteMessage(convertedMsg as IMessage, convertedUser); } - protected async notifyUser(user: IUser, message: IMessage, appId: string): Promise { + protected async notifyUser(user: IAppsUser, message: IAppsMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is notifying a user.`); const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message); @@ -81,21 +84,23 @@ export class AppMessageBridge extends MessageBridge { }); } - protected async notifyRoom(room: IRoom, message: IMessage, appId: string): Promise { + protected async notifyRoom(room: IRoom, message: IAppsMessage, appId: string): Promise { this.orch.debugLog(`The App ${appId} is notifying a room's users.`); if (!room?.id) { return; } - const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const msg: IMessage | undefined = await this.orch.getConverters()?.get('messages').convertAppMessage(message); + const convertedMessage = msg as IMessage; const users = (await Subscriptions.findByRoomIdWhenUserIdExists(room.id, { projection: { 'u._id': 1 } }).toArray()).map((s) => s.u._id); await Users.findByIds(users, { projection: { _id: 1 } }).forEach( ({ _id }: { _id: string }) => void api.broadcast('notify.ephemeralMessage', _id, room.id, { - ...msg, + ...convertedMessage, }), ); } diff --git a/apps/meteor/app/apps/server/bridges/moderation.ts b/apps/meteor/app/apps/server/bridges/moderation.ts index 4581f9a6ac6b..0f1e56bbdec3 100644 --- a/apps/meteor/app/apps/server/bridges/moderation.ts +++ b/apps/meteor/app/apps/server/bridges/moderation.ts @@ -1,13 +1,13 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { ModerationBridge } from '@rocket.chat/apps-engine/server/bridges/ModerationBridge'; import { ModerationReports } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { reportMessage } from '../../../../server/lib/moderation/reportMessage'; export class AppModerationBridge extends ModerationBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/oauthApps.ts b/apps/meteor/app/apps/server/bridges/oauthApps.ts index 943c082ba85a..ba8ed8124690 100644 --- a/apps/meteor/app/apps/server/bridges/oauthApps.ts +++ b/apps/meteor/app/apps/server/bridges/oauthApps.ts @@ -1,3 +1,4 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IOAuthApp, IOAuthAppParams } from '@rocket.chat/apps-engine/definition/accessors/IOAuthApp'; import { OAuthAppsBridge } from '@rocket.chat/apps-engine/server/bridges/OAuthAppsBridge'; import type { IOAuthApps } from '@rocket.chat/core-typings'; @@ -5,10 +6,8 @@ import { OAuthApps, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { v4 as uuidv4 } from 'uuid'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppOAuthAppsBridge extends OAuthAppsBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/persistence.ts b/apps/meteor/app/apps/server/bridges/persistence.ts index 3810ed367e99..857f6a561ed6 100644 --- a/apps/meteor/app/apps/server/bridges/persistence.ts +++ b/apps/meteor/app/apps/server/bridges/persistence.ts @@ -1,11 +1,10 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; import { PersistenceBridge } from '@rocket.chat/apps-engine/server/bridges/PersistenceBridge'; -import type { InsertOneResult, UpdateResult } from 'mongodb'; - -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; +import type { InsertOneResult } from 'mongodb'; export class AppPersistenceBridge extends PersistenceBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } @@ -25,7 +24,7 @@ export class AppPersistenceBridge extends PersistenceBridge { return this.orch .getPersistenceModel() .insertOne({ appId, data }) - .then(({ insertedId }: InsertOneResult) => insertedId || ''); + .then(({ insertedId }: InsertOneResult) => (insertedId as unknown as string) || ''); } protected async createWithAssociations(data: object, associations: Array, appId: string): Promise { @@ -42,7 +41,7 @@ export class AppPersistenceBridge extends PersistenceBridge { return this.orch .getPersistenceModel() .insertOne({ appId, associations, data }) - .then(({ insertedId }: InsertOneResult) => insertedId || ''); + .then(({ insertedId }: InsertOneResult) => (insertedId as unknown as string) || ''); } protected async readById(id: string, appId: string): Promise { @@ -135,6 +134,6 @@ export class AppPersistenceBridge extends PersistenceBridge { return this.orch .getPersistenceModel() .update(query, { $set: { data } }, { upsert }) - .then(({ upsertedId }: UpdateResult) => upsertedId || ''); + .then(({ upsertedId }: any) => upsertedId || ''); } } diff --git a/apps/meteor/app/apps/server/bridges/roles.ts b/apps/meteor/app/apps/server/bridges/roles.ts index f973b7f49ed2..aa0fcdc7b80b 100644 --- a/apps/meteor/app/apps/server/bridges/roles.ts +++ b/apps/meteor/app/apps/server/bridges/roles.ts @@ -1,27 +1,30 @@ -import type { IRole } from '@rocket.chat/apps-engine/definition/roles'; +import type { IAppServerOrchestrator, IAppsRole } from '@rocket.chat/apps'; import { RoleBridge } from '@rocket.chat/apps-engine/server/bridges'; +import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppRoleBridge extends RoleBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } - protected async getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise { + protected async getOneByIdOrName(idOrName: IAppsRole['id'] | IAppsRole['name'], appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the roleByIdOrName: "${idOrName}"`); - const role = await Roles.findOneByIdOrName(idOrName); - return this.orch.getConverters()?.get('roles').convertRole(role); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const role: IRole | null = await Roles.findOneByIdOrName(idOrName); + return this.orch + .getConverters() + ?.get('roles') + .convertRole(role as IRole); } - protected async getCustomRoles(appId: string): Promise> { + protected async getCustomRoles(appId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the custom roles`); const cursor = Roles.findCustomRoles(); - const roles: IRole[] = []; + const roles: IAppsRole[] = []; for await (const role of cursor) { const convRole = await this.orch.getConverters()?.get('roles').convertRole(role); diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 91b0049513f0..bbd24152716f 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -1,3 +1,4 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; @@ -6,7 +7,6 @@ import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom } from '@rocket.chat/core-typings'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; @@ -15,7 +15,7 @@ import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; export class AppRoomBridge extends RoomBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } @@ -65,13 +65,17 @@ export class AppRoomBridge extends RoomBridge { protected async getById(roomId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the roomById: "${roomId}"`); - return this.orch.getConverters()?.get('rooms').convertById(roomId); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promise: Promise = this.orch.getConverters()?.get('rooms').convertById(roomId); + return promise as Promise; } protected async getByName(roomName: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the roomByName: "${roomName}"`); - return this.orch.getConverters()?.get('rooms').convertByName(roomName); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promise: Promise = this.orch.getConverters()?.get('rooms').convertByName(roomName); + return promise as Promise; } protected async getCreatorById(roomId: string, appId: string): Promise { @@ -79,7 +83,7 @@ export class AppRoomBridge extends RoomBridge { const room = await Rooms.findOneById(roomId); - if (!room || !room.u || !room.u._id) { + if (!room?.u?._id) { return undefined; } @@ -91,7 +95,7 @@ export class AppRoomBridge extends RoomBridge { const room = await Rooms.findOneByName(roomName, {}); - if (!room || !room.u || !room.u._id) { + if (!room?.u?._id) { return undefined; } @@ -101,9 +105,12 @@ export class AppRoomBridge extends RoomBridge { protected async getMembers(roomId: string, appId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the room's members by room id: "${roomId}"`); const subscriptions = await Subscriptions.findByRoomId(roomId, {}); - return Promise.all( + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promises: Promise<(IUser | undefined)[]> = Promise.all( (await subscriptions.toArray()).map((sub: ISubscription) => this.orch.getConverters()?.get('users').convertById(sub.u?._id)), ); + + return promises as Promise; } protected async getDirectByUsernames(usernames: Array, appId: string): Promise { @@ -124,7 +131,7 @@ export class AppRoomBridge extends RoomBridge { const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room); - await Rooms.updateOne({ _id: rm._id }, { $set: rm }); + await Rooms.updateOne({ _id: rm._id }, { $set: rm as Partial }); for await (const username of members) { const member = await Users.findOneByUsername(username, {}); @@ -162,9 +169,10 @@ export class AppRoomBridge extends RoomBridge { throw new Error('There must be a parent room to create a discussion.'); } + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. const discussion = { prid: rcRoom.prid, - t_name: rcRoom.fname, + t_name: rcRoom.fname as string, pmid: rcMessage ? rcMessage._id : undefined, reply: reply && reply.trim() !== '' ? reply : undefined, users: members.length > 0 ? members : [], @@ -198,7 +206,7 @@ export class AppRoomBridge extends RoomBridge { }[]; // Was this a bug? const users = await Users.findByIds(subs.map((user: { uid: string }) => user.uid)).toArray(); - const userConverter = this.orch.getConverters()!.get('users'); - return users.map((user: ICoreUser) => userConverter!.convertToApp(user)); + const userConverter = this.orch.getConverters().get('users'); + return users.map((user: ICoreUser) => userConverter.convertToApp(user)); } } diff --git a/apps/meteor/app/apps/server/bridges/scheduler.ts b/apps/meteor/app/apps/server/bridges/scheduler.ts index 0b2995166e63..2f4799c79d70 100644 --- a/apps/meteor/app/apps/server/bridges/scheduler.ts +++ b/apps/meteor/app/apps/server/bridges/scheduler.ts @@ -1,13 +1,12 @@ import type { Job } from '@rocket.chat/agenda'; import { Agenda } from '@rocket.chat/agenda'; +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IProcessor, IOnetimeSchedule, IRecurringSchedule, IJobContext } from '@rocket.chat/apps-engine/definition/scheduler'; import { StartupType } from '@rocket.chat/apps-engine/definition/scheduler'; import { SchedulerBridge } from '@rocket.chat/apps-engine/server/bridges/SchedulerBridge'; import { ObjectID } from 'bson'; import { MongoInternals } from 'meteor/mongo'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - function _callProcessor(processor: IProcessor['processor']): (job: Job) => Promise { return (job) => { const data = job?.attrs?.data || {}; @@ -36,7 +35,7 @@ export class AppSchedulerBridge extends SchedulerBridge { private scheduler: Agenda; - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); this.scheduler = new Agenda({ mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index d61de9e8eca5..e90171813df8 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -1,11 +1,10 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; import { Settings } from '@rocket.chat/models'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppSettingBridge extends ServerSettingBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/thread.ts b/apps/meteor/app/apps/server/bridges/thread.ts index 313f60ecc4ac..099fe9184e00 100644 --- a/apps/meteor/app/apps/server/bridges/thread.ts +++ b/apps/meteor/app/apps/server/bridges/thread.ts @@ -1,10 +1,9 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import { ThreadBridge } from '@rocket.chat/apps-engine/server/bridges/ThreadBridge'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class AppThreadBridge extends ThreadBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/uiInteraction.ts b/apps/meteor/app/apps/server/bridges/uiInteraction.ts index 5783ac2b4eb3..fc68e4e30d3f 100644 --- a/apps/meteor/app/apps/server/bridges/uiInteraction.ts +++ b/apps/meteor/app/apps/server/bridges/uiInteraction.ts @@ -1,13 +1,12 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IUIKitInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { UiInteractionBridge as AppsEngineUiInteractionBridge } from '@rocket.chat/apps-engine/server/bridges/UiInteractionBridge'; import { api } from '@rocket.chat/core-services'; import type * as UiKit from '@rocket.chat/ui-kit'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; - export class UiInteractionBridge extends AppsEngineUiInteractionBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } diff --git a/apps/meteor/app/apps/server/bridges/uploads.ts b/apps/meteor/app/apps/server/bridges/uploads.ts index df16704a3196..b9d0ff67de58 100644 --- a/apps/meteor/app/apps/server/bridges/uploads.ts +++ b/apps/meteor/app/apps/server/bridges/uploads.ts @@ -1,9 +1,9 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; import { UploadBridge } from '@rocket.chat/apps-engine/server/bridges/UploadBridge'; import { determineFileType } from '../../../../ee/lib/misc/determineFileType'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { FileUpload } from '../../../file-upload/server'; import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; import { sendFileLivechatMessage } from '../../../livechat/server/methods/sendFileLivechatMessage'; @@ -16,14 +16,16 @@ const getUploadDetails = (details: IUploadDetails): Partial => { return details; }; export class AppUploadBridge extends UploadBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } protected async getById(id: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the upload: "${id}"`); - return this.orch.getConverters()?.get('uploads').convertById(id); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promise: Promise = this.orch.getConverters()?.get('uploads').convertById(id); + return promise as Promise; } protected async getBuffer(upload: IUpload, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index d3c7dbc2a3d2..b0dfedd6273b 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -1,3 +1,4 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; import { Presence } from '@rocket.chat/core-services'; @@ -5,7 +6,6 @@ import type { UserStatus } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; import { deleteUser } from '../../../lib/server/functions/deleteUser'; import { getUserCreatedByApp } from '../../../lib/server/functions/getUserCreatedByApp'; @@ -13,20 +13,23 @@ import { setUserActiveStatus } from '../../../lib/server/functions/setUserActive import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; export class AppUserBridge extends UserBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } protected async getById(userId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the userId: "${userId}"`); - - return this.orch.getConverters()?.get('users').convertById(userId); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promise: Promise = this.orch.getConverters()?.get('users').convertById(userId); + return promise as Promise; } protected async getByUsername(username: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the username: "${username}"`); - return this.orch.getConverters()?.get('users').convertByUsername(username); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promise: Promise = this.orch.getConverters()?.get('users').convertByUsername(username); + return promise as Promise; } protected async getAppUser(appId?: string): Promise { @@ -61,7 +64,11 @@ export class AppUserBridge extends UserBridge { 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); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const user = this.orch + .getConverters() + ?.get('users') + .convertToRocketChat(userDescriptor as IUser); if (!user._id) { user._id = Random.id(); @@ -74,7 +81,7 @@ export class AppUserBridge extends UserBridge { switch (user.type) { case 'bot': case 'app': - if (!(await checkUsernameAvailability(user.username))) { + if (!(await checkUsernameAvailability(user.username as string))) { throw new Error(`The username "${user.username}" is already being used. Rename or remove the user using it to install this App`); } @@ -139,9 +146,12 @@ export class AppUserBridge extends UserBridge { if (!userId) { throw new Error('Invalid user id'); } - const convertedUser = await this.orch.getConverters()?.get('users').convertById(userId); - await setUserActiveStatus(convertedUser.id, false, confirmRelinquish); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const convertedUser: IUser | undefined = await this.orch.getConverters()?.get('users').convertById(userId); + const { id: uid } = convertedUser as IUser; + + await setUserActiveStatus(uid, false, confirmRelinquish); return true; } diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts index c70f7f562fab..bebcb25a6f51 100644 --- a/apps/meteor/app/apps/server/bridges/videoConferences.ts +++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts @@ -1,21 +1,23 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders'; import type { AppVideoConference, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; import { VideoConferenceBridge } from '@rocket.chat/apps-engine/server/bridges/VideoConferenceBridge'; import { VideoConf } from '@rocket.chat/core-services'; -import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; import { videoConfProviders } from '../../../../server/lib/videoConfProviders'; import type { AppVideoConferencesConverter } from '../converters/videoConferences'; export class AppVideoConferenceBridge extends VideoConferenceBridge { - constructor(private readonly orch: AppServerOrchestrator) { + constructor(private readonly orch: IAppServerOrchestrator) { super(); } protected async getById(callId: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is getting the video conference byId: "${callId}"`); - return this.orch.getConverters()?.get('videoConferences').convertById(callId); + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const promise: Promise = this.orch.getConverters()?.get('videoConferences').convertById(callId); + return promise as Promise; } protected async create(call: AppVideoConference, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/converters/departments.js b/apps/meteor/app/apps/server/converters/departments.js index 087d1956ca6b..3fbfd07e9e99 100644 --- a/apps/meteor/app/apps/server/converters/departments.js +++ b/apps/meteor/app/apps/server/converters/departments.js @@ -1,6 +1,6 @@ import { LivechatDepartment } from '@rocket.chat/models'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; export class AppDepartmentsConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 6243bd2c603e..187a6519339a 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -1,7 +1,7 @@ import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; export class AppMessagesConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/roles.ts b/apps/meteor/app/apps/server/converters/roles.ts index 4ac1f3956420..10841a038c93 100644 --- a/apps/meteor/app/apps/server/converters/roles.ts +++ b/apps/meteor/app/apps/server/converters/roles.ts @@ -1,10 +1,11 @@ +import type { IAppRolesConverter } from '@rocket.chat/apps'; import type { IRole as AppsEngineRole } from '@rocket.chat/apps-engine/definition/roles'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; -export class AppRolesConverter { +export class AppRolesConverter implements IAppRolesConverter { async convertById(roleId: string): Promise { const role = await Roles.findOneById(roleId); @@ -22,8 +23,8 @@ export class AppRolesConverter { mandatory2fa: 'mandatory2fa', protected: 'protected', scope: 'scope', - }; + } as const; - return (await transformMappedData(role, map)) as unknown as AppsEngineRole; + return transformMappedData(role, map); } } diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 905534212836..670c1a248a0f 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -1,7 +1,7 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import { LivechatVisitors, Rooms, LivechatDepartment, Users } from '@rocket.chat/models'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; export class AppRoomsConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/threads.ts b/apps/meteor/app/apps/server/converters/threads.ts index 19d3b4aeae6d..840f4f1613eb 100644 --- a/apps/meteor/app/apps/server/converters/threads.ts +++ b/apps/meteor/app/apps/server/converters/threads.ts @@ -1,18 +1,20 @@ -import type { IMessage as AppsEngineMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IAppRoomsConverter, IAppThreadsConverter, IAppUsersConverter, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; +import type { IMessage as AppsEngineMessage, IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/core-typings'; import { isEditedMessage, type IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; // eslint-disable-next-line @typescript-eslint/naming-convention interface Orchestrator { rooms: () => { - convertById(id: string): Promise; + convertById: IAppRoomsConverter['convertById']; }; users: () => { - convertById(id: string): Promise; - convertToApp(user: unknown): Promise; + convertById: IAppUsersConverter['convertById']; + convertToApp: IAppUsersConverter['convertToApp']; }; } @@ -34,7 +36,7 @@ const cachedFunction = any>(fn: F) => { }) as F; }; -export class AppThreadsConverter { +export class AppThreadsConverter implements IAppThreadsConverter { constructor( private readonly orch: { getConverters: () => { @@ -111,7 +113,7 @@ export class AppThreadsConverter { return convertUserById(editedBy._id); }, - attachments: async (message: IMessage) => { + attachments: async (message: IMessage): Promise => { if (!message.attachments) { return undefined; } @@ -119,26 +121,33 @@ export class AppThreadsConverter { delete message.attachments; return result; }, - sender: async (message: IMessage) => { + sender: async (message: IMessage): Promise => { + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. if (!message.u?._id) { - return undefined; + return undefined as unknown as IAppsUser; } - let user = await convertUserById(message.u._id); + let user: IAppsUser | undefined = await convertUserById(message.u._id); // When the sender of the message is a Guest (livechat) and not a user if (!user) { - user = await convertToApp(message.u); + user = await convertToApp(message.u as unknown as IUser); } - return user; + return user as IAppsUser; }, - }; + } as const; - return (await transformMappedData(msgObj, map)) as unknown as AppsEngineMessage; + // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. + const msgData = { + ...msgObj, + reactions: msgObj.reactions as unknown as AppsEngineMessage['reactions'], + } as IMessage & { reactions?: AppsEngineMessage['reactions'] }; + + return transformMappedData(msgData, map); } - async _convertAttachmentsToApp(attachments: NonNullable) { + async _convertAttachmentsToApp(attachments: NonNullable): Promise> { const map = { collapsed: 'collapsed', color: 'color', @@ -161,7 +170,7 @@ export class AppThreadsConverter { actions: 'actions', type: 'type', description: 'description', - author: (attachment: NonNullable[number]) => { + author: (attachment: NonNullable[number]): IMessageAttachment['author'] => { if (!('author_name' in attachment)) { return; } @@ -188,7 +197,7 @@ export class AppThreadsConverter { delete attachment.ts; return result; }, - }; + } as const; return Promise.all(attachments.map(async (attachment) => transformMappedData(attachment, map))); } diff --git a/apps/meteor/ee/lib/misc/transformMappedData.js b/apps/meteor/app/apps/server/converters/transformMappedData.ts similarity index 73% rename from apps/meteor/ee/lib/misc/transformMappedData.js rename to apps/meteor/app/apps/server/converters/transformMappedData.ts index 963f98cb6cf5..df2f16138d73 100644 --- a/apps/meteor/ee/lib/misc/transformMappedData.js +++ b/apps/meteor/app/apps/server/converters/transformMappedData.ts @@ -12,7 +12,7 @@ import cloneDeep from 'lodash.clonedeep'; * * ```javascript * const data = { _id: 'abcde123456', size: 10 }; - * const map = { id: '_id' } + * const map = Object.freeze({ id: '_id' }); * * transformMappedData(data, map); * // { id: 'abcde123456', _unmappedProperties_: { size: 10 } } @@ -43,14 +43,14 @@ import cloneDeep from 'lodash.clonedeep'; * // { id: 'abcde123456', newSize: 20, _unmappedProperties_: { size: 10 } } * * // You need to explicitly remove it from the original `data` - * const map = { + * const map = Object.freeze({ * id: '_id', * newSize: (data) => { * const result = data.size + 10; * delete data.size; * return result; * } - * }; + * }); * * transformMappedData(data, map); * // { id: 'abcde123456', newSize: 20, _unmappedProperties_: {} } @@ -62,9 +62,25 @@ import cloneDeep from 'lodash.clonedeep'; * @returns Object The data after transformations have been applied */ -export const transformMappedData = async (data, map) => { - const originalData = cloneDeep(data); - const transformedData = {}; +export const transformMappedData = async < + ResultType extends { + -readonly [p in keyof MapType]: MapType[p] extends keyof DataType + ? DataType[MapType[p]] + : MapType[p] extends (...args: any[]) => any + ? Awaited> + : never; + }, + DataType extends Record, + MapType extends { [p in string]: string | ((data: DataType) => Promise) | ((data: DataType) => unknown) }, + UnmappedProperties extends { + [p in keyof DataType as Exclude]: DataType[p]; + }, +>( + data: DataType, + map: MapType, +): Promise => { + const originalData: DataType = cloneDeep(data); + const transformedData: Record = {}; for await (const [to, from] of Object.entries(map)) { if (typeof from === 'function') { @@ -81,7 +97,8 @@ export const transformMappedData = async (data, map) => { } } - transformedData._unmappedProperties_ = originalData; - - return transformedData; + return { + ...(transformedData as ResultType), + _unmappedProperties_: originalData as unknown as UnmappedProperties, + }; }; diff --git a/apps/meteor/app/apps/server/converters/uploads.js b/apps/meteor/app/apps/server/converters/uploads.js index b6531854aa38..60f85a8aa72f 100644 --- a/apps/meteor/app/apps/server/converters/uploads.js +++ b/apps/meteor/app/apps/server/converters/uploads.js @@ -1,6 +1,6 @@ import { Uploads } from '@rocket.chat/models'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; export class AppUploadsConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/videoConferences.ts b/apps/meteor/app/apps/server/converters/videoConferences.ts index 00eb4e915137..7a9120bf8508 100644 --- a/apps/meteor/app/apps/server/converters/videoConferences.ts +++ b/apps/meteor/app/apps/server/converters/videoConferences.ts @@ -1,27 +1,33 @@ -import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { IAppVideoConferencesConverter, AppsVideoConference } from '@rocket.chat/apps'; import { VideoConf } from '@rocket.chat/core-services'; -import type { IVideoConference } from '@rocket.chat/core-typings'; +import type { VideoConference } from '@rocket.chat/core-typings'; -export class AppVideoConferencesConverter { - async convertById(callId: string): Promise { +export class AppVideoConferencesConverter implements IAppVideoConferencesConverter { + async convertById(callId: string): Promise { const call = await VideoConf.getUnfiltered(callId); return this.convertVideoConference(call); } - convertVideoConference(call: IVideoConference | null): VideoConference | undefined { + convertVideoConference(call: undefined | null): undefined; + + convertVideoConference(call: VideoConference): AppsVideoConference; + + convertVideoConference(call: VideoConference | undefined | null): AppsVideoConference | undefined; + + convertVideoConference(call: VideoConference | undefined | null): AppsVideoConference | undefined { if (!call) { return; } return { ...call, - } as VideoConference; + } as AppsVideoConference; } - convertAppVideoConference(call: VideoConference): IVideoConference { + convertAppVideoConference(call: AppsVideoConference): VideoConference { return { ...call, - } as IVideoConference; + } as VideoConference; } } diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index a9f5d450efad..c8fb0b7c4a21 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -1,6 +1,6 @@ import { LivechatVisitors } from '@rocket.chat/models'; -import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData'; +import { transformMappedData } from './transformMappedData'; // TODO: check if functions from this converter can be async export class AppVisitorsConverter { diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 1ce40b9415e5..f5315b4f1e6c 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -1020,13 +1020,13 @@ export class ImportDataConverter { return; } if (roomData.t === 'p') { - const user = await Users.findOneById(startedByUserId); + const user = await Users.findOneById(creatorId); if (!user) { throw new Error('importer-channel-invalid-creator'); } - roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}, true); + roomInfo = await createPrivateGroupMethod(user, roomData.name, members, false, {}, {}); } else { - roomInfo = await createChannelMethod(startedByUserId, roomData.name, members, false, {}, {}, true); + roomInfo = await createChannelMethod(creatorId, roomData.name, members, false, {}, {}); } } diff --git a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index 835f59419ad5..563022ea5a6a 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -1,15 +1,15 @@ import { Message } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; -import { Subscriptions, Rooms } from '@rocket.chat/models'; +import { Subscriptions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig'; +import { getDefaultChannels } from './getDefaultChannels'; export const addUserToDefaultChannels = async function (user: IUser, silenced?: boolean): Promise { await callbacks.run('beforeJoinDefaultChannels', user); - const defaultRooms = await Rooms.findByDefaultAndTypes(true, ['c', 'p'], { - projection: { usernames: 0 }, - }).toArray(); + const defaultRooms = await getDefaultChannels(); + for await (const room of defaultRooms) { if (!(await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { _id: 1 } }))) { const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user); diff --git a/apps/meteor/app/lib/server/functions/getDefaultChannels.ts b/apps/meteor/app/lib/server/functions/getDefaultChannels.ts new file mode 100644 index 000000000000..e47518dd604f --- /dev/null +++ b/apps/meteor/app/lib/server/functions/getDefaultChannels.ts @@ -0,0 +1,25 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; + +export async function getDefaultChannels(): Promise { + const defaultRooms = await Rooms.findByDefaultAndTypes(true, ['c', 'p'], { + projection: { usernames: 0 }, + }).toArray(); + const roomsThatAreGoingToBeJoined = new Set(defaultRooms.map((room) => room._id)); + + // If any of those are teams, we need to get all the channels that have the auto-join flag as well + const teamRooms = defaultRooms.filter((room) => room.teamMain && room.teamId); + if (teamRooms.length > 0) { + for await (const teamRoom of teamRooms) { + const defaultTeamRooms = await Rooms.findDefaultRoomsForTeam(teamRoom.teamId).toArray(); + + const defaultTeamRoomsThatWereNotAlreadyAdded = defaultTeamRooms.filter((channel) => !roomsThatAreGoingToBeJoined.has(channel._id)); + + defaultTeamRoomsThatWereNotAlreadyAdded.forEach((channel) => roomsThatAreGoingToBeJoined.add(channel._id)); + // Add the channels to the defaultRooms list + defaultRooms.push(...defaultTeamRoomsThatWereNotAlreadyAdded); + } + } + + return defaultRooms; +} diff --git a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts index d00f6d30b314..5a8b1c8d1a58 100644 --- a/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts +++ b/apps/meteor/app/lib/server/methods/insertOrUpdateUser.ts @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; import { saveUser } from '../functions/saveUser'; +import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,6 +15,8 @@ declare module '@rocket.chat/ui-contexts' { Meteor.methods({ insertOrUpdateUser: twoFactorRequired(async (userData) => { + methodDeprecationLogger.method('insertOrUpdateUser', '8.0.0'); + check(userData, Object); if (!Meteor.userId()) { diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 6fe7f1db7479..2d0120c93c82 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -39,7 +39,7 @@ const handleDeactivateUser = async (user: IUser) => { const handleActivateUser = async (user: IUser) => { if (isAgent(user) && user.username) { - await LivechatTyped.addAgent(user.username); + await LivechatTyped.afterAgentUserActivated(user); } }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 79d225626ea1..fb1098db4272 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1682,6 +1682,14 @@ class LivechatClass { return false; } + async afterAgentUserActivated(user: IUser) { + if (!user.roles.includes('livechat-agent')) { + throw new Error('invalid-user-role'); + } + await Users.setOperator(user._id, true); + callbacks.runAsync('livechat.onNewAgentCreated', user._id); + } + async addManager(username: string) { check(username, String); diff --git a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx new file mode 100644 index 000000000000..420bf93df66d --- /dev/null +++ b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx @@ -0,0 +1,65 @@ +import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useEffect } from 'react'; +import { useForm, Controller } from 'react-hook-form'; + +import GenericModal from '../../../../client/components/GenericModal'; + +type AddLinkComposerActionModalProps = { + selectedText?: string; + onConfirm: (url: string, text: string) => void; + onClose: () => void; +}; + +const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLinkComposerActionModalProps) => { + const t = useTranslation(); + const textField = useUniqueId(); + const urlField = useUniqueId(); + + const { handleSubmit, setFocus, control } = useForm({ + mode: 'onBlur', + defaultValues: { + text: selectedText || '', + url: '', + }, + }); + + useEffect(() => { + setFocus(selectedText ? 'url' : 'text'); + }, [selectedText, setFocus]); + + const onClickConfirm = ({ url, text }: { url: string; text: string }) => { + onConfirm(url, text); + }; + + const submit = handleSubmit(onClickConfirm); + + return ( + void submit(e)} {...props} />} + title={t('Add_link')} + > + + + {t('Text')} + + } /> + + + + {t('URL')} + + } /> + + + + + ); +}; + +export default AddLinkComposerActionModal; diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts index 8c170f894aa4..84ca6dcc1035 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts @@ -1,24 +1,34 @@ import type { Keys as IconName } from '@rocket.chat/icons'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; +import { imperativeModal } from '../../../../client/lib/imperativeModal'; import { settings } from '../../../settings/client'; +import AddLinkComposerActionModal from './AddLinkComposerActionModal'; -export type FormattingButton = - | { - label: TranslationKey; - icon: IconName; - pattern: string; - // text?: () => string | undefined; - command?: string; - link?: string; - condition?: () => boolean; - } - | { - label: TranslationKey; - text: () => string | undefined; - link: string; - condition?: () => boolean; - }; +type FormattingButtonDefault = { label: TranslationKey; condition?: () => boolean }; + +type TextButton = { + text: () => string | undefined; + link: string; +} & FormattingButtonDefault; + +type PatternButton = { + icon: IconName; + pattern: string; + // text?: () => string | undefined; + command?: string; + link?: string; +} & FormattingButtonDefault; + +type PromptButton = { + prompt: (composer: ComposerAPI) => void; + icon: IconName; +} & FormattingButtonDefault; + +export type FormattingButton = PatternButton | PromptButton | TextButton; + +export const isPromptButton = (button: FormattingButton): button is PromptButton => 'prompt' in button; export const formattingButtons: ReadonlyArray = [ { @@ -48,6 +58,28 @@ export const formattingButtons: ReadonlyArray = [ icon: 'multiline', pattern: '```\n{{text}}\n``` ', }, + { + label: 'Link', + icon: 'link', + prompt: (composerApi: ComposerAPI) => { + const { selection } = composerApi; + + const selectedText = composerApi.substring(selection.start, selection.end); + + const onClose = () => { + imperativeModal.close(); + composerApi.focus(); + }; + + const onConfirm = (url: string, text: string) => { + onClose(); + composerApi.replaceText(`[${text}](${url})`, selection); + composerApi.setCursorToEnd(); + }; + + imperativeModal.open({ component: AddLinkComposerActionModal, props: { onConfirm, selectedText, onClose } }); + }, + }, { label: 'KaTeX' as TranslationKey, icon: 'katex', diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 2ad7deedafc3..c17eb89c4596 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -4,7 +4,7 @@ import { useToggle } from '@rocket.chat/fuselage-hooks'; import { MessageAvatar } from '@rocket.chat/ui-avatar'; import { useUserId } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; -import React, { useRef, memo } from 'react'; +import React, { memo } from 'react'; import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext'; @@ -51,7 +51,6 @@ const RoomMessage = ({ const editing = useIsMessageHighlight(message._id); const [displayIgnoredMessage, toggleDisplayIgnoredMessage] = useToggle(false); const ignored = (ignoredUser || message.ignored) && !displayIgnoredMessage; - const messageRef = useRef(null); const { openUserCard, triggerProps } = useUserCard(); const selecting = useIsSelecting(); @@ -59,7 +58,8 @@ const RoomMessage = ({ const selected = useIsSelectedMessage(message._id); useCountSelected(); - useJumpToMessage(message._id, messageRef); + + const messageRef = useJumpToMessage(message._id); return ( { const t = useTranslation(); diff --git a/apps/meteor/ee/client/lib/voip/parseOutboundPhoneNumber.ts b/apps/meteor/client/lib/voip/parseOutboundPhoneNumber.ts similarity index 100% rename from apps/meteor/ee/client/lib/voip/parseOutboundPhoneNumber.ts rename to apps/meteor/client/lib/voip/parseOutboundPhoneNumber.ts diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index 0de0a3fda0c3..ce0d9385dc64 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -29,13 +29,13 @@ import { createPortal } from 'react-dom'; import type { OutgoingByeRequest } from 'sip.js/lib/core'; import { isOutboundClient, useVoipClient } from '../../../ee/client/hooks/useVoipClient'; -import { parseOutboundPhoneNumber } from '../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { WrapUpCallModal } from '../../../ee/client/voip/components/modals/WrapUpCallModal'; import type { CallContextValue } from '../../contexts/CallContext'; import { CallContext, useIsVoipEnterprise } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; import type { QueueAggregator } from '../../lib/voip/QueueAggregator'; +import { parseOutboundPhoneNumber } from '../../lib/voip/parseOutboundPhoneNumber'; import { useVoipSounds } from './hooks/useVoipSounds'; type NetworkState = 'online' | 'offline'; diff --git a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts index e905f6cf38e6..6b3d59830d1e 100644 --- a/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts +++ b/apps/meteor/client/sidebar/footer/voip/hooks/useOmnichannelContactLabel.ts @@ -2,7 +2,7 @@ import type { ICallerInfo } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; +import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; export const useOmnichannelContactLabel = (caller: ICallerInfo): string => { const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index 18edd8cc815a..1522f4694160 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -182,7 +182,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps) => { {t('Owner')} - + )} diff --git a/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx b/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx index 403f4312772f..616baee2b531 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx @@ -4,9 +4,9 @@ import moment from 'moment'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; -import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable'; import { useIsCallReady } from '../../../../contexts/CallContext'; +import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; import { CallDialpadButton } from '../components/CallDialpadButton'; type CallTableRowProps = { diff --git a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx index 65792b6d73e2..ebfa98273e30 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx @@ -6,7 +6,6 @@ import moment from 'moment'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import { parseOutboundPhoneNumber } from '../../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import { ContextualbarIcon, ContextualbarHeader, @@ -18,6 +17,7 @@ import { import InfoPanel from '../../../../../components/InfoPanel'; import { UserStatus } from '../../../../../components/UserStatus'; import { useIsCallReady } from '../../../../../contexts/CallContext'; +import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber'; import AgentInfoDetails from '../../../components/AgentInfoDetails'; import AgentField from '../../components/AgentField'; import { InfoField } from './InfoField'; diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx index a6516e082e88..1238732edf66 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx @@ -5,7 +5,6 @@ import { hashQueryKey } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; -import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import FilterByText from '../../../../components/FilterByText'; import GenericNoResults from '../../../../components/GenericNoResults'; import { @@ -21,6 +20,7 @@ import { usePagination } from '../../../../components/GenericTable/hooks/usePagi import { useSort } from '../../../../components/GenericTable/hooks/useSort'; import { useIsCallReady } from '../../../../contexts/CallContext'; import { useFormatDate } from '../../../../hooks/useFormatDate'; +import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; import { CallDialpadButton } from '../components/CallDialpadButton'; import { useCurrentContacts } from './hooks/useCurrentContacts'; diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx index 9578c6a07b83..f16f70c76446 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx @@ -7,12 +7,12 @@ import { useQuery } from '@tanstack/react-query'; import React, { useCallback } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { parseOutboundPhoneNumber } from '../../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import ContactManagerInfo from '../../../../../../ee/client/omnichannel/ContactManagerInfo'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../../components/Contextualbar'; import { UserStatus } from '../../../../../components/UserStatus'; import { useIsCallReady } from '../../../../../contexts/CallContext'; import { useFormatDate } from '../../../../../hooks/useFormatDate'; +import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber'; import AgentInfoDetails from '../../../components/AgentInfoDetails'; import CustomField from '../../../components/CustomField'; import Field from '../../../components/Field'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx index c4c5a63a00c1..042379b9fde6 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx @@ -5,8 +5,8 @@ import type { FC } from 'react'; import React, { useCallback, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import { parseOutboundPhoneNumber } from '../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import BurgerMenu from '../../../../components/BurgerMenu'; +import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; import type { RoomHeaderProps } from '../RoomHeader'; import RoomHeader from '../RoomHeader'; import { BackButton } from './BackButton'; diff --git a/apps/meteor/client/views/room/Header/ParentRoom.tsx b/apps/meteor/client/views/room/Header/ParentRoom.tsx index 00b181f47df5..3d598cce4c26 100644 --- a/apps/meteor/client/views/room/Header/ParentRoom.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoom.tsx @@ -13,10 +13,15 @@ type ParentRoomProps = { const ParentRoom = ({ room }: ParentRoomProps): ReactElement => { const icon = useRoomIcon(room); - const handleClick = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); + const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); return ( - + (e.code === 'Space' || e.code === 'Enter') && handleRedirect()} + onClick={handleRedirect} + > {roomCoordinator.getRoomName(room.t, room)} diff --git a/apps/meteor/client/views/room/Header/ParentTeam.tsx b/apps/meteor/client/views/room/Header/ParentTeam.tsx index 2f1f15327c79..33ef98bbe81b 100644 --- a/apps/meteor/client/views/room/Header/ParentTeam.tsx +++ b/apps/meteor/client/views/room/Header/ParentTeam.tsx @@ -41,11 +41,14 @@ const ParentTeam = ({ room }: { room: IRoom }): ReactElement | null => { const redirectToMainRoom = (): void => { const rid = teamInfoData?.teamInfo.roomId; - if (!rid) { return; } + if (!(isTeamPublic || userBelongsToTeam)) { + return; + } + goToRoomById(rid); }; @@ -58,7 +61,12 @@ const ParentTeam = ({ room }: { room: IRoom }): ReactElement | null => { } return ( - + (e.code === 'Space' || e.code === 'Enter') && redirectToMainRoom()} + onClick={redirectToMainRoom} + > {teamInfoData?.teamInfo.name} diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx index 13b628ab3823..4d81d077c154 100644 --- a/apps/meteor/client/views/room/Header/RoomTitle.tsx +++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx @@ -1,22 +1,50 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { HeaderTitle, useDocumentTitle } from '@rocket.chat/ui-client'; -import type { ReactElement } from 'react'; +import { isTeamRoom, type IRoom } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { HeaderTitle, HeaderTitleButton, useDocumentTitle } from '@rocket.chat/ui-client'; +import type { KeyboardEvent, ReactElement } from 'react'; import React from 'react'; +import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import HeaderIconWithRoom from './HeaderIconWithRoom'; -type RoomTitleProps = { - room: IRoom; -}; - -const RoomTitle = ({ room }: RoomTitleProps): ReactElement => { +const RoomTitle = ({ room }: { room: IRoom }): ReactElement => { useDocumentTitle(room.name, false); + const { openTab } = useRoomToolbox(); + + const handleOpenRoomInfo = useEffectEvent(() => { + if (isTeamRoom(room)) { + return openTab('team-info'); + } + + switch (room.t) { + case 'l': + openTab('room-info'); + break; + + case 'v': + openTab('voip-room-info'); + break; + + case 'd': + (room.uids?.length ?? 0) > 2 ? openTab('user-info-group') : openTab('user-info'); + break; + + default: + openTab('channel-settings'); + break; + } + }); return ( - <> + (e.code === 'Enter' || e.code === 'Space') && handleOpenRoomInfo()} + onClick={() => handleOpenRoomInfo()} + tabIndex={0} + role='button' + > {room.name} - + ); }; diff --git a/apps/meteor/client/views/room/Header/icons/Favorite.tsx b/apps/meteor/client/views/room/Header/icons/Favorite.tsx index f66af3443ed8..4a99a7a0411e 100644 --- a/apps/meteor/client/views/room/Header/icons/Favorite.tsx +++ b/apps/meteor/client/views/room/Header/icons/Favorite.tsx @@ -1,39 +1,41 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; import { useUserIsSubscribed } from '../../contexts/RoomContext'; -const Favorite = ({ room: { _id, f: favorite = false, t: type } }: { room: IRoom & { f?: ISubscription['f'] } }) => { +const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: IRoom & { f?: ISubscription['f'] } }) => { const t = useTranslation(); const subscribed = useUserIsSubscribed(); const isFavoritesEnabled = useSetting('Favorite_Rooms') && ['c', 'p', 'd', 't'].includes(type); const toggleFavorite = useMethod('toggleFavorite'); - const handleFavoriteClick = useMutableCallback(() => { + + const handleFavoriteClick = useEffectEvent(() => { if (!isFavoritesEnabled) { return; } + toggleFavorite(_id, !favorite); }); - const favoriteLabel = favorite ? t('Unfavorite') : t('Favorite'); + + const favoriteLabel = favorite ? `${t('Unfavorite')} ${name}` : `${t('Favorite')} ${name}`; if (!subscribed || !isFavoritesEnabled) { return null; } return ( - isFavoritesEnabled && ( - - ) + ); }; diff --git a/apps/meteor/client/views/room/MessageList/MessageList.tsx b/apps/meteor/client/views/room/MessageList/MessageList.tsx index 5a3a267b73dc..b39981d44bb7 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.tsx @@ -1,20 +1,15 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isThreadMessage } from '@rocket.chat/core-typings'; -import { MessageDivider } from '@rocket.chat/fuselage'; -import { useSetting, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentProps } from 'react'; -import React, { Fragment, memo } from 'react'; +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; +import React, { Fragment, forwardRef } from 'react'; import { MessageTypes } from '../../../../app/ui-utils/client'; -import RoomMessage from '../../../components/message/variants/RoomMessage'; -import SystemMessage from '../../../components/message/variants/SystemMessage'; -import ThreadMessagePreview from '../../../components/message/variants/ThreadMessagePreview'; -import { useFormatDate } from '../../../hooks/useFormatDate'; import { useRoomSubscription } from '../contexts/RoomContext'; import { useFirstUnreadMessageId } from '../hooks/useFirstUnreadMessageId'; import { SelectedMessagesProvider } from '../providers/SelectedMessagesProvider'; +import { MessageListItem } from './MessageListItem'; import { useMessages } from './hooks/useMessages'; -import { isMessageNewDay } from './lib/isMessageNewDay'; import { isMessageSequential } from './lib/isMessageSequential'; import MessageListProvider from './providers/MessageListProvider'; @@ -23,14 +18,11 @@ type MessageListProps = { scrollMessageList: ComponentProps['scrollMessageList']; }; -export const MessageList = ({ rid, scrollMessageList }: MessageListProps): ReactElement => { - const t = useTranslation(); +export const MessageList = forwardRef(function MessageList({ rid, scrollMessageList }: MessageListProps) { const messages = useMessages({ rid }); const subscription = useRoomSubscription(); const showUserAvatar = !!useUserPreference('displayAvatars'); const messageGroupingPeriod = Number(useSetting('Message_GroupingPeriod')); - const formatDate = useFormatDate(); - const firstUnreadMessageId = useFirstUnreadMessageId(); return ( @@ -38,62 +30,26 @@ export const MessageList = ({ rid, scrollMessageList }: MessageListProps): React {messages.map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); - - const newDay = isMessageNewDay(message, previous); - const showUnreadDivider = firstUnreadMessageId === message._id; - - const showDivider = newDay || showUnreadDivider; - - const shouldShowAsSequential = sequential && !newDay; - const system = MessageTypes.isSystemMessage(message); const visible = !isThreadMessage(message) && !system; - const unread = Boolean(subscription?.tunread?.includes(message._id)); - const mention = Boolean(subscription?.tunreadUser?.includes(message._id)); - const all = Boolean(subscription?.tunreadGroup?.includes(message._id)); - const ignoredUser = Boolean(subscription?.ignored?.includes(message.u._id)); - return ( - {showDivider && ( - - {newDay && formatDate(message.ts)} - - )} - - {visible && ( - - )} - - {isThreadMessage(message) && ( - - )} - - {system && } + ); })} ); -}; - -export default memo(MessageList); +}); diff --git a/apps/meteor/client/views/room/MessageList/MessageListItem.tsx b/apps/meteor/client/views/room/MessageList/MessageListItem.tsx new file mode 100644 index 000000000000..538290c8a7ad --- /dev/null +++ b/apps/meteor/client/views/room/MessageList/MessageListItem.tsx @@ -0,0 +1,92 @@ +import { isThreadMessage, type IMessage, type ISubscription } from '@rocket.chat/core-typings'; +import { Box, Bubble, MessageDivider } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import RoomMessage from '../../../components/message/variants/RoomMessage'; +import SystemMessage from '../../../components/message/variants/SystemMessage'; +import ThreadMessagePreview from '../../../components/message/variants/ThreadMessagePreview'; +import { useFormatDate } from '../../../hooks/useFormatDate'; +import { useDateRef } from '../providers/DateListProvider'; +import { isMessageNewDay } from './lib/isMessageNewDay'; + +type MessageListItemProps = { + message: IMessage; + previous?: IMessage; + showUnreadDivider: boolean; + + sequential: boolean; + showUserAvatar: boolean; + visible: boolean; + subscription: ISubscription | undefined; + system: boolean; +}; +export const MessageListItem = ({ + message, + previous, + showUnreadDivider, + sequential, + showUserAvatar, + visible, + subscription, + system, +}: MessageListItemProps) => { + const t = useTranslation(); + const formatDate = useFormatDate(); + + const ref = useDateRef(); + + const newDay = isMessageNewDay(message, previous); + const showDivider = newDay || showUnreadDivider; + const unread = Boolean(subscription?.tunread?.includes(message._id)); + const mention = Boolean(subscription?.tunreadUser?.includes(message._id)); + const all = Boolean(subscription?.tunreadGroup?.includes(message._id)); + const ignoredUser = Boolean(subscription?.ignored?.includes(message.u._id)); + const shouldShowAsSequential = sequential && !newDay; + + return ( + <> + {showDivider && ( + + + {newDay && ( + + {formatDate(message.ts)} + + )} + + + )} + {visible && ( + + )} + {isThreadMessage(message) && ( + + )} + {system && } + + ); +}; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts index 40712c262398..b875a6abca69 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts @@ -1,7 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { useRouter } from '@rocket.chat/ui-contexts'; -import type { RefObject } from 'react'; -import { useLayoutEffect } from 'react'; +import { useCallback } from 'react'; import { useMessageListJumpToMessageParam, useMessageListScroll } from '../../../../components/message/list/MessageListContext'; import { setHighlightMessage, clearHighlightMessage } from '../providers/messageHighlightSubscription'; @@ -9,43 +8,51 @@ import { setHighlightMessage, clearHighlightMessage } from '../providers/message // this is an arbitrary value so that there's a gap between the header and the message; const SCROLL_EXTRA_OFFSET = 60; -export const useJumpToMessage = (messageId: IMessage['_id'], messageRef: RefObject): void => { +export const useJumpToMessage = (messageId: IMessage['_id']) => { const jumpToMessageParam = useMessageListJumpToMessageParam(); const scroll = useMessageListScroll(); const router = useRouter(); - useLayoutEffect(() => { - if (jumpToMessageParam !== messageId || !messageRef.current || !scroll) { - return; - } - - setTimeout(() => { - scroll((wrapper) => { - if (!wrapper || !messageRef.current) { - return; - } - const containerRect = wrapper.getBoundingClientRect(); - const messageRect = messageRef.current.getBoundingClientRect(); - - const offset = messageRect.top - containerRect.top; - const scrollPosition = wrapper.scrollTop; - const newScrollPosition = scrollPosition + offset - SCROLL_EXTRA_OFFSET; - - return { top: newScrollPosition, behavior: 'smooth' }; - }); - - const search = router.getSearchParameters(); - delete search.msg; - router.navigate( - { - pathname: router.getLocationPathname(), - search, - }, - { replace: true }, - ); - - setHighlightMessage(messageId); - setTimeout(clearHighlightMessage, 2000); - }, 500); - }, [messageId, jumpToMessageParam, messageRef, scroll, router]); + const ref = useCallback( + (node: HTMLElement | null) => { + if (!node || !scroll) { + return; + } + setTimeout(() => { + scroll((wrapper) => { + if (!wrapper || !node) { + return; + } + const containerRect = wrapper.getBoundingClientRect(); + const messageRect = node.getBoundingClientRect(); + + const offset = messageRect.top - containerRect.top; + const scrollPosition = wrapper.scrollTop; + const newScrollPosition = scrollPosition + offset - SCROLL_EXTRA_OFFSET; + + return { top: newScrollPosition, behavior: 'smooth' }; + }); + + const { msg: _, ...search } = router.getSearchParameters(); + + router.navigate( + { + pathname: router.getLocationPathname(), + search, + }, + { replace: true }, + ); + + setHighlightMessage(messageId); + setTimeout(clearHighlightMessage, 2000); + }, 500); + }, + [messageId, router, scroll], + ); + + if (jumpToMessageParam !== messageId) { + return undefined; + } + + return ref; }; diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index 4d6d7d56f9d5..8dcc76a20e57 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -13,6 +13,7 @@ import { useRoomToolbox } from './contexts/RoomToolboxContext'; import { useAppsContextualBar } from './hooks/useAppsContextualBar'; import RoomLayout from './layout/RoomLayout'; import ChatProvider from './providers/ChatProvider'; +import { DateListProvider } from './providers/DateListProvider'; import { SelectedMessagesProvider } from './providers/SelectedMessagesProvider'; const UiKitContextualBar = lazy(() => import('./contextualBar/uikit/UiKitContextualBar')); @@ -27,34 +28,36 @@ const Room = (): ReactElement => { - } - body={} - aside={ - (toolbox.tab?.tabComponent && ( - - - }>{createElement(toolbox.tab.tabComponent)} - - - )) || - (contextualBarView && ( - - - }> - - - - - )) - } - /> + + } + body={} + aside={ + (toolbox.tab?.tabComponent && ( + + + }>{createElement(toolbox.tab.tabComponent)} + + + )) || + (contextualBarView && ( + + + }> + + + + + )) + } + /> + diff --git a/apps/meteor/client/views/room/body/LeaderBar.tsx b/apps/meteor/client/views/room/body/LeaderBar.tsx index 4cf5266b89d8..bb0ba305633a 100644 --- a/apps/meteor/client/views/room/body/LeaderBar.tsx +++ b/apps/meteor/client/views/room/body/LeaderBar.tsx @@ -66,7 +66,7 @@ const LeaderBar = ({ _id, name, username, visible, onAvatarClick, triggerProps } className={[roomLeaderStyle, 'room-leader', !visible && 'animated-hidden'].filter(isTruthy)} > - + diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 5dd6d9e6bb67..d6fb6e8cbfe1 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -1,5 +1,7 @@ import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { isEditedMessage } from '@rocket.chat/core-typings'; +import { Box, Bubble } from '@rocket.chat/fuselage'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, @@ -22,13 +24,14 @@ import { isTruthy } from '../../../../lib/isTruthy'; import { withDebouncing, withThrottling } from '../../../../lib/utils/highOrderFunctions'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; +import { useFormatDate } from '../../../hooks/useFormatDate'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { RoomManager } from '../../../lib/RoomManager'; import type { Upload } from '../../../lib/chats/Upload'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import { setMessageJumpQueryStringParameter } from '../../../lib/utils/setMessageJumpQueryStringParameter'; import Announcement from '../Announcement'; -import { MessageList } from '../MessageList/MessageList'; +import { MessageList } from '../MessageList'; import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; import ComposerContainer from '../composer/ComposerContainer'; import RoomComposer from '../composer/RoomComposer/RoomComposer'; @@ -36,8 +39,10 @@ import { useChat } from '../contexts/ChatContext'; import { useRoom, useRoomSubscription, useRoomMessages } from '../contexts/RoomContext'; import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import { useUserCard } from '../contexts/UserCardContext'; +import { useDateScroll } from '../hooks/useDateScroll'; import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useScrollMessageList } from '../hooks/useScrollMessageList'; +import { useDateListController } from '../providers/DateListProvider'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; import LeaderBar from './LeaderBar'; @@ -54,6 +59,7 @@ import { useRetentionPolicy } from './hooks/useRetentionPolicy'; import { useUnreadMessages } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { + const formatDate = useFormatDate(); const t = useTranslation(); const isLayoutEmbedded = useEmbeddedLayout(); const room = useRoom(); @@ -62,6 +68,9 @@ const RoomBody = (): ReactElement => { const admin = useRole('admin'); const subscription = useRoomSubscription(); + const { list } = useDateListController(); + const { callbackRef, listStyle, bubbleDate, showBubble, style: bubbleDateStyle } = useDateScroll(); + const [lastMessageDate, setLastMessageDate] = useState(); const [hideLeaderHeader, setHideLeaderHeader] = useState(false); const [hasNewMessages, setHasNewMessages] = useState(false); @@ -385,7 +394,7 @@ const RoomBody = (): ReactElement => { wrapper.removeEventListener('scroll', updateUnreadCount); wrapper.removeEventListener('scroll', handleWrapperScroll); }; - }, [_isAtBottom, room._id, setUnreadCount]); + }, [_isAtBottom, list, room._id, setUnreadCount]); useEffect(() => { const wrapper = wrapperRef.current; @@ -537,17 +546,19 @@ const RoomBody = (): ReactElement => { const { messageListRef, messageListProps } = useMessageListNavigation(); + const ref = useMergedRefs(callbackRef, wrapperRef); + return ( <> {!isLayoutEmbedded && room.announcement && } -
+
-
+
{uploads.map((upload) => ( @@ -561,6 +572,13 @@ const RoomBody = (): ReactElement => { /> ))}
+ {bubbleDate && ( + + + {formatDate(bubbleDate)} + + + )} {unread && ( { .join(' ')} > - +
    {
-
+
); }; diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForeword.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForeword.tsx index 63ba8f895e6c..a765407ad6d4 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForeword.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForeword.tsx @@ -20,7 +20,7 @@ const RoomForeword = ({ user, room }: RoomForewordProps): ReactElement | null => if (!isDirectMessageRoom(room)) { return ( - + {t('Start_of_conversation')} ); @@ -33,7 +33,7 @@ const RoomForeword = ({ user, room }: RoomForewordProps): ReactElement | null => } return ( - + {usernames.map((username, index) => ( diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx index 21eb5b94f6d6..b2ba79792e04 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx @@ -1,7 +1,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; +import { isPromptButton, type FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem'; import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI'; @@ -21,6 +21,9 @@ const FormattingToolbarDropdown = ({ composer, items, disabled }: FormattingTool window.open(formatter.link, '_blank', 'rel=noreferrer noopener'); return; } + if (isPromptButton(formatter)) { + return formatter.prompt(composer); + } composer.wrapSelection(formatter.pattern); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx index afe8a58ec72a..79b210182d77 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx @@ -3,6 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; +import { isPromptButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI'; import FormattingToolbarDropdown from './FormattingToolbarDropdown'; @@ -24,7 +25,9 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab <> {'icon' in featuredFormatter && ( composer.wrapSelection(featuredFormatter.pattern)} + onClick={() => + isPromptButton(featuredFormatter) ? featuredFormatter.prompt(composer) : composer.wrapSelection(featuredFormatter.pattern) + } icon={featuredFormatter.icon} disabled={disabled} /> @@ -45,6 +48,10 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab data-id={formatter.label} title={t(formatter.label)} onClick={(): void => { + if (isPromptButton(formatter)) { + formatter.prompt(composer); + return; + } if ('link' in formatter) { window.open(formatter.link, '_blank', 'rel=noreferrer noopener'); 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 510d4f3e8017..7afe0b442bbc 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -15,6 +15,7 @@ import RoomComposer from '../../../composer/RoomComposer/RoomComposer'; import { useChat } from '../../../contexts/ChatContext'; import { useRoom, useRoomSubscription } from '../../../contexts/RoomContext'; import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; +import { DateListProvider } from '../../../providers/DateListProvider'; import ThreadMessageList from './ThreadMessageList'; type ThreadChatProps = { @@ -93,39 +94,50 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { return ( - - - - - - - - - - - setSendToChannel((checked) => !checked)} - name='alsoSendThreadToChannel' - /> - - {t('Also_send_to_channel')} - - - - - - + + + + + + + + + + + + setSendToChannel((checked) => !checked)} + name='alsoSendThreadToChannel' + /> + + {t('Also_send_to_channel')} + + + + + + + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx new file mode 100644 index 000000000000..1a7da3e0ea63 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageItem.tsx @@ -0,0 +1,67 @@ +import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typings'; +import { Box, Bubble, MessageDivider } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import SystemMessage from '../../../../../components/message/variants/SystemMessage'; +import ThreadMessage from '../../../../../components/message/variants/ThreadMessage'; +import { useFormatDate } from '../../../../../hooks/useFormatDate'; +import { isMessageNewDay } from '../../../MessageList/lib/isMessageNewDay'; +import { useDateRef } from '../../../providers/DateListProvider'; + +type ThreadMessageProps = { + message: IThreadMessage | IThreadMainMessage; + previous: IThreadMessage | IThreadMainMessage; + sequential: boolean; + shouldShowAsSequential: boolean; + showUserAvatar: boolean; + firstUnread: boolean; + system: boolean; +}; + +export const ThreadMessageItem = ({ + message, + previous, + shouldShowAsSequential, + showUserAvatar, + firstUnread, + system, +}: ThreadMessageProps) => { + const t = useTranslation(); + const formatDate = useFormatDate(); + const ref = useDateRef(); + + const newDay = isMessageNewDay(message, previous); + + const showDivider = newDay || firstUnread; + + return ( + <> + {showDivider && ( + + + {newDay && ( + + {formatDate(message.ts)} + + )} + + + )} +
  • + {system ? ( + + ) : ( + + )} +
  • + + ); +}; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index 48b764d51816..77c2854cd7ce 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -1,7 +1,7 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; -import { MessageDivider } from '@rocket.chat/fuselage'; +import { Box, Bubble } from '@rocket.chat/fuselage'; import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import { differenceInSeconds } from 'date-fns'; import type { ReactElement } from 'react'; import React, { Fragment } from 'react'; @@ -9,18 +9,18 @@ import React, { Fragment } from 'react'; import { MessageTypes } from '../../../../../../app/ui-utils/client'; import { isTruthy } from '../../../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../../../components/CustomScrollbars'; -import SystemMessage from '../../../../../components/message/variants/SystemMessage'; -import ThreadMessage from '../../../../../components/message/variants/ThreadMessage'; import { useFormatDate } from '../../../../../hooks/useFormatDate'; import { isMessageNewDay } from '../../../MessageList/lib/isMessageNewDay'; import MessageListProvider from '../../../MessageList/providers/MessageListProvider'; import LoadingMessagesIndicator from '../../../body/LoadingMessagesIndicator'; +import { useDateScroll } from '../../../hooks/useDateScroll'; import { useFirstUnreadMessageId } from '../../../hooks/useFirstUnreadMessageId'; import { useMessageListNavigation } from '../../../hooks/useMessageListNavigation'; import { useScrollMessageList } from '../../../hooks/useScrollMessageList'; import { useLegacyThreadMessageJump } from '../hooks/useLegacyThreadMessageJump'; import { useLegacyThreadMessageListScrolling } from '../hooks/useLegacyThreadMessageListScrolling'; import { useLegacyThreadMessages } from '../hooks/useLegacyThreadMessages'; +import { ThreadMessageItem } from './ThreadMessageItem'; const isMessageSequential = (current: IMessage, previous: IMessage | undefined, groupingRange: number): boolean => { if (!previous) { @@ -50,6 +50,9 @@ type ThreadMessageListProps = { }; const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElement => { + const formatDate = useFormatDate(); + const { callbackRef, listStyle, bubbleDate, showBubble, style: bubbleDateStyle } = useDateScroll(); + const { messages, loading } = useLegacyThreadMessages(mainMessage._id); const { listWrapperRef: listWrapperScrollRef, @@ -60,22 +63,38 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const hideUsernames = useUserPreference('hideUsernames'); const showUserAvatar = !!useUserPreference('displayAvatars'); - - const formatDate = useFormatDate(); - const t = useTranslation(); + const firstUnreadMessageId = useFirstUnreadMessageId(); const messageGroupingPeriod = Number(useSetting('Message_GroupingPeriod')); const scrollMessageList = useScrollMessageList(listWrapperScrollRef); - - const firstUnreadMessageId = useFirstUnreadMessageId(); const { messageListRef, messageListProps } = useMessageListNavigation(); - const listRef = useMergedRefs(listScrollRef, listJumpRef, messageListRef); + const scrollRef = useMergedRefs(callbackRef, listWrapperScrollRef); + return (
    - -
      + {bubbleDate && ( + + + {formatDate(bubbleDate)} + + + )} + { + handleScroll(args); + }} + style={{ scrollBehavior: 'smooth', overflowX: 'hidden' }} + > + {loading ? (
    • @@ -85,38 +104,28 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen {[mainMessage, ...messages].map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous); - const firstUnread = firstUnreadMessageId === message._id; - const showDivider = newDay || firstUnread; - const shouldShowAsSequential = sequential && !newDay; + const firstUnread = firstUnreadMessageId === message._id; const system = MessageTypes.isSystemMessage(message); return ( - {showDivider && ( - - {newDay && formatDate(message.ts)} - - )} -
    • - {system ? ( - - ) : ( - - )} -
    • + ); })} )} -
    +
    ); diff --git a/apps/meteor/client/views/room/hooks/useDateScroll.ts b/apps/meteor/client/views/room/hooks/useDateScroll.ts new file mode 100644 index 000000000000..c10aa3bf9640 --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useDateScroll.ts @@ -0,0 +1,120 @@ +import { css } from '@rocket.chat/css-in-js'; +import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { useCallback, useState } from 'react'; + +import { withThrottling } from '../../../../lib/utils/highOrderFunctions'; +import { useDateListController } from '../providers/DateListProvider'; + +type useDateScrollReturn = { + bubbleDate: string | undefined; + callbackRef: (node: HTMLElement | null) => void; + style?: ReturnType; + showBubble: boolean; + listStyle?: ReturnType; +}; + +export const useDateScroll = (margin = 8): useDateScrollReturn => { + const [bubbleDate, setBubbleDate] = useSafely( + useState<{ + date: string; + show: boolean; + style?: ReturnType; + }>({ + date: '', + show: false, + style: undefined, + }), + ); + + const { list } = useDateListController(); + + const callbackRef = useCallback( + (node: HTMLElement | null) => { + if (!node) { + return; + } + const onScroll = (() => { + let timeout: ReturnType; + return (elements: Set, offset: number) => { + clearTimeout(timeout); + + const bubbleOffset = offset; + + // Gets the first non visible message date and sets the bubble date to it + const [date, message] = [...elements].reduce((ret, message) => { + // Sanitize elements + if (!message.dataset.id) { + return ret; + } + + const { top } = message.getBoundingClientRect(); + const { id } = message.dataset; + + if (top < bubbleOffset) { + // Remove T - . : from the date + return [new Date(id).toISOString(), message]; + } + return ret; + }, [] as [string, HTMLElement] | []); + + // We always keep the previous date if we don't have a new one, so when the bubble disappears it doesn't flicker + + setBubbleDate(() => ({ + date: '', + ...(date && { date }), + show: Boolean(date), + style: css` + position: absolute; + top: ${margin}px; + left: 50%; + translate: -50%; + z-index: 1; + + opacity: 0; + transition: opacity 0.6s; + + &.bubble-visible { + opacity: 1; + } + `, + })); + + if (message) { + const { top } = message.getBoundingClientRect(); + if (top < offset && top > 0) { + return; + } + } + + timeout = setTimeout( + () => + setBubbleDate((current) => ({ + ...current, + show: false, + })), + 1000, + ); + }; + })(); + const fn = withThrottling({ wait: 30 })(() => { + const offset = node.getBoundingClientRect().top; + onScroll(list, offset); + }); + + node.addEventListener('scroll', fn, { passive: true }); + }, + [list, margin, setBubbleDate], + ); + + const listStyle = + bubbleDate.show && bubbleDate.date + ? css` + position: relative; + & [data-time='${bubbleDate.date.replaceAll(/[-T:.]/g, '').substring(0, 8)}'] { + opacity: 0; + } + ` + : undefined; + + return { callbackRef, listStyle, bubbleDate: bubbleDate.date, style: bubbleDate.style, showBubble: Boolean(bubbleDate.show) }; +}; diff --git a/apps/meteor/client/views/room/hooks/useScrollMessageList.ts b/apps/meteor/client/views/room/hooks/useScrollMessageList.ts index 489ecbced48e..4b324dbcb0fa 100644 --- a/apps/meteor/client/views/room/hooks/useScrollMessageList.ts +++ b/apps/meteor/client/views/room/hooks/useScrollMessageList.ts @@ -1,11 +1,11 @@ import type { ComponentProps, RefObject } from 'react'; import { useCallback } from 'react'; -import type { MessageList } from '../MessageList'; +import type MessageListProvider from '../MessageList/providers/MessageListProvider'; export const useScrollMessageList = ( wrapperRef: RefObject, -): Exclude['scrollMessageList'], undefined> => { +): Exclude['scrollMessageList'], undefined> => { // Passing a callback instead of the values so that the wrapper is exposed return useCallback( (callback) => { diff --git a/apps/meteor/client/views/room/providers/DateListProvider.tsx b/apps/meteor/client/views/room/providers/DateListProvider.tsx new file mode 100644 index 000000000000..43fd70943b45 --- /dev/null +++ b/apps/meteor/client/views/room/providers/DateListProvider.tsx @@ -0,0 +1,46 @@ +import React, { createContext, useContext, useMemo, useState } from 'react'; + +type DateListContextValue = { + list: Set; + dateRef: () => (ref: HTMLElement | null) => void; +}; + +const DateListContext = createContext(undefined); + +const useDateRef = () => { + const context = useDateListController(); + return useMemo(() => context.dateRef(), [context]); +}; + +const DateListProvider = ({ children }: { children: React.ReactNode }) => { + const [list] = useState>(new Set()); + + const addToList = (value: HTMLElement) => { + list.add(value); + return () => { + list.delete(value); + }; + }; + + const dateRef = () => { + let remove: () => void; + return (ref: HTMLElement | null) => { + if (remove) remove(); + + if (!ref) return; + remove = addToList(ref); + }; + }; + + return {children}; +}; + +const useDateListController = () => { + const context = useContext(DateListContext); + if (!context) { + throw new Error('useDateController must be used within an DateScrollProvider'); + } + return context; +}; + +export { DateListProvider, useDateListController, useDateRef }; diff --git a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx index 1edd6966d065..b12a035d32cd 100644 --- a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx +++ b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx @@ -1,7 +1,6 @@ -import { Box } from '@rocket.chat/fuselage'; +import { Box, PaletteStyleTag } from '@rocket.chat/fuselage'; import { useLayout, useSetting, useCurrentModal, useRoute, useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import { PaletteStyleTag } from '@rocket.chat/ui-theming/src/PaletteStyleTag'; -import { SidebarPaletteStyleTag } from '@rocket.chat/ui-theming/src/SidebarPaletteStyleTag'; +import { useThemeMode } from '@rocket.chat/ui-theming/src/hooks/useThemeMode'; import type { ReactElement, ReactNode } from 'react'; import React, { useEffect, useRef } from 'react'; @@ -10,6 +9,7 @@ import AccessibilityShortcut from './AccessibilityShortcut'; const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement => { const { isEmbedded: embeddedLayout } = useLayout(); + const [, , theme] = useThemeMode(); const modal = useCurrentModal(); const currentRoutePath = useCurrentRoutePath(); @@ -48,8 +48,8 @@ const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement aria-hidden={Boolean(modal)} > - - + + {!removeSidenav && }
    { }); Accounts.onLogin((login: any): void => { - if (login.type !== 'resume') { + if (login.type !== 'resume' || !login.connection.id) { return; } + + // validate if it is a real WS connection and is still open + const session = Meteor.server.sessions.get(login.connection.id); + if (!session) { + return; + } + void (async function () { await Presence.newConnection(login.user._id, login.connection.id, nodeId); updateConns(); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 26dc53518ce3..68a608f7787f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -122,6 +122,7 @@ "@types/later": "^1.2.8", "@types/ldapjs": "^2.2.5", "@types/less": "~3.0.5", + "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.get": "^4.4.8", "@types/mailparser": "^3.4.3", "@types/marked": "^4.0.8", @@ -229,6 +230,7 @@ "@rocket.chat/account-utils": "workspace:^", "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", + "@rocket.chat/apps": "workspace:^", "@rocket.chat/apps-engine": "1.41.0", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", @@ -240,11 +242,11 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/fuselage-hooks": "^0.33.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "~0.31.25", - "@rocket.chat/fuselage-tokens": "~0.32.0", + "@rocket.chat/fuselage-tokens": "^0.33.0", "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/i18n": "workspace:^", diff --git a/apps/meteor/tests/data/livechat/users.ts b/apps/meteor/tests/data/livechat/users.ts index 38fb176faaa4..3a21cbee923c 100644 --- a/apps/meteor/tests/data/livechat/users.ts +++ b/apps/meteor/tests/data/livechat/users.ts @@ -1,5 +1,5 @@ import { faker } from "@faker-js/faker"; -import type { IUser } from "@rocket.chat/core-typings"; +import type { ILivechatAgent, IUser } from "@rocket.chat/core-typings"; import { IUserCredentialsHeader, password } from "../user"; import { createUser, login } from "../users.helper"; import { createAgent, makeAgentAvailable } from "./rooms"; @@ -24,6 +24,13 @@ export const createBotAgent = async (): Promise<{ export const getRandomVisitorToken = (): string => faker.string.alphanumeric(17); +export const getAgent = async (userId: string): Promise => { + const { body } = await request.get(api(`livechat/users/agent/${userId}`)) + .set(credentials) + .expect(200); + return body.user; +} + export const removeAgent = async (userId: string): Promise => { await request.delete(api(`livechat/users/agent/${userId}`)) .set(credentials) diff --git a/apps/meteor/tests/data/rooms.helper.js b/apps/meteor/tests/data/rooms.helper.js index 78e07f110039..c28f763c00f9 100644 --- a/apps/meteor/tests/data/rooms.helper.js +++ b/apps/meteor/tests/data/rooms.helper.js @@ -1,6 +1,6 @@ import { api, credentials, request } from './api-data'; -export const createRoom = ({ name, type, username, token, agentId, members, credentials: customCredentials, voipCallDirection = 'inbound' }) => { +export const createRoom = ({ name, type, username, token, agentId, members, credentials: customCredentials, extraData, voipCallDirection = 'inbound' }) => { if (!type) { throw new Error('"type" is required in "createRoom.ts" test helper'); } @@ -31,6 +31,7 @@ export const createRoom = ({ name, type, username, token, agentId, members, cred .send({ ...params, ...(members && { members }), + ...(extraData && { extraData }), }); }; @@ -65,3 +66,17 @@ function actionRoom({ action, type, roomId }) { export const deleteRoom = ({ type, roomId }) => actionRoom({ action: 'delete', type, roomId }); export const closeRoom = ({ type, roomId }) => actionRoom({ action: 'close', type, roomId }); + +export const setRoomConfig = ({ roomId, favorite, isDefault }) => { + return request + .post(api('rooms.saveRoomSettings')) + .set(credentials) + .send({ + rid: roomId, + default: isDefault, + favorite: favorite ? { + defaultValue: true, + favorite: false + } : undefined + }); +}; diff --git a/apps/meteor/tests/data/uploads.helper.ts b/apps/meteor/tests/data/uploads.helper.ts index 8eb1e7931965..eabd49e11a3b 100644 --- a/apps/meteor/tests/data/uploads.helper.ts +++ b/apps/meteor/tests/data/uploads.helper.ts @@ -45,6 +45,7 @@ export async function testFileUploads(filesEndpoint: 'channels.files' | 'groups. name: null, username: null, members: null, + extraData: null, }); return roomResponse.body.room; diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index 96d50bae6151..f76752085771 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -11,6 +11,7 @@ test.use({ storageState: Users.admin.state }); test.describe.serial('channel-management', () => { let poHomeChannel: HomeChannel; let targetChannel: string; + let discussionName: string; test.beforeAll(async ({ api }) => { targetChannel = await createTargetChannel(api); @@ -56,7 +57,8 @@ test.describe.serial('channel-management', () => { await expect(page.getByRole('button', { name: 'Start call' })).toBeFocused(); }); - test('expect add "user1" to "targetChannel"', async () => { + // FIXME: bad assertion + test('should add "user1" to "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); @@ -65,7 +67,8 @@ test.describe.serial('channel-management', () => { await expect(poHomeChannel.toastSuccess).toBeVisible(); }); - test('expect create invite to the room', async () => { + // FIXME: bad assertion + test('should create invite to the room', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.inviteUser(); @@ -73,28 +76,29 @@ test.describe.serial('channel-management', () => { await expect(poHomeChannel.toastSuccess).toBeVisible(); }); - test('expect mute "user1"', async () => { + test.fixme('should mute "user1"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.muteUser('user1'); }); - test('expect set "user1" as owner', async () => { + test.fixme('should set "user1" as owner', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.setUserAsOwner('user1'); }); - test('expect set "user1" as moderator', async () => { + + test.fixme('should set "user1" as moderator', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnTabMembers.click(); await poHomeChannel.tabs.members.showAllUsers(); await poHomeChannel.tabs.members.setUserAsModerator('user1'); }); - test('expect edit topic of "targetChannel"', async () => { + test.fixme('should edit topic of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); @@ -102,7 +106,7 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.room.btnSave.click(); }); - test('expect edit announcement of "targetChannel"', async () => { + test.fixme('should edit announcement of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); @@ -110,7 +114,7 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.room.btnSave.click(); }); - test('expect edit description of "targetChannel"', async () => { + test.fixme('should edit description of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); @@ -118,18 +122,65 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.room.btnSave.click(); }); - test('expect edit name of "targetChannel"', async ({ page }) => { + test('should edit name of "targetChannel"', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); await poHomeChannel.tabs.room.inputName.fill(`NAME-EDITED-${targetChannel}`); await poHomeChannel.tabs.room.btnSave.click(); - await poHomeChannel.sidenav.openChat(`NAME-EDITED-${targetChannel}`); - await expect(page).toHaveURL(`/channel/NAME-EDITED-${targetChannel}`); + targetChannel = `NAME-EDITED-${targetChannel}`; + await poHomeChannel.sidenav.openChat(targetChannel); + + await expect(page).toHaveURL(`/channel/${targetChannel}`); + }); + + test('should truncate the room name for small screens', async ({ page }) => { + const hugeName = faker.string.alpha(100); + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.inputName.fill(hugeName); + await poHomeChannel.tabs.room.btnSave.click(); + targetChannel = hugeName; + + await page.setViewportSize({ width: 640, height: 460 }); + await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '423px'); + }); + + test('should info contextualbar when clicking on roomName', async ({ page }) => { + await poHomeChannel.sidenav.openChat(targetChannel); + await page.getByRole('button', { name: targetChannel }).first().focus(); + await page.keyboard.press('Space'); + await page.getByRole('complementary').waitFor(); + + await expect(page.getByRole('complementary')).toBeVisible(); + }); + + test('should create a discussion using the message composer', async ({ page }) => { + discussionName = faker.string.uuid(); + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.btnMenuMoreActions.click(); + await page.getByRole('menuitem', { name: 'Discussion' }).click(); + await page.getByRole('textbox', { name: 'Discussion name' }).fill(discussionName); + await page.getByRole('button', { name: 'Create' }).click(); + + await expect(page.getByRole('heading', { name: discussionName })).toBeVisible(); + }); + + test('should access targetTeam through discussion header', async ({ page }) => { + await poHomeChannel.sidenav.openChat(targetChannel); + await page.locator('[data-qa-type="message"]', { hasText: discussionName }).locator('button').first().click(); + await page.getByRole('button', { name: discussionName }).first().focus(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Space'); + + await expect(page).toHaveURL(`/channel/${targetChannel}`); }); - test.skip('expect edit notification preferences of "targetChannel"', async () => { + // FIXME: bad assertion + test.fixme('should edit notification preferences of "targetChannel"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.kebab.click({ force: true }); await poHomeChannel.tabs.btnNotificationPreferences.click({ force: true }); @@ -140,7 +191,7 @@ test.describe.serial('channel-management', () => { }); let regularUserPage: Page; - test('expect "readOnlyChannel" to show join button', async ({ browser }) => { + test('should "readOnlyChannel" show join button', async ({ browser }) => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); @@ -160,7 +211,7 @@ test.describe.serial('channel-management', () => { await regularUserPage.close(); }); - test.skip('expect all notification preferences of "targetChannel" to be "Mentions"', async () => { + test.fixme('should all notification preferences of "targetChannel" to be "Mentions"', async () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.kebab.click({ force: true }); await poHomeChannel.tabs.btnNotificationPreferences.click({ force: true }); diff --git a/apps/meteor/tests/e2e/fixtures/files/csv_import.zip b/apps/meteor/tests/e2e/fixtures/files/csv_import.zip new file mode 100644 index 000000000000..19415a7cd142 Binary files /dev/null and b/apps/meteor/tests/e2e/fixtures/files/csv_import.zip differ diff --git a/apps/meteor/tests/e2e/fixtures/files/csv_import_rooms.csv b/apps/meteor/tests/e2e/fixtures/files/csv_import_rooms.csv new file mode 100644 index 000000000000..4d605b7d51f4 --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/files/csv_import_rooms.csv @@ -0,0 +1,4 @@ +"imported-csv-group","billy.bob","private","" +"imported-csv-channel","billy.bob","public","" +"imported-csv-with-members-group","billy.bob","private","billy.bob;billy.joe" +"imported-csv-with-members-channel","billy.bob","public","billy.bob;billy.joe" diff --git a/apps/meteor/tests/e2e/fixtures/files/csv_import_users.csv b/apps/meteor/tests/e2e/fixtures/files/csv_import_users.csv new file mode 100644 index 000000000000..c796f6e3fada --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/files/csv_import_users.csv @@ -0,0 +1,2 @@ +billy.bob, billy.bob@example.com, Billy Bob Jr. +billy.joe, billy.joe@example.com, Billy Joe Jr. diff --git a/apps/meteor/tests/e2e/fixtures/files/dm_messages.csv b/apps/meteor/tests/e2e/fixtures/files/dm_messages.csv new file mode 100644 index 000000000000..2837e1dec050 --- /dev/null +++ b/apps/meteor/tests/e2e/fixtures/files/dm_messages.csv @@ -0,0 +1,2 @@ +"billy.joe","billy.bob","1479162481336","this is a test message" +"billy.bob","billy.joe","1479162481654","this is another message, a test message" diff --git a/apps/meteor/tests/e2e/imports.spec.ts b/apps/meteor/tests/e2e/imports.spec.ts index 9ccc14947ca0..5b7c54316758 100644 --- a/apps/meteor/tests/e2e/imports.spec.ts +++ b/apps/meteor/tests/e2e/imports.spec.ts @@ -9,20 +9,79 @@ import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); +type csvRoomSpec = { + name: string; + ownerUsername: string; + visibility: 'public' | 'private'; + members: string; +}; + const rowUserName: string[] = []; +const csvImportedUsernames: string[] = []; +const dmMessages: string[] = []; +const importedRooms: csvRoomSpec[] = []; const slackCsvDir = path.resolve(__dirname, 'fixtures', 'files', 'slack_export_users.csv'); +const zipCsvImportDir = path.resolve(__dirname, 'fixtures', 'files', 'csv_import.zip'); -const csvToJson = (): void => { - fs.createReadStream(slackCsvDir) - .pipe(parse({ delimiter: ',', from_line: 2 })) - .on('data', (rows) => { - rowUserName.push(rows[0]); - }); +// These files have the same content from users.csv, channels.csv and messages1.csv from the zip file +// They have been extracted just so that we don't need to do that on the fly +const usersCsvDir = path.resolve(__dirname, 'fixtures', 'files', 'csv_import_users.csv'); +const roomsCsvDir = path.resolve(__dirname, 'fixtures', 'files', 'csv_import_rooms.csv'); +const dmMessagesCsvDir = path.resolve(__dirname, 'fixtures', 'files', 'dm_messages.csv'); + +const usersCsvsToJson = async (): Promise => { + await new Promise((resolve) => + fs.createReadStream(slackCsvDir) + .pipe(parse({ delimiter: ',', from_line: 2 })) + .on('data', (rows) => { + rowUserName.push(rows[0]); + }) + .on('end', resolve) + ); + + await new Promise((resolve) => + fs.createReadStream(usersCsvDir) + .pipe(parse({ delimiter: ',' })) + .on('data', (rows) => { + rowUserName.push(rows[0]); + csvImportedUsernames.push(rows[0]); + }) + .on('end', resolve) + ); }; +const countDmMessages = (): Promise => ( + new Promise((resolve) => + fs.createReadStream(dmMessagesCsvDir) + .pipe(parse({ delimiter: ',' })) + .on('data', (rows) => { + dmMessages.push(rows[3]); + }) + .on('end', resolve) + ) +); + +const roomsCsvToJson = (): Promise => ( + new Promise((resolve) => + fs.createReadStream(roomsCsvDir) + .pipe(parse({ delimiter: ',' })) + .on('data', (rows) => { + importedRooms.push({ + name: rows[0], + ownerUsername: rows[1], + visibility: rows[2], + members: rows[3], + }); + }) + .on('end', resolve) + ) +); + test.describe.serial('imports', () => { - test.beforeAll(() => { - csvToJson(); + test.beforeAll(async () => { + await usersCsvsToJson(); + await roomsCsvToJson(); + await countDmMessages(); }); test('expect import users data from slack', async ({ page }) => { @@ -43,11 +102,70 @@ test.describe.serial('imports', () => { }); }); - test('expect to all users imported are actually listed as users', async ({ page }) => { + test('expect import users data from zipped CSV files', async ({ page }) => { + const poAdmin: Admin = new Admin(page); + await page.goto('/admin/import'); + + await poAdmin.btnImportNewFile.click(); + + await (await poAdmin.getOptionFileType('CSV')).click(); + + await poAdmin.inputFile.setInputFiles(zipCsvImportDir); + await poAdmin.btnImport.click(); + + await poAdmin.btnStartImport.click(); + + await expect(poAdmin.importStatusTableFirstRowCell).toBeVisible({ + timeout: 30_000, + }); + }); + + test('expect all imported users to be actually listed as users', async ({ page }) => { await page.goto('/admin/users'); for (const user of rowUserName) { expect(page.locator(`tbody tr td:first-child >> text="${user}"`)); } }); + + test('expect all imported rooms to be actually listed as rooms with correct members count', async ({ page }) => { + const poAdmin: Admin = new Admin(page); + await page.goto('/admin/rooms'); + + for await (const room of importedRooms) { + await poAdmin.inputSearchRooms.fill(room.name); + + const expectedMembersCount = room.members.split(';').filter((username) => username !== room.ownerUsername).length + 1; + expect(page.locator(`tbody tr td:nth-child(2) >> text="${ expectedMembersCount }"`)); + } + }); + + test('expect all imported rooms to have correct room type and owner', async ({ page }) => { + const poAdmin: Admin = new Admin(page); + await page.goto('/admin/rooms'); + + for await (const room of importedRooms) { + await poAdmin.inputSearchRooms.fill(room.name); + await poAdmin.getRoomRow(room.name).click(); + + room.visibility === 'private' ? await expect(poAdmin.privateInput).toBeChecked() : await expect(poAdmin.privateInput).not.toBeChecked(); + await expect(poAdmin.roomOwnerInput).toHaveValue(room.ownerUsername); + } + }); + + test('expect imported DM to be actually listed as a room with correct members and messages count', async ({ page }) => { + const poAdmin: Admin = new Admin(page); + await page.goto('/admin/rooms'); + + for await (const user of csvImportedUsernames) { + await poAdmin.inputSearchRooms.fill(user); + expect(page.locator(`tbody tr td:first-child >> text="${user}"`)); + + const expectedMembersCount = 2; + expect(page.locator(`tbody tr td:nth-child(2) >> text="${ expectedMembersCount }"`)); + + const expectedMessagesCount = dmMessages.length; + expect(page.locator(`tbody tr td:nth-child(3) >> text="${ expectedMessagesCount }"`)); + } + }); }); diff --git a/apps/meteor/tests/e2e/message-composer.spec.ts b/apps/meteor/tests/e2e/message-composer.spec.ts index 9ed3e42b941d..8b8888c040a2 100644 --- a/apps/meteor/tests/e2e/message-composer.spec.ts +++ b/apps/meteor/tests/e2e/message-composer.spec.ts @@ -1,3 +1,5 @@ +import { faker } from '@faker-js/faker'; + import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; import { createTargetChannel } from './utils'; @@ -23,21 +25,18 @@ test.describe.serial('message-composer', () => { await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.content.sendMessage('hello composer'); - await expect(poHomeChannel.composerToolbarActions).toHaveCount(11); + await expect(poHomeChannel.composerToolbarActions).toHaveCount(12); }); test('should have only the main formatter and the main action', async ({ page }) => { await page.setViewportSize({ width: 768, height: 600 }); - await poHomeChannel.sidenav.openChat(targetChannel); - await poHomeChannel.content.sendMessage('hello composer'); await expect(poHomeChannel.composerToolbarActions).toHaveCount(5); }); test('should navigate on toolbar using arrow keys', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); - await poHomeChannel.content.sendMessage('hello composer'); await page.keyboard.press('Tab'); await page.keyboard.press('ArrowRight'); @@ -50,11 +49,24 @@ test.describe.serial('message-composer', () => { test('should move the focus away from toolbar using tab key', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); - await poHomeChannel.content.sendMessage('hello composer'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await expect(poHomeChannel.composerToolbar.getByRole('button', { name: 'Emoji' })).not.toBeFocused(); }); + + test('should add a link to the selected text', async ({ page }) => { + const url = faker.internet.url(); + await poHomeChannel.sidenav.openChat(targetChannel); + + await page.keyboard.type('hello composer'); + await page.keyboard.press('Control+A'); // on Windows and Linux + await page.keyboard.press('Meta+A'); // on macOS + await poHomeChannel.composerToolbar.getByRole('button', { name: 'Link' }).click() + await page.keyboard.type(url); + await page.keyboard.press('Enter'); + + await expect(poHomeChannel.composer).toHaveValue(`[hello composer](${url})`); + }); }); diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index 4fa248e72183..ab31dcba2729 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -38,13 +38,14 @@ test.describe.serial('Messaging', () => { await page.keyboard.press('ArrowDown'); await expect(page.locator('[data-qa-type="message"]:has-text("msg1")')).toBeFocused(); - // move focus to the favorite icon + // move focus to the room title await page.keyboard.press('Shift+Tab'); - await expect(poHomeChannel.roomHeaderFavoriteBtn).toBeFocused(); + await expect(page.getByRole('button', { name: targetChannel }).first()).toBeFocused(); // refocus on the first typed message await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); await expect(page.locator('[data-qa-type="message"]:has-text("msg1")')).toBeFocused(); // move focus to the message toolbar diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 30b58cd29bec..519b15f0cc0c 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -28,10 +28,18 @@ export class Admin { return this.page.locator(`label >> text=Private`); } + get privateInput(): Locator { + return this.page.locator('input[name="roomType"]'); + } + get roomNameInput(): Locator { return this.page.locator('input[name="roomName"]'); } + get roomOwnerInput(): Locator { + return this.page.locator('input[name="roomOwner"]'); + } + get archivedLabel(): Locator { return this.page.locator('label >> text=Archived'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 2c739ae6667d..5435985fedb3 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -153,7 +153,7 @@ export class HomeContent { } get btnMenuMoreActions() { - return this.page.locator('[data-qa-id="menu-more-actions"]'); + return this.page.getByRole('button', { name: 'More actions' }); } get userCard(): Locator { diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index 338f5c5eb0ef..11609ec51ed6 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -89,4 +89,14 @@ test.describe.serial('teams-management', () => { await poHomeTeam.tabs.channels.btnAdd.click(); await expect(page.locator('//main//aside >> li')).toContainText(targetChannel); }); + + test('should access team channel through "targetTeam" header', async ({ page }) => { + await poHomeTeam.sidenav.openChat(targetChannel); + await page.getByRole('button', { name: targetChannel }).first().focus(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Space'); + + await expect(page).toHaveURL(`/group/${targetTeam}`); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index eaafc97527a3..1103f8a3cbc5 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -9,8 +9,11 @@ import { getCredentials, api, request, credentials, apiEmail, apiUsername, log, import { MAX_BIO_LENGTH, MAX_NICKNAME_LENGTH } from '../../data/constants.ts'; import { customFieldText, clearCustomFields, setCustomFields } from '../../data/custom-fields.js'; import { imgURL } from '../../data/interactions'; +import { createAgent, makeAgentAvailable } from '../../data/livechat/rooms'; +import { removeAgent, getAgent } from '../../data/livechat/users'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom, deleteRoom } from '../../data/rooms.helper'; +import { createRoom, deleteRoom, setRoomConfig } from '../../data/rooms.helper'; +import { createTeam, deleteTeam } from '../../data/teams.helper'; import { adminEmail, preferences, password, adminUsername } from '../../data/user'; import { createUser, login, deleteUser, getUserStatus, getUserByUsername } from '../../data/users.helper.js'; @@ -277,6 +280,162 @@ describe('[Users]', function () { .end(done); }); }); + + describe('auto join default channels', () => { + let defaultTeamRoomId; + let defaultTeamId; + let group; + let user; + let userCredentials; + let user2; + let user3; + let userNoDefault; + const teamName = `defaultTeam_${Date.now()}`; + + before(async () => { + const defaultTeam = await createTeam(credentials, teamName, 0); + defaultTeamRoomId = defaultTeam.roomId; + defaultTeamId = defaultTeam._id; + }); + + before(async () => { + const { body } = await createRoom({ + name: `defaultGroup_${Date.now()}`, + type: 'p', + credentials, + extraData: { + broadcast: false, + encrypted: false, + teamId: defaultTeamId, + topic: '', + }, + }); + group = body.group; + }); + + after(() => + Promise.all([ + deleteRoom({ roomId: group._id, type: 'p' }), + deleteTeam(credentials, teamName), + deleteUser(user), + deleteUser(user2), + deleteUser(user3), + deleteUser(userNoDefault), + ]), + ); + + it('should not create subscriptions to non default teams or rooms even if joinDefaultChannels is true', async () => { + userNoDefault = await createUser({ joinDefaultChannels: true }); + const noDefaultUserCredentials = await login(userNoDefault.username, password); + await request + .get(api('subscriptions.getOne')) + .set(noDefaultUserCredentials) + .query({ roomId: defaultTeamRoomId }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').that.is.null; + }); + + await request + .get(api('subscriptions.getOne')) + .set(noDefaultUserCredentials) + .query({ roomId: group._id }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').that.is.null; + }); + }); + + it('should create a subscription for a default team room if joinDefaultChannels is true', async () => { + await setRoomConfig({ roomId: defaultTeamRoomId, favorite: true, isDefault: true }); + + user = await createUser({ joinDefaultChannels: true }); + userCredentials = await login(user.username, password); + await request + .get(api('subscriptions.getOne')) + .set(userCredentials) + .query({ roomId: defaultTeamRoomId }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription'); + expect(res.body.subscription).to.have.property('rid', defaultTeamRoomId); + }); + }); + + it('should NOT create a subscription for non auto-join rooms inside a default team', async () => { + await request + .get(api('subscriptions.getOne')) + .set(userCredentials) + .query({ roomId: group._id }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').that.is.null; + }); + }); + + it('should create a subscription for the user in all the auto join rooms of the team', async () => { + await request.post(api('teams.updateRoom')).set(credentials).send({ + roomId: group._id, + isDefault: true, + }); + + user2 = await createUser({ joinDefaultChannels: true }); + const user2Credentials = await login(user2.username, password); + + await request + .get(api('subscriptions.getOne')) + .set(user2Credentials) + .query({ roomId: group._id }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription'); + expect(res.body.subscription).to.have.property('rid', group._id); + }); + }); + + it('should create a subscription for a default room inside a non default team', async () => { + await setRoomConfig({ roomId: defaultTeamRoomId, isDefault: false }); + await setRoomConfig({ roomId: group._id, favorite: true, isDefault: true }); + + user3 = await createUser({ joinDefaultChannels: true }); + const user3Credentials = await login(user3.username, password); + + // New user should be subscribed to the default room inside a team + await request + .get(api('subscriptions.getOne')) + .set(user3Credentials) + .query({ roomId: group._id }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription'); + expect(res.body.subscription).to.have.property('rid', group._id); + }); + + // New user should not be subscribed to the parent team + await request + .get(api('subscriptions.getOne')) + .set(user3Credentials) + .query({ roomId: defaultTeamRoomId }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('subscription').that.is.null; + }); + }); + }); }); describe('[/users.register]', () => { @@ -527,6 +686,41 @@ describe('[Users]', function () { }) .end(done); }); + + describe('Logging in with type: "resume"', () => { + let user; + let userCredentials; + + before(async () => { + user = await createUser({ joinDefaultChannels: false }); + userCredentials = await login(user.username, password); + }); + + after(async () => deleteUser(user)); + + it('should return "offline" after a login type "resume" via REST', async () => { + await request + .post(api('login')) + .send({ + resume: userCredentials['X-Auth-Token'], + }) + .expect('Content-Type', 'application/json') + .expect(200); + + await request + .get(api('users.getPresence')) + .set(credentials) + .query({ + userId: user._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('presence', 'offline'); + }); + }); + }); }); describe('[/users.presence]', () => { @@ -3114,6 +3308,21 @@ describe('[Users]', function () { describe('[/users.setActiveStatus]', () => { let user; + let agent; + let agentUser; + + before(async () => { + agentUser = await createUser(); + const agentUserCredentials = await login(agentUser.username, password); + await createAgent(agentUser.username); + await makeAgentAvailable(agentUserCredentials); + + agent = { + user: agentUser, + credentials: agentUserCredentials, + }; + }); + before((done) => { const username = `user.test.${Date.now()}`; const email = `${username}@rocket.chat`; @@ -3156,6 +3365,12 @@ describe('[Users]', function () { .end(() => updatePermission('edit-other-user-active-status', ['admin']).then(done)); user = undefined; }); + + after(async () => { + await removeAgent(agent.user._id); + await deleteUser(agent.user); + }); + it('should set other user active status to false when the logged user has the necessary permission(edit-other-user-active-status)', (done) => { request .post(api('users.setActiveStatus')) @@ -3484,6 +3699,39 @@ describe('[Users]', function () { await deleteUser(testUser); }); + it('should make agents not-available when the user is deactivated', async () => { + await makeAgentAvailable(agent.credentials); + await request + .post(api('users.setActiveStatus')) + .set(credentials) + .send({ + activeStatus: false, + userId: agent.user._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const agentInfo = await getAgent(agent.user._id); + expect(agentInfo).to.have.property('statusLivechat', 'not-available'); + }); + + it('should not make agents available when the user is activated', async () => { + let agentInfo = await getAgent(agent.user._id); + expect(agentInfo).to.have.property('statusLivechat', 'not-available'); + + await request + .post(api('users.setActiveStatus')) + .set(credentials) + .send({ + activeStatus: true, + userId: agent.user._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + agentInfo = await getAgent(agent.user._id); + expect(agentInfo).to.have.property('statusLivechat', 'not-available'); + }); }); describe('[/users.deactivateIdle]', () => { diff --git a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts index 579e9894ab7b..fb7f42ccdabd 100644 --- a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts +++ b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts @@ -25,6 +25,7 @@ describe('Apps - Video Conferences', function () { agentId: undefined, members: undefined, credentials: undefined, + extraData: undefined, }); roomId = res.body.group._id; diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index df82a24cbf86..37ad74302b46 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -34,7 +34,7 @@ "dependencies": { "@react-pdf/renderer": "^3.1.14", "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/fuselage-tokens": "~0.32.0", + "@rocket.chat/fuselage-tokens": "^0.33.0", "@types/react": "~17.0.69", "emoji-assets": "^7.0.1", "emoji-toolkit": "^7.0.1", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 8243c3a9d664..6065d22c8764 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/fuselage-hooks": "^0.33.0", "@rocket.chat/icons": "^0.33.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/ee/packages/ui-theming/src/PaletteStyleTag.tsx b/ee/packages/ui-theming/src/PaletteStyleTag.tsx deleted file mode 100644 index 62e31ef3d947..000000000000 --- a/ee/packages/ui-theming/src/PaletteStyleTag.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { memo } from 'react'; -import { createPortal } from 'react-dom'; - -import { codeBlock } from './codeBlockStyles'; -import { convertToCss } from './helpers/convertToCss'; -import { useCreateStyleContainer } from './hooks/useCreateStyleContainer'; -import { useThemeMode } from './hooks/useThemeMode'; -import { defaultPalette } from './palette'; -import { darkPalette } from './paletteDark'; -import { paletteHighContrast } from './paletteHighContrast'; - -export const PaletteStyleTag = memo(function PaletteStyleTag() { - const [, , theme] = useThemeMode(); - - const getPalette = () => { - if (theme === 'dark') { - return darkPalette; - } - if (theme === 'high-contrast') { - return paletteHighContrast; - } - return defaultPalette; - }; - const palette = convertToCss(getPalette(), '.rcx-content--main, .rcx-tile'); - - return <>{createPortal(theme === 'dark' ? palette + codeBlock : palette, useCreateStyleContainer('main-palette'))}; -}); diff --git a/ee/packages/ui-theming/src/SidebarPaletteStyleTag.tsx b/ee/packages/ui-theming/src/SidebarPaletteStyleTag.tsx deleted file mode 100644 index 3b1ab5500960..000000000000 --- a/ee/packages/ui-theming/src/SidebarPaletteStyleTag.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { ReactElement } from 'react'; -import React, { memo } from 'react'; -import { createPortal } from 'react-dom'; - -import { convertToCss } from './helpers/convertToCss'; -import { useCreateStyleContainer } from './hooks/useCreateStyleContainer'; -import { darkPalette } from './paletteDark'; - -export const SidebarPaletteStyleTag = memo(function SidebarPaletteStyleTag(): ReactElement | null { - const palette = convertToCss({ ...darkPalette }, '.rcx-sidebar--main'); - - return <>{createPortal(palette, useCreateStyleContainer('sidebar-palette'))}; -}); diff --git a/ee/packages/ui-theming/src/codeBlockStyles.ts b/ee/packages/ui-theming/src/codeBlockStyles.ts deleted file mode 100644 index 2c6b4bf33be0..000000000000 --- a/ee/packages/ui-theming/src/codeBlockStyles.ts +++ /dev/null @@ -1,74 +0,0 @@ -export const codeBlock = `pre code.hljs { - display: block; - overflow-x: auto; - padding: 1em; -} -code.hljs { - padding: 3px 5px; -} -.hljs { - background: #1d1f21; - color: #c5c8c6; -} -.hljs span::selection, -.hljs::selection { - background: #373b41; -} -.hljs span::-moz-selection, -.hljs::-moz-selection { - background: #373b41; -} -.hljs-name, -.hljs-title { - color: #f0c674; -} -.hljs-comment, -.hljs-meta, -.hljs-meta .hljs-keyword { - color: #707880; -} -.hljs-deletion, -.hljs-link, -.hljs-literal, -.hljs-number, -.hljs-symbol { - color: #c66; -} -.hljs-addition, -.hljs-doctag, -.hljs-regexp, -.hljs-selector-attr, -.hljs-selector-pseudo, -.hljs-string { - color: #b5bd68; -} -.hljs-attribute, -.hljs-code, -.hljs-selector-id { - color: #b294bb; -} -.hljs-bullet, -.hljs-keyword, -.hljs-selector-tag, -.hljs-tag { - color: #81a2be; -} -.hljs-subst, -.hljs-template-tag, -.hljs-template-variable, -.hljs-variable { - color: #8abeb7; -} -.hljs-built_in, -.hljs-quote, -.hljs-section, -.hljs-selector-class, -.hljs-type { - color: #de935f; -} -.hljs-emphasis { - font-style: italic; -} -.hljs-strong { - font-weight: 700; -}`; diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts deleted file mode 100644 index b2d9e9955dbf..000000000000 --- a/ee/packages/ui-theming/src/palette.ts +++ /dev/null @@ -1,211 +0,0 @@ -export const palette = [ - { - category: 'Stroke', - description: "Use as component's outline, stroke, dividers", - list: [ - { name: 'stroke-extra-light', token: 'N250', color: '#EBECEF' }, - { name: 'stroke-light', token: 'N500', color: '#CBCED1' }, - { name: 'stroke-medium', token: 'N600', color: '#9EA2A8' }, - { name: 'stroke-dark', token: 'N700', color: '#6C727A' }, - { name: 'stroke-extra-dark', token: 'N800', color: '#2F343D' }, - { name: 'stroke-extra-light-highlight', token: 'P200', color: '#D1EBFE' }, - { name: 'stroke-highlight', token: 'P500', color: '#156FF5' }, - { name: 'stroke-extra-light-error', token: 'D200', color: '#FFC1C9' }, - { name: 'stroke-error', token: 'D500', color: '#EC0D2A' }, - ], - }, - { - category: 'Surface', - description: 'Use as a container on top of the background', - list: [ - { name: 'surface-light', token: 'white', color: '#FFFFFF' }, - { name: 'surface-tint', token: 'N100', color: '#F7F8FA' }, - { name: 'surface-room', token: 'white', color: '#FFFFFF' }, - { name: 'surface-neutral', token: 'N400', color: '#E4E7EA' }, - { name: 'surface-disabled', token: 'N100', color: '#F7F8FA' }, - { name: 'surface-hover', token: 'N200', color: '#F2F3F5' }, - { name: 'surface-selected', token: '', color: '#D7DBE0' }, - { name: 'surface-dark', token: 'N900', color: '#1F2329' }, - { name: 'surface-featured', token: '', color: '#5F1477' }, - { name: 'surface-featured-hover', token: '', color: '#4A105D' }, - { name: 'surface-overlay', token: '', color: 'rgba(47, 52, 61, 0.5)' }, - { name: 'surface-sidebar', token: 'N400', color: '#E4E7EA' }, - ], - }, - { - category: 'Shadow', - description: 'Use as a shadow color', - list: [ - { name: 'shadow-highlight', token: '', color: '#D1EBFE' }, - { name: 'shadow-danger', token: '', color: '#FFE9EC' }, - ], - }, - { - category: 'Font', - description: 'These should be applied according to surfaces', - list: [ - { name: 'font-white', token: 'white', color: '#FFFFFF' }, - { name: 'font-disabled', token: 'N500', color: '#CBCED1' }, - { name: 'font-annotation', token: 'N600', color: '#9EA2A8' }, - { name: 'font-hint', token: 'N700', color: '#6C737A' }, - { name: 'font-secondary-info', token: 'N700', color: '#6C737A' }, - { name: 'font-default', token: 'N800', color: '#2F343D' }, - { name: 'font-titles-labels', token: 'N900', color: '#1F2329' }, - { name: 'font-info', token: 'P600', color: '#095AD2' }, - { name: 'font-danger', token: 'D600', color: '#D40C26' }, - { name: 'font-pure-black', token: '', color: '#2F343D' }, - { name: 'font-pure-white', token: '', color: '#FFFFFF' }, - ], - }, - { - category: 'Status', - description: 'Status Background', - list: [ - { name: 'status-background-info', token: 'P200', color: '#D1EBFE' }, - { name: 'status-background-success', token: 'S500', color: '#C0F6E4' }, - { name: 'status-background-danger', token: 'D200', color: '#FFC1C9' }, - { name: 'status-background-warning', token: 'W200', color: '#FFECAD' }, - { name: 'status-background-warning-2', token: 'W100', color: '#FFF8E0' }, - { name: 'status-background-service-1', token: 'S1-200', color: '#FAD1B0' }, - { name: 'status-background-service-2', token: 'S2-200', color: '#EDD0F7' }, - { name: 'status-background-service-3', token: 'S2-700', color: '#5F1477' }, - ], - }, - { - description: 'Status Font', - list: [ - { name: 'status-font-on-info', token: 'P600', color: '#095AD2' }, - { name: 'status-font-on-success', token: 'S800', color: '#148660' }, - { name: 'status-font-on-danger', token: 'D800', color: '#9B1325' }, - { name: 'status-font-on-warning', token: 'W900', color: '#8E6300' }, - { name: 'status-font-on-warning-2', token: 'N800', color: '#2F343D' }, - { name: 'status-font-on-service-1', token: 'S1-800', color: '#974809' }, - { name: 'status-font-on-service-2 ', token: 'S2-600', color: '#7F1B9F' }, - { name: 'status-font-on-service-3 ', token: 'white', color: '#FFFFFF' }, - ], - }, - { - category: 'Badge', - description: 'Badge Background', - list: [ - { name: 'badge-background-level-0', token: '', color: '#E4E7EA' }, - { name: 'badge-background-level-1', token: 'N700', color: '#6C737A' }, - { name: 'badge-background-level-2', token: '', color: '#156FF5' }, - { name: 'badge-background-level-3', token: '', color: '#F38C39' }, - { name: 'badge-background-level-4', token: '', color: '#EC0D2A' }, - ], - }, - { - category: 'Status Bullet', - description: 'Used to show user status', - list: [ - { name: 'status-bullet-online', token: '', color: '#148660' }, - { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#D40C26' }, - { name: 'status-bullet-disabled', token: '', color: '#F38C39' }, - { name: 'status-bullet-offline', token: '', color: '#6C737A' }, - { name: 'status-bullet-loading', token: '', color: '#6C737A' }, - ], - }, - { - category: 'Elevation', - description: 'Elevation border and shadow levels', - list: [ - { name: 'shadow-elevation-border', token: '', color: '#EBECEF' }, - { name: 'shadow-elevation-1', token: '', color: 'rgba(47, 52, 61, 0.1)' }, - { name: 'shadow-elevation-2x', token: '', color: 'rgba(47, 52, 61, 0.08)' }, - { name: 'shadow-elevation-2y', token: '', color: 'rgba(47, 52, 61, 0.12)' }, - ], - }, - { - category: 'Button', - description: 'Primary Background', - list: [ - { name: 'button-background-primary-default', token: 'P500', color: '#156FF5' }, - { name: 'button-background-primary-hover', token: 'P600', color: '#095AD2' }, - { name: 'button-background-primary-press', token: 'P700', color: '#10529E' }, - { name: 'button-background-primary-focus', token: 'P500', color: '#156FF5' }, - { name: 'button-background-primary-keyfocus', token: 'P500', color: '#156FF5' }, - { name: 'button-background-primary-disabled', token: 'P200', color: '#D1EBFE' }, - ], - }, - { - description: 'Secondary Background', - list: [ - { name: 'button-background-secondary-default', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-press', token: 'N600', color: '#9EA2A8' }, - { name: 'button-background-secondary-focus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-keyfocus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-disabled', token: 'N300', color: '#EEEFF1' }, - ], - }, - { - description: 'Secondary Danger Background', - list: [ - { name: 'button-background-secondary-danger-default', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-danger-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-danger-press', token: 'N600', color: '#9EA2A8' }, - { name: 'button-background-secondary-danger-focus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-danger-keyfocus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-danger-disabled', token: 'N300', color: '#EEEFF1' }, - ], - }, - { - description: 'Danger Background', - list: [ - { name: 'button-background-danger-default', token: 'D500', color: '#EC0D2A' }, - { name: 'button-background-danger-hover', token: 'D600', color: '#D40C26' }, - { name: 'button-background-danger-press', token: 'D700', color: '#BB0B21' }, - { name: 'button-background-danger-focus', token: 'D500', color: '#EC0D2A' }, - { name: 'button-background-danger-keyfocus', token: 'D500', color: '#EC0D2A' }, - { name: 'button-background-danger-disabled', token: 'D200', color: '#FFC1C9' }, - ], - }, - { - description: 'Success Background', - list: [ - { name: 'button-background-success-default', token: '', color: '#148660' }, - { name: 'button-background-success-hover', token: 'S900', color: '#106D4F' }, - { name: 'button-background-success-press', token: 'S1000', color: '#0D5940' }, - { name: 'button-background-success-focus', token: '', color: '#148660' }, - { name: 'button-background-success-keyfocus', token: '', color: '#148660' }, - { name: 'button-background-success-disabled', token: 'S200', color: '#C0F6E4' }, - ], - }, - { - description: 'Font', - list: [ - { name: 'button-font-on-primary', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-primary-disabled', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-secondary', token: 'N900', color: '#1F2329' }, - { name: 'button-font-on-secondary-disabled', token: 'N600', color: '#CBCED1' }, - { name: 'button-font-on-secondary-danger', token: '', color: '#BB0B21' }, - { - name: 'button-font-on-secondary-danger-disabled', - token: 'D300', - color: '#F98F9D', - }, - { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-danger-disabled', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-success', token: '', color: '#FFFFFF' }, - { name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' }, - ], - }, -]; - -export const defaultPalette = { - ...palette.reduce( - (rec, group) => ({ - ...rec, - ...group.list.reduce( - (rec, item) => ({ - ...rec, - [item.name]: item.color, - }), - {} as Record, - ), - }), - {} as Record, - ), -}; diff --git a/ee/packages/ui-theming/src/paletteDark.ts b/ee/packages/ui-theming/src/paletteDark.ts deleted file mode 100644 index e36133650a6c..000000000000 --- a/ee/packages/ui-theming/src/paletteDark.ts +++ /dev/null @@ -1,211 +0,0 @@ -export const palette = [ - { - category: 'Stroke', - description: "Use as component's outline, stroke, dividers", - list: [ - { name: 'stroke-extra-light', token: '', color: '#333842' }, - { name: 'stroke-light', token: '', color: '#404754' }, - { name: 'stroke-medium', token: '', color: '#4B5362' }, - { name: 'stroke-dark', token: 'N600', color: '#9EA2A8' }, - { name: 'stroke-extra-dark', token: 'N400', color: '#CBCED1' }, - { name: 'stroke-extra-light-highlight', token: '', color: '#87CBFC' }, - { name: 'stroke-highlight', token: '', color: '#6292DA' }, - { name: 'stroke-extra-light-error', token: '', color: '#F49AA6' }, - { name: 'stroke-error', token: '', color: '#BB3E4E' }, - ], - }, - { - category: 'Surface', - description: 'Use as a container on top of the background', - list: [ - { name: 'surface-light', token: 'N900', color: '#262931' }, - { name: 'surface-tint', token: '', color: '#1F2329' }, - { name: 'surface-room', token: '', color: '#1F2329' }, - { name: 'surface-neutral', token: '', color: '#2D3039' }, - { name: 'surface-disabled', token: 'N800', color: '#24272E' }, - { name: 'surface-hover', token: '', color: '#1A1E23' }, - { name: 'surface-selected', token: '', color: '#4C5362' }, - { name: 'surface-dark', token: 'N400', color: '#E4E7EA' }, - { name: 'surface-featured', token: '', color: '#5F1477' }, - { name: 'surface-featured-hover', token: '', color: '#4A105D' }, - { name: 'surface-overlay', token: '', color: 'rgba(0, 0, 0, 0.6)' }, - { name: 'surface-sidebar', token: '', color: '#2F343D' }, - ], - }, - { - category: 'Shadow', - description: 'Use as a shadow color', - list: [ - { name: 'shadow-highlight', token: '', color: '#D1EBFE' }, - { name: 'shadow-danger', token: '', color: '#FFE9EC' }, - ], - }, - { - category: 'Font', - description: 'These should be applied according to surfaces', - list: [ - { name: 'font-white', token: 'N800', color: '#2F343D' }, - { name: 'font-disabled', token: '', color: '#60646C' }, - { name: 'font-annotation', token: 'N600', color: '#9EA2A8' }, - { name: 'font-hint', token: 'N600', color: '#9EA2A8' }, - { name: 'font-secondary-info', token: '', color: '#9EA2A8' }, - { name: 'font-default', token: 'N400', color: '#C1C7D0' }, - { name: 'font-titles-labels', token: '', color: '#F2F3F5' }, - { name: 'font-info', token: '', color: '#739EDE' }, - { name: 'font-danger', token: '', color: '#D88892' }, - { name: 'font-pure-black', token: '', color: '#2F343D' }, - { name: 'font-pure-white', token: '', color: '#FFFFFF' }, - ], - }, - { - category: 'Status', - description: 'Status Background', - list: [ - { name: 'status-background-info', token: '', color: '#A8C3EB' }, - { name: 'status-background-success', token: '', color: '#C1EBDD' }, - { name: 'status-background-warning', token: '', color: '#FEEFBE' }, - { name: 'status-background-warning-2', token: '', color: '#3C3625' }, - { name: 'status-background-danger', token: '', color: '#FFBDC5' }, - { name: 'status-background-service-1', token: '', color: '#FCE3CF' }, - { name: 'status-background-service-2', token: '', color: '#EDD0F7' }, - { name: 'status-background-service-3', token: '', color: '#5F1477' }, - ], - }, - { - description: 'Status Font', - list: [ - { name: 'status-font-on-info', token: '', color: '#739EDE' }, - { name: 'status-font-on-success', token: '', color: '#58AD90' }, - { name: 'status-font-on-warning', token: '', color: '#C7AA66' }, - { name: 'status-font-on-warning-2', token: '', color: '#FFFFFF' }, - { name: 'status-font-on-danger', token: '', color: '#D88892' }, - { name: 'status-font-on-service-1', token: '', color: '#CA9163' }, - { name: 'status-font-on-service-2 ', token: '', color: '#C393D2' }, - { name: 'status-font-on-service-3 ', token: '', color: '#FFFFFF' }, - ], - }, - { - category: 'Badge', - description: 'Badge Background', - list: [ - { name: 'badge-background-level-0', token: '', color: '#404754' }, - { name: 'badge-background-level-1', token: '', color: '#484C51' }, - { name: 'badge-background-level-2', token: '', color: '#2C65BA' }, - { name: 'badge-background-level-3', token: '', color: '#A9642D' }, - { name: 'badge-background-level-4', token: '', color: '#BB3E4E' }, - ], - }, - { - category: 'Status Bullet', - description: 'Used to show user status', - list: [ - { name: 'status-bullet-online', token: '', color: '#1CBF89' }, - { name: 'status-bullet-away', token: '', color: '#B08C30' }, - { name: 'status-bullet-busy', token: '', color: '#C75765' }, - { name: 'status-bullet-disabled', token: '', color: '#CC7F42' }, - { name: 'status-bullet-offline', token: '', color: '#8B9098' }, - { name: 'status-bullet-loading', token: '', color: '#8B9098' }, - ], - }, - { - category: 'Elevation', - description: 'Elevation border and shadow levels', - list: [ - { name: 'shadow-elevation-border', token: '', color: '#2F343D' }, - { name: 'shadow-elevation-1', token: '', color: 'rgba(9, 9, 9, 0.35)' }, - { name: 'shadow-elevation-2x', token: '', color: 'rgba(9, 9, 9, 0.3)' }, - { name: 'shadow-elevation-2y', token: '', color: 'rgba(9, 9, 9, 0.45)' }, - ], - }, - { - category: 'Button', - description: 'Primary Background', - list: [ - { name: 'button-background-primary-default', token: '', color: '#095AD2' }, - { name: 'button-background-primary-hover', token: '', color: '#10529E' }, - { name: 'button-background-primary-press', token: '', color: '#01336B' }, - { name: 'button-background-primary-focus', token: '', color: '#095AD2' }, - { name: 'button-background-primary-keyfocus', token: '', color: '#095AD2' }, - { name: 'button-background-primary-disabled', token: '', color: '#012247' }, - ], - }, - { - description: 'Secondary Background', - list: [ - { name: 'button-background-secondary-default', token: 'N800', color: '#353B45' }, - { name: 'button-background-secondary-hover', token: '', color: '#404754' }, - { name: 'button-background-secondary-press', token: '', color: '#4C5362' }, - { name: 'button-background-secondary-focus', token: 'N800', color: '#353B45' }, - { name: 'button-background-secondary-keyfocus', token: 'N800', color: '#353B45' }, - { name: 'button-background-secondary-disabled', token: '', color: '#353B45' }, - ], - }, - { - description: 'Secondary Danger Background', - list: [ - { name: 'button-background-secondary-danger-default', token: 'N800', color: '#353B45' }, - { name: 'button-background-secondary-danger-hover', token: '', color: '#404754' }, - { name: 'button-background-secondary-danger-press', token: '', color: '#4C5362' }, - { name: 'button-background-secondary-danger-focus', token: 'N800', color: '#353B45' }, - { name: 'button-background-secondary-danger-keyfocus', token: 'N800', color: '#353B45' }, - { name: 'button-background-secondary-danger-disabled', token: '', color: '#353B45' }, - ], - }, - { - description: 'Danger Background', - list: [ - { name: 'button-background-danger-default', token: '', color: '#BB3E4E' }, - { name: 'button-background-danger-hover', token: '', color: '#95323F' }, - { name: 'button-background-danger-press', token: '', color: '#822C37' }, - { name: 'button-background-danger-focus', token: '', color: '#BB3E4E' }, - { name: 'button-background-danger-keyfocus', token: '', color: '#BB3E4E' }, - { name: 'button-background-danger-disabled', token: '', color: '#3D2126' }, - ], - }, - { - description: 'Success Background', - list: [ - { name: 'button-background-success-default', token: '', color: '#1D7256' }, - { name: 'button-background-success-hover', token: '', color: '#175943' }, - { name: 'button-background-success-press', token: '', color: '#134937' }, - { name: 'button-background-success-focus', token: '', color: '#1D7256' }, - { name: 'button-background-success-keyfocus', token: '', color: '#1D7256' }, - { name: 'button-background-success-disabled', token: '', color: '#1E4B40' }, - ], - }, - { - description: 'Font', - list: [ - { name: 'button-font-on-primary', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-secondary', token: 'N400', color: '#E4E7EA' }, - { name: 'button-font-on-secondary-danger', token: '', color: '#FFC1C9' }, - { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-success', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-primary-disabled', token: 'N700', color: '#6C727A' }, - { name: 'button-font-on-secondary-disabled', token: 'N700', color: '#6C727A' }, - { - name: 'button-font-on-secondary-danger-disabled', - token: '', - color: '#6B0513', - }, - { name: 'button-font-on-danger-disabled', token: '', color: '#757575' }, - { name: 'button-font-on-success-disabled', token: '', color: '#757575' }, - ], - }, -]; - -export const darkPalette = { - ...palette.reduce( - (rec, group) => ({ - ...rec, - ...group.list.reduce( - (rec, item) => ({ - ...rec, - [item.name]: item.color, - }), - {} as Record, - ), - }), - {} as Record, - ), -}; diff --git a/ee/packages/ui-theming/src/paletteHighContrast.ts b/ee/packages/ui-theming/src/paletteHighContrast.ts deleted file mode 100644 index 1ec7bcf793de..000000000000 --- a/ee/packages/ui-theming/src/paletteHighContrast.ts +++ /dev/null @@ -1,211 +0,0 @@ -export const palette = [ - { - category: 'Stroke', - description: "Use as component's outline, stroke, dividers", - list: [ - { name: 'stroke-extra-light', token: 'N250', color: '#EBECEF' }, - { name: 'stroke-light', token: 'N500', color: '#CBCED1' }, - { name: 'stroke-medium', token: 'N600', color: '#9EA2A8' }, - { name: 'stroke-dark', token: 'N700', color: '#6C727A' }, - { name: 'stroke-extra-dark', token: 'N800', color: '#2F343D' }, - { name: 'stroke-extra-light-highlight', token: 'P200', color: '#D1EBFE' }, - { name: 'stroke-highlight', token: 'P500', color: '#156FF5' }, - { name: 'stroke-extra-light-error', token: 'D200', color: '#FFC1C9' }, - { name: 'stroke-error', token: 'D500', color: '#EC0D2A' }, - ], - }, - { - category: 'Surface', - description: 'Use as a container on top of the background', - list: [ - { name: 'surface-light', token: 'white', color: '#FFFFFF' }, - { name: 'surface-tint', token: 'N100', color: '#F7F8FA' }, - { name: 'surface-room', token: 'white', color: '#FFFFFF' }, - { name: 'surface-neutral', token: 'N400', color: '#E4E7EA' }, - { name: 'surface-disabled', token: 'N100', color: '#F7F8FA' }, - { name: 'surface-hover', token: 'N200', color: '#F2F3F5' }, - { name: 'surface-selected', token: 'N400', color: '#D7DBE0' }, - { name: 'surface-dark', token: 'N900', color: '#1F2329' }, - { name: 'surface-featured', token: '', color: '#5F1477' }, - { name: 'surface-featured-hover', token: '', color: '#4A105D' }, - { name: 'surface-overlay', token: '', color: 'rgba(47, 52, 61, 0.5)' }, - { name: 'surface-sidebar', token: 'N400', color: '#E4E7EA' }, - ], - }, - { - category: 'Shadow', - description: 'Use as a shadow color', - list: [ - { name: 'shadow-highlight', token: '', color: '#D1EBFE' }, - { name: 'shadow-danger', token: '', color: '#FFE9EC' }, - ], - }, - { - category: 'Font', - description: 'These should be applied according to surfaces', - list: [ - { name: 'font-white', token: 'white', color: '#FFFFFF' }, - { name: 'font-disabled', token: '', color: '#CBCED1' }, - { name: 'font-annotation', token: '', color: '#1F2329' }, - { name: 'font-hint', token: '', color: '#1F2329' }, - { name: 'font-secondary-info', token: '', color: '#1F2329' }, - { name: 'font-default', token: '', color: '#1F2329' }, - { name: 'font-titles-labels', token: '', color: '#1F2329' }, - { name: 'font-info', token: '', color: '#01336B' }, - { name: 'font-danger', token: '', color: '#9B1325' }, - { name: 'font-pure-black', token: '', color: '#1F2329' }, - { name: 'font-pure-white', token: '', color: '#FFFFFF' }, - ], - }, - { - category: 'Status', - description: 'Status Background', - list: [ - { name: 'status-background-info', token: 'P200', color: '#D1EBFE' }, - { name: 'status-background-success', token: 'S500', color: '#C0F6E4' }, - { name: 'status-background-danger', token: 'D200', color: '#FFC1C9' }, - { name: 'status-background-warning', token: 'W200', color: '#FFECAD' }, - { name: 'status-background-warning-2', token: 'W100', color: '#FFF8E0' }, - { name: 'status-background-service-1', token: 'S1-200', color: '#FAD1B0' }, - { name: 'status-background-service-2', token: 'S2-200', color: '#EDD0F7' }, - { name: 'status-background-service-3', token: 'S2-700', color: '#5F1477' }, - ], - }, - { - description: 'Status Font', - list: [ - { name: 'status-font-on-info', token: '', color: '#10529E' }, - { name: 'status-font-on-success', token: '', color: '#0D5940' }, - { name: 'status-font-on-danger', token: 'D800', color: '#6B0513' }, - { name: 'status-font-on-warning', token: 'W900', color: '#573D00' }, - { name: 'status-font-on-warning-2', token: 'N800', color: '#2F343D' }, - { name: 'status-font-on-service-1', token: 'S1-800', color: '#5B2C06' }, - { name: 'status-font-on-service-2 ', token: 'S2-600', color: '#5F1477' }, - { name: 'status-font-on-service-3 ', token: 'white', color: '#FFFFFF' }, - ], - }, - { - category: 'Badge', - description: 'Badge Background', - list: [ - { name: 'badge-background-level-0', token: '', color: '#E4E7EA' }, - { name: 'badge-background-level-1', token: '', color: '#2F343D' }, - { name: 'badge-background-level-2', token: '', color: '#10529E' }, - { name: 'badge-background-level-3', token: '', color: '#713607' }, - { name: 'badge-background-level-4', token: '', color: '#9B1325' }, - ], - }, - { - category: 'Status Bullet', - description: 'Used to show user status', - list: [ - { name: 'status-bullet-online', token: '', color: '#0D5940' }, - { name: 'status-bullet-away', token: '', color: '#573D00' }, - { name: 'status-bullet-busy', token: '', color: '#9B1325' }, - { name: 'status-bullet-disabled', token: '', color: '#BD5A0B' }, - { name: 'status-bullet-offline', token: '', color: '#1F2329' }, - { name: 'status-bullet-loading', token: '', color: '#1F2329' }, - ], - }, - { - category: 'Elevation', - description: 'Elevation border and shadow levels', - list: [ - { name: 'shadow-elevation-border', token: '', color: '#EBECEF' }, - { name: 'shadow-elevation-1', token: '', color: 'rgba(47, 52, 61, 0.1)' }, - { name: 'shadow-elevation-2x', token: '', color: 'rgba(47, 52, 61, 0.08)' }, - { name: 'shadow-elevation-2y', token: '', color: 'rgba(47, 52, 61, 0.12)' }, - ], - }, - { - category: 'Button', - description: 'Primary Background', - list: [ - { name: 'button-background-primary-default', token: '', color: '#10529E' }, - { name: 'button-background-primary-hover', token: '', color: '#01336B' }, - { name: 'button-background-primary-press', token: '', color: '#012247' }, - { name: 'button-background-primary-focus', token: '', color: '#10529E' }, - { name: 'button-background-primary-keyfocus', token: '', color: '#10529E' }, - { name: 'button-background-primary-disabled', token: '', color: '#D1EBFE' }, - ], - }, - { - description: 'Secondary Background', - list: [ - { name: 'button-background-secondary-default', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-press', token: '', color: '#9EA2A8' }, - { name: 'button-background-secondary-focus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-keyfocus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-disabled', token: 'N300', color: '#EEEFF1' }, - ], - }, - { - description: 'Secondary Danger Background', - list: [ - { name: 'button-background-secondary-danger-default', token: '', color: '#E4E7EA' }, - { name: 'button-background-secondary-danger-hover', token: '', color: '#CBCED1' }, - { name: 'button-background-secondary-danger-press', token: '', color: '#9EA2A8' }, - { name: 'button-background-secondary-danger-focus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-danger-keyfocus', token: 'N400', color: '#E4E7EA' }, - { name: 'button-background-secondary-danger-disabled', token: '', color: '#EEEFF1' }, - ], - }, - { - description: 'Danger Background', - list: [ - { name: 'button-background-danger-default', token: '', color: '#9B1325' }, - { name: 'button-background-danger-hover', token: '', color: '#8B0719' }, - { name: 'button-background-danger-press', token: '', color: '#8B0719' }, - { name: 'button-background-danger-focus', token: '', color: '#9B1325' }, - { name: 'button-background-danger-keyfocus', token: '', color: '#9B1325' }, - { name: 'button-background-danger-disabled', token: 'D200', color: '#FFC1C9' }, - ], - }, - { - description: 'Success Background', - list: [ - { name: 'button-background-success-default', token: '', color: '#148660' }, - { name: 'button-background-success-hover', token: 'S900', color: '#106D4F' }, - { name: 'button-background-success-press', token: 'S1000', color: '#0D5940' }, - { name: 'button-background-success-focus', token: '', color: '#148660' }, - { name: 'button-background-success-keyfocus', token: '', color: '#148660' }, - { name: 'button-background-success-disabled', token: 'S200', color: '#C0F6E4' }, - ], - }, - { - description: 'Font', - list: [ - { name: 'button-font-on-primary', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-primary-disabled', token: '', color: '#FFFFFF' }, - { name: 'button-font-on-secondary', token: '', color: '#1F2329' }, - { name: 'button-font-on-secondary-disabled', token: '', color: '#CBCED1' }, - { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-danger-disabled', token: '', color: '#FFFFFF' }, - { name: 'button-font-on-secondary-danger', token: '', color: '#8B0719' }, - { - name: 'button-font-on-secondary-danger-disabled', - token: '', - color: '#F98F9D', - }, - { name: 'button-font-on-success', token: '', color: '#FFFFFF' }, - { name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' }, - ], - }, -]; - -export const paletteHighContrast = { - ...palette.reduce( - (rec, group) => ({ - ...rec, - ...group.list.reduce( - (rec, item) => ({ - ...rec, - [item.name]: item.color, - }), - {} as Record, - ), - }), - {} as Record, - ), -}; diff --git a/packages/apps/.eslintrc.json b/packages/apps/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/packages/apps/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/apps/package.json b/packages/apps/package.json new file mode 100644 index 000000000000..33c46a314d21 --- /dev/null +++ b/packages/apps/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rocket.chat/apps", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/jest": "~29.5.7", + "eslint": "~8.45.0", + "jest": "~29.6.4", + "ts-jest": "~29.1.1", + "typescript": "~5.3.3" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "test": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "dependencies": { + "@rocket.chat/apps-engine": "^1.41.0", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/model-typings": "workspace:^" + } +} diff --git a/packages/apps/src/AppsEngine.ts b/packages/apps/src/AppsEngine.ts new file mode 100644 index 000000000000..117e93c0ec2f --- /dev/null +++ b/packages/apps/src/AppsEngine.ts @@ -0,0 +1,20 @@ +export type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +export type { + IDepartment as IAppsDepartment, + ILivechatMessage as IAppsLivechatMessage, + ILivechatRoom as IAppsLivechatRoom, + IVisitor as IAppsVisitor, + IVisitorEmail as IAppsVisitorEmail, + IVisitorPhone as IAppsVisitorPhone, +} from '@rocket.chat/apps-engine/definition/livechat'; +export type { IMessage as IAppsMessage } from '@rocket.chat/apps-engine/definition/messages'; +export type { IUser as IAppsUser } from '@rocket.chat/apps-engine/definition/users'; +export type { IRole as IAppsRole } from '@rocket.chat/apps-engine/definition/roles'; +export type { IRoom as IAppsRoom } from '@rocket.chat/apps-engine/definition/rooms'; +export type { ISetting as IAppsSetting } from '@rocket.chat/apps-engine/definition/settings'; +export type { IUpload as IAppsUpload } from '@rocket.chat/apps-engine/definition/uploads'; +export type { + IVideoConference as IAppsVideoConference, + VideoConference as AppsVideoConference, +} from '@rocket.chat/apps-engine/definition/videoConferences'; +export { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; diff --git a/packages/apps/src/IAppServerNotifier.ts b/packages/apps/src/IAppServerNotifier.ts new file mode 100644 index 000000000000..954f4a2ba5df --- /dev/null +++ b/packages/apps/src/IAppServerNotifier.ts @@ -0,0 +1,14 @@ +import type { AppStatus, IAppsSetting } from './AppsEngine'; + +export interface IAppServerNotifier { + appAdded(appId: string): Promise; + appRemoved(appId: string): Promise; + appUpdated(appId: string): Promise; + appStatusUpdated(appId: string, status: AppStatus): Promise; + appSettingsChange(appId: string, setting: IAppsSetting): Promise; + commandAdded(command: string): Promise; + commandDisabled(command: string): Promise; + commandUpdated(command: string): Promise; + commandRemoved(command: string): Promise; + actionsChanged(): Promise; +} diff --git a/packages/apps/src/IAppServerOrchestrator.ts b/packages/apps/src/IAppServerOrchestrator.ts new file mode 100644 index 000000000000..dbfc5aee7a20 --- /dev/null +++ b/packages/apps/src/IAppServerOrchestrator.ts @@ -0,0 +1,17 @@ +import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import type { Logger } from '@rocket.chat/logger'; +import type { IAppsPersistenceModel } from '@rocket.chat/model-typings'; + +import type { IAppServerNotifier } from './IAppServerNotifier'; +import type { IAppConvertersMap } from './converters'; + +export interface IAppServerOrchestrator { + initialize(): void; + getNotifier(): IAppServerNotifier; + isDebugging(): boolean; + debugLog(...args: any[]): void; + getManager(): AppManager; + getConverters(): IAppConvertersMap; + getPersistenceModel(): IAppsPersistenceModel; + getRocketChatLogger(): Logger; +} diff --git a/packages/apps/src/converters/IAppConvertersMap.ts b/packages/apps/src/converters/IAppConvertersMap.ts new file mode 100644 index 000000000000..63c94d44cb75 --- /dev/null +++ b/packages/apps/src/converters/IAppConvertersMap.ts @@ -0,0 +1,27 @@ +import type { IAppDepartmentsConverter } from './IAppDepartmentsConverter'; +import type { IAppMessagesConverter } from './IAppMessagesConverter'; +import type { IAppRolesConverter } from './IAppRolesConverter'; +import type { IAppRoomsConverter } from './IAppRoomsConverter'; +import type { IAppSettingsConverter } from './IAppSettingsConverter'; +import type { IAppThreadsConverter } from './IAppThreadsConverter'; +import type { IAppUploadsConverter } from './IAppUploadsConverter'; +import type { IAppUsersConverter } from './IAppUsersConverter'; +import type { IAppVideoConferencesConverter } from './IAppVideoConferencesConverter'; +import type { IAppVisitorsConverter } from './IAppVisitorsConverter'; + +type AppConverters = { + departments: IAppDepartmentsConverter; + messages: IAppMessagesConverter; + rooms: IAppRoomsConverter; + roles: IAppRolesConverter; + settings: IAppSettingsConverter; + threads: IAppThreadsConverter; + uploads: IAppUploadsConverter; + users: IAppUsersConverter; + visitors: IAppVisitorsConverter; + videoConferences: IAppVideoConferencesConverter; +}; + +export interface IAppConvertersMap extends Map { + get(key: T): AppConverters[T]; +} diff --git a/packages/apps/src/converters/IAppDepartmentsConverter.ts b/packages/apps/src/converters/IAppDepartmentsConverter.ts new file mode 100644 index 000000000000..8dee30caa38c --- /dev/null +++ b/packages/apps/src/converters/IAppDepartmentsConverter.ts @@ -0,0 +1,13 @@ +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; + +import type { IAppsDepartment } from '../AppsEngine'; + +export interface IAppDepartmentsConverter { + convertById(departmentId: ILivechatDepartment['_id']): Promise; + convertDepartment(department: undefined | null): Promise; + convertDepartment(department: ILivechatDepartment): Promise; + convertDepartment(department: ILivechatDepartment | undefined | null): Promise; + convertAppDepartment(department: undefined | null): undefined; + convertAppDepartment(department: IAppsDepartment): ILivechatDepartment; + convertAppDepartment(department: IAppsDepartment | undefined | null): ILivechatDepartment | undefined; +} diff --git a/packages/apps/src/converters/IAppMessagesConverter.ts b/packages/apps/src/converters/IAppMessagesConverter.ts new file mode 100644 index 000000000000..185e247895de --- /dev/null +++ b/packages/apps/src/converters/IAppMessagesConverter.ts @@ -0,0 +1,13 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +import type { IAppsMessage } from '../AppsEngine'; + +export interface IAppMessagesConverter { + convertById(messageId: IMessage['_id']): Promise; + convertMessage(message: undefined | null): Promise; + convertMessage(message: IMessage): Promise; + convertMessage(message: IMessage | undefined | null): Promise; + convertAppMessage(message: undefined | null): Promise; + convertAppMessage(message: IAppsMessage): Promise; + convertAppMessage(message: IAppsMessage | undefined | null): Promise; +} diff --git a/packages/apps/src/converters/IAppRolesConverter.ts b/packages/apps/src/converters/IAppRolesConverter.ts new file mode 100644 index 000000000000..07ed84232ade --- /dev/null +++ b/packages/apps/src/converters/IAppRolesConverter.ts @@ -0,0 +1,8 @@ +import type { IRole } from '@rocket.chat/core-typings'; + +import type { IAppsRole } from '../AppsEngine'; + +export interface IAppRolesConverter { + convertById(roleId: IRole['_id']): Promise; + convertRole(role: IRole): Promise; +} diff --git a/packages/apps/src/converters/IAppRoomsConverter.ts b/packages/apps/src/converters/IAppRoomsConverter.ts new file mode 100644 index 000000000000..9408b3f9b63c --- /dev/null +++ b/packages/apps/src/converters/IAppRoomsConverter.ts @@ -0,0 +1,14 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import type { IAppsRoom, IAppsLivechatRoom } from '../AppsEngine'; + +export interface IAppRoomsConverter { + convertById(roomId: IRoom['_id']): Promise; + convertByName(roomName: IRoom['name']): Promise; + convertRoom(room: undefined | null): Promise; + convertRoom(room: IRoom): Promise; + convertRoom(room: IRoom | undefined | null): Promise; + convertAppRoom(room: undefined | null): Promise; + convertAppRoom(room: IAppsRoom): Promise; + convertAppRoom(room: IAppsRoom | undefined | null): Promise; +} diff --git a/packages/apps/src/converters/IAppSettingsConverter.ts b/packages/apps/src/converters/IAppSettingsConverter.ts new file mode 100644 index 000000000000..32db63e06c70 --- /dev/null +++ b/packages/apps/src/converters/IAppSettingsConverter.ts @@ -0,0 +1,8 @@ +import type { ISetting } from '@rocket.chat/core-typings'; + +import type { IAppsSetting } from '../AppsEngine'; + +export interface IAppSettingsConverter { + convertById(settingId: ISetting['_id']): Promise; + convertToApp(setting: ISetting): IAppsSetting; +} diff --git a/packages/apps/src/converters/IAppThreadsConverter.ts b/packages/apps/src/converters/IAppThreadsConverter.ts new file mode 100644 index 000000000000..5253651c2683 --- /dev/null +++ b/packages/apps/src/converters/IAppThreadsConverter.ts @@ -0,0 +1,14 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +import type { IAppsMessage, IAppsRoom } from '../AppsEngine'; +import type { IAppUsersConverter } from './IAppUsersConverter'; + +export interface IAppThreadsConverter { + convertById(threadId: string): Promise; + convertMessage( + msgObj: IMessage, + room: IAppsRoom, + convertUserById: IAppUsersConverter['convertById'], + convertToApp: IAppUsersConverter['convertToApp'], + ): Promise; +} diff --git a/packages/apps/src/converters/IAppUploadsConverter.ts b/packages/apps/src/converters/IAppUploadsConverter.ts new file mode 100644 index 000000000000..4c7e4c2855c9 --- /dev/null +++ b/packages/apps/src/converters/IAppUploadsConverter.ts @@ -0,0 +1,13 @@ +import type { IUpload } from '@rocket.chat/core-typings'; + +import type { IAppsUpload } from '../AppsEngine'; + +export interface IAppUploadsConverter { + convertById(uploadId: string): Promise; + convertToApp(upload: undefined | null): Promise; + convertToApp(upload: IUpload): Promise; + convertToApp(upload: IUpload | undefined | null): Promise; + convertToRocketChat(upload: undefined | null): undefined; + convertToRocketChat(upload: IAppsUpload): IUpload; + convertToRocketChat(upload: IAppsUpload | undefined | null): IUpload | undefined; +} diff --git a/packages/apps/src/converters/IAppUsersConverter.ts b/packages/apps/src/converters/IAppUsersConverter.ts new file mode 100644 index 000000000000..8d67cb9e5240 --- /dev/null +++ b/packages/apps/src/converters/IAppUsersConverter.ts @@ -0,0 +1,14 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +import type { IAppsUser } from '../AppsEngine'; + +export interface IAppUsersConverter { + convertById(userId: IUser['_id']): Promise; + convertByUsername(username: IUser['username']): Promise; + convertToApp(user: undefined | null): undefined; + convertToApp(user: IUser): IAppsUser; + convertToApp(user: IUser | undefined | null): IAppsUser | undefined; + convertToRocketChat(user: undefined | null): undefined; + convertToRocketChat(user: IAppsUser): IUser; + convertToRocketChat(user: IAppsUser | undefined | null): IUser | undefined; +} diff --git a/packages/apps/src/converters/IAppVideoConferencesConverter.ts b/packages/apps/src/converters/IAppVideoConferencesConverter.ts new file mode 100644 index 000000000000..b599c29e3864 --- /dev/null +++ b/packages/apps/src/converters/IAppVideoConferencesConverter.ts @@ -0,0 +1,11 @@ +import type { VideoConference } from '@rocket.chat/core-typings'; + +import type { AppsVideoConference } from '../AppsEngine'; + +export interface IAppVideoConferencesConverter { + convertById(videoConferenceId: VideoConference['_id']): Promise; + convertVideoConference(videoConference: undefined | null): undefined; + convertVideoConference(videoConference: VideoConference): AppsVideoConference; + convertVideoConference(videoConference: VideoConference | undefined | null): AppsVideoConference | undefined; + convertAppVideoConference(videoConference: AppsVideoConference): VideoConference; +} diff --git a/packages/apps/src/converters/IAppVisitorsConverter.ts b/packages/apps/src/converters/IAppVisitorsConverter.ts new file mode 100644 index 000000000000..575845b57c10 --- /dev/null +++ b/packages/apps/src/converters/IAppVisitorsConverter.ts @@ -0,0 +1,14 @@ +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; + +import type { IAppsVisitor } from '../AppsEngine'; + +export interface IAppVisitorsConverter { + convertById(visitorId: ILivechatVisitor['_id']): Promise; + convertByToken(token: string): Promise; + convertVisitor(visitor: undefined | null): Promise; + convertVisitor(visitor: ILivechatVisitor): Promise; + convertVisitor(visitor: ILivechatVisitor | undefined | null): Promise; + convertAppVisitor(visitor: undefined | null): undefined; + convertAppVisitor(visitor: IAppsVisitor): ILivechatVisitor; + convertAppVisitor(visitor: IAppsVisitor | undefined | null): ILivechatVisitor | undefined; +} diff --git a/packages/apps/src/converters/index.ts b/packages/apps/src/converters/index.ts new file mode 100644 index 000000000000..61560afb5a4e --- /dev/null +++ b/packages/apps/src/converters/index.ts @@ -0,0 +1,11 @@ +export * from './IAppConvertersMap'; +export * from './IAppDepartmentsConverter'; +export * from './IAppMessagesConverter'; +export * from './IAppRolesConverter'; +export * from './IAppRoomsConverter'; +export * from './IAppSettingsConverter'; +export * from './IAppThreadsConverter'; +export * from './IAppUploadsConverter'; +export * from './IAppUsersConverter'; +export * from './IAppVideoConferencesConverter'; +export * from './IAppVisitorsConverter'; diff --git a/packages/apps/src/index.ts b/packages/apps/src/index.ts new file mode 100644 index 000000000000..e137fa3cf007 --- /dev/null +++ b/packages/apps/src/index.ts @@ -0,0 +1,4 @@ +export * from './converters'; +export * from './AppsEngine'; +export * from './IAppServerNotifier'; +export * from './IAppServerOrchestrator'; diff --git a/packages/apps/tsconfig.json b/packages/apps/tsconfig.json new file mode 100644 index 000000000000..52e9dd8c4976 --- /dev/null +++ b/packages/apps/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/fuselage-ui-kit/.storybook/DocsContainer.tsx b/packages/fuselage-ui-kit/.storybook/DocsContainer.tsx new file mode 100644 index 000000000000..39f4c0cccb0b --- /dev/null +++ b/packages/fuselage-ui-kit/.storybook/DocsContainer.tsx @@ -0,0 +1,42 @@ +import { DocsContainer as BaseContainer } from '@storybook/addon-docs/blocks'; +import { themes } from '@storybook/theming'; +import type { ComponentProps } from 'react'; +import { useDarkMode } from 'storybook-dark-mode'; + +import { surface } from './helpers'; + +export const DocsContainer = ({ + children, + context, +}: ComponentProps) => { + const dark = useDarkMode(); + + return ( + { + const storyContext = context.storyById(id); + return { + ...storyContext, + parameters: { + ...storyContext?.parameters, + docs: { + ...storyContext?.parameters?.docs, + theme: dark + ? { + ...themes.dark, + appContentBg: surface.main, + barBg: surface.main, + } + : themes.light, + }, + }, + }; + }, + }} + > + {children} + + ); +}; diff --git a/packages/fuselage-ui-kit/.storybook/helpers.ts b/packages/fuselage-ui-kit/.storybook/helpers.ts new file mode 100644 index 000000000000..aee5f8ad0969 --- /dev/null +++ b/packages/fuselage-ui-kit/.storybook/helpers.ts @@ -0,0 +1,4 @@ +export const surface = { + sidebar: '#2F343D', + main: '#262931', +}; diff --git a/packages/fuselage-ui-kit/.storybook/preview.tsx b/packages/fuselage-ui-kit/.storybook/preview.tsx index 82ba8a060ef6..9982de43f6bb 100644 --- a/packages/fuselage-ui-kit/.storybook/preview.tsx +++ b/packages/fuselage-ui-kit/.storybook/preview.tsx @@ -1,17 +1,12 @@ -import { codeBlock } from '@rocket.chat/ui-theming/src/codeBlockStyles'; -import { convertToCss } from '@rocket.chat/ui-theming/src/helpers/convertToCss'; -import { filterOnlyChangedColors } from '@rocket.chat/ui-theming/src/helpers/filterOnlyChangedColors'; -import { useCreateStyleContainer } from '@rocket.chat/ui-theming/src/hooks/useCreateStyleContainer'; -import { defaultPalette } from '@rocket.chat/ui-theming/src/palette'; -import { darkPalette } from '@rocket.chat/ui-theming/src/paletteDark'; +import { PaletteStyleTag } from '@rocket.chat/fuselage'; import { type Parameters } from '@storybook/addons'; import { type DecoratorFn } from '@storybook/react'; import { themes } from '@storybook/theming'; -import { createElement } from 'react'; -import { createPortal } from 'react-dom'; import { useDarkMode } from 'storybook-dark-mode'; import manifest from '../package.json'; +import { DocsContainer } from './DocsContainer'; +import { surface } from './helpers'; import logo from './logo.svg'; import '@rocket.chat/fuselage/dist/fuselage.css'; @@ -32,9 +27,15 @@ export const parameters: Parameters = { storySort: ([, a], [, b]) => a.kind.localeCompare(b.kind), }, layout: 'fullscreen', + docs: { + container: DocsContainer, + }, darkMode: { dark: { ...themes.dark, + appBg: surface.sidebar, + appContentBg: surface.main, + barBg: surface.main, brandTitle: manifest.name, brandImage: logo, brandUrl: manifest.homepage, @@ -49,23 +50,14 @@ export const parameters: Parameters = { }; export const decorators: DecoratorFn[] = [ - (fn) => - createElement(function RocketChatDarkMode() { - const dark = useDarkMode(); - - const palette = convertToCss( - filterOnlyChangedColors(defaultPalette, dark ? darkPalette : {}), - 'body' - ); + (fn) => { + const dark = useDarkMode(); - return ( - <> - {createPortal( - dark ? palette + codeBlock : palette, - useCreateStyleContainer('main-palette') - )} - {fn()} - - ); - }), + return ( + <> + + {fn()} + + ); + }, ]; diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 068e559f101a..a9a9e7a7fd99 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -63,7 +63,7 @@ "@babel/preset-typescript": "~7.22.15", "@rocket.chat/apps-engine": "1.41.0", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/fuselage-hooks": "^0.33.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "^0.33.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index b95a374fe96c..78226d19012b 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,8 +6,8 @@ "@babel/core": "~7.22.20", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.48.0", - "@rocket.chat/fuselage-tokens": "~0.32.0", + "@rocket.chat/fuselage": "^0.49.0", + "@rocket.chat/fuselage-tokens": "^0.33.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/styled": "~0.31.25", "@rocket.chat/ui-client": "workspace:^", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 271907a68caa..95aadb64a5d5 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -312,6 +312,7 @@ "Add_files_from": "Add files from", "Add_manager": "Add manager", "Add_monitor": "Add monitor", + "Add_link": "Add link", "Add_Reaction": "Add reaction", "Add_Role": "Add Role", "Add_Sender_To_ReplyTo": "Add Sender to Reply-To", @@ -5108,6 +5109,7 @@ "test-push-notifications": "Test push notifications", "test-push-notifications_description": "Permission to test push notifications", "Texts": "Texts", + "Text": "Text", "Thank_you_for_your_feedback": "Thank you for your feedback", "The_application_name_is_required": "The application name is required", "The_application_will_be_able_to": "<1>{{appName}} will be able to:", diff --git a/packages/livechat/package.json b/packages/livechat/package.json index c5bbb001cd1d..9ba1be9afb56 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -31,7 +31,7 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/ddp-client": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage-tokens": "~0.32.0", + "@rocket.chat/fuselage-tokens": "^0.33.0", "@rocket.chat/logo": "^0.31.29", "@storybook/addon-essentials": "~6.5.16", "@storybook/addon-postcss": "~2.0.0", diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index 3445f8e18231..138ff482d208 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -359,10 +359,10 @@ EmphasisForReferences = BoldForReferences / ItalicForReferences / StrikethroughF Italic = value:$([a-zA-Z0-9]+ [\x5F] [\x5F]?) { return plain(value); } / [\x5F] [\x5F] i:ItalicContentItems [\x5F] [\x5F] t:$[a-zA-Z0-9]+ { - return reducePlainTexts([plain('__'), ...i, plain('__'), plain(t)])[0]; + return reducePlainTexts([plain('__'), ...i, plain('__'), plain(t)]); } / [\x5F] i:ItalicContentItems [\x5F] t:$[a-zA-Z]+ { - return reducePlainTexts([plain('_'), ...i, plain('_'), plain(t)])[0]; + return reducePlainTexts([plain('_'), ...i, plain('_'), plain(t)]); } / [\x5F] [\x5F] @ItalicContent [\x5F] [\x5F] / [\x5F] @ItalicContent [\x5F] diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 5902d8e33e7d..44527529b4a6 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -197,20 +197,21 @@ const joinEmoji = ( export const reducePlainTexts = ( values: Paragraph['value'] ): Paragraph['value'] => - values.reduce((result, item, index) => { - const next = values[index + 1]; - const current = joinEmoji(item, values[index - 1], next); - const previous: Inlines = result[result.length - 1]; - - if (previous) { - if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { - previous.value += current.value; - return result; + values + .flatMap((item) => item) + .reduce((result, item, index, values) => { + const next = values[index + 1]; + const current = joinEmoji(item, values[index - 1], next); + const previous: Inlines = result[result.length - 1]; + + if (previous) { + if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { + previous.value += current.value; + return result; + } } - } - - return [...result, current]; - }, [] as Paragraph['value']); + return [...result, current]; + }, [] as Paragraph['value']); export const lineBreak = (): LineBreak => ({ type: 'LINE_BREAK', value: undefined, diff --git a/packages/message-parser/tests/emphasis.test.ts b/packages/message-parser/tests/emphasis.test.ts index bae05be7d158..e8e72a5882f1 100644 --- a/packages/message-parser/tests/emphasis.test.ts +++ b/packages/message-parser/tests/emphasis.test.ts @@ -11,6 +11,7 @@ import { emojiUnicode, mentionChannel, mentionUser, + inlineCode, } from '../src/utils'; test.each([ @@ -169,6 +170,21 @@ test.each([ ]), ], ], + ['_ouch_ouch', [paragraph([plain('_ouch_ouch')])]], + [ + `_@mention _gone`, + [paragraph([plain('_'), mentionUser('mention'), plain(' _gone')])], + ], + [ + '_nothing `should` be _gone', + [ + paragraph([ + plain('_nothing '), + inlineCode(plain('should')), + plain(' be _gone'), + ]), + ], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); diff --git a/packages/message-parser/tsconfig.json b/packages/message-parser/tsconfig.json index 3e59aeae898f..619475cd7914 100644 --- a/packages/message-parser/tsconfig.json +++ b/packages/message-parser/tsconfig.json @@ -6,6 +6,7 @@ "declaration": true, "declarationMap": true, "sourceMap": true, + "lib": ["es2019"], "outDir": "./dist", "strict": true, "esModuleInterop": true, diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 7bf05ddaf253..3d4468366c72 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@babel/core": "~7.22.20", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/ui-contexts": "workspace:^", "@types/babel__core": "~7.20.3", "@types/react": "~17.0.69", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 29518b2febe9..d50cb7738f40 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/fuselage-hooks": "^0.33.0", "@rocket.chat/icons": "^0.33.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx b/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx index c921f2b93525..44661229707c 100644 --- a/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx +++ b/packages/ui-client/src/components/Header/HeaderTag/HeaderTag.tsx @@ -2,7 +2,7 @@ import { Box, Tag } from '@rocket.chat/fuselage'; import type { ComponentProps, FC } from 'react'; const HeaderTag: FC> = ({ children, ...props }) => ( - + {children} diff --git a/packages/ui-client/src/components/Header/HeaderTitleButton.tsx b/packages/ui-client/src/components/Header/HeaderTitleButton.tsx new file mode 100644 index 000000000000..c27f20ac9e09 --- /dev/null +++ b/packages/ui-client/src/components/Header/HeaderTitleButton.tsx @@ -0,0 +1,25 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +const HeaderTitleButton = ({ className, ...props }: { className?: string } & ComponentProps) => { + const customClass = css` + border-width: 1px; + border-style: solid; + border-color: transparent; + + &:hover { + cursor: pointer; + background-color: ${Palette.surface['surface-hover']}; + } + &:focus.focus-visible { + outline: 0; + box-shadow: 0 0 0 2px ${Palette.stroke['stroke-extra-light-highlight']}; + border-color: ${Palette.stroke['stroke-highlight']}; + } + `; + + return ; +}; + +export default HeaderTitleButton; diff --git a/packages/ui-client/src/components/Header/index.ts b/packages/ui-client/src/components/Header/index.ts index b8d62fb777f9..00e2c0ab17dc 100644 --- a/packages/ui-client/src/components/Header/index.ts +++ b/packages/ui-client/src/components/Header/index.ts @@ -8,4 +8,5 @@ export { default as HeaderState } from './HeaderState'; export { default as HeaderSubtitle } from './HeaderSubtitle'; export * from './HeaderTag'; export { default as HeaderTitle } from './HeaderTitle'; +export { default as HeaderTitleButton } from './HeaderTitleButton'; export * from './HeaderToolbar'; diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index bc63d18be2b3..65035ce9a77c 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/icons": "^0.33.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 2bdc366985b0..2079018cb029 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/fuselage-hooks": "^0.33.0", "@rocket.chat/icons": "^0.33.0", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 4bad525b77a9..26b65d9bcbda 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,11 +15,11 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.48.0", + "@rocket.chat/fuselage": "^0.49.0", "@rocket.chat/fuselage-hooks": "^0.33.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.31.25", - "@rocket.chat/fuselage-tokens": "~0.32.0", + "@rocket.chat/fuselage-tokens": "^0.33.0", "@rocket.chat/fuselage-ui-kit": "workspace:~", "@rocket.chat/icons": "^0.33.0", "@rocket.chat/logo": "^0.31.29", diff --git a/packages/web-ui-registration/src/LoginForm.tsx b/packages/web-ui-registration/src/LoginForm.tsx index 3290b9f16191..d63886091fa0 100644 --- a/packages/web-ui-registration/src/LoginForm.tsx +++ b/packages/web-ui-registration/src/LoginForm.tsx @@ -56,7 +56,9 @@ const LOGIN_SUBMIT_ERRORS = { }, } as const; -export type LoginErrors = keyof typeof LOGIN_SUBMIT_ERRORS | 'totp-canceled'; +export type LoginErrors = keyof typeof LOGIN_SUBMIT_ERRORS | 'totp-canceled' | string; + +export type LoginErrorState = [error: LoginErrors, message?: string] | undefined; export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRouter }): ReactElement => { const { @@ -72,7 +74,7 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute const { t } = useTranslation(); const formLabelId = useUniqueId(); - const [errorOnSubmit, setErrorOnSubmit] = useState(undefined); + const [errorOnSubmit, setErrorOnSubmit] = useState(undefined); const isResetPasswordAllowed = useSetting('Accounts_PasswordReset'); const login = useLoginWithPassword(); const showFormLogin = useSetting('Accounts_ShowFormLogin'); @@ -92,11 +94,11 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute } if ('error' in error && error.error !== 403) { - setErrorOnSubmit(error.error); + setErrorOnSubmit([error.error, error.reason]); return; } - setErrorOnSubmit('user-not-found'); + setErrorOnSubmit(['user-not-found']); }, }); @@ -110,18 +112,28 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute } }, [errorOnSubmit]); - const renderErrorOnSubmit = (error: LoginErrors) => { + const renderErrorOnSubmit = ([error, message]: Exclude) => { + if (error in LOGIN_SUBMIT_ERRORS) { + const { type, i18n } = LOGIN_SUBMIT_ERRORS[error as Exclude]; + return ( + + {t(i18n)} + + ); + } + if (error === 'totp-canceled') { return null; } - const { type, i18n } = LOGIN_SUBMIT_ERRORS[error]; - - return ( - - {t(i18n)} - - ); + if (message) { + return ( + + {message} + + ); + } + return null; }; if (errors.usernameOrEmail?.type === 'invalid-email') { diff --git a/packages/web-ui-registration/src/LoginServices.tsx b/packages/web-ui-registration/src/LoginServices.tsx index 530dce7e9438..a5068fbaac3f 100644 --- a/packages/web-ui-registration/src/LoginServices.tsx +++ b/packages/web-ui-registration/src/LoginServices.tsx @@ -3,7 +3,7 @@ import { useLoginServices, useSetting } from '@rocket.chat/ui-contexts'; import type { Dispatch, ReactElement, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; -import type { LoginErrors } from './LoginForm'; +import type { LoginErrorState } from './LoginForm'; import LoginServicesButton from './LoginServicesButton'; const LoginServices = ({ @@ -11,7 +11,7 @@ const LoginServices = ({ setError, }: { disabled?: boolean; - setError: Dispatch>; + setError: Dispatch>; }): ReactElement | null => { const { t } = useTranslation(); const services = useLoginServices(); diff --git a/packages/web-ui-registration/src/LoginServicesButton.tsx b/packages/web-ui-registration/src/LoginServicesButton.tsx index cdba5e26474e..d9f43b0e484c 100644 --- a/packages/web-ui-registration/src/LoginServicesButton.tsx +++ b/packages/web-ui-registration/src/LoginServicesButton.tsx @@ -5,7 +5,7 @@ import { useLoginWithService, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, SetStateAction, Dispatch } from 'react'; import { useCallback } from 'react'; -import type { LoginErrors } from './LoginForm'; +import type { LoginErrorState, LoginErrors } from './LoginForm'; const LoginServicesButton = ({ buttonLabelText, @@ -19,17 +19,17 @@ const LoginServicesButton = ({ }: T & { className?: string; disabled?: boolean; - setError?: Dispatch>; + setError?: Dispatch>; }): ReactElement => { const t = useTranslation(); const handler = useLoginWithService({ service, buttonLabelText, ...props }); const handleOnClick = useCallback(() => { - handler().catch((e: { error?: LoginErrors }) => { + handler().catch((e: { error?: LoginErrors; reason?: string }) => { if (!e.error || typeof e.error !== 'string') { return; } - setError?.(e.error); + setError?.([e.error, e.reason]); }); }, [handler, setError]); diff --git a/yarn.lock b/yarn.lock index 5a9c7311afaa..72c94c3be6d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8331,7 +8331,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.41.0": +"@rocket.chat/apps-engine@npm:1.41.0, @rocket.chat/apps-engine@npm:^1.41.0": version: 1.41.0 resolution: "@rocket.chat/apps-engine@npm:1.41.0" dependencies: @@ -8349,6 +8349,21 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/apps@workspace:^, @rocket.chat/apps@workspace:packages/apps": + version: 0.0.0-use.local + resolution: "@rocket.chat/apps@workspace:packages/apps" + dependencies: + "@rocket.chat/apps-engine": ^1.41.0 + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@types/jest": ~29.5.7 + eslint: ~8.45.0 + jest: ~29.6.4 + ts-jest: ~29.1.1 + typescript: ~5.3.3 + languageName: unknown + linkType: soft + "@rocket.chat/authorization-service@workspace:ee/apps/authorization-service": version: 0.0.0-use.local resolution: "@rocket.chat/authorization-service@workspace:ee/apps/authorization-service" @@ -8700,10 +8715,10 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/fuselage-tokens@npm:^0.32.0, @rocket.chat/fuselage-tokens@npm:~0.32.0": - version: 0.32.0 - resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0" - checksum: 8da7836877ba93462f90d13de6d3d3add8b2758b58c7988e14a8f0deffd1ceef0547f26d4c60a7ddc881e21e3327b5a04cbf17336e5ca8ab9c19789d8e6af3c0 +"@rocket.chat/fuselage-tokens@npm:^0.33.0": + version: 0.33.0 + resolution: "@rocket.chat/fuselage-tokens@npm:0.33.0" + checksum: fee164884da145fdc1e121f4c96c46d32106cd04ed5d1901634552cc3a0d512f9e41cf7905bac0dc9d4212a3d984db4ff23c3227005ba347782a72851cbc7277 languageName: node linkType: hard @@ -8717,7 +8732,7 @@ __metadata: "@babel/preset-typescript": ~7.22.15 "@rocket.chat/apps-engine": 1.41.0 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/fuselage-hooks": ^0.33.0 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" @@ -8772,13 +8787,13 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.48.0": - version: 0.48.0 - resolution: "@rocket.chat/fuselage@npm:0.48.0" +"@rocket.chat/fuselage@npm:^0.49.0": + version: 0.49.0 + resolution: "@rocket.chat/fuselage@npm:0.49.0" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 - "@rocket.chat/fuselage-tokens": ^0.32.0 + "@rocket.chat/fuselage-tokens": ^0.33.0 "@rocket.chat/memo": ^0.31.25 "@rocket.chat/styled": ^0.31.25 invariant: ^2.2.4 @@ -8792,7 +8807,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: d82121b6e6d0fa6ea204d8cbd2cc32460c4ca3585c05a1a1d908250c9150ed52edfd0acc75520cb29c6bd81a8671dc7d91f427913bcc4364a14b4eacd4bd2bf4 + checksum: 06adb80ca8399f5ca01364a433dcab9fbd3da819e109b6eff764f7c0ffa19fc533c6ff023fc23b6beba1a389f6b4d483450a5d747bc59987b1355508fa81d6c0 languageName: node linkType: hard @@ -8803,8 +8818,8 @@ __metadata: "@babel/core": ~7.22.20 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.48.0 - "@rocket.chat/fuselage-tokens": ~0.32.0 + "@rocket.chat/fuselage": ^0.49.0 + "@rocket.chat/fuselage-tokens": ^0.33.0 "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/styled": ~0.31.25 "@rocket.chat/ui-client": "workspace:^" @@ -8952,7 +8967,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage-tokens": ~0.32.0 + "@rocket.chat/fuselage-tokens": ^0.33.0 "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/logo": ^0.31.29 "@rocket.chat/message-parser": "workspace:^" @@ -9146,6 +9161,7 @@ __metadata: "@rocket.chat/account-utils": "workspace:^" "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" + "@rocket.chat/apps": "workspace:^" "@rocket.chat/apps-engine": 1.41.0 "@rocket.chat/base64": "workspace:^" "@rocket.chat/cas-validate": "workspace:^" @@ -9158,11 +9174,11 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3 - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/fuselage-hooks": ^0.33.0 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ~0.31.25 - "@rocket.chat/fuselage-tokens": ~0.32.0 + "@rocket.chat/fuselage-tokens": ^0.33.0 "@rocket.chat/fuselage-ui-kit": "workspace:^" "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/i18n": "workspace:^" @@ -9253,6 +9269,7 @@ __metadata: "@types/ldapjs": ^2.2.5 "@types/less": ~3.0.5 "@types/lodash": ^4.14.200 + "@types/lodash.clonedeep": ^4.5.9 "@types/lodash.debounce": ^4.0.8 "@types/lodash.get": ^4.4.8 "@types/mailparser": ^3.4.3 @@ -9671,7 +9688,7 @@ __metadata: dependencies: "@react-pdf/renderer": ^3.1.14 "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/fuselage-tokens": ~0.32.0 + "@rocket.chat/fuselage-tokens": ^0.33.0 "@storybook/addon-essentials": ~6.5.16 "@storybook/react": ~6.5.16 "@testing-library/jest-dom": ^5.16.5 @@ -10037,7 +10054,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": ~7.22.20 - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/ui-contexts": "workspace:^" "@types/babel__core": ~7.20.3 "@types/react": ~17.0.69 @@ -10063,7 +10080,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/fuselage-hooks": ^0.33.0 "@rocket.chat/icons": ^0.33.0 "@rocket.chat/mock-providers": "workspace:^" @@ -10116,7 +10133,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/icons": ^0.33.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10208,7 +10225,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/fuselage-hooks": ^0.33.0 "@rocket.chat/icons": ^0.33.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -10251,7 +10268,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/fuselage-hooks": ^0.33.0 "@rocket.chat/icons": ^0.33.0 "@rocket.chat/styled": ~0.31.25 @@ -10296,11 +10313,11 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.48.0 + "@rocket.chat/fuselage": ^0.49.0 "@rocket.chat/fuselage-hooks": ^0.33.0 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.31.25 - "@rocket.chat/fuselage-tokens": ~0.32.0 + "@rocket.chat/fuselage-tokens": ^0.33.0 "@rocket.chat/fuselage-ui-kit": "workspace:~" "@rocket.chat/icons": ^0.33.0 "@rocket.chat/logo": ^0.31.29 @@ -13615,6 +13632,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash.clonedeep@npm:^4.5.9": + version: 4.5.9 + resolution: "@types/lodash.clonedeep@npm:4.5.9" + dependencies: + "@types/lodash": "*" + checksum: ef85512b7dce7a4f981a818ae44d11982907e1f26b5b26bedf0957c35e8591eb8e1d24fa31ca851d4b40e0a1ee88563853d762412691fe5f357e8335cead2325 + languageName: node + linkType: hard + "@types/lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "@types/lodash.debounce@npm:4.0.8"