Skip to content

Commit 4d31365

Browse files
aleksandernsilvasampaiodiego
authored andcommitted
feat: Added invitation badge to room members list (RocketChat#37643)
Co-authored-by: Diego Sampaio <chinello@gmail.com>
1 parent 93b3386 commit 4d31365

File tree

9 files changed

+273
-22
lines changed

9 files changed

+273
-22
lines changed

.changeset/nasty-moons-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
---
4+
5+
Adds invitation badge to room members list

apps/meteor/app/api/server/v1/im.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,26 @@ API.v1.addRoute(
416416

417417
const [members, total] = await Promise.all([cursor.toArray(), totalCount]);
418418

419+
// find subscriptions of those users
420+
const subs = await Subscriptions.findByRoomIdAndUserIds(
421+
room._id,
422+
members.map((member) => member._id),
423+
{ projection: { u: 1, status: 1, ts: 1, roles: 1 } },
424+
).toArray();
425+
426+
const membersWithSubscriptionInfo = members.map((member) => {
427+
const sub = subs.find((sub) => sub.u._id === member._id);
428+
429+
const { u: _u, ...subscription } = sub || {};
430+
431+
return {
432+
...member,
433+
subscription,
434+
};
435+
});
436+
419437
return API.v1.success({
420-
members,
438+
members: membersWithSubscriptionInfo,
421439
count: members.length,
422440
offset,
423441
total,

apps/meteor/client/views/hooks/useMembersList.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IRole, IUser, AtLeast } from '@rocket.chat/core-typings';
1+
import type { IRole, IUser, AtLeast, ISubscription, Serialized } from '@rocket.chat/core-typings';
22
import { useEndpoint, useSetting, useStream } from '@rocket.chat/ui-contexts';
33
import type { InfiniteData, QueryClient } from '@tanstack/react-query';
44
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
@@ -20,9 +20,18 @@ const endpointsByRoomType = {
2020
c: '/v1/rooms.membersOrderedByRole',
2121
} as const;
2222

23-
export type RoomMember = Pick<IUser, 'username' | '_id' | 'name' | 'status' | 'freeSwitchExtension'> & { roles?: IRole['_id'][] };
24-
25-
type MembersListPage = { members: RoomMember[]; count: number; total: number; offset: number };
23+
export type RoomMember = Serialized<
24+
Pick<IUser, 'username' | '_id' | 'name' | 'status' | 'federated' | 'freeSwitchExtension'> & { roles?: IRole['_id'][] } & {
25+
subscription: Pick<ISubscription, '_id' | 'status' | 'ts' | 'roles'>;
26+
}
27+
>;
28+
29+
type MembersListPage = {
30+
members: RoomMember[];
31+
count: number;
32+
total: number;
33+
offset: number;
34+
};
2635

2736
const getSortedMembers = (members: RoomMember[], useRealName = false) => {
2837
const membersWithRolePriority: (RoomMember & { rolePriority: number })[] = members.map((member) => ({

apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ Default.args = {
2525
username: 'rocket.cat',
2626
status: UserStatus.ONLINE,
2727
name: 'Rocket.Cat',
28+
roles: ['user'],
29+
subscription: {
30+
_id: 'sub-rocket.cat',
31+
ts: '2025-01-01T00:00:00Z',
32+
},
2833
},
2934
],
3035
text: 'filter',
@@ -57,6 +62,40 @@ WithABACRoom.args = {
5762
username: 'rocket.cat',
5863
status: UserStatus.ONLINE,
5964
name: 'Rocket.Cat',
65+
roles: ['user'],
66+
subscription: {
67+
_id: 'sub-rocket.cat',
68+
ts: '2025-01-01T00:00:00Z',
69+
},
70+
},
71+
],
72+
text: 'filter',
73+
type: 'online',
74+
setText: action('Lorem Ipsum'),
75+
setType: action('online'),
76+
total: 123,
77+
loadMoreItems: action('loadMoreItems'),
78+
rid: '!roomId',
79+
isTeam: false,
80+
isDirect: false,
81+
reload: action('reload'),
82+
isABACRoom: true,
83+
};
84+
85+
export const WithInvitedMember = Template.bind({});
86+
WithInvitedMember.args = {
87+
loading: false,
88+
members: [
89+
{
90+
_id: 'rocket.cat',
91+
username: 'rocket.cat',
92+
roles: ['user'],
93+
subscription: {
94+
_id: 'sub-rocket.cat',
95+
status: 'INVITED',
96+
ts: '2025-01-01T00:00:00Z',
97+
},
98+
name: 'Rocket.Cat',
6099
},
61100
],
62101
text: 'filter',

apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IRoom, IUser, IRole } from '@rocket.chat/core-typings';
1+
import type { IRoom } from '@rocket.chat/core-typings';
22
import type { SelectOption } from '@rocket.chat/fuselage';
33
import { Box, Icon, TextInput, Select, Throbber, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage';
44
import { useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
@@ -22,8 +22,7 @@ import { GroupedVirtuoso } from 'react-virtuoso';
2222
import { MembersListDivider } from './MembersListDivider';
2323
import RoomMembersRow from './RoomMembersRow';
2424
import InfiniteListAnchor from '../../../../components/InfiniteListAnchor';
25-
26-
export type RoomMemberUser = Pick<IUser, 'username' | '_id' | 'name' | 'status' | 'freeSwitchExtension'> & { roles?: IRole['_id'][] };
25+
import type { RoomMember } from '../../../hooks/useMembersList';
2726

2827
type RoomMembersProps = {
2928
rid: IRoom['_id'];
@@ -34,7 +33,7 @@ type RoomMembersProps = {
3433
type: string;
3534
setText: FormEventHandler<HTMLInputElement>;
3635
setType: (type: 'online' | 'all') => void;
37-
members: RoomMemberUser[];
36+
members: RoomMember[];
3837
total: number;
3938
error?: Error;
4039
onClickClose: () => void;
@@ -91,10 +90,10 @@ const RoomMembers = ({
9190
const useRealName = useSetting('UI_Use_Real_Name', false);
9291

9392
const { counts, titles } = useMemo(() => {
94-
const owners: RoomMemberUser[] = [];
95-
const leaders: RoomMemberUser[] = [];
96-
const moderators: RoomMemberUser[] = [];
97-
const normalMembers: RoomMemberUser[] = [];
93+
const owners: RoomMember[] = [];
94+
const leaders: RoomMember[] = [];
95+
const moderators: RoomMember[] = [];
96+
const normalMembers: RoomMember[] = [];
9897

9998
members.forEach((member) => {
10099
if (member.roles?.includes('owner')) {

apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IRoom, IUser } from '@rocket.chat/core-typings';
1+
import type { IRoom } from '@rocket.chat/core-typings';
22
import {
33
Option,
44
OptionAvatar,
@@ -17,15 +17,17 @@ import { useState } from 'react';
1717

1818
import UserActions from './RoomMembersActions';
1919
import { getUserDisplayNames } from '../../../../../lib/getUserDisplayNames';
20+
import InvitationBadge from '../../../../components/InvitationBadge';
2021
import { ReactiveUserStatus } from '../../../../components/UserStatus';
2122
import { usePreventPropagation } from '../../../../hooks/usePreventPropagation';
23+
import type { RoomMember } from '../../../hooks/useMembersList';
2224

23-
type RoomMembersItemProps = {
24-
onClickView: (e: MouseEvent<HTMLElement>) => void;
25+
type RoomMembersItemProps = Pick<RoomMember, 'federated' | 'username' | 'name' | '_id' | 'freeSwitchExtension' | 'subscription'> & {
2526
rid: IRoom['_id'];
26-
reload: () => void;
2727
useRealName: boolean;
28-
} & Pick<IUser, 'federated' | 'username' | 'name' | '_id' | 'freeSwitchExtension'>;
28+
reload: () => void;
29+
onClickView: (e: MouseEvent<HTMLElement>) => void;
30+
};
2931

3032
const RoomMembersItem = ({
3133
_id,
@@ -37,6 +39,7 @@ const RoomMembersItem = ({
3739
rid,
3840
reload,
3941
useRealName,
42+
subscription,
4043
}: RoomMembersItemProps): ReactElement => {
4144
const [showButton, setShowButton] = useState();
4245
const isReduceMotionEnabled = usePrefersReducedMotion();
@@ -57,6 +60,11 @@ const RoomMembersItem = ({
5760
<OptionContent data-qa={`MemberItem-${username}`}>
5861
{nameOrUsername} {displayUsername && <OptionDescription>({displayUsername})</OptionDescription>}
5962
</OptionContent>
63+
{subscription?.status === 'INVITED' && (
64+
<OptionColumn>
65+
<InvitationBadge mbs={2} size='x20' invitationDate={subscription.ts} />
66+
</OptionColumn>
67+
)}
6068
<OptionMenu onClick={preventPropagation}>
6169
{showButton ? (
6270
<UserActions username={username} name={name} rid={rid} _id={_id} freeSwitchExtension={freeSwitchExtension} reload={reload} />

apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersRow.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { IUser, IRoom } from '@rocket.chat/core-typings';
1+
import type { IRoom } from '@rocket.chat/core-typings';
22
import type { MouseEvent, ReactElement } from 'react';
33
import { memo } from 'react';
44

55
import RoomMembersItem from './RoomMembersItem';
6+
import type { RoomMember } from '../../../hooks/useMembersList';
67

78
type RoomMembersRowProps = {
8-
user: Pick<IUser, 'federated' | 'username' | 'name' | '_id' | 'freeSwitchExtension'>;
9+
user: Pick<RoomMember, 'federated' | 'username' | 'name' | '_id' | 'freeSwitchExtension' | 'subscription'>;
910
data: {
1011
onClickView: (e: MouseEvent<HTMLElement>) => void;
1112
rid: IRoom['_id'];
@@ -30,6 +31,7 @@ const RoomMembersRow = ({ user, data: { onClickView, rid }, index, reload, useRe
3031
name={user.name}
3132
federated={user.federated}
3233
freeSwitchExtension={user.freeSwitchExtension}
34+
subscription={user.subscription}
3335
onClickView={onClickView}
3436
reload={reload}
3537
/>

apps/meteor/client/views/room/contextualBar/RoomMembers/__snapshots__/RoomMembers.spec.tsx.snap

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,172 @@ exports[`renders WithABACRoom without crashing 1`] = `
478478
</div>
479479
</body>
480480
`;
481+
482+
exports[`renders WithInvitedMember without crashing 1`] = `
483+
<body>
484+
<div>
485+
<div
486+
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-yefw2m"
487+
>
488+
<span
489+
data-focus-scope-start="true"
490+
hidden=""
491+
/>
492+
<div
493+
aria-labelledby="contextualbarTitle"
494+
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-20qf5w"
495+
role="dialog"
496+
tabindex="-1"
497+
>
498+
<div
499+
class="rcx-box rcx-box--full rcx-css-zsa0ng"
500+
>
501+
<div
502+
class="rcx-box rcx-box--full rcx-css-1sl6k6j"
503+
>
504+
<i
505+
aria-hidden="true"
506+
class="rcx-box rcx-box--full rcx-icon--name-members rcx-icon rcx-css-x7bl3q rcx-css-g86psg"
507+
>
508+
509+
</i>
510+
<div
511+
class="rcx-box rcx-box--full rcx-css-x7bl3q rcx-css-1to6ka7"
512+
id="contextualbarTitle"
513+
>
514+
Members
515+
</div>
516+
</div>
517+
</div>
518+
<div
519+
class="rcx-box rcx-box--full rcx-vertical-bar__section rcx-css-137qgp8"
520+
>
521+
<label
522+
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper"
523+
>
524+
<input
525+
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-text rcx-input-box"
526+
placeholder="Search_by_username"
527+
size="1"
528+
type="text"
529+
value="filter"
530+
/>
531+
<span
532+
class="rcx-box rcx-box--full rcx-input-box__addon"
533+
>
534+
<i
535+
aria-hidden="true"
536+
class="rcx-box rcx-box--full rcx-icon--name-magnifier rcx-icon rcx-css-4pvxx3"
537+
>
538+
539+
</i>
540+
</span>
541+
</label>
542+
<div
543+
class="rcx-box rcx-box--full rcx-css-l1qvi5"
544+
>
545+
<button
546+
aria-expanded="false"
547+
aria-haspopup="listbox"
548+
aria-labelledby="react-aria-:r13:"
549+
class="rcx-box rcx-box--full rcx-select rcx-css-1vw6rc6"
550+
type="button"
551+
>
552+
<div
553+
aria-hidden="true"
554+
data-a11y-ignore="aria-hidden-focus"
555+
data-react-aria-prevent-focus="true"
556+
data-testid="hidden-select-container"
557+
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
558+
>
559+
<label>
560+
<select
561+
tabindex="-1"
562+
>
563+
<option />
564+
<option
565+
value="online"
566+
>
567+
online
568+
</option>
569+
<option
570+
value="all"
571+
>
572+
all
573+
</option>
574+
</select>
575+
</label>
576+
</div>
577+
<span
578+
class="rcx-box rcx-box--full rcx-css-e3nij6"
579+
id="react-aria-:r13:"
580+
>
581+
Online
582+
</span>
583+
<i
584+
aria-hidden="true"
585+
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-1wz6xj9"
586+
>
587+
588+
</i>
589+
</button>
590+
</div>
591+
</div>
592+
<div
593+
class="rcx-box rcx-box--full rcx-vertical-bar__content rcx-css-1w66n2o"
594+
>
595+
<div
596+
class="rcx-box rcx-box--full rcx-css-nyqak3"
597+
>
598+
<span
599+
class="rcx-box rcx-box--full rcx-css-1848tdm"
600+
>
601+
Showing_current_of_total
602+
</span>
603+
</div>
604+
<div
605+
class="rcx-box rcx-box--full rcx-css-4k230l"
606+
>
607+
<div
608+
class="rcx-box rcx-box--full rcx-css-vlo1oi rcx-css-1cb6i7s"
609+
>
610+
<div
611+
data-testid="virtuoso-scroller"
612+
data-virtuoso-scroller="true"
613+
style="height: 100%; outline: none; overflow-y: auto; position: relative; width: 100%;"
614+
tabindex="-1"
615+
>
616+
<div
617+
style="width: 100%; position: -webkit-sticky; top: 0px; z-index: 1; margin-top: 0px;"
618+
>
619+
<div
620+
data-testid="virtuoso-top-item-list"
621+
/>
622+
</div>
623+
<div
624+
data-viewport-type="element"
625+
style="width: 100%; height: 100%; position: absolute; top: 0px;"
626+
>
627+
<div
628+
data-testid="virtuoso-item-list"
629+
style="box-sizing: border-box; margin-top: 0px; padding-top: 0px; padding-bottom: 0px;"
630+
/>
631+
<div>
632+
<div
633+
class="rcx-box rcx-box--full rcx-css-jsq7k5"
634+
/>
635+
</div>
636+
</div>
637+
</div>
638+
</div>
639+
</div>
640+
</div>
641+
</div>
642+
<span
643+
data-focus-scope-end="true"
644+
hidden=""
645+
/>
646+
</div>
647+
</div>
648+
</body>
649+
`;

0 commit comments

Comments
 (0)