Extract avatars from permalink hook (#10328)

This commit is contained in:
Michael Weimann 2023-03-09 12:48:36 +01:00 committed by GitHub
parent edd8865670
commit 85e8d27697
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 28 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useState } from "react"; import React, { ReactElement, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
@ -22,6 +22,8 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import Tooltip, { Alignment } from "../elements/Tooltip"; import Tooltip, { Alignment } from "../elements/Tooltip";
import { usePermalink } from "../../../hooks/usePermalink"; import { usePermalink } from "../../../hooks/usePermalink";
import RoomAvatar from "../avatars/RoomAvatar";
import MemberAvatar from "../avatars/MemberAvatar";
export enum PillType { export enum PillType {
UserMention = "TYPE_USER_MENTION", UserMention = "TYPE_USER_MENTION",
@ -52,13 +54,13 @@ export interface PillProps {
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar }) => { export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar }) => {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { avatar, onClick, resourceId, text, type } = usePermalink({ const { member, onClick, resourceId, targetRoom, text, type } = usePermalink({
room, room,
type: propType, type: propType,
url, url,
}); });
if (!type) { if (!type || !text) {
return null; return null;
} }
@ -79,6 +81,27 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
}; };
const tip = hover && resourceId ? <Tooltip label={resourceId} alignment={Alignment.Right} /> : null; const tip = hover && resourceId ? <Tooltip label={resourceId} alignment={Alignment.Right} /> : null;
let avatar: ReactElement | null = null;
switch (type) {
case PillType.AtRoomMention:
case PillType.RoomMention:
case "space":
avatar = targetRoom ? <RoomAvatar room={targetRoom} width={16} height={16} aria-hidden="true" /> : null;
break;
case PillType.UserMention:
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" hideTitle />;
break;
default:
return null;
}
const content = (
<>
{shouldShowPillAvatar && avatar}
<span className="mx_Pill_linkText">{text}</span>
</>
);
return ( return (
<bdi> <bdi>
@ -91,14 +114,12 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
onMouseOver={onMouseOver} onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
{shouldShowPillAvatar && avatar} {content}
<span className="mx_Pill_linkText">{text}</span>
{tip} {tip}
</a> </a>
) : ( ) : (
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}> <span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
{shouldShowPillAvatar && avatar} {content}
<span className="mx_Pill_linkText">{text}</span>
{tip} {tip}
</span> </span>
)} )}

View file

