Skip to content

Commit ad26925

Browse files
authored
Refactor pill and add tests (matrix-org#10304)
1 parent c0e4021 commit ad26925

File tree

10 files changed

+654
-276
lines changed

10 files changed

+654
-276
lines changed

Diff for: src/components/views/elements/Pill.tsx

+61-265
Original file line numberDiff line numberDiff line change
@@ -14,299 +14,95 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React from "react";
17+
import React, { useState } from "react";
1818
import classNames from "classnames";
1919
import { Room } from "matrix-js-sdk/src/models/room";
20-
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
21-
import { logger } from "matrix-js-sdk/src/logger";
22-
import { MatrixClient } from "matrix-js-sdk/src/client";
23-
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
2420

25-
import dis from "../../../dispatcher/dispatcher";
2621
import { MatrixClientPeg } from "../../../MatrixClientPeg";
27-
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
2822
import MatrixClientContext from "../../../contexts/MatrixClientContext";
29-
import { Action } from "../../../dispatcher/actions";
30-
import Tooltip, { Alignment } from "./Tooltip";
31-
import RoomAvatar from "../avatars/RoomAvatar";
32-
import MemberAvatar from "../avatars/MemberAvatar";
33-
import { objectHasDiff } from "../../../utils/objects";
34-
import { ButtonEvent } from "./AccessibleButton";
23+
import Tooltip, { Alignment } from "../elements/Tooltip";
24+
import { usePermalink } from "../../../hooks/usePermalink";
3525

3626
export enum PillType {
3727
UserMention = "TYPE_USER_MENTION",
3828
RoomMention = "TYPE_ROOM_MENTION",
3929
AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention
4030
}
4131

42-
interface IProps {
32+
export const pillRoomNotifPos = (text: string): number => {
33+
return text.indexOf("@room");
34+
};
35+
36+
export const pillRoomNotifLen = (): number => {
37+
return "@room".length;
38+
};
39+
40+
export interface PillProps {
4341
// The Type of this Pill. If url is given, this is auto-detected.
4442
type?: PillType;
4543
// The URL to pillify (no validation is done)
4644
url?: string;
47-
// Whether the pill is in a message
45+
/** Whether the pill is in a message. It will act as a link then. */
4846
inMessage?: boolean;
4947
// The room in which this pill is being rendered
5048
room?: Room;
5149
// Whether to include an avatar in the pill
5250
shouldShowPillAvatar?: boolean;
5351
}
5452

55-
interface IState {
56-
// ID/alias of the room/user
57-
resourceId: string;
58-
// Type of pill
59-
pillType: string;
60-
// The member related to the user pill
61-
member?: RoomMember;
62-
// The room related to the room pill
63-
room?: Room;
64-
// Is the user hovering the pill
65-
hover: boolean;
66-
}
67-
68-
export default class Pill extends React.Component<IProps, IState> {
69-
private unmounted = true;
70-
private matrixClient: MatrixClient;
71-
72-
public static roomNotifPos(text: string): number {
73-
return text.indexOf("@room");
74-
}
75-
76-
public static roomNotifLen(): number {
77-
return "@room".length;
78-
}
79-
80-
public constructor(props: IProps) {
81-
super(props);
82-
83-
this.state = {
84-
resourceId: null,
85-
pillType: null,
86-
member: null,
87-
room: null,
88-
hover: false,
89-
};
90-
}
91-
92-
private load(): void {
93-
let resourceId: string;
94-
let prefix: string;
95-
96-
if (this.props.url) {
97-
if (this.props.inMessage) {
98-
const parts = parsePermalink(this.props.url);
99-
resourceId = parts.primaryEntityId; // The room/user ID
100-
prefix = parts.sigil; // The first character of prefix
101-
} else {
102-
resourceId = getPrimaryPermalinkEntity(this.props.url);
103-
prefix = resourceId ? resourceId[0] : undefined;
104-
}
105-
}
106-
107-
const pillType =
108-
this.props.type ||
109-
{
110-
"@": PillType.UserMention,
111-
"#": PillType.RoomMention,
112-
"!": PillType.RoomMention,
113-
}[prefix];
53+
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar }) => {
54+
const [hover, setHover] = useState(false);
55+
const { avatar, onClick, resourceId, text, type } = usePermalink({
56+
room,
57+
type: propType,
58+
url,
59+
});
11460

115-
let member: RoomMember;
116-
let room: Room;
117-
switch (pillType) {
118-
case PillType.AtRoomMention:
119-
{
120-
room = this.props.room;
121-
}
122-
break;
123-
case PillType.UserMention:
124-
{
125-
const localMember = this.props.room?.getMember(resourceId);
126-
member = localMember;
127-
if (!localMember) {
128-
member = new RoomMember(null, resourceId);
129-
this.doProfileLookup(resourceId, member);
130-
}
131-
}
132-
break;
133-
case PillType.RoomMention:
134-
{
135-
const localRoom =
136-
resourceId[0] === "#"
137-
? MatrixClientPeg.get()
138-
.getRooms()
139-
.find((r) => {
140-
return (
141-
r.getCanonicalAlias() === resourceId || r.getAltAliases().includes(resourceId)
142-
);
143-
})
144-
: MatrixClientPeg.get().getRoom(resourceId);
145-
room = localRoom;
146-
if (!localRoom) {
147-
// TODO: This would require a new API to resolve a room alias to
148-
// a room avatar and name.
149-
// this.doRoomProfileLookup(resourceId, member);
150-
}
151-
}
152-
break;
153-
}
154-
this.setState({ resourceId, pillType, member, room });
61+
if (!type) {
62+
return null;
15563
}
15664

157-
public componentDidMount(): void {
158-
this.unmounted = false;
159-
this.matrixClient = MatrixClientPeg.get();
160-
this.load();
161-
}
162-
163-
public componentDidUpdate(prevProps: Readonly<IProps>): void {
164-
if (objectHasDiff(this.props, prevProps)) {
165-
this.load();
166-
}
167-
}
168-
169-
public componentWillUnmount(): void {
170-
this.unmounted = true;
171-
}
65+
const classes = classNames("mx_Pill", {
66+
mx_AtRoomPill: type === PillType.AtRoomMention,
67+
mx_RoomPill: type === PillType.RoomMention,
68+
mx_SpacePill: type === "space",
69+
mx_UserPill: type === PillType.UserMention,
70+
mx_UserPill_me: resourceId === MatrixClientPeg.get().getUserId(),
71+
});
17272

173-
private onMouseOver = (): void => {
174-
this.setState({
175-
hover: true,
176-
});
73+
const onMouseOver = (): void => {
74+
setHover(true);
17775
};
17876

179-
private onMouseLeave = (): void => {
180-
this.setState({
181-
hover: false,
182-
});
77+
const onMouseLeave = (): void => {
78+
setHover(false);
18379
};
18480

185-
private doProfileLookup(userId: string, member: RoomMember): void {
186-
MatrixClientPeg.get()
187-
.getProfileInfo(userId)
188-
.then((resp) => {
189-
if (this.unmounted) {
190-
return;
191-
}
192-
member.name = resp.displayname;
193-
member.rawDisplayName = resp.displayname;
194-
member.events.member = {
195-
getContent: () => {
196-
return { avatar_url: resp.avatar_url };
197-
},
198-
getDirectionalContent: function () {
199-
return this.getContent();
200-
},
201-
} as MatrixEvent;
202-
this.setState({ member });
203-
})
204-
.catch((err) => {
205-
logger.error("Could not retrieve profile data for " + userId + ":", err);
206-
});
207-
}
208-
209-
private onUserPillClicked = (e: ButtonEvent): void => {
210-
e.preventDefault();
211-
dis.dispatch({
212-
action: Action.ViewUser,
213-
member: this.state.member,
214-
});
215-
};
216-
217-
public render(): React.ReactNode {
218-
const resource = this.state.resourceId;
219-
220-
let avatar = null;
221-
let linkText = resource;
222-
let pillClass;
223-
let userId;
224-
let href = this.props.url;
225-
let onClick;
226-
switch (this.state.pillType) {
227-
case PillType.AtRoomMention:
228-
{
229-
const room = this.props.room;
230-
if (room) {
231-
linkText = "@room";
232-
if (this.props.shouldShowPillAvatar) {
233-
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
234-
}
235-
pillClass = "mx_AtRoomPill";
236-
}
237-
}
238-
break;
239-
case PillType.UserMention:
240-
{
241-
// If this user is not a member of this room, default to the empty member
242-
const member = this.state.member;
243-
if (member) {
244-
userId = member.userId;
245-
member.rawDisplayName = member.rawDisplayName || "";
246-
linkText = member.rawDisplayName;
247-
if (this.props.shouldShowPillAvatar) {
248-
avatar = (
249-
<MemberAvatar member={member} width={16} height={16} aria-hidden="true" hideTitle />
250-
);
251-
}
252-
pillClass = "mx_UserPill";
253-
href = null;
254-
onClick = this.onUserPillClicked;
255-
}
256-
}
257-
break;
258-
case PillType.RoomMention:
259-
{
260-
const room = this.state.room;
261-
if (room) {
262-
linkText = room.name || resource;
263-
if (this.props.shouldShowPillAvatar) {
264-
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
265-
}
266-
}
267-
pillClass = room?.isSpaceRoom() ? "mx_SpacePill" : "mx_RoomPill";
268-
}
269-
break;
270-
}
271-
272-
const classes = classNames("mx_Pill", pillClass, {
273-
mx_UserPill_me: userId === MatrixClientPeg.get().getUserId(),
274-
});
275-
276-
if (this.state.pillType) {
277-
let tip;
278-
if (this.state.hover && resource) {
279-
tip = <Tooltip label={resource} alignment={Alignment.Right} />;
280-
}
281-
282-
return (
283-
<bdi>
284-
<MatrixClientContext.Provider value={this.matrixClient}>
285-
{this.props.inMessage ? (
286-
<a
287-
className={classes}
288-
href={href}
289-
onClick={onClick}
290-
onMouseOver={this.onMouseOver}
291-
onMouseLeave={this.onMouseLeave}
292-
>
293-
{avatar}
294-
<span className="mx_Pill_linkText">{linkText}</span>
295-
{tip}
296-
</a>
297-
) : (
298-
<span className={classes} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave}>
299-
{avatar}
300-
<span className="mx_Pill_linkText">{linkText}</span>
301-
{tip}
302-
</span>
303-
)}
304-
</MatrixClientContext.Provider>
305-
</bdi>
306-
);
307-
} else {
308-
// Deliberately render nothing if the URL isn't recognised
309-
return null;
310-
}
311-
}
312-
}
81+
const tip = hover && resourceId ? <Tooltip label={resourceId} alignment={Alignment.Right} /> : null;
82+
83+
return (
84+
<bdi>
85+
<MatrixClientContext.Provider value={MatrixClientPeg.get()}>
86+
{inMessage && url ? (
87+
<a
88+
className={classes}
89+
href={url}
90+
onClick={onClick}
91+
onMouseOver={onMouseOver}
92+
onMouseLeave={onMouseLeave}
93+
>
94+
{shouldShowPillAvatar && avatar}
95+
<span className="mx_Pill_linkText">{text}</span>
96+
{tip}
97+
</a>
98+
) : (
99+
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
100+
{shouldShowPillAvatar && avatar}
101+
<span className="mx_Pill_linkText">{text}</span>
102+
{tip}
103+
</span>
104+
)}
105+
</MatrixClientContext.Provider>
106+
</bdi>
107+
);
108+
};

Diff for: src/components/views/elements/ReplyChain.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
2+
Copyright 2017 - 2023 The Matrix.org Foundation C.I.C.
33
Copyright 2019 Michael Telatynski <[email protected]>
44
55
Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,7 +30,7 @@ import { getUserNameColorClass } from "../../../utils/FormattingUtils";
3030
import { Action } from "../../../dispatcher/actions";
3131
import Spinner from "./Spinner";
3232
import ReplyTile from "../rooms/ReplyTile";
33-
import Pill, { PillType } from "./Pill";
33+
import { Pill, PillType } from "./Pill";
3434
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
3535
import { getParentEventId, shouldDisplayReply } from "../../../utils/Reply";
3636
import RoomContext from "../../../contexts/RoomContext";

Diff for: src/components/views/settings/BridgeTile.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2020 The Matrix.org Foundation C.I.C.
2+
Copyright 2020-2023 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
2020
import { logger } from "matrix-js-sdk/src/logger";
2121

2222
import { _t } from "../../../languageHandler";
23-
import Pill, { PillType } from "../elements/Pill";
23+
import { Pill, PillType } from "../elements/Pill";
2424
import { makeUserPermalink } from "../../../utils/permalinks/Permalinks";
2525
import BaseAvatar from "../avatars/BaseAvatar";
2626
import SettingsStore from "../../../settings/SettingsStore";

0 commit comments

Comments
 (0)