diff --git a/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts b/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts deleted file mode 100644 index 6ea1f70e9897e..0000000000000 --- a/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const CannedResponse = new Mongo.Collection<{ - _id: string; - shortcut: string; - text: string; -}>(null); diff --git a/apps/meteor/app/canned-responses/client/index.ts b/apps/meteor/app/canned-responses/client/index.ts deleted file mode 100644 index 36fd7b5cc6af9..0000000000000 --- a/apps/meteor/app/canned-responses/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './startup/responses'; diff --git a/apps/meteor/app/canned-responses/client/startup/responses.ts b/apps/meteor/app/canned-responses/client/startup/responses.ts deleted file mode 100644 index 6d761adb890da..0000000000000 --- a/apps/meteor/app/canned-responses/client/startup/responses.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { hasPermission } from '../../../authorization/client'; -import { settings } from '../../../settings/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; -import { CannedResponse } from '../collections/CannedResponse'; - -Meteor.startup(() => { - Tracker.autorun(async (c) => { - if (!Meteor.userId()) { - return; - } - if (!settings.get('Canned_Responses_Enable')) { - return; - } - if (!hasPermission('view-canned-responses')) { - return; - } - Tracker.afterFlush(() => { - try { - // TODO: check options - sdk.stream('canned-responses', ['canned-responses'], (...[response, options]) => { - const { agentsId } = options || {}; - if (Array.isArray(agentsId) && !agentsId.includes(Meteor.userId())) { - return; - } - - switch (response.type) { - case 'changed': { - const { type, ...fields } = response; - CannedResponse.upsert({ _id: response._id }, fields); - break; - } - - case 'removed': { - CannedResponse.remove({ _id: response._id }); - break; - } - } - }); - } catch (error) { - console.log(error); - } - }); - - const { responses } = await sdk.rest.get('/v1/canned-responses.get'); - responses.forEach((response) => CannedResponse.insert(response)); - c.stop(); - }); -}); diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index ad7f06e15603a..d47cc47eae4d7 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -1,7 +1,6 @@ import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; -import '../app/canned-responses/client'; import '../app/custom-sounds/client'; import '../app/emoji/client'; import '../app/emoji-emojione/client'; diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index a9217935b8219..ad15b225ab88d 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -14,3 +14,7 @@ export const subscriptionsQueryKeys = { all: ['subscriptions'] as const, subscription: (rid: IRoom['_id']) => [...subscriptionsQueryKeys.all, { rid }] as const, }; + +export const cannedResponsesQueryKeys = { + all: ['canned-responses'] as const, +}; diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 22557f4a6d15b..6293a401da8b0 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -3,16 +3,17 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { useMethod, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { hasAtLeastOnePermission } from '../../../../app/authorization/client'; -import { CannedResponse } from '../../../../app/canned-responses/client/collections/CannedResponse'; import { emoji } from '../../../../app/emoji/client'; import { Subscriptions } from '../../../../app/models/client'; import { usersFromRoomMessages } from '../../../../app/ui-message/client/popup/messagePopupConfig'; import { slashCommands } from '../../../../app/utils/client'; +import { cannedResponsesQueryKeys } from '../../../lib/queryKeys'; import ComposerBoxPopupCannedResponse from '../composer/ComposerBoxPopupCannedResponse'; import type { ComposerBoxPopupEmojiProps } from '../composer/ComposerBoxPopupEmoji'; import ComposerBoxPopupEmoji from '../composer/ComposerBoxPopupEmoji'; @@ -24,6 +25,9 @@ import ComposerBoxPopupUser from '../composer/ComposerBoxPopupUser'; import type { ComposerBoxPopupUserProps } from '../composer/ComposerBoxPopupUser'; import type { ComposerPopupContextValue } from '../contexts/ComposerPopupContext'; import { ComposerPopupContext, createMessageBoxPopupConfig } from '../contexts/ComposerPopupContext'; +import useCannedResponsesQuery from './hooks/useCannedResponsesQuery'; + +export type CannedResponse = { _id: string; shortcut: string; text: string }; type ComposerPopupProviderProps = { children: ReactNode; @@ -32,6 +36,11 @@ type ComposerPopupProviderProps = { const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) => { const { _id: rid, encrypted: isRoomEncrypted } = room; + + // TODO: this is awful because we are just triggering the query to get the data + // and we are not using the data itself, we should find a better way to do this + useCannedResponsesQuery(room); + const userSpotlight = useMethod('spotlight'); const suggestionsCount = useSetting('Number_of_users_autocomplete_suggestions', 5); const cannedResponseEnabled = useSetting('Canned_Responses_Enable', true); @@ -43,6 +52,7 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const e2eEnabled = useSetting('E2E_Enable', false); const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false); const encrypted = isRoomEncrypted && e2eEnabled && !unencryptedMessagesAllowed; + const queryClient = useQueryClient(); const call = useMethod('getSlashCommandPreviews'); const value: ComposerPopupContextValue = useMemo(() => { @@ -334,18 +344,12 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = renderItem: ({ item }) => , getItemsFromLocal: async (filter: string) => { const exp = new RegExp(filter, 'i'); - return CannedResponse.find( - { - shortcut: exp, - }, - { - limit: 12, - sort: { - shortcut: -1, - }, - }, - ) - .fetch() + // TODO: this is bad, but can only be fixed by refactoring the whole thing + const cannedResponses = queryClient.getQueryData(cannedResponsesQueryKeys.all) ?? []; + return cannedResponses + .filter((record) => record.shortcut.match(exp)) + .sort((a, b) => a.shortcut.localeCompare(b.shortcut)) + .slice(0, 11) .map((record) => ({ _id: record._id, text: record.text, @@ -353,9 +357,7 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = })); }, getItemsFromServer: async () => [], - getValue: (item) => { - return item.text; - }, + getValue: (item) => item.text, }), createMessageBoxPopupConfig({ title: previewTitle, @@ -388,8 +390,8 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = rid, recentEmojis, i18n, + queryClient, call, - setPreviewTitle, ]); return ; diff --git a/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts b/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts new file mode 100644 index 0000000000000..0150ecdf9ae50 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts @@ -0,0 +1,64 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { useEndpoint, usePermission, useSetting, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { cannedResponsesQueryKeys } from '../../../../lib/queryKeys'; + +type CannedResponse = { _id: string; shortcut: string; text: string }; + +const useCannedResponsesQuery = (room: IRoom) => { + const isOmnichannel = isOmnichannelRoom(room); + const uid = useUserId(); + const isCannedResponsesEnabled = useSetting('Canned_Responses_Enable', true); + const canViewCannedResponses = usePermission('view-canned-responses'); + const subscribeToCannedResponses = useStream('canned-responses'); + + const enabled = isOmnichannel && !!uid && isCannedResponsesEnabled && canViewCannedResponses; + + const getCannedResponses = useEndpoint('GET', '/v1/canned-responses.get'); + + const queryClient = useQueryClient(); + + useEffect(() => { + if (!enabled) return; + + return subscribeToCannedResponses('canned-responses', (...[response, options]) => { + const { agentsId } = options || {}; + if (Array.isArray(agentsId) && !agentsId.includes(uid)) { + return; + } + + switch (response.type) { + case 'changed': { + const { _id, shortcut, text } = response; + queryClient.setQueryData(cannedResponsesQueryKeys.all, (responses) => + responses?.filter((response) => response._id !== _id).concat([{ _id, shortcut, text }]), + ); + break; + } + + case 'removed': { + const { _id } = response; + queryClient.setQueryData(cannedResponsesQueryKeys.all, (responses) => + responses?.filter((response) => response._id !== _id), + ); + break; + } + } + }); + }, [enabled, getCannedResponses, queryClient, subscribeToCannedResponses, uid]); + + return useQuery({ + queryKey: cannedResponsesQueryKeys.all, + queryFn: async () => { + const { responses } = await getCannedResponses(); + return responses.map(({ _id, shortcut, text }) => ({ _id, shortcut, text })); + }, + enabled, + staleTime: Infinity, + }); +}; + +export default useCannedResponsesQuery;