Skip to content

Commit

Permalink
Merge branch 'develop' into feat/navigation-phase-3
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Sep 6, 2024
2 parents c8421a8 + d27cc36 commit afb646c
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 66 deletions.
5 changes: 5 additions & 0 deletions .changeset/khaki-cameras-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes an issue where the retention policy warning keep displaying even if the retention is disabled inside the room
5 changes: 5 additions & 0 deletions .changeset/rich-toes-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Prevented uiInteraction to subscribe multiple times
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Free for 30 days. Afterward, choose between continuing to host on our secure clo
You can follow these instructions to setup a dev environment:

- Install **Node 14.x (LTS)** either [manually](https://nodejs.org/dist/latest-v14.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended)
- Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://www.meteor.com/developers/install
- Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://docs.meteor.com/about/install.html
- Install **yarn**: https://yarnpkg.com/getting-started/install
- Clone this repo: `git clone https://github.com/RocketChat/Rocket.Chat.git`
- Run `yarn` to install dependencies
Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/client/hooks/useAppUiKitInteraction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useStream, useUserId } from '@rocket.chat/ui-contexts';
import type * as UiKit from '@rocket.chat/ui-kit';
import { useEffect } from 'react';
Expand All @@ -6,13 +7,12 @@ export const useAppUiKitInteraction = (handleServerInteraction: (interaction: Ui
const notifyUser = useStream('notify-user');
const uid = useUserId();

const handle = useEffectEvent(handleServerInteraction);
useEffect(() => {
if (!uid) {
return;
}

return notifyUser(`${uid}/uiInteraction`, (interaction) => {
handleServerInteraction(interaction);
});
}, [notifyUser, uid, handleServerInteraction]);
return notifyUser(`${uid}/uiInteraction`, handle);
}, [notifyUser, uid, handle]);
};
18 changes: 0 additions & 18 deletions apps/meteor/client/lib/utils/createAnchor.ts

This file was deleted.

11 changes: 0 additions & 11 deletions apps/meteor/client/lib/utils/deleteAnchor.ts

This file was deleted.

