Tweak pill UI (#10417)

This commit is contained in:
Michael Weimann 2023-03-22 13:27:24 +01:00 committed by GitHub
parent 4c2b18c5d9
commit 3eb6a55b93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 54 deletions

View file

@ -59,12 +59,6 @@ limitations under the License.
min-width: $font-16px; /* ensure the avatar is not compressed */ min-width: $font-16px; /* ensure the avatar is not compressed */
} }
&.mx_EventPill .mx_BaseAvatar {
/* Event pill avatars are inside the text. */
margin-inline-start: 0.2em;
margin-inline-end: 0.2em;
}
.mx_Pill_text { .mx_Pill_text {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;

View file

@ -45,6 +45,8 @@ export const pillRoomNotifLen = (): number => {
return "@room".length; return "@room".length;
}; };
const linkIcon = <LinkIcon className="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image" />;
const PillRoomAvatar: React.FC<{ const PillRoomAvatar: React.FC<{
shouldShowPillAvatar: boolean; shouldShowPillAvatar: boolean;
room: Room | null; room: Room | null;
@ -56,7 +58,7 @@ const PillRoomAvatar: React.FC<{
if (room) { if (room) {
return <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />; return <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
} }
return <LinkIcon className="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image" />; return linkIcon;
}; };
const PillMemberAvatar: React.FC<{ const PillMemberAvatar: React.FC<{
@ -88,7 +90,7 @@ export interface PillProps {
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => { export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { member, onClick, resourceId, targetRoom, text, type } = usePermalink({ const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
room, room,
type: propType, type: propType,
url, url,
@ -116,35 +118,38 @@ 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 content: (ReactElement | string)[] = []; let avatar: ReactElement | null = null;
const textElement = <span className="mx_Pill_text">{text}</span>; let pillText: string | null = text;
switch (type) { switch (type) {
case PillType.EventInOtherRoom: case PillType.EventInOtherRoom:
{ {
const avatar = <PillRoomAvatar shouldShowPillAvatar={shouldShowPillAvatar} room={targetRoom} />; avatar = <PillRoomAvatar shouldShowPillAvatar={shouldShowPillAvatar} room={targetRoom} />;
content = [_t("Message in"), avatar || " ", textElement]; pillText = _t("Message in %(room)s", {
room: text,
});
} }
break; break;
case PillType.EventInSameRoom: case PillType.EventInSameRoom:
{ {
const avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />; if (event) {
content = [_t("Message from"), avatar || " ", textElement]; avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
pillText = _t("Message from %(user)s", {
user: text,
});
} else {
avatar = linkIcon;
pillText = _t("Message");
}
} }
break; break;
case PillType.AtRoomMention: case PillType.AtRoomMention:
case PillType.RoomMention: case PillType.RoomMention:
case "space": case "space":
{ avatar = <PillRoomAvatar shouldShowPillAvatar={shouldShowPillAvatar} room={targetRoom} />;
const avatar = <PillRoomAvatar shouldShowPillAvatar={shouldShowPillAvatar} room={targetRoom} />;
content = [avatar, textElement];
}
break; break;
case PillType.UserMention: case PillType.UserMention:
{ avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
const avatar = <PillMemberAvatar shouldShowPillAvatar={shouldShowPillAvatar} member={member} />;
content = [avatar, textElement];
}
break; break;
default: default:
return null; return null;
@ -161,12 +166,14 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
onMouseOver={onMouseOver} onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
{content} {avatar}
<span className="mx_Pill_text">{pillText}</span>
{tip} {tip}
</a> </a>
) : ( ) : (
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}> <span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
{content} {avatar}
<span className="mx_Pill_text">{pillText}</span>
{tip} {tip}
</span> </span>
)} )}

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 { Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
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";
@ -70,6 +70,11 @@ interface HookResult {
* null here means that the type cannot be detected. Most likely if the URL was not a permalink. * 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;
/**
* Target event of the permalink.
* Null if unable to load the event.
*/
event: MatrixEvent | null;
} }
/** /**
@ -166,6 +171,7 @@ export const usePermalink: (args: Args) => HookResult = ({
} }
return { return {
event,
member, member,
onClick, onClick,
resourceId, resourceId,

View file

@ -2592,8 +2592,8 @@
"Rotate Right": "Rotate Right", "Rotate Right": "Rotate Right",
"Information": "Information", "Information": "Information",
"Language Dropdown": "Language Dropdown", "Language Dropdown": "Language Dropdown",
"Message in": "Message in", "Message in %(room)s": "Message in %(room)s",
"Message from": "Message from", "Message from %(user)s": "Message from %(user)s",
"Create poll": "Create poll", "Create poll": "Create poll",
"Create Poll": "Create Poll", "Create Poll": "Create Poll",
"Edit poll": "Edit poll", "Edit poll": "Edit poll",

View file

@ -71,7 +71,6 @@ exports[`<Pill> should render the expected pill for a message in another room 1`
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/$123-456" href="https://matrix.to/#/!room1:example.com/$123-456"
> >
Message in
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar" class="mx_BaseAvatar"
@ -96,7 +95,7 @@ exports[`<Pill> should render the expected pill for a message in another room 1`
<span <span
class="mx_Pill_text" class="mx_Pill_text"
> >
Room 1 Message in Room 1
</span> </span>
</a> </a>
</bdi> </bdi>
@ -112,7 +111,6 @@ exports[`<Pill> should render the expected pill for a message in the same room 1
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/$123-456" href="https://matrix.to/#/!room1:example.com/$123-456"
> >
Message from
<span <span
aria-hidden="true" aria-hidden="true"
class="mx_BaseAvatar" class="mx_BaseAvatar"
@ -137,7 +135,7 @@ exports[`<Pill> should render the expected pill for a message in the same room 1
<span <span
class="mx_Pill_text" class="mx_Pill_text"
> >
User 1 Message from User 1
</span> </span>
</a> </a>
</bdi> </bdi>

View file

@ -16,8 +16,9 @@ limitations under the License.
import React from "react"; import React from "react";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MockedObject } from "jest-mock"; import { mocked, MockedObject } from "jest-mock";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import * as prettier from "prettier";
import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom } from "../../../test-utils"; import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
@ -28,10 +29,18 @@ import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper";
const mkRoomTextMessage = (body: string): MatrixEvent => { const room1Id = "!room1:example.com";
const room2Id = "!room2:example.com";
const room2Name = "Room 2";
interface MkRoomTextMessageOpts {
roomId?: string;
}
const mkRoomTextMessage = (body: string, mkRoomTextMessageOpts?: MkRoomTextMessageOpts): MatrixEvent => {
return mkMessage({ return mkMessage({
msg: body, msg: body,
room: "room_id", room: mkRoomTextMessageOpts?.roomId ?? room1Id,
user: "sender", user: "sender",
event: true, event: true,
}); });
@ -42,7 +51,7 @@ const mkFormattedMessage = (body: string, formattedBody: string): MatrixEvent =>
msg: body, msg: body,
formattedMsg: formattedBody, formattedMsg: formattedBody,
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
room: "room_id", room: room1Id,
user: "sender", user: "sender",
event: true, event: true,
}); });
@ -53,12 +62,29 @@ describe("<TextualBody />", () => {
jest.spyOn(MatrixClientPeg, "get").mockRestore(); jest.spyOn(MatrixClientPeg, "get").mockRestore();
}); });
const defaultRoom = mkStubRoom("room_id", "test room", undefined); const defaultRoom = mkStubRoom(room1Id, "test room", undefined);
const otherRoom = mkStubRoom(room2Id, room2Name, undefined);
let defaultMatrixClient: MockedObject<MatrixClient>; let defaultMatrixClient: MockedObject<MatrixClient>;
const defaultEvent = mkEvent({
type: "m.room.message",
room: room1Id,
user: "sender",
content: {
body: "winks",
msgtype: "m.emote",
},
event: true,
});
beforeEach(() => { beforeEach(() => {
defaultMatrixClient = getMockClientWithEventEmitter({ defaultMatrixClient = getMockClientWithEventEmitter({
getRoom: () => defaultRoom, getRoom: (roomId: string | undefined) => {
getRooms: () => [defaultRoom], if (roomId === room1Id) return defaultRoom;
if (roomId === room2Id) return otherRoom;
return null;
},
getRooms: () => [defaultRoom, otherRoom],
getAccountData: (): MatrixEvent | undefined => undefined, getAccountData: (): MatrixEvent | undefined => undefined,
isGuest: () => false, isGuest: () => false,
mxcUrlToHttp: (s: string) => s, mxcUrlToHttp: (s: string) => s,
@ -67,18 +93,13 @@ describe("<TextualBody />", () => {
throw new Error("MockClient event not found"); throw new Error("MockClient event not found");
}, },
}); });
mocked(defaultRoom).findEventById.mockImplementation((eventId: string) => {
if (eventId === defaultEvent.getId()) return defaultEvent;
return undefined;
});
}); });
const defaultEvent = mkEvent({
type: "m.room.message",
room: "room_id",
user: "sender",
content: {
body: "winks",
msgtype: "m.emote",
},
event: true,
});
const defaultProps = { const defaultProps = {
mxEvent: defaultEvent, mxEvent: defaultEvent,
highlights: [] as string[], highlights: [] as string[],
@ -88,6 +109,7 @@ describe("<TextualBody />", () => {
permalinkCreator: new RoomPermalinkCreator(defaultRoom), permalinkCreator: new RoomPermalinkCreator(defaultRoom),
mediaEventHelper: {} as MediaEventHelper, mediaEventHelper: {} as MediaEventHelper,
}; };
const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) => const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) =>
(renderingFn ?? render)( (renderingFn ?? render)(
<MatrixClientContext.Provider value={matrixClient}> <MatrixClientContext.Provider value={matrixClient}>
@ -100,7 +122,7 @@ describe("<TextualBody />", () => {
const ev = mkEvent({ const ev = mkEvent({
type: "m.room.message", type: "m.room.message",
room: "room_id", room: room1Id,
user: "sender", user: "sender",
content: { content: {
body: "winks", body: "winks",
@ -120,7 +142,7 @@ describe("<TextualBody />", () => {
const ev = mkEvent({ const ev = mkEvent({
type: "m.room.message", type: "m.room.message",
room: "room_id", room: room1Id,
user: "bot_sender", user: "bot_sender",
content: { content: {
body: "this is a notice, probably from a bot", body: "this is a notice, probably from a bot",
@ -196,13 +218,42 @@ describe("<TextualBody />", () => {
`"Visit <span><bdi><a class="mx_Pill mx_RoomPill" href="https://matrix.to/#/#room:example.com"><div class="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image"></div><span class="mx_Pill_text">#room:example.com</span></a></bdi></span>"`, `"Visit <span><bdi><a class="mx_Pill mx_RoomPill" href="https://matrix.to/#/#room:example.com"><div class="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image"></div><span class="mx_Pill_text">#room:example.com</span></a></bdi></span>"`,
); );
}); });
it("should pillify a permalink to a message in the same room with the label »Message from Member«", () => {
const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room1Id}/${defaultEvent.getId()}`);
const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body");
expect(
prettier.format(content.innerHTML.replace(defaultEvent.getId(), "%event_id%"), {
parser: "html",
}),
).toMatchSnapshot();
});
it("should pillify a permalink to an unknown message in the same room with the label »Message«", () => {
const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room1Id}/!abc123`);
const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body");
expect(content).toMatchSnapshot();
});
it("should pillify a permalink to an event in another room with the label »Message in Room 2«", () => {
const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room2Id}/${defaultEvent.getId()}`);
const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body");
expect(
prettier.format(content.innerHTML.replace(defaultEvent.getId(), "%event_id%"), {
parser: "html",
}),
).toMatchSnapshot();
});
}); });
describe("renders formatted m.text correctly", () => { describe("renders formatted m.text correctly", () => {
let matrixClient: MatrixClient; let matrixClient: MatrixClient;
beforeEach(() => { beforeEach(() => {
matrixClient = getMockClientWithEventEmitter({ matrixClient = getMockClientWithEventEmitter({
getRoom: () => mkStubRoom("room_id", "room name", undefined), getRoom: () => mkStubRoom(room1Id, "room name", undefined),
getAccountData: (): MatrixEvent | undefined => undefined, getAccountData: (): MatrixEvent | undefined => undefined,
getUserId: () => "@me:my_server", getUserId: () => "@me:my_server",
getHomeserverUrl: () => "https://my_server/", getHomeserverUrl: () => "https://my_server/",

View file

@ -88,7 +88,6 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for eve
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com" href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com"
> >
Message in
<img <img
alt="" alt=""
aria-hidden="true" aria-hidden="true"
@ -100,7 +99,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for eve
<span <span
class="mx_Pill_text" class="mx_Pill_text"
> >
room name Message in room name
</span> </span>
</a> </a>
</bdi> </bdi>
@ -240,3 +239,71 @@ exports[`<TextualBody /> renders formatted m.text correctly pills get injected c
</span> </span>
</span> </span>
`; `;
exports[`<TextualBody /> renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `
"Visit
<span
><bdi
><a
class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/%event_id%"
><img
class="mx_BaseAvatar mx_BaseAvatar_image"
src="mxc://avatar.url/image.png"
style="width: 16px; height: 16px"
alt=""
data-testid="avatar-img"
aria-hidden="true"
/><span class="mx_Pill_text">Message from Member</span></a
></bdi
></span
>
"
`;
exports[`<TextualBody /> renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `
"Visit
<span
><bdi
><a
class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room2:example.com/%event_id%"
><img
class="mx_BaseAvatar mx_BaseAvatar_image"
src="mxc://avatar.url/room.png"
style="width: 16px; height: 16px"
alt=""
data-testid="avatar-img"
aria-hidden="true"
/><span class="mx_Pill_text">Message in Room 2</span></a
></bdi
></span
>
"
`;
exports[`<TextualBody /> renders plain-text m.text correctly should pillify a permalink to an unknown message in the same room with the label »Message« 1`] = `
<span
class="mx_EventTile_body"
dir="auto"
>
Visit
<span>
<bdi>
<a
class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/!abc123"
>
<div
class="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image"
/>
<span
class="mx_Pill_text"
>
Message
</span>
</a>
</bdi>
</span>
</span>
`;

View file

@ -537,7 +537,7 @@ export function mkStubRoom(
} as unknown as RoomState, } as unknown as RoomState,
eventShouldLiveIn: jest.fn().mockReturnValue({}), eventShouldLiveIn: jest.fn().mockReturnValue({}),
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()), fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
findEventById: (_: string) => undefined as MatrixEvent | undefined, findEventById: jest.fn().mockReturnValue(undefined),
findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }), findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }),
getAccountData: (_: EventType | string) => undefined as MatrixEvent | undefined, getAccountData: (_: EventType | string) => undefined as MatrixEvent | undefined,
getAltAliases: jest.fn().mockReturnValue([]), getAltAliases: jest.fn().mockReturnValue([]),