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.
*/
import React, { useState } from "react";
import React, { ReactElement, useState } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
@ -22,6 +22,8 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import Tooltip, { Alignment } from "../elements/Tooltip";
import { usePermalink } from "../../../hooks/usePermalink";
import RoomAvatar from "../avatars/RoomAvatar";
import MemberAvatar from "../avatars/MemberAvatar";
export enum PillType {
UserMention = "TYPE_USER_MENTION",
@ -52,13 +54,13 @@ export interface PillProps {
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar }) => {
const [hover, setHover] = useState(false);
const { avatar, onClick, resourceId, text, type } = usePermalink({
const { member, onClick, resourceId, targetRoom, text, type } = usePermalink({
room,
type: propType,
url,
});
if (!type) {
if (!type || !text) {
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;
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 (
<bdi>
@ -91,14 +114,12 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
>
{shouldShowPillAvatar && avatar}
<span className="mx_Pill_linkText">{text}</span>
{content}
{tip}
</a>
) : (
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
{shouldShowPillAvatar && avatar}
<span className="mx_Pill_linkText">{text}</span>
{content}
{tip}
</span>
)}

View file

@ -16,7 +16,7 @@ limitations under the License.
import { logger } from "matrix-js-sdk/src/logger";
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 { PillType } from "../components/views/elements/Pill";
@ -24,8 +24,6 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
import { parsePermalink } from "../utils/permalinks/Permalinks";
import dis from "../dispatcher/dispatcher";
import { Action } from "../dispatcher/actions";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import MemberAvatar from "../components/views/avatars/MemberAvatar";
interface Args {
/** Room in which the permalink should be displayed. */
@ -37,13 +35,38 @@ interface Args {
}
interface HookResult {
/** Avatar of the permalinked resource. */
avatar: ReactElement | null;
/** Displayable text of the permalink resource. Can for instance be a user or room name. */
/**
* Room member of a user mention permalink.
* 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;
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;
/**
* 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;
}
@ -53,7 +76,7 @@ interface HookResult {
export const usePermalink: (args: Args) => HookResult = ({ room, type: argType, url }): HookResult => {
const [member, setMember] = useState<RoomMember | null>(null);
// 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;
@ -101,9 +124,6 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
useMemo(() => {
switch (type) {
case PillType.AtRoomMention:
setTargetRoom(room);
break;
case PillType.UserMention:
{
if (resourceId) {
@ -131,23 +151,20 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
);
})
: MatrixClientPeg.get().getRoom(resourceId);
setTargetRoom(newRoom);
setTargetRoom(newRoom || null);
}
}
break;
}
}, [doProfileLookup, type, resourceId, room]);
let onClick: ((e: ButtonEvent) => void) | null = null;
let avatar: ReactElement | null = null;
let onClick: (e: ButtonEvent) => void = () => {};
let text = resourceId;
if (type === PillType.AtRoomMention && room) {
text = "@room";
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
} else if (type === PillType.UserMention && member) {
text = member.name || resourceId;
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" hideTitle />;
onClick = (e: ButtonEvent): void => {
e.preventDefault();
dis.dispatch({
@ -158,15 +175,15 @@ export const usePermalink: (args: Args) => HookResult = ({ room, type: argType,
} else if (type === PillType.RoomMention) {
if (targetRoom) {
text = targetRoom.name || resourceId;
avatar = <RoomAvatar room={targetRoom} width={16} height={16} aria-hidden="true" />;
}
}
return {
avatar,
text,
member,
onClick,
resourceId,
targetRoom,
text,
type,
};
};

View file

@ -38,6 +38,8 @@ describe("<Pill>", () => {
const room1Alias = "#room1:example.com";
const room1Id = "!room1:example.com";
let room1: Room;
const space1Id = "!space1:example.com";
let space1: Room;
const user1Id = "@user1:example.com";
const user2Id = "@user2:example.com";
let renderResult: RenderResult;
@ -70,9 +72,13 @@ describe("<Pill>", () => {
]);
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) => {
if (roomId === room1.roomId) return room1;
if (roomId === space1.roomId) return space1;
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", () => {
renderPill({
url: permalinkPrefix + room1Alias,

View file

@ -1,5 +1,7 @@
// 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`] = `
<DocumentFragment>
<bdi>
@ -91,6 +93,44 @@ exports[`<Pill> should render the expected pill for a room alias 1`] = `
</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`] = `
<DocumentFragment>
<bdi>