Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/odd-squids-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes an issue with `room-changed` event not being fired properly when switching between rooms that are available on cache.
114 changes: 114 additions & 0 deletions apps/meteor/client/hooks/useFireGlobalEvent.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook, waitFor } from '@testing-library/react';

import { useFireGlobalEvent } from './useFireGlobalEvent';
import { fireGlobalEventBase } from '../lib/utils/fireGlobalEventBase';

jest.mock('../lib/utils/fireGlobalEventBase', () => ({
fireGlobalEventBase: jest.fn(() => () => undefined),
}));

const fireGlobalMock = fireGlobalEventBase as jest.MockedFunction<typeof fireGlobalEventBase>;

describe('useInstance', () => {
Comment thread
gabriellsh marked this conversation as resolved.
Outdated
beforeEach(() => {
jest.clearAllMocks();
});

it('should dispatch event only once if scope is defined', async () => {
const scope = 'scope';
const { result } = renderHook(({ scope }) => useFireGlobalEvent('room-opened', scope), {
initialProps: { scope },

wrapper: mockAppRoot()
Comment thread
dougfabris marked this conversation as resolved.
.withSetting('Iframe_Integration_send_enable', true)
.withSetting('Iframe_Integration_send_target_origin', '')
.build(),
});
result.current.mutate(null);

await waitFor(() => expect(result.current.status).toBe('success'));

expect(fireGlobalMock).toHaveBeenCalledTimes(1);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));

expect(fireGlobalMock).toHaveBeenCalledTimes(1);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));

expect(fireGlobalMock).toHaveBeenCalledTimes(1);
});

it('should dispatch event only once for each (eventName/scope)', async () => {
const { result, rerender } = renderHook(({ scope }) => useFireGlobalEvent('room-opened', scope), {
initialProps: { scope: 'scope' },
wrapper: mockAppRoot()
.withSetting('Iframe_Integration_send_enable', true)
.withSetting('Iframe_Integration_send_target_origin', '')
.build(),
});
result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(1);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(1);

rerender({ scope: 'another' });

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(2);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(2);
});

it('should dispatch event multiple times if scope is not defined', async () => {
const { result } = renderHook(() => useFireGlobalEvent('room-opened'), {
wrapper: mockAppRoot()
.withSetting('Iframe_Integration_send_enable', true)
.withSetting('Iframe_Integration_send_target_origin', '')
.build(),
});

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(1);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(2);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(3);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(4);
});

it('should pass required settings to postMessage', async () => {
const { result } = renderHook(() => useFireGlobalEvent('room-opened'), {
wrapper: mockAppRoot()
.withSetting('Iframe_Integration_send_enable', true)
.withSetting('Iframe_Integration_send_target_origin', 'origin')
.build(),
});

const postMessage = jest.fn();

fireGlobalMock.mockImplementation(() => postMessage);

result.current.mutate(null);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(fireGlobalMock).toHaveBeenCalledTimes(1);
expect(postMessage).toHaveBeenCalledWith(true, 'origin');
});
});
34 changes: 34 additions & 0 deletions apps/meteor/client/hooks/useFireGlobalEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useSetting } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';

import { fireGlobalEventBase } from '../lib/utils/fireGlobalEventBase';

const getScopeForEvent = (eventName: string, scope?: string) => (scope ? `${eventName}/${scope}` : eventName);

export const useFireGlobalEvent = (eventName: string, scope?: string) => {
const sendEnabled = useSetting('Iframe_Integration_send_enable');
const origin = useSetting('Iframe_Integration_send_target_origin');

const dispatchedRef = useRef({ scope: getScopeForEvent(eventName, scope), dispatched: false });

useEffect(() => {
const newScope = getScopeForEvent(eventName, scope);
if (dispatchedRef.current?.scope !== newScope) {
dispatchedRef.current = { scope: newScope, dispatched: false };
}
}, [scope, eventName]);

return useMutation({
mutationFn: async (data?: unknown) => {
if (scope && dispatchedRef.current.dispatched) {
return;
}

const postMessage = fireGlobalEventBase(eventName, data);
postMessage(sendEnabled as boolean, origin as string);
dispatchedRef.current.dispatched = true;
},
scope: scope ? { id: getScopeForEvent(eventName, scope) } : undefined,
});
};
13 changes: 3 additions & 10 deletions apps/meteor/client/lib/utils/fireGlobalEvent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Tracker } from 'meteor/tracker';

import { fireGlobalEventBase } from './fireGlobalEventBase';
import { settings } from '../../../app/settings/client';

