diff --git a/apps/meteor/app/api/server/lib/messages.ts b/apps/meteor/app/api/server/lib/messages.ts index d6955ce7b933..71f9f540c6bf 100644 --- a/apps/meteor/app/api/server/lib/messages.ts +++ b/apps/meteor/app/api/server/lib/messages.ts @@ -55,7 +55,7 @@ export async function findStarredMessages({ }): Promise<{ messages: IMessage[]; count: number; - offset: any; + offset: number; total: number; }> { const room = await Rooms.findOneById(roomId); diff --git a/apps/meteor/app/mentions-flextab/client/actionButton.ts b/apps/meteor/app/mentions-flextab/client/actionButton.ts deleted file mode 100644 index aa39b619f3d4..000000000000 --- a/apps/meteor/app/mentions-flextab/client/actionButton.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { MessageAction, RoomHistoryManager } from '../../ui-utils/client'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; -import { Rooms } from '../../models/client'; - -Meteor.startup(function () { - MessageAction.addButton({ - id: 'jump-to-message', - icon: 'jump', - label: 'Jump_to_message', - context: ['mentions', 'threads'], - action(e, props) { - e.preventDefault(); - e.stopPropagation(); - const { message = messageArgs(this).msg } = props; - if (window.matchMedia('(max-width: 500px)').matches) { - (Template.instance() as any).tabBar.close(); - } - if (message.tmid) { - return FlowRouter.go( - FlowRouter.getRouteName(), - { - tab: 'thread', - context: message.tmid, - rid: message.rid, - name: Rooms.findOne({ _id: message.rid })?.name ?? '', - }, - { - jump: message._id, - }, - ); - } - RoomHistoryManager.getSurroundingMessages(message); - }, - order: 100, - group: ['message', 'menu'], - }); -}); diff --git a/apps/meteor/app/mentions-flextab/client/index.js b/apps/meteor/app/mentions-flextab/client/index.js deleted file mode 100644 index f1443a980322..000000000000 --- a/apps/meteor/app/mentions-flextab/client/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import './views/mentionsFlexTab.html'; -import './views/mentionsFlexTab'; -import './actionButton'; -import './tabBar'; diff --git a/apps/meteor/app/mentions-flextab/client/tabBar.ts b/apps/meteor/app/mentions-flextab/client/tabBar.ts deleted file mode 100644 index fa88a0f2d96d..000000000000 --- a/apps/meteor/app/mentions-flextab/client/tabBar.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { addAction } from '../../../client/views/room/lib/Toolbox'; - -addAction('mentions', { - groups: ['channel', 'group', 'team'], - id: 'mentions', - title: 'Mentions', - icon: 'at', - template: 'mentionsFlexTab', - order: 9, -}); diff --git a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.html b/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.html deleted file mode 100644 index 4884793a3156..000000000000 --- a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.html +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js b/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js deleted file mode 100644 index 6ae77a09fa24..000000000000 --- a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js +++ /dev/null @@ -1,86 +0,0 @@ -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; - -import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; -import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { APIClient } from '../../../utils/client'; -import { Messages, Users } from '../../../models/client'; -import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; - -const LIMIT_DEFAULT = 50; - -Template.mentionsFlexTab.helpers({ - hasMessages() { - return Template.instance().messages.find().count(); - }, - messages() { - const instance = Template.instance(); - return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); - }, - hasMore() { - return Template.instance().hasMore.get(); - }, - messageContext: createMessageContext, -}); - -Template.mentionsFlexTab.onCreated(function () { - this.messages = new Mongo.Collection(null); - - this.hasMore = new ReactiveVar(true); - this.limit = new ReactiveVar(LIMIT_DEFAULT); - - this.autorun(() => { - const query = { - '_hidden': { $ne: true }, - 'mentions.username': Users.findOne(Meteor.userId(), { fields: { username: 1 } }).username, - 'rid': this.data.rid, - '_updatedAt': { - $gt: new Date(), - }, - }; - - this.cursor && this.cursor.stop(); - - this.limit.set(LIMIT_DEFAULT); - - this.cursor = Messages.find(query).observe({ - added: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - changed: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - removed: ({ _id }) => { - this.messages.remove({ _id }); - }, - }); - }); - - this.autorun(async () => { - const limit = this.limit.get(); - const { messages, total } = await APIClient.get('/v1/chat.getMentionedMessages', { - roomId: this.data.rid, - count: limit, - }); - - upsertMessageBulk({ msgs: messages }, this.messages); - - this.hasMore.set(total > limit); - }); -}); - -Template.mentionsFlexTab.onDestroyed(function () { - this.cursor.stop(); -}); - -Template.mentionsFlexTab.events({ - ...getCommonRoomEvents(), - 'scroll .js-list': _.throttle(function (e, instance) { - if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get()) { - return instance.limit.set(instance.limit.get() + 50); - } - }, 200), -}); diff --git a/apps/meteor/app/message-pin/client/actionButton.ts b/apps/meteor/app/message-pin/client/actionButton.ts deleted file mode 100644 index a661ba0c0b7a..000000000000 --- a/apps/meteor/app/message-pin/client/actionButton.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { RoomHistoryManager, MessageAction } from '../../ui-utils/client'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; -import { settings } from '../../settings/client'; -import { hasAtLeastOnePermission } from '../../authorization/client'; -import { Rooms } from '../../models/client'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; - -Meteor.startup(function () { - MessageAction.addButton({ - id: 'pin-message', - icon: 'pin', - label: 'Pin', - context: ['pinned', 'message', 'message-mobile', 'threads', 'direct'], - action(_, props) { - const { message = messageArgs(this).msg } = props; - message.pinned = true; - Meteor.call('pinMessage', message, function (error: Error) { - if (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - }, - condition({ message, subscription, room }) { - if (!settings.get('Message_AllowPinning') || message.pinned || !subscription) { - return false; - } - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } - return hasAtLeastOnePermission('pin-message', message.rid); - }, - order: 7, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'unpin-message', - icon: 'pin', - label: 'Unpin', - context: ['pinned', 'message', 'message-mobile', 'threads', 'direct'], - action(_, props) { - const { message = messageArgs(this).msg } = props; - message.pinned = false; - Meteor.call('unpinMessage', message, function (error: Error) { - if (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - }, - condition({ message, subscription }) { - if (!subscription || !settings.get('Message_AllowPinning') || !message.pinned) { - return false; - } - - return hasAtLeastOnePermission('pin-message', message.rid); - }, - order: 8, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'jump-to-pin-message', - icon: 'jump', - label: 'Jump_to_message', - context: ['pinned', 'message-mobile', 'direct'], - action(_, props) { - const { message = messageArgs(this).msg } = props; - if (window.matchMedia('(max-width: 500px)').matches) { - (Template.instance() as any).tabBar.close(); - } - if (message.tmid) { - return FlowRouter.go( - FlowRouter.getRouteName(), - { - tab: 'thread', - context: message.tmid, - rid: message.rid, - jump: message._id, - name: Rooms.findOne({ _id: message.rid })?.name ?? '', - }, - { - jump: message._id, - }, - ); - } - return RoomHistoryManager.getSurroundingMessages(message); - }, - condition({ subscription }) { - return !!subscription; - }, - order: 100, - group: ['message', 'menu'], - }); - - MessageAction.addButton({ - id: 'permalink-pinned', - icon: 'permalink', - label: 'Get_link', - // classes: 'clipboard', - context: ['pinned'], - async action(_, props) { - try { - const { message = messageArgs(this).msg } = props; - const permalink = await MessageAction.getPermaLink(message._id); - navigator.clipboard.writeText(permalink); - dispatchToastMessage({ type: 'success', message: TAPi18n.__('Copied') }); - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - }, - condition({ subscription }) { - return !!subscription; - }, - order: 101, - group: 'menu', - }); -}); diff --git a/apps/meteor/app/message-pin/client/index.js b/apps/meteor/app/message-pin/client/index.js deleted file mode 100644 index c48cf4ab0b98..000000000000 --- a/apps/meteor/app/message-pin/client/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import './actionButton'; -import './messageType'; -import './pinMessage'; -import './tabBar'; -import './views/pinnedMessages.html'; -import './views/pinnedMessages'; -import './views/stylesheets/messagepin.css'; diff --git a/apps/meteor/app/message-pin/client/messageType.js b/apps/meteor/app/message-pin/client/messageType.js deleted file mode 100644 index 0ce3da33853d..000000000000 --- a/apps/meteor/app/message-pin/client/messageType.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { MessageTypes } from '../../ui-utils'; - -Meteor.startup(function () { - MessageTypes.registerType({ - id: 'message_pinned', - system: true, - message: 'Pinned_a_message', - }); -}); diff --git a/apps/meteor/app/message-pin/client/pinMessage.js b/apps/meteor/app/message-pin/client/pinMessage.js deleted file mode 100644 index b45bed7839c3..000000000000 --- a/apps/meteor/app/message-pin/client/pinMessage.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { settings } from '../../settings'; -import { ChatMessage, Subscriptions } from '../../models/client'; -import { dispatchToastMessage } from '../../../client/lib/toast'; - -Meteor.methods({ - pinMessage(message) { - if (!Meteor.userId()) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-not-authorized') }); - return false; - } - if (!settings.get('Message_AllowPinning')) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('pinning-not-allowed') }); - return false; - } - if (Subscriptions.findOne({ rid: message.rid }) == null) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-pinning-message') }); - return false; - } - if (typeof message._id !== 'string') { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-pinning-message') }); - return false; - } - dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_pinned') }); - return ChatMessage.update( - { - _id: message._id, - rid: message.rid, - }, - { - $set: { - pinned: true, - }, - }, - ); - }, - unpinMessage(message) { - if (!Meteor.userId()) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-not-authorized') }); - return false; - } - if (!settings.get('Message_AllowPinning')) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('unpinning-not-allowed') }); - return false; - } - if (Subscriptions.findOne({ rid: message.rid }) == null) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-unpinning-message') }); - return false; - } - if (typeof message._id !== 'string') { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-unpinning-message') }); - return false; - } - dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_unpinned') }); - return ChatMessage.update( - { - _id: message._id, - rid: message.rid, - }, - { - $set: { - pinned: false, - }, - }, - ); - }, -}); diff --git a/apps/meteor/app/message-pin/client/views/pinnedMessages.html b/apps/meteor/app/message-pin/client/views/pinnedMessages.html deleted file mode 100644 index 69aa06225a8b..000000000000 --- a/apps/meteor/app/message-pin/client/views/pinnedMessages.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/apps/meteor/app/message-pin/client/views/pinnedMessages.js b/apps/meteor/app/message-pin/client/views/pinnedMessages.js deleted file mode 100644 index f5843f32d42e..000000000000 --- a/apps/meteor/app/message-pin/client/views/pinnedMessages.js +++ /dev/null @@ -1,87 +0,0 @@ -import _ from 'underscore'; -import { Mongo } from 'meteor/mongo'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; - -import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; -import { APIClient } from '../../../utils/client'; -import { Messages } from '../../../models/client'; -import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; - -const LIMIT_DEFAULT = 50; - -Template.pinnedMessages.helpers({ - hasMessages() { - return Template.instance().messages.find().count(); - }, - messages() { - const instance = Template.instance(); - return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); - }, - hasMore() { - return Template.instance().hasMore.get(); - }, - messageContext: createMessageContext, -}); - -Template.pinnedMessages.onCreated(function () { - this.pinnedMessages = new ReactiveVar([]); - this.hasMore = new ReactiveVar(true); - this.limit = new ReactiveVar(LIMIT_DEFAULT); - this.rid = this.data.rid; - this.messages = new Mongo.Collection(null); - - this.autorun(() => { - const query = { - t: { $ne: 'rm' }, - _hidden: { $ne: true }, - pinned: true, - rid: this.data.rid, - _updatedAt: { - $gte: new Date(), - }, - }; - - this.cursor && this.cursor.stop(); - - this.limit.set(LIMIT_DEFAULT); - - this.cursor = Messages.find(query).observe({ - added: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - changed: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - removed: ({ _id }) => { - this.messages.remove({ _id }); - }, - }); - }); - - this.autorun(async () => { - const limit = this.limit.get(); - const { messages, total } = await APIClient.get('/v1/chat.getPinnedMessages', { - roomId: this.rid, - count: limit, - }); - - upsertMessageBulk({ msgs: messages }, this.messages); - - this.hasMore.set(total > limit); - }); -}); - -Template.mentionsFlexTab.onDestroyed(function () { - this.cursor.stop(); -}); - -Template.pinnedMessages.events({ - ...getCommonRoomEvents(), - 'scroll .js-list': _.throttle(function (e, instance) { - if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get()) { - return instance.limit.set(instance.limit.get() + 50); - } - }, 200), -}); diff --git a/apps/meteor/app/message-pin/client/views/stylesheets/messagepin.css b/apps/meteor/app/message-pin/client/views/stylesheets/messagepin.css deleted file mode 100644 index c4dc8a9b1cef..000000000000 --- a/apps/meteor/app/message-pin/client/views/stylesheets/messagepin.css +++ /dev/null @@ -1,25 +0,0 @@ -.icon-pin.rotate-45::before { - transform: rotate(45deg); -} - -.pinned-messages-list { - & li.empty { - margin-top: 60px; - - text-align: center; - - color: #7f7f7f; - } - - & .load-more { - text-align: center; - text-transform: lowercase; - - font-style: italic; - line-height: 40px; - - & .load-more-loading { - color: #aaaaaa; - } - } -} diff --git a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js index 3166433bb527..48da295d07bc 100644 --- a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js +++ b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js @@ -67,7 +67,7 @@ Template.snippetedMessages.onCreated(function () { }); }); -Template.mentionsFlexTab.onDestroyed(function () { +Template.snippetedMessages.onDestroyed(function () { this.cursor.stop(); }); diff --git a/apps/meteor/app/message-star/client/actionButton.ts b/apps/meteor/app/message-star/client/actionButton.ts deleted file mode 100644 index 8a37b39b3507..000000000000 --- a/apps/meteor/app/message-star/client/actionButton.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -import { settings } from '../../settings/client'; -import { RoomHistoryManager, MessageAction } from '../../ui-utils/client'; -import { messageArgs } from '../../../client/lib/utils/messageArgs'; -import { Rooms } from '../../models/client'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; - -Meteor.startup(function () { - MessageAction.addButton({ - id: 'star-message', - icon: 'star', - label: 'Star', - context: ['starred', 'message', 'message-mobile', 'threads', 'federated'], - action(_, props) { - const { message = messageArgs(this).msg } = props; - Meteor.call('starMessage', { ...message, starred: true }, function (error: any) { - if (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - }, - condition({ message, subscription, user, room }) { - if (subscription == null && settings.get('Message_AllowStarring')) { - return false; - } - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } - - return !Array.isArray(message.starred) || !message.starred.find((star: any) => star._id === user?._id); - }, - order: 9, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'unstar-message', - icon: 'star', - label: 'Unstar_Message', - context: ['starred', 'message', 'message-mobile', 'threads', 'federated'], - action(_, props) { - const { message = messageArgs(this).msg } = props; - - Meteor.call('starMessage', { ...message, starred: false }, function (error?: any) { - if (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - }, - condition({ message, subscription, user }) { - if (subscription == null && settings.get('Message_AllowStarring')) { - return false; - } - - return Boolean(message.starred?.find((star: any) => star._id === user?._id)); - }, - order: 9, - group: 'menu', - }); - - MessageAction.addButton({ - id: 'jump-to-star-message', - icon: 'jump', - label: 'Jump_to_message', - - context: ['starred', 'threads', 'message-mobile'], - action() { - const { msg: message } = messageArgs(this); - if (window.matchMedia('(max-width: 500px)').matches) { - (Template.instance() as any).tabBar.close(); - } - if (message.tmid) { - return FlowRouter.go( - FlowRouter.getRouteName(), - { - tab: 'thread', - context: message.tmid, - rid: message.rid, - jump: message._id, - name: Rooms.findOne({ _id: message.rid })?.name ?? '', - }, - { - jump: message._id, - }, - ); - } - RoomHistoryManager.getSurroundingMessages(message); - }, - condition({ message, subscription, user }) { - if (subscription == null || !settings.get('Message_AllowStarring')) { - return false; - } - - return Boolean(message.starred?.find((star) => star._id === user?._id)); - }, - order: 100, - group: ['message', 'menu'], - }); - - MessageAction.addButton({ - id: 'permalink-star', - icon: 'permalink', - label: 'Get_link', - // classes: 'clipboard', - context: ['starred', 'threads'], - async action(_, props) { - try { - const { message = messageArgs(this).msg } = props; - const permalink = await MessageAction.getPermaLink(message._id); - navigator.clipboard.writeText(permalink); - dispatchToastMessage({ type: 'success', message: TAPi18n.__('Copied') }); - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - }, - condition({ message, subscription, user }) { - if (subscription == null) { - return false; - } - - return Boolean(message.starred?.find((star) => star._id === user?._id)); - }, - order: 101, - group: 'menu', - }); -}); diff --git a/apps/meteor/app/message-star/client/index.js b/apps/meteor/app/message-star/client/index.js deleted file mode 100644 index 68b7c6434334..000000000000 --- a/apps/meteor/app/message-star/client/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import './actionButton'; -import './starMessage'; -import './tabBar'; -import './views/starredMessages.html'; -import './views/starredMessages'; -import './views/stylesheets/messagestar.css'; diff --git a/apps/meteor/app/message-star/client/starMessage.js b/apps/meteor/app/message-star/client/starMessage.js deleted file mode 100644 index 41e2c7ac7775..000000000000 --- a/apps/meteor/app/message-star/client/starMessage.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { settings } from '../../settings'; -import { ChatMessage, Subscriptions } from '../../models/client'; -import { dispatchToastMessage } from '../../../client/lib/toast'; - -Meteor.methods({ - starMessage(message) { - if (!Meteor.userId()) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); - return false; - } - if (Subscriptions.findOne({ rid: message.rid }) == null) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); - return false; - } - if (!ChatMessage.findOneByRoomIdAndMessageId(message.rid, message._id)) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); - return false; - } - if (!settings.get('Message_AllowStarring')) { - dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); - return false; - } - if (message.starred) { - dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_starred') }); - } else { - dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_unstarred') }); - } - return ChatMessage.update( - { - _id: message._id, - }, - { - $addToSet: { - starred: !!message.starred, - }, - }, - ); - }, -}); diff --git a/apps/meteor/app/message-star/client/views/starredMessages.html b/apps/meteor/app/message-star/client/views/starredMessages.html deleted file mode 100644 index 1f2e71150085..000000000000 --- a/apps/meteor/app/message-star/client/views/starredMessages.html +++ /dev/null @@ -1,23 +0,0 @@ - diff --git a/apps/meteor/app/message-star/client/views/starredMessages.js b/apps/meteor/app/message-star/client/views/starredMessages.js deleted file mode 100644 index 4c486444036f..000000000000 --- a/apps/meteor/app/message-star/client/views/starredMessages.js +++ /dev/null @@ -1,86 +0,0 @@ -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; -import { Mongo } from 'meteor/mongo'; - -import { createMessageContext } from '../../../ui-utils/client/lib/messageContext'; -import { Messages } from '../../../models/client'; -import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { APIClient } from '../../../utils/client'; -import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; - -const LIMIT_DEFAULT = 50; - -Template.starredMessages.helpers({ - hasMessages() { - return Template.instance().messages.find().count(); - }, - messages() { - const instance = Template.instance(); - return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); - }, - hasMore() { - return Template.instance().hasMore.get(); - }, - messageContext: createMessageContext, -}); - -Template.starredMessages.onCreated(function () { - this.rid = this.data.rid; - this.messages = new Mongo.Collection(null); - this.hasMore = new ReactiveVar(true); - this.limit = new ReactiveVar(LIMIT_DEFAULT); - - this.autorun(() => { - const query = { - '_hidden': { $ne: true }, - 'starred._id': Meteor.userId(), - 'rid': this.rid, - '_updatedAt': { - $gt: new Date(), - }, - }; - - this.cursor && this.cursor.stop(); - - this.limit.set(LIMIT_DEFAULT); - - this.cursor = Messages.find(query).observe({ - added: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - changed: ({ _id, ...message }) => { - this.messages.upsert({ _id }, message); - }, - removed: ({ _id }) => { - this.messages.remove({ _id }); - }, - }); - }); - - this.autorun(async () => { - const limit = this.limit.get(); - const { messages, total } = await APIClient.get('/v1/chat.getStarredMessages', { - roomId: this.rid, - count: limit, - }); - - upsertMessageBulk({ msgs: messages }, this.messages); - - this.hasMore.set(total > limit); - }); -}); - -Template.mentionsFlexTab.onDestroyed(function () { - this.cursor.stop(); -}); - -Template.starredMessages.events({ - ...getCommonRoomEvents(), - 'scroll .js-list': _.throttle(function (e, instance) { - if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) { - return instance.limit.set(instance.limit.get() + 50); - } - }, 200), -}); diff --git a/apps/meteor/app/message-star/client/views/stylesheets/messagestar.css b/apps/meteor/app/message-star/client/views/stylesheets/messagestar.css deleted file mode 100644 index 12ac25a3efb3..000000000000 --- a/apps/meteor/app/message-star/client/views/stylesheets/messagestar.css +++ /dev/null @@ -1,21 +0,0 @@ -.starred-messages-list { - & li.empty { - margin-top: 60px; - - text-align: center; - - color: #7f7f7f; - } - - & .load-more { - text-align: center; - text-transform: lowercase; - - font-style: italic; - line-height: 40px; - - & .load-more-loading { - color: #aaaaaa; - } - } -} diff --git a/apps/meteor/app/models/client/models/ChatMessage.ts b/apps/meteor/app/models/client/models/ChatMessage.ts index a3d0356ef8dd..fbe1d3bd376d 100644 --- a/apps/meteor/app/models/client/models/ChatMessage.ts +++ b/apps/meteor/app/models/client/models/ChatMessage.ts @@ -1,7 +1,12 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Mongo } from 'meteor/mongo'; -class ChatMessageCollection extends Mongo.Collection { +import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; + +class ChatMessageCollection + extends Mongo.Collection + implements MinimongoCollection +{ constructor() { super(null); } @@ -14,14 +19,13 @@ class ChatMessageCollection extends Mongo.Collection & { ignored?: boolean }, - IMessage & { ignored?: boolean } -> & { - direct: Mongo.Collection, IMessage>; + public declare _collection: MinimongoCollection['_collection']; + + public declare direct: MinimongoCollection['direct']; + + public declare queries: MinimongoCollection['queries']; +} - queries: unknown[]; -}; +/** @deprecated */ +export const ChatMessage = new ChatMessageCollection(); diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index 84100252b47c..b563e37757a8 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -11,18 +11,7 @@ import { call } from '../../../../client/lib/utils/call'; import { CachedCollectionManager } from './CachedCollectionManager'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; import { isTruthy } from '../../../../lib/isTruthy'; - -type Collection = Mongo.Collection & { - _collection: Mongo.Collection & { - queries: Record; - _docs: { - _idStringify: (id: string) => string; - _map: Map; - }; - _recomputeResults: (query: unknown) => void; - }; - direct: Mongo.Collection; -}; +import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; type EventType = Extract; type Name = 'rooms' | 'subscriptions' | 'permissions' | 'public-settings' | 'private-settings'; @@ -47,7 +36,7 @@ const hasUnserializedUpdatedAt = (record: T): record is T & { _updatedAt: Con export class CachedCollection extends Emitter<{ changed: T; removed: T }> { private static MAX_CACHE_TIME = 60 * 60 * 24 * 30; - public collection: Collection; + public collection: MinimongoCollection; public ready = new ReactiveVar(false); @@ -68,7 +57,7 @@ export class CachedCollection extends Emitter<{ changed: T; re constructor({ name, eventType = 'onUser', userRelated = true }: { name: Name; eventType?: EventType; userRelated?: boolean }) { super(); - this.collection = new Mongo.Collection(null) as Collection; + this.collection = new Mongo.Collection(null) as MinimongoCollection; this.name = name; this.eventType = eventType; diff --git a/apps/meteor/app/ui-login/username/username.ts b/apps/meteor/app/ui-login/username/username.ts index 6180c2cbbdbc..157a208dca96 100644 --- a/apps/meteor/app/ui-login/username/username.ts +++ b/apps/meteor/app/ui-login/username/username.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import type { Blaze } from 'meteor/blaze'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { settings } from '../../settings/client'; @@ -11,28 +10,7 @@ import { callbacks } from '../../../lib/callbacks'; import { dispatchToastMessage } from '../../../client/lib/toast'; import './username.html'; -type UsernameTemplateInstance = Blaze.TemplateInstance> & { - customFields: ReactiveVar | null>; - username: ReactiveVar<{ - ready: boolean; - username: string; - empty?: boolean; - error?: boolean; - invalid?: boolean; - escaped?: string; - blocked?: boolean; - unavailable?: boolean; - }>; - validate: () => unknown; -}; -Template.username.onCreated(function (this: UsernameTemplateInstance) { +Template.username.onCreated(function () { this.customFields = new ReactiveVar(null); this.username = new ReactiveVar({ ready: false, @@ -118,7 +96,7 @@ Template.username.onCreated(function (this: UsernameTemplateInstance) { Template.username.helpers({ username() { - return (Template.instance() as UsernameTemplateInstance).username.get(); + return Template.instance<'username'>().username.get(); }, backgroundUrl() { @@ -143,7 +121,7 @@ Template.username.events({ 'reset #login-card'() { Meteor.logout(); }, - 'submit #login-card'(event: JQuery.SubmitEvent, instance: UsernameTemplateInstance) { + 'submit #login-card'(event: JQuery.SubmitEvent, instance) { event.preventDefault(); const formData = instance.validate(); diff --git a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts index d7f3f4d543c2..6b22ca91a94d 100644 --- a/apps/meteor/app/ui-message/client/messageBox/messageBox.ts +++ b/apps/meteor/app/ui-message/client/messageBox/messageBox.ts @@ -1,53 +1,5 @@ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import type { Blaze } from 'meteor/blaze'; -import type { ContextType } from 'react'; - -import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; import './messageBoxActions'; -export type MessageBoxTemplateInstance = Blaze.TemplateInstance<{ - rid: IRoom['_id']; - tmid?: IMessage['_id']; - readOnly: boolean; - onSend?: (params: { value: string; tshow?: boolean }) => Promise; - onJoin?: () => Promise; - onResize?: () => void; - onTyping?: () => void; - onEscape?: () => void; - onNavigateToPreviousMessage?: () => void; - onNavigateToNextMessage?: () => void; - onUploadFiles?: (files: readonly File[]) => void; - tshow?: IMessage['tshow']; - subscription?: ISubscription; - showFormattingTips: boolean; - isEmbedded?: boolean; - chatContext: ContextType; -}> & { - state: ReactiveDict<{ - mustJoinWithCode?: boolean; - isBlockedOrBlocker?: boolean; - room?: boolean; - }>; - popupConfig: ReactiveVar<{ - rid: string; - tmid?: string; - getInput: () => HTMLTextAreaElement; - } | null>; - replyMessageData: ReactiveVar; - isMicrophoneDenied: ReactiveVar; - isSendIconVisible: ReactiveVar; - input: HTMLTextAreaElement; - source?: HTMLTextAreaElement; - autogrow: { - update: () => void; - destroy: () => void; - } | null; - set: (value: string) => void; - insertNewLine: () => void; - send: (event: Event) => void; - sendIconDisabled: ReactiveVar; -}; - const lastFocusedInput: HTMLTextAreaElement | undefined = undefined; export const refocusComposer = () => { diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index db0bdffb6dfb..960d886e5fa2 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -18,6 +18,7 @@ import { clearHighlightMessage, } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; import { normalizeThreadMessage } from '../../../../client/lib/normalizeThreadMessage'; +import type { MinimongoCollection } from '../../../../client/definitions/MinimongoCollection'; export async function upsertMessage( { @@ -29,7 +30,7 @@ export async function upsertMessage( subscription?: ISubscription; uid?: IUser['_id']; }, - { direct } = ChatMessage, + { direct }: MinimongoCollection = ChatMessage, ) { const userId = msg.u?._id; @@ -58,10 +59,13 @@ export async function upsertMessage( ); } - return direct.upsert({ _id }, messageToUpsert); + return direct.upsert({ _id }, { $set: messageToUpsert }); } -export function upsertMessageBulk({ msgs, subscription }: { msgs: IMessage[]; subscription?: ISubscription }, collection = ChatMessage) { +export function upsertMessageBulk( + { msgs, subscription }: { msgs: IMessage[]; subscription?: ISubscription }, + collection: MinimongoCollection = ChatMessage, +) { const uid = Tracker.nonreactive(() => Meteor.userId()) ?? undefined; const { queries } = collection; collection.queries = []; diff --git a/apps/meteor/app/ui-utils/lib/MessageTypes.ts b/apps/meteor/app/ui-utils/lib/MessageTypes.ts index 514b36798c25..a4f77d10cbf7 100644 --- a/apps/meteor/app/ui-utils/lib/MessageTypes.ts +++ b/apps/meteor/app/ui-utils/lib/MessageTypes.ts @@ -11,7 +11,8 @@ export type MessageType = { message: TranslationKey; data?: (message: IMessage) => Record; }; -class MessageTypesClass { + +class MessageTypes { private types = new Map(); registerType(options: MessageType): MessageType { @@ -34,4 +35,7 @@ class MessageTypesClass { return Boolean(type?.system); } } -export const MessageTypes = new MessageTypesClass(); + +const instance = new MessageTypes(); + +export { instance as MessageTypes }; diff --git a/apps/meteor/app/ui/client/views/app/roomSearch.ts b/apps/meteor/app/ui/client/views/app/roomSearch.ts index 88d13d4e7f83..f9fd91e0d73b 100644 --- a/apps/meteor/app/ui/client/views/app/roomSearch.ts +++ b/apps/meteor/app/ui/client/views/app/roomSearch.ts @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import { roomCoordinator } from '../../../../../client/lib/rooms/roomCoordinator'; Template.roomSearch.helpers({ - roomIcon() { + roomIcon(this: any) { if (this.type === 'u') { return 'icon-at'; } @@ -11,7 +11,7 @@ Template.roomSearch.helpers({ return roomCoordinator.getIcon(this); } }, - userStatus() { + userStatus(this: any) { if (this.type === 'u') { return `status-${this.status}`; } diff --git a/apps/meteor/client/components/message/ToolboxHolder.tsx b/apps/meteor/client/components/message/ToolboxHolder.tsx index 97e3f1acb4aa..295fe798fc3b 100644 --- a/apps/meteor/client/components/message/ToolboxHolder.tsx +++ b/apps/meteor/client/components/message/ToolboxHolder.tsx @@ -1,14 +1,15 @@ -import type { IMessage, ToolboxMessageType } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; import { MessageToolboxWrapper } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React, { memo, useRef } from 'react'; +import type { MessageActionContext } from '../../../app/ui-utils/client/lib/MessageAction'; import { useIsVisible } from '../../views/room/hooks/useIsVisible'; import Toolbox from './toolbox/Toolbox'; type ToolboxHolderProps = { message: IMessage; - context?: ToolboxMessageType; + context?: MessageActionContext; }; export const ToolboxHolder = ({ message, context }: ToolboxHolderProps): ReactElement => { diff --git a/apps/meteor/client/components/message/hooks/useMessageNormalization.ts b/apps/meteor/client/components/message/hooks/useMessageNormalization.ts new file mode 100644 index 000000000000..a4ed5d6c52b3 --- /dev/null +++ b/apps/meteor/client/components/message/hooks/useMessageNormalization.ts @@ -0,0 +1,33 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import type { MessageWithMdEnforced } from '../../../lib/parseMessageTextToAstMarkdown'; +import { parseMessageTextToAstMarkdown, removePossibleNullMessageValues } from '../../../lib/parseMessageTextToAstMarkdown'; +import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; +import { useKatex } from '../../../views/room/MessageList/hooks/useKatex'; +import { useRoomSubscription } from '../../../views/room/contexts/RoomContext'; + +export const useMessageNormalization = (): ((message: TMessage) => MessageWithMdEnforced) => { + const { katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled } = useKatex(); + + const subscription = useRoomSubscription(); + const autoTranslateOptions = useAutoTranslate(subscription); + const showColors = useSetting('HexColorPreview_Enabled'); + + return useMemo(() => { + const parseOptions = { + colors: showColors, + emoticons: true, + ...(katexEnabled && { + katex: { + dollarSyntax: katexDollarSyntaxEnabled, + parenthesisSyntax: katexParenthesisSyntaxEnabled, + }, + }), + }; + + return (message: TTMessage): MessageWithMdEnforced => + parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); + }, [showColors, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, autoTranslateOptions]); +}; diff --git a/apps/meteor/client/components/message/toolbox/DesktopToolboxDropdown.tsx b/apps/meteor/client/components/message/toolbox/DesktopToolboxDropdown.tsx index 3e8392fc6c79..3d19f352bb28 100644 --- a/apps/meteor/client/components/message/toolbox/DesktopToolboxDropdown.tsx +++ b/apps/meteor/client/components/message/toolbox/DesktopToolboxDropdown.tsx @@ -1,29 +1,54 @@ import { Tile } from '@rocket.chat/fuselage'; import { useMergedRefs, usePosition } from '@rocket.chat/fuselage-hooks'; import type { ReactNode, Ref, RefObject } from 'react'; -import React, { useRef, forwardRef } from 'react'; +import React, { useMemo, useRef, forwardRef } from 'react'; + +const getDropdownContainer = (descendant: HTMLElement | null) => { + for (let element = descendant ?? document.body; element !== document.body; element = element.parentElement ?? document.body) { + if ( + getComputedStyle(element).transform !== 'none' || + getComputedStyle(element).position === 'fixed' || + getComputedStyle(element).willChange === 'transform' + ) { + return element; + } + } + + return document.body; +}; + +const useDropdownPosition = (reference: RefObject, target: RefObject) => { + const innerContainer = getDropdownContainer(reference.current); + const boundingRect = innerContainer.getBoundingClientRect(); + + const { style } = usePosition(reference, target, { + watch: true, + placement: 'bottom-end', + container: innerContainer, + }); + + const left = `${parseFloat(style.left) - boundingRect.left}px`; + const top = `${parseFloat(style.top) - boundingRect.top}px`; + + return useMemo(() => ({ ...style, left, top }), [style, left, top]); +}; type DesktopToolboxDropdownProps = { children: ReactNode; reference: RefObject; - container: Element; }; const DesktopToolboxDropdown = forwardRef(function ToolboxDropdownDesktop( - { reference, container, children, ...props }: DesktopToolboxDropdownProps, + { reference, children }: DesktopToolboxDropdownProps, ref: Ref, ) { const targetRef = useRef(null); const mergedRef = useMergedRefs(ref, targetRef); - const { style } = usePosition(reference, targetRef, { - watch: true, - placement: 'bottom-end', - container, - }); + const style = useDropdownPosition(reference, targetRef); return ( - + {children} ); diff --git a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx index 3772ea5351a7..f438e91d3d8b 100644 --- a/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageActionMenu.tsx @@ -34,8 +34,6 @@ export const MessageActionMenu = ({ options, ...props }: MessageActionMenuProps) [key: string]: MessageActionConfigOption[]; }; - const messagesContainer = document.querySelector('.messages-container') || document.body; - return ( {visible && ( - + {Object.entries(groupOptions).map(([, options], index, arr) => ( {options.map((option) => ( diff --git a/apps/meteor/client/components/message/toolbox/Toolbox.tsx b/apps/meteor/client/components/message/toolbox/Toolbox.tsx index 59910fbc2f9c..c4787818c225 100644 --- a/apps/meteor/client/components/message/toolbox/Toolbox.tsx +++ b/apps/meteor/client/components/message/toolbox/Toolbox.tsx @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, ITranslatedMessage, ToolboxMessageType } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ITranslatedMessage } from '@rocket.chat/core-typings'; import { isThreadMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { MessageToolbox, MessageToolboxItem } from '@rocket.chat/fuselage'; import { useUser, useSettings, useTranslation } from '@rocket.chat/ui-contexts'; @@ -15,22 +15,29 @@ import { useRoom, useRoomSubscription } from '../../../views/room/contexts/RoomC import { useToolboxContext } from '../../../views/room/contexts/ToolboxContext'; import MessageActionMenu from './MessageActionMenu'; -const getMessageContext = (message: IMessage, room: IRoom, context?: ToolboxMessageType): MessageActionContext => { +const getMessageContext = (message: IMessage, room: IRoom, context?: MessageActionContext): MessageActionContext => { + if (context) { + return context; + } + if (message.t === 'videoconf') { return 'videoconf'; } + if (isRoomFederated(room)) { return 'federated'; } - if (isThreadMessage(message) || context === 'thread') { + + if (isThreadMessage(message)) { return 'threads'; } + return 'message'; }; type ToolboxProps = { message: IMessage & Partial; - messageContext?: ToolboxMessageType; + messageContext?: MessageActionContext; }; const Toolbox = ({ message, messageContext }: ToolboxProps): ReactElement | null => { diff --git a/apps/meteor/client/components/message/toolbox/ToolboxDropdown.tsx b/apps/meteor/client/components/message/toolbox/ToolboxDropdown.tsx index 0e532be592fa..5be1aadf27e1 100644 --- a/apps/meteor/client/components/message/toolbox/ToolboxDropdown.tsx +++ b/apps/meteor/client/components/message/toolbox/ToolboxDropdown.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactNode, ReactElement } from 'react'; @@ -7,20 +6,15 @@ import React, { useRef } from 'react'; import DesktopToolboxDropdown from './DesktopToolboxDropdown'; import MobileToolboxDropdown from './MobileToolboxDropdown'; -const style = css` - top: 0; - bottom: 0; - left: 0; - right: 0; -`; - type ToolboxDropdownProps = { children: ReactNode; reference: React.RefObject; - container: Element; }; -const ToolboxDropdown = ({ children, reference, container, ...props }: ToolboxDropdownProps): ReactElement => { +const ToolboxDropdown = ({ + children, + reference, +}: ToolboxDropdownProps): ReactElement => { const { isMobile } = useLayout(); const target = useRef(null); @@ -28,8 +22,8 @@ const ToolboxDropdown = ({ children, reference, container return ( <> - - + + {children} diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 1f7868d9c6ad..783479f7a1a4 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -1,11 +1,12 @@ +import type { IMessage } from '@rocket.chat/core-typings'; import { Message, MessageLeftContainer, MessageContainer, CheckBox } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; import { useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; +import type { MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; import { useUserCard } from '../../../hooks/useUserCard'; -import type { MessageWithMdEnforced } from '../../../lib/parseMessageTextToAstMarkdown'; import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext'; import { useIsSelecting, @@ -21,14 +22,15 @@ import ToolboxHolder from '../ToolboxHolder'; import RoomMessageContent from './room/RoomMessageContent'; type RoomMessageProps = { - message: MessageWithMdEnforced; + message: IMessage; sequential: boolean; unread: boolean; mention: boolean; all: boolean; + context?: MessageActionContext; }; -const RoomMessage = ({ message, sequential, all, mention, unread }: RoomMessageProps): ReactElement => { +const RoomMessage = ({ message, sequential, all, mention, unread, context }: RoomMessageProps): ReactElement => { const uid = useUserId(); const editing = useIsMessageHighlight(message._id); const [ignored, toggleIgnoring] = useToggle((message as { ignored?: boolean }).ignored ?? false); @@ -79,7 +81,7 @@ const RoomMessage = ({ message, sequential, all, mention, unread }: RoomMessageP )} - {!message.private && } + {!message.private && } ); }; diff --git a/apps/meteor/client/components/message/variants/ThreadMessage.tsx b/apps/meteor/client/components/message/variants/ThreadMessage.tsx index 6b79aa63b079..4f4dfdfec8b4 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessage.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessage.tsx @@ -1,20 +1,17 @@ import type { IThreadMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Message, MessageLeftContainer, MessageContainer } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; -import { useUserId, useUserSubscription } from '@rocket.chat/ui-contexts'; +import { useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useMemo, memo } from 'react'; +import React, { memo } from 'react'; import { useUserCard } from '../../../hooks/useUserCard'; -import { parseMessageTextToAstMarkdown, removePossibleNullMessageValues } from '../../../lib/parseMessageTextToAstMarkdown'; import { useIsMessageHighlight } from '../../../views/room/MessageList/contexts/MessageHighlightContext'; -import { useAutoTranslate } from '../../../views/room/MessageList/hooks/useAutoTranslate'; import UserAvatar from '../../avatar/UserAvatar'; import IgnoredContent from '../IgnoredContent'; import MessageHeader from '../MessageHeader'; import StatusIndicators from '../StatusIndicators'; import ToolboxHolder from '../ToolboxHolder'; -import { useMessageListContext } from '../list/MessageListContext'; import ThreadMessageContent from './thread/ThreadMessageContent'; type ThreadMessageProps = { @@ -29,27 +26,6 @@ const ThreadMessage = ({ message, sequential, unread }: ThreadMessageProps): Rea const [ignored, toggleIgnoring] = useToggle((message as { ignored?: boolean }).ignored); const { open: openUserCard } = useUserCard(); - const { katex, showColors } = useMessageListContext(); - const subscription = useUserSubscription(message.rid); - const autoTranslateOptions = useAutoTranslate(subscription); - - const normalizeMessage = useMemo(() => { - const parseOptions = { - colors: showColors, - emoticons: true, - ...(Boolean(katex) && { - katex: { - dollarSyntax: katex?.dollarSyntaxEnabled, - parenthesisSyntax: katex?.parenthesisSyntaxEnabled, - }, - }), - }; - return (message: TMessage) => - parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); - }, [katex, showColors, autoTranslateOptions]); - - const normalizedMessage = useMemo(() => normalizeMessage(message), [message, normalizeMessage]); - return ( {!sequential && } - {ignored ? : } + {ignored ? : } - {!message.private && } + {!message.private && } ); }; diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index c8b05a5ca06b..15de13bc5ca0 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -3,10 +3,9 @@ import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage } from '@rocket import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { memo } from 'react'; +import React, { useMemo, memo } from 'react'; import { useUserData } from '../../../../hooks/useUserData'; -import type { MessageWithMdEnforced } from '../../../../lib/parseMessageTextToAstMarkdown'; import type { UserPresence } from '../../../../lib/presence'; import { useRoomSubscription } from '../../../../views/room/contexts/RoomContext'; import MessageContentBody from '../../MessageContentBody'; @@ -20,10 +19,11 @@ import Reactions from '../../content/Reactions'; import ThreadMetrics from '../../content/ThreadMetrics'; import UiKitSurface from '../../content/UiKitSurface'; import UrlPreviews from '../../content/UrlPreviews'; +import { useMessageNormalization } from '../../hooks/useMessageNormalization'; import { useOembedLayout } from '../../hooks/useOembedLayout'; type RoomMessageContentProps = { - message: MessageWithMdEnforced; + message: IMessage; unread: boolean; mention: boolean; all: boolean; @@ -39,27 +39,34 @@ const RoomMessageContent = ({ message, unread, all, mention }: RoomMessageConten const t = useTranslation(); + const normalizeMessage = useMessageNormalization(); + const normalizedMessage = useMemo(() => normalizeMessage(message), [message, normalizeMessage]); + return ( <> - {!message.blocks?.length && !!message.md?.length && ( + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> - {(!encrypted || message.e2e === 'done') && ( - + {(!encrypted || normalizedMessage.e2e === 'done') && ( + )} - {encrypted && message.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} + {encrypted && normalizedMessage.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} )} - {message.blocks && } + {normalizedMessage.blocks && ( + + )} - {!!message?.attachments?.length && } + {!!normalizedMessage?.attachments?.length && ( + + )} - {oembedEnabled && !!message.urls?.length && } + {oembedEnabled && !!normalizedMessage.urls?.length && } - {message.actionLinks?.length && ( + {normalizedMessage.actionLinks?.length && ( ({ + message={normalizedMessage} + actions={normalizedMessage.actionLinks.map(({ method_id: methodId, i18nLabel, ...action }) => ({ methodId, i18nLabel: i18nLabel as TranslationKey, ...action, @@ -67,31 +74,38 @@ const RoomMessageContent = ({ message, unread, all, mention }: RoomMessageConten /> )} - {message.reactions && Object.keys(message.reactions).length && } + {normalizedMessage.reactions && Object.keys(normalizedMessage.reactions).length && } - {isThreadMainMessage(message) && ( + {isThreadMainMessage(normalizedMessage) && ( -1)} - mid={message._id} - rid={message.rid} - lm={message.tlm} + counter={normalizedMessage.tcount} + following={Boolean(uid && normalizedMessage?.replies?.indexOf(uid) > -1)} + mid={normalizedMessage._id} + rid={normalizedMessage.rid} + lm={normalizedMessage.tlm} unread={unread} mention={mention} all={all} - participants={message?.replies?.length} + participants={normalizedMessage?.replies?.length} /> )} - {isDiscussionMessage(message) && } + {isDiscussionMessage(normalizedMessage) && ( + + )} - {message.location && } + {normalizedMessage.location && } - {broadcast && !!messageUser.username && message.u._id !== uid && ( - + {broadcast && !!messageUser.username && normalizedMessage.u._id !== uid && ( + )} - {readReceiptEnabled && } + {readReceiptEnabled && } ); }; diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index d484c8cebe87..15a2e883be6f 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -3,10 +3,9 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useUserId, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { memo } from 'react'; +import React, { useMemo, memo } from 'react'; import { useUserData } from '../../../../hooks/useUserData'; -import type { MessageWithMdEnforced } from '../../../../lib/parseMessageTextToAstMarkdown'; import type { UserPresence } from '../../../../lib/presence'; import { useRoomSubscription } from '../../../../views/room/contexts/RoomContext'; import MessageContentBody from '../../MessageContentBody'; @@ -18,10 +17,11 @@ import MessageActions from '../../content/MessageActions'; import Reactions from '../../content/Reactions'; import UiKitSurface from '../../content/UiKitSurface'; import UrlPreviews from '../../content/UrlPreviews'; +import { useMessageNormalization } from '../../hooks/useMessageNormalization'; import { useOembedLayout } from '../../hooks/useOembedLayout'; type ThreadMessageContentProps = { - message: MessageWithMdEnforced; + message: IThreadMessage | IThreadMainMessage; }; const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElement => { @@ -34,27 +34,32 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const t = useTranslation(); + const normalizeMessage = useMessageNormalization(); + const normalizedMessage = useMemo(() => normalizeMessage(message), [message, normalizeMessage]); + return ( <> - {!message.blocks?.length && !!message.md?.length && ( + {!normalizedMessage.blocks?.length && !!normalizedMessage.md?.length && ( <> - {(!encrypted || message.e2e === 'done') && ( - + {(!encrypted || normalizedMessage.e2e === 'done') && ( + )} - {encrypted && message.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} + {encrypted && normalizedMessage.e2e === 'pending' && t('E2E_message_encrypted_placeholder')} )} - {message.blocks && } + {normalizedMessage.blocks && ( + + )} - {message.attachments && } + {normalizedMessage.attachments && } - {oembedEnabled && !!message.urls?.length && } + {oembedEnabled && !!normalizedMessage.urls?.length && } - {message.actionLinks?.length && ( + {normalizedMessage.actionLinks?.length && ( ({ + message={normalizedMessage} + actions={normalizedMessage.actionLinks.map(({ method_id: methodId, i18nLabel, ...action }) => ({ methodId, i18nLabel: i18nLabel as TranslationKey, ...action, @@ -62,15 +67,15 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem /> )} - {message.reactions && Object.keys(message.reactions).length && } + {normalizedMessage.reactions && Object.keys(normalizedMessage.reactions).length && } - {message.location && } + {normalizedMessage.location && } - {broadcast && !!messageUser.username && message.u._id !== uid && ( - + {broadcast && !!messageUser.username && normalizedMessage.u._id !== uid && ( + )} - {readReceiptEnabled && } + {readReceiptEnabled && } ); }; diff --git a/apps/meteor/client/definitions/MinimongoCollection.ts b/apps/meteor/client/definitions/MinimongoCollection.ts new file mode 100644 index 000000000000..1c3b846dff59 --- /dev/null +++ b/apps/meteor/client/definitions/MinimongoCollection.ts @@ -0,0 +1,14 @@ +import type { Mongo } from 'meteor/mongo'; + +export type MinimongoCollection = Mongo.Collection & { + _collection: Mongo.Collection & { + queries: Record; + _docs: { + _idStringify: (id: string) => string; + _map: Map; + }; + _recomputeResults: (query: unknown) => void; + }; + direct: Mongo.Collection; + queries: unknown[]; +}; diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index d0d15739dcfe..3dc430e1eda6 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -27,12 +27,9 @@ import '../app/lib/client'; import '../app/livestream/client'; import '../app/logger/client'; import '../app/markdown/client'; -import '../app/mentions-flextab/client'; import '../app/message-attachments/client'; import '../app/message-mark-as-unread/client'; -import '../app/message-pin/client'; import '../app/message-snippet/client'; -import '../app/message-star/client'; import '../app/nextcloud/client'; import '../app/oauth2-server-config/client'; import '../app/oembed/client'; diff --git a/apps/meteor/client/lib/utils/jumpToMessage.ts b/apps/meteor/client/lib/utils/jumpToMessage.ts new file mode 100644 index 000000000000..4ade65112439 --- /dev/null +++ b/apps/meteor/client/lib/utils/jumpToMessage.ts @@ -0,0 +1,28 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { FlowRouter } from 'meteor/kadira:flow-router'; + +import { ChatRoom } from '../../../app/models/client'; +import { RoomHistoryManager } from '../../../app/ui-utils/client'; + +export const jumpToMessage = (message: IMessage) => { + if (matchMedia('(max-width: 500px)').matches) { + (Template.instance() as any).tabBar.close(); + } + + if (message.tmid) { + return FlowRouter.go( + FlowRouter.getRouteName(), + { + tab: 'thread', + context: message.tmid, + rid: message.rid, + jump: message._id, + name: ChatRoom.findOne({ _id: message.rid })?.name ?? '', + }, + { + jump: message._id, + }, + ); + } + return RoomHistoryManager.getSurroundingMessages(message); +}; diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index 80be3c1a9f14..36f0a29eabc3 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -10,11 +10,7 @@ import './importPackages'; import '../ee/client'; import './templateHelpers'; -import './methods/hideRoom'; -import './methods/openRoom'; -import './methods/setUserActiveStatus'; -import './methods/toggleFavorite'; -import './methods/updateMessage'; +import './methods'; import './startup'; import './views/admin'; import './views/account'; diff --git a/apps/meteor/client/methods/index.ts b/apps/meteor/client/methods/index.ts new file mode 100644 index 000000000000..990a3a20f24c --- /dev/null +++ b/apps/meteor/client/methods/index.ts @@ -0,0 +1,8 @@ +import './hideRoom'; +import './openRoom'; +import './pinMessage'; +import './setUserActiveStatus'; +import './starMessage'; +import './toggleFavorite'; +import './unpinMessage'; +import './updateMessage'; diff --git a/apps/meteor/client/methods/pinMessage.ts b/apps/meteor/client/methods/pinMessage.ts new file mode 100644 index 000000000000..6b1c50a7b8b7 --- /dev/null +++ b/apps/meteor/client/methods/pinMessage.ts @@ -0,0 +1,40 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { ChatMessage, ChatSubscription } from '../../app/models/client'; +import { settings } from '../../app/settings/client'; +import { t } from '../../app/utils/client'; +import { dispatchToastMessage } from '../lib/toast'; + +Meteor.methods({ + pinMessage(message: IMessage) { + if (!Meteor.userId()) { + dispatchToastMessage({ type: 'error', message: t('error-not-authorized') }); + return false; + } + if (!settings.get('Message_AllowPinning')) { + dispatchToastMessage({ type: 'error', message: t('pinning-not-allowed') }); + return false; + } + if (!ChatSubscription.findOne({ rid: message.rid })) { + dispatchToastMessage({ type: 'error', message: t('error-pinning-message') }); + return false; + } + if (typeof message._id !== 'string') { + dispatchToastMessage({ type: 'error', message: t('error-pinning-message') }); + return false; + } + dispatchToastMessage({ type: 'success', message: t('Message_has_been_pinned') }); + return ChatMessage.update( + { + _id: message._id, + rid: message.rid, + }, + { + $set: { + pinned: true, + }, + }, + ); + }, +}); diff --git a/apps/meteor/client/methods/starMessage.ts b/apps/meteor/client/methods/starMessage.ts new file mode 100644 index 000000000000..5eeebf7700b4 --- /dev/null +++ b/apps/meteor/client/methods/starMessage.ts @@ -0,0 +1,57 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { ChatMessage, ChatSubscription } from '../../app/models/client'; +import { settings } from '../../app/settings/client'; +import { t } from '../../app/utils/client'; +import { dispatchToastMessage } from '../lib/toast'; + +Meteor.methods({ + starMessage(message: Omit & { starred: boolean }) { + const uid = Meteor.userId(); + + if (!uid) { + dispatchToastMessage({ type: 'error', message: t('error-starring-message') }); + return false; + } + + if (!ChatSubscription.findOne({ rid: message.rid })) { + dispatchToastMessage({ type: 'error', message: t('error-starring-message') }); + return false; + } + + if (!ChatMessage.findOneByRoomIdAndMessageId(message.rid, message._id)) { + dispatchToastMessage({ type: 'error', message: t('error-starring-message') }); + return false; + } + + if (!settings.get('Message_AllowStarring')) { + dispatchToastMessage({ type: 'error', message: t('error-starring-message') }); + return false; + } + + if (message.starred) { + ChatMessage.update( + { _id: message._id }, + { + $addToSet: { + starred: { _id: uid }, + }, + }, + ); + + dispatchToastMessage({ type: 'success', message: t('Message_has_been_starred') }); + } else { + ChatMessage.update( + { _id: message._id }, + { + $pull: { + starred: { _id: uid }, + }, + }, + ); + + dispatchToastMessage({ type: 'success', message: t('Message_has_been_unstarred') }); + } + }, +}); diff --git a/apps/meteor/client/methods/unpinMessage.ts b/apps/meteor/client/methods/unpinMessage.ts new file mode 100644 index 000000000000..44ad40765c74 --- /dev/null +++ b/apps/meteor/client/methods/unpinMessage.ts @@ -0,0 +1,40 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { ChatMessage, ChatSubscription } from '../../app/models/client'; +import { settings } from '../../app/settings/client'; +import { t } from '../../app/utils/client'; +import { dispatchToastMessage } from '../lib/toast'; + +Meteor.methods({ + unpinMessage(message: IMessage) { + if (!Meteor.userId()) { + dispatchToastMessage({ type: 'error', message: t('error-not-authorized') }); + return false; + } + if (!settings.get('Message_AllowPinning')) { + dispatchToastMessage({ type: 'error', message: t('unpinning-not-allowed') }); + return false; + } + if (!ChatSubscription.findOne({ rid: message.rid })) { + dispatchToastMessage({ type: 'error', message: t('error-unpinning-message') }); + return false; + } + if (typeof message._id !== 'string') { + dispatchToastMessage({ type: 'error', message: t('error-unpinning-message') }); + return false; + } + dispatchToastMessage({ type: 'success', message: t('Message_has_been_unpinned') }); + return ChatMessage.update( + { + _id: message._id, + rid: message.rid, + }, + { + $set: { + pinned: false, + }, + }, + ); + }, +}); diff --git a/apps/meteor/client/polyfills/cssVars.ts b/apps/meteor/client/polyfills/cssVars.ts index a8dd9bdcbbdf..8b0b88c60125 100644 --- a/apps/meteor/client/polyfills/cssVars.ts +++ b/apps/meteor/client/polyfills/cssVars.ts @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { withDebouncing } from '../../lib/utils/highOrderFunctions'; type Variables = { [name: string]: (variables: Variables) => string; @@ -27,7 +27,7 @@ const replaceReferences = (code: string, variables: Variables): string => let cssVariablesElement: HTMLElement; const originalCodes = new Map(); -const update = _.debounce(() => { +const update = withDebouncing({ wait: 100 })(() => { const declarations = ([] as [string, Variables[keyof Variables]][]).concat( ...Array.from(originalCodes.values(), findDeclarations), findDeclarations(cssVariablesElement.innerHTML), @@ -52,7 +52,7 @@ const update = _.debounce(() => { } sheet.insertRule(`@media all {${patchedCode}}`, 0); }); -}, 100); +}); const findAndPatchFromLinkElements = (): void => { Array.from(document.querySelectorAll('link[type="text/css"].__meteor-css__')).forEach(async (linkElement) => { diff --git a/apps/meteor/client/startup/actionButtons/index.ts b/apps/meteor/client/startup/actionButtons/index.ts new file mode 100644 index 000000000000..b238cb531ecd --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/index.ts @@ -0,0 +1,10 @@ +import './jumpToMessage'; +import './jumpToPinMessage'; +import './jumpToStarMessage'; +import './permalinkPinned'; +import './permalinkStar'; +import './pinMessage'; +import './readReceipt'; +import './starMessage'; +import './unpinMessage'; +import './unstarMessage'; diff --git a/apps/meteor/client/startup/actionButtons/jumpToMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToMessage.ts new file mode 100644 index 000000000000..f9446a3424e6 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/jumpToMessage.ts @@ -0,0 +1,20 @@ +import { Meteor } from 'meteor/meteor'; + +import { MessageAction } from '../../../app/ui-utils/client'; +import { jumpToMessage } from '../../lib/utils/jumpToMessage'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'jump-to-message', + icon: 'jump', + label: 'Jump_to_message', + context: ['mentions', 'threads'], + action(_, props) { + const { message = messageArgs(this).msg } = props; + jumpToMessage(message); + }, + order: 100, + group: ['message', 'menu'], + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts new file mode 100644 index 000000000000..72f3bac1ee24 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/jumpToPinMessage.ts @@ -0,0 +1,23 @@ +import { Meteor } from 'meteor/meteor'; + +import { MessageAction } from '../../../app/ui-utils/client'; +import { jumpToMessage } from '../../lib/utils/jumpToMessage'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'jump-to-pin-message', + icon: 'jump', + label: 'Jump_to_message', + context: ['pinned', 'message-mobile', 'direct'], + action(_, props) { + const { message = messageArgs(this).msg } = props; + jumpToMessage(message); + }, + condition({ subscription }) { + return !!subscription; + }, + order: 100, + group: ['message', 'menu'], + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts b/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts new file mode 100644 index 000000000000..b1d644e0e41d --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/jumpToStarMessage.ts @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { jumpToMessage } from '../../lib/utils/jumpToMessage'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'jump-to-star-message', + icon: 'jump', + label: 'Jump_to_message', + context: ['starred', 'threads', 'message-mobile'], + action(_, props) { + const { message = messageArgs(this).msg } = props; + jumpToMessage(message); + }, + condition({ message, subscription, user }) { + if (subscription == null || !settings.get('Message_AllowStarring')) { + return false; + } + + return Boolean(message.starred?.find((star) => star._id === user?._id)); + }, + order: 100, + group: ['message', 'menu'], + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/permalinkPinned.ts b/apps/meteor/client/startup/actionButtons/permalinkPinned.ts new file mode 100644 index 000000000000..f67e884f6462 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/permalinkPinned.ts @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; + +import { MessageAction } from '../../../app/ui-utils/client'; +import { t } from '../../../app/utils/client'; +import { dispatchToastMessage } from '../../lib/toast'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'permalink-pinned', + icon: 'permalink', + label: 'Get_link', + context: ['pinned'], + async action(_, props) { + try { + const { message = messageArgs(this).msg } = props; + const permalink = await MessageAction.getPermaLink(message._id); + navigator.clipboard.writeText(permalink); + dispatchToastMessage({ type: 'success', message: t('Copied') }); + } catch (e) { + dispatchToastMessage({ type: 'error', message: e }); + } + }, + condition({ subscription }) { + return !!subscription; + }, + order: 101, + group: 'menu', + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/permalinkStar.ts b/apps/meteor/client/startup/actionButtons/permalinkStar.ts new file mode 100644 index 000000000000..0acbce7661f4 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/permalinkStar.ts @@ -0,0 +1,35 @@ +import { Meteor } from 'meteor/meteor'; + +import { MessageAction } from '../../../app/ui-utils/client'; +import { t } from '../../../app/utils/client'; +import { dispatchToastMessage } from '../../lib/toast'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'permalink-star', + icon: 'permalink', + label: 'Get_link', + // classes: 'clipboard', + context: ['starred', 'threads'], + async action(_, props) { + try { + const { message = messageArgs(this).msg } = props; + const permalink = await MessageAction.getPermaLink(message._id); + navigator.clipboard.writeText(permalink); + dispatchToastMessage({ type: 'success', message: t('Copied') }); + } catch (e) { + dispatchToastMessage({ type: 'error', message: e }); + } + }, + condition({ message, subscription, user }) { + if (subscription == null) { + return false; + } + + return Boolean(message.starred?.find((star) => star._id === user?._id)); + }, + order: 101, + group: 'menu', + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/pinMessage.ts b/apps/meteor/client/startup/actionButtons/pinMessage.ts new file mode 100644 index 000000000000..70164fcdba70 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/pinMessage.ts @@ -0,0 +1,41 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasAtLeastOnePermission } from '../../../app/authorization/client'; +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { queryClient } from '../../lib/queryClient'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { dispatchToastMessage } from '../../lib/toast'; +import { call } from '../../lib/utils/call'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'pin-message', + icon: 'pin', + label: 'Pin', + context: ['pinned', 'message', 'message-mobile', 'threads', 'direct'], + async action(_, props) { + const { message = messageArgs(this).msg } = props; + message.pinned = true; + try { + await call('pinMessage', message); + queryClient.invalidateQueries(['rooms', message.rid, 'pinned-messages']); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + condition({ message, subscription, room }) { + if (!settings.get('Message_AllowPinning') || message.pinned || !subscription) { + return false; + } + const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); + if (isLivechatRoom) { + return false; + } + return hasAtLeastOnePermission('pin-message', message.rid); + }, + order: 7, + group: 'menu', + }); +}); diff --git a/apps/meteor/client/startup/readReceipt.ts b/apps/meteor/client/startup/actionButtons/readReceipt.ts similarity index 69% rename from apps/meteor/client/startup/readReceipt.ts rename to apps/meteor/client/startup/actionButtons/readReceipt.ts index 306f07082dac..dbc9fc670bc0 100644 --- a/apps/meteor/client/startup/readReceipt.ts +++ b/apps/meteor/client/startup/actionButtons/readReceipt.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { settings } from '../../app/settings/client'; -import { MessageAction } from '../../app/ui-utils/client'; -import { imperativeModal } from '../lib/imperativeModal'; -import { messageArgs } from '../lib/utils/messageArgs'; -import ReadReceiptsModal from '../views/room/modals/ReadReceiptsModal'; +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { imperativeModal } from '../../lib/imperativeModal'; +import { messageArgs } from '../../lib/utils/messageArgs'; +import ReadReceiptsModal from '../../views/room/modals/ReadReceiptsModal'; Meteor.startup(() => { Tracker.autorun(() => { diff --git a/apps/meteor/client/startup/actionButtons/starMessage.ts b/apps/meteor/client/startup/actionButtons/starMessage.ts new file mode 100644 index 000000000000..fd069d75e874 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/starMessage.ts @@ -0,0 +1,41 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { queryClient } from '../../lib/queryClient'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { dispatchToastMessage } from '../../lib/toast'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'star-message', + icon: 'star', + label: 'Star', + context: ['starred', 'message', 'message-mobile', 'threads', 'federated'], + action(_, props) { + const { message = messageArgs(this).msg } = props; + Meteor.call('starMessage', { ...message, starred: true }, (error: any) => { + if (error) { + dispatchToastMessage({ type: 'error', message: error }); + return; + } + + queryClient.invalidateQueries(['rooms', message.rid, 'starred-messages']); + }); + }, + condition({ message, subscription, user, room }) { + if (subscription == null && settings.get('Message_AllowStarring')) { + return false; + } + const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); + if (isLivechatRoom) { + return false; + } + + return !Array.isArray(message.starred) || !message.starred.find((star: any) => star._id === user?._id); + }, + order: 9, + group: 'menu', + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/unpinMessage.ts b/apps/meteor/client/startup/actionButtons/unpinMessage.ts new file mode 100644 index 000000000000..3c7e305dc7e5 --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/unpinMessage.ts @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasAtLeastOnePermission } from '../../../app/authorization/client'; +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { queryClient } from '../../lib/queryClient'; +import { dispatchToastMessage } from '../../lib/toast'; +import { call } from '../../lib/utils/call'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'unpin-message', + icon: 'pin', + label: 'Unpin', + context: ['pinned', 'message', 'message-mobile', 'threads', 'direct'], + async action(_, props) { + const { message = messageArgs(this).msg } = props; + message.pinned = false; + try { + await call('unpinMessage', message); + queryClient.invalidateQueries(['rooms', message.rid, 'pinned-messages']); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + condition({ message, subscription }) { + if (!subscription || !settings.get('Message_AllowPinning') || !message.pinned) { + return false; + } + + return hasAtLeastOnePermission('pin-message', message.rid); + }, + order: 8, + group: 'menu', + }); +}); diff --git a/apps/meteor/client/startup/actionButtons/unstarMessage.ts b/apps/meteor/client/startup/actionButtons/unstarMessage.ts new file mode 100644 index 000000000000..da2a1a9f261b --- /dev/null +++ b/apps/meteor/client/startup/actionButtons/unstarMessage.ts @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../app/settings/client'; +import { MessageAction } from '../../../app/ui-utils/client'; +import { queryClient } from '../../lib/queryClient'; +import { dispatchToastMessage } from '../../lib/toast'; +import { messageArgs } from '../../lib/utils/messageArgs'; + +Meteor.startup(() => { + MessageAction.addButton({ + id: 'unstar-message', + icon: 'star', + label: 'Unstar_Message', + context: ['starred', 'message', 'message-mobile', 'threads', 'federated'], + action(_, props) { + const { message = messageArgs(this).msg } = props; + + Meteor.call('starMessage', { ...message, starred: false }, (error?: any) => { + if (error) { + dispatchToastMessage({ type: 'error', message: error }); + return; + } + + queryClient.invalidateQueries(['rooms', message.rid, 'starred-messages']); + }); + }, + condition({ message, subscription, user }) { + if (subscription == null && settings.get('Message_AllowStarring')) { + return false; + } + + return Boolean(message.starred?.find((star: any) => star._id === user?._id)); + }, + order: 9, + group: 'menu', + }); +}); diff --git a/apps/meteor/client/startup/contextualBar/exportMessages.ts b/apps/meteor/client/startup/contextualBar/exportMessagesTab.ts similarity index 100% rename from apps/meteor/client/startup/contextualBar/exportMessages.ts rename to apps/meteor/client/startup/contextualBar/exportMessagesTab.ts diff --git a/apps/meteor/client/startup/contextualBar/index.ts b/apps/meteor/client/startup/contextualBar/index.ts index 46bf4c2cb7cc..cde839b9be06 100644 --- a/apps/meteor/client/startup/contextualBar/index.ts +++ b/apps/meteor/client/startup/contextualBar/index.ts @@ -1 +1,4 @@ -import './exportMessages'; +import './exportMessagesTab'; +import './mentionsTab'; +import './pinnedMessagesTab'; +import './starredMessagesTab'; diff --git a/apps/meteor/client/startup/contextualBar/mentionsTab.ts b/apps/meteor/client/startup/contextualBar/mentionsTab.ts new file mode 100644 index 000000000000..510a22ecbf99 --- /dev/null +++ b/apps/meteor/client/startup/contextualBar/mentionsTab.ts @@ -0,0 +1,12 @@ +import { lazy } from 'react'; + +import { addAction } from '../../views/room/lib/Toolbox'; + +addAction('mentions', { + groups: ['channel', 'group', 'team'], + id: 'mentions', + title: 'Mentions', + icon: 'at', + template: lazy(() => import('../../views/room/contextualBar/MentionsTab')), + order: 9, +}); diff --git a/apps/meteor/app/message-pin/client/tabBar.ts b/apps/meteor/client/startup/contextualBar/pinnedMessagesTab.ts similarity index 78% rename from apps/meteor/app/message-pin/client/tabBar.ts rename to apps/meteor/client/startup/contextualBar/pinnedMessagesTab.ts index 6065172f7cb6..1488e7d3a9e6 100644 --- a/apps/meteor/app/message-pin/client/tabBar.ts +++ b/apps/meteor/client/startup/contextualBar/pinnedMessagesTab.ts @@ -1,8 +1,8 @@ -import { useMemo } from 'react'; -import { useSetting } from '@rocket.chat/ui-contexts'; import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { lazy, useMemo } from 'react'; -import { addAction } from '../../../client/views/room/lib/Toolbox'; +import { addAction } from '../../views/room/lib/Toolbox'; addAction('pinned-messages', ({ room }) => { const pinningAllowed = useSetting('Message_AllowPinning'); @@ -15,7 +15,7 @@ addAction('pinned-messages', ({ room }) => { id: 'pinned-messages', title: 'Pinned_Messages', icon: 'pin', - template: 'pinnedMessages', + template: lazy(() => import('../../views/room/contextualBar/PinnedMessagesTab')), ...(federated && { 'data-tooltip': 'Pinned_messages_unavailable_for_federation', 'disabled': true, diff --git a/apps/meteor/app/message-star/client/tabBar.ts b/apps/meteor/client/startup/contextualBar/starredMessagesTab.ts similarity index 51% rename from apps/meteor/app/message-star/client/tabBar.ts rename to apps/meteor/client/startup/contextualBar/starredMessagesTab.ts index d4cf0680ba64..4b3677db7600 100644 --- a/apps/meteor/app/message-star/client/tabBar.ts +++ b/apps/meteor/client/startup/contextualBar/starredMessagesTab.ts @@ -1,10 +1,12 @@ -import { addAction } from '../../../client/views/room/lib/Toolbox'; +import { lazy } from 'react'; + +import { addAction } from '../../views/room/lib/Toolbox'; addAction('starred-messages', { groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], id: 'starred-messages', title: 'Starred_Messages', icon: 'star', - template: 'starredMessages', + template: lazy(() => import('../../views/room/contextualBar/StarredMessagesTab')), order: 10, }); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 4a2566dfd937..240cdb46d717 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -1,4 +1,6 @@ +import '../lib/rooms/roomTypes'; import './absoluteUrl'; +import './actionButtons'; import './afterLogoutCleanUp'; import './appRoot'; import './banners'; @@ -22,7 +24,6 @@ import './oauth'; import './openedRoom'; import './otr'; import './readMessage'; -import './readReceipt'; import './reloadRoomAfterLogin'; import './renderMessage'; import './renderNotification'; @@ -38,4 +39,3 @@ import './UserDeleted'; import './userRoles'; import './usersObserve'; import './userStatusManuallySet'; -import '../lib/rooms/roomTypes'; diff --git a/apps/meteor/client/startup/messageTypes.ts b/apps/meteor/client/startup/messageTypes.ts index 6b4d0d104b21..4ad514335016 100644 --- a/apps/meteor/client/startup/messageTypes.ts +++ b/apps/meteor/client/startup/messageTypes.ts @@ -57,4 +57,10 @@ Meteor.startup(() => { }; }, }); + + MessageTypes.registerType({ + id: 'message_pinned', + system: true, + message: 'Pinned_a_message', + }); }); diff --git a/apps/meteor/client/views/room/MessageList/MessageList.tsx b/apps/meteor/client/views/room/MessageList/MessageList.tsx index 61609e7a05bf..c85974065456 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.tsx @@ -32,9 +32,7 @@ export const MessageList = ({ rid }: MessageListProps): ReactElement => { return ( - {messages.map((message, index, arr) => { - const previous = arr[index - 1]; - + {messages.map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts b/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts index a71f0ec2b03b..fb91bd1d9533 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessages.ts @@ -1,41 +1,17 @@ import type { IRoom, IMessage, MessageTypesValues } from '@rocket.chat/core-typings'; import { useStableArray } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useUserSubscription } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; import type { Mongo } from 'meteor/mongo'; import { useCallback, useMemo } from 'react'; -import { Messages } from '../../../../../app/models/client'; +import { ChatMessage } from '../../../../../app/models/client'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import type { MessageWithMdEnforced } from '../../../../lib/parseMessageTextToAstMarkdown'; -import { parseMessageTextToAstMarkdown, removePossibleNullMessageValues } from '../../../../lib/parseMessageTextToAstMarkdown'; -import { useAutoTranslate } from './useAutoTranslate'; -import { useKatex } from './useKatex'; -export const useMessages = ({ rid }: { rid: IRoom['_id'] }): MessageWithMdEnforced[] => { - const { katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled } = useKatex(); - const subscription = useUserSubscription(rid); - - const autoTranslateOptions = useAutoTranslate(subscription); - const showColors = Boolean(useSetting('HexColorPreview_Enabled')); +export const useMessages = ({ rid }: { rid: IRoom['_id'] }): IMessage[] => { const hideSysMes = useSetting('Hide_System_Messages'); const hideSysMessages = useStableArray(Array.isArray(hideSysMes) ? hideSysMes : []); - const normalizeMessage = useMemo(() => { - const parseOptions = { - colors: showColors, - emoticons: true, - ...(katexEnabled && { - katex: { - dollarSyntax: katexDollarSyntaxEnabled, - parenthesisSyntax: katexParenthesisSyntaxEnabled, - }, - }), - }; - return (message: IMessage): MessageWithMdEnforced => - parseMessageTextToAstMarkdown(removePossibleNullMessageValues(message), parseOptions, autoTranslateOptions); - }, [showColors, katexEnabled, katexDollarSyntaxEnabled, katexParenthesisSyntaxEnabled, autoTranslateOptions]); - const query: Mongo.Query = useMemo( () => ({ rid, @@ -46,17 +22,15 @@ export const useMessages = ({ rid }: { rid: IRoom['_id'] }): MessageWithMdEnforc [rid, hideSysMessages], ); - return useReactiveValue( + return useReactiveValue( useCallback( () => - Messages.find(query, { + ChatMessage.find(query, { sort: { ts: 1, }, - }) - .fetch() - .map(normalizeMessage), - [query, normalizeMessage], + }).fetch(), + [query], ), ); }; diff --git a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx index 6f3ea86ad8e5..5d90f6d569da 100644 --- a/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/components/body/composer/messageBox/MessageBox.tsx @@ -1,3 +1,4 @@ +import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { Button, Tag, Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { @@ -26,7 +27,6 @@ import { useSubscription } from 'use-subscription'; import { EmojiPicker } from '../../../../../../../app/emoji/client'; import { createComposerAPI } from '../../../../../../../app/ui-message/client/messageBox/createComposerAPI'; -import type { MessageBoxTemplateInstance } from '../../../../../../../app/ui-message/client/messageBox/messageBox'; import type { FormattingButton } from '../../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { formattingButtons } from '../../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; import { messageBox, popover } from '../../../../../../../app/ui-utils/client'; @@ -80,7 +80,23 @@ const getEmptyFalse = () => false; const a: any[] = []; const getEmptyArray = () => a; -type MessageBoxProps = Omit; +type MessageBoxProps = { + rid: IRoom['_id']; + tmid?: IMessage['_id']; + readOnly: boolean; + onSend?: (params: { value: string; tshow?: boolean }) => Promise; + onJoin?: () => Promise; + onResize?: () => void; + onTyping?: () => void; + onEscape?: () => void; + onNavigateToPreviousMessage?: () => void; + onNavigateToNextMessage?: () => void; + onUploadFiles?: (files: readonly File[]) => void; + tshow?: IMessage['tshow']; + subscription?: ISubscription; + showFormattingTips: boolean; + isEmbedded?: boolean; +}; const MessageBox = ({ rid, diff --git a/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx b/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx new file mode 100644 index 000000000000..2f136067d96b --- /dev/null +++ b/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx @@ -0,0 +1,125 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import type { Icon } from '@rocket.chat/fuselage'; +import { Box, MessageDivider, Throbber } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { UseQueryResult } from '@tanstack/react-query'; +import type { ReactElement, ComponentProps, ReactNode } from 'react'; +import React, { useCallback } from 'react'; +import { Virtuoso } from 'react-virtuoso'; + +import { MessageTypes } from '../../../../../app/ui-utils/client'; +import type { MessageActionContext } from '../../../../../app/ui-utils/client/lib/MessageAction'; +import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; +import VerticalBarClose from '../../../../components/VerticalBar/VerticalBarClose'; +import VerticalBarContent from '../../../../components/VerticalBar/VerticalBarContent'; +import VerticalBarHeader from '../../../../components/VerticalBar/VerticalBarHeader'; +import VerticalBarIcon from '../../../../components/VerticalBar/VerticalBarIcon'; +import VerticalBarText from '../../../../components/VerticalBar/VerticalBarText'; +import RoomMessage from '../../../../components/message/variants/RoomMessage'; +import SystemMessage from '../../../../components/message/variants/SystemMessage'; +import { useFormatDate } from '../../../../hooks/useFormatDate'; +import MessageListErrorBoundary from '../../MessageList/MessageListErrorBoundary'; +import { isMessageFirstUnread } from '../../MessageList/lib/isMessageFirstUnread'; +import { isMessageNewDay } from '../../MessageList/lib/isMessageNewDay'; +import MessageListProvider from '../../MessageList/providers/MessageListProvider'; +import { useRoomSubscription } from '../../contexts/RoomContext'; +import { useTabBarClose } from '../../contexts/ToolboxContext'; + +type MessageListTabProps = { + iconName: ComponentProps['name']; + title: ReactNode; + emptyResultMessage: ReactNode; + context: MessageActionContext; + queryResult: UseQueryResult; +}; + +const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryResult }: MessageListTabProps): ReactElement => { + const t = useTranslation(); + const formatDate = useFormatDate(); + + const closeTabBar = useTabBarClose(); + const handleTabBarCloseButtonClick = useCallback(() => { + closeTabBar(); + }, [closeTabBar]); + + const subscription = useRoomSubscription(); + + return ( + <> + + + {title} + + + + {queryResult.isLoading && ( + + + + )} + {queryResult.isSuccess && ( + <> + {queryResult.data.length === 0 && ( + + {emptyResultMessage} + + )} + + {queryResult.data.length > 0 && ( + + + + { + const previous = queryResult.data[index - 1]; + + const newDay = isMessageNewDay(message, previous); + const firstUnread = isMessageFirstUnread(subscription, message, previous); + const showDivider = newDay || firstUnread; + + const system = MessageTypes.isSystemMessage(message); + + const unread = subscription?.tunread?.includes(message._id) ?? false; + const mention = subscription?.tunreadUser?.includes(message._id) ?? false; + const all = subscription?.tunreadGroup?.includes(message._id) ?? false; + + return ( + <> + {showDivider && ( + + {newDay && formatDate(message.ts)} + + )} + + {system ? ( + + ) : ( + + )} + + ); + }} + /> + + + + )} + + )} + + + ); +}; + +export default MessageListTab; diff --git a/apps/meteor/client/views/room/contextualBar/MentionsTab.tsx b/apps/meteor/client/views/room/contextualBar/MentionsTab.tsx new file mode 100644 index 000000000000..2777157e2a19 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/MentionsTab.tsx @@ -0,0 +1,44 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi'; +import MessageListTab from '../components/contextualBar/MessageListTab'; +import { useRoom } from '../contexts/RoomContext'; + +const MentionsTab = (): ReactElement => { + const getMentionedMessages = useEndpoint('GET', '/v1/chat.getMentionedMessages'); + + const room = useRoom(); + + const mentionedMessagesQueryResult = useQuery(['rooms', room._id, 'mentioned-messages'] as const, async () => { + const messages: IMessage[] = []; + + for ( + let offset = 0, result = await getMentionedMessages({ roomId: room._id, offset: 0 }); + result.count > 0; + // eslint-disable-next-line no-await-in-loop + offset += result.count, result = await getMentionedMessages({ roomId: room._id, offset }) + ) { + messages.push(...result.messages.map(mapMessageFromApi)); + } + + return messages; + }); + + const t = useTranslation(); + + return ( + + ); +}; + +export default MentionsTab; diff --git a/apps/meteor/client/views/room/contextualBar/PinnedMessagesTab.tsx b/apps/meteor/client/views/room/contextualBar/PinnedMessagesTab.tsx new file mode 100644 index 000000000000..3c212e018d28 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/PinnedMessagesTab.tsx @@ -0,0 +1,44 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi'; +import MessageListTab from '../components/contextualBar/MessageListTab'; +import { useRoom } from '../contexts/RoomContext'; + +const PinnedMessagesTab = (): ReactElement => { + const getPinnedMessages = useEndpoint('GET', '/v1/chat.getPinnedMessages'); + + const room = useRoom(); + + const pinnedMessagesQueryResult = useQuery(['rooms', room._id, 'pinned-messages'] as const, async () => { + const messages: IMessage[] = []; + + for ( + let offset = 0, result = await getPinnedMessages({ roomId: room._id, offset: 0 }); + result.count > 0; + // eslint-disable-next-line no-await-in-loop + offset += result.count, result = await getPinnedMessages({ roomId: room._id, offset }) + ) { + messages.push(...result.messages.map(mapMessageFromApi)); + } + + return messages; + }); + + const t = useTranslation(); + + return ( + + ); +}; + +export default PinnedMessagesTab; diff --git a/apps/meteor/client/views/room/contextualBar/StarredMessagesTab.tsx b/apps/meteor/client/views/room/contextualBar/StarredMessagesTab.tsx new file mode 100644 index 000000000000..eee3be6808cf --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/StarredMessagesTab.tsx @@ -0,0 +1,44 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi'; +import MessageListTab from '../components/contextualBar/MessageListTab'; +import { useRoom } from '../contexts/RoomContext'; + +const StarredMessagesTab = (): ReactElement => { + const getStarredMessages = useEndpoint('GET', '/v1/chat.getStarredMessages'); + + const room = useRoom(); + + const starredMessagesQueryResult = useQuery(['rooms', room._id, 'starred-messages'] as const, async () => { + const messages: IMessage[] = []; + + for ( + let offset = 0, result = await getStarredMessages({ roomId: room._id, offset: 0 }); + result.count > 0; + // eslint-disable-next-line no-await-in-loop + offset += result.count, result = await getStarredMessages({ roomId: room._id, offset }) + ) { + messages.push(...result.messages.map(mapMessageFromApi)); + } + + return messages; + }); + + const t = useTranslation(); + + return ( + + ); +}; + +export default StarredMessagesTab; diff --git a/apps/meteor/definition/externals/meteor/templating.d.ts b/apps/meteor/definition/externals/meteor/templating.d.ts new file mode 100644 index 000000000000..5ec2bb975589 --- /dev/null +++ b/apps/meteor/definition/externals/meteor/templating.d.ts @@ -0,0 +1,116 @@ +import 'meteor/templating'; +import type { Blaze } from 'meteor/blaze'; +import type { ReactiveVar } from 'meteor/reactive-var'; + +declare module 'meteor/blaze' { + namespace Blaze { + interface Template { + events( + eventsMap: Record< + string, + ( + this: TInstance, + event: { + [K in keyof JQuery.TriggeredEvent]: any; + }, + instance: TInstance, + ) => void + >, + ): void; + } + } +} + +declare module 'meteor/templating' { + interface TemplateStatic { + requiresPermission: Blaze.Template>; + ChatpalAdmin: Blaze.Template>; + ChatpalSearchResultTemplate: Blaze.Template>; + ChatpalSearchSingleTemplate: Blaze.Template>; + ChatpalSearchSingleUser: Blaze.Template>; + ChatpalSearchSingleRoom: Blaze.Template>; + ChatpalSuggestionItemTemplate: Blaze.Template>; + emojiPicker: Blaze.Template>; + lazyloadImage: Blaze.Template>; + customFieldsForm: Blaze.Template>; + ExternalFrameContainer: Blaze.Template>; + broadcastView: Blaze.Template>; + liveStreamBroadcast: Blaze.Template>; + liveStreamTab: Blaze.Template>; + liveStreamView: Blaze.Template>; + snippetPage: Blaze.Template>; + snippetedMessages: Blaze.Template>; + inputAutocomplete: Blaze.Template>; + textareaAutocomplete: Blaze.Template>; + _autocompleteContainer: Blaze.Template>; + _noMatch: Blaze.Template>; + authorize: Blaze.Template>; + oauth404: Blaze.Template>; + oembedBaseWidget: Blaze.Template>; + oembedAudioWidget: Blaze.Template>; + oembedFrameWidget: Blaze.Template>; + oembedImageWidget: Blaze.Template>; + oembedUrlWidget: Blaze.Template>; + oembedVideoWidget: Blaze.Template>; + oembedYoutubeWidget: Blaze.Template>; + DefaultSearchResultTemplate: Blaze.Template>; + DefaultSuggestionItemTemplate: Blaze.Template>; + RocketSearch: Blaze.Template>; + icon: Blaze.Template>; + popupList: Blaze.Template>; + popupList_default: Blaze.Template>; + popupList_item_default: Blaze.Template>; + popupList_loading: Blaze.Template>; + popupList_item_channel: Blaze.Template>; + popupList_item_custom: Blaze.Template>; + selectDropdown: Blaze.Template>; + CodeMirror: Blaze.Template>; + photoswipeContent: Blaze.Template>; + roomSearch: Blaze.Template>; + roomSearchEmpty: Blaze.Template>; + avatar: Blaze.Template>; + username: Blaze.Template< + Record, + Blaze.TemplateInstance> & { + customFields: ReactiveVar | null>; + username: ReactiveVar<{ + ready: boolean; + username: string; + empty?: boolean; + error?: boolean; + invalid?: boolean; + escaped?: string; + blocked?: boolean; + unavailable?: boolean; + }>; + validate: () => unknown; + } + >; + error: Blaze.Template>; + loading: Blaze.Template>; + message: Blaze.Template>; + messageThread: Blaze.Template>; + messagePopup: Blaze.Template>; + messagePopupChannel: Blaze.Template>; + messagePopupConfig: Blaze.Template>; + messagePopupEmoji: Blaze.Template>; + messagePopupSlashCommand: Blaze.Template>; + messagePopupSlashCommandPreview: Blaze.Template>; + messagePopupUser: Blaze.Template>; + collapseArrow: Blaze.Template>; + rc_modal: Blaze.Template>; + popout: Blaze.Template>; + popover: Blaze.Template>; + audit: Blaze.Template>; + messagePopupCannedResponse: Blaze.Template>; + + instance(): TemplateStatic[TTemplateName] extends Blaze.Template ? I : never; + } +} diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index e9fffaa5e81e..c88101fcd108 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -89,6 +89,7 @@ export type MessageTypesValues = | 'room-disallowed-reacting' | 'command' | 'videoconf' + | 'message_pinned' | LivechatMessageTypes | TeamMessageTypes | VoipMessageTypesValues @@ -186,8 +187,6 @@ export interface IMessage extends IRocketChatRecord { }; } -export type ToolboxMessageType = 'message' | 'thread' | 'federated'; - export type MessageSystem = { t: 'system'; }; diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 2857df0ca438..a939f93b0dc7 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -29,5 +29,8 @@ "@rocket.chat/message-parser": "next", "@rocket.chat/ui-kit": "next", "ajv": "^8.11.0" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index 0995896edcf4..0df97e5b0275 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -405,6 +405,102 @@ const ChatGetMessageReadReceiptsSchema = { export const isChatGetMessageReadReceiptsProps = ajv.compile(ChatGetMessageReadReceiptsSchema); +type GetStarredMessages = { + roomId: IRoom['_id']; + count?: number; + offset?: number; + sort?: string; +}; + +const GetStarredMessagesSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isChatGetStarredMessagesPayload = ajv.compile(GetStarredMessagesSchema); + +type GetPinnedMessages = { + roomId: IRoom['_id']; + count?: number; + offset?: number; + sort?: string; +}; + +const GetPinnedMessagesSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isChatGetPinnedMessagesPayload = ajv.compile(GetPinnedMessagesSchema); + +type GetMentionedMessages = { + roomId: IRoom['_id']; + count?: number; + offset?: number; + sort?: string; +}; + +const GetMentionedMessagesSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isChatGetMentionedMessagesPayload = ajv.compile(GetMentionedMessagesSchema); + export type ChatEndpoints = { '/v1/chat.sendMessage': { POST: (params: ChatSendMessage) => IMessage; @@ -481,4 +577,28 @@ export type ChatEndpoints = { '/v1/chat.getMessageReadReceipts': { GET: (params: ChatGetMessageReadReceipts) => { receipts: ReadReceipt[] }; }; + '/v1/chat.getStarredMessages': { + GET: (params: GetStarredMessages) => { + messages: IMessage[]; + count: number; + offset: number; + total: number; + }; + }; + '/v1/chat.getPinnedMessages': { + GET: (params: GetPinnedMessages) => { + messages: IMessage[]; + count: number; + offset: number; + total: number; + }; + }; + '/v1/chat.getMentionedMessages': { + GET: (params: GetMentionedMessages) => { + messages: IMessage[]; + count: number; + offset: number; + total: number; + }; + }; }; diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 0ee4f5b6c178..d40a943e0d5c 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -266,6 +266,8 @@ export interface ServerMethods { 'permissions/get': (updatedSince?: Date) => IPermission[] | { update: IPermission[]; remove: IPermission[] }; 'public-settings/get': (updatedSince?: Date) => ISetting[] | { update: ISetting[]; remove: ISetting[] }; 'private-settings/get': (updatedSince?: Date) => ISetting[] | { update: ISetting[]; remove: ISetting[] }; + 'pinMessage': (message: IMessage) => void; + 'unpinMessage': (message: IMessage) => void; } export type ServerMethodName = keyof ServerMethods;