Skip to content

Commit 1644169

Browse files
Implement changes to memberlist from feedback (#29029)
* Add a separator between joined and invited members * Fix user label in tile having wrong color * Changes to member tiles - ThreePidInviteTile now contains an user label showing "(Invited)" and an email icon. - RoomMemberTile now includes an icon similar to above. - Refactors a bunch of code to make this change sensible. * Remove redundant css code * Fix tests * Update src/components/viewmodels/memberlist/MemberListViewModel.tsx Co-authored-by: Michael Telatynski <[email protected]> * Update year in license * Fix lint error --------- Co-authored-by: Michael Telatynski <[email protected]>
1 parent cf895b4 commit 1644169

File tree

14 files changed

+180
-61
lines changed

14 files changed

+180
-61
lines changed
Loading

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@
283283
@import "./views/rooms/_EventTile.pcss";
284284
@import "./views/rooms/_HistoryTile.pcss";
285285
@import "./views/rooms/_IRCLayout.pcss";
286+
@import "./views/rooms/_InvitedIconView.pcss";
286287
@import "./views/rooms/_JumpToBottomButton.pcss";
287288
@import "./views/rooms/_LinkPreviewGroup.pcss";
288289
@import "./views/rooms/_LinkPreviewWidget.pcss";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.mx_InvitedIconView {
9+
color: var(--cpd-color-icon-tertiary);
10+
}

res/css/views/rooms/_MemberListView.pcss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ Please see LICENSE files in the repository root for full details.
1414
.mx_MemberListView_container {
1515
height: 100%;
1616
}
17+
18+
.mx_MemberListView_separator {
19+
margin: 0;
20+
border: none;
21+
border-top: 2px solid var(--cpd-color-bg-subtle-primary);
22+
}
1723
}

res/css/views/rooms/_MemberTileView.pcss

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,15 @@ Please see LICENSE files in the repository root for full details.
3131
min-width: 0;
3232
}
3333

34-
.mx_MemberTileView_user_label {
34+
.mx_MemberTileView_userLabel {
3535
font: var(--cpd-font-body-sm-regular);
3636
font-size: 13px;
37+
color: var(--cpd-color-text-secondary);
3738
}
3839

3940
.mx_MemberTileView_avatar {
4041
position: relative;
4142
height: 32px;
4243
width: 32px;
4344
}
44-
45-
.mx_E2EIconView {
46-
display: flex;
47-
justify-content: center;
48-
align-items: center;
49-
}
50-
51-
.mx_E2EIconView_warning {
52-
color: var(--cpd-color-icon-critical-primary);
53-
}
54-
55-
.mx_E2EIconView_verified {
56-
color: var(--cpd-color-icon-success-primary);
57-
}
5845
}

src/components/viewmodels/memberlist/MemberListViewModel.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,12 @@ export function sdkRoomMemberToRoomMember(member: SdkRoomMember): Member {
9999
};
100100
}
101101

102+
export const SEPARATOR = "SEPARATOR";
103+
export type MemberWithSeparator = Member | typeof SEPARATOR;
104+
102105
export interface MemberListViewState {
103-
members: Member[];
106+
members: MemberWithSeparator[];
107+
memberCount: number;
104108
search: (searchQuery: string) => void;
105109
isPresenceEnabled: boolean;
106110
shouldShowInvite: boolean;
@@ -118,10 +122,16 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
118122
}
119123

120124
const sdkContext = useContext(SDKContext);
121-
const [memberMap, setMemberMap] = useState<Map<string, Member>>(new Map());
125+
const [memberMap, setMemberMap] = useState<Map<string, MemberWithSeparator>>(new Map());
122126
const [isLoading, setIsLoading] = useState<boolean>(true);
123127
// This is the last known total number of members in this room.
124128
const [totalMemberCount, setTotalMemberCount] = useState(0);
129+
/**
130+
* This is the current number of members in the list.
131+
* This number will be less than the total number of members
132+
* in the room when the search functionality is used.
133+
*/
134+
const [memberCount, setMemberCount] = useState(0);
125135

