Improve invite dialog ui - Part 1 (#30764)

* refactor: move `humanize` in shared components

* feat: add `RichItem` component

* feat: add `RichList` component

* refactor: use `RichList` and `RichItem` in `InviteDialog`

* fix: exclude `InviteDialog` button to css override

* test: update selector in invite dialog

* test(e2e): update crypto test to use correct selector

* test(e2e): update invite dialog

* test: add test for `humanize.ts`

* fix: add space between the list and the input when the list is scrollable

* test(e2e): update screenshots
This commit is contained in:
Florian Duros
2025-09-23 14:29:22 +02:00
committed by GitHub
parent 3c6d341357
commit b89de61e12
30 changed files with 848 additions and 159 deletions

View File

@@ -11,7 +11,6 @@ import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/m
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { humanizeTime } from "../../../utils/humanize";
import { preventDefaultWrapper } from "../../../utils/NativeEventUtils";
import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar";
@@ -19,6 +18,7 @@ import BeaconStatus from "./BeaconStatus";
import { BeaconDisplayStatus } from "./displayStatus";
import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon";
import ShareLatestLocation from "./ShareLatestLocation";
import { humanizeTime } from "../../../shared-components/utils/humanize";
interface Props {
beacon: Beacon;

View File

@@ -24,7 +24,6 @@ import { getDefaultIdentityServerUrl, setToDefaultIdentityServer } from "../../.
import { buildActivityScores, buildMemberScores, compareMembers } from "../../../utils/SortMembers";
import { abbreviateUrl } from "../../../utils/UrlUtils";
import IdentityAuthClient from "../../../IdentityAuthClient";
import { humanizeTime } from "../../../utils/humanize";
import { type IInviteResult, inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models";
@@ -65,6 +64,8 @@ import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDi
import { SdkContextClass } from "../../../contexts/SDKContext";
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
import InviteProgressBody from "./InviteProgressBody.tsx";
import { RichList } from "../../../shared-components/rich-list/RichList";
import { RichItem } from "../../../shared-components/rich-list/RichItem";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@@ -163,7 +164,6 @@ interface IDMRoomTileProps {
member: Member;
lastActiveTs?: number;
onToggle(member: Member): void;
highlightWord: string;
isSelected: boolean;
}
@@ -176,54 +176,8 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
this.props.onToggle(this.props.member);
};
private highlightName(str: string): ReactNode {
if (!this.props.highlightWord) return str;
// We convert things to lowercase for index searching, but pull substrings from
// the submitted text to preserve case. Note: we don't need to htmlEntities the
// string because React will safely encode the text for us.
const lowerStr = str.toLowerCase();
const filterStr = this.props.highlightWord.toLowerCase();
const result: JSX.Element[] = [];
let i = 0;
let ii: number;
while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) {
// Push any text we missed (first bit/middle of text)
if (ii > i) {
// Push any text we aren't highlighting (middle of text match, or beginning of text)
result.push(<span key={i + "begin"}>{str.substring(i, ii)}</span>);
}
i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
// Highlight the word the user entered
const substr = str.substring(i, filterStr.length + i);
result.push(
<span className="mx_InviteDialog_tile--room_highlight" key={i + "bold"}>
{substr}
</span>,
);
i += substr.length;
}
// Push any text we missed (end of text)
if (i < str.length) {
result.push(<span key={i + "end"}>{str.substring(i)}</span>);
}
return result;
}
public render(): React.ReactNode {
let timestamp: JSX.Element | undefined;
if (this.props.lastActiveTs) {
const humanTs = humanizeTime(this.props.lastActiveTs);
timestamp = <span className="mx_InviteDialog_tile--room_time">{humanTs}</span>;
}
const avatarSize = "36px";
const avatarSize = "32px";
const avatar = (this.props.member as ThreepidMember).isEmail ? (
<EmailPillAvatarIcon width={avatarSize} height={avatarSize} />
) : (
@@ -241,40 +195,23 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
/>
);
let checkmark: JSX.Element | undefined;
if (this.props.isSelected) {
// To reduce flickering we put the 'selected' room tile above the real avatar
checkmark = <div className="mx_InviteDialog_tile--room_selected" />;
}
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
// the browser from reloading the image source when the avatar remounts).
const stackedAvatar = (
<span className="mx_InviteDialog_tile_avatarStack">
{avatar}
{checkmark}
</span>
);
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
withDisplayName: true,
});
const caption = (this.props.member as ThreepidMember).isEmail
? _t("invite|email_caption")
: this.highlightName(userIdentifier || this.props.member.userId);
: userIdentifier || this.props.member.userId;
return (
<AccessibleButton className="mx_InviteDialog_tile mx_InviteDialog_tile--room" onClick={this.onClick}>
{stackedAvatar}
<span className="mx_InviteDialog_tile_nameStack">
<div className="mx_InviteDialog_tile_nameStack_name">
{this.highlightName(this.props.member.name)}
</div>
<div className="mx_InviteDialog_tile_nameStack_userId">{caption}</div>
</span>
{timestamp}
</AccessibleButton>
<RichItem
avatar={avatar}
title={this.props.member.name}
description={caption}
timestamp={this.props.lastActiveTs}
onClick={this.onClick}
selected={this.props.isSelected}
/>
);
}
}
@@ -1048,8 +985,13 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
return (
<div className="mx_InviteDialog_section">
<h3>{sectionName}</h3>
<p>{_t("common|no_results")}</p>
<RichList
title={sectionName}
titleAttributes={{ "role": "heading", "aria-level": 3 }}
isEmpty={true}
>
{_t("common|no_results")}
</RichList>
</div>
);
}
@@ -1084,14 +1026,15 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
lastActiveTs={lastActive(r)}
key={r.user.userId}
onToggle={this.toggleMember}
highlightWord={this.state.filterText}
isSelected={this.state.targets.some((t) => t.userId === r.userId)}
/>
));
return (
<div className="mx_InviteDialog_section">
<h3>{sectionName}</h3>
{tiles}
<RichList title={sectionName} titleAttributes={{ "role": "heading", "aria-level": 3 }}>
{tiles}
</RichList>
{showMore}
</div>
);