diff --git a/.changeset/chilly-pants-hunt.md b/.changeset/chilly-pants-hunt.md new file mode 100644 index 000000000000..0127ae7e174f --- /dev/null +++ b/.changeset/chilly-pants-hunt.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removes a validation that allowed only the room creator to propagate E2EE room keys. This was causing issues when the rooms were created via apps or some other integration, as the creator may not be online or able to create E2EE keys diff --git a/.changeset/green-shirts-fold.md b/.changeset/green-shirts-fold.md new file mode 100644 index 000000000000..c4dc5cfcf3ad --- /dev/null +++ b/.changeset/green-shirts-fold.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes condition causing Omnichannel queue to start more than once. diff --git a/.changeset/proud-cups-share.md b/.changeset/proud-cups-share.md new file mode 100644 index 000000000000..eb51d15a9382 --- /dev/null +++ b/.changeset/proud-cups-share.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes `im.counters` endpoint returning `null` on `unread` messages property for users that have never opened the queried DM diff --git a/.changeset/real-crabs-grin.md b/.changeset/real-crabs-grin.md new file mode 100644 index 000000000000..fe0066e0183f --- /dev/null +++ b/.changeset/real-crabs-grin.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Organizes App Settings interface by introducing section-based accordion groups to improve navigation and readability for administrators. diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index fa274ef69467..d74d3decfbab 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -195,9 +195,9 @@ API.v1.addRoute( lm = room?.lm ? new Date(room.lm).toISOString() : new Date(room._updatedAt).toISOString(); // lm is the last message timestamp - if (subscription?.open) { + if (subscription) { + unreads = subscription.unread ?? null; if (subscription.ls && room.msgs) { - unreads = subscription.unread; unreadsFrom = new Date(subscription.ls).toISOString(); // last read timestamp } userMentions = subscription.userMentions; diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts index dc7efb60dc14..f9913831533b 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.ts @@ -326,8 +326,7 @@ export class E2ERoom extends Emitter { try { const room = Rooms.findOne({ _id: this.roomId })!; - // Only room creator can set keys for room - if (!room.e2eKeyId && this.userShouldCreateKeys(room)) { + if (!room.e2eKeyId) { this.setState(E2ERoomState.CREATING_KEYS); await this.createGroupKey(); this.setState(E2ERoomState.READY); @@ -343,15 +342,6 @@ export class E2ERoom extends Emitter { } } - userShouldCreateKeys(room: any) { - // On DMs, we'll allow any user to set the keys - if (room.t === 'd') { - return true; - } - - return room.u._id === this.userId; - } - isSupportedRoomType(type: any) { return roomCoordinator.getRoomDirectives(type).allowRoomSettingChange({}, RoomSettingsEnum.E2E); } diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts deleted file mode 100644 index fc052eaf63d3..000000000000 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { router } from '../../../client/providers/RouterProvider'; -import { Subscriptions } from '../../models/client'; -import { LegacyRoomManager, MessageAction } from '../../ui-utils/client'; -import { sdk } from '../../utils/client/lib/SDKClient'; - -Meteor.startup(() => { - MessageAction.addButton({ - id: 'mark-message-as-unread', - icon: 'flag', - label: 'Mark_unread', - context: ['message', 'message-mobile', 'threads'], - type: 'interaction', - async action(_, { message }) { - try { - const subscription = Subscriptions.findOne({ - rid: message.rid, - }); - - if (subscription == null) { - return; - } - router.navigate('/home'); - await LegacyRoomManager.close(subscription.t + subscription.name); - await sdk.call('unreadMessages', message); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }, - condition({ message, user, room }) { - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } - - if (!user) { - return false; - } - - return message.u._id !== user._id; - }, - order: 4, - group: 'menu', - }); -}); diff --git a/apps/meteor/app/message-mark-as-unread/client/index.ts b/apps/meteor/app/message-mark-as-unread/client/index.ts deleted file mode 100644 index 4a15cde3d04c..000000000000 --- a/apps/meteor/app/message-mark-as-unread/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './actionButton'; diff --git a/apps/meteor/app/reactions/client/index.ts b/apps/meteor/app/reactions/client/index.ts index 8d85a264dd7d..935ab48e93ad 100644 --- a/apps/meteor/app/reactions/client/index.ts +++ b/apps/meteor/app/reactions/client/index.ts @@ -1,2 +1 @@ -import './init'; import './methods/setReaction'; diff --git a/apps/meteor/app/reactions/client/init.ts b/apps/meteor/app/reactions/client/init.ts deleted file mode 100644 index d9573fbfe833..000000000000 --- a/apps/meteor/app/reactions/client/init.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -import { MessageAction } from '../../ui-utils/client'; -import { sdk } from '../../utils/client/lib/SDKClient'; - -Meteor.startup(() => { - MessageAction.addButton({ - id: 'reaction-message', - icon: 'add-reaction', - label: 'Add_Reaction', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - action(event, { message, chat }) { - event?.stopPropagation(); - chat?.emojiPicker.open(event?.currentTarget as Element, (emoji) => sdk.call('setReaction', `:${emoji}:`, message._id)); - }, - condition({ message, user, room, subscription }) { - if (!room) { - return false; - } - - if (!subscription) { - return false; - } - - if (message.private) { - return false; - } - - if (roomCoordinator.readOnly(room._id, user!) && !room.reactWhenReadOnly) { - return false; - } - const isLivechatRoom = roomCoordinator.isLivechatRoom(room.t); - if (isLivechatRoom) { - return false; - } - - return true; - }, - order: -3, - group: 'message', - }); -}); diff --git a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts index 460ac2941e82..1301863b7e53 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageActionDefault.ts @@ -118,32 +118,6 @@ Meteor.startup(async () => { group: 'message', }); - MessageAction.addButton({ - id: 'permalink', - icon: 'permalink', - label: 'Copy_link', - // classes: 'clipboard', - context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], - type: 'duplication', - async action(_, { message }) { - try { - const permalink = await getPermaLink(message._id); - await navigator.clipboard.writeText(permalink); - dispatchToastMessage({ type: 'success', message: t('Copied') }); - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - }, - condition({ subscription }) { - return !!subscription; - }, - order: 5, - group: 'menu', - disabled({ message }) { - return isE2EEMessage(message); - }, - }); - MessageAction.addButton({ id: 'copy', icon: 'copy', diff --git a/apps/meteor/client/components/UserCard/UserCard.stories.tsx b/apps/meteor/client/components/UserCard/UserCard.stories.tsx index 95b7e27becdc..ac68b113a22d 100644 --- a/apps/meteor/client/components/UserCard/UserCard.stories.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.stories.tsx @@ -3,6 +3,20 @@ import React from 'react'; import { UserCard, UserCardRole, UserCardAction } from '.'; +const user = { + name: 'guilherme.gazzo', + customStatus: '🛴 currently working on User Card', + roles: ( + <> + Admin + Rocket.Chat + Team + + ), + bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus. Mauris in justo vel lorem ullamcorper hendrerit. Nam est metus, viverra a pellentesque vitae, ornare eget odio. Morbi tempor feugiat mattis. Morbi non felis tempor, aliquam justo sed, sagittis nibh. Mauris consequat ex metus. Praesent sodales sit amet nibh a vulputate. Integer commodo, mi vel bibendum sollicitudin, urna lectus accumsan ante, eget faucibus augue ex id neque. Aenean consectetur, orci a pellentesque mattis, tortor tellus fringilla elit, non ullamcorper risus nunc feugiat risus. Fusce sit amet nisi dapibus turpis commodo placerat. In tortor ante, vehicula sit amet augue et, imperdiet porta sem.', + localTime: 'Local Time: 7:44 AM', +}; + export default { title: 'Components/UserCard', component: UserCard, @@ -10,23 +24,13 @@ export default { layout: 'centered', }, args: { - name: 'guilherme.gazzo', - customStatus: '🛴 currently working on User Card', - roles: ( - <> - Admin - Rocket.Chat - Team - - ), - bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus. Mauris in justo vel lorem ullamcorper hendrerit. Nam est metus, viverra a pellentesque vitae, ornare eget odio. Morbi tempor feugiat mattis. Morbi non felis tempor, aliquam justo sed, sagittis nibh. Mauris consequat ex metus. Praesent sodales sit amet nibh a vulputate. Integer commodo, mi vel bibendum sollicitudin, urna lectus accumsan ante, eget faucibus augue ex id neque. Aenean consectetur, orci a pellentesque mattis, tortor tellus fringilla elit, non ullamcorper risus nunc feugiat risus. Fusce sit amet nisi dapibus turpis commodo placerat. In tortor ante, vehicula sit amet augue et, imperdiet porta sem.', + user, actions: ( <> ), - localTime: 'Local Time: 7:44 AM', }, } satisfies Meta; @@ -36,18 +40,27 @@ export const Example = Template.bind({}); export const Nickname = Template.bind({}); Nickname.args = { - nickname: 'nicknamenickname', + user: { + ...user, + nickname: 'nicknamenickname', + }, } as any; export const LargeName = Template.bind({}); LargeName.args = { - customStatus: '🛴 currently working on User Card on User Card on User Card on User Card on User Card ', - name: 'guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.', + user: { + ...user, + customStatus: '🛴 currently working on User Card on User Card on User Card on User Card on User Card ', + name: 'guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.', + }, } as any; export const NoRoles = Template.bind({}); NoRoles.args = { - roles: undefined, + user: { + ...user, + roles: undefined, + }, } as any; export const NoActions = Template.bind({}); @@ -57,25 +70,37 @@ NoActions.args = { export const NoLocalTime = Template.bind({}); NoLocalTime.args = { - localTime: undefined, + user: { + ...user, + localTime: undefined, + }, } as any; export const NoBio = Template.bind({}); NoBio.args = { - bio: undefined, + user: { + ...user, + bio: undefined, + }, } as any; export const NoBioAndNoLocalTime = Template.bind({}); NoBioAndNoLocalTime.args = { - bio: undefined, - localTime: undefined, + user: { + ...user, + bio: undefined, + localTime: undefined, + }, } as any; export const NoBioNoLocalTimeNoRoles = Template.bind({}); NoBioNoLocalTimeNoRoles.args = { - bio: undefined, - localTime: undefined, - roles: undefined, + user: { + ...user, + bio: undefined, + localTime: undefined, + roles: undefined, + }, } as any; export const Loading = () => ; diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index 98e1cce2ab78..9ac99a6b4cbd 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -23,33 +23,27 @@ const clampStyle = css` `; type UserCardProps = { - onOpenUserInfo?: () => void; - name?: string; - username?: string; - etag?: string; - customStatus?: ReactNode; - roles?: ReactNode; - bio?: ReactNode; - status?: ReactNode; + user?: { + nickname?: string; + name?: string; + username?: string; + etag?: string; + customStatus?: ReactNode; + roles?: ReactNode; + bio?: ReactNode; + status?: ReactNode; + localTime?: ReactNode; + }; actions?: ReactNode; - localTime?: ReactNode; + onOpenUserInfo?: () => void; onClose?: () => void; - nickname?: string; } & ComponentProps; const UserCard = ({ - onOpenUserInfo, - name, - username, - etag, - customStatus, - roles, - bio, - status = , + user: { name, username, etag, customStatus, roles, bio, status = , localTime, nickname } = {}, actions, - localTime, + onOpenUserInfo, onClose, - nickname, ...props }: UserCardProps) => { const { t } = useTranslation(); diff --git a/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx b/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx new file mode 100644 index 000000000000..cd52c5e34dd8 --- /dev/null +++ b/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx @@ -0,0 +1,20 @@ +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; + +import { LegacyRoomManager } from '../../../../app/ui-utils/client'; +import { sdk } from '../../../../app/utils/client/lib/SDKClient'; + +export const useMarkAsUnreadMutation = () => { + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutation({ + mutationFn: async ({ message, subscription }: { message: IMessage; subscription: ISubscription }) => { + await LegacyRoomManager.close(subscription.t + subscription.name); + await sdk.call('unreadMessages', message); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + }); +}; diff --git a/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts b/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts index 6ac601f44a13..ce8ec3f9cd81 100644 --- a/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts +++ b/apps/meteor/client/components/message/hooks/usePinMessageMutation.ts @@ -21,8 +21,9 @@ export const usePinMessageMutation = () => { onSuccess: () => { dispatchToastMessage({ type: 'success', message: t('Message_has_been_pinned') }); }, - onError: (error) => { + onError: (error, message) => { dispatchToastMessage({ type: 'error', message: error }); + updatePinMessage(message, { pinned: false }); }, onSettled: (_data, _error, message) => { queryClient.invalidateQueries(roomsQueryKeys.pinnedMessages(message.rid)); diff --git a/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts b/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts index 5c2c716e63f6..a3c4c2882b0b 100644 --- a/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts +++ b/apps/meteor/client/components/message/hooks/useUnpinMessageMutation.ts @@ -21,8 +21,9 @@ export const useUnpinMessageMutation = () => { onSuccess: () => { dispatchToastMessage({ type: 'success', message: t('Message_has_been_unpinned') }); }, - onError: (error) => { + onError: (error, message) => { dispatchToastMessage({ type: 'error', message: error }); + updatePinMessage(message, { pinned: true }); }, onSettled: (_data, _error, message) => { queryClient.invalidateQueries(roomsQueryKeys.pinnedMessages(message.rid)); diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx index 464cb51bc175..9e5a0f10f85f 100644 --- a/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageToolbar.tsx @@ -12,9 +12,11 @@ import MessageActionMenu from './MessageActionMenu'; import MessageToolbarStarsActionMenu from './MessageToolbarStarsActionMenu'; import { useFollowMessageAction } from './useFollowMessageAction'; import { useJumpToMessageContextAction } from './useJumpToMessageContextAction'; +import { useMarkAsUnreadMessageAction } from './useMarkAsUnreadMessageAction'; import { useNewDiscussionMessageAction } from './useNewDiscussionMessageAction'; -import { usePermalinkStar } from './usePermalinkStar'; +import { usePermalinkAction } from './usePermalinkAction'; import { usePinMessageAction } from './usePinMessageAction'; +import { useReactionMessageAction } from './useReactionMessageAction'; import { useReplyInThreadMessageAction } from './useReplyInThreadMessageAction'; import { useStarMessageAction } from './useStarMessageAction'; import { useUnFollowMessageAction } from './useUnFollowMessageAction'; @@ -102,7 +104,15 @@ const MessageToolbar = ({ usePinMessageAction(message, { room, subscription }); useStarMessageAction(message, { room, user }); useUnstarMessageAction(message, { room, user }); - usePermalinkStar(message, { subscription, user }); + usePermalinkAction(message, { subscription, id: 'permalink-star', context: ['starred'], order: 10 }); + usePermalinkAction(message, { subscription, id: 'permalink-pinned', context: ['pinned'], order: 5 }); + usePermalinkAction(message, { + subscription, + id: 'permalink', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + type: 'duplication', + order: 5, + }); useFollowMessageAction(message, { room, user, context }); useUnFollowMessageAction(message, { room, user, context }); useReplyInThreadMessageAction(message, { room, subscription }); @@ -123,6 +133,8 @@ const MessageToolbar = ({ order: 100, context: ['starred'], }); + useReactionMessageAction(message, { user, room, subscription }); + useMarkAsUnreadMessageAction(message, { user, room, subscription }); const actionsQueryResult = useQuery({ queryKey: roomsQueryKeys.messageActionsWithParameters(room._id, message), diff --git a/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts b/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts new file mode 100644 index 000000000000..208a83679f7e --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useMarkAsUnreadMessageAction.ts @@ -0,0 +1,47 @@ +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { ISubscription, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { useRouter } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { MessageAction } from '../../../../app/ui-utils/client'; +import { useMarkAsUnreadMutation } from '../hooks/useMarkAsUnreadMutation'; + +export const useMarkAsUnreadMessageAction = ( + message: IMessage, + { user, room, subscription }: { user: IUser | undefined; room: IRoom; subscription: ISubscription | undefined }, +) => { + const { mutateAsync: markAsUnread } = useMarkAsUnreadMutation(); + + const router = useRouter(); + + useEffect(() => { + if (isOmnichannelRoom(room) || !user) { + return; + } + + if (!subscription) { + return; + } + + if (message.u._id === user._id) { + return; + } + + MessageAction.addButton({ + id: 'mark-message-as-unread', + icon: 'flag', + label: 'Mark_unread', + context: ['message', 'message-mobile', 'threads'], + type: 'interaction', + async action() { + router.navigate('/home'); + await markAsUnread({ message, subscription }); + }, + order: 4, + group: 'menu', + }); + return () => { + MessageAction.removeButton('mark-message-as-unread'); + }; + }, [markAsUnread, message, room, router, subscription, user]); +}; diff --git a/apps/meteor/client/components/message/toolbar/usePermalinkStar.tsx b/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts similarity index 62% rename from apps/meteor/client/components/message/toolbar/usePermalinkStar.tsx rename to apps/meteor/client/components/message/toolbar/usePermalinkAction.ts index 15e6cc5056a7..78a197d5c5d7 100644 --- a/apps/meteor/client/components/message/toolbar/usePermalinkStar.tsx +++ b/apps/meteor/client/components/message/toolbar/usePermalinkAction.ts @@ -1,15 +1,22 @@ -import type { IMessage, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { isE2EEMessage } from '@rocket.chat/core-typings'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import type { MessageActionConfig, MessageActionContext } from '../../../../app/ui-utils/client/lib/MessageAction'; import { MessageAction } from '../../../../app/ui-utils/client/lib/MessageAction'; import { getPermaLink } from '../../../lib/getPermaLink'; -export const usePermalinkStar = ( +export const usePermalinkAction = ( message: IMessage, - { user, subscription }: { user: IUser | undefined; subscription: ISubscription | undefined }, + { + subscription, + id, + context, + type, + order, + }: { subscription: ISubscription | undefined; context: MessageActionContext[]; order: number } & Pick, ) => { const { t } = useTranslation(); @@ -18,15 +25,12 @@ export const usePermalinkStar = ( const encrypted = isE2EEMessage(message); useEffect(() => { - if (!subscription) { - return; - } - MessageAction.addButton({ - id: 'permalink-star', + id, icon: 'permalink', label: 'Copy_link', - context: ['starred'], + context, + type, async action() { try { const permalink = await getPermaLink(message._id); @@ -36,13 +40,13 @@ export const usePermalinkStar = ( dispatchToastMessage({ type: 'error', message: e }); } }, - order: 10, + order, group: 'menu', disabled: () => encrypted, }); return () => { - MessageAction.removeButton('permalink-star'); + MessageAction.removeButton(id); }; - }, [dispatchToastMessage, encrypted, message._id, message.starred, subscription, t, user?._id]); + }, [context, dispatchToastMessage, encrypted, id, message._id, order, subscription, t, type]); }; diff --git a/apps/meteor/client/components/message/toolbar/useReactionMessageAction.ts b/apps/meteor/client/components/message/toolbar/useReactionMessageAction.ts new file mode 100644 index 000000000000..3456a01204a4 --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/useReactionMessageAction.ts @@ -0,0 +1,39 @@ +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription, IUser, IMessage } from '@rocket.chat/core-typings'; +import { useEffect } from 'react'; + +import { MessageAction } from '../../../../app/ui-utils/client'; +import { sdk } from '../../../../app/utils/client/lib/SDKClient'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; + +export const useReactionMessageAction = ( + message: IMessage, + { user, room, subscription }: { user: IUser | undefined; room: IRoom; subscription: ISubscription | undefined }, +) => { + useEffect(() => { + if (!room || isOmnichannelRoom(room) || !subscription || message.private || !user) { + return; + } + + if (roomCoordinator.readOnly(room._id, user) && !room.reactWhenReadOnly) { + return; + } + + MessageAction.addButton({ + id: 'reaction-message', + icon: 'add-reaction', + label: 'Add_Reaction', + context: ['message', 'message-mobile', 'threads', 'federated', 'videoconf', 'videoconf-threads'], + action(event, { message, chat }) { + event?.stopPropagation(); + chat?.emojiPicker.open(event?.currentTarget as Element, (emoji) => sdk.call('setReaction', `:${emoji}:`, message._id)); + }, + order: -3, + group: 'message', + }); + + return () => { + MessageAction.removeButton('reaction-message'); + }; + }, [message.private, room, subscription, user]); +}; diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index f51e51335c9b..59e7ec3d7c81 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -16,7 +16,6 @@ import '../app/iframe-login/client'; import '../app/license/client'; import '../app/lib/client'; import '../app/livechat-enterprise/client'; -import '../app/message-mark-as-unread/client'; import '../app/nextcloud/client'; import '../app/notifications/client'; import '../app/otr/client'; diff --git a/apps/meteor/client/methods/index.ts b/apps/meteor/client/methods/index.ts index e97c644745ce..6e054fd54892 100644 --- a/apps/meteor/client/methods/index.ts +++ b/apps/meteor/client/methods/index.ts @@ -1,2 +1 @@ import './openRoom'; -import './unpinMessage'; diff --git a/apps/meteor/client/methods/unpinMessage.ts b/apps/meteor/client/methods/unpinMessage.ts deleted file mode 100644 index 0c9ca6909567..000000000000 --- a/apps/meteor/client/methods/unpinMessage.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { Messages, Subscriptions } from '../../app/models/client'; -import { settings } from '../../app/settings/client'; -import { t } from '../../app/utils/lib/i18n'; -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 (!Subscriptions.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; - } - Messages.update( - { - _id: message._id, - rid: message.rid, - }, - { - $set: { - pinned: false, - }, - }, - ); - - return true; - }, -}); diff --git a/apps/meteor/client/startup/actionButtons/index.ts b/apps/meteor/client/startup/actionButtons/index.ts deleted file mode 100644 index d1e6724638c6..000000000000 --- a/apps/meteor/client/startup/actionButtons/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './permalinkPinned'; diff --git a/apps/meteor/client/startup/actionButtons/permalinkPinned.ts b/apps/meteor/client/startup/actionButtons/permalinkPinned.ts deleted file mode 100644 index add09ca7dcd0..000000000000 --- a/apps/meteor/client/startup/actionButtons/permalinkPinned.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { isE2EEMessage } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; - -import { MessageAction } from '../../../app/ui-utils/client'; -import { t } from '../../../app/utils/lib/i18n'; -import { getPermaLink } from '../../lib/getPermaLink'; -import { dispatchToastMessage } from '../../lib/toast'; - -Meteor.startup(() => { - MessageAction.addButton({ - id: 'permalink-pinned', - icon: 'permalink', - label: 'Copy_link', - context: ['pinned'], - async action(_, { message }) { - try { - const permalink = await 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: 5, - group: 'menu', - disabled({ message }) { - return isE2EEMessage(message); - }, - }); -}); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 569b11bf1c18..3edff17dc427 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -1,6 +1,5 @@ import '../lib/rooms/roomTypes'; import './absoluteUrl'; -import './actionButtons'; import './afterLogoutCleanUp'; import './appRoot'; import './audit'; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx index 8e6d297e433e..e5aa1d203a6f 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSetting.tsx @@ -1,53 +1,13 @@ import type { ISettingSelectValue } from '@rocket.chat/apps-engine/definition/settings'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings/ISetting'; -import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { Utilities } from '../../../../../../ee/lib/misc/Utilities'; import MarkdownText from '../../../../../components/MarkdownText'; import MemoizedSetting from '../../../../admin/settings/Setting/MemoizedSetting'; - -type AppTranslationFunction = { - (key: string, ...replaces: unknown[]): string; - has: (key: string | undefined) => boolean; -}; - -const useAppTranslation = (appId: string): AppTranslationFunction => { - const t = useTranslation(); - - const tApp = useCallback( - (key: string, ...args: unknown[]) => { - if (!key) { - return ''; - } - const appKey = Utilities.getI18nKeyForApp(key, appId); - - if (t.has(appKey)) { - return t(appKey, ...args); - } - if (t.has(key)) { - return t(key, ...args); - } - return key; - }, - [t, appId], - ); - - return Object.assign(tApp, { - has: useCallback( - (key: string | undefined) => { - if (!key) { - return false; - } - - return t.has(Utilities.getI18nKeyForApp(key, appId)) || t.has(key); - }, - [t, appId], - ), - }); -}; +import { useAppTranslation } from '../../../hooks/useAppTranslation'; const AppSetting = ({ id, type, i18nLabel, i18nDescription, values, value, packageValue, ...props }: ISetting): ReactElement => { const appId = useRouteParameter('id'); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSettings.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSettings.tsx index 72ad86378747..3501ae0eb563 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSettings.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppSettings/AppSettings.tsx @@ -1,26 +1,45 @@ -import { Box, FieldGroup } from '@rocket.chat/fuselage'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; +import { Box, FieldGroup, Accordion, AccordionItem } from '@rocket.chat/fuselage'; +import { useRouteParameter } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; import AppSetting from './AppSetting'; import type { ISettings } from '../../../../../apps/@types/IOrchestrator'; +import { useAppTranslation } from '../../../hooks/useAppTranslation'; const AppSettings = ({ settings }: { settings: ISettings }) => { - const { t } = useTranslation(); + const appId = useRouteParameter('id'); + const tApp = useAppTranslation(appId || ''); + + const groupedSettings = useMemo(() => { + const groups = Object.values(settings).reduce( + (acc, setting) => { + const section = setting.section || 'general'; + if (!acc[section]) { + acc[section] = []; + } + acc[section].push(setting); + return acc; + }, + {} as Record, + ); + + return Object.entries(groups); + }, [settings]); return ( - <> - - - {t('Settings')} - - - {Object.values(settings).map((field) => ( - - ))} - - - + + + {groupedSettings.map(([section, sectionSettings], index) => ( + + + {sectionSettings.map((field) => ( + + ))} + + + ))} + + ); }; diff --git a/apps/meteor/client/views/marketplace/hooks/useAppTranslation.ts b/apps/meteor/client/views/marketplace/hooks/useAppTranslation.ts new file mode 100644 index 000000000000..e897a4c19b39 --- /dev/null +++ b/apps/meteor/client/views/marketplace/hooks/useAppTranslation.ts @@ -0,0 +1,44 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { Utilities } from '../../../../ee/lib/misc/Utilities'; + +type AppTranslationFunction = { + (key: string, ...replaces: unknown[]): string; + has: (key: string | undefined) => boolean; +}; + +export const useAppTranslation = (appId: string): AppTranslationFunction => { + const t = useTranslation(); + + const tApp = useCallback( + (key: string, ...args: unknown[]) => { + if (!key) { + return ''; + } + const appKey = Utilities.getI18nKeyForApp(key, appId); + + if (t.has(appKey)) { + return t(appKey, ...args); + } + if (t.has(key)) { + return t(key, ...args); + } + return key; + }, + [t, appId], + ); + + return Object.assign(tApp, { + has: useCallback( + (key: string | undefined) => { + if (!key) { + return false; + } + + return t.has(Utilities.getI18nKeyForApp(key, appId)) || t.has(key); + }, + [t, appId], + ), + }); +}; diff --git a/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx b/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx index 59e1584d5ae2..7eaba997727b 100644 --- a/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx +++ b/apps/meteor/client/views/oauth/components/CurrentUserDisplay.tsx @@ -2,7 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { UserStatus } from '@rocket.chat/ui-client'; import { useRolesDescription, useSetting } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import LocalTime from '../../../components/LocalTime'; @@ -26,29 +26,33 @@ const CurrentUserDisplay = ({ user }: CurrentUserDisplayProps) => { const getRoles = useRolesDescription(); const { t } = useTranslation(); + const { username, avatarETag, name, statusText, nickname, roles, utcOffset, bio } = user; + + const data = useMemo( + () => ({ + username, + etag: avatarETag, + name: showRealNames ? name : username, + nickname, + status: , + customStatus: statusText ?? <>, + roles: roles && getRoles(roles).map((role, index) => {role}), + localTime: utcOffset && Number.isInteger(utcOffset) && , + bio: bio ? ( + + {typeof bio === 'string' ? : bio} + + ) : ( + <> + ), + }), + [avatarETag, bio, getRoles, name, nickname, roles, showRealNames, statusText, username, utcOffset], + ); return ( <>

{t('core.You_are_logged_in_as')}

- } - customStatus={user.statusText ?? <>} - roles={user.roles && getRoles(user.roles).map((role, index) => {role})} - localTime={user.utcOffset && Number.isInteger(user.utcOffset) && } - bio={ - user.bio ? ( - - {typeof user.bio === 'string' ? : user.bio} - - ) : ( - <> - ) - } - /> + ); }; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx b/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx index f9daf3d14816..e58db0f06162 100644 --- a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx +++ b/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx @@ -7,7 +7,7 @@ type AnnouncementComponenttParams = { }; const AnnouncementComponent: FC = ({ children, onClickOpen }) => ( - + {children} ); diff --git a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx index 70826fd24e40..4c3312770435 100644 --- a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx +++ b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx @@ -110,7 +110,7 @@ const UserCardWithData = ({ username, rid, onOpenUserInfo, onClose }: UserCardWi return ; } - return ; + return ; }; export default UserCardWithData; diff --git a/apps/meteor/client/views/room/body/RoomTopic.tsx b/apps/meteor/client/views/room/body/RoomTopic.tsx index 0987f39602a8..fee06bd087d6 100644 --- a/apps/meteor/client/views/room/body/RoomTopic.tsx +++ b/apps/meteor/client/views/room/body/RoomTopic.tsx @@ -51,7 +51,7 @@ export const RoomTopic = ({ room, user }: RoomTopicProps) => { if (!topic && !roomLeader) return null; return ( - + {roomLeader && !topic && canEdit ? ( diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index 8db6eedd386b..29d48b9f1f6b 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -1,3 +1,4 @@ +import { ServiceStarter } from '@rocket.chat/core-services'; import { type InquiryWithAgentInfo, type IOmnichannelQueue } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; @@ -11,6 +12,17 @@ import { settings } from '../../../app/settings/server'; const DEFAULT_RACE_TIMEOUT = 5000; export class OmnichannelQueue implements IOmnichannelQueue { + private serviceStarter: ServiceStarter; + + private timeoutHandler: ReturnType | null = null; + + constructor() { + this.serviceStarter = new ServiceStarter( + () => this._start(), + () => this._stop(), + ); + } + private running = false; private queues: (string | undefined)[] = []; @@ -24,7 +36,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { return this.running; } - async start() { + private async _start() { if (this.running) { return; } @@ -37,7 +49,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { return this.execute(); } - async stop() { + private async _stop() { if (!this.running) { return; } @@ -45,9 +57,23 @@ export class OmnichannelQueue implements IOmnichannelQueue { await LivechatInquiry.unlockAll(); this.running = false; + + if (this.timeoutHandler !== null) { + clearTimeout(this.timeoutHandler); + this.timeoutHandler = null; + } + queueLogger.info('Service stopped'); } + async start() { + return this.serviceStarter.start(); + } + + async stop() { + return this.serviceStarter.stop(); + } + private async getActiveQueues() { // undefined = public queue(without department) return ([undefined] as typeof this.queues).concat(await LivechatInquiry.getDistinctQueuedDepartments({})); @@ -118,10 +144,21 @@ export class OmnichannelQueue implements IOmnichannelQueue { err: e, }); } finally { - setTimeout(this.execute.bind(this), this.delay()); + this.scheduleExecution(); } } + private scheduleExecution(): void { + if (this.timeoutHandler !== null) { + return; + } + + this.timeoutHandler = setTimeout(() => { + this.timeoutHandler = null; + return this.execute(); + }, this.delay()); + } + async shouldStart() { if (!settings.get('Livechat_enabled')) { void this.stop(); diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index ccfe2026b2ba..e5b21f4aae97 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -33,11 +33,7 @@ export class OmnichannelService extends ServiceClassInternal implements IOmnicha } async started() { - settings.watch('Livechat_enabled', (enabled) => { - void (enabled && RoutingManager.isMethodSet() ? this.queueWorker.shouldStart() : this.queueWorker.stop()); - }); - - settings.watch('Livechat_Routing_Method', async () => { + settings.watchMultiple(['Livechat_enabled', 'Livechat_Routing_Method'], () => { this.queueWorker.shouldStart(); }); diff --git a/apps/meteor/tests/end-to-end/api/direct-message.ts b/apps/meteor/tests/end-to-end/api/direct-message.ts index 3146d351798b..9a6155fd40fe 100644 --- a/apps/meteor/tests/end-to-end/api/direct-message.ts +++ b/apps/meteor/tests/end-to-end/api/direct-message.ts @@ -343,26 +343,117 @@ describe('[Direct Messages]', () => { .end(done); }); - it('/im.counters', (done) => { - void request - .get(api('im.counters')) - .set(credentials) - .query({ - roomId: directMessage._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('joined', true); - expect(res.body).to.have.property('members'); - expect(res.body).to.have.property('unreads'); - expect(res.body).to.have.property('unreadsFrom'); - expect(res.body).to.have.property('msgs'); - expect(res.body).to.have.property('latest'); - expect(res.body).to.have.property('userMentions'); - }) - .end(done); + describe('/im.counters', () => { + it('should require auth', async () => { + await request + .get(api('im.counters')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res) => { + expect(res.body).to.have.property('status', 'error'); + }); + }); + it('should require a roomId', async () => { + await request + .get(api('im.counters')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }); + }); + it('should work with all params right', (done) => { + void request + .get(api('im.counters')) + .set(credentials) + .query({ + roomId: directMessage._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('joined', true); + expect(res.body).to.have.property('members'); + expect(res.body).to.have.property('unreads'); + expect(res.body).to.have.property('unreadsFrom'); + expect(res.body).to.have.property('msgs'); + expect(res.body).to.have.property('latest'); + expect(res.body).to.have.property('userMentions'); + }) + .end(done); + }); + + describe('with valid room id', () => { + let testDM: IRoom & { rid: IRoom['_id'] }; + let user2: TestUser; + let userCreds: Credentials; + + before(async () => { + user2 = await createUser(); + userCreds = await login(user2.username, password); + await request + .post(api('im.create')) + .set(credentials) + .send({ + username: user2.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + testDM = res.body.room; + }); + + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + text: 'Sample message', + rid: testDM._id, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + + after(async () => { + await request + .post(api('im.delete')) + .set(credentials) + .send({ + roomId: testDM._id, + }) + .expect(200); + + await deleteUser(user2); + }); + + it('should properly return counters before opening the dm', async () => { + await request + .get(api('im.counters')) + .set(userCreds) + .query({ + roomId: testDM._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('joined', true); + expect(res.body).to.have.property('members').and.to.be.a('number').and.to.be.eq(2); + expect(res.body).to.have.property('unreads').and.to.be.a('number').and.to.be.eq(1); + expect(res.body).to.have.property('unreadsFrom'); + expect(res.body).to.have.property('msgs').and.to.be.a('number').and.to.be.eq(1); + expect(res.body).to.have.property('latest'); + expect(res.body).to.have.property('userMentions').and.to.be.a('number').and.to.be.eq(0); + }); + }); + }); }); describe('[/im.files]', async () => { diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index a0b3f65ded0c..cae8d7c77d64 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -78,6 +78,7 @@ export { } from './types/IOmnichannelAnalyticsService'; export { getConnection, getTrashCollection } from './lib/mongo'; +export { ServiceStarter } from './lib/ServiceStarter'; export { AutoUpdateRecord, diff --git a/packages/core-services/src/lib/ServiceStarter.ts b/packages/core-services/src/lib/ServiceStarter.ts new file mode 100644 index 000000000000..9c38ea6b07ec --- /dev/null +++ b/packages/core-services/src/lib/ServiceStarter.ts @@ -0,0 +1,68 @@ +// This class is used to manage calls to a service's .start and .stop functions +// Specifically for cases where the start function has different conditions that may cause the service to actually start or not, +// or when the start process can take a while to complete +// Using this class, you ensure that calls to .start and .stop will be chained, so you avoid race conditions +// At the same time, it prevents those functions from running more times than necessary if there are several calls to them (for example when loading setting values) +export class ServiceStarter { + private lock = Promise.resolve(); + + private currentCall?: 'start' | 'stop'; + + private nextCall?: 'start' | 'stop'; + + private starterFn: () => Promise; + + private stopperFn?: () => Promise; + + constructor(starterFn: () => Promise, stopperFn?: () => Promise) { + this.starterFn = starterFn; + this.stopperFn = stopperFn; + } + + private async checkStatus(): Promise { + if (this.nextCall === 'start') { + return this.doCall('start'); + } + + if (this.nextCall === 'stop') { + return this.doCall('stop'); + } + } + + private async doCall(call: 'start' | 'stop'): Promise { + this.nextCall = undefined; + this.currentCall = call; + try { + if (call === 'start') { + await this.starterFn(); + } else if (this.stopperFn) { + await this.stopperFn(); + } + } finally { + this.currentCall = undefined; + await this.checkStatus(); + } + } + + private async call(call: 'start' | 'stop'): Promise { + // If something is already chained to run after the current call, it's okay to replace it with the new call + this.nextCall = call; + if (this.currentCall) { + return this.lock; + } + this.lock = this.checkStatus(); + return this.lock; + } + + async start(): Promise { + return this.call('start'); + } + + async stop(): Promise { + return this.call('stop'); + } + + async wait(): Promise { + return this.lock; + } +} diff --git a/packages/core-services/tests/ServiceStarter.test.ts b/packages/core-services/tests/ServiceStarter.test.ts new file mode 100644 index 000000000000..2c1a20da6115 --- /dev/null +++ b/packages/core-services/tests/ServiceStarter.test.ts @@ -0,0 +1,91 @@ +import { ServiceStarter } from '../src/lib/ServiceStarter'; + +const wait = (time: number) => { + return new Promise((resolve) => { + setTimeout(() => resolve(undefined), time); + }); +}; + +describe('ServiceStarter', () => { + it('should call the starterFn and stopperFn when calling .start and .stop', async () => { + const start = jest.fn(); + const stop = jest.fn(); + + const instance = new ServiceStarter(start, stop); + + expect(start).not.toHaveBeenCalled(); + expect(stop).not.toHaveBeenCalled(); + + await instance.start(); + + expect(start).toHaveBeenCalled(); + expect(stop).not.toHaveBeenCalled(); + + start.mockReset(); + + await instance.stop(); + + expect(start).not.toHaveBeenCalled(); + expect(stop).toHaveBeenCalled(); + }); + + it('should only call .start for the second time after the initial call has finished running', async () => { + let running = false; + const start = jest.fn(async () => { + expect(running).toBe(false); + + running = true; + await wait(100); + running = false; + }); + const stop = jest.fn(); + + const instance = new ServiceStarter(start, stop); + + void instance.start(); + void instance.start(); + + await instance.wait(); + + expect(start).toHaveBeenCalledTimes(2); + expect(stop).not.toHaveBeenCalled(); + }); + + it('should chain up to two calls to .start', async () => { + const start = jest.fn(async () => { + await wait(100); + }); + const stop = jest.fn(); + + const instance = new ServiceStarter(start, stop); + + void instance.start(); + void instance.start(); + void instance.start(); + void instance.start(); + + await instance.wait(); + + expect(start).toHaveBeenCalledTimes(2); + expect(stop).not.toHaveBeenCalled(); + }); + + it('should skip the chained calls to .start if .stop is called', async () => { + const start = jest.fn(async () => { + await wait(100); + }); + const stop = jest.fn(); + + const instance = new ServiceStarter(start, stop); + + void instance.start(); + void instance.start(); + void instance.start(); + void instance.stop(); + + await instance.wait(); + + expect(start).toHaveBeenCalledTimes(1); + expect(stop).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/release-action/src/getMetadata.ts b/packages/release-action/src/getMetadata.ts index 3c677b8e7774..5ca93ed5e8c5 100644 --- a/packages/release-action/src/getMetadata.ts +++ b/packages/release-action/src/getMetadata.ts @@ -1,8 +1,6 @@ import { readFile } from 'fs/promises'; import path from 'path'; -import { getExecOutput } from '@actions/exec'; - import { readPackageJson } from './utils'; export async function getMongoVersion(cwd: string) { @@ -27,14 +25,11 @@ export async function getNodeNpmVersions(cwd: string): Promise<{ node: string; y return packageJson.engines; } -export async function getAppsEngineVersion() { +export async function getAppsEngineVersion(cwd: string) { try { - const result = await getExecOutput('yarn why @rocket.chat/apps-engine --json'); + const result = await readPackageJson(path.join(cwd, 'packages/apps-engine')); - const match = result.stdout.match(/"@rocket\.chat\/meteor@workspace:apps\/meteor".*"@rocket\.chat\/apps\-engine@[^#]+#npm:([^"]+)"/); - if (match) { - return match[1]; - } + return result.version ?? 'Not Available'; } catch (e) { console.error(e); } diff --git a/packages/release-action/src/utils.ts b/packages/release-action/src/utils.ts index 608379fb7c37..ff7dc06318e1 100644 --- a/packages/release-action/src/utils.ts +++ b/packages/release-action/src/utils.ts @@ -109,7 +109,7 @@ Bump ${pkgName} version. export async function getEngineVersionsMd(cwd: string) { const { node } = await getNodeNpmVersions(cwd); - const appsEngine = await getAppsEngineVersion(); + const appsEngine = await getAppsEngineVersion(cwd); const mongo = await getMongoVersion(cwd); return `### Engine versions diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx index e5ab04580314..8b9bda9e4020 100644 --- a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx +++ b/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx @@ -10,6 +10,10 @@ const clickable = css` } `; +const style = css` + background-color: ${Palette.surface['surface-room']}; +`; + export const RoomBanner = ({ onClick, className, ...props }: ComponentProps) => { const { isMobile } = useLayout(); @@ -25,8 +29,7 @@ export const RoomBanner = ({ onClick, className, ...props }: ComponentProps, 'is'>) => ( - + );