Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
eda104d
feat: allow not rendering the room toolbox
aleksandernsilva Nov 25, 2025
1f88a9c
refactor: abstracted RoomHeader
aleksandernsilva Nov 25, 2025
6b0bdfb
feat: allow not rendering the room toolbox v2
aleksandernsilva Nov 25, 2025
6f39743
feat: useRoomInvitation
aleksandernsilva Nov 25, 2025
20f750f
feat: RoomInviteHeader v1
aleksandernsilva Nov 25, 2025
6aed61b
feat: RoomInviteHeader v2
aleksandernsilva Nov 25, 2025
afbad98
feat: RoomInviteBody
aleksandernsilva Nov 25, 2025
6f190f1
chore: added subscription RoomHeader
aleksandernsilva Nov 25, 2025
a9e6470
chore: isInviteSubscription
aleksandernsilva Nov 25, 2025
b5b40a5
feat: added RoomInviteHeader to HeaderV1
aleksandernsilva Nov 25, 2025
dce5617
feat: added RoomInviteHeader to HeaderV2
aleksandernsilva Nov 25, 2025
406ef3c
feat: RoomInvite
aleksandernsilva Nov 25, 2025
fbee3f0
feat: added RoomInvite to Room.tsx
aleksandernsilva Nov 25, 2025
9899ccf
refactor: added roomReference query key
aleksandernsilva Nov 26, 2025
b69eed3
feat: added redirect to home when invitation is rejected
aleksandernsilva Nov 26, 2025
f3f2259
feat: granted room access when subscription invite is pending (to be …
aleksandernsilva Nov 26, 2025
cbe351f
refactor: adjusted query invalidation in useRoomInvitation
aleksandernsilva Nov 26, 2025
a5463af
chore: changeset
aleksandernsilva Nov 26, 2025
8616953
refactor: added slots prop to RoomInviteHeader v2
aleksandernsilva Nov 26, 2025
ba4f37f
feat: added link to federation documentation
aleksandernsilva Nov 26, 2025
ec55443
chore: adjusted import
aleksandernsilva Nov 26, 2025
dca3d0f
feat: added reject confirmation modal
aleksandernsilva Dec 2, 2025
1c16de3
chore: code style
aleksandernsilva Dec 2, 2025
820b601
chore: removed unused import
aleksandernsilva Dec 8, 2025
91d0a6d
chore: use new inviter field from subscription
sampaiodiego Dec 8, 2025
e7f34ef
fix: use subscription to show room invite screen
sampaiodiego Dec 8, 2025
c93efc8
refactor: reverted RoomHeader change
aleksandernsilva Dec 8, 2025
cc91182
refactor: accept learn more link as prop
aleksandernsilva Dec 8, 2025
9541c83
refactor: pass inviter object to RoomInviteBody
aleksandernsilva Dec 8, 2025
face6f7
chore: adjusted stories
aleksandernsilva Dec 8, 2025
e6ab496
test: adjusted tests
aleksandernsilva Dec 8, 2025
0df146b
chore: updated incorrect interface name
aleksandernsilva Dec 9, 2025
523ce40
refactor: adjusted reject username fallback
aleksandernsilva Dec 9, 2025
eebab8f
chore: added story with info link
aleksandernsilva Dec 9, 2025
286e811
test: adjusted mocks
aleksandernsilva Dec 9, 2025
f81b0af
test: updated snapshots
aleksandernsilva Dec 9, 2025
3560b5e
chore: removed randomness from FakeRoomProvider subscription
aleksandernsilva Dec 9, 2025
852fea9
refactor: simplify room reference query key structure
ggazzo Dec 9, 2025
741b318
refactor: remove unnecessary translation for inviter username
ggazzo Dec 9, 2025
e4a3790
review
ggazzo Dec 9, 2025
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
7 changes: 7 additions & 0 deletions .changeset/chatty-roses-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/core-typings": patch
"@rocket.chat/i18n": patch
---

Adds invitation request support to rooms
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/functions/createDirectRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export async function createDirectRoom(
status: 'INVITED',
inviter: {
_id: creatorUser._id,
username: creatorUser.username,
username: creatorUser.username!,
name: creatorUser.name,
},
open: true,
Expand Down
17 changes: 16 additions & 1 deletion apps/meteor/client/lib/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import type { ILivechatDepartment, IMessage, IRoom, ITeam, IUser, ILivechatAgent, IOutboundProvider } from '@rocket.chat/core-typings';
import type {
ILivechatDepartment,
IMessage,
IRoom,
ITeam,
IUser,
ILivechatAgent,
IOutboundProvider,
RoomType,
} from '@rocket.chat/core-typings';
import type { PaginatedRequest } from '@rocket.chat/rest-typings';

export const roomsQueryKeys = {
all: ['rooms'] as const,
room: (rid: IRoom['_id']) => ['rooms', rid] as const,
roomReference: (reference: string, type: RoomType, uid?: IUser['_id'], username?: IUser['username']) => [
...roomsQueryKeys.all,
reference,
type,
uid ?? username,
],
starredMessages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'starred-messages'] as const,
pinnedMessages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'pinned-messages'] as const,
messages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'messages'] as const,
Expand Down
12 changes: 9 additions & 3 deletions apps/meteor/client/views/room/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isDirectMessageRoom, isVoipRoom } from '@rocket.chat/core-typings';
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
import { isDirectMessageRoom, isVoipRoom, isInviteSubscription } from '@rocket.chat/core-typings';
import { useLayout, useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { lazy, memo, useMemo } from 'react';

import { HeaderToolbar } from '../../../components/Header';
import SidebarToggler from '../../../components/SidebarToggler';

const RoomInviteHeader = lazy(() => import('./RoomInviteHeader'));
const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader'));
const VoipRoomHeader = lazy(() => import('./Omnichannel/VoipRoomHeader'));
const RoomHeaderE2EESetup = lazy(() => import('./RoomHeaderE2EESetup'));
Expand All @@ -15,9 +16,10 @@ const RoomHeader = lazy(() => import('./RoomHeader'));

type HeaderProps<T> = {
room: T;
subscription?: ISubscription;
};

const Header = ({ room }: HeaderProps<IRoom>): ReactElement | null => {
const Header = ({ room, subscription }: HeaderProps<IRoom>): ReactElement | null => {
const { isMobile, isEmbedded, showTopNavbarEmbeddedLayout } = useLayout();
const encrypted = Boolean(room.encrypted);
const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false);
Expand All @@ -38,6 +40,10 @@ const Header = ({ room }: HeaderProps<IRoom>): ReactElement | null => {
return null;
}

if (subscription && isInviteSubscription(subscription)) {
return <RoomInviteHeader room={room} />;
}

if (room.t === 'l') {
return <OmnichannelRoomHeader slots={slots} />;
}
Expand Down
80 changes: 80 additions & 0 deletions apps/meteor/client/views/room/Header/RoomHeader.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';

import RoomHeader from './RoomHeader';
import FakeRoomProvider from '../../../../tests/mocks/client/FakeRoomProvider';
import { createFakeRoom } from '../../../../tests/mocks/data';

const mockedRoom = createFakeRoom({ prid: undefined });
const appRoot = mockAppRoot()
.withRoom(mockedRoom)
.wrap((children) => <FakeRoomProvider roomOverrides={mockedRoom}>{children}</FakeRoomProvider>)
.build();

jest.mock('../../../../app/utils/client', () => ({
getURL: (url: string) => url,
}));

jest.mock('./ParentRoomWithData', () => ({
__esModule: true,
default: jest.fn(() => <div>ParentRoomWithData</div>),
}));

jest.mock('./ParentTeam', () => ({
__esModule: true,
default: jest.fn(() => <div>ParentTeam</div>),
}));

jest.mock('./RoomToolbox', () => ({
__esModule: true,
default: jest.fn(() => <div>RoomToolbox</div>),
}));

describe('RoomHeader', () => {
describe('Toolbox', () => {
it('should render toolbox by default', async () => {
render(<RoomHeader room={mockedRoom} slots={{}} />, { wrapper: appRoot });
expect(screen.getByLabelText('Toolbox_room_actions')).toBeInTheDocument();
});

it('should not render toolbox if roomToolbox is null and no slots are provided', () => {
render(
<RoomHeader
room={mockedRoom}
slots={{
toolbox: {
hidden: true,
},
}}
/>,
{ wrapper: appRoot },
);
expect(screen.queryByLabelText('Toolbox_room_actions')).not.toBeInTheDocument();
});

it('should render toolbox if slots.toolbox is provided', () => {
render(<RoomHeader room={mockedRoom} slots={{ toolbox: {} }} />, { wrapper: appRoot });
expect(screen.getByLabelText('Toolbox_room_actions')).toBeInTheDocument();
});

it('should render custom toolbox content from roomToolbox prop', () => {
render(
<RoomHeader
room={mockedRoom}
slots={{
toolbox: {
content: <div>Custom Toolbox</div>,
},
}}
/>,
{ wrapper: appRoot },
);
expect(screen.getByText('Custom Toolbox')).toBeInTheDocument();
});

it('should render custom toolbox content from slots.toolbox.content', () => {
render(<RoomHeader room={mockedRoom} slots={{ toolbox: { content: <div>Slotted Toolbox</div> } }} />, { wrapper: appRoot });
expect(screen.getByText('Slotted Toolbox')).toBeInTheDocument();
});
});
});
47 changes: 47 additions & 0 deletions apps/meteor/client/views/room/Header/RoomInviteHeader.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';

import * as stories from './RoomInviteHeader.stories';

const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);

const appRoot = mockAppRoot().build();

jest.mock('../../../../app/utils/client', () => ({
getURL: (url: string) => url,
}));

jest.mock('./ParentRoomWithData', () => ({
__esModule: true,
default: jest.fn(() => <div>ParentRoomWithData</div>),
}));

jest.mock('./ParentTeam', () => ({
__esModule: true,
default: jest.fn(() => <div>ParentTeam</div>),
}));

jest.mock('./RoomToolbox', () => ({
__esModule: true,
default: jest.fn(() => <div>RoomToolbox</div>),
}));

describe('RoomInviteHeader', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const view = render(<Story />, { wrapper: appRoot });
expect(view.baseElement).toMatchSnapshot();
});

