Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/twelve-forks-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/i18n": patch
---

Adds invitation badge to sidebar
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';

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

const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});

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

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

import InvitationBadge from './InvitationBadge';

const meta = {
component: InvitationBadge,
parameters: {
layout: 'centered',
},
decorators: [
mockAppRoot()
.withTranslations('en', 'core', {
Invited__date__: 'Invited {{date}}',
})
.buildStoryDecorator(),
],
} satisfies Meta<typeof InvitationBadge>;

export default meta;

export const WithISOStringDate = {
args: {
invitationDate: '2025-01-01T12:00:00Z',
},
};

export const WithDateObject = {
args: {
invitationDate: new Date('2025-01-01T12:00:00Z'),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Icon } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
import { useTranslation } from 'react-i18next';

import { useTimeAgo } from '../../hooks/useTimeAgo';

type InvitationBadgeProps = Omit<ComponentProps<typeof Icon>, 'name' | 'color' | 'role'> & {
invitationDate: string | Date;
};

const InvitationBadge = ({ invitationDate, ...props }: InvitationBadgeProps) => {
const { t } = useTranslation();
const timeAgo = useTimeAgo();

return (
<Icon
size='x20'
{...props}
role='status'
color='info'
name='mail'
aria-hidden='false'
title={t('Invited__date__', { date: timeAgo(invitationDate) })}
/>
);
};

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

exports[`renders WithDateObject without crashing 1`] = `
<body>
<div>
<i
aria-hidden="false"
class="rcx-box rcx-box--full rcx-icon--name-mail rcx-icon rcx-css-dpa92h"
role="status"
title="Invited January 1, 2025"
>
</i>
</div>
</body>
`;

exports[`renders WithISOStringDate without crashing 1`] = `
<body>
<div>
<i
aria-hidden="false"
class="rcx-box rcx-box--full rcx-icon--name-mail rcx-icon rcx-css-dpa92h"
role="status"
title="Invited January 1, 2025"
>
</i>
</div>
</body>
`;
1 change: 1 addition & 0 deletions apps/meteor/client/components/InvitationBadge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './InvitationBadge';
25 changes: 24 additions & 1 deletion apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jest.mock('../../views/omnichannel/components/OmnichannelBadges', () => ({
describe('SidebarItemBadges', () => {
const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Message_request: 'Message request',
Invited__date__: 'Invited {{date}}',
mentions_counter_one: '{{count}} mention',
mentions_counter_other: '{{count}} mentions',
__unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}',
Expand Down Expand Up @@ -50,4 +50,27 @@ describe('SidebarItemBadges', () => {

expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});

it('should render InvitationBadge when subscription has status INVITED', () => {
render(
<SidebarItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
wrapper: appRoot,
},
);

expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument();
});

it('should not render InvitationBadge when subscription does not have status INVITED', () => {
render(<SidebarItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });

expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
});
});
4 changes: 3 additions & 1 deletion apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, isInviteSubscription } from '@rocket.chat/core-typings';
import { Margins } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import UnreadBadge from './UnreadBadge';
import InvitationBadge from '../../components/InvitationBadge';
import OmnichannelBadges from '../../views/omnichannel/components/OmnichannelBadges';
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';

Expand All @@ -18,6 +19,7 @@ const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
<Margins inlineStart={8}>
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
{isInviteSubscription(room) && <InvitationBadge mbs={2} invitationDate={room.ts} />}
</Margins>
);
};
Expand Down
27 changes: 26 additions & 1 deletion apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jest.mock('../../views/omnichannel/components/OmnichannelBadges', () => ({
describe('SidebarItemBadges', () => {
const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Message_request: 'Message request',
Invited__date__: 'Invited {{date}}',
mentions_counter_one: '{{count}} mention',
mentions_counter_other: '{{count}} mentions',
__unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}',
Expand Down Expand Up @@ -50,4 +50,29 @@ describe('SidebarItemBadges', () => {

expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});

it('should render InvitationBadge when subscription has status INVITED', () => {
render(
<SidebarItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
wrapper: appRoot,
},
);

expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument();
});

it('should not render InvitationBadge when subscription does not have status INVITED', () => {
render(<SidebarItemBadges room={createFakeSubscription()} />, {
wrapper: appRoot,
});

expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
});
});
4 changes: 3 additions & 1 deletion apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { isInviteSubscription, isOmnichannelRoom } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import UnreadBadge from './UnreadBadge';
import InvitationBadge from '../../components/InvitationBadge';
import OmnichannelBadges from '../../views/omnichannel/components/OmnichannelBadges';
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';

Expand All @@ -17,6 +18,7 @@ const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
<>
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
{isInviteSubscription(room) && <InvitationBadge mbs={2} invitationDate={room.ts} />}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createFakeSubscription } from '../../../../../tests/mocks/data';
describe('SidebarItemBadges', () => {
const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Message_request: 'Message request',
Invited__date__: 'Invited {{date}}',
mentions_counter_one: '{{count}} mention',
mentions_counter_other: '{{count}} mentions',
__unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}',
Expand All @@ -33,4 +33,27 @@ describe('SidebarItemBadges', () => {

expect(screen.queryByRole('status', { name: 'Test Room' })).not.toBeInTheDocument();
});

it('should render InvitationBadge when subscription has status INVITED and has inviter', () => {
render(
<SidebarItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
wrapper: appRoot,
},
);

expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument();
});

