snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Utilities } from '../../../../../ee/lib/misc/Utilities';
|
||||
import { useAppActionButtons } from '../../../../hooks/useAppActionButtons';
|
||||
import { useApplyButtonFilters } from '../../../../hooks/useApplyButtonFilters';
|
||||
import { UiKitTriggerTimeoutError } from '../../../../lib/errors/UiKitTriggerTimeoutError';
|
||||
import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager';
|
||||
import { useRoom } from '../../contexts/RoomContext';
|
||||
|
||||
export const useAppsRoomActions = () => {
|
||||
const result = useAppActionButtons('roomAction');
|
||||
const actionManager = useUiKitActionManager();
|
||||
const applyButtonFilters = useApplyButtonFilters();
|
||||
const room = useRoom();
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
result.data?.filter(applyButtonFilters).map((action): RoomToolboxActionConfig => ({
|
||||
id: action.actionId,
|
||||
variant: action.variant,
|
||||
icon: undefined as unknown as RoomToolboxActionConfig['icon'],
|
||||
order: 300,
|
||||
title: Utilities.getI18nKeyForApp(action.labelI18n, action.appId),
|
||||
groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'],
|
||||
// Filters were applied in the applyButtonFilters function
|
||||
// if the code made it this far, the button should be shown
|
||||
action: () => {
|
||||
void actionManager
|
||||
.emitInteraction(action.appId, {
|
||||
type: 'actionButton',
|
||||
actionId: action.actionId,
|
||||
rid: room._id,
|
||||
payload: { context: action.context },
|
||||
})
|
||||
.catch(async (reason) => {
|
||||
if (reason instanceof UiKitTriggerTimeoutError) {
|
||||
dispatchToastMessage({
|
||||
type: 'error',
|
||||
message: t('UIKit_Interaction_Timeout'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return reason;
|
||||
});
|
||||
},
|
||||
type: 'apps',
|
||||
})) ?? [],
|
||||
[actionManager, applyButtonFilters, dispatchToastMessage, result.data, room._id, t],
|
||||
);
|
||||
};
|
||||
@@ -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<CannedResponse[]>(cannedResponsesQueryKeys.all, (responses) =>
|
||||
responses?.filter((response) => response._id !== _id).concat([{ _id, shortcut, text }]),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'removed': {
|
||||
const { _id } = response;
|
||||
queryClient.setQueryData<CannedResponse[]>(cannedResponsesQueryKeys.all, (responses) =>
|
||||
responses?.filter((response) => response._id !== _id),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [enabled, getCannedResponses, queryClient, subscribeToCannedResponses, uid]);
|
||||
|
||||
return useQuery<CannedResponse[]>({
|
||||
queryKey: cannedResponsesQueryKeys.all,
|
||||
queryFn: async () => {
|
||||
const { responses } = await getCannedResponses();
|
||||
return responses.map(({ _id, shortcut, text }) => ({ _id, shortcut, text }));
|
||||
},
|
||||
enabled,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
export default useCannedResponsesQuery;
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { ISubscription } from '@rocket.chat/core-typings';
|
||||
import type { IActionManager } from '@rocket.chat/ui-contexts';
|
||||
import { useUserId } from '@rocket.chat/ui-contexts';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useChatMessagesInstance } from './useChatMessagesInstance';
|
||||
import { useEmojiPicker } from '../../../../contexts/EmojiPickerContext';
|
||||
import { ChatMessages } from '../../../../lib/ChatMessages';
|
||||
import type { E2ERoomState } from '../../../../lib/e2ee/E2ERoomState';
|
||||
import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager';
|
||||
import { useRoomSubscription } from '../../contexts/RoomContext';
|
||||
import { useE2EERoomState } from '../../hooks/useE2EERoomState';
|
||||
|
||||
jest.mock('@rocket.chat/ui-contexts', () => ({
|
||||
useUserId: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../contexts/RoomContext', () => ({
|
||||
useRoomSubscription: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../../uikit/hooks/useUiKitActionManager', () => ({
|
||||
useUiKitActionManager: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../hooks/useE2EERoomState', () => ({
|
||||
useE2EERoomState: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../../contexts/EmojiPickerContext', () => ({
|
||||
useEmojiPicker: jest.fn(),
|
||||
}));
|
||||
|
||||
const updateSubscriptionMock = jest.fn();
|
||||
jest.mock('../../../../lib/ChatMessages', () => {
|
||||
return {
|
||||
ChatMessages: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
release: jest.fn(),
|
||||
readStateManager: {
|
||||
updateSubscription: updateSubscriptionMock,
|
||||
subscribeToMessages: jest.fn(),
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useChatMessagesInstance', () => {
|
||||
let mockUid: string;
|
||||
let mockSubscription: Pick<ISubscription, 'u' | 't' | 'rid'>;
|
||||
let mockActionManager: IActionManager | undefined;
|
||||
let mockE2EERoomState: E2ERoomState;
|
||||
let mockEmojiPicker: {
|
||||
open: jest.Mock;
|
||||
isOpen: boolean;
|
||||
close: jest.Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUid = 'mockUid';
|
||||
mockSubscription = {
|
||||
u: {
|
||||
_id: mockUid,
|
||||
username: 'usernameMock',
|
||||
name: 'nameMock',
|
||||
},
|
||||
t: 'p',
|
||||
rid: 'roomId',
|
||||
};
|
||||
mockActionManager = undefined;
|
||||
mockE2EERoomState = 'READY';
|
||||
mockEmojiPicker = {
|
||||
open: jest.fn(),
|
||||
isOpen: false,
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(useUserId as jest.Mock).mockReturnValue(mockUid);
|
||||
(useRoomSubscription as jest.Mock).mockReturnValue(mockSubscription);
|
||||
(useUiKitActionManager as jest.Mock).mockReturnValue(mockActionManager);
|
||||
(useE2EERoomState as jest.Mock).mockReturnValue(mockE2EERoomState);
|
||||
(useEmojiPicker as jest.Mock).mockReturnValue(mockEmojiPicker);
|
||||
});
|
||||
|
||||
it('should initialize ChatMessages instance with correct arguments', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useChatMessagesInstance({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
encrypted: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(1);
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should update ChatMessages subscription', () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useChatMessagesInstance({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
encrypted: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(1);
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
(useRoomSubscription as jest.Mock).mockReturnValue({ ...mockSubscription, rid: 'newRoomId' });
|
||||
|
||||
rerender();
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(1);
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should update ChatMessages instance when dependencies changes', () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useChatMessagesInstance({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
encrypted: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(1);
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
(useE2EERoomState as jest.Mock).mockReturnValue('WAITING_KEYS');
|
||||
|
||||
rerender();
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(2);
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should update ChatMessages instance when hook props changes', () => {
|
||||
const initialProps = {
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
encrypted: false,
|
||||
};
|
||||
const { result, rerender } = renderHook((props = initialProps) => useChatMessagesInstance(props as any));
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(1);
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
encrypted: true,
|
||||
});
|
||||
|
||||
expect(ChatMessages).toHaveBeenCalledWith({
|
||||
rid: mockSubscription.rid,
|
||||
tmid: 'threadId',
|
||||
uid: mockUid,
|
||||
actionManager: mockActionManager,
|
||||
});
|
||||
|
||||
expect(result.current.emojiPicker).toBe(mockEmojiPicker);
|
||||
expect(updateSubscriptionMock).toHaveBeenCalledTimes(2);
|
||||
expect(ChatMessages).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
|
||||
import { useUserId } from '@rocket.chat/ui-contexts';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useInstance } from './useInstance';
|
||||
import { useEmojiPicker } from '../../../../contexts/EmojiPickerContext';
|
||||
import { ChatMessages } from '../../../../lib/ChatMessages';
|
||||
import type { ChatAPI } from '../../../../lib/chats/ChatAPI';
|
||||
import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionManager';
|
||||
import { useRoomSubscription } from '../../contexts/RoomContext';
|
||||
import { useE2EERoomState } from '../../hooks/useE2EERoomState';
|
||||
|
||||
export function useChatMessagesInstance({
|
||||
rid,
|
||||
tmid,
|
||||
encrypted,
|
||||
}: {
|
||||
rid: IRoom['_id'];
|
||||
tmid?: IMessage['_id'];
|
||||
encrypted: IRoom['encrypted'];
|
||||
}): ChatAPI {
|
||||
const uid = useUserId();
|
||||
const subscription = useRoomSubscription();
|
||||
const actionManager = useUiKitActionManager();
|
||||
const e2eRoomState = useE2EERoomState(rid);
|
||||
|
||||
const chatMessages = useInstance(() => {
|
||||
const instance = new ChatMessages({ rid, tmid, uid, actionManager });
|
||||
|
||||
return [instance, () => instance.release()];
|
||||
}, [rid, tmid, uid, encrypted, e2eRoomState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription?.rid) {
|
||||
return chatMessages?.readStateManager.subscribeToMessages();
|
||||
}
|
||||
}, [subscription?.rid, chatMessages?.readStateManager]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription) {
|
||||
chatMessages?.readStateManager.updateSubscription(subscription);
|
||||
}
|
||||
}, [subscription, chatMessages?.readStateManager]);
|
||||
|
||||
chatMessages.emojiPicker = useEmojiPicker();
|
||||
|
||||
return chatMessages;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useStableArray } from '@rocket.chat/fuselage-hooks';
|
||||
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import { roomActionHooks } from '../../../../ui';
|
||||
|
||||
export const useCoreRoomActions = () => {
|
||||
return useStableArray(
|
||||
roomActionHooks.map((roomActionHook) => roomActionHook()).filter((roomAction): roomAction is RoomToolboxActionConfig => !!roomAction),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import MediaCallHistoryContextualbarRoom from '../../../mediaCallHistory/MediaCallHistoryContextualbarRoom';
|
||||
|
||||
const mediaCallHistoryRoute: RoomToolboxActionConfig = {
|
||||
id: 'media-call-history',
|
||||
title: 'Call_Information',
|
||||
tabComponent: MediaCallHistoryContextualbarRoom,
|
||||
icon: 'info-circled',
|
||||
groups: ['direct'],
|
||||
};
|
||||
|
||||
const coreRoomRoutes = [mediaCallHistoryRoute];
|
||||
|
||||
// This isn't really a proper hook, but it could be extended in the future
|
||||
// So we're maitaning the same pattern as `useCoreRoomActions`
|
||||
export const useCoreRoomRoutes = (): Array<RoomToolboxActionConfig> => {
|
||||
return coreRoomRoutes;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useDepsMatch } from './useDepsMatch';
|
||||
|
||||
describe('useDepsMatch', () => {
|
||||
it('should return true when dependencies match', () => {
|
||||
const { result, rerender } = renderHook(({ deps }) => useDepsMatch(deps), {
|
||||
initialProps: { deps: ['dep1', 'dep2'] },
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
rerender({ deps: ['dep1', 'dep2'] });
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when dependencies do not match', () => {
|
||||
const { result, rerender } = renderHook(({ deps }) => useDepsMatch(deps), {
|
||||
initialProps: { deps: ['dep1', 'dep2'] },
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
rerender({ deps: ['dep1', 'dep3'] });
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when dependencies length changes', () => {
|
||||
const { result, rerender } = renderHook(({ deps }) => useDepsMatch(deps), {
|
||||
initialProps: { deps: ['dep1', 'dep2'] },
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
rerender({ deps: ['dep1'] });
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
const depsMatch = (a: unknown[], b: unknown[]): boolean => a.every((value, index) => Object.is(value, b[index]));
|
||||
|
||||
export const useDepsMatch = (deps: unknown[]): boolean => {
|
||||
const prevDepsRef = useRef(deps);
|
||||
const { current: prevDeps } = prevDepsRef;
|
||||
|
||||
const match = depsMatch(prevDeps, deps);
|
||||
|
||||
prevDepsRef.current = deps;
|
||||
|
||||
return match;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useInstance } from './useInstance';
|
||||
|
||||
class MockChatMessages {
|
||||
release() {
|
||||
return 'released';
|
||||
}
|
||||
}
|
||||
|
||||
describe('useInstance', () => {
|
||||
let factory: jest.Mock;
|
||||
let release: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
release = jest.fn();
|
||||
factory = jest.fn().mockReturnValue([{ instance: new MockChatMessages() }, release]);
|
||||
});
|
||||
|
||||
it('should create a new instance when dependencies change', () => {
|
||||
const { result, rerender } = renderHook(({ deps }) => useInstance(factory, deps), {
|
||||
initialProps: { deps: ['initial-dep'] },
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({ instance: new MockChatMessages() });
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(release).toHaveBeenCalledTimes(0);
|
||||
|
||||
rerender({ deps: ['new-dep'] });
|
||||
|
||||
expect(result.current).toEqual({ instance: new MockChatMessages() });
|
||||
expect(factory).toHaveBeenCalledTimes(2);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not create a new instance when dependencies do not change', () => {
|
||||
const { result, rerender } = renderHook(({ deps }) => useInstance(factory, deps), {
|
||||
initialProps: { deps: ['initial-dep'] },
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({ instance: new MockChatMessages() });
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(release).toHaveBeenCalledTimes(0);
|
||||
|
||||
rerender({ deps: ['initial-dep'] });
|
||||
|
||||
expect(result.current).toEqual({ instance: new MockChatMessages() });
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(release).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should call release function when component unmounts', () => {
|
||||
const { unmount } = renderHook(() => useInstance(factory, ['initial-dep']));
|
||||
|
||||
unmount();
|
||||
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
23
apps/meteor/client/views/room/providers/hooks/useInstance.ts
Normal file
23
apps/meteor/client/views/room/providers/hooks/useInstance.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
import { useDepsMatch } from './useDepsMatch';
|
||||
|
||||
export function useInstance<T>(factory: () => [instance: T, release?: () => void], deps: unknown[]): T {
|
||||
const ref = useRef<[instance: T, release?: () => void]>(undefined);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
ref.current?.[1]?.();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const depsMatch = useDepsMatch(deps);
|
||||
|
||||
if (!ref.current || !depsMatch) {
|
||||
ref.current?.[1]?.();
|
||||
ref.current = factory();
|
||||
}
|
||||
|
||||
return ref.current[0];
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { ISubscription } from '@rocket.chat/core-typings';
|
||||
import { useRouter } from '@rocket.chat/ui-contexts';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { LegacyRoomManager } from '../../../../lib/LegacyRoomManager';
|
||||
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
const routeNameToRoomTypeMap: Record<string, string> = {
|
||||
channel: 'c',
|
||||
group: 'p',
|
||||
direct: 'd',
|
||||
live: 'l',
|
||||
};
|
||||
|
||||
export const useRedirectOnSettingsChanged = (subscription?: ISubscription | null) => {
|
||||
const router = useRouter();
|
||||
|
||||
const subExists = !!subscription;
|
||||
|
||||
useEffect(() => {
|
||||
if (!subExists) {
|
||||
return;
|
||||
}
|
||||
const redirect = async () => {
|
||||
const routeConfig = roomCoordinator.getRoomDirectives(subscription.t).config.route;
|
||||
|
||||
const channelName = router.getRouteParameters().name;
|
||||
const routeName = router.getRouteName() as string;
|
||||
|
||||
if (!routeConfig?.path || !routeName || !channelName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (routeConfig.name === routeName && channelName === subscription.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const routeRoomType = routeNameToRoomTypeMap[routeName];
|
||||
|
||||
if (routeRoomType) {
|
||||
await LegacyRoomManager.close(routeRoomType + routeName);
|
||||
}
|
||||
|
||||
router.navigate({
|
||||
pattern: routeConfig.path,
|
||||
params: { ...router.getRouteParameters(), name: subscription.name },
|
||||
search: router.getSearchParameters(),
|
||||
});
|
||||
};
|
||||
redirect();
|
||||
}, [subscription?.t, subscription?.name, router, subExists]);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { IEditedMessage } from '@rocket.chat/core-typings';
|
||||
import { isEditedMessage } from '@rocket.chat/core-typings';
|
||||
import { useStream } from '@rocket.chat/ui-contexts';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Messages, Subscriptions } from '../../../../stores';
|
||||
|
||||
export const useUsersNameChanged = () => {
|
||||
const notify = useStream('notify-logged');
|
||||
const updateMessages = Messages.use((state) => state.update);
|
||||
useEffect(() => {
|
||||
return notify('Users:NameChanged', ({ _id, name, username }) => {
|
||||
updateMessages(
|
||||
(record) => record.u._id === _id,
|
||||
(record) => ({
|
||||
...record,
|
||||
u: {
|
||||
...record.u,
|
||||
username: username ?? record.u.username,
|
||||
name,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
updateMessages(
|
||||
(record): record is IEditedMessage => isEditedMessage(record) && record.editedBy?._id === _id,
|
||||
(record) => ({
|
||||
...record,
|
||||
editedBy: {
|
||||
...record.editedBy,
|
||||
username,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
updateMessages(
|
||||
(record) => record.mentions?.some((mention) => mention._id === _id) ?? false,
|
||||
(record) => ({
|
||||
...record,
|
||||
mentions: record.mentions?.map((mention) => (mention._id === _id ? { ...mention, username, name } : mention)),
|
||||
}),
|
||||
);
|
||||
|
||||
Subscriptions.state.update(
|
||||
(record) => record.name === username && record.t === 'd',
|
||||
(record) => ({ ...record, fname: name }),
|
||||
);
|
||||
});
|
||||
}, [notify, updateMessages]);
|
||||
};
|
||||
Reference in New Issue
Block a user