Extract avatars from permalink hook (#10328)
This commit is contained in:
parent
edd8865670
commit
85e8d27697
4 changed files with 126 additions and 28 deletions
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue