diff --git a/apps/meteor/app/ui-master/client/body.html b/apps/meteor/app/ui-master/client/body.html deleted file mode 100644 index 641116de7ce..00000000000 --- a/apps/meteor/app/ui-master/client/body.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/meteor/app/ui-master/client/body.js b/apps/meteor/app/ui-master/client/body.js deleted file mode 100644 index c1a46fe25b2..00000000000 --- a/apps/meteor/app/ui-master/client/body.js +++ /dev/null @@ -1,108 +0,0 @@ -import Clipboard from 'clipboard'; -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; - -import { APIClient, t } from '../../utils/client'; -import { settings } from '../../settings'; -import { ChatSubscription } from '../../models/client'; -import { imperativeModal } from '../../../client/lib/imperativeModal'; -import GenericModal from '../../../client/components/GenericModal'; -import { fireGlobalEvent } from '../../../client/lib/utils/fireGlobalEvent'; -import { isLayoutEmbedded } from '../../../client/lib/utils/isLayoutEmbedded'; -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { rtrim } from '../../../lib/utils/stringUtils'; -import './body.html'; - -Template.body.onRendered(function () { - new Clipboard('.clipboard'); - - $(document.body).on('keydown', function (e) { - const unread = Session.get('unread'); - if (e.keyCode === 27 && (e.shiftKey === true || e.ctrlKey === true) && unread != null && unread !== '') { - e.preventDefault(); - e.stopPropagation(); - - const handleClearUnreadAllMessages = () => { - const subscriptions = ChatSubscription.find( - { - open: true, - }, - { - fields: { - unread: 1, - alert: 1, - rid: 1, - t: 1, - name: 1, - ls: 1, - }, - }, - ); - - subscriptions.forEach((subscription) => { - if (subscription.alert || subscription.unread > 0) { - APIClient.post('/v1/subscriptions.read', { rid: subscription.rid, readThreads: true }).catch((err) => { - dispatchToastMessage({ type: 'error', message: err }); - }); - } - }); - - imperativeModal.close(); - }; - - imperativeModal.open({ - component: GenericModal, - props: { - children: t('Are_you_sure_you_want_to_clear_all_unread_messages'), - variant: 'warning', - title: t('Clear_all_unreads_question'), - confirmText: t('Yes_clear_all'), - onClose: imperativeModal.close, - onCancel: imperativeModal.close, - onConfirm: handleClearUnreadAllMessages, - }, - }); - } - }); - - const handleMessageLinkClick = (event) => { - const link = event.currentTarget; - if (link.origin === rtrim(Meteor.absoluteUrl(), '/') && /msg=([a-zA-Z0-9]+)/.test(link.search)) { - fireGlobalEvent('click-message-link', { link: link.pathname + link.search }); - } - }; - - this.autorun(() => { - if (isLayoutEmbedded()) { - $(document.body).on('click', 'a', handleMessageLinkClick); - } else { - $(document.body).off('click', 'a', handleMessageLinkClick); - } - }); - - this.autorun(function (c) { - const w = window; - const d = document; - const script = 'script'; - const l = 'dataLayer'; - const i = settings.get('GoogleTagManager_id'); - if (Match.test(i, String) && i.trim() !== '') { - c.stop(); - return (function (w, d, s, l, i) { - w[l] = w[l] || []; - w[l].push({ - 'gtm.start': new Date().getTime(), - 'event': 'gtm.js', - }); - const f = d.getElementsByTagName(s)[0]; - const j = d.createElement(s); - const dl = l !== 'dataLayer' ? `&l=${l}` : ''; - j.async = true; - j.src = `//www.googletagmanager.com/gtm.js?id=${i}${dl}`; - return f.parentNode.insertBefore(j, f); - })(w, d, script, l, i); - } - }); -}); diff --git a/apps/meteor/app/ui-master/client/index.js b/apps/meteor/app/ui-master/client/index.ts similarity index 54% rename from apps/meteor/app/ui-master/client/index.js rename to apps/meteor/app/ui-master/client/index.ts index a73a7881037..742691f8745 100644 --- a/apps/meteor/app/ui-master/client/index.js +++ b/apps/meteor/app/ui-master/client/index.ts @@ -1,2 +1 @@ -import './body'; import './loading'; diff --git a/apps/meteor/app/ui-master/client/loading/index.js b/apps/meteor/app/ui-master/client/loading/index.ts similarity index 100% rename from apps/meteor/app/ui-master/client/loading/index.js rename to apps/meteor/app/ui-master/client/loading/index.ts diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.js index d88197bd79c..ab7eb8e2115 100644 --- a/apps/meteor/app/ui-master/server/index.js +++ b/apps/meteor/app/ui-master/server/index.js @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { Inject } from 'meteor/meteorhacks:inject-initial'; import { Tracker } from 'meteor/tracker'; -import _ from 'underscore'; import { Settings } from '@rocket.chat/models'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { settings } from '../../settings/server'; import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; +import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; import './scripts'; @@ -124,7 +124,7 @@ Meteor.startup(() => { injectIntoHead('css-theme', ''); }); -const renderDynamicCssList = _.debounce( +const renderDynamicCssList = withDebouncing({ wait: 500 })( Meteor.bindEnvironment(async () => { // const variables = RocketChat.models.Settings.findOne({_id:'theme-custom-variables'}, {fields: { value: 1}}); const colors = await Settings.find({ _id: /theme-color-rc/i }, { projection: { value: 1, editor: 1 } }).toArray(); @@ -139,15 +139,10 @@ const renderDynamicCssList = _.debounce( .join('\n'); injectIntoBody('dynamic-variables', ``); }), - 500, ); renderDynamicCssList(); -// RocketChat.models.Settings.find({_id:'theme-custom-variables'}, {fields: { value: 1}}).observe({ -// changed: renderDynamicCssList -// }); - settings.watchByRegex(/theme-color-rc/i, renderDynamicCssList); injectIntoBody( diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 89ab514669c..6e093dff27b 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -1,14 +1,29 @@ import { PaletteStyleTag } from '@rocket.chat/ui-theming/src/PaletteStyleTag'; import { SidebarPaletteStyleTag } from '@rocket.chat/ui-theming/src/SidebarPaletteStyleTag'; -import type { FC } from 'react'; -import React, { Suspense } from 'react'; +import type { ReactElement } from 'react'; +import React, { useEffect, Suspense } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { appLayout } from '../../lib/appLayout'; import { blazePortals, useBlazePortals } from '../../lib/portals/blazePortals'; import PageLoading from './PageLoading'; +import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; +import { useGoogleTagManager } from './hooks/useGoogleTagManager'; +import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; + +const AppLayout = (): ReactElement => { + useEffect(() => { + document.body.classList.add('color-primary-font-color'); + + return () => { + document.body.classList.add('color-primary-font-color'); + }; + }, []); + + useMessageLinkClicks(); + useGoogleTagManager(); + useEscapeKeyStroke(); -const AppLayout: FC = () => { const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); const [portals] = useBlazePortals(blazePortals); diff --git a/apps/meteor/client/views/root/AppRoot.tsx b/apps/meteor/client/views/root/AppRoot.tsx index 85f0e869d04..cfc1d80ab03 100644 --- a/apps/meteor/client/views/root/AppRoot.tsx +++ b/apps/meteor/client/views/root/AppRoot.tsx @@ -1,6 +1,6 @@ import { QueryClientProvider } from '@tanstack/react-query'; -import type { FC } from 'react'; -import React, { lazy, Suspense } from 'react'; +import type { ReactElement } from 'react'; +import React, { StrictMode, lazy, Suspense } from 'react'; import { queryClient } from '../../lib/queryClient'; import PageLoading from './PageLoading'; @@ -12,18 +12,20 @@ const AppLayout = lazy(() => import('./AppLayout')); const PortalsWrapper = lazy(() => import('./PortalsWrapper')); const ModalRegion = lazy(() => import('../modal/ModalRegion')); -const AppRoot: FC = () => ( - }> - - - - - - - - - - +const AppRoot = (): ReactElement => ( + + }> + + + + + + + + + + + ); export default AppRoot; diff --git a/apps/meteor/client/views/root/hooks/useClearUnreadAllMessagesMutation.ts b/apps/meteor/client/views/root/hooks/useClearUnreadAllMessagesMutation.ts new file mode 100644 index 00000000000..e3b8e6dc1fb --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useClearUnreadAllMessagesMutation.ts @@ -0,0 +1,35 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import type { UseMutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; + +import { ChatSubscription } from '../../../../app/models/client'; + +export const useClearUnreadAllMessagesMutation = (options?: Omit, 'mutationFn'>) => { + const readSubscription = useEndpoint('POST', '/v1/subscriptions.read'); + + return useMutation(async () => { + const promises = ChatSubscription.find( + { + open: true, + }, + { + fields: { + unread: 1, + alert: 1, + rid: 1, + t: 1, + name: 1, + ls: 1, + }, + }, + ).map((subscription) => { + if (subscription.alert || subscription.unread > 0) { + return readSubscription({ rid: subscription.rid, readThreads: true }); + } + + return Promise.resolve(); + }); + + await Promise.all(promises); + }, options); +}; diff --git a/apps/meteor/client/views/root/hooks/useEscapeKeyStroke.ts b/apps/meteor/client/views/root/hooks/useEscapeKeyStroke.ts new file mode 100644 index 00000000000..86d62832cd4 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useEscapeKeyStroke.ts @@ -0,0 +1,58 @@ +import { useSession, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; +import { useEffect, useRef } from 'react'; + +import GenericModal from '../../../components/GenericModal'; +import { imperativeModal } from '../../../lib/imperativeModal'; +import { useClearUnreadAllMessagesMutation } from './useClearUnreadAllMessagesMutation'; + +export const useEscapeKeyStroke = () => { + const dispatchToastMessage = useToastMessageDispatch(); + const t = useTranslation(); + + const clearUnreadAllMessagesMutation = useClearUnreadAllMessagesMutation({ + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onMutate: () => { + imperativeModal.close(); + }, + }); + + const { current: unread } = useRef(useSession('unread')); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.code !== 'Escape' || + !(event.shiftKey === true || event.ctrlKey === true) || + unread === undefined || + unread === null || + unread === '' + ) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + imperativeModal.open({ + component: GenericModal, + props: { + children: t('Are_you_sure_you_want_to_clear_all_unread_messages'), + variant: 'warning', + title: t('Clear_all_unreads_question'), + confirmText: t('Yes_clear_all'), + onClose: imperativeModal.close, + onCancel: imperativeModal.close, + onConfirm: clearUnreadAllMessagesMutation.mutate, + }, + }); + }; + + document.body.addEventListener('keydown', handleKeyDown); + + return () => { + document.body.removeEventListener('keydown', handleKeyDown); + }; + }, [clearUnreadAllMessagesMutation.mutate, dispatchToastMessage, t, unread]); +}; diff --git a/apps/meteor/client/views/root/hooks/useGoogleTagManager.ts b/apps/meteor/client/views/root/hooks/useGoogleTagManager.ts new file mode 100644 index 00000000000..72a9a6d8675 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useGoogleTagManager.ts @@ -0,0 +1,29 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +export const useGoogleTagManager = () => { + const i = useSetting('GoogleTagManager_id'); + + useEffect(() => { + if (typeof i !== 'string' || i.trim() === '') { + return; + } + + const w: Window & { dataLayer?: { 'gtm.start': number; 'event': string }[] } = window; + + w.dataLayer = w.dataLayer || []; + w.dataLayer.push({ + 'gtm.start': new Date().getTime(), + 'event': 'gtm.js', + }); + const f = document.getElementsByTagName('script')[0]; + const j = document.createElement('script'); + j.async = true; + j.src = `//www.googletagmanager.com/gtm.js?id=${i}`; + f.parentNode?.insertBefore(j, f); + + return () => { + f.parentNode?.removeChild(j); + }; + }, [i]); +}; diff --git a/apps/meteor/client/views/root/hooks/useMessageLinkClicks.ts b/apps/meteor/client/views/root/hooks/useMessageLinkClicks.ts new file mode 100644 index 00000000000..bd3782ed8ad --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useMessageLinkClicks.ts @@ -0,0 +1,39 @@ +import { useAbsoluteUrl, useLayout } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { fireGlobalEvent } from '../../../lib/utils/fireGlobalEvent'; + +export const useMessageLinkClicks = () => { + const absoluteUrl = useAbsoluteUrl(); + const { isEmbedded: embeddedLayout } = useLayout(); + + useEffect(() => { + if (!embeddedLayout) { + return; + } + + const handleMessageLinkClick = (event: Event) => { + const element = event.currentTarget as Element | null; + + if (!element || !(element instanceof HTMLElement)) { + return; + } + + if (!(element instanceof HTMLAnchorElement)) { + return; + } + + if (element.origin !== absoluteUrl('').replace(/\/+$/, '') || !/msg=([a-zA-Z0-9]+)/.test(element.search)) { + return; + } + + fireGlobalEvent('click-message-link', { link: element.pathname + element.search }); + }; + + document.body.addEventListener('click', handleMessageLinkClick); + + return () => { + document.body.removeEventListener('click', handleMessageLinkClick); + }; + }, [absoluteUrl, embeddedLayout]); +};