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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user