diff --git a/.changeset/thin-cycles-return.md b/.changeset/thin-cycles-return.md new file mode 100644 index 0000000000000..e86391f5a0c18 --- /dev/null +++ b/.changeset/thin-cycles-return.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/ui-contexts': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/apps/meteor/client/NavBarV2/NavBar.tsx b/apps/meteor/client/NavBarV2/NavBar.tsx index 26a281452c54e..497a90c9a1f58 100644 --- a/apps/meteor/client/NavBarV2/NavBar.tsx +++ b/apps/meteor/client/NavBarV2/NavBar.tsx @@ -1,85 +1,18 @@ -import { useToolbar } from '@react-aria/toolbar'; -import { NavBar as NavBarComponent, NavBarSection, NavBarGroup, NavBarDivider } from '@rocket.chat/fuselage'; -import { usePermission, useTranslation, useUser } from '@rocket.chat/ui-contexts'; -import { useVoipState } from '@rocket.chat/ui-voip'; -import { useRef } from 'react'; +import { NavBar as NavBarComponent } from '@rocket.chat/fuselage'; +import { useLayout } from '@rocket.chat/ui-contexts'; -import { - NavBarItemOmniChannelCallDialPad, - NavBarItemOmnichannelContact, - NavBarItemOmnichannelLivechatToggle, - NavBarItemOmnichannelQueue, - NavBarItemOmnichannelCallToggle, -} from './NavBarOmnichannelToolbar'; -import { NavBarItemMarketPlaceMenu, NavBarItemAuditMenu, NavBarItemDirectoryPage, NavBarItemHomePage } from './NavBarPagesToolbar'; -import { NavBarItemLoginPage, NavBarItemAdministrationMenu, UserMenu } from './NavBarSettingsToolbar'; -import { NavBarItemVoipDialer } from './NavBarVoipToolbar'; -import { useIsCallEnabled, useIsCallReady } from '../contexts/CallContext'; -import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnabled'; -import { useOmnichannelShowQueueLink } from '../hooks/omnichannel/useOmnichannelShowQueueLink'; -import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; +import NavBarControlsSection from './NavBarControls/NavBarControlsSection'; +import NavBarNavigation from './NavBarNavigation'; +import NavBarPagesSection from './NavBarPagesSection'; const NavBar = () => { - const t = useTranslation(); - const user = useUser(); - - const hasAuditLicense = useHasLicenseModule('auditing') === true; - - const showOmnichannel = useOmnichannelEnabled(); - const hasManageAppsPermission = usePermission('manage-apps'); - const hasAccessMarketplacePermission = usePermission('access-marketplace'); - const showMarketplace = hasAccessMarketplacePermission || hasManageAppsPermission; - - const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); - const isCallEnabled = useIsCallEnabled(); - const isCallReady = useIsCallReady(); - const { isEnabled: showVoip } = useVoipState(); - - const pagesToolbarRef = useRef(null); - const { toolbarProps: pagesToolbarProps } = useToolbar({ 'aria-label': t('Pages') }, pagesToolbarRef); - - const omnichannelToolbarRef = useRef(null); - const { toolbarProps: omnichannelToolbarProps } = useToolbar({ 'aria-label': t('Omnichannel') }, omnichannelToolbarRef); - - const voipToolbarRef = useRef(null); - const { toolbarProps: voipToolbarProps } = useToolbar({ 'aria-label': t('Voice_Call') }, voipToolbarRef); + const { navbar } = useLayout(); return ( - - - - - {showMarketplace && } - {hasAuditLicense && } - - {showOmnichannel && ( - <> - - - {showOmnichannelQueueLink && } - {isCallReady && } - - {isCallEnabled && } - - - - )} - {showVoip && ( - <> - - - - - - )} - - - - - {user ? : } - - + {!navbar.searchExpanded && } + + {!navbar.searchExpanded && } ); }; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx new file mode 100644 index 0000000000000..8c92510d88fcf --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsMenu.tsx @@ -0,0 +1,49 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import { useVoipState } from '@rocket.chat/ui-voip'; +import type { HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled'; + +type NavBarControlsMenuProps = Omit, 'is'> & { + voipItems: GenericMenuItemProps[]; + omnichannelItems: GenericMenuItemProps[]; + isPressed: boolean; +}; + +const NavBarControlsMenu = ({ voipItems, omnichannelItems, isPressed, ...props }: NavBarControlsMenuProps) => { + const { t } = useTranslation(); + const { isEnabled: showVoip } = useVoipState(); + const showOmnichannel = useOmnichannelEnabled(); + + const sections = [ + { + title: t('Voice_Call'), + items: showVoip ? voipItems : [], + }, + { + title: t('Omnichannel'), + items: showOmnichannel ? omnichannelItems : [], + }, + ].filter((section) => section.items.length > 0); + + if (sections.length === 0) { + return null; + } + + return ( + + ); +}; + +export default NavBarControlsMenu; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsSection.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsSection.tsx new file mode 100644 index 0000000000000..75b3431fc5066 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsSection.tsx @@ -0,0 +1,41 @@ +import { NavBarSection, NavBarGroup, NavBarDivider } from '@rocket.chat/fuselage'; +import { useUser, useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import NavBarControlsWithData from './NavBarControlsWithData'; +import NavBarOmnichannelGroup from '../NavBarOmnichannelGroup'; +import { NavBarItemLoginPage, NavBarItemAdministrationMenu, UserMenu } from '../NavBarSettingsToolbar'; +import NavBarVoipGroup from '../NavBarVoipGroup'; + +const NavBarControlsSection = () => { + const { t } = useTranslation(); + const user = useUser(); + + const { isMobile } = useLayout(); + + if (isMobile) { + return ( + + + + + + {user ? : } + + + ); + } + + return ( + + + + + + {user ? : } + + + ); +}; + +export default NavBarControlsSection; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx new file mode 100644 index 0000000000000..716279688c679 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithCall.tsx @@ -0,0 +1,50 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import type { HTMLAttributes } from 'react'; + +import NavBarControlsMenu from './NavBarControlsMenu'; +import { useOmnichannelCallDialPadAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction'; +import { useOmnichannelCallToggleAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction'; + +type NavBarControlsMenuProps = Omit, 'is'> & { + voipItems: GenericMenuItemProps[]; + omnichannelItems: GenericMenuItemProps[]; + isPressed: boolean; +}; + +const NavBarControlsWithCall = ({ voipItems, omnichannelItems, isPressed, ...props }: NavBarControlsMenuProps) => { + const { + icon: omnichannelCallIcon, + title: omnichannelCallTitle, + handleOpenDialModal, + isDisabled: callDialPadDisabled, + } = useOmnichannelCallDialPadAction(); + + const { + title: omnichannelCallTogglerTitle, + icon: omnichannelCallTogglerIcon, + handleToggleCall, + isDisabled: callTogglerDisabled, + } = useOmnichannelCallToggleAction(); + + const omnichannelItemsWithCall = [ + ...omnichannelItems, + { + id: 'omnichannelCallDialPad', + icon: omnichannelCallIcon, + content: omnichannelCallTitle, + onClick: handleOpenDialModal, + disabled: callDialPadDisabled, + }, + { + id: 'omnichannelCallToggler', + icon: omnichannelCallTogglerIcon, + content: omnichannelCallTogglerTitle, + onClick: handleToggleCall, + disabled: callTogglerDisabled, + }, + ] as GenericMenuItemProps[]; + + return ; +}; + +export default NavBarControlsWithCall; diff --git a/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx new file mode 100644 index 0000000000000..ce4c7a9a3081f --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarControls/NavBarControlsWithData.tsx @@ -0,0 +1,90 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import type { HTMLAttributes } from 'react'; + +import NavBarControlsMenu from './NavBarControlsMenu'; +import NavbarControlsWithCall from './NavBarControlsWithCall'; +import { useIsCallEnabled } from '../../contexts/CallContext'; +import { useOmnichannelContactAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelContactAction'; +import { useOmnichannelLivechatToggle } from '../NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle'; +import { useOmnichannelQueueAction } from '../NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction'; +import { useVoipDialerAction } from '../NavBarVoipGroup/hooks/useVoipDialerAction'; +import { useVoipTogglerAction } from '../NavBarVoipGroup/hooks/useVoipTogglerAction'; + +type NavBarControlsMenuProps = Omit, 'is'>; + +const NavBarControlsWithData = (props: NavBarControlsMenuProps) => { + const isCallEnabled = useIsCallEnabled(); + + const { title: dialerTitle, handleToggleDialer, isPressed: isVoipDialerPressed, isDisabled: dialerDisabled } = useVoipDialerAction(); + const { isRegistered, title: togglerTitle, handleToggleVoip, isDisabled: togglerDisabled } = useVoipTogglerAction(); + + const { + isEnabled: queueEnabled, + icon: queueIcon, + title: queueTitle, + handleGoToQueue, + isPressed: isQueuePressed, + } = useOmnichannelQueueAction(); + + const { + title: contactCenterTitle, + icon: contactCenterIcon, + handleGoToContactCenter, + isPressed: isContactPressed, + } = useOmnichannelContactAction(); + + const { + title: omnichannelLivechatTogglerTitle, + icon: omnichannelLivechatTogglerIcon, + handleAvailableStatusChange, + } = useOmnichannelLivechatToggle(); + + const voipItems = [ + { + id: 'voipDialer', + icon: 'dialpad', + content: dialerTitle, + onClick: handleToggleDialer, + disabled: dialerDisabled, + }, + { + id: 'voipToggler', + icon: isRegistered ? 'phone-disabled' : 'phone', + content: togglerTitle, + onClick: handleToggleVoip, + disabled: togglerDisabled, + }, + ].filter(Boolean) as GenericMenuItemProps[]; + + const omnichannelItems = [ + queueEnabled && { + id: 'omnichannelQueue', + icon: queueIcon, + content: queueTitle, + onClick: handleGoToQueue, + disabled: dialerDisabled, + }, + { + id: 'omnichannelContact', + icon: contactCenterIcon, + content: contactCenterTitle, + onClick: handleGoToContactCenter, + }, + { + id: 'omnichannelLivechatToggler', + icon: omnichannelLivechatTogglerIcon, + content: omnichannelLivechatTogglerTitle, + onClick: handleAvailableStatusChange, + }, + ].filter(Boolean) as GenericMenuItemProps[]; + + const isPressed = isVoipDialerPressed || isQueuePressed || isContactPressed; + + if (isCallEnabled) { + return ; + } + + return ; +}; + +export default NavBarControlsWithData; diff --git a/apps/meteor/client/NavBarV2/NavBarNavigation.tsx b/apps/meteor/client/NavBarV2/NavBarNavigation.tsx new file mode 100644 index 0000000000000..e29f1d194e443 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarNavigation.tsx @@ -0,0 +1,28 @@ +import { NavBarGroup, NavBarItem, Box } from '@rocket.chat/fuselage'; +import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; +import { FocusScope } from 'react-aria'; +import { useTranslation } from 'react-i18next'; + +import NavBarSearch from './NavBarSearch'; + +const NavbarNavigation = () => { + const { t } = useTranslation(); + const { navigate } = useRouter(); + const { isMobile } = useLayout(); + + return ( + + + + + {!isMobile && ( + + navigate(-1)} icon='chevron-right' small /> + navigate(1)} icon='chevron-left' small /> + + )} + + ); +}; + +export default NavbarNavigation; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx new file mode 100644 index 0000000000000..7206bbea812ac --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmniChannelCallDialPad.tsx @@ -0,0 +1,19 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelCallDialPadAction } from './hooks/useOmnichannelCallDialPadAction'; + +type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; + +const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { + const { t } = useTranslation(); + + const { title, icon, handleOpenDialModal, isDisabled } = useOmnichannelCallDialPadAction(); + + return ( + + ); +}; + +export default NavBarItemOmniChannelCallDialPad; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx new file mode 100644 index 0000000000000..d6589117f6fbb --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelCallToggle.tsx @@ -0,0 +1,28 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelCallToggleAction } from './hooks/useOmnichannelCallToggleAction'; + +type NavBarItemOmnichannelCallToggleProps = Omit, 'is'>; + +const NavBarItemOmnichannelCallToggle = (props: NavBarItemOmnichannelCallToggleProps) => { + const { t } = useTranslation(); + const { icon, title, handleToggleCall, isSuccess, isWarning, isDanger, isDisabled } = useOmnichannelCallToggleAction(); + + return ( + + ); +}; + +export default NavBarItemOmnichannelCallToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelContact.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelContact.tsx new file mode 100644 index 0000000000000..f0f25efb78d56 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelContact.tsx @@ -0,0 +1,14 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useOmnichannelContactAction } from './hooks/useOmnichannelContactAction'; + +type NavBarItemOmnichannelContactProps = Omit, 'is'>; + +const NavBarItemOmnichannelContact = (props: NavBarItemOmnichannelContactProps) => { + const { icon, isPressed, title, handleGoToContactCenter } = useOmnichannelContactAction(); + + return ; +}; + +export default NavBarItemOmnichannelContact; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelLivechatToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelLivechatToggle.tsx new file mode 100644 index 0000000000000..47252f0e9f856 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelLivechatToggle.tsx @@ -0,0 +1,23 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useOmnichannelLivechatToggle } from './hooks/useOmnichannelLivechatToggle'; + +type NavBarItemOmnichannelLivechatToggleProps = Omit, 'is'>; + +const NavBarItemOmnichannelLivechatToggle = (props: NavBarItemOmnichannelLivechatToggleProps) => { + const { handleAvailableStatusChange, title, icon, isSuccess } = useOmnichannelLivechatToggle(); + + return ( + + ); +}; + +export default NavBarItemOmnichannelLivechatToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelQueue.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelQueue.tsx new file mode 100644 index 0000000000000..c31ce4a296cd0 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarItemOmnichannelQueue.tsx @@ -0,0 +1,18 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useOmnichannelQueueAction } from './hooks/useOmnichannelQueueAction'; + +type NavBarItemOmnichannelQueueProps = Omit, 'is'>; + +const NavBarItemOmnichannelQueue = (props: NavBarItemOmnichannelQueueProps) => { + const { isEnabled, title, icon, isPressed, handleGoToQueue } = useOmnichannelQueueAction(); + + if (!isEnabled) { + return null; + } + + return ; +}; + +export default NavBarItemOmnichannelQueue; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx new file mode 100644 index 0000000000000..d7eeccea40091 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/NavBarOmnichannelGroup.tsx @@ -0,0 +1,37 @@ +import { NavBarDivider, NavBarGroup } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; + +import NavBarItemOmniChannelCallDialPad from './NavBarItemOmniChannelCallDialPad'; +import NavBarItemOmnichannelCallToggle from './NavBarItemOmnichannelCallToggle'; +import NavBarItemOmnichannelContact from './NavBarItemOmnichannelContact'; +import NavBarItemOmnichannelLivechatToggle from './NavBarItemOmnichannelLivechatToggle'; +import NavBarItemOmnichannelQueue from './NavBarItemOmnichannelQueue'; +import { useIsCallEnabled, useIsCallReady } from '../../contexts/CallContext'; +import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled'; + +const NavBarOmnichannelGroup = () => { + const { t } = useTranslation(); + const showOmnichannel = useOmnichannelEnabled(); + + const isCallEnabled = useIsCallEnabled(); + const isCallReady = useIsCallReady(); + + if (!showOmnichannel) { + return null; + } + + return ( + <> + + + {isCallReady && } + + {isCallEnabled && } + + + + + ); +}; + +export default NavBarOmnichannelGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts new file mode 100644 index 0000000000000..6cf14e43446a0 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallDialPadAction.ts @@ -0,0 +1,20 @@ +import type { Keys } from '@rocket.chat/icons'; +import { useTranslation } from 'react-i18next'; + +import { useVoipOutboundStates } from '../../../contexts/CallContext'; +import { useDialModal } from '../../../hooks/useDialModal'; + +export const useOmnichannelCallDialPadAction = () => { + const { t } = useTranslation(); + + const { openDialModal } = useDialModal(); + + const { outBoundCallsAllowed, outBoundCallsEnabledForUser } = useVoipOutboundStates(); + + return { + isDisabled: !outBoundCallsEnabledForUser, + handleOpenDialModal: () => openDialModal(), + icon: 'dialpad' as Keys, + title: outBoundCallsAllowed ? t('New_Call') : t('New_Call_Premium_Only'), + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts new file mode 100644 index 0000000000000..a58f3de3cebe4 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelCallToggleAction.ts @@ -0,0 +1,75 @@ +import type { Keys as IconName } from '@rocket.chat/icons'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + useIsCallReady, + useIsCallError, + useCallerInfo, + useCallRegisterClient, + useCallUnregisterClient, + useVoipNetworkStatus, +} from '../../../contexts/CallContext'; + +export const useOmnichannelCallToggleAction = () => { + const { t } = useTranslation(); + const isCallReady = useIsCallReady(); + const isCallError = useIsCallError(); + + const caller = useCallerInfo(); + const unregister = useCallUnregisterClient(); + const register = useCallRegisterClient(); + + const networkStatus = useVoipNetworkStatus(); + const registered = !['ERROR', 'INITIAL', 'UNREGISTERED'].includes(caller.state); + const inCall = ['IN_CALL'].includes(caller.state); + + const handleToggleCall = useCallback(() => { + if (registered) { + unregister(); + return; + } + register(); + }, [registered, register, unregister]); + + const title = useMemo(() => { + if (isCallError) { + return t('Error'); + } + + if (!isCallReady) { + return t('Loading'); + } + + if (networkStatus === 'offline') { + return t('Waiting_for_server_connection'); + } + + if (inCall) { + return t('Cannot_disable_while_on_call'); + } + + if (registered) { + return t('Turn_off_answer_calls'); + } + + return t('Turn_on_answer_calls'); + }, [inCall, isCallError, isCallReady, networkStatus, registered, t]); + + const icon: IconName = useMemo(() => { + if (networkStatus === 'offline') { + return 'phone-issue'; + } + return registered ? 'phone' : 'phone-disabled'; + }, [networkStatus, registered]); + + return { + handleToggleCall, + title, + icon, + isDisabled: inCall || isCallError || !isCallReady, + isDanger: isCallError, + isSuccess: registered, + isWarning: networkStatus === 'offline', + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelContactAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelContactAction.ts new file mode 100644 index 0000000000000..a9ecbd09a96d8 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelContactAction.ts @@ -0,0 +1,16 @@ +import type { Keys } from '@rocket.chat/icons'; +import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +export const useOmnichannelContactAction = () => { + const { t } = useTranslation(); + const router = useRouter(); + const currentRoute = useCurrentRoutePath(); + + return { + icon: 'address-book' as Keys, + title: t('Contact_Center'), + handleGoToContactCenter: () => router.navigate('/omnichannel-directory'), + isPressed: currentRoute?.includes('/omnichannel-directory') || false, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle.ts new file mode 100644 index 0000000000000..45fb208f05382 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelLivechatToggle.ts @@ -0,0 +1,28 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import type { Keys } from '@rocket.chat/icons'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelAgentAvailable } from '../../../hooks/omnichannel/useOmnichannelAgentAvailable'; + +export const useOmnichannelLivechatToggle = () => { + const { t } = useTranslation(); + const agentAvailable = useOmnichannelAgentAvailable(); + const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); + const dispatchToastMessage = useToastMessageDispatch(); + + const handleAvailableStatusChange = useEffectEvent(async () => { + try { + await changeAgentStatus({}); + } catch (error: unknown) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + return { + title: agentAvailable ? t('Turn_off_answer_chats') : t('Turn_on_answer_chats'), + isSuccess: agentAvailable, + icon: (agentAvailable ? 'message' : 'message-disabled') as Keys, + handleAvailableStatusChange, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction.ts new file mode 100644 index 0000000000000..307d01372fa5a --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/hooks/useOmnichannelQueueAction.ts @@ -0,0 +1,21 @@ +import type { Keys } from '@rocket.chat/icons'; +import { useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import { useOmnichannelShowQueueLink } from '../../../hooks/omnichannel/useOmnichannelShowQueueLink'; + +export const useOmnichannelQueueAction = () => { + const { t } = useTranslation(); + const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); + + const router = useRouter(); + const currentRoute = useCurrentRoutePath(); + + return { + isEnabled: showOmnichannelQueueLink, + icon: 'queue' as Keys, + title: t('Queue'), + handleGoToQueue: () => router.navigate('/livechat-queue'), + isPressed: currentRoute?.includes('/livechat-queue') || false, + }; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/index.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/index.ts new file mode 100644 index 0000000000000..d60b0385dd5e5 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelGroup/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarOmnichannelGroup'; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx deleted file mode 100644 index a1abf735a1191..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useVoipOutboundStates } from '../../contexts/CallContext'; -import { useDialModal } from '../../hooks/useDialModal'; - -type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; - -const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { - const { t } = useTranslation(); - - const { openDialModal } = useDialModal(); - - const { outBoundCallsAllowed, outBoundCallsEnabledForUser } = useVoipOutboundStates(); - - return ( - openDialModal()} - disabled={!outBoundCallsEnabledForUser} - aria-label={t('Open_Dialpad')} - data-tooltip={outBoundCallsAllowed ? t('New_Call') : t('New_Call_Premium_Only')} - {...props} - /> - ); -}; - -export default NavBarItemOmniChannelCallDialPad; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx deleted file mode 100644 index 83c0da87964dd..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggle.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentPropsWithoutRef } from 'react'; - -import NavBarItemOmnichannelCallToggleError from './NavBarItemOmnichannelCallToggleError'; -import NavBarItemOmnichannelCallToggleLoading from './NavBarItemOmnichannelCallToggleLoading'; -import NavBarItemOmnichannelCallToggleReady from './NavBarItemOmnichannelCallToggleReady'; -import { useIsCallReady, useIsCallError } from '../../contexts/CallContext'; - -type NavBarItemOmnichannelCallToggleProps = ComponentPropsWithoutRef< - typeof NavBarItemOmnichannelCallToggleError | typeof NavBarItemOmnichannelCallToggleLoading | typeof NavBarItemOmnichannelCallToggleReady ->; - -const NavBarItemOmnichannelCallToggle = (props: NavBarItemOmnichannelCallToggleProps) => { - const isCallReady = useIsCallReady(); - const isCallError = useIsCallError(); - if (isCallError) { - return ; - } - - if (!isCallReady) { - return ; - } - - return ; -}; - -export default NavBarItemOmnichannelCallToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx deleted file mode 100644 index a713310d6d831..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -type NavBarItemOmnichannelCallToggleErrorProps = ComponentPropsWithoutRef; - -const NavBarItemOmnichannelCallToggleError = (props: NavBarItemOmnichannelCallToggleErrorProps) => { - const { t } = useTranslation(); - return ; -}; - -export default NavBarItemOmnichannelCallToggleError; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx deleted file mode 100644 index b39a2541513eb..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -type NavBarItemOmnichannelCallToggleLoadingProps = ComponentPropsWithoutRef; - -const NavBarItemOmnichannelCallToggleLoading = (props: NavBarItemOmnichannelCallToggleLoadingProps) => { - const { t } = useTranslation(); - return ; -}; - -export default NavBarItemOmnichannelCallToggleLoading; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx deleted file mode 100644 index 104c6a2cc7b30..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import type { ComponentPropsWithoutRef } from 'react'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useCallerInfo, useCallRegisterClient, useCallUnregisterClient, useVoipNetworkStatus } from '../../contexts/CallContext'; - -type NavBarItemOmnichannelCallToggleReadyProps = ComponentPropsWithoutRef; - -const NavBarItemOmnichannelCallToggleReady = (props: NavBarItemOmnichannelCallToggleReadyProps) => { - const { t } = useTranslation(); - - const caller = useCallerInfo(); - const unregister = useCallUnregisterClient(); - const register = useCallRegisterClient(); - - const networkStatus = useVoipNetworkStatus(); - const registered = !['ERROR', 'INITIAL', 'UNREGISTERED'].includes(caller.state); - const inCall = ['IN_CALL'].includes(caller.state); - - const onClickVoipButton = useCallback((): void => { - if (registered) { - unregister(); - return; - } - register(); - }, [registered, register, unregister]); - - const getTitle = (): string => { - if (networkStatus === 'offline') { - return t('Waiting_for_server_connection'); - } - - if (inCall) { - return t('Cannot_disable_while_on_call'); - } - - if (registered) { - return t('Turn_off_answer_calls'); - } - - return t('Turn_on_answer_calls'); - }; - - const getIcon = (): 'phone-issue' | 'phone' | 'phone-disabled' => { - if (networkStatus === 'offline') { - return 'phone-issue'; - } - return registered ? 'phone' : 'phone-disabled'; - }; - - return ( - - ); -}; - -export default NavBarItemOmnichannelCallToggleReady; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelContact.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelContact.tsx deleted file mode 100644 index 08eaeaabacb4e..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelContact.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import { useRouter, useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import type { HTMLAttributes } from 'react'; - -type NavBarItemOmnichannelContactProps = Omit, 'is'>; - -const NavBarItemOmnichannelContact = (props: NavBarItemOmnichannelContactProps) => { - const router = useRouter(); - const currentRoute = useCurrentRoutePath(); - - return ( - router.navigate('/omnichannel-directory')} - pressed={currentRoute?.includes('/omnichannel-directory')} - /> - ); -}; - -export default NavBarItemOmnichannelContact; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx deleted file mode 100644 index f67c21647a064..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelLivechatToggle.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Sidebar } from '@rocket.chat/fuselage'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentProps } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useOmnichannelAgentAvailable } from '../../hooks/omnichannel/useOmnichannelAgentAvailable'; - -type NavBarItemOmnichannelLivechatToggleProps = Omit, 'icon'>; - -const NavBarItemOmnichannelLivechatToggle = (props: NavBarItemOmnichannelLivechatToggleProps): ReactElement => { - const { t } = useTranslation(); - const agentAvailable = useOmnichannelAgentAvailable(); - const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status'); - const dispatchToastMessage = useToastMessageDispatch(); - - const handleAvailableStatusChange = useEffectEvent(async () => { - try { - await changeAgentStatus({}); - } catch (error: unknown) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - - return ( - - ); -}; - -export default NavBarItemOmnichannelLivechatToggle; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelQueue.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelQueue.tsx deleted file mode 100644 index 471a1293ddbc0..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelQueue.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import { useRouter, useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import type { HTMLAttributes } from 'react'; - -type NavBarItemOmnichannelQueueProps = Omit, 'is'>; - -const NavBarItemOmnichannelQueue = (props: NavBarItemOmnichannelQueueProps) => { - const router = useRouter(); - const currentRoute = useCurrentRoutePath(); - - return ( - router.navigate('/livechat-queue')} - pressed={currentRoute?.includes('/livechat-queue')} - /> - ); -}; - -export default NavBarItemOmnichannelQueue; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/index.ts b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/index.ts deleted file mode 100644 index 8dacb885deb3b..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { default as NavBarItemOmniChannelCallDialPad } from './NavBarItemOmniChannelCallDialPad'; -export { default as NavBarItemOmnichannelCallToggle } from './NavBarItemOmnichannelCallToggle'; -export { default as NavBarItemOmnichannelContact } from './NavBarItemOmnichannelContact'; -export { default as NavBarItemOmnichannelLivechatToggle } from './NavBarItemOmnichannelLivechatToggle'; -export { default as NavBarItemOmnichannelQueue } from './NavBarItemOmnichannelQueue'; diff --git a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemCreateNew.tsx similarity index 50% rename from apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemCreateNew.tsx index c720b8a768f40..f2b3c2956e97d 100644 --- a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemCreateNew.tsx @@ -3,16 +3,16 @@ import { GenericMenu } from '@rocket.chat/ui-client'; import type { HTMLAttributes } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCreateRoom } from './hooks/useCreateRoomMenu'; +import { useCreateNewMenu } from './hooks/useCreateNewMenu'; type CreateRoomProps = Omit, 'is'>; -const CreateRoom = (props: CreateRoomProps) => { +const NavBarItemCreateNew = (props: CreateRoomProps) => { const { t } = useTranslation(); - const sections = useCreateRoom(); + const sections = useCreateNewMenu(); - return ; + return ; }; -export default CreateRoom; +export default NavBarItemCreateNew; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemDirectoryPage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemDirectoryPage.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemDirectoryPage.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemDirectoryPage.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemHomePage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemHomePage.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemHomePage.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemHomePage.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemMarketPlaceMenu.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemMarketPlaceMenu.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemSort.tsx similarity index 74% rename from apps/meteor/client/sidebarv2/header/actions/Sort.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemSort.tsx index 5f0a19b2c66da..430718e036820 100644 --- a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarItemSort.tsx @@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'; import { useSortMenu } from './hooks/useSortMenu'; -type SortProps = Omit, 'is'>; +type NavBarItemSortProps = Omit, 'is'>; -const Sort = (props: SortProps) => { +const NavBarItemSort = (props: NavBarItemSortProps) => { const { t } = useTranslation(); const sections = useSortMenu(); @@ -15,4 +15,4 @@ const Sort = (props: SortProps) => { return ; }; -export default Sort; +export default NavBarItemSort; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesGroup.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesGroup.tsx new file mode 100644 index 0000000000000..f4d4a89068bac --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesGroup.tsx @@ -0,0 +1,36 @@ +import { NavBarGroup } from '@rocket.chat/fuselage'; +import { useLayout, usePermission } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import NavBarItemCreateNew from './NavBarItemCreateNew'; +import NavBarItemDirectoryPage from './NavBarItemDirectoryPage'; +import NavBarItemHomePage from './NavBarItemHomePage'; +import NavBarItemMarketPlaceMenu from './NavBarItemMarketPlaceMenu'; +import NavBarItemSort from './NavBarItemSort'; +import NavBarPagesStackMenu from './NavBarPagesStackMenu'; + +const NavBarPagesGroup = () => { + const { t } = useTranslation(); + const { isTablet, isMobile } = useLayout(); + + const hasManageAppsPermission = usePermission('manage-apps'); + const hasAccessMarketplacePermission = usePermission('access-marketplace'); + const showMarketplace = hasAccessMarketplacePermission || hasManageAppsPermission; + + return ( + + {isTablet && } + {!isTablet && ( + <> + + + + )} + {showMarketplace && !isMobile && } + + {!isMobile && } + + ); +}; + +export default NavBarPagesGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesStackMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesStackMenu.tsx new file mode 100644 index 0000000000000..16e7b81e4378e --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/NavBarPagesStackMenu.tsx @@ -0,0 +1,46 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import { useCurrentRoutePath, useLayout, useRouter, useSetting } from '@rocket.chat/ui-contexts'; +import type { HTMLAttributes } from 'react'; +import { useTranslation } from 'react-i18next'; + +type NavBarPagesStackMenuProps = Omit, 'is'>; + +const NavBarPagesStackMenu = (props: NavBarPagesStackMenuProps) => { + const { t } = useTranslation(); + + const showHome = useSetting('Layout_Show_Home_Button'); + const { sidebar } = useLayout(); + const router = useRouter(); + + const handleGoToHome = useEffectEvent(() => { + sidebar.toggle(); + router.navigate('/home'); + }); + + const currentRoute = useCurrentRoutePath(); + const pressed = currentRoute?.includes('/directory') || currentRoute?.includes('/home'); + + const items = [ + showHome && { + id: 'home', + icon: 'home', + content: t('Home'), + onClick: handleGoToHome, + }, + { + id: 'directory', + icon: 'notebook-hashtag', + content: t('Directory'), + onClick: () => router.navigate('/directory'), + }, + ].filter(Boolean) as GenericMenuItemProps[]; + + return ( + + ); +}; + +export default NavBarPagesStackMenu; diff --git a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx similarity index 97% rename from apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx index b279993cd9229..b5cdfe223002a 100644 --- a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx @@ -28,10 +28,10 @@ import type { ComponentProps, ReactElement } from 'react'; import { useId, useEffect, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; -import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; +import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateChannelModalProps = { teamId?: string; diff --git a/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx similarity index 94% rename from apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx index cdb02d364b05c..1a7a5a5f5b7ff 100644 --- a/apps/meteor/client/sidebarv2/header/CreateDirectMessage.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx @@ -5,8 +5,8 @@ import { useMutation } from '@tanstack/react-query'; import { useId, memo } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; +import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateDirectMessageProps = { onClose: () => void }; diff --git a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx similarity index 98% rename from apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx index 84ad009c96f91..29585f2cee1ec 100644 --- a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateTeamModal.tsx @@ -30,9 +30,9 @@ import type { ComponentProps, ReactElement } from 'react'; import { useId, memo, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { useEncryptedRoomDescription } from './hooks/useEncryptedRoomDescription'; -import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple'; -import { goToRoomById } from '../../lib/utils/goToRoomById'; +import { useEncryptedRoomDescription } from './useEncryptedRoomDescription'; +import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; type CreateTeamModalInputs = { name: string; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/useEncryptedRoomDescription.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx similarity index 84% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx index 4d313975d6f54..7ac76f3f7815e 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewItems.tsx @@ -1,18 +1,18 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useSetting, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; -import CreateDiscussion from '../../../../components/CreateDiscussion'; -import CreateChannelModal from '../../CreateChannelModal'; -import CreateDirectMessage from '../../CreateDirectMessage'; -import CreateTeamModal from '../../CreateTeamModal'; -import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; +import { useCreateRoomModal } from './useCreateRoomModal'; +import CreateDiscussion from '../../../components/CreateDiscussion'; +import CreateChannelModal from '../actions/CreateChannelModal'; +import CreateDirectMessage from '../actions/CreateDirectMessage'; +import CreateTeamModal from '../actions/CreateTeamModal'; const CREATE_CHANNEL_PERMISSIONS = ['create-c', 'create-p']; const CREATE_TEAM_PERMISSIONS = ['create-team']; const CREATE_DIRECT_PERMISSIONS = ['create-d']; const CREATE_DISCUSSION_PERMISSIONS = ['start-discussion', 'start-discussion-other-user']; -export const useCreateRoomItems = (): GenericMenuItemProps[] => { +export const useCreateNewItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const discussionEnabled = useSetting('Discussion_enabled'); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewMenu.tsx similarity index 81% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewMenu.tsx index 795944b8d819a..781f7728bcfd7 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateNewMenu.tsx @@ -1,20 +1,20 @@ import { useAtLeastOnePermission, useSetting } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; -import { useCreateRoomItems } from './useCreateRoomItems'; +import { useCreateNewItems } from './useCreateNewItems'; import { useMatrixFederationItems } from './useMatrixFederationItems'; -import { useIsEnterprise } from '../../../../hooks/useIsEnterprise'; +import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; const CREATE_ROOM_PERMISSIONS = ['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user']; -export const useCreateRoom = () => { +export const useCreateNewMenu = () => { const { t } = useTranslation(); const showCreate = useAtLeastOnePermission(CREATE_ROOM_PERMISSIONS); const { data } = useIsEnterprise(); const isMatrixEnabled = useSetting('Federation_Matrix_enabled') && data?.isEnterprise; - const createRoomItems = useCreateRoomItems(); + const createRoomItems = useCreateNewItems(); const matrixFederationSearchItems = useMatrixFederationItems({ isMatrixEnabled }); const sections = [ diff --git a/apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateRoomModal.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useCreateRoomModal.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.spec.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.spec.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useGroupingListItems.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.spec.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.spec.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.tsx similarity index 100% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMarketPlaceMenu.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts similarity index 82% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts index cd0abc9bdfb20..2a90219e03fd7 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useMatrixFederationItems.ts @@ -1,8 +1,8 @@ import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from 'react-i18next'; -import MatrixFederationSearch from '../../MatrixFederationSearch'; -import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; +import { useCreateRoomModal } from './useCreateRoomModal'; +import MatrixFederationSearch from '../../../sidebarv2/header/MatrixFederationSearch'; export const useMatrixFederationItems = ({ isMatrixEnabled, diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortMenu.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortMenu.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.spec.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.spec.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.tsx similarity index 95% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.tsx index b0f77eab7a0a6..c1dfb4f3d71ec 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useSortModeItems.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { OmnichannelSortingDisclaimer, useOmnichannelSortingDisclaimer, -} from '../../../../components/Omnichannel/OmnichannelSortingDisclaimer'; +} from '../../../components/Omnichannel/OmnichannelSortingDisclaimer'; export const useSortModeItems = (): GenericMenuItemProps[] => { const { t } = useTranslation(); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.spec.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.spec.tsx diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.tsx similarity index 100% rename from apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx rename to apps/meteor/client/NavBarV2/NavBarPagesGroup/hooks/useViewModeItems.tsx diff --git a/apps/meteor/client/NavBarV2/NavBarPagesGroup/index.ts b/apps/meteor/client/NavBarV2/NavBarPagesGroup/index.ts new file mode 100644 index 0000000000000..b8f036c41fb61 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesGroup/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarPagesGroup'; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx b/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx new file mode 100644 index 0000000000000..f3f949753a903 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx @@ -0,0 +1,25 @@ +import { NavBarDivider, NavBarGroup, NavBarSection } from '@rocket.chat/fuselage'; +import { useLayout } from '@rocket.chat/ui-contexts'; + +import NavBarPagesGroup from './NavBarPagesGroup'; +import { SidebarTogglerV2 } from '../components/SidebarTogglerV2'; + +const NavBarPagesSection = () => { + const { isTablet } = useLayout(); + + return ( + + {isTablet && ( + <> + + + + + + )} + + + ); +}; + +export default NavBarPagesSection; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx deleted file mode 100644 index 2da6f7529be03..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; -import { GenericMenu } from '@rocket.chat/ui-client'; -import { useCurrentRoutePath } from '@rocket.chat/ui-contexts'; -import type { HTMLAttributes } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useAuditMenu } from './hooks/useAuditMenu'; - -type NavBarItemAuditMenuProps = Omit, 'is'>; - -const NavBarItemAuditMenu = (props: NavBarItemAuditMenuProps) => { - const { t } = useTranslation(); - const sections = useAuditMenu(); - const currentRoute = useCurrentRoutePath(); - - return ( - - ); -}; - -export default NavBarItemAuditMenu; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/index.ts b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/index.ts deleted file mode 100644 index 2b334cab4b2d2..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as NavBarItemAuditMenu } from './NavBarItemAuditMenu'; -export { default as NavBarItemHomePage } from './NavBarItemHomePage'; -export { default as NavBarItemMarketPlaceMenu } from './NavBarItemMarketPlaceMenu'; -export { default as NavBarItemDirectoryPage } from './NavBarItemDirectoryPage'; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx new file mode 100644 index 0000000000000..eab5c43c16015 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx @@ -0,0 +1,94 @@ +import { Box, Icon, TextInput } from '@rocket.chat/fuselage'; +import { useEffectEvent, useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { useCallback, useEffect, useRef } from 'react'; +import { useFocusManager, useOverlayTrigger } from 'react-aria'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useOverlayTriggerState } from 'react-stately'; +import tinykeys from 'tinykeys'; + +import NavBarSearchListBox from './NavBarSearchListbox'; +import { getShortcutLabel } from './getShortcutLabel'; +import { useSearchFocus } from './hooks/useSearchFocus'; +import { useSearchInputNavigation } from './hooks/useSearchNavigation'; + +const NavBarSearch = () => { + const { t } = useTranslation(); + const focusManager = useFocusManager(); + const shortcut = getShortcutLabel(); + + const placeholder = [t('Search_rooms'), shortcut].filter(Boolean).join(' '); + + const methods = useForm({ defaultValues: { filterText: '' } }); + const { + formState: { isDirty }, + register, + resetField, + setFocus, + } = methods; + + const { ref: filterRef, ...rest } = register('filterText'); + + const triggerRef = useRef(null); + const mergedRefs = useMergedRefs(filterRef, triggerRef); + + const state = useOverlayTriggerState({}); + const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'listbox' }, state, triggerRef); + delete triggerProps.onPress; + + const handleKeyDown = useSearchInputNavigation(state); + const handleFocus = useSearchFocus(state); + + const handleEscSearch = useCallback(() => { + resetField('filterText'); + state.close(); + }, [resetField, state]); + + const handleClearText = useEffectEvent(() => { + resetField('filterText'); + setFocus('filterText'); + }); + + useEffect(() => { + const unsubscribe = tinykeys(window, { + '$mod+K': (event) => { + event.preventDefault(); + setFocus('filterText'); + }, + '$mod+P': (event) => { + event.preventDefault(); + setFocus('filterText'); + }, + 'Escape': (event) => { + event.preventDefault(); + handleEscSearch(); + }, + }); + + return (): void => { + unsubscribe(); + }; + }, [focusManager, handleEscSearch, setFocus]); + + return ( + + + } + /> + {state.isOpen && } + + + ); +}; + +export default NavBarSearch; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx new file mode 100644 index 0000000000000..7c26ad4febcee --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx @@ -0,0 +1,28 @@ +import { SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemTitle } from '@rocket.chat/fuselage'; +import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; + +type NavBarSearchItemProps = { + title: string; + avatar: ReactElement; + icon: ReactNode; + actions?: ReactElement; + href?: string; + unread?: boolean; + selected?: boolean; + badges?: ReactElement; + clickable?: boolean; +} & Omit, 'is'>; + +const NavBarSearchItem = ({ icon, title, avatar, actions, unread, badges, ...props }: NavBarSearchItemProps) => { + return ( + + {avatar && {avatar}} + {icon && icon} + {title} + {badges && badges} + {actions && actions} + + ); +}; + +export default NavBarSearchItem; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx new file mode 100644 index 0000000000000..da83a9ca1c129 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItemWithData.tsx @@ -0,0 +1,59 @@ +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import NavBarSearchItem from './NavBarSearchItem'; +import { RoomIcon } from '../../components/RoomIcon'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; +import { OmnichannelBadges } from '../../sidebarv2/badges/OmnichannelBadges'; +import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay'; + +type NavBarSearchItemWithDataProps = { + room: SubscriptionWithRoom; + id: string; + AvatarTemplate: ReactElement; +} & Partial>; + +const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSearchItemWithDataProps) => { + const { t } = useTranslation(); + + const href = roomCoordinator.getRouteLink(room.t, room) || ''; + const title = roomCoordinator.getRoomName(room.t, room) || ''; + + const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room); + + const icon = } />; + + const badges = ( + <> + {showUnread && ( + + {unreadCount.total} + + )} + {isOmnichannelRoom(room) && } + + ); + + return ( + + ); +}; + +export default NavBarSearchItemWithData; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx new file mode 100644 index 0000000000000..4d5a326b37268 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx @@ -0,0 +1,74 @@ +import { Box, Tile } from '@rocket.chat/fuselage'; +import { useDebouncedValue, useEffectEvent, useOutsideClick } from '@rocket.chat/fuselage-hooks'; +import { useRef } from 'react'; +import type { OverlayTriggerAria } from 'react-aria'; +import { useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { OverlayTriggerState } from 'react-stately'; + +import NavBarSearchNoResults from './NavBarSearchNoResults'; +import NavBarSearchRow from './NavBarSearchRow'; +import { useSearchItems } from './hooks/useSearchItems'; +import { useListboxNavigation } from './hooks/useSearchNavigation'; +import { CustomScrollbars } from '../../components/CustomScrollbars'; + +type NavBarSearchListBoxProps = { + state: OverlayTriggerState; + overlayProps: OverlayTriggerAria['overlayProps']; +}; + +const NavBarSearchListBox = ({ state, overlayProps }: NavBarSearchListBoxProps) => { + const { t } = useTranslation(); + const containerRef = useRef(null); + + const handleKeyDown = useListboxNavigation(state); + useOutsideClick([containerRef], state.close); + + const { resetField, watch } = useFormContext(); + const { filterText } = watch(); + + const debouncedFilter = useDebouncedValue(filterText, 200); + + const handleSelect = useEffectEvent(() => { + state.close(); + resetField('filterText'); + }); + + const { data: items = [], isLoading } = useSearchItems(debouncedFilter); + + return ( + + +
+ {items.length === 0 && !isLoading && } + {items.length > 0 && ( + + {filterText ? t('Results') : t('Recent')} + + )} + {items.map((item) => ( +
+ +
+ ))} +
+
+
+ ); +}; + +export default NavBarSearchListBox; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx new file mode 100644 index 0000000000000..ef468da5d166b --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx @@ -0,0 +1,10 @@ +import { useTranslation } from 'react-i18next'; + +import GenericNoResults from '../../components/GenericNoResults'; + +const NavBarSearchNoResults = () => { + const { t } = useTranslation(); + return ; +}; + +export default NavBarSearchNoResults; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchRow.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchRow.tsx new file mode 100644 index 0000000000000..e60d9c6ae5c77 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchRow.tsx @@ -0,0 +1,24 @@ +import { RoomAvatar } from '@rocket.chat/ui-avatar'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import { memo } from 'react'; + +import NavBarSearchItemWithData from './NavBarSearchItemWithData'; +import NavBarSearchUserRow from './NavBarSearchUserRow'; + +type NavBarSearchRowProps = { + room: SubscriptionWithRoom; + onClick: () => void; +}; + +const NavBarSearchRow = ({ room, onClick }: NavBarSearchRowProps): ReactElement => { + const Avatar = ; + + if (room.t === 'd' && !room.u) { + return ; + } + + return ; +}; + +export default memo(NavBarSearchRow); diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchUserRow.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchUserRow.tsx new file mode 100644 index 0000000000000..4dab2f976d936 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchUserRow.tsx @@ -0,0 +1,26 @@ +import { SidebarV2ItemIcon } from '@rocket.chat/fuselage'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ReactElement } from 'react'; +import { memo } from 'react'; + +import NavBarSearchItem from './NavBarSearchItem'; +import { ReactiveUserStatus } from '../../components/UserStatus'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; + +type NavBarSearchUserRowProps = { + room: SubscriptionWithRoom; + id: string; + AvatarTemplate: ReactElement; +} & Partial>; + +const NavBarSearchUserRow = ({ room, id, AvatarTemplate, ...props }: NavBarSearchUserRowProps) => { + const useRealName = useSetting('UI_Use_Real_Name'); + const title = useRealName ? room.fname || room.name : room.name || room.fname || ''; + const icon = } />; + const href = roomCoordinator.getRouteLink(room.t, { name: room.name }) || ''; + + return ; +}; + +export default memo(NavBarSearchUserRow); diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/getShortcutLabel.ts b/apps/meteor/client/NavBarV2/NavBarSearch/getShortcutLabel.ts new file mode 100644 index 0000000000000..e041f944d25b6 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/getShortcutLabel.ts @@ -0,0 +1,25 @@ +const mobileCheck = function () { + let check = false; + (function (a: string) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4), + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera || ''); + return check; +}; + +export const getShortcutLabel = (): string => { + if (navigator.userAgentData?.mobile || mobileCheck()) { + return ''; + } + if (window.navigator.platform.toLowerCase().includes('mac')) { + return '(\u2318+K)'; + } + return '(Ctrl+K)'; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchFocus.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchFocus.ts new file mode 100644 index 0000000000000..c4d348e252a0c --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchFocus.ts @@ -0,0 +1,20 @@ +import { useLayout } from '@rocket.chat/ui-contexts'; +import { useCallback, useEffect } from 'react'; +import type { OverlayTriggerState } from 'react-stately'; + +export const useSearchFocus = (state: OverlayTriggerState) => { + const { navbar } = useLayout(); + + useEffect(() => { + if (!state.isOpen) { + navbar.collapseSearch?.(); + } + }, [navbar, state.isOpen]); + + const handleFocus = useCallback(() => { + navbar.expandSearch?.(); + state.setOpen(true); + }, [navbar, state]); + + return handleFocus; +}; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchItems.ts similarity index 100% rename from apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts rename to apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchItems.ts diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchNavigation.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchNavigation.ts new file mode 100644 index 0000000000000..b1ee47279e68a --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchNavigation.ts @@ -0,0 +1,64 @@ +import type { KeyboardEvent } from 'react'; +import { useCallback } from 'react'; +import { useFocusManager } from 'react-aria'; +import type { OverlayTriggerState } from 'react-stately'; + +export const isOption = (node: Element) => node.getAttribute('role') === 'option'; + +export const useListboxNavigation = (state: OverlayTriggerState) => { + const focusManager = useFocusManager(); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.code === 'Tab') { + state.close(); + } + + if (e.code === 'ArrowUp' || e.code === 'ArrowDown') { + e.preventDefault(); + + if (e.code === 'ArrowUp') { + return focusManager?.focusPrevious({ + wrap: true, + accept: (node) => isOption(node), + }); + } + + if (e.code === 'ArrowDown') { + focusManager?.focusNext({ + wrap: true, + accept: (node) => isOption(node), + }); + } + } + }, + [focusManager, state], + ); + + return handleKeyDown; +}; + +export const useSearchInputNavigation = (state: OverlayTriggerState) => { + const focusManager = useFocusManager(); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + state.setOpen(true); + + if ((e.code === 'Tab' && e.shiftKey) || e.key === 'Escape') { + state.close(); + } + + if (e.code === 'ArrowUp' || e.code === 'ArrowDown') { + e.preventDefault(); + + focusManager?.focusNext({ + accept: (node) => isOption(node), + }); + } + }, + [focusManager, state], + ); + + return handleKeyDown; +}; diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/index.ts b/apps/meteor/client/NavBarV2/NavBarSearch/index.ts new file mode 100644 index 0000000000000..4cee7a4b8ea42 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarSearch'; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.spec.tsx new file mode 100644 index 0000000000000..b933d520a9630 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.spec.tsx @@ -0,0 +1,69 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import NavBarItemAdministrationMenu from './NavBarItemAdministrationMenu'; + +const handleMenuClick = async () => { + const menuButton = await screen.findByRole('button', { name: 'Manage' }); + await userEvent.click(menuButton); +}; + +it('should not display the menu if no permission is set', async () => { + render(, { wrapper: mockAppRoot().build() }); + + expect(screen.queryByRole('button', { name: 'Manage' })).not.toBeInTheDocument(); +}); + +it('should display the workspace menu item if at least one admin permission is set', async () => { + render(, { wrapper: mockAppRoot().withPermission('access-permissions').build() }); + + await handleMenuClick(); + expect(await screen.findByRole('menuitem', { name: 'Workspace' })).toBeInTheDocument(); +}); + +it('should display the omnichannel menu item if view-livechat-manager permission is set', async () => { + render(, { + wrapper: mockAppRoot().withPermission('view-livechat-manager').withPermission('access-permissions').build(), + }); + + await handleMenuClick(); + expect(await screen.findByRole('menuitem', { name: 'Omnichannel' })).toBeInTheDocument(); +}); + +it('should not display any audit items if has at least one admin permission, some audit permission and the auditing module is not enabled', async () => { + render(, { + wrapper: mockAppRoot().withPermission('access-permissions').withPermission('can-audit').build(), + }); + + await handleMenuClick(); + expect(screen.queryByRole('menuitem', { name: 'Messages' })).not.toBeInTheDocument(); +}); + +it('should display audit items if has at least one admin permission, both audit permission and the auditing module is enabled', async () => { + render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/licenses.info', () => ({ + license: { + license: { + // @ts-expect-error: just for testing + grantedModules: [{ module: 'auditing' }], + }, + // @ts-expect-error: just for testing + activeModules: ['auditing'], + }, + })) + .withJohnDoe() + .withPermission('can-audit') + .withPermission('can-audit-log') + .build(), + }); + + await handleMenuClick(); + await waitFor(() => { + expect(screen.getByText('Messages')).toBeInTheDocument(); + }); + + expect(await screen.findByRole('menuitem', { name: 'Messages' })).toBeInTheDocument(); + expect(await screen.findByRole('menuitem', { name: 'Logs' })).toBeInTheDocument(); +}); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx index d4ff95ae9f632..ed451873a741e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx @@ -5,6 +5,7 @@ import type { HTMLAttributes } from 'react'; import { useTranslation } from 'react-i18next'; import { useAdministrationMenu } from './hooks/useAdministrationMenu'; +import { useAuditMenu } from './hooks/useAuditMenu'; type NavBarItemAdministrationMenuProps = Omit, 'is'>; @@ -12,21 +13,20 @@ const NavBarItemAdministrationMenu = (props: NavBarItemAdministrationMenuProps) const { t } = useTranslation(); const currentRoute = useCurrentRoutePath(); - const sections = useAdministrationMenu(); + const adminSection = useAdministrationMenu(); + const auditSection = useAuditMenu(); - if (!sections[0].items.length) { + const adminRoutesRegex = new RegExp(['/omnichannel/', '/admin', '/audit'].join('|')); + const pressed = adminRoutesRegex.test(currentRoute || ''); + + const sections = [adminSection, auditSection].filter((section) => section.items.length > 0); + + if (sections.length === 0) { return null; } + return ( - + ); }; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx index 437a35c73833e..126f1665c5e7f 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx @@ -7,14 +7,12 @@ import { useTranslation } from 'react-i18next'; import UserMenuHeader from '../UserMenuHeader'; import { useAccountItems } from './useAccountItems'; import { useStatusItems } from './useStatusItems'; -import { useVoipItemsSection } from './useVoipItemsSection'; export const useUserMenu = (user: IUser) => { const { t } = useTranslation(); const statusItems = useStatusItems(); const accountItems = useAccountItems(); - const voipSection = useVoipItemsSection(); const logout = useLogout(); const handleLogout = useEffectEvent(() => { @@ -37,7 +35,6 @@ export const useUserMenu = (user: IUser) => { title: t('Status'), items: statusItems, }, - voipSection, { title: t('Account'), items: accountItems, diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx index 55f92cde95762..6eb991ca61afb 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.spec.tsx @@ -20,7 +20,7 @@ it('should return omnichannel item if has `view-livechat-manager` permission ', }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'omnichannel', }), @@ -45,7 +45,7 @@ it('should show administration item if has at least one admin permission', async }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'workspace', }), diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx index 01357109c0fe9..7d4190bd00b8e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx @@ -48,10 +48,8 @@ export const useAdministrationMenu = () => { onClick: () => router.navigate('/omnichannel/current'), }; - return [ - { - title: t('Manage'), - items: [isAdmin && workspace, isOmnichannel && omnichannel].filter(Boolean) as GenericMenuItemProps[], - }, - ]; + return { + title: t('Manage'), + items: [isAdmin && workspace, isOmnichannel && omnichannel].filter(Boolean) as GenericMenuItemProps[], + }; }; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx similarity index 91% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx rename to apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx index dbf16bc4dfb24..de197ab30f09f 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.spec.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.spec.tsx @@ -18,7 +18,7 @@ it('should return an empty array of items if doesn`t have license', async () => .build(), }); - await waitFor(() => expect(result.current).toEqual([])); + await waitFor(() => expect(result.current.items).toEqual([])); }); it('should return an empty array of items if have license and not have permissions', async () => { @@ -39,7 +39,7 @@ it('should return an empty array of items if have license and not have permissio .build(), }); - await waitFor(() => expect(result.current).toEqual([])); + await waitFor(() => expect(result.current.items).toEqual([])); }); it('should return auditItems if have license and permissions', async () => { @@ -62,14 +62,14 @@ it('should return auditItems if have license and permissions', async () => { }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'messages', }), ), ); - expect(result.current[0].items[1]).toEqual( + expect(result.current.items[1]).toEqual( expect.objectContaining({ id: 'auditLog', }), @@ -95,7 +95,7 @@ it('should return auditMessages item if have license and can-audit permission', }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'messages', }), @@ -122,7 +122,7 @@ it('should return audiLogs item if have license and can-audit-log permission', a }); await waitFor(() => - expect(result.current[0]?.items[0]).toEqual( + expect(result.current.items[0]).toEqual( expect.objectContaining({ id: 'auditLog', }), diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx similarity index 74% rename from apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx rename to apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx index 7c0c36dc9f24a..be412480c3007 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAuditMenu.tsx @@ -13,27 +13,20 @@ export const useAuditMenu = () => { const hasAuditPermission = usePermission('can-audit') && hasAuditLicense; const hasAuditLogPermission = usePermission('can-audit-log') && hasAuditLicense; - if (!hasAuditPermission && !hasAuditLogPermission) { - return []; - } - const auditMessageItem: GenericMenuItemProps = { id: 'messages', - icon: 'document-eye', content: t('Messages'), onClick: () => router.navigate('/audit'), }; + const auditLogItem: GenericMenuItemProps = { id: 'auditLog', - icon: 'document-eye', content: t('Logs'), onClick: () => router.navigate('/audit-log'), }; - return [ - { - title: t('Audit'), - items: [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[], - }, - ]; + return { + title: t('Audit'), + items: [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[], + }; }; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipDialer.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipDialer.tsx new file mode 100644 index 0000000000000..d3c857b712a25 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipDialer.tsx @@ -0,0 +1,16 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useVoipDialerAction } from './hooks/useVoipDialerAction'; + +type NavBarItemVoipDialerProps = Omit, 'is'> & { + primary?: boolean; +}; + +const NavBarItemVoipDialer = (props: NavBarItemVoipDialerProps) => { + const { title, handleToggleDialer, isPressed, isDisabled } = useVoipDialerAction(); + + return ; +}; + +export default NavBarItemVoipDialer; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipToggler.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipToggler.tsx new file mode 100644 index 0000000000000..d4b508709f40b --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarItemVoipToggler.tsx @@ -0,0 +1,16 @@ +import { NavBarItem } from '@rocket.chat/fuselage'; +import type { HTMLAttributes } from 'react'; + +import { useVoipTogglerAction } from './hooks/useVoipTogglerAction'; + +type NavBarItemVoipDialerProps = Omit, 'is'> & { + primary?: boolean; +}; + +const NavBarItemVoipToggler = (props: NavBarItemVoipDialerProps) => { + const { title, icon, isDisabled, handleToggleVoip } = useVoipTogglerAction(); + + return ; +}; + +export default NavBarItemVoipToggler; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx new file mode 100644 index 0000000000000..7b16053b30566 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/NavBarVoipGroup.tsx @@ -0,0 +1,29 @@ +import { NavBarDivider, NavBarGroup } from '@rocket.chat/fuselage'; +import { useVoipState } from '@rocket.chat/ui-voip'; +import { useTranslation } from 'react-i18next'; + +import NavBarItemVoipDialer from './NavBarItemVoipDialer'; +import NavBarItemVoipToggler from './NavBarItemVoipToggler'; +import { useIsCallEnabled } from '../../contexts/CallContext'; + +const NavBarVoipGroup = () => { + const { t } = useTranslation(); + const { isEnabled: showVoip } = useVoipState(); + const isCallEnabled = useIsCallEnabled(); + + if (!showVoip) { + return null; + } + + return ( + <> + + + + + + + ); +}; + +export default NavBarVoipGroup; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/NavBarItemVoipDialer.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipDialerAction.ts similarity index 57% rename from apps/meteor/client/NavBarV2/NavBarVoipToolbar/NavBarItemVoipDialer.tsx rename to apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipDialerAction.ts index ce3f0b8294587..765c6af619d79 100644 --- a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/NavBarItemVoipDialer.tsx +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipDialerAction.ts @@ -1,19 +1,13 @@ -import { NavBarItem } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useLayout } from '@rocket.chat/ui-contexts'; import { useVoipDialer, useVoipState } from '@rocket.chat/ui-voip'; -import type { HTMLAttributes } from 'react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -type NavBarItemVoipDialerProps = Omit, 'is'> & { - primary?: boolean; -}; - -const NavBarItemVoipDialer = (props: NavBarItemVoipDialerProps) => { +export const useVoipDialerAction = () => { const { t } = useTranslation(); const { sidebar } = useLayout(); - const { clientError, isEnabled, isReady, isRegistered } = useVoipState(); + const { clientError, isReady, isRegistered } = useVoipState(); const { open: isDialerOpen, openDialer, closeDialer } = useVoipDialer(); const handleToggleDialer = useEffectEvent(() => { @@ -33,16 +27,5 @@ const NavBarItemVoipDialer = (props: NavBarItemVoipDialerProps) => { return t('New_Call'); }, [clientError, isReady, isRegistered, t]); - return isEnabled ? ( - - ) : null; + return { handleToggleDialer, title, isPressed: isDialerOpen, isDisabled: !isReady || !isRegistered }; }; - -export default NavBarItemVoipDialer; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipTogglerAction.ts similarity index 55% rename from apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx rename to apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipTogglerAction.ts index 6025affc42f99..b60e13804644e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/hooks/useVoipTogglerAction.ts @@ -1,16 +1,15 @@ -import { Box } from '@rocket.chat/fuselage'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import type { Keys } from '@rocket.chat/icons'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useVoipAPI, useVoipState } from '@rocket.chat/ui-voip'; import { useMutation } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undefined => { +export const useVoipTogglerAction = () => { const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { clientError, isEnabled, isReady, isRegistered, isReconnecting } = useVoipState(); + const { clientError, isReady, isRegistered, isReconnecting } = useVoipState(); const { register, unregister, onRegisteredOnce, onUnregisteredOnce } = useVoipAPI(); const toggleVoip = useMutation({ @@ -35,7 +34,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef }, }); - const tooltip = useMemo(() => { + const title = useMemo(() => { if (clientError) { return t(clientError.message); } @@ -48,30 +47,14 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef return t('Reconnecting'); } - return ''; - }, [clientError, isReady, toggleVoip.isPending, t, isReconnecting]); + return isRegistered ? t('Disable_voice_calling') : t('Enable_voice_calling'); + }, [clientError, isRegistered, isReconnecting, isReady, toggleVoip.isPending, t]); - return useMemo(() => { - if (!isEnabled) { - return; - } - - return { - items: [ - { - id: 'toggle-voip', - icon: isRegistered ? 'phone-disabled' : 'phone', - disabled: !isReady || toggleVoip.isPending || isReconnecting, - onClick: () => toggleVoip.mutate(), - content: ( - - {isRegistered ? t('Disable_voice_calling') : t('Enable_voice_calling')} - - ), - }, - ], - }; - }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip, isReconnecting]); + return { + handleToggleVoip: () => toggleVoip.mutate(), + title, + icon: (isRegistered ? 'phone' : 'phone-disabled') as Keys, + isRegistered, + isDisabled: !isReady || toggleVoip.isPending || isReconnecting, + }; }; - -export default useVoipItemsSection; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipGroup/index.ts b/apps/meteor/client/NavBarV2/NavBarVoipGroup/index.ts new file mode 100644 index 0000000000000..a9d8cb47bd8a4 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarVoipGroup/index.ts @@ -0,0 +1 @@ +export { default } from './NavBarVoipGroup'; diff --git a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/index.ts b/apps/meteor/client/NavBarV2/NavBarVoipToolbar/index.ts deleted file mode 100644 index 7f6d317af2298..0000000000000 --- a/apps/meteor/client/NavBarV2/NavBarVoipToolbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as NavBarItemVoipDialer } from './NavBarItemVoipDialer'; diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx index 09b0e62c8f115..2d9aefb14ccef 100644 --- a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx +++ b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx @@ -1,11 +1,11 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useDocumentTitle } from '@rocket.chat/ui-client'; +import { useDocumentTitle, FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { HeaderToolbar } from '../Header'; -import SidebarToggler from '../SidebarToggler'; +import { SidebarTogglerV2 } from '../SidebarTogglerV2'; type PageHeaderProps = { title: ReactNode; @@ -33,9 +33,14 @@ const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props color='default' > {isMobile && ( - - - + + + + + + + {null} + )} {onClickBack && } diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx new file mode 100644 index 0000000000000..6f4762e858b7a --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarToggler.tsx @@ -0,0 +1,25 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useLayout, useSession } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import { memo } from 'react'; + +import SidebarTogglerButton from './SidebarTogglerButton'; +import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; + +const SideBarToggler = (): ReactElement => { + const { sidebar } = useLayout(); + const isLayoutEmbedded = useEmbeddedLayout(); + const unreadMessagesBadge = useSession('unread') as number | string | undefined; + + const toggleSidebar = useEffectEvent(() => sidebar.toggle()); + + return ( + + ); +}; + +export default memo(SideBarToggler); diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerBadge.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerBadge.tsx new file mode 100644 index 0000000000000..6de65e701bc89 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerBadge.tsx @@ -0,0 +1,22 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Badge } from '@rocket.chat/fuselage'; +import type { ReactNode } from 'react'; + +type SidebarTogglerBadgeProps = { + children?: ReactNode; +}; + +const SidebarTogglerBadge = ({ children }: SidebarTogglerBadgeProps) => ( + + {children} + +); + +export default SidebarTogglerBadge; diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.stories.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.stories.tsx new file mode 100644 index 0000000000000..4c1aa58ab7650 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.stories.tsx @@ -0,0 +1,24 @@ +import { action } from '@storybook/addon-actions'; +import type { Meta, StoryFn } from '@storybook/react'; + +import SidebarTogglerButton from './SidebarTogglerButton'; + +export default { + title: 'Components/SidebarToggler/SidebarTogglerButtonV2', + component: SidebarTogglerButton, + parameters: { + layout: 'centered', + controls: { hideNoControlsWarning: true }, + actions: { argTypesRegex: '^on.*' }, + }, +} satisfies Meta; + +export const Example: StoryFn = () => ; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +export const WithBadge = Template.bind({}); +WithBadge.args = { + badge: 99, +}; diff --git a/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx new file mode 100644 index 0000000000000..6da63f4786a78 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/SidebarTogglerButton.tsx @@ -0,0 +1,24 @@ +import { Box, IconButton } from '@rocket.chat/fuselage'; +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SidebarTogglerBadge from './SidebarTogglerBadge'; + +type SideBarTogglerButtonProps = { + pressed?: boolean; + badge?: ReactNode; + onClick: () => void; +}; + +const SideBarTogglerButton = ({ pressed, badge, onClick }: SideBarTogglerButtonProps) => { + const { t } = useTranslation(); + + return ( + + + {badge && {badge}} + + ); +}; + +export default SideBarTogglerButton; diff --git a/apps/meteor/client/components/SidebarTogglerV2/index.ts b/apps/meteor/client/components/SidebarTogglerV2/index.ts new file mode 100644 index 0000000000000..4698327ba2938 --- /dev/null +++ b/apps/meteor/client/components/SidebarTogglerV2/index.ts @@ -0,0 +1 @@ +export { default as SidebarTogglerV2 } from './SidebarToggler'; diff --git a/apps/meteor/client/providers/LayoutProvider.tsx b/apps/meteor/client/providers/LayoutProvider.tsx index 46fd033667561..391c9228dcb25 100644 --- a/apps/meteor/client/providers/LayoutProvider.tsx +++ b/apps/meteor/client/providers/LayoutProvider.tsx @@ -17,6 +17,7 @@ type LayoutProviderProps = { const LayoutProvider = ({ children }: LayoutProviderProps) => { const showTopNavbarEmbeddedLayout = useSetting('UI_Show_top_navbar_embedded_layout', false); const [isCollapsed, setIsCollapsed] = useState(false); + const [navBarSearchExpanded, setNavBarSearchExpanded] = useState(false); const breakpoints = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"] const [hiddenActions, setHiddenActions] = useState(hiddenActionsDefaultValue); @@ -25,10 +26,13 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { const [isEmbedded] = useState(() => router.getSearchParameters().layout === 'embedded'); const isMobile = !breakpoints.includes('md'); + const isTablet = !breakpoints.includes('lg'); + + const shouldToggle = isTablet || isMobile; useEffect(() => { - setIsCollapsed(isMobile); - }, [isMobile]); + setIsCollapsed(shouldToggle); + }, [shouldToggle]); useEffect(() => { const eventHandler = (event: MessageEvent) => { @@ -48,11 +52,17 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { value={useMemo( () => ({ isMobile, + isTablet, isEmbedded, showTopNavbarEmbeddedLayout, + navbar: { + searchExpanded: navBarSearchExpanded, + expandSearch: isMobile ? () => setNavBarSearchExpanded(true) : undefined, + collapseSearch: isMobile ? () => setNavBarSearchExpanded(false) : undefined, + }, sidebar: { isCollapsed, - toggle: isMobile ? () => setIsCollapsed((isCollapsed) => !isCollapsed) : () => undefined, + toggle: shouldToggle ? () => setIsCollapsed((isCollapsed) => !isCollapsed) : () => undefined, collapse: () => setIsCollapsed(true), expand: () => setIsCollapsed(false), close: () => (isEmbedded ? setIsCollapsed(true) : router.navigate('/home')), @@ -68,7 +78,18 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { contextualBarPosition: breakpoints.includes('sm') ? (breakpoints.includes('lg') ? 'relative' : 'absolute') : 'fixed', hiddenActions, }), - [isMobile, isEmbedded, showTopNavbarEmbeddedLayout, isCollapsed, breakpoints, router, hiddenActions], + [ + isMobile, + isTablet, + navBarSearchExpanded, + isEmbedded, + showTopNavbarEmbeddedLayout, + isCollapsed, + shouldToggle, + breakpoints, + hiddenActions, + router, + ], )} /> ); diff --git a/apps/meteor/client/sidebarv2/Sidebar.tsx b/apps/meteor/client/sidebarv2/Sidebar.tsx index e292a1a0da80d..278c8b4f9d589 100644 --- a/apps/meteor/client/sidebarv2/Sidebar.tsx +++ b/apps/meteor/client/sidebarv2/Sidebar.tsx @@ -4,7 +4,6 @@ import { memo } from 'react'; import SidebarRoomList from './RoomList'; import SidebarFooter from './footer'; -import SearchSection from './header/SearchSection'; import BannerSection from './sections/BannerSection'; const Sidebar = () => { @@ -18,7 +17,6 @@ const Sidebar = () => { .filter(Boolean) .join(' ')} > - diff --git a/apps/meteor/client/sidebarv2/SidebarRegion.tsx b/apps/meteor/client/sidebarv2/SidebarRegion.tsx index 9a09cabfa7be1..83eb4e750c6da 100644 --- a/apps/meteor/client/sidebarv2/SidebarRegion.tsx +++ b/apps/meteor/client/sidebarv2/SidebarRegion.tsx @@ -7,7 +7,7 @@ import { FocusScope } from 'react-aria'; import Sidebar from './Sidebar'; const SidebarRegion = () => { - const { isMobile, sidebar } = useLayout(); + const { isTablet, sidebar } = useLayout(); const sidebarMobileClass = css` position: absolute; @@ -93,14 +93,14 @@ const SidebarRegion = () => { - {isMobile && ( - sidebar.toggle()}> + {isTablet && ( + sidebar.toggle()} /> )} ); diff --git a/apps/meteor/client/sidebarv2/header/SearchList.tsx b/apps/meteor/client/sidebarv2/header/SearchList.tsx deleted file mode 100644 index 2f9d09b704ab2..0000000000000 --- a/apps/meteor/client/sidebarv2/header/SearchList.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Box, SidebarV2GroupTitle } from '@rocket.chat/fuselage'; -import { useTranslation, useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; -import type { MouseEventHandler, ReactElement } from 'react'; -import { useMemo, useRef } from 'react'; -import { Virtuoso } from 'react-virtuoso'; - -import { VirtualizedScrollbars } from '../../components/CustomScrollbars'; -import RoomListWrapper from '../RoomList/RoomListWrapper'; -import { useAvatarTemplate } from '../hooks/useAvatarTemplate'; -import { usePreventDefault } from '../hooks/usePreventDefault'; -import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; -import Row from '../search/Row'; -import { useSearchItems } from './hooks/useSearchItems'; - -type SearchListProps = { filterText: string; onEscSearch: () => void; showRecentList?: boolean }; - -const SearchList = ({ filterText, onEscSearch, showRecentList }: SearchListProps) => { - const t = useTranslation(); - - const boxRef = useRef(null); - usePreventDefault(boxRef); - - const { data: items = [], isLoading } = useSearchItems(filterText); - - const sidebarViewMode = useUserPreference('sidebarViewMode'); - const useRealName = useSetting('UI_Use_Real_Name'); - - const sideBarItemTemplate = useTemplateByViewMode(); - const avatarTemplate = useAvatarTemplate(); - - const extended = sidebarViewMode === 'extended'; - - const itemData = useMemo( - () => ({ - items, - t, - SidebarItemTemplate: sideBarItemTemplate, - avatarTemplate, - useRealName, - extended, - sidebarViewMode, - }), - [avatarTemplate, extended, items, useRealName, sideBarItemTemplate, sidebarViewMode, t], - ); - - const handleClick: MouseEventHandler = (e): void => { - if (e.target instanceof Element && [e.target.tagName, e.target.parentElement?.tagName].includes('BUTTON')) { - return; - } - return onEscSearch(); - }; - - return ( - - {showRecentList && } - - room._id} - itemContent={(_, data): ReactElement => } - /> - - - ); -}; - -export default SearchList; diff --git a/apps/meteor/client/sidebarv2/header/SearchSection.tsx b/apps/meteor/client/sidebarv2/header/SearchSection.tsx deleted file mode 100644 index 2fcdceb4ec035..0000000000000 --- a/apps/meteor/client/sidebarv2/header/SearchSection.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Icon, TextInput, Palette, SidebarV2Section, IconButton } from '@rocket.chat/fuselage'; -import { useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, useUser } from '@rocket.chat/ui-contexts'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { FocusScope, useFocusManager } from 'react-aria'; -import { useForm } from 'react-hook-form'; -import tinykeys from 'tinykeys'; - -import SearchList from './SearchList'; -import CreateRoom from './actions/CreateRoom'; -import Sort from './actions/Sort'; - -const wrapperStyle = css` - position: absolute; - height: 100%; - width: 100%; - display: flex; - flex-direction: column; - z-index: 99; - top: 0; - left: 0; - background-color: ${Palette.surface['surface-sidebar']}; -`; - -const mobileCheck = function () { - let check = false; - (function (a: string) { - if ( - /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( - a, - ) || - /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( - a.substr(0, 4), - ) - ) - check = true; - })(navigator.userAgent || navigator.vendor || window.opera || ''); - return check; -}; - -const shortcut = ((): string => { - if (navigator.userAgentData?.mobile || mobileCheck()) { - return ''; - } - if (window.navigator.platform.toLowerCase().includes('mac')) { - return '(\u2318+K)'; - } - return '(Ctrl+K)'; -})(); - -const isRecentButton = (node: EventTarget) => (node as HTMLElement).title === 'Recent'; - -const SearchSection = () => { - const t = useTranslation(); - const focusManager = useFocusManager(); - const user = useUser(); - const [recentButtonPressed, setRecentButtonPressed] = useState(false); - - const { - formState: { isDirty }, - register, - watch, - resetField, - setFocus, - } = useForm({ defaultValues: { filterText: '' } }); - const { filterText } = watch(); - const { ref: filterRef, ...rest } = register('filterText'); - - const showRecentList = Boolean(recentButtonPressed && !filterText); - - const inputRef = useRef(null); - const wrapperRef = useRef(null); - const mergedRefs = useMergedRefs(filterRef, inputRef); - - const handleEscSearch = useCallback(() => { - resetField('filterText'); - setRecentButtonPressed(false); - inputRef.current?.blur(); - }, [resetField]); - - useOutsideClick([wrapperRef], handleEscSearch); - - useEffect(() => { - const unsubscribe = tinykeys(window, { - '$mod+K': (event) => { - event.preventDefault(); - setFocus('filterText'); - }, - '$mod+P': (event) => { - event.preventDefault(); - setFocus('filterText'); - }, - 'Shift+$mod+K': (event) => { - event.preventDefault(); - setRecentButtonPressed(true); - focusManager?.focusNext({ accept: (node) => isRecentButton(node) }); - }, - 'Escape': (event) => { - event.preventDefault(); - handleEscSearch(); - }, - }); - - return (): void => { - unsubscribe(); - }; - }, [focusManager, handleEscSearch, setFocus]); - - const placeholder = [t('Search'), shortcut].filter(Boolean).join(' '); - - return ( - - - } - /> - - {user && !isDirty && ( - <> - setRecentButtonPressed(!recentButtonPressed)} - pressed={recentButtonPressed} - /> - {recentButtonPressed ? : } - - - )} - - {(isDirty || recentButtonPressed) && ( - - - - )} - - ); -}; - -export default SearchSection; diff --git a/apps/meteor/client/views/room/HeaderV2/Header.tsx b/apps/meteor/client/views/room/HeaderV2/Header.tsx index 6501dad41bd40..067abcd88f05c 100644 --- a/apps/meteor/client/views/room/HeaderV2/Header.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Header.tsx @@ -2,10 +2,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isVoipRoom } from '@rocket.chat/core-typings'; import { useLayout, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import { lazy, memo, useMemo } from 'react'; - -import { HeaderToolbar } from '../../../components/Header'; -import SidebarToggler from '../../../components/SidebarToggler'; +import { lazy, memo } from 'react'; const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader')); const VoipRoomHeader = lazy(() => import('./Omnichannel/VoipRoomHeader')); @@ -17,39 +14,28 @@ type HeaderProps = { }; const Header = ({ room }: HeaderProps): ReactElement | null => { - const { isMobile, isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); + const { isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); const encrypted = Boolean(room.encrypted); const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false); const shouldDisplayE2EESetup = encrypted && !unencryptedMessagesAllowed; - const slots = useMemo( - () => ({ - start: isMobile && ( - - - - ), - }), - [isMobile], - ); - if (isEmbedded && !showTopNavbarEmbeddedLayout) { return null; } if (room.t === 'l') { - return ; + return ; } if (isVoipRoom(room)) { - return ; + return ; } if (shouldDisplayE2EESetup) { - return ; + return ; } - return ; + return ; }; export default memo(Header); diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx index 0c5bc0458cf5c..a937bc93e7208 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx @@ -1,31 +1,14 @@ -import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; -import type { ReactNode } from 'react'; +import { useRouter } from '@rocket.chat/ui-contexts'; import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { HeaderToolbar } from '../../../../components/Header'; -import SidebarToggler from '../../../../components/SidebarToggler'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; import RoomHeader from '../RoomHeader'; import BackButton from './BackButton'; import OmnichannelRoomHeaderTag from './OmnichannelRoomHeaderTag'; import QuickActions from './QuickActions'; -type OmnichannelRoomHeaderProps = { - slots: { - start?: ReactNode; - preContent?: ReactNode; - insideContent?: ReactNode; - posContent?: ReactNode; - end?: ReactNode; - toolbox?: { - pre?: ReactNode; - content?: ReactNode; - pos?: ReactNode; - }; - }; -}; - -const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps) => { +const OmnichannelRoomHeader = () => { const router = useRouter(); const currentRouteName = useSyncExternalStore( @@ -33,22 +16,19 @@ const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps useCallback(() => router.getRouteName(), [router]), ); - const { isMobile } = useLayout(); const room = useOmnichannelRoom(); const slots = useMemo( () => ({ - ...parentSlot, - start: (!!isMobile || currentRouteName === 'omnichannel-directory' || currentRouteName === 'omnichannel-current-chats') && ( + start: (currentRouteName === 'omnichannel-directory' || currentRouteName === 'omnichannel-current-chats') && ( - {isMobile && } ), insideContent: , posContent: , }), - [isMobile, currentRouteName, parentSlot], + [currentRouteName], ); return ; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx index 9ec8d738d43ec..e8e492478bfc2 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx @@ -1,9 +1,8 @@ import type { IVoipRoom } from '@rocket.chat/core-typings'; -import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; +import { useRouter } from '@rocket.chat/ui-contexts'; import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { HeaderToolbar } from '../../../../components/Header'; -import SidebarToggler from '../../../../components/SidebarToggler'; import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; import type { RoomHeaderProps } from '../RoomHeader'; import RoomHeader from '../RoomHeader'; @@ -13,7 +12,7 @@ type VoipRoomHeaderProps = { room: IVoipRoom; } & Omit; -const VoipRoomHeader = ({ slots: parentSlot, room }: VoipRoomHeaderProps) => { +const VoipRoomHeader = ({ room }: VoipRoomHeaderProps) => { const router = useRouter(); const currentRouteName = useSyncExternalStore( @@ -21,19 +20,13 @@ const VoipRoomHeader = ({ slots: parentSlot, room }: VoipRoomHeaderProps) => { useCallback(() => router.getRouteName(), [router]), ); - const { isMobile } = useLayout(); - const slots = useMemo( () => ({ - ...parentSlot, - start: (!!isMobile || currentRouteName === 'omnichannel-directory') && ( - - {isMobile && } - {currentRouteName === 'omnichannel-directory' && } - + start: currentRouteName === 'omnichannel-directory' && ( + {currentRouteName === 'omnichannel-directory' && } ), }), - [isMobile, currentRouteName, parentSlot], + [currentRouteName], ); return ; }; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx index 4f3100668afed..3fd1677675216 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx @@ -16,7 +16,7 @@ import { Header, HeaderContent, HeaderContentRow, HeaderToolbar } from '../../.. export type RoomHeaderProps = { room: IRoom; - slots: { + slots?: { start?: ReactNode; preContent?: ReactNode; insideContent?: ReactNode; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx index f1b959cd0ce94..be1ac31e733e9 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx @@ -9,15 +9,15 @@ import { useE2EEState } from '../hooks/useE2EEState'; const RoomToolboxE2EESetup = lazy(() => import('./RoomToolbox/RoomToolboxE2EESetup')); -const RoomHeaderE2EESetup = ({ room, slots = {} }: RoomHeaderProps) => { +const RoomHeaderE2EESetup = ({ room }: RoomHeaderProps) => { const e2eeState = useE2EEState(); const e2eRoomState = useE2EERoomState(room._id); if (e2eeState === E2EEState.SAVE_PASSWORD || e2eeState === E2EEState.ENTER_PASSWORD || e2eRoomState === E2ERoomState.WAITING_KEYS) { - return } />; + return } />; } - return ; + return ; }; export default RoomHeaderE2EESetup; diff --git a/apps/meteor/client/views/room/NotSubscribedRoom.tsx b/apps/meteor/client/views/room/NotSubscribedRoom.tsx index 2cf97519a3018..f055ead54d0e3 100644 --- a/apps/meteor/client/views/room/NotSubscribedRoom.tsx +++ b/apps/meteor/client/views/room/NotSubscribedRoom.tsx @@ -1,12 +1,12 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { Header, HeaderToolbar } from '@rocket.chat/ui-client'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, Header, HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import RoomLayout from './layout/RoomLayout'; -import SidebarToggler from '../../components/SidebarToggler'; +import { SidebarTogglerV2 } from '../../components/SidebarTogglerV2'; import { useJoinRoom } from '../../hooks/useJoinRoom'; type NotSubscribedRoomProps = { @@ -26,11 +26,16 @@ const NotSubscribedRoom = ({ rid, reference, type }: NotSubscribedRoomProps): Re - - - - + + +
+ + + +
+
+ {null} +
) } body={ diff --git a/apps/meteor/client/views/room/RoomNotFound.tsx b/apps/meteor/client/views/room/RoomNotFound.tsx index 6c3b849a60b35..3d13b1a5e930f 100644 --- a/apps/meteor/client/views/room/RoomNotFound.tsx +++ b/apps/meteor/client/views/room/RoomNotFound.tsx @@ -1,12 +1,12 @@ import { Box } from '@rocket.chat/fuselage'; -import { Header, HeaderToolbar } from '@rocket.chat/ui-client'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, Header, HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import RoomLayout from './layout/RoomLayout'; import NotFoundState from '../../components/NotFoundState'; -import SidebarToggler from '../../components/SidebarToggler'; +import { SidebarTogglerV2 } from '../../components/SidebarTogglerV2'; const RoomNotFound = (): ReactElement => { const { t } = useTranslation(); @@ -16,11 +16,16 @@ const RoomNotFound = (): ReactElement => { - - - - + + +
+ + + +
+
+ {null} +
) } body={ diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index a8b105222e4e3..b477cf664ac2b 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -90,11 +90,40 @@ test.describe.serial('feature preview', () => { await expect(poHomeChannel.navbar.navbar).toBeVisible(); }); - test('should display "Recent" button on sidebar search section, and display recent chats when clicked', async ({ page }) => { + test('should render global header navigation', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.btnRecent.click(); - await expect(poHomeChannel.sidebar.sidebar.getByRole('heading', { name: 'Recent' })).toBeVisible(); + await test.step('should display recent chats when navbar search is clicked', async () => { + await poHomeChannel.navbar.searchInput.click(); + await expect(poHomeChannel.navbar.searchList).toBeVisible(); + await poHomeChannel.navbar.searchInput.blur(); + }); + + await test.step('should display home and directory button', async () => { + await expect(poHomeChannel.navbar.homeButton).toBeVisible(); + await expect(poHomeChannel.navbar.btnDirectory).toBeVisible(); + }); + + await test.step('should display home and directory inside a menu and sidebar toggler in tablet view', async () => { + await page.setViewportSize({ width: 1023, height: 767 }); + await expect(poHomeChannel.navbar.btnMenuPages).toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler).toBeVisible(); + }); + + await test.step('should display voice and omnichannel items inside a menu in mobile view', async () => { + await page.setViewportSize({ width: 767, height: 510 }); + await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).toBeVisible(); + }); + + await test.step('should hide everything else when navbar search is focused in mobile view', async () => { + await page.setViewportSize({ width: 767, height: 510 }); + await poHomeChannel.navbar.searchInput.click(); + + await expect(poHomeChannel.navbar.btnMenuPages).not.toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler).not.toBeVisible(); + await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).not.toBeVisible(); + await expect(poHomeChannel.navbar.groupHistoryNavigation).not.toBeVisible(); + }); }); test('should not display room topic in direct message', async ({ page }) => { @@ -169,10 +198,9 @@ test.describe.serial('feature preview', () => { test('should show unread badge on collapser when group is collapsed and has unread items', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.navbar.openChat(targetChannel); await poHomeChannel.content.sendMessage('hello world'); - await poHomeChannel.sidebar.typeSearch(targetChannel); const item = poHomeChannel.sidebar.getSearchRoomByName(targetChannel); await poHomeChannel.sidebar.markItemAsUnread(item); await poHomeChannel.sidebar.escSearch(); @@ -185,7 +213,7 @@ test.describe.serial('feature preview', () => { test('should not show NavBar in embedded layout', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.navbar.openChat(targetChannel); await expect(page.locator('role=navigation[name="header"]')).toBeVisible(); const embeddedLayoutURL = `${page.url()}?layout=embedded`; await page.goto(embeddedLayoutURL); @@ -194,7 +222,7 @@ test.describe.serial('feature preview', () => { test('should display the room header properly', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetDiscussion.fname); + await poHomeChannel.navbar.openChat(targetDiscussion.fname); await test.step('should not display avatar in room header', async () => { await expect(page.locator('main').locator('header').getByRole('figure')).not.toBeVisible(); @@ -330,8 +358,8 @@ test.describe.serial('feature preview', () => { await page.goto('/home'); const message = 'hello world'; - await poHomeChannel.sidebar.setDisplayMode('Extended'); - await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.navbar.setDisplayMode('Extended'); + await poHomeChannel.navbar.openChat(sidepanelTeam); await poHomeChannel.content.sendMessage(message); await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); }); @@ -341,8 +369,8 @@ test.describe.serial('feature preview', () => { const message = 'hello > world'; const parsedWrong = 'hello > world'; - await poHomeChannel.sidebar.setDisplayMode('Extended'); - await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.navbar.setDisplayMode('Extended'); + await poHomeChannel.navbar.openChat(sidepanelTeam); await poHomeChannel.content.sendMessage(message); await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts index 55cda22ed09ef..c9e432527d0b8 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { expect } from '../../utils/test'; + export class Navbar { private readonly page: Page; @@ -11,11 +13,72 @@ export class Navbar { return this.page.getByRole('navigation', { name: 'header' }); } - get pagesToolbar(): Locator { - return this.navbar.getByRole('toolbar', { name: 'Pages' }); + get btnSidebarToggler(): Locator { + return this.navbar.getByRole('button', { name: 'Open sidebar' }); + } + + get btnVoiceAndOmnichannel(): Locator { + return this.navbar.getByRole('button', { name: 'Voice and omnichannel' }); + } + + get groupHistoryNavigation(): Locator { + return this.navbar.getByRole('group', { name: 'History navigation' }); + } + + get pagesGroup(): Locator { + return this.navbar.getByRole('group', { name: 'Pages and actions' }); } get homeButton(): Locator { - return this.pagesToolbar.getByRole('button', { name: 'Home' }); + return this.pagesGroup.getByRole('button', { name: 'Home' }); + } + + get btnDirectory(): Locator { + return this.pagesGroup.getByRole('button', { name: 'Directory' }); + } + + get btnMenuPages(): Locator { + return this.pagesGroup.getByRole('button', { name: 'Pages' }); + } + + get navbarSearchSection(): Locator { + return this.navbar.getByRole('search'); + } + + get searchInput(): Locator { + return this.navbarSearchSection.getByRole('combobox'); + } + + get searchList(): Locator { + return this.navbarSearchSection.getByRole('listbox', { name: 'Channels' }); + } + + async typeSearch(name: string): Promise { + return this.searchInput.fill(name); + } + + async waitForChannel(): Promise { + await this.page.locator('role=main').waitFor(); + await this.page.locator('role=main >> role=heading[level=1]').waitFor(); + const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); + await messageList.waitFor(); + + await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); + } + + getSearchRoomByName(name: string): Locator { + return this.searchList.getByRole('option', { name }); + } + + async openChat(name: string): Promise { + await this.typeSearch(name); + await this.getSearchRoomByName(name).click(); + await this.waitForChannel(); + } + + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { + await this.pagesGroup.getByRole('button', { name: 'Display', exact: true }).click(); + await this.pagesGroup.getByRole('menuitemcheckbox', { name: mode }).click(); + await this.pagesGroup.click(); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 6c546b2fc9368..27ae799b4e4d9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -1,7 +1,5 @@ import type { Locator, Page } from '@playwright/test'; -import { expect } from '../../utils/test'; - export class Sidebar { private readonly page: Page; @@ -14,20 +12,12 @@ export class Sidebar { return this.page.getByRole('navigation', { name: 'sidebar' }); } - get sidebarSearchSection(): Locator { - return this.sidebar.getByRole('search'); - } - - get btnRecent(): Locator { - return this.sidebarSearchSection.getByRole('button', { name: 'Recent' }); - } - get channelsList(): Locator { return this.sidebar.getByRole('list', { name: 'Channels' }); } - get searchList(): Locator { - return this.sidebar.getByRole('search').getByRole('list', { name: 'Channels' }); + getSearchRoomByName(name: string) { + return this.channelsList.getByRole('link', { name }); } get firstCollapser(): Locator { @@ -38,43 +28,10 @@ export class Sidebar { return this.channelsList.getByRole('listitem').first(); } - get searchInput(): Locator { - return this.sidebarSearchSection.getByRole('searchbox'); - } - - async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { - await this.sidebarSearchSection.getByRole('button', { name: 'Display', exact: true }).click(); - await this.sidebarSearchSection.getByRole('menuitemcheckbox', { name: mode }).click(); - await this.sidebarSearchSection.click(); - } - async escSearch(): Promise { await this.page.keyboard.press('Escape'); } - async waitForChannel(): Promise { - await this.page.locator('role=main').waitFor(); - await this.page.locator('role=main >> role=heading[level=1]').waitFor(); - const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); - await messageList.waitFor(); - - await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); - } - - async typeSearch(name: string): Promise { - return this.searchInput.fill(name); - } - - getSearchRoomByName(name: string): Locator { - return this.searchList.getByRole('link', { name }); - } - - async openChat(name: string): Promise { - await this.typeSearch(name); - await this.getSearchRoomByName(name).click(); - await this.waitForChannel(); - } - async markItemAsUnread(item: Locator): Promise { await item.hover(); await item.focus(); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 31495c3ea1dd7..d20ebda4f568f 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -761,6 +761,7 @@ "away": "away", "Away": "Away", "Back": "Back", + "Back_in_history": "Back in history", "Back_to_applications": "Back to applications", "Back_to_chat": "Back to chat", "Back_to_integration_detail": "Back to the integration detail", @@ -2396,6 +2397,7 @@ "Estimated_due_time": "Estimated due time", "error-password-in-history": "Entered password has been previously used", "Forward": "Forward", + "Forward_in_history": "Forward in history", "Estimated_due_time_in_minutes": "Estimated due time (time in minutes)", "Forward_chat": "Forward chat", "Forward_to_department": "Forward to department", @@ -2510,6 +2512,7 @@ "Highlights_How_To": "To be notified when someone mentions a word or phrase, add it here. You can separate words or phrases with commas. Highlight Words are not case sensitive.", "Highlights_List": "Highlight words", "History": "History", + "History_navigation": "History navigation", "every_12_hours": "Once every 12 hours", "every_24_hours": "Once every 24 hours", "every_48_hours": "Once every 48 hours", @@ -4684,6 +4687,7 @@ "set-react-when-readonly_description": "Permission to set the ability to react to messages in a read only channel", "set-readonly": "Set ReadOnly", "Pages": "Pages", + "Pages_and_actions": "Pages and actions", "set-readonly_description": "Permission to set a channel to read only channel", "Settings": "Settings", "Setting": "Setting", @@ -5978,6 +5982,7 @@ "Troubleshoot_Force_Caching_Version": "Force browsers to clear networking cache based on version change", "Troubleshoot_Force_Caching_Version_Alert": "If the value provided is not empty and different from previous one the browsers will try to clear the cache. This setting should not be set for a long period since it affects the browser performance, please clear it as soon as possible.", "Try_now": "Try now", + "Try_entering_a_different_search_term": "Try entering a different search term.", "Try_different_filters": "Try different filters", "Try_searching_in_the_marketplace_instead": "Try searching in the Marketplace instead", "Turn_on_video": "Turn on video", @@ -6217,6 +6222,7 @@ "Visitor_Name_Placeholder": "Please enter a visitor name...", "Visitor_not_found": "Visitor not found", "Visitor_does_not_exist": "Visitor does not exist!", + "Voice_and_omnichannel": "Voice and omnichannel", "Voice_Call": "Voice Call", "Voice_call": "Voice call", "Voice_call_extension": "Voice call extension", @@ -6775,6 +6781,7 @@ "Zoom_out": "Zoom out", "Zoom_in": "Zoom in", "Close_gallery": "Close gallery", + "Close_sidebar": "Close sidebar", "Next_image": "Next Image", "Previous_image": "Previous image", "Image_gallery": "Image gallery", @@ -6784,7 +6791,7 @@ "You_cant_take_chats_offline": "You cannot take new conversations because you're offline", "New_navigation": "Enhanced navigation experience", "New_navigation_description": "Explore our improved navigation, designed with clear scopes for easy access to what you need. This change serves as the foundation for future advancements in navigation management.", - "Workspace_and_user_settings": "Workspace and user settings", + "Workspace_and_user_preferences": "Workspace and user preferences", "Sidebar_Sections_Order": "Sidebar sections order", "Sidebar_Sections_Order_Description": "Select the categories in your preferred order", "Incoming_Calls": "Incoming calls", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 6915b9d7bc663..e48ae441ee9cb 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -6159,7 +6159,6 @@ "You_cant_take_chats_offline": "Du kan ikke ta nye samtaler fordi du er frakoblet", "New_navigation": "Forbedret navigasjonsopplevelse", "New_navigation_description": "Utforsk vår forbedrede navigasjon, designet med ett klart omfang for enkel tilgang til det du trenger. Denne endringen fungerer som grunnlaget for fremtidige fremskritt innen navigasjonsadministrasjon.", - "Workspace_and_user_settings": "Arbeidsområde og brukerinnstillinger", "Sidebar_Sections_Order": "Rekkefølge på sidefeltseksjoner", "Sidebar_Sections_Order_Description": "Velg kategoriene i din foretrukne rekkefølge", "Incoming_Calls": "Innkommende anrop", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 42d0418e2ba94..7f79e1b825117 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -6159,7 +6159,6 @@ "You_cant_take_chats_offline": "Du kan ikke ta nye samtaler fordi du er frakoblet", "New_navigation": "Forbedret navigasjonsopplevelse", "New_navigation_description": "Utforsk vår forbedrede navigasjon, designet med ett klart omfang for enkel tilgang til det du trenger. Denne endringen fungerer som grunnlaget for fremtidige fremskritt innen navigasjonsadministrasjon.", - "Workspace_and_user_settings": "Arbeidsområde og brukerinnstillinger", "Sidebar_Sections_Order": "Rekkefølge på sidefeltseksjoner", "Sidebar_Sections_Order_Description": "Velg kategoriene i din foretrukne rekkefølge", "Incoming_Calls": "Innkommende anrop", diff --git a/packages/ui-client/src/components/HeaderV2/Header.tsx b/packages/ui-client/src/components/HeaderV2/Header.tsx index 4515a5fc6104c..fe36431541265 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.tsx @@ -1,40 +1,35 @@ import { Box } from '@rocket.chat/fuselage'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import HeaderDivider from './HeaderDivider'; type HeaderProps = ComponentPropsWithoutRef; -const Header = (props: HeaderProps) => { - const { isMobile } = useLayout(); - - return ( +const Header = (props: HeaderProps) => ( + - - - - ); -}; + flexDirection='row' + bg='room' + {...props} + /> + + +); export default Header; diff --git a/packages/ui-contexts/src/LayoutContext.ts b/packages/ui-contexts/src/LayoutContext.ts index 2d900b5a7612e..3e4c9a665a864 100644 --- a/packages/ui-contexts/src/LayoutContext.ts +++ b/packages/ui-contexts/src/LayoutContext.ts @@ -8,6 +8,7 @@ export type SizeLayout = { export type LayoutContextValue = { isEmbedded: boolean; showTopNavbarEmbeddedLayout: boolean; + isTablet: boolean; isMobile: boolean; roomToolboxExpanded: boolean; sidebar: { @@ -17,6 +18,11 @@ export type LayoutContextValue = { expand: () => void; close: () => void; }; + navbar: { + searchExpanded: boolean; + expandSearch?: () => void; + collapseSearch?: () => void; + }; size: SizeLayout; contextualBarExpanded: boolean; contextualBarPosition: 'absolute' | 'relative' | 'fixed'; @@ -31,8 +37,14 @@ export type LayoutContextValue = { export const LayoutContext = createContext({ isEmbedded: false, showTopNavbarEmbeddedLayout: false, + isTablet: false, isMobile: false, roomToolboxExpanded: true, + navbar: { + searchExpanded: false, + expandSearch: () => undefined, + collapseSearch: () => undefined, + }, sidebar: { isCollapsed: false, toggle: () => undefined,