126136
const loadMembers = useMemo(
127137
() =>
@@ -131,24 +141,34 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
131141
roomId,
132142
searchQuery,
133143
);
134-
const newMemberMap = new Map<string, Member>();
135-
// First add the invited room members
144+
const threePidInvited = getPending3PidInvites(room, searchQuery);
145+
146+
const newMemberMap = new Map<string, MemberWithSeparator>();
147+
148+
// First add the joined room members
149+
for (const member of joinedSdk) {
150+
const roomMember = sdkRoomMemberToRoomMember(member);
151+
newMemberMap.set(member.userId, roomMember);
152+
}
153+
154+
// Then a separator if needed
155+
if (joinedSdk.length > 0 && (invitedSdk.length > 0 || threePidInvited.length > 0))
156+
newMemberMap.set(SEPARATOR, SEPARATOR);
157+
158+
// Then add the invited room members
136159
for (const member of invitedSdk) {
137160
const roomMember = sdkRoomMemberToRoomMember(member);
138161
newMemberMap.set(member.userId, roomMember);
139162
}
140-
// Then add the third party invites
141-
const threePidInvited = getPending3PidInvites(room, searchQuery);
163+
164+
// Finally add the third party invites
142165
for (const invited of threePidInvited) {
143166
const key = invited.threePidInvite!.event.getContent().display_name;
144167
newMemberMap.set(key, invited);
145168
}
146-
// Finally add the joined room members
147-
for (const member of joinedSdk) {
148-
const roomMember = sdkRoomMemberToRoomMember(member);
149-
newMemberMap.set(member.userId, roomMember);
150-
}
169+
151170
setMemberMap(newMemberMap);
171+
setMemberCount(joinedSdk.length + invitedSdk.length + threePidInvited.length);
152172
if (!searchQuery) {
153173
/**
154174
* Since searching for members only gives you the relevant
@@ -241,6 +261,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
241261

242262
return {
243263
members: Array.from(memberMap.values()),
264+
memberCount,
244265
search: loadMembers,
245266
shouldShowInvite,
246267
isPresenceEnabled,

src/components/viewmodels/memberlist/tiles/ThreePidTileViewModel.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
88
import dis from "../../../../dispatcher/dispatcher";
99
import { Action } from "../../../../dispatcher/actions";
1010
import { ThreePIDInvite } from "../../../../models/rooms/ThreePIDInvite";
11+
import { _t } from "../../../../languageHandler";
1112

1213
interface ThreePidTileViewModelProps {
1314
threePidInvite: ThreePIDInvite;
@@ -16,6 +17,7 @@ interface ThreePidTileViewModelProps {
1617
export interface ThreePidTileViewState {
1718
name: string;
1819
onClick: () => void;
20+
userLabel?: string;
1921
}
2022

2123
export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): ThreePidTileViewState {
@@ -28,8 +30,11 @@ export function useThreePidTileViewModel(props: ThreePidTileViewModelProps): Thr
2830
});
2931
};
3032

33+
const userLabel = `(${_t("member_list|invited_label")})`;
34+
3135
return {
3236
name,
3337
onClick,
38+
userLabel,
3439
};
3540
}

src/components/views/rooms/MemberList/MemberListHeaderView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,10 @@ function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
8888
</Flex>
8989
);
9090
}
91-
92-
const filteredMemberCount = vm.members.length;
93-
if (filteredMemberCount === 0) {
91+
if (vm.memberCount === 0) {
9492
return _t("member_list|no_matches");
9593
}
96-
return _t("member_list|count", { count: filteredMemberCount });
94+
return _t("member_list|count", { count: vm.memberCount });
9795
}
9896

9997
export const MemberListHeaderView: React.FC<Props> = (props: Props) => {

src/components/views/rooms/MemberList/MemberListView.tsx

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
1111
import { AutoSizer } from "react-virtualized";
1212

1313
import { Flex } from "../../../utils/Flex";
14-
import { useMemberListViewModel } from "../../../viewmodels/memberlist/MemberListViewModel";
14+
import {
15+
MemberWithSeparator,
16+
SEPARATOR,
17+
useMemberListViewModel,
18+
} from "../../../viewmodels/memberlist/MemberListViewModel";
1519
import { RoomMemberTileView } from "./tiles/RoomMemberTileView";
1620
import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
1721
import { MemberListHeaderView } from "./MemberListHeaderView";
@@ -26,22 +30,49 @@ interface IProps {
2630
const MemberListView: React.FC<IProps> = (props: IProps) => {
2731
const vm = useMemberListViewModel(props.roomId);
2832

29-
const memberCount = vm.members.length;
33+
const totalRows = vm.members.length;
34+
35+
const getRowComponent = (item: MemberWithSeparator): React.JSX.Element => {
36+
if (item === SEPARATOR) {
37+
return <hr className="mx_MemberListView_separator" />;
38+
} else if (item.member) {
39+
return <RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />;
40+
} else {
41+
return <ThreePidInviteTileView threePidInvite={item.threePidInvite} />;
42+
}
43+
};
44+
45+
const getRowHeight = ({ index }: { index: number }): number => {
46+
if (vm.members[index] === SEPARATOR) {
47+
/**
48+
* This is a separator of 2px height rendered between
49+
* joined and invited members.
50+
*/
51+
return 2;
52+
} else if (totalRows && index === totalRows) {
53+
/**
54+
* The empty spacer div rendered at the bottom should
55+
* have a height of 32px.
56+
*/
57+
return 32;
58+
} else {
59+
/**
60+
* The actual member tiles have a height of 56px.
61+
*/
62+
return 56;
63+
}
64+
};
3065