@ -16,7 +16,7 @@ limitations under the License.
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import React, { ReactElement, useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { ButtonEvent } from "../components/views/elements/AccessibleButton"; import { ButtonEvent } from "../components/views/elements/AccessibleButton";
import { PillType } from "../components/views/elements/Pill"; import { PillType } from "../components/views/elements/Pill";
@ -24,8 +24,6 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
import { parsePermalink } from "../utils/permalinks/Permalinks"; import { parsePermalink } from "../utils/permalinks/Permalinks";
import dis from "../dispatcher/dispatcher"; import dis from "../dispatcher/dispatcher";
import { Action } from "../dispatcher/actions"; import { Action } from "../dispatcher/actions";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import MemberAvatar from "../components/views/avatars/MemberAvatar";
interface Args { interface Args {
/** Room in which the permalink should be displayed. */ /** Room in which the permalink should be displayed. */
@ -37,13 +35,38 @@ interface Args {
} }
interface HookResult { interface HookResult {
/** Avatar of the permalinked resource. */ /**
avatar: ReactElement | null; * Room member of a user mention permalink.
/** Displayable text of the permalink resource. Can for instance be a user or room name. */ * null for other links, if the profile was not found or not yet loaded.
* This can change, for instance, from null to a RoomMember after the profile lookup completed.
*/
member: RoomMember | null;
/**
* Displayable text of the permalink resource. Can for instance be a user or room name.
* null here means that there is nothing to display. Most likely if the URL was not a permalink.
*/
text: string | null; text: string | null;
onClick: ((e: ButtonEvent) => void) | null; /**
/** This can be for instance a user or room Id. */ * Should be used for click actions on the permalink.
* In case of a user permalink, a view profile action is dispatched.
*/
onClick: (e: ButtonEvent) => void;
/**
* This can be for instance a user or room Id.
* null here means that the resource cannot be detected. Most likely if the URL was not a permalink.
*/
resourceId: string | null; resourceId: string | null;
/**
* Target room of the permalink:
* For an @room mention, this is the room where the permalink should be displayed.
* For a room permalink, it is the room from the permalink.
* null for other links or if the room cannot be found.
*/
targetRoom: Room | null;
/**
* Type of the pill plus "space" for spaces.
* null here means that the type cannot be detected. Most likely if the URL was not a permalink.
*/
type: PillType | "space" | null; type: PillType | "space" | null;
} }
@ -53,7 +76,7 @@ interface HookResult {
export const usePermalink: (args: Args) => HookResult = ({ room, type: argType, url }): HookResult => { export const usePermalink: (args: Args) => HookResult = ({ room, type: argType, url }): HookResult => {
const [member, setMember] = useState<RoomMember | null>(null); const [member, setMember] = useState<RoomMember | null>(null);
// room of the entity this pill points to // room of the entity this pill points to
const [targetRoom, setTargetRoom] = useState<Room | undefined | null>(room); const [targetRoom, setTargetRoom] = useState<Room | null>(room ?? null);
let resourceId: string | null = null; let resourceId: string | null = null;
@ -101,9 +124,6 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
useMemo(() => { useMemo(() => {
switch (type) { switch (type) {
case PillType.AtRoomMention:
setTargetRoom(room);
break;
case PillType.UserMention: case PillType.UserMention:
{ {
if (resourceId) { if (resourceId) {
@ -131,23 +151,20 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
); );
}) })
: MatrixClientPeg.get().getRoom(resourceId); : MatrixClientPeg.get().getRoom(resourceId);
setTargetRoom(newRoom); setTargetRoom(newRoom || null);
} }
} }
break; break;
} }
}, [doProfileLookup, type, resourceId, room]); }, [doProfileLookup, type, resourceId, room]);
let onClick: ((e: ButtonEvent) => void) | null = null; let onClick: (e: ButtonEvent) => void = () => {};
let avatar: ReactElement | null = null;
let text = resourceId; let text = resourceId;
if (type === PillType.AtRoomMention && room) { if (type === PillType.AtRoomMention && room) {
text = "@room"; text = "@room";
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
} else if (type === PillType.UserMention && member) { } else if (type === PillType.UserMention && member) {
text = member.name || resourceId; text = member.name || resourceId;
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" hideTitle />;
onClick = (e: ButtonEvent): void => { onClick = (e: ButtonEvent): void => {
e.preventDefault(); e.preventDefault();
dis.dispatch({ dis.dispatch({
@ -158,15 +175,15 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
} else if (type === PillType.RoomMention) { } else if (type === PillType.RoomMention) {
if (targetRoom) { if (targetRoom) {
text = targetRoom.name || resourceId; text = targetRoom.name || resourceId;
avatar = <RoomAvatar room={targetRoom} width={16} height={16} aria-hidden="true" />;
} }
} }
return { return {
avatar, member,
text,
onClick, onClick,
resourceId, resourceId,
targetRoom,
text,
type, type,
}; };
}; };

View file

@ -38,6 +38,8 @@ describe("<Pill>", () => {
const room1Alias = "#room1:example.com"; const room1Alias = "#room1:example.com";
const room1Id = "!room1:example.com"; const room1Id = "!room1:example.com";
let room1: Room; let room1: Room;
const space1Id = "!space1:example.com";
let space1: Room;
const user1Id = "@user1:example.com"; const user1Id = "@user1:example.com";
const user2Id = "@user2:example.com"; const user2Id = "@user2:example.com";
let renderResult: RenderResult; let renderResult: RenderResult;
@ -70,9 +72,13 @@ describe("<Pill>", () => {
]); ]);
room1.getMember(user1Id)!.setMembershipEvent(user1JoinRoom1Event); room1.getMember(user1Id)!.setMembershipEvent(user1JoinRoom1Event);
client.getRooms.mockReturnValue([room1]); space1 = new Room(space1Id, client, client.getSafeUserId());
space1.name = "Space 1";
client.getRooms.mockReturnValue([room1, space1]);
client.getRoom.mockImplementation((roomId: string) => { client.getRoom.mockImplementation((roomId: string) => {
if (roomId === room1.roomId) return room1; if (roomId === room1.roomId) return room1;
if (roomId === space1.roomId) return space1;
return null; return null;
}); });
@ -116,6 +122,20 @@ describe("<Pill>", () => {
}); });
}); });
it("should not render a non-permalink", () => {
renderPill({
url: "https://example.com/hello",
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for a space", () => {
renderPill({
url: permalinkPrefix + space1Id,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for a room alias", () => { it("should render the expected pill for a room alias", () => {
renderPill({ renderPill({
url: permalinkPrefix + room1Alias, url: permalinkPrefix + room1Alias,

View file

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Pill> should not render a non-permalink 1`] = `<DocumentFragment />`;
exports[`<Pill> should not render an avatar or link when called with inMessage = false and shouldShowPillAvatar = false 1`] = ` exports[`<Pill> should not render an avatar or link when called with inMessage = false and shouldShowPillAvatar = false 1`] = `
<DocumentFragment> <DocumentFragment>
<bdi> <bdi>
@ -91,6 +93,44 @@ exports[`<Pill> should render the expected pill for a room alias 1`] = `
</DocumentFragment> </DocumentFragment>
`; `;
exports[`<Pill> should render the expected pill for a space 1`] = `
<DocumentFragment>
<bdi>
<a
class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/!space1:example.com"
>
<span
aria-hidden="true"
class="mx_BaseAvatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 10.4px; width: 16px; line-height: 16px;"
>
S
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
src="data:image/png;base64,00"
style="width: 16px; height: 16px;"
/>
</span>
<span
class="mx_Pill_linkText"
>
Space 1
</span>
</a>
</bdi>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for a user not in the room 1`] = ` exports[`<Pill> should render the expected pill for a user not in the room 1`] = `
<DocumentFragment> <DocumentFragment>
<bdi> <bdi>