Fix room list often showing the wrong icons for calls (#32881)
* Give rooms with calls a proper accessible description Besides improving accessibility, this makes it possible to check for the presence of a call indicator in the room list in Playwright tests. * Make room list react to calls in a room, even when not connected to them To use the results of CallStore.getRoom reactively, you need to listen for Call events, not ConnectedCalls events. * Don't assume that every call starts off as a video call If a Call object is created by way of someone starting a voice call, then of course the call's initial type needs to be 'voice'. * Make room list items react to changes in call type The type of a call may change over time; therefore room list items explicitly need to react to the changes. * Update a call's type before notifying listeners of the change If we notify listeners of a change in a call's type before actually making that change, the listeners will be working with glitched state. This would cause the room list to show the wrong call type in certain situations. * Ignore the Vitest attachments directory
This commit is contained in:
@@ -27,6 +27,7 @@ storybook-static
|
|||||||
/packages/shared-components/node_modules
|
/packages/shared-components/node_modules
|
||||||
/packages/shared-components/dist
|
/packages/shared-components/dist
|
||||||
/packages/shared-components/src/i18nKeys.d.ts
|
/packages/shared-components/src/i18nKeys.d.ts
|
||||||
|
/packages/shared-components/.vitest-attachments
|
||||||
|
|
||||||
# TSC incremental compilation information
|
# TSC incremental compilation information
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
@@ -189,25 +189,31 @@ test.describe("Element Call", () => {
|
|||||||
expect(hash.get("skipLobby")).toEqual("true");
|
expect(hash.get("skipLobby")).toEqual("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
|
["voice", "video"].forEach((callType) => {
|
||||||
await app.viewRoomById(room.roomId);
|
test(`should be able to join a ${callType} call in progress`, async ({ page, user, bot, room, app }) => {
|
||||||
// Allow bob to create a call
|
await app.viewRoomById(room.roomId);
|
||||||
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
// Allow bob to create a call
|
||||||
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
|
||||||
// Fake a start of a call
|
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
|
||||||
await sendRTCState(bot, room.roomId);
|
// Fake a start of a call
|
||||||
const button = page.getByTestId("join-call-button");
|
await sendRTCState(bot, room.roomId, undefined, callType === "voice" ? "audio" : "video");
|
||||||
await expect(button).toBeInViewport({ timeout: 5000 });
|
const button = page.getByTestId("join-call-button");
|
||||||
// And test joining
|
await expect(button).toBeInViewport({ timeout: 5000 });
|
||||||
await button.click();
|
// Room list should show that a call is ongoing
|
||||||
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
await expect(
|
||||||
await expect(frameUrlStr).toBeDefined();
|
page.getByRole("option", { name: `Open room TestRoom with a ${callType} call.` }),
|
||||||
const url = new URL(frameUrlStr);
|
).toBeVisible();
|
||||||
const hash = new URLSearchParams(url.hash.slice(1));
|
// And test joining
|
||||||
assertCommonCallParameters(url.searchParams, hash, user, room);
|
await button.click();
|
||||||
|
const frameUrlStr = await page.locator("iframe").getAttribute("src");
|
||||||
|
await expect(frameUrlStr).toBeDefined();
|
||||||
|
const url = new URL(frameUrlStr);
|
||||||
|
const hash = new URLSearchParams(url.hash.slice(1));
|
||||||
|
assertCommonCallParameters(url.searchParams, hash, user, room);
|
||||||
|
|
||||||
expect(hash.get("intent")).toEqual("join_existing");
|
expect(hash.get("intent")).toEqual("join_existing");
|
||||||
expect(hash.get("skipLobby")).toEqual(null);
|
expect(hash.get("skipLobby")).toEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
[true, false].forEach((skipLobbyToggle) => {
|
[true, false].forEach((skipLobbyToggle) => {
|
||||||
|
|||||||
@@ -108,16 +108,15 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
protected readonly widgetUid: string;
|
protected readonly widgetUid: string;
|
||||||
protected readonly room: Room;
|
protected readonly room: Room;
|
||||||
|
|
||||||
private _callType: CallType = CallType.Video;
|
private _callType: CallType;
|
||||||
public get callType(): CallType {
|
public get callType(): CallType {
|
||||||
return this._callType;
|
return this._callType;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected set callType(callType: CallType) {
|
protected set callType(callType: CallType) {
|
||||||
if (this._callType !== callType) {
|
const prevCallType = this._callType;
|
||||||
this.emit(CallEvent.CallTypeChanged, callType);
|
|
||||||
}
|
|
||||||
this._callType = callType;
|
this._callType = callType;
|
||||||
|
if (callType !== prevCallType) this.emit(CallEvent.CallTypeChanged, callType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,11 +183,13 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
*/
|
*/
|
||||||
public readonly widget: IApp,
|
public readonly widget: IApp,
|
||||||
protected readonly client: MatrixClient,
|
protected readonly client: MatrixClient,
|
||||||
|
initialCallType: CallType,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.widgetUid = WidgetUtils.getWidgetUid(this.widget);
|
this.widgetUid = WidgetUtils.getWidgetUid(this.widget);
|
||||||
this.room = this.client.getRoom(this.roomId)!;
|
this.room = this.client.getRoom(this.roomId)!;
|
||||||
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
||||||
|
this._callType = initialCallType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,7 +348,7 @@ export class JitsiCall extends Call {
|
|||||||
private participantsExpirationTimer: number | null = null;
|
private participantsExpirationTimer: number | null = null;
|
||||||
|
|
||||||
private constructor(widget: IApp, client: MatrixClient) {
|
private constructor(widget: IApp, client: MatrixClient) {
|
||||||
super(widget, client);
|
super(widget, client, CallType.Video);
|
||||||
|
|
||||||
this.room.on(RoomStateEvent.Update, this.onRoomState);
|
this.room.on(RoomStateEvent.Update, this.onRoomState);
|
||||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||||
@@ -899,7 +900,7 @@ export class ElementCall extends Call {
|
|||||||
widget: IApp,
|
widget: IApp,
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
) {
|
) {
|
||||||
super(widget, client);
|
super(widget, client, session.getConsensusCallIntent() === "audio" ? CallType.Voice : CallType.Video);
|
||||||
|
|
||||||
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
||||||
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
|
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export class RoomListItemViewModel
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to call state changes
|
// Subscribe to call state changes
|
||||||
this.disposables.trackListener(CallStore.instance, CallStoreEvent.ConnectedCalls, this.onCallStateChanged);
|
this.disposables.trackListener(CallStore.instance, CallStoreEvent.Call, this.onCallStateChanged);
|
||||||
// If there is an active call for this room, listen to participant changes
|
// If there is an active call for this room, listen to participant changes
|
||||||
this.listenToCallParticipants();
|
this.listenToCallParticipants();
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ export class RoomListItemViewModel
|
|||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged);
|
this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged);
|
||||||
|
this.currentCall?.off(CallEvent.CallTypeChanged, this.onCallTypeChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNotificationChanged = (): void => {
|
private onNotificationChanged = (): void => {
|
||||||
@@ -128,16 +129,25 @@ export class RoomListItemViewModel
|
|||||||
this.updateItem();
|
this.updateItem();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for call type changes. Only updates the item if the call type is actually present in the snapshot.
|
||||||
|
*/
|
||||||
|
private onCallTypeChanged = (): void => {
|
||||||
|
if (this.snapshot.current.notification.callType !== undefined) this.updateItem();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen to participant changes for the current call in this room (if any) to trigger updates when participants join/leave the call.
|
* Listen to participant changes for the current call in this room (if any) to trigger updates when participants join/leave the call.
|
||||||
*/
|
*/
|
||||||
private listenToCallParticipants(): void {
|
private listenToCallParticipants(): void {
|
||||||
const call = CallStore.instance.getCall(this.props.room.roomId);
|
const call = CallStore.instance.getCall(this.props.room.roomId);
|
||||||
|
|
||||||
// Remove listener from previous call (if any) and add to new call to track participant changes
|
// Remove listeners from previous call (if any) and add to new call to track changes
|
||||||
if (call !== this.currentCall) {
|
if (call !== this.currentCall) {
|
||||||
this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged);
|
this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged);
|
||||||
|
this.currentCall?.off(CallEvent.CallTypeChanged, this.onCallTypeChanged);
|
||||||
call?.on(CallEvent.Participants, this.onCallParticipantsChanged);
|
call?.on(CallEvent.Participants, this.onCallParticipantsChanged);
|
||||||
|
call?.on(CallEvent.CallTypeChanged, this.onCallTypeChanged);
|
||||||
}
|
}
|
||||||
this.currentCall = call;
|
this.currentCall = call;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
type IContent,
|
type IContent,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { mocked, type Mocked } from "jest-mock";
|
import { mocked, type Mocked } from "jest-mock";
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ export class MockedCall extends Call {
|
|||||||
waitForIframeLoad: false,
|
waitForIframeLoad: false,
|
||||||
},
|
},
|
||||||
room.client,
|
room.client,
|
||||||
|
CallType.Video,
|
||||||
);
|
);
|
||||||
this.groupCall = { creationTs: this.event.getTs() } as unknown as GroupCall;
|
this.groupCall = { creationTs: this.event.getTs() } as unknown as GroupCall;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ export function createStubMatrixRTC(): MatrixRTCSessionManager {
|
|||||||
const session = new EventEmitter() as MatrixRTCSession;
|
const session = new EventEmitter() as MatrixRTCSession;
|
||||||
session.memberships = [];
|
session.memberships = [];
|
||||||
session.getOldestMembership = () => undefined;
|
session.getOldestMembership = () => undefined;
|
||||||
|
session.getConsensusCallIntent = () => "video";
|
||||||
return session;
|
return session;
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
MatrixRTCSession,
|
MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/src/matrixrtc";
|
} from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
import type { Mocked } from "jest-mock";
|
import type { Mocked } from "jest-mock";
|
||||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||||
@@ -987,6 +988,35 @@ describe("ElementCall", () => {
|
|||||||
call.off(CallEvent.Participants, onParticipants);
|
call.off(CallEvent.Participants, onParticipants);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits events when call type changes", async () => {
|
||||||
|
const onCallTypeChanged = jest.fn();
|
||||||
|
call.on(CallEvent.CallTypeChanged, onCallTypeChanged);
|
||||||
|
// Should default to video when unknown
|
||||||
|
expect(call.callType).toBe(CallType.Video);
|
||||||
|
|
||||||
|
// Change call type to voice
|
||||||
|
roomSession.memberships = [
|
||||||
|
{ sender: alice.userId, deviceId: "alices_device", callIntent: "audio" } as Mocked<CallMembership>,
|
||||||
|
];
|
||||||
|
roomSession.getConsensusCallIntent.mockReturnValue("audio");
|
||||||
|
roomSession.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []);
|
||||||
|
|
||||||
|
expect(call.callType).toBe(CallType.Voice);
|
||||||
|
expect(onCallTypeChanged.mock.calls).toEqual([[CallType.Voice]]);
|
||||||
|
|
||||||
|
// Change call type back to video
|
||||||
|
roomSession.memberships = [
|
||||||
|
{ sender: alice.userId, deviceId: "alices_device", callIntent: "video" } as Mocked<CallMembership>,
|
||||||
|
];
|
||||||
|
roomSession.getConsensusCallIntent.mockReturnValue("video");
|
||||||
|
roomSession.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []);
|
||||||
|
|
||||||
|
expect(call.callType).toBe(CallType.Video);
|
||||||
|
expect(onCallTypeChanged.mock.calls).toEqual([[CallType.Voice], [CallType.Video]]);
|
||||||
|
|
||||||
|
call.off(CallEvent.CallTypeChanged, onCallTypeChanged);
|
||||||
|
});
|
||||||
|
|
||||||
it("ends the call immediately if the session ended", async () => {
|
it("ends the call immediately if the session ended", async () => {
|
||||||
await connect(call, widgetApi);
|
await connect(call, widgetApi);
|
||||||
const onDestroy = jest.fn();
|
const onDestroy = jest.fn();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import EventEmitter from "events";
|
||||||
import {
|
import {
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
type MatrixEvent,
|
type MatrixEvent,
|
||||||
@@ -26,7 +27,7 @@ import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag";
|
|||||||
import dispatcher from "../../../src/dispatcher/dispatcher";
|
import dispatcher from "../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
import { Action } from "../../../src/dispatcher/actions";
|
||||||
import { CallStore } from "../../../src/stores/CallStore";
|
import { CallStore } from "../../../src/stores/CallStore";
|
||||||
import type { Call } from "../../../src/models/Call";
|
import { CallEvent, type Call } from "../../../src/models/Call";
|
||||||
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
|
import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel";
|
||||||
|
|
||||||
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
jest.mock("../../../src/viewmodels/room-list/utils", () => ({
|
||||||
@@ -436,6 +437,28 @@ describe("RoomListItemViewModel", () => {
|
|||||||
// The new call must have a listener registered
|
// The new call must have a listener registered
|
||||||
expect(secondCall.on).toHaveBeenCalledWith("participants", expect.any(Function));
|
expect(secondCall.on).toHaveBeenCalledWith("participants", expect.any(Function));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should listen to call type changes", async () => {
|
||||||
|
// Start with a voice call
|
||||||
|
let callType = CallType.Voice;
|
||||||
|
const mockCall = new (class extends EventEmitter {
|
||||||
|
get callType() {
|
||||||
|
return callType;
|
||||||
|
}
|
||||||
|
participants = new Map([[matrixClient.getUserId()!, {}]]);
|
||||||
|
})() as unknown as Call;
|
||||||
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall);
|
||||||
|
|
||||||
|
viewModel = new RoomListItemViewModel({ room, client: matrixClient });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(viewModel.getSnapshot().notification.callType).toBe("voice");
|
||||||
|
|
||||||
|
// Now turn it into a video call
|
||||||
|
callType = CallType.Video;
|
||||||
|
mockCall.emit(CallEvent.CallTypeChanged, callType);
|
||||||
|
expect(viewModel.getSnapshot().notification.callType).toBe("video");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Room name updates", () => {
|
describe("Room name updates", () => {
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -79,7 +79,9 @@
|
|||||||
"one": "Open room %(roomName)s with 1 unread message.",
|
"one": "Open room %(roomName)s with 1 unread message.",
|
||||||
"other": "Open room %(roomName)s with %(count)s unread messages."
|
"other": "Open room %(roomName)s with %(count)s unread messages."
|
||||||
},
|
},
|
||||||
"unsent_message": "Open room %(roomName)s with an unsent message."
|
"unsent_message": "Open room %(roomName)s with an unsent message.",
|
||||||
|
"video_call": "Open room %(roomName)s with a video call.",
|
||||||
|
"voice_call": "Open room %(roomName)s with a voice call."
|
||||||
},
|
},
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"collapse_filters": "Collapse filter list",
|
"collapse_filters": "Collapse filter list",
|
||||||
|
|||||||
+36
@@ -160,6 +160,42 @@ export const WithMention: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithVoiceCall: Story = {
|
||||||
|
args: {
|
||||||
|
isBold: true,
|
||||||
|
notification: {
|
||||||
|
hasAnyNotificationOrActivity: true,
|
||||||
|
isUnsentMessage: false,
|
||||||
|
invited: false,
|
||||||
|
isMention: false,
|
||||||
|
isActivityNotification: false,
|
||||||
|
isNotification: false,
|
||||||
|
hasUnreadCount: false,
|
||||||
|
count: 0,
|
||||||
|
muted: false,
|
||||||
|
callType: "voice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithVideoCall: Story = {
|
||||||
|
args: {
|
||||||
|
isBold: true,
|
||||||
|
notification: {
|
||||||
|
hasAnyNotificationOrActivity: true,
|
||||||
|
isUnsentMessage: false,
|
||||||
|
invited: false,
|
||||||
|
isMention: false,
|
||||||
|
isActivityNotification: false,
|
||||||
|
isNotification: false,
|
||||||
|
hasUnreadCount: false,
|
||||||
|
count: 0,
|
||||||
|
muted: false,
|
||||||
|
callType: "video",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Invitation: Story = {
|
export const Invitation: Story = {
|
||||||
args: {
|
args: {
|
||||||
name: "Secret Project",
|
name: "Secret Project",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const {
|
|||||||
Bold,
|
Bold,
|
||||||
WithNotification,
|
WithNotification,
|
||||||
WithMention,
|
WithMention,
|
||||||
|
WithVoiceCall,
|
||||||
|
WithVideoCall,
|
||||||
Invitation,
|
Invitation,
|
||||||
UnsentMessage,
|
UnsentMessage,
|
||||||
NoMessagePreview,
|
NoMessagePreview,
|
||||||
@@ -52,6 +54,16 @@ describe("<RoomListItemView />", () => {
|
|||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders WithVoiceCall story", () => {
|
||||||
|
const { container } = render(<WithVoiceCall />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders WithVideoCall story", () => {
|
||||||
|
const { container } = render(<WithVideoCall />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders Invitation story", () => {
|
it("renders Invitation story", () => {
|
||||||
const { container } = render(<Invitation />);
|
const { container } = render(<Invitation />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ function getA11yLabel(roomName: string, notification: NotificationDecorationData
|
|||||||
return _t("room_list|a11y|mention", { roomName, count: notification.count });
|
return _t("room_list|a11y|mention", { roomName, count: notification.count });
|
||||||
} else if (notification.hasUnreadCount && notification.count) {
|
} else if (notification.hasUnreadCount && notification.count) {
|
||||||
return _t("room_list|a11y|unread", { roomName, count: notification.count });
|
return _t("room_list|a11y|unread", { roomName, count: notification.count });
|
||||||
|
} else if (notification.callType === "voice") {
|
||||||
|
return _t("room_list|a11y|voice_call", { roomName });
|
||||||
|
} else if (notification.callType === "video") {
|
||||||
|
return _t("room_list|a11y|video_call", { roomName });
|
||||||
} else {
|
} else {
|
||||||
return _t("room_list|a11y|default", { roomName });
|
return _t("room_list|a11y|default", { roomName });
|
||||||
}
|
}
|
||||||
|
|||||||
+312
-16
@@ -319,11 +319,11 @@ exports[`<RoomListItemView /> > renders Invitation story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="More Options"
|
aria-label="More Options"
|
||||||
aria-labelledby="_r_2i_"
|
aria-labelledby="_r_3i_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_2g_"
|
id="radix-_r_3g_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -351,11 +351,11 @@ exports[`<RoomListItemView /> > renders Invitation story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="Notification options"
|
aria-label="Notification options"
|
||||||
aria-labelledby="_r_2p_"
|
aria-labelledby="_r_3p_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_2n_"
|
id="radix-_r_3n_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -461,11 +461,11 @@ exports[`<RoomListItemView /> > renders NoMessagePreview story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="More Options"
|
aria-label="More Options"
|
||||||
aria-labelledby="_r_3i_"
|
aria-labelledby="_r_4i_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_3g_"
|
id="radix-_r_4g_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -493,11 +493,11 @@ exports[`<RoomListItemView /> > renders NoMessagePreview story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="Notification options"
|
aria-label="Notification options"
|
||||||
aria-labelledby="_r_3p_"
|
aria-labelledby="_r_4p_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_3n_"
|
id="radix-_r_4n_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -721,11 +721,11 @@ exports[`<RoomListItemView /> > renders UnsentMessage story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="More Options"
|
aria-label="More Options"
|
||||||
aria-labelledby="_r_32_"
|
aria-labelledby="_r_42_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_30_"
|
id="radix-_r_40_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -753,11 +753,11 @@ exports[`<RoomListItemView /> > renders UnsentMessage story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="Notification options"
|
aria-label="Notification options"
|
||||||
aria-labelledby="_r_39_"
|
aria-labelledby="_r_49_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_37_"
|
id="radix-_r_47_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -869,11 +869,11 @@ exports[`<RoomListItemView /> > renders WithHoverMenu story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="More Options"
|
aria-label="More Options"
|
||||||
aria-labelledby="_r_42_"
|
aria-labelledby="_r_52_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_40_"
|
id="radix-_r_50_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -901,11 +901,11 @@ exports[`<RoomListItemView /> > renders WithHoverMenu story 1`] = `
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="Notification options"
|
aria-label="Notification options"
|
||||||
aria-labelledby="_r_49_"
|
aria-labelledby="_r_59_"
|
||||||
class="_icon-button_1215g_8"
|
class="_icon-button_1215g_8"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
id="radix-_r_47_"
|
id="radix-_r_57_"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@@ -1234,3 +1234,299 @@ exports[`<RoomListItemView /> > renders WithNotification story 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomListItemView /> > renders WithVideoCall story 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Room list"
|
||||||
|
role="listbox"
|
||||||
|
style="width: 320px; padding: 8px;"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="Open room General with a video call."
|
||||||
|
aria-selected="false"
|
||||||
|
class="flex roomListItem mx_RoomListItemView bold"
|
||||||
|
data-state="closed"
|
||||||
|
role="option"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex container"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="General avatar"
|
||||||
|
role="img"
|
||||||
|
style="width: 32px; height: 32px; border-radius: 50%; background-color: rgb(11, 127, 103); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 12px;"
|
||||||
|
>
|
||||||
|
GE
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex content"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ellipsis"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="roomName"
|
||||||
|
data-testid="room-name"
|
||||||
|
title="General"
|
||||||
|
>
|
||||||
|
General
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 ellipsis"
|
||||||
|
title="Alice: Hey everyone!"
|
||||||
|
>
|
||||||
|
Alice: Hey everyone!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex hoverMenu"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="More Options"
|
||||||
|
aria-labelledby="_r_32_"
|
||||||
|
class="_icon-button_1215g_8"
|
||||||
|
data-kind="primary"
|
||||||
|
data-state="closed"
|
||||||
|
id="radix-_r_30_"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="Notification options"
|
||||||
|
aria-labelledby="_r_39_"
|
||||||
|
class="_icon-button_1215g_8"
|
||||||
|
data-kind="primary"
|
||||||
|
data-state="closed"
|
||||||
|
id="radix-_r_37_"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20.293 17.293c.63.63.184 1.707-.707 1.707H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-7 7-7 7 7 7 7v6zM12 22a2 2 0 0 1-2-2h4a2 2 0 0 1-2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="notificationDecoration"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex"
|
||||||
|
data-testid="notification-decoration"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="var(--cpd-color-icon-accent-primary)"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<RoomListItemView /> > renders WithVoiceCall story 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Room list"
|
||||||
|
role="listbox"
|
||||||
|
style="width: 320px; padding: 8px;"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="Open room General with a voice call."
|
||||||
|
aria-selected="false"
|
||||||
|
class="flex roomListItem mx_RoomListItemView bold"
|
||||||
|
data-state="closed"
|
||||||
|
role="option"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex container"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="General avatar"
|
||||||
|
role="img"
|
||||||
|
style="width: 32px; height: 32px; border-radius: 50%; background-color: rgb(11, 127, 103); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 12px;"
|
||||||
|
>
|
||||||
|
GE
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex content"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: space-between; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ellipsis"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="roomName"
|
||||||
|
data-testid="room-name"
|
||||||
|
title="General"
|
||||||
|
>
|
||||||
|
General
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 ellipsis"
|
||||||
|
title="Alice: Hey everyone!"
|
||||||
|
>
|
||||||
|
Alice: Hey everyone!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex hoverMenu"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="More Options"
|
||||||
|
aria-labelledby="_r_2i_"
|
||||||
|
class="_icon-button_1215g_8"
|
||||||
|
data-kind="primary"
|
||||||
|
data-state="closed"
|
||||||
|
id="radix-_r_2g_"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-label="Notification options"
|
||||||
|
aria-labelledby="_r_2p_"
|
||||||
|
class="_icon-button_1215g_8"
|
||||||
|
data-kind="primary"
|
||||||
|
data-state="closed"
|
||||||
|
id="radix-_r_2n_"
|
||||||
|
role="button"
|
||||||
|
style="--cpd-icon-button-size: 24px; padding: 2px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_indicator-icon_147l5_17"
|
||||||
|
style="--cpd-icon-button-size: 100%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20.293 17.293c.63.63.184 1.707-.707 1.707H4.414c-.89 0-1.337-1.077-.707-1.707L5 16v-6s0-7 7-7 7 7 7 7v6zM12 22a2 2 0 0 1-2-2h4a2 2 0 0 1-2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="notificationDecoration"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex"
|
||||||
|
data-testid="notification-decoration"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="var(--cpd-color-icon-accent-primary)"
|
||||||
|
height="20px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="20px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user