it('should not render InvitationBadge when subscription does not have status INVITED', () => {
render(<SidebarItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });

expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { isInviteSubscription } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import InvitationBadge from '../../../../components/InvitationBadge';
import UnreadBadge from '../../../../sidebarv2/badges/UnreadBadge';
import { useUnreadDisplay } from '../hooks/useUnreadDisplay';

Expand All @@ -11,7 +13,12 @@ type SidebarItemBadgesProps = {
const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
const { unreadCount, unreadTitle, unreadVariant, showUnread } = useUnreadDisplay(room);

return <>{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}</>;
return (
<>
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isInviteSubscription(room) && <InvitationBadge mbs={2} invitationDate={room.ts} />}
</>
);
};

export default SidebarItemBadges;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SidebarV2ItemBadge } from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';

type UnreadBadgeProps = {
title: string;
roomTitle?: string;
variant: 'primary' | 'warning' | 'danger' | 'secondary';
total: number;
};

const UnreadBadge = ({ title, variant, total, roomTitle }: UnreadBadgeProps) => {
const { t } = useTranslation();

return (
<SidebarV2ItemBadge
variant={variant}
title={title}
role='status'
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle: title, roomTitle })}
>
<span aria-hidden>{total}</span>
</SidebarV2ItemBadge>
);
};

export default UnreadBadge;
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jest.mock('../omnichannel/SidePanelOmnichannelBadges', () => ({
describe('RoomSidePanelItemBadges', () => {
const appRoot = mockAppRoot()
.withTranslations('en', 'core', {
Message_request: 'Message request',
Invited__date__: 'Invited {{date}}',
mentions_counter_one: '{{count}} mention',
mentions_counter_other: '{{count}} mentions',
__unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}',
Expand Down Expand Up @@ -52,4 +52,27 @@ describe('RoomSidePanelItemBadges', () => {

expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});

it('should render InvitationBadge when subscription has status INVITED', () => {
render(
<RoomSidePanelItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
wrapper: appRoot,
},
);

expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument();
});

it('should not render InvitationBadge when subscription does not have status INVITED', () => {
render(<RoomSidePanelItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });

expect(screen.queryByRole('status', { name: 'Invited' })).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { isInviteSubscription, isOmnichannelRoom } from '@rocket.chat/core-typings';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';

import InvitationBadge from '../../../../components/InvitationBadge';
import UnreadBadge from '../../../../sidebarv2/badges/UnreadBadge';
import { useUnreadDisplay } from '../../../../sidebarv2/hooks/useUnreadDisplay';
import SidePanelOmnichannelBadges from '../omnichannel/SidePanelOmnichannelBadges';
Expand All @@ -17,6 +18,7 @@ const RoomSidePanelItemBadges = ({ room, roomTitle }: RoomSidePanelItemBadgesPro
<>
{isOmnichannelRoom(room) && <SidePanelOmnichannelBadges room={room} />}
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isInviteSubscription(room) && <InvitationBadge invitationDate={room.ts} />}
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2679,6 +2679,7 @@
"Invitation_Subject": "Invitation Subject",
"Invitation_Subject_Default": "You have been invited to [Site_Name]",
"Invite": "Invite",
"Invited__date__": "Invited {{date}}",
"Invite_Link": "Invite Link",
"Invite_Users": "Invite Members",
"Invite_and_add_members_to_this_workspace_to_start_communicating": "Invite and add members to this workspace to start communicating.",
Expand Down
Loading