From a9564a4176b3355accfa7a1cfab6c0bdb8fe5968 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 8 Jan 2024 09:49:13 -0300 Subject: [PATCH 1/6] chore: remove RedHat stuff (#31388) --- .github/workflows/ci.yml | 15 ---------- apps/meteor/.docker/Dockerfile.rhel | 44 ----------------------------- package.json | 1 - 3 files changed, 60 deletions(-) delete mode 100644 apps/meteor/.docker/Dockerfile.rhel diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdcea4c42133..a8f8d29610bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -724,21 +724,6 @@ jobs: # Makes build fail if the release isn't there curl --fail https://releases.rocket.chat/$RC_VERSION/info - - name: RedHat Registry - if: github.event_name == 'release' - env: - REDHAT_REGISTRY_PID: ${{ secrets.REDHAT_REGISTRY_PID }} - REDHAT_REGISTRY_KEY: ${{ secrets.REDHAT_REGISTRY_KEY }} - run: | - GIT_TAG="${GITHUB_REF#*tags/}" - - curl -X POST \ - https://connect.redhat.com/api/v2/projects/$REDHAT_REGISTRY_PID/build \ - -H "Authorization: Bearer $REDHAT_REGISTRY_KEY" \ - -H 'Cache-Control: no-cache' \ - -H 'Content-Type: application/json' \ - -d '{"tag":"'$GIT_TAG'"}' - trigger-dependent-workflows: runs-on: ubuntu-latest if: github.event_name == 'release' diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel deleted file mode 100644 index dc90b0f88133..000000000000 --- a/apps/meteor/.docker/Dockerfile.rhel +++ /dev/null @@ -1,44 +0,0 @@ -FROM registry.access.redhat.com/ubi8/nodejs-12 - -ENV RC_VERSION 6.6.0-develop - -MAINTAINER buildmaster@rocket.chat - -LABEL name="Rocket.Chat" \ - vendor="Rocket.Chat" \ - version="${RC_VERSION}" \ - release="1" \ - url="https://rocket.chat" \ - summary="The Ultimate Open Source Web Chat Platform" \ - description="The Ultimate Open Source Web Chat Platform" \ - run="docker run -d --name ${NAME} ${IMAGE}" - -USER root -RUN dnf install -y python38 && rm -rf /var/cache /var/log/dnf* /var/log/yum.* -USER default - -RUN set -x \ - && gpg --keyserver keys.openpgp.org --recv-keys 0E163286C20D07B9787EBE9FD7F9D0414FD08104 \ - && curl -SLf "https://releases.rocket.chat/${RC_VERSION}/download" -o rocket.chat.tgz \ - && curl -SLf "https://releases.rocket.chat/${RC_VERSION}/asc" -o rocket.chat.tgz.asc \ - && gpg --verify rocket.chat.tgz.asc \ - && tar -zxf rocket.chat.tgz -C /opt/app-root/src/ \ - && cd /opt/app-root/src/bundle/programs/server \ - && npm install - -COPY licenses /licenses - -VOLUME /opt/app-root/src/uploads - -WORKDIR /opt/app-root/src/bundle - -ENV DEPLOY_METHOD=docker-redhat \ - NODE_ENV=production \ - MONGO_URL=mongodb://mongo:27017/rocketchat \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 - -EXPOSE 3000 - -CMD ["node", "main.js"] diff --git a/package.json b/package.json index c0ffec861cda..b6e3ba26cc0f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "updateFiles": [ "package.json", "apps/meteor/package.json", - "apps/meteor/.docker/Dockerfile.rhel", "apps/meteor/app/utils/rocketchat.info" ] }, From d6165ad77fabac3427cc28c3b12abf3d3f9821bc Mon Sep 17 00:00:00 2001 From: Hardik Bhatia <98163873+hardikbhatia777@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:09:36 +0530 Subject: [PATCH 2/6] fix: Disable quote avatars according to user preference (#31393) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/itchy-zoos-appear.md | 5 +++++ .../message/content/attachments/QuoteAttachment.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-zoos-appear.md diff --git a/.changeset/itchy-zoos-appear.md b/.changeset/itchy-zoos-appear.md new file mode 100644 index 000000000000..6d9ab31eb7c8 --- /dev/null +++ b/.changeset/itchy-zoos-appear.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Fixes an issue where avatars are not being disabled based on preference on quote attachments diff --git a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx index 16f4764fb63c..493e3e9ea918 100644 --- a/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/QuoteAttachment.tsx @@ -1,6 +1,7 @@ import type { MessageQuoteAttachment } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -37,6 +38,7 @@ type QuoteAttachmentProps = { export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElement => { const formatTime = useTimeAgo(); + const displayAvatarPreference = useUserPreference('displayAvatars'); return ( <> @@ -50,7 +52,7 @@ export const QuoteAttachment = ({ attachment }: QuoteAttachmentProps): ReactElem borderInlineStartColor='light' > - + {displayAvatarPreference && } From 7a187dcbaa0f621be5e1c30225342db21bc5b8a6 Mon Sep 17 00:00:00 2001 From: Sayan4444 <112304873+Sayan4444@users.noreply.github.com> Date: Mon, 8 Jan 2024 20:55:07 +0530 Subject: [PATCH 3/6] fix: Dropping a file from another browser window creates two upload dialogs (#31332) Co-authored-by: Hugo Costa <20212776+hugocostadev@users.noreply.github.com> --- .changeset/clean-melons-return.md | 5 +++++ .../room/body/hooks/useFileUploadDropTarget.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/clean-melons-return.md diff --git a/.changeset/clean-melons-return.md b/.changeset/clean-melons-return.md new file mode 100644 index 000000000000..3b521860efbc --- /dev/null +++ b/.changeset/clean-melons-return.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed image dropping from another browser window creates two upload dialogs in some OS and browsers diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 2df567e77fb0..2427f7217401 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -40,7 +40,21 @@ export const useFileUploadDropTarget = (): readonly [ const onFileDrop = useMutableCallback(async (files: File[]) => { const { mime } = await import('../../../../../app/utils/lib/mimeTypes'); - const uploads = Array.from(files).map((file) => { + const getUniqueFiles = () => { + const uniqueFiles: File[] = []; + const st: Set = new Set(); + files.forEach((file) => { + const key = file.size; + if (!st.has(key)) { + uniqueFiles.push(file); + st.add(key); + } + }); + return uniqueFiles; + }; + const uniqueFiles = getUniqueFiles(); + + const uploads = Array.from(uniqueFiles).map((file) => { Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); return file; }); From 2fa8055d067f6e26d1774dd975b2f1a0e5038faf Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Mon, 8 Jan 2024 13:08:46 -0300 Subject: [PATCH 4/6] fix: room avatar UnHandledPromiseRejection (#31389) --- apps/meteor/server/ufs/ufs-methods.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/ufs/ufs-methods.ts b/apps/meteor/server/ufs/ufs-methods.ts index 05228e059292..23a6048fda45 100644 --- a/apps/meteor/server/ufs/ufs-methods.ts +++ b/apps/meteor/server/ufs/ufs-methods.ts @@ -71,7 +71,7 @@ export async function ufsComplete(fileId: string, storeName: string): Promise Date: Mon, 8 Jan 2024 15:25:03 -0300 Subject: [PATCH 5/6] feat: Hide UI elements through window postmessage (#31184) --- .changeset/big-teachers-change.md | 6 + .../app/ui-utils/client/lib/messageBox.ts | 3 +- .../message/toolbox/MessageToolbox.tsx | 11 +- .../client/hooks/useAppActionButtons.ts | 2 +- apps/meteor/client/hooks/useFileInput.ts | 23 ++++ .../client/providers/LayoutProvider.tsx | 23 +++- .../room/Header/RoomToolbox/RoomToolbox.tsx | 2 +- .../room/composer/messageBox/MessageBox.tsx | 10 +- .../ActionsToolbarDropdown.tsx | 96 ++------------- .../MessageBoxActionsToolbar.tsx | 93 ++++++++++----- .../actions/FileUploadAction.tsx | 75 ------------ .../hooks/ToolbarAction.ts | 10 ++ .../useAudioMessageAction.ts} | 39 +++--- .../useCreateDiscussionAction.tsx} | 24 ++-- .../hooks/useFileUploadAction.ts | 52 ++++++++ .../useShareLocationAction.tsx} | 27 +++-- .../hooks/useToolbarActions.ts | 112 ++++++++++++++++++ .../useVideoMessageAction.ts} | 45 ++----- .../useWebdavActions.tsx} | 48 ++++---- .../useUserInfoActions/useUserInfoActions.ts | 6 +- .../room/providers/RoomToolboxProvider.tsx | 5 +- .../page-objects/fragments/home-content.ts | 2 +- packages/ui-contexts/src/LayoutContext.ts | 12 ++ .../src/hooks/useLayoutHiddenActions.ts | 6 + packages/ui-contexts/src/index.ts | 1 + 25 files changed, 420 insertions(+), 313 deletions(-) create mode 100644 .changeset/big-teachers-change.md create mode 100644 apps/meteor/client/hooks/useFileInput.ts delete mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/AudioMessageAction.tsx => hooks/useAudioMessageAction.ts} (68%) rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/CreateDiscussionAction.tsx => hooks/useCreateDiscussionAction.tsx} (68%) create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/ShareLocationAction.tsx => hooks/useShareLocationAction.tsx} (65%) create mode 100644 apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/VideoMessageAction.tsx => hooks/useVideoMessageAction.ts} (63%) rename apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/{actions/WebdavAction.tsx => hooks/useWebdavActions.tsx} (60%) create mode 100644 packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts diff --git a/.changeset/big-teachers-change.md b/.changeset/big-teachers-change.md new file mode 100644 index 000000000000..ec8980779031 --- /dev/null +++ b/.changeset/big-teachers-change.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/ui-contexts": minor +--- + +Add the possibility to hide some elements through postMessage events. diff --git a/apps/meteor/app/ui-utils/client/lib/messageBox.ts b/apps/meteor/app/ui-utils/client/lib/messageBox.ts index 3f3c545af57e..3418adef1c1c 100644 --- a/apps/meteor/app/ui-utils/client/lib/messageBox.ts +++ b/apps/meteor/app/ui-utils/client/lib/messageBox.ts @@ -1,4 +1,5 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { Keys as IconName } from '@rocket.chat/icons'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ChatAPI } from '../../../../client/lib/chats/ChatAPI'; @@ -6,7 +7,7 @@ import type { ChatAPI } from '../../../../client/lib/chats/ChatAPI'; export type MessageBoxAction = { label: TranslationKey; id: string; - icon?: string; + icon: IconName; action: (params: { rid: IRoom['_id']; tmid?: IMessage['_id']; event: Event; chat: ChatAPI }) => void; condition?: () => boolean; }; diff --git a/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx b/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx index 3b9cdd84c25d..d0c426dcc466 100644 --- a/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx +++ b/apps/meteor/client/components/message/toolbox/MessageToolbox.tsx @@ -2,7 +2,7 @@ import type { IMessage, IRoom, ISubscription, ITranslatedMessage } from '@rocket import { isThreadMessage, isRoomFederated, isVideoConfMessage } from '@rocket.chat/core-typings'; import { MessageToolbox as FuselageMessageToolbox, MessageToolboxItem } from '@rocket.chat/fuselage'; import { useFeaturePreview } from '@rocket.chat/ui-client'; -import { useUser, useSettings, useTranslation, useMethod } from '@rocket.chat/ui-contexts'; +import { useUser, useSettings, useTranslation, useMethod, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; @@ -70,13 +70,18 @@ const MessageToolbox = ({ const actionButtonApps = useMessageActionAppsActionButtons(context); + const { messageToolbox: hiddenActions } = useLayoutHiddenActions(); + const actionsQueryResult = useQuery(['rooms', room._id, 'messages', message._id, 'actions'] as const, async () => { const props = { message, room, user, subscription, settings: mapSettings, chat }; const toolboxItems = await MessageAction.getAll(props, context, 'message'); const menuItems = await MessageAction.getAll(props, context, 'menu'); - return { message: toolboxItems, menu: menuItems }; + return { + message: toolboxItems.filter((action) => !hiddenActions.includes(action.id)), + menu: menuItems.filter((action) => !hiddenActions.includes(action.id)), + }; }); const toolbox = useRoomToolbox(); @@ -85,7 +90,7 @@ const MessageToolbox = ({ const autoTranslateOptions = useAutoTranslate(subscription); - if (selecting) { + if (selecting || (!actionsQueryResult.data?.message.length && !actionsQueryResult.data?.menu.length)) { return null; } diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 5647ca36656e..5ee20f7772bf 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -76,7 +76,7 @@ export const useMessageboxAppsActionButtons = () => { return applyButtonFilters(action); }) .map((action) => { - const item: MessageBoxAction = { + const item: Omit = { id: getIdForActionButton(action), label: Utilities.getI18nKeyForApp(action.labelI18n, action.appId), action: (params) => { diff --git a/apps/meteor/client/hooks/useFileInput.ts b/apps/meteor/client/hooks/useFileInput.ts new file mode 100644 index 000000000000..c9662b820d8f --- /dev/null +++ b/apps/meteor/client/hooks/useFileInput.ts @@ -0,0 +1,23 @@ +import { useRef, useEffect } from 'react'; +import type { AllHTMLAttributes } from 'react'; + +export const useFileInput = (props: AllHTMLAttributes) => { + const ref = useRef(); + + useEffect(() => { + const fileInput = document.createElement('input'); + fileInput.setAttribute('style', 'display: none;'); + Object.entries(props).forEach(([key, value]) => { + fileInput.setAttribute(key, value); + }); + document.body.appendChild(fileInput); + ref.current = fileInput; + + return (): void => { + ref.current = undefined; + fileInput.remove(); + }; + }, [props]); + + return ref; +}; diff --git a/apps/meteor/client/providers/LayoutProvider.tsx b/apps/meteor/client/providers/LayoutProvider.tsx index 5cc113e172c5..a4f8fa84f9ff 100644 --- a/apps/meteor/client/providers/LayoutProvider.tsx +++ b/apps/meteor/client/providers/LayoutProvider.tsx @@ -3,10 +3,18 @@ import { LayoutContext, useRouter, useSetting } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React, { useMemo, useState, useEffect } from 'react'; +const hiddenActionsDefaultValue = { + roomToolbox: [], + messageToolbox: [], + composerToolbox: [], + userToolbox: [], +}; + const LayoutProvider: FC = ({ children }) => { const showTopNavbarEmbeddedLayout = Boolean(useSetting('UI_Show_top_navbar_embedded_layout')); const [isCollapsed, setIsCollapsed] = useState(false); const breakpoints = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"] + const [hiddenActions, setHiddenActions] = useState(hiddenActionsDefaultValue); const router = useRouter(); // Once the layout is embedded, it can't be changed @@ -18,6 +26,18 @@ const LayoutProvider: FC = ({ children }) => { setIsCollapsed(isMobile); }, [isMobile]); + useEffect(() => { + const eventHandler = (event: MessageEvent) => { + if (event.data?.event !== 'overrideUi') { + return; + } + + setHiddenActions({ ...hiddenActionsDefaultValue, ...event.data.hideActions }); + }; + window.addEventListener('message', eventHandler); + return () => window.removeEventListener('message', eventHandler); + }, []); + return ( { contextualBarExpanded: breakpoints.includes('sm'), // eslint-disable-next-line no-nested-ternary contextualBarPosition: breakpoints.includes('sm') ? (breakpoints.includes('lg') ? 'relative' : 'absolute') : 'fixed', + hiddenActions, }), - [isMobile, isEmbedded, showTopNavbarEmbeddedLayout, isCollapsed, breakpoints, router], + [isMobile, isEmbedded, showTopNavbarEmbeddedLayout, isCollapsed, breakpoints, router, hiddenActions], )} /> ); diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index 00c9d9cdeecd..17f80a490064 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -91,7 +91,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {(normalActions.length > 6 || !roomToolboxExpanded) && ( + {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index 330fdbb8771d..25bc84e2a64e 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; import { Button, Tag, Box } from '@rocket.chat/fuselage'; import { useContentBoxSize, useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -410,15 +411,14 @@ const MessageBox = ({ disabled={isRecording || !canSend} /> )} - diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx index 5066ecb192e1..8da907c99c35 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/ActionsToolbarDropdown.tsx @@ -1,106 +1,26 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { Dropdown, IconButton, Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; -import { useTranslation, useUserRoom } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, ReactNode } from 'react'; -import React, { useRef, Fragment } from 'react'; +import { Dropdown, IconButton } from '@rocket.chat/fuselage'; +import type { ReactNode } from 'react'; +import React, { useRef } from 'react'; -import { messageBox } from '../../../../../../app/ui-utils/client'; -import { useMessageboxAppsActionButtons } from '../../../../../hooks/useAppActionButtons'; -import type { ChatAPI } from '../../../../../lib/chats/ChatAPI'; import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; -import { useChat } from '../../../contexts/ChatContext'; -import CreateDiscussionAction from './actions/CreateDiscussionAction'; -import ShareLocationAction from './actions/ShareLocationAction'; -import WebdavAction from './actions/WebdavAction'; type ActionsToolbarDropdownProps = { - chatContext?: ChatAPI; - rid: IRoom['_id']; - isRecording?: boolean; - tmid?: string; - actions?: ReactNode[]; + disabled?: boolean; + children: () => ReactNode[]; }; -const ActionsToolbarDropdown = ({ isRecording, rid, tmid, actions, ...props }: ActionsToolbarDropdownProps) => { - const chatContext = useChat(); - - if (!chatContext) { - throw new Error('useChat must be used within a ChatProvider'); - } - - const t = useTranslation(); +const ActionsToolbarDropdown = ({ children, ...props }: ActionsToolbarDropdownProps) => { const reference = useRef(null); const target = useRef(null); - const room = useUserRoom(rid); - const { isVisible, toggle } = useDropdownVisibility({ reference, target }); - const apps = useMessageboxAppsActionButtons(); - - const groups = { - ...(apps.isSuccess && - apps.data.length > 0 && { - Apps: apps.data, - }), - ...messageBox.actions.get(), - }; - - const messageBoxActions = Object.entries(groups).map(([name, group]) => { - const items = group.map((item) => ({ - icon: item.icon, - name: t(item.label), - type: 'messagebox-action', - id: item.id, - action: item.action, - })); - - return { - title: t.has(name) && t(name), - items, - }; - }); - return ( <> - toggle()} - {...props} - /> + toggle()} {...props} /> {isVisible && ( - {t('Create_new')} - {room && } - {actions} - - {room && } - {messageBoxActions?.map((actionGroup, index) => ( - - {actionGroup.title} - {actionGroup.items.map((item) => ( - - ))} - - ))} + {children()} )} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index d83b134e67af..ab0c9ec1fd5d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,52 +1,91 @@ import type { IRoom, IMessage } from '@rocket.chat/core-typings'; +import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; +import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; import React, { memo } from 'react'; +import { useChat } from '../../../contexts/ChatContext'; import ActionsToolbarDropdown from './ActionsToolbarDropdown'; -import AudioMessageAction from './actions/AudioMessageAction'; -import FileUploadAction from './actions/FileUploadAction'; -import VideoMessageAction from './actions/VideoMessageAction'; +import { useToolbarActions } from './hooks/useToolbarActions'; type MessageBoxActionsToolbarProps = { + canSend: boolean; + typing: boolean; + isMicrophoneDenied: boolean; variant: 'small' | 'large'; isRecording: boolean; - typing: boolean; - canSend: boolean; rid: IRoom['_id']; tmid?: IMessage['_id']; - isMicrophoneDenied?: boolean; }; const MessageBoxActionsToolbar = ({ - variant = 'large', - isRecording, - typing, canSend, + typing, + isRecording, rid, tmid, + variant = 'large', isMicrophoneDenied, - ...props }: MessageBoxActionsToolbarProps) => { - const actions = [ - , - , - , - ]; - - let featuredAction; - if (variant === 'small') { - featuredAction = actions.splice(1, 1); + const data = useToolbarActions({ + canSend, + typing, + isRecording, + isMicrophoneDenied: Boolean(isMicrophoneDenied), + rid, + tmid, + variant, + }); + + const { featured, menu } = data; + const t = useTranslation(); + const chatContext = useChat(); + + if (!chatContext) { + throw new Error('useChat must be used within a ChatProvider'); + } + + if (!featured.length && !menu.length) { + return null; } return ( <> - {variant !== 'small' && actions} - {variant === 'small' && featuredAction} - + + {featured.map((action) => ( + + ))} + {menu.length > 0 && ( + + {() => + menu.map((option) => { + if (typeof option === 'string') { + return {t.has(option) ? t(option) : option}; + } + + return ( + + ); + }) + } + + )} ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx deleted file mode 100644 index f9c826fceb4b..000000000000 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Option, OptionContent, OptionIcon } from '@rocket.chat/fuselage'; -import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; -import type { ChangeEvent, AllHTMLAttributes } from 'react'; -import React, { useRef } from 'react'; - -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; -import { useChat } from '../../../../contexts/ChatContext'; - -type FileUploadActionProps = { - collapsed?: boolean; - chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React -} & Omit, 'is'>; - -const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUploadActionProps) => { - const t = useTranslation(); - const fileUploadEnabled = useSetting('FileUpload_Enabled'); - const fileInputRef = useRef(null); - const chat = useChat() ?? chatContext; - - const resetFileInput = () => { - if (!fileInputRef.current) { - return; - } - - fileInputRef.current.value = ''; - }; - - const handleUploadChange = async (e: ChangeEvent) => { - const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); - const filesToUpload = Array.from(e.target.files ?? []).map((file) => { - Object.defineProperty(file, 'type', { - value: mime.lookup(file.name), - }); - return file; - }); - chat?.flows.uploadFiles(filesToUpload, resetFileInput); - }; - - const handleUpload = () => { - fileInputRef.current?.click(); - }; - - if (collapsed) { - return ( - <> - - - - ); - } - - return ( - <> - - - - ); -}; - -export default FileUploadAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts new file mode 100644 index 000000000000..63f8ded271f5 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/ToolbarAction.ts @@ -0,0 +1,10 @@ +import type { Keys as IconName } from '@rocket.chat/icons'; + +export type ToolbarAction = { + title?: string; + disabled?: boolean; + onClick: (...params: any) => unknown; + icon: IconName; + label: string; + id: string; +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts similarity index 68% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts index 41bee06c19c0..87ece1793299 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/AudioMessageAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts @@ -1,28 +1,22 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { MessageComposerAction } from '@rocket.chat/ui-composer'; -import { useSetting } from '@rocket.chat/ui-contexts'; -import type { AllHTMLAttributes } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEffect, useMemo } from 'react'; import { AudioRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/AudioRecorder'; -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; +import type { ToolbarAction } from './ToolbarAction'; const audioRecorder = new AudioRecorder(); -type AudioMessageActionProps = { - chatContext?: ChatAPI; - isMicrophoneDenied?: boolean; -} & Omit, 'is'>; - -const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...props }: AudioMessageActionProps) => { +export const useAudioMessageAction = (disabled: boolean, isMicrophoneDenied: boolean): ToolbarAction => { const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean; const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string; const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string; const [isPermissionDenied] = useMediaPermissions('microphone'); + const t = useTranslation(); const isAllowed = useMemo( () => @@ -39,7 +33,7 @@ const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...prop const getMediaActionTitle = useMediaActionTitle('audio', isPermissionDenied, isFileUploadEnabled, isAudioRecorderEnabled, isAllowed); - const chat = useChat() ?? chatContext; + const chat = useChat(); const stopRecording = useMutableCallback(() => { chat?.action.stop('recording'); @@ -61,17 +55,12 @@ const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...prop const handleRecordButtonClick = () => chat?.composer?.setRecordingMode(true); - return ( - - ); + return { + id: 'audio-message', + title: getMediaActionTitle, + disabled: !isAllowed || Boolean(disabled), + onClick: handleRecordButtonClick, + icon: 'mic', + label: t('Audio_message'), + }; }; - -export default AudioMessageAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx similarity index 68% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx index 419c6c2cfdda..9b85a8a7a6c3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/CreateDiscussionAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx @@ -1,12 +1,16 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useTranslation, useSetting, usePermission, useSetModal } from '@rocket.chat/ui-contexts'; import React from 'react'; import CreateDiscussion from '../../../../../../components/CreateDiscussion'; +import type { ToolbarAction } from './ToolbarAction'; + +export const useCreateDiscussionAction = (room?: IRoom): ToolbarAction => { + if (!room) { + throw new Error('Invalid room'); + } -const CreateDiscussionAction = ({ room }: { room: IRoom }) => { const setModal = useSetModal(); const t = useTranslation(); @@ -19,12 +23,12 @@ const CreateDiscussionAction = ({ room }: { room: IRoom }) => { const allowDiscussion = room && discussionEnabled && !isRoomFederated(room) && (canStartDiscussion || canSstartDiscussionOtherUser); - return ( - - ); + return { + id: 'create-discussion', + title: !allowDiscussion ? t('Not_Available') : undefined, + disabled: !allowDiscussion, + onClick: handleCreateDiscussion, + icon: 'discussion', + label: t('Discussion'), + }; }; - -export default CreateDiscussionAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts new file mode 100644 index 000000000000..8794aa687b28 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -0,0 +1,52 @@ +import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { useFileInput } from '../../../../../../hooks/useFileInput'; +import { useChat } from '../../../../contexts/ChatContext'; +import type { ToolbarAction } from './ToolbarAction'; + +const fileInputProps = { type: 'file', multiple: true }; + +export const useFileUploadAction = (disabled: boolean): ToolbarAction => { + const t = useTranslation(); + const fileUploadEnabled = useSetting('FileUpload_Enabled'); + const fileInputRef = useFileInput(fileInputProps); + const chat = useChat(); + + useEffect(() => { + const resetFileInput = () => { + if (!fileInputRef?.current) { + return; + } + + fileInputRef.current.value = ''; + }; + + const handleUploadChange = async () => { + const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); + const filesToUpload = Array.from(fileInputRef?.current?.files ?? []).map((file) => { + Object.defineProperty(file, 'type', { + value: mime.lookup(file.name), + }); + return file; + }); + chat?.flows.uploadFiles(filesToUpload, resetFileInput); + }; + + fileInputRef.current?.addEventListener('change', handleUploadChange); + return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); + }, [chat, fileInputRef]); + + const handleUpload = () => { + fileInputRef?.current?.click(); + }; + + return { + id: 'file-upload', + icon: 'clip', + label: t('File'), + title: t('File'), + onClick: handleUpload, + disabled: !fileUploadEnabled || disabled, + }; +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/ShareLocationAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx similarity index 65% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/ShareLocationAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx index 3589406108f9..4a30e3b2b646 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/ShareLocationAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx @@ -1,12 +1,16 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useSetting, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import ShareLocationModal from '../../../../ShareLocation/ShareLocationModal'; +import type { ToolbarAction } from './ToolbarAction'; + +export const useShareLocationAction = (room?: IRoom, tmid?: string): ToolbarAction => { + if (!room) { + throw new Error('Invalid room'); + } -const ShareLocationAction = ({ room, tmid }: { room: IRoom; tmid?: string }) => { const t = useTranslation(); const setModal = useSetModal(); @@ -19,15 +23,12 @@ const ShareLocationAction = ({ room, tmid }: { room: IRoom; tmid?: string }) => const allowGeolocation = room && canGetGeolocation && !isRoomFederated(room); - return ( - <> - {t('Share')} - - - ); + return { + id: 'share-location', + icon: 'map-pin', + label: t('Location'), + title: !allowGeolocation ? t('Not_Available') : undefined, + onClick: handleShareLocation, + disabled: !allowGeolocation, + }; }; - -export default ShareLocationAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts new file mode 100644 index 000000000000..a98d2e885671 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useToolbarActions.ts @@ -0,0 +1,112 @@ +import { useUserRoom, useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; + +import { messageBox } from '../../../../../../../app/ui-utils/client'; +import { isTruthy } from '../../../../../../../lib/isTruthy'; +import { useMessageboxAppsActionButtons } from '../../../../../../hooks/useAppActionButtons'; +import type { ToolbarAction } from './ToolbarAction'; +import { useAudioMessageAction } from './useAudioMessageAction'; +import { useCreateDiscussionAction } from './useCreateDiscussionAction'; +import { useFileUploadAction } from './useFileUploadAction'; +import { useShareLocationAction } from './useShareLocationAction'; +import { useVideoMessageAction } from './useVideoMessageAction'; +import { useWebdavActions } from './useWebdavActions'; + +type ToolbarActionsOptions = { + variant: 'small' | 'large'; + canSend: boolean; + typing: boolean; + isRecording: boolean; + isMicrophoneDenied: boolean; + rid: string; + tmid?: string; +}; + +const isHidden = (hiddenActions: Array, action: ToolbarAction) => { + if (!action) { + return true; + } + return hiddenActions.includes(action.id); +}; + +export const useToolbarActions = ({ canSend, typing, isRecording, isMicrophoneDenied, rid, tmid, variant }: ToolbarActionsOptions) => { + const room = useUserRoom(rid); + const t = useTranslation(); + + const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); + const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); + const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); + const webdavActions = useWebdavActions(); + const createDiscussionAction = useCreateDiscussionAction(room); + const shareLocationAction = useShareLocationAction(room, tmid); + + const apps = useMessageboxAppsActionButtons(); + const { composerToolbox: hiddenActions } = useLayoutHiddenActions(); + + const allActions = { + ...(!isHidden(hiddenActions, videoMessageAction) && { videoMessageAction }), + ...(!isHidden(hiddenActions, audioMessageAction) && { audioMessageAction }), + ...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }), + ...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }), + ...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }), + ...(!hiddenActions.includes('webdav-add') && { webdavActions }), + }; + + const data: { featured: ToolbarAction[]; menu: Array } = (() => { + const featured: Array = []; + const createNew = []; + const share = []; + + if (variant === 'small') { + featured.push(allActions.audioMessageAction); + createNew.push(allActions.videoMessageAction, allActions.fileUploadAction); + } else { + featured.push(allActions.videoMessageAction, allActions.audioMessageAction, allActions.fileUploadAction); + } + + if (allActions.webdavActions) { + createNew.push(...allActions.webdavActions); + } + + share.push(allActions.shareLocationAction); + + const groups = { + ...(apps.isSuccess && + apps.data.length > 0 && { + Apps: apps.data, + }), + ...messageBox.actions.get(), + }; + + const messageBoxActions = Object.entries(groups).reduce>((acc, [name, group]) => { + const items = group + .filter((item) => !hiddenActions.includes(item.id)) + .map( + (item): ToolbarAction => ({ + id: item.id, + icon: item.icon, + label: t(item.label), + onClick: item.action, + }), + ); + + if (items.length === 0) { + return acc; + } + return [...acc, (t.has(name) && t(name)) || name, ...items]; + }, []); + + const createNewFiltered = createNew.filter(isTruthy); + const shareFiltered = share.filter(isTruthy); + + return { + featured: featured.filter(isTruthy), + menu: [ + ...(createNewFiltered.length > 0 ? ['Create_new', ...createNewFiltered] : []), + ...(shareFiltered.length > 0 ? ['Share', ...shareFiltered] : []), + ...messageBoxActions, + ], + }; + })(); + + return data; +}; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/VideoMessageAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts similarity index 63% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/VideoMessageAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts index fa6fa2484c9f..7068f1338b11 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/VideoMessageAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts @@ -1,22 +1,14 @@ -import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { MessageComposerAction } from '@rocket.chat/ui-composer'; import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; -import type { AllHTMLAttributes } from 'react'; -import React, { useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { VideoRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/videoRecorder'; -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; +import type { ToolbarAction } from './ToolbarAction'; -type VideoMessageActionProps = { - collapsed?: boolean; - chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React -} & Omit, 'is'>; - -const VideoMessageAction = ({ collapsed, chatContext, disabled, ...props }: VideoMessageActionProps) => { +export const useVideoMessageAction = (disabled: boolean): ToolbarAction => { const t = useTranslation(); const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean; const isVideoRecorderEnabled = useSetting('Message_VideoRecorderEnabled') as boolean; @@ -41,7 +33,7 @@ const VideoMessageAction = ({ collapsed, chatContext, disabled, ...props }: Vide const getMediaActionTitle = useMediaActionTitle('video', isPermissionDenied, isFileUploadEnabled, isVideoRecorderEnabled, isAllowed); - const chat = useChat() ?? chatContext; + const chat = useChat(); const handleOpenVideoMessage = () => { if (!chat?.composer?.recordingVideo.get()) { @@ -61,25 +53,12 @@ const VideoMessageAction = ({ collapsed, chatContext, disabled, ...props }: Vide handleDenyVideo(isPermissionDenied); }, [handleDenyVideo, isPermissionDenied]); - if (collapsed) { - return ( - - ); - } - - return ( - - ); + return { + id: 'video-message', + title: getMediaActionTitle, + disabled: !isAllowed || Boolean(disabled), + onClick: handleOpenVideoMessage, + icon: 'video', + label: t('Video_message'), + }; }; - -export default VideoMessageAction; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/WebdavAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx similarity index 60% rename from apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/WebdavAction.tsx rename to apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx index 333c1e4968f3..c60d4d533f75 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/WebdavAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx @@ -1,18 +1,17 @@ import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; -import { Option, OptionIcon, OptionContent } from '@rocket.chat/fuselage'; import { useTranslation, useSetting, useSetModal } from '@rocket.chat/ui-contexts'; import React from 'react'; import { WebdavAccounts } from '../../../../../../../app/models/client'; import { useReactiveValue } from '../../../../../../hooks/useReactiveValue'; -import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; import { useChat } from '../../../../contexts/ChatContext'; import AddWebdavAccountModal from '../../../../webdav/AddWebdavAccountModal'; import WebdavFilePickerModal from '../../../../webdav/WebdavFilePickerModal'; +import type { ToolbarAction } from './ToolbarAction'; const getWebdavAccounts = (): IWebdavAccountIntegration[] => WebdavAccounts.find().fetch(); -const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { +export const useWebdavActions = (): Array => { const t = useTranslation(); const setModal = useSetModal(); const webDavAccounts = useReactiveValue(getWebdavAccounts); @@ -21,7 +20,7 @@ const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { const handleCreateWebDav = () => setModal( setModal(null)} onConfirm={() => setModal(null)} />); - const chat = useChat() ?? chatContext; + const chat = useChat(); const handleUpload = async (file: File, description?: string) => chat?.uploads.send(file, { @@ -31,26 +30,23 @@ const WebdavAction = ({ chatContext }: { chatContext?: ChatAPI }) => { const handleOpenWebdav = (account: IWebdavAccountIntegration) => setModal( setModal(null)} />); - return ( - <> - - {webDavEnabled && - webDavAccounts.length > 0 && - webDavAccounts.map((account) => ( - - ))} - - ); + return [ + { + id: 'webdav-add', + title: !webDavEnabled ? t('WebDAV_Integration_Not_Allowed') : undefined, + disabled: !webDavEnabled, + onClick: handleCreateWebDav, + icon: 'cloud-plus', + label: t('Add_Server'), + }, + ...(webDavEnabled && webDavAccounts.length > 0 + ? webDavAccounts.map((account) => ({ + id: account._id, + disabled: false, + onClick: () => handleOpenWebdav(account), + icon: 'cloud-plus' as const, + label: account.name, + })) + : []), + ]; }; - -export default WebdavAction; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index dfe9c0341e00..a058fb862ad5 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -1,5 +1,6 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import type { Icon } from '@rocket.chat/fuselage'; +import { useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { useMemo } from 'react'; @@ -51,6 +52,7 @@ export const useUserInfoActions = ( const call = useCallAction(user); const reportUserOption = useReportUser(user); const isLayoutEmbedded = useEmbeddedLayout(); + const { userToolbox: hiddenActions } = useLayoutHiddenActions(); const userinfoActions = useMemo( () => ({ @@ -83,7 +85,7 @@ export const useUserInfoActions = ( ); const actionSpread = useMemo(() => { - const entries = Object.entries(userinfoActions); + const entries = Object.entries(userinfoActions).filter(([key]) => !hiddenActions.includes(key)); const options = entries.slice(0, size); const slicedOptions = entries.slice(size, entries.length); @@ -105,7 +107,7 @@ export const useUserInfoActions = ( }, [] as UserMenuAction); return { actions: options, menuActions }; - }, [size, userinfoActions]); + }, [size, userinfoActions, hiddenActions]); return actionSpread; }; diff --git a/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx b/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx index c05885f5ab4d..2071a67bd7c3 100644 --- a/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomToolboxProvider.tsx @@ -1,6 +1,6 @@ import type { RoomType, IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback, useStableArray } from '@rocket.chat/fuselage-hooks'; -import { useUserId, useSetting, useRouter, useRouteParameter } from '@rocket.chat/ui-contexts'; +import { useUserId, useSetting, useRouter, useRouteParameter, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React, { useMemo } from 'react'; @@ -87,10 +87,13 @@ const RoomToolboxProvider = ({ children }: RoomToolboxProviderProps) => { const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead', false); const uid = useUserId(); + const { roomToolbox: hiddenActions } = useLayoutHiddenActions(); + const actions = useStableArray( [...coreRoomActions, ...appsRoomActions] .filter((action) => uid || (allowAnonymousRead && 'anonymous' in action && action.anonymous)) .filter((action) => !action.groups || action.groups.includes(getGroup(room))) + .filter((action) => !hiddenActions.includes(action.id)) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), ); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 434fe6ba95eb..79c9617355e9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -149,7 +149,7 @@ export class HomeContent { } get btnRecordAudio(): Locator { - return this.page.locator('[data-qa-id="audio-record"]'); + return this.page.locator('[data-qa-id="audio-message"]'); } get btnMenuMoreActions() { diff --git a/packages/ui-contexts/src/LayoutContext.ts b/packages/ui-contexts/src/LayoutContext.ts index 694f55cffe38..2d900b5a7612 100644 --- a/packages/ui-contexts/src/LayoutContext.ts +++ b/packages/ui-contexts/src/LayoutContext.ts @@ -20,6 +20,12 @@ export type LayoutContextValue = { size: SizeLayout; contextualBarExpanded: boolean; contextualBarPosition: 'absolute' | 'relative' | 'fixed'; + hiddenActions: { + roomToolbox: Array; + messageToolbox: Array; + composerToolbox: Array; + userToolbox: Array; + }; }; export const LayoutContext = createContext({ @@ -40,4 +46,10 @@ export const LayoutContext = createContext({ }, contextualBarPosition: 'relative', contextualBarExpanded: false, + hiddenActions: { + roomToolbox: [], + messageToolbox: [], + composerToolbox: [], + userToolbox: [], + }, }); diff --git a/packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts b/packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts new file mode 100644 index 000000000000..d578f02e9bc9 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useLayoutHiddenActions.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; + +import type { LayoutContextValue } from '../LayoutContext'; +import { LayoutContext } from '../LayoutContext'; + +export const useLayoutHiddenActions = (): LayoutContextValue['hiddenActions'] => useContext(LayoutContext).hiddenActions; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 6e7b31d8eaf3..fb2f2b84d377 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -38,6 +38,7 @@ export { useLayout } from './hooks/useLayout'; export { useLayoutContextualBarExpanded } from './hooks/useLayoutContextualBarExpanded'; export { useLayoutContextualBarPosition } from './hooks/useLayoutContextualBarPosition'; export { useLayoutSizes } from './hooks/useLayoutSizes'; +export { useLayoutHiddenActions } from './hooks/useLayoutHiddenActions'; export { useLoadLanguage } from './hooks/useLoadLanguage'; export { useLoginWithPassword } from './hooks/useLoginWithPassword'; export { useLoginServices } from './hooks/useLoginServices'; From 86d75ce221b3699163d717c481d2ef5e6b427076 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:07:27 -0300 Subject: [PATCH 6/6] refactor: Subscribe to only one stream at a time (#31345) --- apps/meteor/app/utils/client/lib/SDKClient.ts | 196 +++++++++++------- .../client/providers/ServerProvider.tsx | 62 +----- 2 files changed, 134 insertions(+), 124 deletions(-) diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index e9e20bbe658b..18ff309970df 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -45,114 +45,166 @@ const isChangedCollectionPayload = ( return true; }; -export const createSDK = (rest: RestClientInterface) => { - const ev = new Emitter(); +type EventMap = StreamKeys> = { + [key in `stream-${N}/${K}`]: StreamerCallbackArgs; +}; + +type StreamMapValue = { + stop: () => void; + onChange: ReturnType['onChange']; + ready: () => Promise; + isReady: boolean; + unsubList: Set<() => void>; +}; + +const createNewMeteorStream = (streamName: StreamNames, key: StreamKeys, args: unknown[]): StreamMapValue => { + const ee = new Emitter(); + const meta = { + ready: false, + }; + const sub = Meteor.connection.subscribe( + `stream-${streamName}`, + key, + { useCollection: false, args }, + { + onReady: (args: any) => { + meta.ready = true; + ee.emit('ready', [undefined, args]); + }, + onError: (err: any) => { + console.error(err); + ee.emit('ready', [err]); + }, + }, + ); + + const onChange: ReturnType['onChange'] = (cb) => { + if (meta.ready) { + cb({ + msg: 'ready', + + subs: [], + }); + return; + } + ee.once('ready', ([error, result]) => { + if (error) { + cb({ + msg: 'nosub', + + id: '', + error, + }); + return; + } - const streams = new Map void>(); + cb(result); + }); + }; + + const ready = () => { + if (meta.ready) { + return Promise.resolve(); + } + return new Promise((r) => { + ee.once('ready', r); + }); + }; + + return { + stop: sub.stop, + onChange, + ready, + get isReady() { + return meta.ready; + }, + unsubList: new Set(), + }; +}; + +const createStreamManager = () => { + // Emitter that replicates stream messages to registered callbacks + const streamProxy = new Emitter(); + + // Collection of unsubscribe callbacks for each stream. + // const proxyUnsubLists = new Map void>>(); + + const streams = new Map(); Meteor.connection._stream.on('message', (rawMsg: string) => { const msg = DDPCommon.parseDDP(rawMsg); if (!isChangedCollectionPayload(msg)) { return; } - ev.emit(`${msg.collection}/${msg.fields.eventName}`, msg.fields.args); + streamProxy.emit(`${msg.collection}/${msg.fields.eventName}` as any, msg.fields.args as any); }); const stream: SDK['stream'] = >( name: N, data: [key: K, ...args: unknown[]], - cb: (...args: StreamerCallbackArgs) => void, + callback: (...args: StreamerCallbackArgs) => void, + _options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, ): ReturnType => { const [key, ...args] = data; - const streamName = `stream-${name}`; - const streamKey = `${streamName}/${key}`; - - const ee = new Emitter(); + const eventLiteral = `stream-${name}/${key}` as const; - const meta = { - ready: false, + const proxyCallback = (args?: unknown): void => { + if (!args || !Array.isArray(args)) { + throw new Error('Invalid streamer callback'); + } + callback(...(args as StreamerCallbackArgs)); }; - const sub = Meteor.connection.subscribe( - streamName, - key, - { useCollection: false, args }, - { - onReady: (args: any) => { - meta.ready = true; - ee.emit('ready', [undefined, args]); - }, - onError: (err: any) => { - console.error(err); - ee.emit('ready', [err]); - }, - }, - ); + streamProxy.on(eventLiteral, proxyCallback); - const onChange: ReturnType['onChange'] = (cb) => { - if (meta.ready) { - cb({ - msg: 'ready', + const stop = (): void => { + streamProxy.off(eventLiteral, proxyCallback); - subs: [], - }); + // If someone is still listening, don't unsubscribe + if (streamProxy.has(eventLiteral)) { return; } - ee.once('ready', ([error, result]) => { - if (error) { - cb({ - msg: 'nosub', - - id: '', - error, - }); - return; - } - - cb(result); - }); - }; - const ready = () => { - if (meta.ready) { - return Promise.resolve(); + if (stream) { + stream.stop(); + streams.delete(eventLiteral); } - return new Promise((r) => { - ee.once('ready', r); - }); - }; - - const removeEv = ev.on(`${streamKey}`, (args) => cb(...args)); - - const stop = () => { - streams.delete(`${streamKey}`); - sub.stop(); - removeEv(); }; - streams.set(`${streamKey}`, stop); + const stream = streams.get(eventLiteral) || createNewMeteorStream(name, key, args); + stream.unsubList.add(stop); + if (!streams.has(eventLiteral)) { + streams.set(eventLiteral, stream); + } return { id: '', name, params: data as any, stop, - ready, - onChange, - get isReady() { - return meta.ready; - }, + ready: stream.ready, + onChange: stream.onChange, + isReady: stream.isReady, }; }; - const stop = (name: string, key: string) => { - const streamKey = `stream-${name}/${key}`; - const stop = streams.get(streamKey); - if (stop) { - stop(); + const stopAll = (streamName: string, key: string) => { + const stream = streams.get(`stream-${streamName}/${key}`); + + if (stream) { + stream.unsubList.forEach((stop) => stop()); } }; + return { stream, stopAll }; +}; + +export const createSDK = (rest: RestClientInterface) => { + const { stream, stopAll } = createStreamManager(); + const publish = (name: string, args: unknown[]) => { Meteor.call(`stream-${name}`, ...args); }; @@ -163,7 +215,7 @@ export const createSDK = (rest: RestClientInterface) => { return { rest, - stop, + stop: stopAll, stream, publish, call, diff --git a/apps/meteor/client/providers/ServerProvider.tsx b/apps/meteor/client/providers/ServerProvider.tsx index 8fab8415849d..8eb5e2e37b6b 100644 --- a/apps/meteor/client/providers/ServerProvider.tsx +++ b/apps/meteor/client/providers/ServerProvider.tsx @@ -1,5 +1,4 @@ import type { Serialized } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; import type { Method, PathFor, OperationParams, OperationResult, UrlParams, PathPattern } from '@rocket.chat/rest-typings'; import type { ServerMethodName, @@ -59,57 +58,16 @@ const callEndpoint = ( const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise => sdk.rest.post(endpoint as any, formData); -type EventMap = StreamKeys> = { - [key in `${N}/${K}`]: StreamerCallbackArgs; -}; - -const ee = new Emitter(); - -const events = new Map void>(); - -const getStream = ( - streamName: N, - _options?: { - retransmit?: boolean | undefined; - retransmitToSelf?: boolean | undefined; - }, -) => { - return >(eventName: K, callback: (...args: StreamerCallbackArgs) => void): (() => void) => { - const eventLiteral = `${streamName}/${eventName}` as const; - const emitterCallback = (args?: unknown): void => { - if (!args || !Array.isArray(args)) { - throw new Error('Invalid streamer callback'); - } - callback(...(args as StreamerCallbackArgs)); - }; - - ee.on(eventLiteral, emitterCallback); - - const streamHandler = (...args: StreamerCallbackArgs): void => { - ee.emit(eventLiteral, args); - }; - - const stop = (): void => { - // If someone is still listening, don't unsubscribe - ee.off(eventLiteral, emitterCallback); - - if (ee.has(eventLiteral)) { - return; - } - - const unsubscribe = events.get(eventLiteral); - if (unsubscribe) { - unsubscribe(); - events.delete(eventLiteral); - } - }; - - if (!events.has(eventLiteral)) { - events.set(eventLiteral, sdk.stream(streamName, [eventName], streamHandler).stop); - } - return stop; - }; -}; +const getStream = + ( + streamName: N, + _options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, + ) => + >(eventName: K, callback: (...args: StreamerCallbackArgs) => void): (() => void) => + sdk.stream(streamName, [eventName], callback).stop; const contextValue = { info,