diff --git a/.changeset/twelve-forks-destroy.md b/.changeset/twelve-forks-destroy.md
new file mode 100644
index 0000000000000..b4cc7321bed9f
--- /dev/null
+++ b/.changeset/twelve-forks-destroy.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": patch
+"@rocket.chat/i18n": patch
+---
+
+Adds invitation badge to sidebar
diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx
new file mode 100644
index 0000000000000..b47b32f73497f
--- /dev/null
+++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx
@@ -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();
+ expect(baseElement).toMatchSnapshot();
+});
+
+test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
+ const { container } = render();
+
+ const results = await axe(container);
+ expect(results).toHaveNoViolations();
+});
diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx
new file mode 100644
index 0000000000000..658f206075718
--- /dev/null
+++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx
@@ -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;
+
+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'),
+ },
+};
diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx
new file mode 100644
index 0000000000000..5c47878208703
--- /dev/null
+++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx
@@ -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, 'name' | 'color' | 'role'> & {
+ invitationDate: string | Date;
+};
+
+const InvitationBadge = ({ invitationDate, ...props }: InvitationBadgeProps) => {
+ const { t } = useTranslation();
+ const timeAgo = useTimeAgo();
+
+ return (
+
+ );
+};
+
+export default InvitationBadge;
diff --git a/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap b/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap
new file mode 100644
index 0000000000000..260d7e11131a4
--- /dev/null
+++ b/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`renders WithDateObject without crashing 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`renders WithISOStringDate without crashing 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/apps/meteor/client/components/InvitationBadge/index.ts b/apps/meteor/client/components/InvitationBadge/index.ts
new file mode 100644
index 0000000000000..78b459e5d05da
--- /dev/null
+++ b/apps/meteor/client/components/InvitationBadge/index.ts
@@ -0,0 +1 @@
+export { default } from './InvitationBadge';
diff --git a/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx b/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx
index eec3c2820b6b6..285c5ecdbc72d 100644
--- a/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx
+++ b/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx
@@ -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}}',
@@ -50,4 +50,27 @@ describe('SidebarItemBadges', () => {
expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});
+
+ it('should render InvitationBadge when subscription has status INVITED', () => {
+ render(
+ ,
+ {
+ 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(, { wrapper: appRoot });
+
+ expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
+ });
});
diff --git a/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx b/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx
index cec0b35344391..de8264eb4a422 100644
--- a/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx
+++ b/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx
@@ -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';
@@ -18,6 +19,7 @@ const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
{showUnread && }
{isOmnichannelRoom(room) && }
+ {isInviteSubscription(room) && }
);
};
diff --git a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx
index eec3c2820b6b6..9866febc2dcaa 100644
--- a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx
+++ b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx
@@ -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}}',
@@ -50,4 +50,29 @@ describe('SidebarItemBadges', () => {
expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});
+
+ it('should render InvitationBadge when subscription has status INVITED', () => {
+ render(
+ ,
+ {
+ 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(, {
+ wrapper: appRoot,
+ });
+
+ expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
+ });
});
diff --git a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx
index 762c4bb175f8b..aedfd3610ea9c 100644
--- a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx
+++ b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx
@@ -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';
@@ -17,6 +18,7 @@ const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
<>
{showUnread && }
{isOmnichannelRoom(room) && }
+ {isInviteSubscription(room) && }
>
);
};
diff --git a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx
index 599b1ae643879..9a2fbf9fc711f 100644
--- a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx
+++ b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx
@@ -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}}',
@@ -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(
+ ,
+ {
+ 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(, { wrapper: appRoot });
+
+ expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
+ });
});
diff --git a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx
index 6cc2203a5234c..9b4e6d4539d1d 100644
--- a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx
+++ b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx
@@ -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';
@@ -11,7 +13,12 @@ type SidebarItemBadgesProps = {
const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => {
const { unreadCount, unreadTitle, unreadVariant, showUnread } = useUnreadDisplay(room);
- return <>{showUnread && }>;
+ return (
+ <>
+ {showUnread && }
+ {isInviteSubscription(room) && }
+ >
+ );
};
export default SidebarItemBadges;
diff --git a/apps/meteor/client/views/navigation/sidebar/badges/UnreadBadge.tsx b/apps/meteor/client/views/navigation/sidebar/badges/UnreadBadge.tsx
new file mode 100644
index 0000000000000..d3abe4f8aaca6
--- /dev/null
+++ b/apps/meteor/client/views/navigation/sidebar/badges/UnreadBadge.tsx
@@ -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 (
+
+ {total}
+
+ );
+};
+
+export default UnreadBadge;
diff --git a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx
index 49dda4aa6b842..991dd9fa97ab6 100644
--- a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx
+++ b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx
@@ -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}}',
@@ -52,4 +52,27 @@ describe('RoomSidePanelItemBadges', () => {
expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument();
});
+
+ it('should render InvitationBadge when subscription has status INVITED', () => {
+ render(
+ ,
+ {
+ 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(, { wrapper: appRoot });
+
+ expect(screen.queryByRole('status', { name: 'Invited' })).not.toBeInTheDocument();
+ });
});
diff --git a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx
index 7519a9338b99f..8a9a759439af6 100644
--- a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx
+++ b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx
@@ -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';
@@ -17,6 +18,7 @@ const RoomSidePanelItemBadges = ({ room, roomTitle }: RoomSidePanelItemBadgesPro
<>
{isOmnichannelRoom(room) && }
{showUnread && }
+ {isInviteSubscription(room) && }
>
);
};
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index ce902b70ee484..f99625450eb86 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -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.",