11 changes: 4 additions & 7 deletions apps/meteor/client/portals/TooltipPortal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { AnchorPortal } from '@rocket.chat/ui-client';
import type { ReactNode } from 'react';
import React, { memo, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import React, { memo } from 'react';

import { createAnchor } from '../lib/utils/createAnchor';
import { deleteAnchor } from '../lib/utils/deleteAnchor';
const tooltipAnchorId = 'tooltip-root';

type TooltipPortalProps = {
children?: ReactNode;
};

const TooltipPortal = ({ children }: TooltipPortalProps) => {
const [tooltipRoot] = useState(() => createAnchor('tooltip-root'));
useEffect(() => (): void => deleteAnchor(tooltipRoot), [tooltipRoot]);
return <>{createPortal(children, tooltipRoot)}</>;
return <AnchorPortal id={tooltipAnchorId}>{children}</AnchorPortal>;
};

export default memo(TooltipPortal);
11 changes: 4 additions & 7 deletions apps/meteor/client/portals/VideoConfPopupPortal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { AnchorPortal } from '@rocket.chat/ui-client';
import type { ReactElement, ReactNode } from 'react';
import React, { memo, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import React, { memo } from 'react';

import { createAnchor } from '../lib/utils/createAnchor';
import { deleteAnchor } from '../lib/utils/deleteAnchor';
const videoConfAnchorId = 'video-conf-root';

type VideoConfPortalProps = {
children?: ReactNode;
};

const VideoConfPortal = ({ children }: VideoConfPortalProps): ReactElement => {
const [videoConfRoot] = useState(() => createAnchor('video-conf-root'));
useEffect(() => (): void => deleteAnchor(videoConfRoot), [videoConfRoot]);
return <>{createPortal(children, videoConfRoot)}</>;
return <AnchorPortal id={videoConfAnchorId}>{children}</AnchorPortal>;
};

export default memo(VideoConfPortal);
23 changes: 10 additions & 13 deletions apps/meteor/client/views/room/hooks/useRetentionPolicy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,13 @@ it('should return enabled and active true global retention is active for rooms o
expect(result.current).toEqual(expect.objectContaining({ ...defaultValue, enabled: true, isActive: true }));
});

it.failing(
'should isActive be false if global retention is active for rooms of the type and room has retention.enabled false',
async () => {
const fakeRoom = createFakeRoom({ t: CHANNELS_TYPE, retention: { enabled: false } });

const { result } = renderHook(() => useRetentionPolicy(fakeRoom), {
legacyRoot: true,
wrapper: getGlobalSettings({ enabled: true, ...roomTypeConfig[CHANNELS_TYPE] }).build(),
});

expect(result.current?.isActive).toBe(false);
},
);
it('should isActive be false if global retention is active for rooms of the type and room has retention.enabled false', async () => {
const fakeRoom = createFakeRoom({ t: CHANNELS_TYPE, retention: { enabled: false } });

const { result } = renderHook(() => useRetentionPolicy(fakeRoom), {
legacyRoot: true,
wrapper: getGlobalSettings({ enabled: true, ...roomTypeConfig[CHANNELS_TYPE] }).build(),
});

expect(result.current?.isActive).toBe(false);
});
12 changes: 7 additions & 5 deletions apps/meteor/client/views/room/hooks/useRetentionPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { useSetting } from '@rocket.chat/ui-contexts';
import { TIMEUNIT, isValidTimespan, timeUnitToMs } from '../../../lib/convertTimeUnit';

const hasRetentionPolicy = (room: IRoom & { retention?: any }): room is IRoomWithRetentionPolicy =>
'retention' in room && room.retention !== undefined && 'overrideGlobal' in room.retention && isValidTimespan(room.retention.maxAge);
'retention' in room && room.retention !== undefined;

const isRetentionOverridden = (room: IRoom & { retention?: any }) => 'overrideGlobal' in room.retention && room.retention.overrideGlobal;

type RetentionPolicySettings = {
enabled: boolean;
Expand Down Expand Up @@ -41,31 +43,31 @@ const isActive = (room: IRoom, { enabled, appliesToChannels, appliesToGroups, ap
};

const extractFilesOnly = (room: IRoom, { filesOnly }: RetentionPolicySettings): boolean => {
if (hasRetentionPolicy(room) && room.retention.overrideGlobal) {
if (hasRetentionPolicy(room) && isRetentionOverridden(room)) {
return room.retention.filesOnly;
}

return filesOnly;
};

const extractExcludePinned = (room: IRoom, { doNotPrunePinned }: RetentionPolicySettings): boolean => {
if (hasRetentionPolicy(room) && room.retention.overrideGlobal) {
if (hasRetentionPolicy(room) && isRetentionOverridden(room)) {
return room.retention.excludePinned;
}

return doNotPrunePinned;
};

const extractIgnoreThreads = (room: IRoom, { ignoreThreads }: RetentionPolicySettings): boolean => {
if (hasRetentionPolicy(room) && room.retention.overrideGlobal) {
if (hasRetentionPolicy(room) && isRetentionOverridden(room)) {
return room.retention.ignoreThreads;
}

return ignoreThreads;
};

const getMaxAge = (room: IRoom, { maxAgeChannels, maxAgeGroups, maxAgeDMs }: RetentionPolicySettings): number => {
if (hasRetentionPolicy(room) && room.retention.overrideGlobal) {
if (hasRetentionPolicy(room) && isRetentionOverridden(room) && isValidTimespan(room.retention.maxAge)) {
return timeUnitToMs(TIMEUNIT.days, room.retention.maxAge);
}

Expand Down
45 changes: 45 additions & 0 deletions packages/ui-client/src/components/AnchorPortal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';

import AnchorPortal from './AnchorPortal';

it('should render children', () => {
render(<AnchorPortal id='test-anchor' children={<div role='presentation' aria-label='example' />} />, { legacyRoot: true });

expect(screen.getByRole('presentation', { name: 'example' })).toBeInTheDocument();
});

it('should not recreate the anchor element', () => {
render(<AnchorPortal id='test-anchor' children={<div role='presentation' aria-label='example A' />} />, { legacyRoot: true });
const anchorA = document.getElementById('test-anchor');

render(<AnchorPortal id='test-anchor' children={<div role='presentation' aria-label='example B' />} />, { legacyRoot: true });
const anchorB = document.getElementById('test-anchor');

expect(anchorA).toBe(anchorB);
expect(screen.getByRole('presentation', { name: 'example A' })).toBeInTheDocument();
expect(screen.getByRole('presentation', { name: 'example B' })).toBeInTheDocument();
});

it('should remove the anchor element when unmounted', () => {
const { unmount } = render(<AnchorPortal id='test-anchor' children={<div role='presentation' aria-label='example' />} />, {
legacyRoot: true,
});
expect(document.getElementById('test-anchor')).toBeInTheDocument();

unmount();
expect(document.getElementById('test-anchor')).not.toBeInTheDocument();
});

it('should not remove the anchor element after unmounting if there are other portals with the same id', () => {
const { unmount } = render(<AnchorPortal id='test-anchor' children={<div role='presentation' aria-label='example' />} />, {
legacyRoot: true,
});
expect(document.getElementById('test-anchor')).toBeInTheDocument();

render(<AnchorPortal id='test-anchor' children={<div role='presentation' aria-label='example' />} />, {
legacyRoot: true,
});
unmount();

expect(document.getElementById('test-anchor')).toBeInTheDocument();
});
25 changes: 25 additions & 0 deletions packages/ui-client/src/components/AnchorPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ReactNode, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';

import { ensureAnchorElement, refAnchorElement, unrefAnchorElement } from '../helpers/anchors';

export type AnchorPortalProps = {
id: string;
children: ReactNode;
};

const AnchorPortal = ({ id, children }: AnchorPortalProps) => {
const anchorElement = ensureAnchorElement(id);

useLayoutEffect(() => {
refAnchorElement(anchorElement);

return () => {
unrefAnchorElement(anchorElement);
};
}, [anchorElement]);

return <>{createPortal(children, anchorElement)}</>;
};

export default AnchorPortal;
1 change: 1 addition & 0 deletions packages/ui-client/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as AnchorPortal, AnchorPortalProps } from './AnchorPortal';
export * from './EmojiPicker';
export * from './ExternalLink';
export * from './DotLeader';
Expand Down
32 changes: 32 additions & 0 deletions packages/ui-client/src/helpers/anchors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const ensureAnchorElement = (id: string): HTMLElement => {
const existingAnchor = document.getElementById(id);
if (existingAnchor) return existingAnchor;

const newAnchor = document.createElement('div');
newAnchor.id = id;
document.body.appendChild(newAnchor);
return newAnchor;
};

const getAnchorRefCount = (anchorElement: HTMLElement): number => {
const { refCount } = anchorElement.dataset;
if (refCount) return parseInt(refCount, 10);
return 0;
};

const setAnchorRefCount = (anchorElement: HTMLElement, refCount: number): void => {
anchorElement.dataset.refCount = String(refCount);
};

export const refAnchorElement = (anchorElement: HTMLElement): void => {
setAnchorRefCount(anchorElement, getAnchorRefCount(anchorElement) + 1);
};

export const unrefAnchorElement = (anchorElement: HTMLElement): void => {
const refCount = getAnchorRefCount(anchorElement) - 1;
setAnchorRefCount(anchorElement, refCount);

if (refCount <= 0) {
document.body.removeChild(anchorElement);
}
};

0 comments on commit afb646c

Please sign in to comment.