3166
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
32-
if (index === memberCount) {
67+
if (index === totalRows) {
3368
// We've rendered all the members,
3469
// now we render an empty div to add some space to the end of the list.
3570
return <div key={key} style={style} />;
3671
}
3772
const item = vm.members[index];
3873
return (
3974
<div key={key} style={style}>
40-
{item.member ? (
41-
<RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />
42-
) : (
43-
<ThreePidInviteTileView threePidInvite={item.threePidInvite} />
44-
)}
75+
{getRowComponent(item)}
4576
</div>
4677
);
4778
};
@@ -63,11 +94,9 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
6394
{({ height, width }) => (
6495
<List
6596
rowRenderer={rowRenderer}
66-
// All the member tiles will have a height of 56px.
67-
// The additional empty div at the end of the list should have a height of 32px.
68-
rowHeight={({ index }) => (index === memberCount ? 32 : 56)}
97+
rowHeight={getRowHeight}
6998
// The +1 refers to the additional empty div that we render at the end of the list.
70-
rowCount={memberCount + 1}
99+
rowCount={totalRows + 1}
71100
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
72101
height={height - 113}
73102
width={width}

src/components/views/rooms/MemberList/tiles/RoomMemberTileView.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { E2EIconView } from "./common/E2EIconView";
1414
import AvatarPresenceIconView from "./common/PresenceIconView";
1515
import BaseAvatar from "../../../avatars/BaseAvatar";
1616
import { _t } from "../../../../../languageHandler";
17-
import { MemberTileLayout } from "./common/MemberTileLayout";
17+
import { MemberTileView } from "./common/MemberTileView";
18+
import { InvitedIconView } from "./common/InvitedIconView";
1819

1920
interface IProps {
2021
member: RoomMember;
@@ -43,25 +44,23 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
4344
presenceJSX = <AvatarPresenceIconView presenceState={presenceState} />;
4445
}
4546

46-
let userLabelJSX;
47-
if (vm.userLabel) {
48-
userLabelJSX = <div className="mx_MemberTileView_user_label">{vm.userLabel}</div>;
49-
}
50-
51-
let e2eIcon;
47+
let iconJsx;
5248
if (vm.e2eStatus) {
53-
e2eIcon = <E2EIconView status={vm.e2eStatus} />;
49+
iconJsx = <E2EIconView status={vm.e2eStatus} />;
50+
}
51+
if (member.isInvite) {
52+
iconJsx = <InvitedIconView isThreePid={false} />;
5453
}
5554

5655
return (
57-
<MemberTileLayout
56+
<MemberTileView
5857
title={vm.title}
5958
onClick={vm.onClick}
6059
avatarJsx={av}
6160
presenceJsx={presenceJSX}
6261
nameJsx={nameJSX}
63-
userLabelJsx={userLabelJSX}
64-
e2eIconJsx={e2eIcon}
62+
userLabel={vm.userLabel}
63+
iconJsx={iconJsx}
6564
/>
6665
);
6766
}

src/components/views/rooms/MemberList/tiles/ThreePidInviteTileView.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import React from "react";
1010
import { useThreePidTileViewModel } from "../../../../viewmodels/memberlist/tiles/ThreePidTileViewModel";
1111
import { ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite";
1212
import BaseAvatar from "../../../avatars/BaseAvatar";
13-
import { MemberTileLayout } from "./common/MemberTileLayout";
13+
import { MemberTileView } from "./common/MemberTileView";
14+
import { InvitedIconView } from "./common/InvitedIconView";
1415

1516
interface Props {
1617
threePidInvite: ThreePIDInvite;
@@ -19,5 +20,15 @@ interface Props {
1920
export function ThreePidInviteTileView(props: Props): JSX.Element {
2021
const vm = useThreePidTileViewModel(props);
2122
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
22-
return <MemberTileLayout nameJsx={vm.name} avatarJsx={av} onClick={vm.onClick} />;
23+
const iconJsx = <InvitedIconView isThreePid={true} />;
24+
25+
return (
26+
<MemberTileView
27+
nameJsx={vm.name}
28+
avatarJsx={av}
29+
onClick={vm.onClick}
30+
userLabel={vm.userLabel}
31+
iconJsx={iconJsx}
32+
/>
33+
);
2334
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import React from "react";
9+
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
10+
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
11+
12+
import { Flex } from "../../../../../utils/Flex";
13+
14+
interface Props {
15+
isThreePid: boolean;
16+
}
17+
18+
export function InvitedIconView({ isThreePid }: Props): JSX.Element {
19+
const Icon = isThreePid ? EmailIcon : UserAddIcon;
20+
return (
21+
<Flex align="center" className="mx_InvitedIconView">
22+
<Icon height="16px" width="16px" />
23+
</Flex>
24+
);
25+
}

src/components/views/rooms/MemberList/tiles/common/MemberTileLayout.tsx renamed to src/components/views/rooms/MemberList/tiles/common/MemberTileView.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ interface Props {
1515
onClick: () => void;
1616
title?: string;
1717
presenceJsx?: JSX.Element;
18-
userLabelJsx?: JSX.Element;
19-
e2eIconJsx?: JSX.Element;
18+
userLabel?: React.ReactNode;
19+
iconJsx?: JSX.Element;
2020
}
2121

22-
export function MemberTileLayout(props: Props): JSX.Element {
22+
export function MemberTileView(props: Props): JSX.Element {
23+
let userLabelJsx: React.ReactNode;
24+
if (props.userLabel) {
25+
userLabelJsx = <div className="mx_MemberTileView_userLabel">{props.userLabel}</div>;
26+
}
27+
2328
return (
2429
// The wrapping div is required to make the magic mouse listener work, for some reason.
2530
<div>
@@ -31,8 +36,8 @@ export function MemberTileLayout(props: Props): JSX.Element {
3136
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
3237
</div>
3338
<div className="mx_MemberTileView_right">
34-
{props.userLabelJsx}
35-
{props.e2eIconJsx}
39+
{userLabelJsx}
40+
{props.iconJsx}
3641
</div>
3742
</AccessibleButton>
3843
</div>

0 commit comments

Comments
 (0)