Refactor RoomStatusBar into MVVM (#31523)

* Refactor RoomStatusBar into MVVM

* cleanup

* updated snaps

* More cleanup

* fix loop

* fixup

* drop comment

* lint

* cleanup console statements

* Starting to move to a MVVM v2 component.

* extra

* Refactor as a shared-componend / MVVM v2

* some cleanup

* i18n for banner

* remove removed css

* Update playwright tests to have a two stage on the consent bar.

* Update snaps

* Update snapshots

* cleanup

* update snaps

* refactor to use enum

* fix slight differences in pw snaps

* Add unit tests

* fix snaps

* snaps updated

* more test cleanups

* fix snaps

* fixed now?

* Disable animationsq

* lint lint lint

* remove console

* lint

* fix snap

* Refactor based on review comments.

* update view model test

* oops!

* fix snap

* Update snaps

* snap snap snap

* switch to a const map of strings

* Use this.disposables

* Update translations to be inside shared-components

* fix the tac

* Also retry

* Cleanup

* update snaps

* update other snaps

* snap updates
This commit is contained in:
Will Hunt
2026-01-12 21:13:15 +00:00
committed by GitHub
parent d9be851965
commit 2e6cf8734b
39 changed files with 1662 additions and 1022 deletions

View File

@@ -20,7 +20,7 @@ const config: TestRunnerConfig = {
// If you want to take screenshot of multiple browsers, use
// page.context().browser().browserType().name() to get the browser name to prefix the file name
const image = await page.screenshot();
const image = await page.screenshot({ animations: "disabled" });
expect(image).toMatchImageSnapshot({
customSnapshotsDir,
customSnapshotIdentifier: `${context.id}-${process.platform}`,

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -12,6 +12,7 @@ import React, {
type ReactNode,
type PropsWithChildren,
useMemo,
type HTMLAttributes,
} from "react";
import { Button } from "@vector-im/compound-web";
import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle";
@@ -32,8 +33,6 @@ interface BannerProps {
*/
avatar?: React.ReactNode;
className?: string;
/**
* Actions presented to the user in the right-hand side of the banner alongside the dismiss button.
*/
@@ -60,21 +59,21 @@ export function Banner({
actions,
onClose,
...props
}: PropsWithChildren<BannerProps>): ReactElement {
}: PropsWithChildren<BannerProps & HTMLAttributes<HTMLDivElement>>): ReactElement {
const classes = classNames(styles.banner, className);
const icon = useMemo(() => {
const icon = useMemo((): ReactElement => {
switch (type) {
case "critical":
return <ErrorIcon fontSize={24} {...props} />;
return <ErrorIcon fontSize={24} />;
case "info":
return <InfoIcon fontSize={24} {...props} />;
return <InfoIcon fontSize={24} />;
case "success":
return <CheckCircleIcon fontSize={24} {...props} />;
return <CheckCircleIcon fontSize={24} />;
default:
return <InfoIcon fontSize={24} {...props} />;
return <InfoIcon fontSize={24} />;
}
}, [type, props]);
}, [type]);
return (
<div {...props} className={classes} data-type={type}>

View File

@@ -8,6 +8,7 @@
"explore_rooms": "Explore rooms",
"pause": "Pause",
"play": "Play",
"retry": "Retry",
"search": "Search"
},
"left_panel": {
@@ -15,9 +16,24 @@
},
"room": {
"status_bar": {
"history_visible": "This room has been configured so that new members can read history. <a>Learn More</a>"
"delete_all": "Delete all",
"exceeded_resource_limit_description": "Please contact your service administrator to continue using the service.",
"exceeded_resource_limit_title": "Your message wasn't sent because this homeserver has exceeded a resource limit.",
"failed_to_create_room_title": "Could not start a chat with this user",
"history_visible": "This room has been configured so that new members can read history. <a>Learn More</a>",
"homeserver_blocked_title": "Your message wasn't sent because this homeserver has been blocked by its administrator.",
"monthly_user_limit_reached_title": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit.",
"requires_consent_agreement_title": "You can't send any messages until you review and agree to our terms and conditions.",
"retry_all": "Retry all",
"select_messages_to_retry": "You can select all or individual messages to retry or delete",
"server_connectivity_lost_description": "Sent messages will be stored until your connection has returned.",
"server_connectivity_lost_title": "Connectivity to the server has been lost.",
"some_messages_not_sent": "Some of your messages have not been sent"
}
},
"terms": {
"tac_button": "Review terms and conditions"
},
"time": {
"about_day_ago": "about a day ago",
"about_hour_ago": "about an hour ago",

View File

@@ -17,6 +17,7 @@ export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./pill-input/Pill";
export * from "./pill-input/PillInput";
export * from "./room/RoomStatusBar";
export * from "./rich-list/RichItem";
export * from "./rich-list/RichList";
export * from "./room-list/RoomListSearchView";

View File

@@ -0,0 +1,11 @@
.container {
color: var(--cpd-color-text-primary);
svg {
/* Ensure button icons are primary too */
color: var(--cpd-color-text-primary) !important;
}
}
.description {
color: var(--cpd-color-text-secondary);
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Meta, type StoryFn } from "@storybook/react-vite";
import React, { type JSX } from "react";
import { fn } from "storybook/test";
import { useMockedViewModel } from "../../useMockedViewModel";
import {
RoomStatusBarState,
RoomStatusBarView,
type RoomStatusBarViewActions,
type RoomStatusBarViewSnapshot,
} from "./RoomStatusBarView";
type RoomStatusBarProps = RoomStatusBarViewSnapshot & RoomStatusBarViewActions;
const RoomStatusBarViewWrapper = ({
onResendAllClick,
onDeleteAllClick,
onRetryRoomCreationClick,
onTermsAndConditionsClicked,
...rest
}: RoomStatusBarProps): JSX.Element => {
const vm = useMockedViewModel(rest, {
onResendAllClick,
onDeleteAllClick,
onRetryRoomCreationClick,
onTermsAndConditionsClicked,
});
return <RoomStatusBarView vm={vm} />;
};
export default {
title: "room/RoomStatusBarView",
component: RoomStatusBarViewWrapper,
tags: ["autodocs"],
argTypes: {},
args: {
onResendAllClick: fn(),
onDeleteAllClick: fn(),
onRetryRoomCreationClick: fn(),
onTermsAndConditionsClicked: fn(),
},
} as Meta<typeof RoomStatusBarViewWrapper>;
const Template: StoryFn<typeof RoomStatusBarViewWrapper> = (args) => <RoomStatusBarViewWrapper {...args} />;
/**
* Rendered when the client has lost connection with the server.
*/
export const WithConnectionLost = Template.bind({});
WithConnectionLost.args = {
state: RoomStatusBarState.ConnectionLost,
};
/**
* Rendered when the client needs the user to consent to some terms and conditions before
* they can perform any room actions.
*/
export const WithConsentLink = Template.bind({});
WithConsentLink.args = {
state: RoomStatusBarState.NeedsConsent,
consentUri: "#example",
};
/**
* Rendered when the server has hit a usage limit and is forbidding the user from performing
* any actions in the room. There is an optional parameter to link to an admin to contact.
*/
export const WithResourceLimit = Template.bind({});
WithResourceLimit.args = {
state: RoomStatusBarState.ResourceLimited,
resourceLimit: "hs_disabled",
adminContactHref: "#example",
};
/**
* Rendered when the client has some unsent messages in the room, stored locally.
*/
export const WithUnsentMessages = Template.bind({});
WithUnsentMessages.args = {
state: RoomStatusBarState.UnsentMessages,
isResending: false,
};
/**
* Rendered when the client has some unsent messages in the room, stored locally and is
* trying to send them.
*/
export const WithUnsentMessagesSending = Template.bind({});
WithUnsentMessagesSending.args = {
state: RoomStatusBarState.UnsentMessages,
isResending: true,
};
/**
* Rendered when a local room has failed to be created.
*/
export const WithLocalRoomRetry = Template.bind({});
WithLocalRoomRetry.args = {
state: RoomStatusBarState.LocalRoomFailed,
};

View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { composeStories } from "@storybook/react-vite";
import userEvent from "@testing-library/user-event";
import * as stories from "./RoomStatusBarView.stories.tsx";
const { WithConnectionLost, WithConsentLink, WithResourceLimit, WithUnsentMessages, WithLocalRoomRetry } =
composeStories(stories);
describe("RoomStatusBarView", () => {
it("renders connection lost", () => {
const { container } = render(<WithConnectionLost />);
expect(container).toMatchSnapshot();
});
it("renders resource limit error", () => {
const { container } = render(<WithResourceLimit />);
expect(container).toMatchSnapshot();
});
it("renders consent link", () => {
const { container, getByRole } = render(<WithConsentLink />);
expect(container).toMatchSnapshot();
const button = getByRole("link");
expect(button.getAttribute("href")).toEqual("#example");
});
it("renders unsent messages", async () => {
const { container } = render(
<WithUnsentMessages onDeleteAllClick={jest.fn()} onRetryRoomCreationClick={jest.fn()} />,
);
expect(container).toMatchSnapshot();
});
it("renders unsent messages and deletes all", async () => {
const onDeleteAllClick = jest.fn();
const { container, getByRole } = render(<WithUnsentMessages onDeleteAllClick={onDeleteAllClick} />);
expect(container).toMatchSnapshot();
const button = getByRole("button", { name: "Delete all" });
await userEvent.click(button);
expect(onDeleteAllClick).toHaveBeenCalled();
});
it("renders unsent messages and resends all", async () => {
const onResendAllClick = jest.fn();
const { container, getByRole } = render(<WithUnsentMessages onResendAllClick={onResendAllClick} />);
expect(container).toMatchSnapshot();
const button = getByRole("button", { name: "Retry all" });
await userEvent.click(button);
expect(onResendAllClick).toHaveBeenCalled();
});
it("renders local room error", async () => {
const onRetryRoomCreationClick = jest.fn();
const { container, getByRole } = render(
<WithLocalRoomRetry onRetryRoomCreationClick={onRetryRoomCreationClick} />,
);
expect(container).toMatchSnapshot();
const button = getByRole("button", { name: "Retry" });
await userEvent.click(button);
expect(onRetryRoomCreationClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,310 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback, useId, type JSX } from "react";
import { RestartIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Button, InlineSpinner, Text } from "@vector-im/compound-web";
import styles from "./RoomStatusBarView.module.css";
import { useViewModel } from "../../useViewModel";
import { type ViewModel } from "../../viewmodel";
import { useI18n } from "../../utils/i18nContext";
import { Banner } from "../../composer/Banner";
export interface RoomStatusBarViewActions {
/**
* Called when the user clicks on the 'resend all' button in the 'unsent messages' bar.
*/
onResendAllClick?: () => Promise<void>;
/**
* Called when the user clicks on the 'cancel all' button in the 'unsent messages' bar.
*/
onDeleteAllClick?: () => void;
/**
* Called when the user clicks on the 'Retry' button in the 'failed to start chat' bar.
*/
onRetryRoomCreationClick?: () => void;
/**
* Called when the user clicks on the 'Review Terms and Conditions' button.
*/
onTermsAndConditionsClicked?: () => void;
}
export const RoomStatusBarState = {
/**
* Connectivity to the homeserver has been lost. The user can not take any actions
* until the connection is restored.
*/
ConnectionLost: "ConnectionLost",
/**
* The homeserver has indiciated the user needs to consent to the Terms and Conditions
* before they can send a message.
*/
NeedsConsent: "NeedsConsent",
/**
* The homeserver has indiciated that messages can not be sent due to a resource limit
* being reached. The user may use the given admin contact details.
*/
ResourceLimited: "ResourceLimited",
/**
* There are messages stored locally that previously failed to send that the user
* may now retry or delete.
*/
UnsentMessages: "UnsentMessages",
/**
* There was an error creating a room. The user may retry creation.
*/
LocalRoomFailed: "LocalRoomFailed",
} as const;
export interface RoomStatusBarNotVisible {
state: null;
}
export interface RoomStatusBarNoConnection {
state: "ConnectionLost";
}
export interface RoomStatusBarConsentState {
state: "NeedsConsent";
consentUri: string;
}
export interface RoomStatusBarResourceLimitedState {
state: "ResourceLimited";
resourceLimit: "monthly_active_user" | "hs_disabled" | string;
adminContactHref?: string;
}
export interface RoomStatusBarUnsentMessagesState {
state: "UnsentMessages";
isResending: boolean;
}
export interface RoomStatusBarLocalRoomError {
state: "LocalRoomFailed";
}
export type RoomStatusBarViewSnapshot =
| RoomStatusBarNoConnection
| RoomStatusBarConsentState
| RoomStatusBarResourceLimitedState
| RoomStatusBarUnsentMessagesState
| RoomStatusBarLocalRoomError
| RoomStatusBarNotVisible;
/**
* The view model for RoomStatusBarView.
*/
export type RoomStatusBarViewModel = ViewModel<RoomStatusBarViewSnapshot> & RoomStatusBarViewActions;
interface RoomStatusBarViewProps {
/**
* The view model for the banner.
*/
vm: RoomStatusBarViewModel;
}
/**
* A component to alert to a failure in the context of a room.
*
* @example
* ```tsx
* <RoomStatusBarView vm={RoomStatusBarViewModel} />
* ```
*/
export function RoomStatusBarView({ vm }: Readonly<RoomStatusBarViewProps>): JSX.Element | null {
const { translate: _t } = useI18n();
const snapshot = useViewModel(vm);
const bannerTitleId = useId();
const deleteAllClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(ev) => {
ev.preventDefault();
vm.onDeleteAllClick?.();
},
[vm],
);
const resendClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(ev) => {
ev.preventDefault();
void vm.onResendAllClick?.();
},
[vm],
);
const retryRoomCreationClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(ev) => {
ev.preventDefault();
vm.onRetryRoomCreationClick?.();
},
[vm],
);
const termsAndConditionsClicked = useCallback<React.MouseEventHandler<HTMLAnchorElement>>(() => {
// Allow the link to go through.
vm.onTermsAndConditionsClicked?.();
}, [vm]);
if (snapshot.state === null) {
// Nothing to show!
return null;
}
switch (snapshot.state) {
case RoomStatusBarState.ConnectionLost:
return (
<Banner type="critical" role="status" aria-labelledby={bannerTitleId}>
<div className={styles.container}>
<Text id={bannerTitleId} weight="semibold">
{_t("room|status_bar|server_connectivity_lost_title")}
</Text>
<Text className={styles.description} size="sm">
{_t("room|status_bar|server_connectivity_lost_description")}
</Text>
</div>
</Banner>
);
case RoomStatusBarState.NeedsConsent:
return (
<Banner
type="critical"
role="status"
aria-labelledby={bannerTitleId}
actions={
<Button
onClick={termsAndConditionsClicked}
kind="secondary"
size="sm"
as="a"
href={snapshot.consentUri}
target="_blank"
rel="noreferrer noopener"
>
{_t("terms|tac_button")}
</Button>
}
>
<div className={styles.container}>
<Text id={bannerTitleId} weight="semibold">
{_t("room|status_bar|requires_consent_agreement_title")}
</Text>
</div>
</Banner>
);
case RoomStatusBarState.ResourceLimited:
return (
<Banner
type="critical"
role="status"
aria-labelledby={bannerTitleId}
actions={
snapshot.adminContactHref && (
<Button
kind="secondary"
size="sm"
as="a"
href={snapshot.adminContactHref}
target="_blank"
rel="noreferrer noopener"
>
Contact admin
</Button>
)
}
>
<div className={styles.container}>
<Text id={bannerTitleId} weight="semibold">
{{
monthly_active_user: _t("room|status_bar|monthly_user_limit_reached_title"),
hs_disabled: _t("room|status_bar|homeserver_blocked_title"),
}[snapshot.resourceLimit] || _t("room|status_bar|exceeded_resource_limit_title")}
</Text>
<Text className={styles.description} size="sm">
{_t("room|status_bar|exceeded_resource_limit_description")}
</Text>
</div>
</Banner>
);
case RoomStatusBarState.LocalRoomFailed:
return (
<Banner
role="status"
type="critical"
aria-labelledby={bannerTitleId}
actions={
<Button
size="sm"
kind="secondary"
className={styles.container}
Icon={RestartIcon}
onClick={retryRoomCreationClick}
>
{_t("action|retry")}
</Button>
}
>
<Text id={bannerTitleId} weight="semibold" className={styles.container}>
{_t("room|status_bar|failed_to_create_room_title")}
</Text>
</Banner>
);
case RoomStatusBarState.UnsentMessages:
return (
<Banner
role="status"
type="critical"
actions={
snapshot.isResending ? (
<InlineSpinner />
) : (
<>
{vm.onDeleteAllClick && (
<Button
size="sm"
kind="destructive"
Icon={DeleteIcon}
disabled={snapshot.isResending}
onClick={deleteAllClick}
>
{_t("room|status_bar|delete_all")}
</Button>
)}
{vm.onResendAllClick && (
<Button
size="sm"
kind="secondary"
Icon={RestartIcon}
disabled={snapshot.isResending}
onClick={resendClick}
className={styles.container}
>
{_t("room|status_bar|retry_all")}
</Button>
)}
</>
)
}
aria-labelledby={bannerTitleId}
>
<div className={styles.container}>
<Text id={bannerTitleId} weight="semibold">
{_t("room|status_bar|some_messages_not_sent")}
</Text>
<Text className={styles.description} size="sm">
{_t("room|status_bar|select_messages_to_retry")}
</Text>
</div>
</Banner>
);
default:
// We should never get into this state.
return null;
}
}

View File

@@ -0,0 +1,520 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`RoomStatusBarView renders connection lost 1`] = `
<div>
<div
aria-labelledby="_r_0_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<div
class="container"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
id="_r_0_"
>
Connectivity to the server has been lost.
</p>
<p
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 description"
>
Sent messages will be stored until your connection has returned.
</p>
</div>
</div>
<div
class="actions"
/>
</div>
</div>
`;
exports[`RoomStatusBarView renders consent link 1`] = `
<div>
<div
aria-labelledby="_r_2_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<div
class="container"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
id="_r_2_"
>
You can't send any messages until you review and agree to our terms and conditions.
</p>
</div>
</div>
<div
class="actions"
>
<a
class="_button_13vu4_8"
data-kind="secondary"
data-size="sm"
href="#example"
rel="noreferrer noopener"
role="link"
tabindex="0"
target="_blank"
>
Review terms and conditions
</a>
</div>
</div>
</div>
`;
exports[`RoomStatusBarView renders local room error 1`] = `
<div>
<div
aria-labelledby="_r_6_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55 container"
id="_r_6_"
>
Could not start a chat with this user
</p>
</div>
<div
class="actions"
>
<button
class="_button_13vu4_8 container _has-icon_13vu4_60"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
/>
</svg>
Retry
</button>
</div>
</div>
</div>
`;
exports[`RoomStatusBarView renders resource limit error 1`] = `
<div>
<div
aria-labelledby="_r_1_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<div
class="container"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
id="_r_1_"
>
Your message wasn't sent because this homeserver has been blocked by its administrator.
</p>
<p
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 description"
>
Please contact your service administrator to continue using the service.
</p>
</div>
</div>
<div
class="actions"
>
<a
class="_button_13vu4_8"
data-kind="secondary"
data-size="sm"
href="#example"
rel="noreferrer noopener"
role="link"
tabindex="0"
target="_blank"
>
Contact admin
</a>
</div>
</div>
</div>
`;
exports[`RoomStatusBarView renders unsent messages 1`] = `
<div>
<div
aria-labelledby="_r_3_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<div
class="container"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
id="_r_3_"
>
Some of your messages have not been sent
</p>
<p
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 description"
>
You can select all or individual messages to retry or delete
</p>
</div>
</div>
<div
class="actions"
>
<button
aria-disabled="false"
class="_button_13vu4_8 _has-icon_13vu4_60 _destructive_13vu4_110"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
/>
</svg>
Delete all
</button>
<button
aria-disabled="false"
class="_button_13vu4_8 container _has-icon_13vu4_60"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
/>
</svg>
Retry all
</button>
</div>
</div>
</div>
`;
exports[`RoomStatusBarView renders unsent messages and deletes all 1`] = `
<div>
<div
aria-labelledby="_r_4_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<div
class="container"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
id="_r_4_"
>
Some of your messages have not been sent
</p>
<p
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 description"
>
You can select all or individual messages to retry or delete
</p>
</div>
</div>
<div
class="actions"
>
<button
aria-disabled="false"
class="_button_13vu4_8 _has-icon_13vu4_60 _destructive_13vu4_110"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
/>
</svg>
Delete all
</button>
<button
aria-disabled="false"
class="_button_13vu4_8 container _has-icon_13vu4_60"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
/>
</svg>
Retry all
</button>
</div>
</div>
</div>
`;
exports[`RoomStatusBarView renders unsent messages and resends all 1`] = `
<div>
<div
aria-labelledby="_r_5_"
class="banner"
data-type="critical"
role="status"
>
<div
class="icon"
>
<svg
fill="currentColor"
font-size="24"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 15a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 16q0 .424.287.712.288.288.713.288m0-4q.424 0 .713-.287A.97.97 0 0 0 13 12V8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8v4q0 .424.287.713.288.287.713.287m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
/>
</svg>
</div>
<div
class="content"
>
<div
class="container"
>
<p
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
id="_r_5_"
>
Some of your messages have not been sent
</p>
<p
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31 description"
>
You can select all or individual messages to retry or delete
</p>
</div>
</div>
<div
class="actions"
>
<button
aria-disabled="false"
class="_button_13vu4_8 _has-icon_13vu4_60 _destructive_13vu4_110"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21q-.824 0-1.412-.587A1.93 1.93 0 0 1 5 19V6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 4 5q0-.424.287-.713A.97.97 0 0 1 5 4h4q0-.424.287-.712A.97.97 0 0 1 10 3h4q.424 0 .713.288Q15 3.575 15 4h4q.424 0 .712.287Q20 4.576 20 5t-.288.713A.97.97 0 0 1 19 6v13q0 .824-.587 1.413A1.93 1.93 0 0 1 17 21zM7 6v13h10V6zm2 10q0 .424.287.712Q9.576 17 10 17t.713-.288A.97.97 0 0 0 11 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 10 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 9 9zm4 0q0 .424.287.712.288.288.713.288.424 0 .713-.288A.97.97 0 0 0 15 16V9a.97.97 0 0 0-.287-.713A.97.97 0 0 0 14 8a.97.97 0 0 0-.713.287A.97.97 0 0 0 13 9z"
/>
</svg>
Delete all
</button>
<button
aria-disabled="false"
class="_button_13vu4_8 container _has-icon_13vu4_60"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.93 8A8 8 0 1 1 4 12a1 1 0 1 0-2 0c0 5.523 4.477 10 10 10s10-4.477 10-10a10 10 0 0 0-.832-4A10 10 0 0 0 12 2a9.99 9.99 0 0 0-8 3.999V4a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1h4a1 1 0 0 0 0-2H5.755A7.99 7.99 0 0 1 12 4a8 8 0 0 1 6.93 4"
/>
</svg>
Retry all
</button>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,8 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export * from "./RoomStatusBarView";