test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: appRoot });

const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';

import RoomInviteHeader from './RoomInviteHeader';
import FakeRoomProvider from '../../../../tests/mocks/client/FakeRoomProvider';
import { createFakeRoom } from '../../../../tests/mocks/data';

const mockedRoom = createFakeRoom({ name: 'rocket.cat', federated: true });

const meta = {
component: RoomInviteHeader,
args: {
room: mockedRoom,
},
decorators: [(story) => <FakeRoomProvider roomOverrides={mockedRoom}>{story()}</FakeRoomProvider>],
} satisfies Meta<typeof RoomInviteHeader>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
17 changes: 17 additions & 0 deletions apps/meteor/client/views/room/Header/RoomInviteHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import RoomHeader from './RoomHeader';
import type { RoomHeaderProps } from './RoomHeader';

const RoomInviteHeader = ({ room }: Pick<RoomHeaderProps, 'room'>) => {
return (
<RoomHeader
room={room}
slots={{
toolbox: {
hidden: true,
},
}}
/>
);
};

export default RoomInviteHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`RoomInviteHeader renders Default without crashing 1`] = `
<body>
<div>
<header
class="rcx-box rcx-box--full rcx-room-header rcx-css-7tefqp"
>
<div
class="rcx-box rcx-box--full rcx-css-1apkof4"
>
<div
class="rcx-box rcx-box--full rcx-css-llee4e"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
aria-hidden="true"
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1axz7ym"
>
<div
class="rcx-box rcx-box--full rcx-css-1yimpo4"
>
<div
class="rcx-box rcx-box--full rcx-css-i0csg7 rcx-css-f2vsf1"
role="button"
tabindex="0"
>
<div
class="rcx-box rcx-box--full rcx-css-v5o1rw"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-globe rcx-icon rcx-css-1t9h2ff"
>
</i>
</div>
<h1
class="rcx-box rcx-box--full rcx-css-1w5kdwh"
>
rocket.cat
</h1>
</div>
<button
class="rcx-box rcx-box--full rcx-button--tiny-square rcx-button--square rcx-button--icon rcx-button rcx-css-sdt442"
title="Favorite rocket.cat"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-star rcx-icon rcx-css-1g87xs3"
>
</i>
</button>
</div>
</div>
</div>
<hr
class="rcx-box rcx-box--full rcx-divider rcx-css-emj6cu"
/>
</header>
</div>
</body>
`;
12 changes: 9 additions & 3 deletions apps/meteor/client/views/room/HeaderV2/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isVoipRoom } from '@rocket.chat/core-typings';
import { isInviteSubscription, isVoipRoom } from '@rocket.chat/core-typings';
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
import { useLayout, useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { lazy, memo } from 'react';

const RoomInviteHeader = lazy(() => import('./RoomInviteHeader'));
const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader'));
const VoipRoomHeader = lazy(() => import('./Omnichannel/VoipRoomHeader'));
const RoomHeaderE2EESetup = lazy(() => import('./RoomHeaderE2EESetup'));
const RoomHeader = lazy(() => import('./RoomHeader'));

type HeaderProps = {
room: IRoom;
subscription?: ISubscription;
};

const Header = ({ room }: HeaderProps): ReactElement | null => {
const Header = ({ room, subscription }: HeaderProps): ReactElement | null => {
const { isEmbedded, showTopNavbarEmbeddedLayout } = useLayout();
const encrypted = Boolean(room.encrypted);
const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false);
Expand All @@ -23,6 +25,10 @@ const Header = ({ room }: HeaderProps): ReactElement | null => {
return null;
}

if (subscription && isInviteSubscription(subscription)) {
return <RoomInviteHeader room={room} />;
}

if (room.t === 'l') {
return <OmnichannelRoomHeader />;
}
Expand Down
Loading
Loading