mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-08 21:56:19 +02:00
Refactor as a shared-componend / MVVM v2
This commit is contained in:
parent
15713604e3
commit
e449a9d8d1
@ -0,0 +1,12 @@
|
||||
.container {
|
||||
color: var(--cpd-color-text-primary);
|
||||
svg {
|
||||
/* Ensure button icons are primary too */
|
||||
color: var(--cpd-color-text-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--cpd-font-size-body-sm);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
@ -6,20 +6,23 @@
|
||||
*/
|
||||
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 {
|
||||
RoomStatusBarView,
|
||||
type RoomStatusBarViewActions,
|
||||
type RoomStatusBarViewSnapshot,
|
||||
} from "./RoomStatusBarView";
|
||||
import { RoomStatusBarView, type RoomStatusBarViewActions, type RoomStatusBarViewSnapshot } from "./RoomStatusBarView";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
type RoomStatusBarProps = RoomStatusBarViewSnapshot & RoomStatusBarViewActions;
|
||||
|
||||
const RoomStatusBarViewWrapper = ({ onClose, ...rest }: RoomStatusBarProps): JSX.Element => {
|
||||
const RoomStatusBarViewWrapper = ({
|
||||
onResendAllClick,
|
||||
onDeleteAllClick,
|
||||
onRetryRoomCreationClick,
|
||||
...rest
|
||||
}: RoomStatusBarProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {
|
||||
onClose,
|
||||
onResendAllClick,
|
||||
onDeleteAllClick,
|
||||
onRetryRoomCreationClick,
|
||||
});
|
||||
return <RoomStatusBarView vm={vm} />;
|
||||
};
|
||||
@ -30,13 +33,73 @@ export default {
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
args: {
|
||||
visible: true,
|
||||
onClose: fn(),
|
||||
onResendAllClick: fn(),
|
||||
onDeleteAllClick: fn(),
|
||||
onRetryRoomCreationClick: fn(),
|
||||
},
|
||||
} as Meta<typeof RoomStatusBarViewWrapper>;
|
||||
|
||||
const Template: StoryFn<typeof RoomStatusBarViewWrapper> = (args) => (
|
||||
<RoomStatusBarViewWrapper {...args} />
|
||||
);
|
||||
const Template: StoryFn<typeof RoomStatusBarViewWrapper> = (args) => <RoomStatusBarViewWrapper {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
/**
|
||||
* Rendered when the client has lost connection with the server.
|
||||
*/
|
||||
export const WithConnectionLost = Template.bind({});
|
||||
WithConnectionLost.args = {
|
||||
state: {
|
||||
connectionLost: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
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: {
|
||||
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: {
|
||||
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: {
|
||||
isResending: true,
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Rendered when a local room has failed to be created.
|
||||
*/
|
||||
export const WithLocalRoomRetry = Template.bind({});
|
||||
WithLocalRoomRetry.args = {
|
||||
state: {
|
||||
isRetryingRoomCreation: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -10,19 +10,60 @@ import { render } from "jest-matrix-react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
|
||||
import * as stories from "./RoomStatusBarView.stories.tsx";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
const { WithConnectionLost, WithConsentLink, WithResourceLimit, WithUnsentMessages, WithLocalRoomRetry } =
|
||||
composeStories(stories);
|
||||
|
||||
describe("RoomStatusBarView", () => {
|
||||
it("renders a history visible banner", () => {
|
||||
const dismissFn = jest.fn();
|
||||
|
||||
const { container } = render(<Default onClose={dismissFn} />);
|
||||
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 = container.querySelector("button");
|
||||
expect(button).not.toBeNull();
|
||||
button?.click();
|
||||
expect(dismissFn).toHaveBeenCalled();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,12 +5,15 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import React, { useCallback, type JSX } from "react";
|
||||
|
||||
import styles from "./RoomStatusBarView.module.css";
|
||||
import { useViewModel } from "../../useViewModel";
|
||||
import { _t } from "../../utils/i18n";
|
||||
import { type ViewModel } from "../../viewmodel";
|
||||
|
||||
import { RestartIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
import { Button, InlineSpinner, Text } from "@vector-im/compound-web";
|
||||
import { Banner } from "../../composer/Banner";
|
||||
export interface RoomStatusBarViewActions {
|
||||
/**
|
||||
* Called when the user clicks on the 'resend all' button in the 'unsent messages' bar.
|
||||
@ -20,11 +23,17 @@ export interface RoomStatusBarViewActions {
|
||||
/**
|
||||
* Called when the user clicks on the 'cancel all' button in the 'unsent messages' bar.
|
||||
*/
|
||||
onCancelAllClick?: () => void;
|
||||
onDeleteAllClick?: () => void;
|
||||
|
||||
/**
|
||||
* For local rooms which haven't been created yet, this will retry creating the room.
|
||||
* @returns
|
||||
*/
|
||||
onRetryRoomCreationClick?: () => void;
|
||||
}
|
||||
|
||||
export interface RoomStatusBarNoConnection {
|
||||
connectionLost: boolean;
|
||||
connectionLost: true;
|
||||
}
|
||||
|
||||
export interface RoomStatusBarConsentState {
|
||||
@ -32,27 +41,31 @@ export interface RoomStatusBarConsentState {
|
||||
}
|
||||
|
||||
export interface RoomStatusBarResourceLimitedState {
|
||||
consentUri: string;
|
||||
resourceLimit: "monthly_active_user" | "hs_disabled" | string;
|
||||
adminContactHref?: string;
|
||||
}
|
||||
|
||||
export interface RoomStatusBarUnsentMessagesState {
|
||||
consentUri: string;
|
||||
isResending: boolean;
|
||||
}
|
||||
export interface RoomStatusBarLocalRoomError {
|
||||
isRetryingRoomCreation: boolean;
|
||||
}
|
||||
|
||||
export interface RoomStatusBarViewSnapshot {
|
||||
/**
|
||||
* Whether the banner is currently visible.
|
||||
*/
|
||||
visible: boolean;
|
||||
state: RoomStatusBarNoConnection|RoomStatusBarConsentState|RoomStatusBarResourceLimitedState|RoomStatusBarUnsentMessagesState|null;
|
||||
state:
|
||||
| RoomStatusBarNoConnection
|
||||
| RoomStatusBarConsentState
|
||||
| RoomStatusBarResourceLimitedState
|
||||
| RoomStatusBarUnsentMessagesState
|
||||
| RoomStatusBarLocalRoomError
|
||||
| null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The view model for the banner.
|
||||
*/
|
||||
export type RoomStatusBarViewModel = ViewModel<RoomStatusBarViewSnapshot> &
|
||||
RoomStatusBarViewActions;
|
||||
export type RoomStatusBarViewModel = ViewModel<RoomStatusBarViewSnapshot> & RoomStatusBarViewActions;
|
||||
|
||||
interface RoomStatusBarViewProps {
|
||||
/**
|
||||
@ -62,7 +75,7 @@ interface RoomStatusBarViewProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* A component to alert that history is shared to new members of the room.
|
||||
* A component to alert to a failure in the context of a room.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
@ -70,9 +83,170 @@ interface RoomStatusBarViewProps {
|
||||
* ```
|
||||
*/
|
||||
export function RoomStatusBarView({ vm }: Readonly<RoomStatusBarViewProps>): JSX.Element {
|
||||
const { visible } = useViewModel(vm);
|
||||
|
||||
const { translate: _t } = useI18n();
|
||||
const { state } = useViewModel(vm);
|
||||
|
||||
const deleteAllClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onDeleteAllClick?.();
|
||||
},
|
||||
[vm.onDeleteAllClick],
|
||||
);
|
||||
|
||||
const resendClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onResendAllClick?.();
|
||||
},
|
||||
[vm.onResendAllClick],
|
||||
);
|
||||
|
||||
const retryRoomCreationClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onRetryRoomCreationClick?.();
|
||||
},
|
||||
[vm.onRetryRoomCreationClick],
|
||||
);
|
||||
|
||||
if (state === null) {
|
||||
// Nothing to show!
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if ("connectionLost" in state) {
|
||||
return (
|
||||
<Banner type="critical">
|
||||
<div className={styles.container}>
|
||||
<Text weight="semibold">{_t("room|status_bar|server_connectivity_lost_title")}</Text>
|
||||
<Text className={styles.description}>
|
||||
{_t("room|status_bar|server_connectivity_lost_description")}
|
||||
</Text>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
if ("consentUri" in state) {
|
||||
return (
|
||||
<Banner
|
||||
type="critical"
|
||||
actions={
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
as="a"
|
||||
href={state.consentUri}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
View Terms and Conditions
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<Text weight="semibold">{_t("room|status_bar|requires_consent_agreement_title")}</Text>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
if ("resourceLimit" in state) {
|
||||
const title =
|
||||
{
|
||||
monthly_active_user: _t("room|status_bar|monthly_user_limit_reached_title"),
|
||||
hs_disabled: _t("room|status_bar|homeserver_blocked_title"),
|
||||
}[state.resourceLimit] || _t("room|status_bar|exceeded_resource_limit_title");
|
||||
|
||||
return (
|
||||
<Banner
|
||||
type="critical"
|
||||
actions={
|
||||
state.adminContactHref && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
as="a"
|
||||
href={state.adminContactHref}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Contact admin
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<Text weight="semibold">{title}</Text>
|
||||
<Text className={styles.description}>
|
||||
{_t("room|status_bar|exceeded_resource_limit_description")}
|
||||
</Text>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
if ("isRetryingRoomCreation" in state) {
|
||||
return (
|
||||
<Banner
|
||||
type="critical"
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
kind="secondary"
|
||||
className={styles.container}
|
||||
Icon={RestartIcon}
|
||||
disabled={state.isRetryingRoomCreation}
|
||||
onClick={retryRoomCreationClick}
|
||||
>
|
||||
{_t("action|retry")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<Text weight="semibold">{_t("room|status_bar|some_messages_not_sent")}</Text>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
const actions = state.isResending ? (
|
||||
<InlineSpinner />
|
||||
) : (
|
||||
<>
|
||||
{vm.onDeleteAllClick && (
|
||||
<Button
|
||||
size="sm"
|
||||
kind="destructive"
|
||||
Icon={DeleteIcon}
|
||||
disabled={state.isResending}
|
||||
onClick={deleteAllClick}
|
||||
>
|
||||
{_t("room|status_bar|delete_all")}
|
||||
</Button>
|
||||
)}
|
||||
{vm.onResendAllClick && (
|
||||
<Button
|
||||
size="sm"
|
||||
kind="secondary"
|
||||
Icon={RestartIcon}
|
||||
disabled={state.isResending}
|
||||
onClick={resendClick}
|
||||
className={styles.container}
|
||||
>
|
||||
{_t("room|status_bar|retry_all")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<p> Tada! </p>
|
||||
<Banner type="critical" actions={actions}>
|
||||
<div className={styles.container}>
|
||||
<Text weight="semibold">{_t("room|status_bar|failed_to_create_room_title")}</Text>
|
||||
<Text className={styles.description}>{_t("room|status_bar|select_messages_to_retry")}</Text>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,504 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`RoomStatusBarView renders connection lost 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Connectivity to the server has been lost.
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 description"
|
||||
>
|
||||
Sent messages will be stored until your connection has returned.
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomStatusBarView renders consent link 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
You can't send any messages until you review and agree to our terms and conditions.
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<a
|
||||
class="_button_187yx_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
href="#example"
|
||||
rel="noreferrer noopener"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
View Terms and Conditions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomStatusBarView renders local room error 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Some of your messages have not been sent
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_187yx_8 container _has-icon_187yx_57"
|
||||
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
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Your message wasn't sent because this homeserver has been blocked by its administrator.
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 description"
|
||||
>
|
||||
Please contact your service administrator to continue using the service.
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<a
|
||||
class="_button_187yx_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
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Could not start a chat with this user
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 description"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _destructive_187yx_107"
|
||||
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_187yx_8 container _has-icon_187yx_57"
|
||||
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
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Could not start a chat with this user
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 description"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _destructive_187yx_107"
|
||||
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_187yx_8 container _has-icon_187yx_57"
|
||||
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
|
||||
class="banner"
|
||||
data-type="critical"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Could not start a chat with this user
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 description"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_187yx_8 _has-icon_187yx_57 _destructive_187yx_107"
|
||||
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_187yx_8 container _has-icon_187yx_57"
|
||||
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>
|
||||
`;
|
||||
@ -1,175 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
|
||||
margin-left: 65px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_typingIndicatorAvatars {
|
||||
width: 52px;
|
||||
margin-top: -1px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_typingIndicatorRemaining {
|
||||
display: inline-block;
|
||||
color: #acacac;
|
||||
background-color: #ddd;
|
||||
border: 1px solid $background;
|
||||
border-radius: 40px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: $font-24px;
|
||||
font-size: 0.8em;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_scrollDownIndicator {
|
||||
cursor: pointer;
|
||||
padding-left: 1px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_unreadMessagesBar {
|
||||
padding-top: 10px;
|
||||
color: $alert;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_connectionLostBar {
|
||||
display: flex;
|
||||
|
||||
margin-top: 19px;
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_unsentMessages {
|
||||
> div[role="alert"] {
|
||||
/* cheat some basic alignment */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 70px;
|
||||
margin: 12px;
|
||||
padding-left: 16px;
|
||||
background-color: $header-panel-bg-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_unsentBadge {
|
||||
margin-right: 12px;
|
||||
|
||||
.mx_NotificationBadge {
|
||||
/* Override sizing from the default badge */
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 24px !important;
|
||||
|
||||
.mx_NotificationBadge_count {
|
||||
font-size: $font-16px !important; /* override default */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_unsentTitle {
|
||||
color: $alert;
|
||||
font-size: $font-15px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_unsentDescription {
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_unsentButtonBar {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
margin-right: 22px;
|
||||
color: $muted-fg-color;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
padding: 5px 10px;
|
||||
padding-left: 30px; /* 18px for the icon, 2px margin to text, 10px regular padding */
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
& + .mx_AccessibleButton {
|
||||
border-left: 1px solid $resend-button-divider-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
left: 10px; /* inset for regular button padding */
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InlineSpinner {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
top: 1px; /* just to help the vertical alignment be slightly better */
|
||||
|
||||
& + span {
|
||||
margin-right: 10px; /* same margin/padding as the rightmost button */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_connectionLostBar svg {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_connectionLostBar_title {
|
||||
color: $alert;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_connectionLostBar_desc {
|
||||
color: $primary-content;
|
||||
font-size: $font-13px;
|
||||
opacity: 0.5;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_resend_link {
|
||||
color: $primary-content !important;
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_typingBar {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
|
||||
color: $primary-content;
|
||||
opacity: 0.5;
|
||||
overflow-y: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_MatrixChat_useCompactLayout {
|
||||
.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_indicator {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_typingBar {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2025 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, { useEffect, type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { RestartIcon, WarningIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
|
||||
import { useRoomStatusBarViewModel } from "../viewmodels/rooms/RoomStatusBarViewModel";
|
||||
|
||||
interface IProps {
|
||||
// the room this statusbar is representing.
|
||||
room: Room;
|
||||
/**
|
||||
* Called when the component becomes visible.
|
||||
* @returns
|
||||
*/
|
||||
onVisible: () => void;
|
||||
/**
|
||||
* Called when the component becomes hidden.
|
||||
* @returns
|
||||
*/
|
||||
onHidden: () => void;
|
||||
}
|
||||
|
||||
export function RoomStatusBar({ room, onVisible, onHidden }: IProps): JSX.Element | null {
|
||||
const vm = useRoomStatusBarViewModel({ room });
|
||||
|
||||
useEffect(() => {
|
||||
if (vm.visible) {
|
||||
onVisible();
|
||||
} else {
|
||||
onHidden();
|
||||
}
|
||||
}, [vm.visible, onVisible, onHidden]);
|
||||
|
||||
if (!vm.visible) {
|
||||
return null;
|
||||
}
|
||||
if ("connectivityLost" in vm) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
<div role="alert">
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<WarningIcon width="24px" height="24px" />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{_t("room|status_bar|server_connectivity_lost_title")}
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{_t("room|status_bar|server_connectivity_lost_description")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (vm.isResending) {
|
||||
return (
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={vm.title}
|
||||
description={vm.description}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
buttons={
|
||||
<>
|
||||
<InlineSpinner w={20} h={20} />
|
||||
{/* span for css */}
|
||||
<span>{_t("forward|sending")}</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={vm.title}
|
||||
description={vm.description}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
buttons={
|
||||
<>
|
||||
{vm.onCancelAllClick && (
|
||||
<AccessibleButton onClick={vm.onCancelAllClick}>
|
||||
<DeleteIcon />
|
||||
{_t("room|status_bar|delete_all")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
{vm.onResendAllClick && (
|
||||
<AccessibleButton onClick={vm.onResendAllClick}>
|
||||
<RestartIcon />
|
||||
{_t("room|status_bar|retry_all")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, { type ReactElement, type ReactNode } from "react";
|
||||
|
||||
import { type StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||
|
||||
interface RoomStatusBarUnsentMessagesProps {
|
||||
title: ReactNode;
|
||||
description?: string;
|
||||
notificationState: StaticNotificationState;
|
||||
buttons: ReactElement;
|
||||
}
|
||||
|
||||
export const RoomStatusBarUnsentMessages = (props: RoomStatusBarUnsentMessagesProps): ReactElement => {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
|
||||
<div role="alert">
|
||||
<div className="mx_RoomStatusBar_unsentBadge">
|
||||
<NotificationBadge notification={props.notificationState} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_unsentTitle">{props.title}</div>
|
||||
{props.description && <div className="mx_RoomStatusBar_unsentDescription">{props.description}</div>}
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_unsentButtonBar">{props.buttons}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -16,6 +16,7 @@ import React, {
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
type JSX,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
@ -45,7 +46,6 @@ import { debounce, throttle } from "lodash";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { type RoomViewProps } from "@element-hq/element-web-module-api";
|
||||
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
import { _t } from "../../languageHandler";
|
||||
@ -111,10 +111,8 @@ import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
|
||||
import { createRoomFromLocalRoom } from "../../utils/direct-messages";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
import EncryptionEvent from "../views/messages/EncryptionEvent";
|
||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
|
||||
import { type ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
|
||||
import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
|
||||
import { LargeLoader } from "./LargeLoader";
|
||||
import { isVideoRoom } from "../../utils/video-rooms";
|
||||
import { SDKContext } from "../../contexts/SDKContext";
|
||||
@ -318,33 +316,11 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||
encryptionTile = <EncryptionEvent mxEvent={encryptionEvent} />;
|
||||
}
|
||||
|
||||
const onRetryClicked = (): void => {
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
room.state = LocalRoomState.NEW;
|
||||
defaultDispatcher.dispatch({
|
||||
action: "local_room_event",
|
||||
roomId: room.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
let statusBar: ReactElement | null = null;
|
||||
let composer: ReactElement | null = null;
|
||||
|
||||
if (room.isError) {
|
||||
const buttons = (
|
||||
<AccessibleButton onClick={onRetryClicked}>
|
||||
<RestartIcon />
|
||||
{_t("action|retry")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
statusBar = (
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={_t("room|status_bar|some_messages_not_sent")}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
buttons={buttons}
|
||||
/>
|
||||
);
|
||||
statusBar = <RoomStatusBarWrappedView room={room} />;
|
||||
} else {
|
||||
composer = (
|
||||
<MessageComposer
|
||||
@ -404,6 +380,24 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
||||
|
||||
function RoomStatusBarWrappedView(props: ConstructorParameters<typeof RoomStatusBarViewModel>[0]): ReactElement {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new RoomStatusBarViewModel(props));
|
||||
useEffect(() => {
|
||||
if ("onVisible" in props) {
|
||||
// Initial setup
|
||||
if (vm.getSnapshot().state !== null) {
|
||||
props.onVisible();
|
||||
} else {
|
||||
props.onHidden?.();
|
||||
}
|
||||
vm.subscribe(() => {
|
||||
if (vm.getSnapshot().state !== null) {
|
||||
props.onVisible?.();
|
||||
} else {
|
||||
props.onHidden?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [vm]);
|
||||
|
||||
return <RoomStatusBarView vm={vm} />;
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
Thread,
|
||||
ThreadEvent,
|
||||
ReceiptType,
|
||||
EventStatus,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { debounce } from "lodash";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
@ -1004,6 +1005,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||
lastReadEventIndex: number | null,
|
||||
): lastReadEvent is MatrixEvent {
|
||||
if (!lastReadEvent) return false;
|
||||
if (lastReadEvent.status === EventStatus.NOT_SENT) return false;
|
||||
|
||||
// We want to avoid sending out read receipts when we are looking at
|
||||
// events in the past which are before the latest RR.
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
/*
|
||||
Copyright 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 {
|
||||
ClientEvent,
|
||||
EventStatus,
|
||||
type MatrixError,
|
||||
type Room,
|
||||
RoomEvent,
|
||||
SyncState,
|
||||
type SyncStateData,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React, { type ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { _t, _td } from "@element-hq/web-shared-components";
|
||||
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import Resend from "../../../Resend";
|
||||
import { messageForResourceLimitError } from "../../../utils/ErrorUtils";
|
||||
import ExternalLink from "../../views/elements/ExternalLink";
|
||||
|
||||
interface RoomStatusBarInvisible {
|
||||
visible: false;
|
||||
}
|
||||
|
||||
interface RoomStatusBarWithError {
|
||||
visible: true;
|
||||
connectivityLost: boolean;
|
||||
}
|
||||
|
||||
interface RoomStatusBarWithUnsentMessages {
|
||||
visible: true;
|
||||
title: ReactNode;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface RoomStatusBarWithUnsentMessagesActions extends RoomStatusBarWithUnsentMessages {
|
||||
isResending: false;
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick?: () => void;
|
||||
|
||||
// callback for when the user clicks on the 'cancel all' button in the
|
||||
// 'unsent messages' bar
|
||||
onCancelAllClick?: () => void;
|
||||
}
|
||||
|
||||
interface RoomStatusBarWithUnsentMessagesResending extends RoomStatusBarWithUnsentMessages {
|
||||
isResending: true;
|
||||
}
|
||||
|
||||
type RoomStatusBarVM =
|
||||
| RoomStatusBarWithError
|
||||
| RoomStatusBarWithUnsentMessagesActions
|
||||
| RoomStatusBarWithUnsentMessagesResending
|
||||
| RoomStatusBarInvisible;
|
||||
|
||||
interface IProps {
|
||||
// the room this statusbar is representing.
|
||||
room: Room;
|
||||
}
|
||||
|
||||
export function useRoomStatusBarViewModel({ room }: IProps): RoomStatusBarVM {
|
||||
const client = useMatrixClientContext();
|
||||
const syncState = useTypedEventEmitterState(
|
||||
client,
|
||||
ClientEvent.Sync,
|
||||
(state: SyncState, prevState: SyncState, data: SyncStateData) => {
|
||||
return { state, data };
|
||||
},
|
||||
);
|
||||
const [isResending, setResending] = useState(false);
|
||||
const unsentMessages = useTypedEventEmitterState(room, RoomEvent.LocalEchoUpdated, () => {
|
||||
return room.getPendingEvents().filter(function (ev) {
|
||||
const isNotSent = ev.status === EventStatus.NOT_SENT;
|
||||
return isNotSent;
|
||||
});
|
||||
});
|
||||
|
||||
const onResendAllClick = useCallback(() => {
|
||||
setResending(true);
|
||||
Resend.resendUnsentEvents(room).finally(() => {
|
||||
setResending(false);
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}, [room]);
|
||||
|
||||
const onCancelAllClick = useCallback(() => {
|
||||
Resend.cancelUnsentEvents(room);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
}, [room]);
|
||||
|
||||
const unsentMessagesTitle = useMemo(() => {
|
||||
let consentError: MatrixError | null = null;
|
||||
let resourceLimitError: MatrixError | null = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error) {
|
||||
if (m.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED") {
|
||||
resourceLimitError = m.error;
|
||||
}
|
||||
if (m.error.errcode === "M_CONSENT_NOT_GIVEN") {
|
||||
consentError = m.error;
|
||||
break; // This is the most important thing to show, so break here if we find one.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
return _t(
|
||||
"room|status_bar|requires_consent_agreement",
|
||||
{},
|
||||
{
|
||||
consentLink: (sub) => (
|
||||
<ExternalLink href={consentError!.data?.consent_uri} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
);
|
||||
} else if (resourceLimitError) {
|
||||
return messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact,
|
||||
{
|
||||
"monthly_active_user": _td("room|status_bar|monthly_user_limit_reached"),
|
||||
"hs_disabled": _td("room|status_bar|homeserver_blocked"),
|
||||
"": _td("room|status_bar|exceeded_resource_limit"),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _t("room|status_bar|some_messages_not_sent");
|
||||
}
|
||||
}, [unsentMessages]);
|
||||
|
||||
const hasConnectionError = useMemo(() => {
|
||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||
// if it's a resource limit exceeded error: those are shown in the top bar.
|
||||
const errorIsMauError = Boolean(
|
||||
syncState.data && syncState.data.error && syncState.data.error.name === "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
);
|
||||
return syncState.state === SyncState.Error && !errorIsMauError;
|
||||
}, [syncState]);
|
||||
|
||||
if (hasConnectionError) {
|
||||
return { visible: true, connectivityLost: true };
|
||||
}
|
||||
|
||||
if (unsentMessages.length) {
|
||||
if (isResending) {
|
||||
return {
|
||||
visible: true,
|
||||
title: unsentMessagesTitle,
|
||||
description: _t("room|status_bar|select_messages_to_retry"),
|
||||
isResending: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
visible: true,
|
||||
title: unsentMessagesTitle,
|
||||
description: _t("room|status_bar|select_messages_to_retry"),
|
||||
isResending,
|
||||
onResendAllClick,
|
||||
onCancelAllClick,
|
||||
};
|
||||
}
|
||||
|
||||
return { visible: false };
|
||||
}
|
||||
@ -2127,16 +2127,18 @@
|
||||
},
|
||||
"status_bar": {
|
||||
"delete_all": "Delete all",
|
||||
"exceeded_resource_limit": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||
"exceeded_resource_limit_title": "Your message wasn't sent because this homeserver has exceeded a resource limit.",
|
||||
"history_visible": "Messages you send will be shared with new members invited to this room. <a>Learn more</a>",
|
||||
"homeserver_blocked": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please <a>contact your service administrator</a> to continue using the service.",
|
||||
"monthly_user_limit_reached": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
|
||||
"requires_consent_agreement": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
|
||||
"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.",
|
||||
"exceeded_resource_limit_description": "Please contact your service administrator to continue using the service.",
|
||||
"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"
|
||||
"some_messages_not_sent": "Some of your messages have not been sent",
|
||||
"failed_to_create_room_title": "Could not start a chat with this user"
|
||||
},
|
||||
"unknown_status_code_for_timeline_jump": "unknown status code",
|
||||
"unread_notifications_predecessor": {
|
||||
|
||||
@ -10,54 +10,197 @@ import {
|
||||
type RoomStatusBarViewModel as RoomStatusBarViewModelInterface,
|
||||
type RoomStatusBarViewSnapshot,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import { HistoryVisibility, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
ClientEvent,
|
||||
SyncState,
|
||||
MatrixClient,
|
||||
type Room,
|
||||
MatrixError,
|
||||
RoomEvent,
|
||||
EventStatus,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import Resend from "../../Resend";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../settings/SettingLevel";
|
||||
|
||||
interface Props {
|
||||
interface PropsWithRoom {
|
||||
room: Room;
|
||||
}
|
||||
interface PropsWithVisibility extends PropsWithRoom {
|
||||
/**
|
||||
* Called when the bar becomes visible.
|
||||
*/
|
||||
onVisible: () => void;
|
||||
/**
|
||||
* Called when the bar becomes hidden.
|
||||
*/
|
||||
onHidden: () => void;
|
||||
}
|
||||
|
||||
type Props = PropsWithRoom | PropsWithVisibility;
|
||||
|
||||
export class RoomStatusBarViewModel
|
||||
extends BaseViewModel<RoomStatusBarViewSnapshot, Props>
|
||||
implements RoomStatusBarViewModelInterface
|
||||
{
|
||||
|
||||
private static readonly computeSnapshot = (
|
||||
room: Room
|
||||
): RoomStatusBarViewSnapshot => {
|
||||
const featureEnabled = SettingsStore.getValue("feature_share_history_on_invite");
|
||||
const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", room.roomId);
|
||||
|
||||
private static readonly determineStateForUnreadMessages = (room: Room): RoomStatusBarViewSnapshot["state"] => {
|
||||
const unsentMessages = room.getPendingEvents().filter((ev) => ev.status === EventStatus.NOT_SENT);
|
||||
if (unsentMessages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let resourceLimitError: MatrixError | null = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error) {
|
||||
if (m.error.errcode === "M_CONSENT_NOT_GIVEN") {
|
||||
// This is the most important thing to show, so break here if we find one.
|
||||
return {
|
||||
// This MUST exist.
|
||||
consentUri: m.error.data.consent_uri,
|
||||
};
|
||||
}
|
||||
if (m.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED") {
|
||||
resourceLimitError = m.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resourceLimitError) {
|
||||
return {
|
||||
resourceLimit: resourceLimitError.data.limit_type ?? "",
|
||||
adminContactHref: resourceLimitError.data.admin_contact,
|
||||
};
|
||||
}
|
||||
return {
|
||||
visible: true
|
||||
isResending: false,
|
||||
};
|
||||
};
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props, RoomStatusBarViewModel.computeSnapshot(props.room));
|
||||
}
|
||||
|
||||
private setSnapshot(): void {
|
||||
const acknowledged = SettingsStore.getValue("acknowledgedHistoryVisibility", this.props.room.roomId);
|
||||
|
||||
// Reset the acknowleded flag when the history visibility is set back to joined.
|
||||
if (this.props.room.getHistoryVisibility() === HistoryVisibility.Joined && acknowledged) {
|
||||
SettingsStore.setValue(
|
||||
"acknowledgedHistoryVisibility",
|
||||
this.props.room.roomId,
|
||||
SettingLevel.ROOM_ACCOUNT,
|
||||
false,
|
||||
);
|
||||
private static readonly computeSnapshot = (
|
||||
room: Room,
|
||||
client: MatrixClient,
|
||||
isResending: boolean,
|
||||
isRetryingRoomCreation: boolean,
|
||||
): RoomStatusBarViewSnapshot => {
|
||||
if (room instanceof LocalRoom) {
|
||||
if (isRetryingRoomCreation) {
|
||||
return {
|
||||
state: {
|
||||
isRetryingRoomCreation,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (room.isError) {
|
||||
return {
|
||||
state: {
|
||||
isRetryingRoomCreation,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Local rooms do not have to worry about these other conditions :)
|
||||
return { state: null };
|
||||
}
|
||||
}
|
||||
|
||||
this.snapshot.set(RoomStatusBarViewModel.computeSnapshot(this.props.room, this.props.threadId));
|
||||
// If we're in the process of resending, don't flicker.
|
||||
if (isResending) {
|
||||
return {
|
||||
state: {
|
||||
isResending,
|
||||
},
|
||||
};
|
||||
}
|
||||
const syncState = client.getSyncState();
|
||||
|
||||
// Highest priority.
|
||||
if (syncState === SyncState.Error) {
|
||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||
// if it's a resource limit exceeded error: those are shown in the top bar.
|
||||
const syncData = client.getSyncStateData();
|
||||
if (syncData?.error?.name === "M_RESOURCE_LIMIT_EXCEEDED") {
|
||||
const error = syncData.error as MatrixError;
|
||||
return {
|
||||
state: {
|
||||
// TODO: Correct limit
|
||||
resourceLimit: error.data.limit_type ?? "",
|
||||
adminContactHref: error.data.admin_contact,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
state: {
|
||||
connectionLost: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Then check messages.
|
||||
return { state: this.determineStateForUnreadMessages(room) };
|
||||
};
|
||||
|
||||
private readonly client: MatrixClient;
|
||||
|
||||
public constructor(props: Props) {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
super(props, RoomStatusBarViewModel.computeSnapshot(props.room, client, false, false));
|
||||
this.client = client;
|
||||
client.on(ClientEvent.Sync, this.onClientSync);
|
||||
props.room.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
|
||||
}
|
||||
|
||||
private readonly onClientSync = () => {
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
private readonly onRoomLocalEchoUpdated = () => {
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
private isResending = false;
|
||||
private isRetryingRoomCreation = false;
|
||||
|
||||
private setSnapshot(): void {
|
||||
this.snapshot.set(
|
||||
RoomStatusBarViewModel.computeSnapshot(
|
||||
this.props.room,
|
||||
this.client,
|
||||
this.isResending,
|
||||
this.isRetryingRoomCreation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.client.off(ClientEvent.Sync, this.onClientSync);
|
||||
this.props.room.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public onDeleteAllClick = (): void => {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
public onResendAllClick = (): void => {
|
||||
this.isResending = true;
|
||||
this.setSnapshot();
|
||||
void Resend.resendUnsentEvents(this.props.room).finally(() => {
|
||||
this.isResending = false;
|
||||
this.setSnapshot();
|
||||
});
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
public onRetryRoomCreationClick = (): void => {
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
(this.props.room as LocalRoom).state = LocalRoomState.NEW;
|
||||
dis.dispatch({
|
||||
action: "local_room_event",
|
||||
roomId: this.props.room.roomId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {
|
||||
type MatrixClient,
|
||||
PendingEventOrdering,
|
||||
EventStatus,
|
||||
type MatrixEvent,
|
||||
Room,
|
||||
MatrixError,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RoomStatusBar } from "../../../../src/components/structures/RoomStatusBar";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { mkEvent, stubClient } from "../../../test-utils/test-utils";
|
||||
|
||||
describe("RoomStatusBar", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
client.getSyncStateData = jest.fn().mockReturnValue({});
|
||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
});
|
||||
|
||||
const getComponent = () =>
|
||||
render(<RoomStatusBar room={room} onVisible={() => {}} onHidden={() => {}} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
it("should render nothing when room has no error or unsent messages", () => {
|
||||
const { container } = getComponent();
|
||||
expect(container.firstChild).toBe(null);
|
||||
});
|
||||
|
||||
describe("unsent messages", () => {
|
||||
it("should render warning when messages are unsent due to consent", () => {
|
||||
const unsentMessage = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
unsentMessage.status = EventStatus.NOT_SENT;
|
||||
unsentMessage.error = new MatrixError({
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
data: { consent_uri: "terms.com" },
|
||||
});
|
||||
|
||||
room.addPendingEvent(unsentMessage, "123");
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render warning when messages are unsent due to resource limit", () => {
|
||||
const unsentMessage = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
unsentMessage.status = EventStatus.NOT_SENT;
|
||||
unsentMessage.error = new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
data: { limit_type: "monthly_active_user" },
|
||||
});
|
||||
|
||||
room.addPendingEvent(unsentMessage, "123");
|
||||
|
||||
const { container } = getComponent();
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,39 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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, screen } from "jest-matrix-react";
|
||||
|
||||
import { RoomStatusBarUnsentMessages } from "../../../../src/components/structures/RoomStatusBarUnsentMessages";
|
||||
import { StaticNotificationState } from "../../../../src/stores/notifications/StaticNotificationState";
|
||||
|
||||
describe("RoomStatusBarUnsentMessages", () => {
|
||||
const title = "test title";
|
||||
const description = "test description";
|
||||
const buttonsText = "test buttons";
|
||||
const buttons = <div>{buttonsText}</div>;
|
||||
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={title}
|
||||
description={description}
|
||||
buttons={buttons}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the values passed as props", () => {
|
||||
screen.getByText(title);
|
||||
screen.getByText(description);
|
||||
screen.getByText(buttonsText);
|
||||
// notification state
|
||||
screen.getByText("!");
|
||||
});
|
||||
});
|
||||
@ -1,182 +0,0 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`RoomStatusBar unsent messages should render warning when messages are unsent due to consent 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_2char cpd-theme-light"
|
||||
>
|
||||
<span
|
||||
class="mx_NotificationBadge_count"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentTitle"
|
||||
>
|
||||
<span>
|
||||
You can't send any messages until you review and agree to
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
our terms and conditions
|
||||
<svg
|
||||
class="mx_ExternalLink_icon"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
|
||||
/>
|
||||
<path
|
||||
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentDescription"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentButtonBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomStatusBar unsent messages should render warning when messages are unsent due to resource limit 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentBadge"
|
||||
>
|
||||
<div
|
||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_2char cpd-theme-light"
|
||||
>
|
||||
<span
|
||||
class="mx_NotificationBadge_count"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentTitle"
|
||||
>
|
||||
Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentDescription"
|
||||
>
|
||||
You can select all or individual messages to retry or delete
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentButtonBar"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -200,53 +200,63 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||
class="_banner_15kcv_20"
|
||||
data-type="critical"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
class="_icon_15kcv_65"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="_content_15kcv_53"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentBadge"
|
||||
class="_container_sjla8_1"
|
||||
>
|
||||
<div
|
||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_2char cpd-theme-light"
|
||||
>
|
||||
<span
|
||||
class="mx_NotificationBadge_count"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentTitle"
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55"
|
||||
>
|
||||
Some of your messages have not been sent
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentButtonBar"
|
||||
</span>
|
||||
<div
|
||||
class="_actions_15kcv_85"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
class="_button_187yx_8 _has-icon_187yx_57"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
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"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
actions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user