export const fireGlobalEvent = (eventName: string, detail?: unknown): void => {
window.dispatchEvent(new CustomEvent(eventName, { detail }));
const dispatchIframeMessage = fireGlobalEventBase(eventName, detail);

Tracker.autorun((computation) => {
const enabled = settings.get('Iframe_Integration_send_enable');
Expand All @@ -13,14 +14,6 @@ export const fireGlobalEvent = (eventName: string, detail?: unknown): void => {

computation.stop();

if (enabled) {
Comment thread
dougfabris marked this conversation as resolved.
parent.postMessage(
{
eventName,
data: detail,
},
settings.get('Iframe_Integration_send_target_origin'),
);
}
dispatchIframeMessage(enabled, settings.get('Iframe_Integration_send_target_origin'));
});
};
59 changes: 59 additions & 0 deletions apps/meteor/client/lib/utils/fireGlobalEventBase.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { fireGlobalEventBase } from './fireGlobalEventBase';

const postMessageMock = jest.fn();
const dispatchEventMock = jest.fn();

const originalDispatch = window.dispatchEvent;
const originalPostMessage = parent.postMessage;

beforeAll(() => {
window.dispatchEvent = dispatchEventMock;
parent.postMessage = postMessageMock;
});

beforeEach(() => {
postMessageMock.mockClear();
dispatchEventMock.mockClear();
});

afterAll(() => {
window.dispatchEvent = originalDispatch;
parent.postMessage = originalPostMessage;
});

it('should dispatch event but not post message', () => {
const detail = 'test-detail';
const postMessage = fireGlobalEventBase('test-event', detail);
postMessage(false, '');

expect(postMessageMock).not.toHaveBeenCalled();

expect(dispatchEventMock).toHaveBeenCalledTimes(1);

const result = dispatchEventMock.mock.lastCall[0];
expect(result).toBeInstanceOf(CustomEvent);
expect(result.detail).toBe(detail);
expect(result.type).toBe('test-event');
});

it('should dispatch event and post message', () => {
const detail = 'test-detail';
const origin = 'test-origin';
const postMessage = fireGlobalEventBase('test-event', detail);
postMessage(true, origin);

expect(postMessageMock).toHaveBeenCalledTimes(1);

expect(dispatchEventMock).toHaveBeenCalledTimes(1);

const dispatchResult = dispatchEventMock.mock.lastCall[0];
expect(dispatchResult).toBeInstanceOf(CustomEvent);
expect(dispatchResult.detail).toBe(detail);
expect(dispatchResult.type).toBe('test-event');

const [postEventResult, originResult] = postMessageMock.mock.lastCall;
expect(postEventResult).toBeInstanceOf(Object);
expect(originResult).toBe(origin);
expect(postEventResult.data).toBe(detail);
expect(postEventResult.eventName).toBe('test-event');
});
18 changes: 18 additions & 0 deletions apps/meteor/client/lib/utils/fireGlobalEventBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const fireGlobalEventBase = (eventName: string, detail?: unknown) => {
window.dispatchEvent(new CustomEvent(eventName, { detail }));

const dispatchMessage = (iframeSendEnabled: boolean, sendTargetOrigin: string) => {
if (!iframeSendEnabled) {
return;
}
parent.postMessage(
{
eventName,
data: detail,
},
sendTargetOrigin,
);
};

return dispatchMessage;
};
9 changes: 1 addition & 8 deletions apps/meteor/client/views/room/hooks/useOpenRoom.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { IRoom, RoomType } from '@rocket.chat/core-typings';
import { useMethod, usePermission, useRoute, useSetting, useUser } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';

import { useOpenRoomMutation } from './useOpenRoomMutation';
import { Rooms } from '../../../../app/models/client';
import { roomFields } from '../../../../lib/publishFields';
import { omit } from '../../../../lib/utils/omit';
import { NotAuthorizedError } from '../../../lib/errors/NotAuthorizedError';
import { NotSubscribedToRoomError } from '../../../lib/errors/NotSubscribedToRoomError';
import { OldUrlRoomError } from '../../../lib/errors/OldUrlRoomError';
Expand All @@ -21,8 +20,6 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st
const directRoute = useRoute('direct');
const openRoom = useOpenRoomMutation();

const unsubscribeFromRoomOpenedEvent = useRef<() => void>(() => undefined);

const result = useQuery({
// we need to add uid and username here because `user` is not loaded all at once (see UserProvider -> Meteor.user())
queryKey: ['rooms', { reference, type }, { uid: user?._id, username: user?.username }] as const,
Expand Down Expand Up @@ -89,10 +86,6 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st
}

const { RoomManager } = await import('../../../lib/RoomManager');
const { fireGlobalEvent } = await import('../../../lib/utils/fireGlobalEvent');

unsubscribeFromRoomOpenedEvent.current();
unsubscribeFromRoomOpenedEvent.current = RoomManager.once('opened', () => fireGlobalEvent('room-opened', omit(room, 'usernames')));

const sub = Subscriptions.findOne({ rid: room._id });

Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/client/views/room/providers/RoomProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { useUsersNameChanged } from './hooks/useUsersNameChanged';
import { Subscriptions } from '../../../../app/models/client';
import { UserAction } from '../../../../app/ui/client/lib/UserAction';
import { RoomHistoryManager } from '../../../../app/ui-utils/client';
import { omit } from '../../../../lib/utils/omit';
import { useFireGlobalEvent } from '../../../hooks/useFireGlobalEvent';
import { useReactiveQuery } from '../../../hooks/useReactiveQuery';
import { useReactiveValue } from '../../../hooks/useReactiveValue';
import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint';
Expand Down Expand Up @@ -86,6 +88,14 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => {

const isSidepanelFeatureEnabled = useSidePanelNavigation();

const { mutate: fireRoomOpenedEvent } = useFireGlobalEvent('room-opened', rid);

useEffect(() => {
if (resultFromLocal.data) {
fireRoomOpenedEvent(omit(resultFromLocal.data, 'usernames'));
}
}, [rid, resultFromLocal.data, fireRoomOpenedEvent]);

useEffect(() => {
if (isSidepanelFeatureEnabled) {
if (resultFromServer.isSuccess) {
Expand Down