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
@ -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}`,
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
@ -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}>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
`;
|
||||
@ -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";
|
||||
@ -83,6 +83,13 @@ test.describe("Room Status Bar", () => {
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await expect(banner).toBeVisible({ timeout: 15000 });
|
||||
await expect(banner).toMatchScreenshot("consent.png");
|
||||
|
||||
// Click consent
|
||||
await banner.getByRole("link", { name: "View Terms and Conditions" }).click();
|
||||
await page.unroute("**/_matrix/client/**/send**");
|
||||
// Should now be allowed to retry.
|
||||
await banner.getByRole("button", { name: "Retry all" }).click();
|
||||
await expect(banner).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
test.describe("Message fails to send", () => {
|
||||
@ -161,7 +168,7 @@ test.describe("Room Status Bar", () => {
|
||||
await composer.fill("Hello");
|
||||
await composer.press("Enter");
|
||||
|
||||
const banner = page.getByText("!Some of your messages have");
|
||||
const banner = page.getByRole("status", { name: "Could not start a chat with this user" });
|
||||
await expect(banner).toBeVisible();
|
||||
await expect(banner).toMatchScreenshot("local_room_create_failed.png");
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 17 KiB |
@ -74,7 +74,6 @@
|
||||
@import "./structures/_QuickSettingsButton.pcss";
|
||||
@import "./structures/_RightPanel.pcss";
|
||||
@import "./structures/_RoomSearch.pcss";
|
||||
@import "./structures/_RoomStatusBar.pcss";
|
||||
@import "./structures/_RoomView.pcss";
|
||||
@import "./structures/_SearchBox.pcss";
|
||||
@import "./structures/_SpaceHierarchy.pcss";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -13,16 +13,38 @@ import {
|
||||
PushRuleActionName,
|
||||
PushRuleKind,
|
||||
TweakName,
|
||||
EventStatus,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { IPushRule, Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type { IPushRule, Room, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { NotificationLevel } from "./stores/notifications/NotificationLevel";
|
||||
import { getUnsentMessages } from "./components/structures/RoomStatusBar";
|
||||
import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread";
|
||||
import { EffectiveMembership, getEffectiveMembership, isKnockDenied } from "./utils/membership";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import { getMarkedUnreadState } from "./utils/notifications";
|
||||
|
||||
/**
|
||||
* Gets all pending events in a room that have a status of `EventStatus.NOT_SENT`
|
||||
* and belong to the current thread, if specified.
|
||||
* @param room The room to check.
|
||||
* @param threadId The thread to check. If not specified, no thread filtering is performed.
|
||||
* @returns An array of unsent matrix events.
|
||||
*/
|
||||
export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] {
|
||||
if (!room) {
|
||||
return [];
|
||||
}
|
||||
return room.getPendingEvents().filter(function (ev) {
|
||||
if (ev.status !== EventStatus.NOT_SENT) {
|
||||
return false;
|
||||
}
|
||||
if (threadId && threadId !== ev.threadRootId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export enum RoomNotifState {
|
||||
AllMessagesLoud = "all_messages_loud",
|
||||
AllMessages = "all_messages",
|
||||
|
||||
@ -1,297 +0,0 @@
|
||||
/*
|
||||
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, { type JSX, type ReactNode } from "react";
|
||||
import {
|
||||
ClientEvent,
|
||||
EventStatus,
|
||||
type MatrixError,
|
||||
type MatrixEvent,
|
||||
type Room,
|
||||
RoomEvent,
|
||||
type SyncState,
|
||||
type SyncStateData,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { RestartIcon, WarningIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t, _td } from "../../languageHandler";
|
||||
import Resend from "../../Resend";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { messageForResourceLimitError } from "../../utils/ErrorUtils";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
|
||||
import ExternalLink from "../views/elements/ExternalLink";
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||
|
||||
export function getUnsentMessages(room: Room, threadId?: string): MatrixEvent[] {
|
||||
if (!room) {
|
||||
return [];
|
||||
}
|
||||
return room.getPendingEvents().filter(function (ev) {
|
||||
const isNotSent = ev.status === EventStatus.NOT_SENT;
|
||||
const belongsToTheThread = threadId === ev.threadRootId;
|
||||
return isNotSent && (!threadId || belongsToTheThread);
|
||||
});
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// the room this statusbar is representing.
|
||||
room: Room;
|
||||
|
||||
// true if the room is being peeked at. This affects components that shouldn't
|
||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||
isPeeking?: boolean;
|
||||
// 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;
|
||||
|
||||
// callback for when the user clicks on the 'invite others' button in the
|
||||
// 'you are alone' bar
|
||||
onInviteClick?: () => void;
|
||||
|
||||
// callback for when we do something that changes the size of the
|
||||
// status bar. This is used to trigger a re-layout in the parent
|
||||
// component.
|
||||
onResize?: () => void;
|
||||
|
||||
// callback for when the status bar can be hidden from view, as it is
|
||||
// not displaying anything
|
||||
onHidden?: () => void;
|
||||
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible?: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
syncState: SyncState | null;
|
||||
syncStateData: SyncStateData | null;
|
||||
unsentMessages: MatrixEvent[];
|
||||
isResending: boolean;
|
||||
}
|
||||
|
||||
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
public static contextType = MatrixClientContext;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
syncState: this.context.getSyncState(),
|
||||
syncStateData: this.context.getSyncStateData(),
|
||||
unsentMessages: getUnsentMessages(this.props.room),
|
||||
isResending: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.unmounted = false;
|
||||
|
||||
const client = this.context;
|
||||
client.on(ClientEvent.Sync, this.onSyncStateChange);
|
||||
client.on(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
|
||||
|
||||
this.checkSize();
|
||||
}
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
this.checkSize();
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||
const client = this.context;
|
||||
if (client) {
|
||||
client.removeListener(ClientEvent.Sync, this.onSyncStateChange);
|
||||
client.removeListener(RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
private onSyncStateChange = (state: SyncState, prevState: SyncState | null, data?: SyncStateData): void => {
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
return;
|
||||
}
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
syncState: state,
|
||||
syncStateData: data ?? null,
|
||||
});
|
||||
};
|
||||
|
||||
private onResendAllClick = (): void => {
|
||||
Resend.resendUnsentEvents(this.props.room).then(() => {
|
||||
this.setState({ isResending: false });
|
||||
});
|
||||
this.setState({ isResending: true });
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
private onCancelAllClick = (): void => {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
};
|
||||
|
||||
private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room): void => {
|
||||
if (room.roomId !== this.props.room.roomId) return;
|
||||
const messages = getUnsentMessages(this.props.room);
|
||||
this.setState({
|
||||
unsentMessages: messages,
|
||||
isResending: messages.length > 0 && this.state.isResending,
|
||||
});
|
||||
};
|
||||
|
||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||
private checkSize(): void {
|
||||
if (this.getSize()) {
|
||||
if (this.props.onVisible) this.props.onVisible();
|
||||
} else {
|
||||
if (this.props.onHidden) this.props.onHidden();
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need the actual height - just whether it is likely to have
|
||||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
private getSize(): number {
|
||||
if (this.shouldShowConnectionError()) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
return STATUS_BAR_HIDDEN;
|
||||
}
|
||||
|
||||
private shouldShowConnectionError(): boolean {
|
||||
// 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(
|
||||
this.state.syncStateData &&
|
||||
this.state.syncStateData.error &&
|
||||
this.state.syncStateData.error.name === "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
);
|
||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||
}
|
||||
|
||||
private getUnsentMessageContent(): JSX.Element {
|
||||
const unsentMessages = this.state.unsentMessages;
|
||||
|
||||
let title: ReactNode;
|
||||
|
||||
let consentError: MatrixError | null = null;
|
||||
let resourceLimitError: MatrixError | null = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === "M_CONSENT_NOT_GIVEN") {
|
||||
consentError = m.error;
|
||||
break;
|
||||
} else if (m.error && m.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED") {
|
||||
resourceLimitError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _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) {
|
||||
title = 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 {
|
||||
title = _t("room|status_bar|some_messages_not_sent");
|
||||
}
|
||||
|
||||
let buttonRow = (
|
||||
<>
|
||||
<AccessibleButton onClick={this.onCancelAllClick}>
|
||||
<DeleteIcon />
|
||||
{_t("room|status_bar|delete_all")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentRetry">
|
||||
<RestartIcon />
|
||||
{_t("room|status_bar|retry_all")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
if (this.state.isResending) {
|
||||
buttonRow = (
|
||||
<>
|
||||
<InlineSpinner w={20} h={20} />
|
||||
{/* span for css */}
|
||||
<span>{_t("forward|sending")}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RoomStatusBarUnsentMessages
|
||||
title={title}
|
||||
description={_t("room|status_bar|select_messages_to_retry")}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
buttons={buttonRow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.shouldShowConnectionError()) {
|
||||
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 (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||
return this.getUnsentMessageContent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -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,7 @@ 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 { RoomStatusBarView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
import { _t } from "../../languageHandler";
|
||||
@ -92,7 +93,6 @@ import { type IOpts } from "../../createRoom";
|
||||
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import UploadBar from "./UploadBar";
|
||||
import RoomStatusBar from "./RoomStatusBar";
|
||||
import MessageComposer from "../views/rooms/MessageComposer";
|
||||
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
|
||||
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
|
||||
@ -112,10 +112,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";
|
||||
@ -137,6 +135,7 @@ import { DeclineAndBlockInviteDialog } from "../views/dialogs/DeclineAndBlockInv
|
||||
import { type FocusMessageSearchPayload } from "../../dispatcher/payloads/FocusMessageSearchPayload.ts";
|
||||
import { isRoomEncrypted } from "../../hooks/useIsEncrypted";
|
||||
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
|
||||
import { RoomStatusBarViewModel } from "../../viewmodels/room/RoomStatusBar.ts";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
@ -317,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
|
||||
@ -400,6 +377,33 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Wrap a RoomStatusBarView and ViewModel into one component, for usage with legacy React components.
|
||||
*/
|
||||
function RoomStatusBarWrappedView(props: ConstructorParameters<typeof RoomStatusBarViewModel>[0]): ReactElement {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new RoomStatusBarViewModel(props));
|
||||
useEffect(() => {
|
||||
// Note: We need to tell the parent component whether the viewmodel expects to render anything
|
||||
// (see onStatusBarVisible). This is ugly, but works.
|
||||
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, props]);
|
||||
|
||||
return <RoomStatusBarView vm={vm} />;
|
||||
}
|
||||
|
||||
export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// We cache the latest computed e2eStatus per room to show as soon as we switch rooms otherwise defaulting to
|
||||
@ -1687,14 +1691,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
private onInviteClick = (): void => {
|
||||
// open the room inviter
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_invite",
|
||||
roomId: this.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
private onJoinButtonClicked = (): void => {
|
||||
// If the user is a ROU, allow them to transition to a PWLU
|
||||
if (this.context.client?.isGuest()) {
|
||||
@ -2401,10 +2397,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
} else if (!this.state.search) {
|
||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = (
|
||||
<RoomStatusBar
|
||||
<RoomStatusBarWrappedView
|
||||
room={this.state.room}
|
||||
isPeeking={myMembership !== KnownMembership.Join}
|
||||
onInviteClick={this.onInviteClick}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
/>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -2128,18 +2128,6 @@
|
||||
},
|
||||
"this_room_button": "Search this room"
|
||||
},
|
||||
"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.",
|
||||
"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>.",
|
||||
"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"
|
||||
},
|
||||
"unknown_status_code_for_timeline_jump": "unknown status code",
|
||||
"unread_notifications_predecessor": {
|
||||
"one": "You have %(count)s unread notification in a prior version of this room.",
|
||||
|
||||
@ -141,7 +141,6 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
||||
|
||||
private updateNotificationState(): void {
|
||||
const snapshot = this.snapshot();
|
||||
|
||||
const { level, symbol, count, invited } = RoomNotifs.determineUnreadState(
|
||||
this.room,
|
||||
undefined,
|
||||
|
||||
223
src/viewmodels/room/RoomStatusBar.ts
Normal file
@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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 {
|
||||
BaseViewModel,
|
||||
RoomStatusBarState,
|
||||
type RoomStatusBarViewModel as RoomStatusBarViewModelInterface,
|
||||
type RoomStatusBarViewSnapshot,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import {
|
||||
ClientEvent,
|
||||
SyncState,
|
||||
type MatrixClient,
|
||||
type Room,
|
||||
type 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";
|
||||
|
||||
interface PropsWithRoom {
|
||||
room: Room | LocalRoom;
|
||||
}
|
||||
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
|
||||
{
|
||||
/**
|
||||
* Check if the room has any unread messages. If it does, we should render the specific message
|
||||
* depending on the kind of error encountered when sending them.
|
||||
*
|
||||
* Because a room can contain multiple unsent messages, the resultant state is based on the
|
||||
* "most important" error to show.
|
||||
*
|
||||
* @param room The room being viewed.
|
||||
* @param hasClickedTermsAndConditions Whether the terms and conditions button has just been pressed.
|
||||
* @returns A snapshot if an error should be visible, or null if not.
|
||||
*/
|
||||
private static readonly determineStateForUnreadMessages = (
|
||||
room: Room,
|
||||
hasClickedTermsAndConditions: boolean,
|
||||
): RoomStatusBarViewSnapshot => {
|
||||
const unsentMessages = room.getPendingEvents().filter((ev) => ev.status === EventStatus.NOT_SENT);
|
||||
if (unsentMessages.length === 0) {
|
||||
return {
|
||||
state: null,
|
||||
};
|
||||
}
|
||||
if (hasClickedTermsAndConditions) {
|
||||
// The user has just clicked (and we *assume* accepted) the terms and contitions, so show them the retry buttons.
|
||||
// If the user has not accepted the terms, we will just prompt the same error again anyway.
|
||||
return {
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Filter through the errors and find the most important error.
|
||||
let resourceLimitError: MatrixError | null = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error?.errcode === "M_CONSENT_NOT_GIVEN") {
|
||||
// This is the most important thing to show, so break here if we find one.
|
||||
return {
|
||||
state: RoomStatusBarState.NeedsConsent,
|
||||
consentUri: m.error.data.consent_uri,
|
||||
};
|
||||
}
|
||||
if (m.error?.errcode === "M_RESOURCE_LIMIT_EXCEEDED") {
|
||||
resourceLimitError = m.error;
|
||||
}
|
||||
}
|
||||
if (resourceLimitError) {
|
||||
return {
|
||||
state: RoomStatusBarState.ResourceLimited,
|
||||
resourceLimit: resourceLimitError.data.limit_type ?? "",
|
||||
adminContactHref: resourceLimitError.data.admin_contact,
|
||||
};
|
||||
}
|
||||
// Otherwise, we know there are unsent messages but the error is not special.
|
||||
return {
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending: false,
|
||||
};
|
||||
};
|
||||
|
||||
private static readonly computeSnapshot = (
|
||||
room: Room,
|
||||
client: MatrixClient,
|
||||
isResending: boolean,
|
||||
hasClickedTermsAndConditions: boolean,
|
||||
): RoomStatusBarViewSnapshot => {
|
||||
const isLocalRoomAndIsError = (room as LocalRoom)["isError"];
|
||||
if (isLocalRoomAndIsError !== undefined) {
|
||||
return {
|
||||
// Local room errors can only be about failed room creation.
|
||||
state: isLocalRoomAndIsError ? RoomStatusBarState.LocalRoomFailed : null,
|
||||
};
|
||||
}
|
||||
|
||||
// If we're in the process of resending, always show a resending state so we don't flicker.
|
||||
if (isResending) {
|
||||
return {
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending,
|
||||
};
|
||||
}
|
||||
|
||||
const syncState = client.getSyncState();
|
||||
|
||||
// Highest priority.
|
||||
// A no-connection bar trumps all else, as you won't be able to resend or do anything!
|
||||
if (syncState === SyncState.Error) {
|
||||
const syncData = client.getSyncStateData();
|
||||
if (syncData?.error?.name === "M_RESOURCE_LIMIT_EXCEEDED") {
|
||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||
// if it's a M_RESOURCE_LIMIT_EXCEEDED error: those are shown as a toast by LoggedInView.
|
||||
return {
|
||||
state: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: RoomStatusBarState.ConnectionLost,
|
||||
};
|
||||
}
|
||||
|
||||
// Connection is good, so check room messages for any failures.
|
||||
return this.determineStateForUnreadMessages(room, hasClickedTermsAndConditions);
|
||||
};
|
||||
|
||||
private readonly client: MatrixClient;
|
||||
|
||||
public constructor(props: Props) {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
super(props, RoomStatusBarViewModel.computeSnapshot(props.room, client, false, false));
|
||||
this.client = client;
|
||||
this.disposables.trackListener(client, ClientEvent.Sync, this.onClientSync);
|
||||
this.disposables.trackListener(props.room, RoomEvent.LocalEchoUpdated, this.onRoomLocalEchoUpdated);
|
||||
}
|
||||
|
||||
private readonly onClientSync = (): void => {
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
private readonly onRoomLocalEchoUpdated = (): void => {
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
private isResending = false;
|
||||
private hasClickedTermsAndConditions = false;
|
||||
|
||||
private setSnapshot(): void {
|
||||
this.snapshot.set(
|
||||
RoomStatusBarViewModel.computeSnapshot(
|
||||
this.props.room,
|
||||
this.client,
|
||||
this.isResending,
|
||||
this.hasClickedTermsAndConditions,
|
||||
),
|
||||
);
|
||||
// Reset `hasClickedTermsAndConditions` once the state has cleared.
|
||||
if (this.hasClickedTermsAndConditions && !this.snapshot.current.state) {
|
||||
this.hasClickedTermsAndConditions = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onTermsAndConditionsClicked = (): void => {
|
||||
this.hasClickedTermsAndConditions = true;
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
public onDeleteAllClick = (): void => {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
this.setSnapshot();
|
||||
};
|
||||
|
||||
public onResendAllClick = async (): Promise<void> => {
|
||||
this.isResending = true;
|
||||
this.setSnapshot();
|
||||
try {
|
||||
await Resend.resendUnsentEvents(this.props.room);
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
} finally {
|
||||
this.isResending = false;
|
||||
this.setSnapshot();
|
||||
}
|
||||
};
|
||||
|
||||
public onRetryRoomCreationClick = (): void => {
|
||||
if (this.props.room instanceof LocalRoom === false) {
|
||||
throw Error("Tried to recreate local room, but room was not local.");
|
||||
}
|
||||
|
||||
// This resets the local room state from error.
|
||||
this.props.room.state = LocalRoomState.NEW;
|
||||
dis.dispatch({
|
||||
action: "local_room_event",
|
||||
roomId: this.props.room.roomId,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -32,6 +32,7 @@ import {
|
||||
type GroupCall,
|
||||
HistoryVisibility,
|
||||
type ICreateRoomOpts,
|
||||
type EventStatus,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
@ -303,6 +304,7 @@ export function createTestClient(): MatrixClient {
|
||||
room_id: roomId,
|
||||
});
|
||||
}),
|
||||
resendEvent: jest.fn().mockResolvedValue({}),
|
||||
|
||||
_unstable_sendDelayedEvent: jest.fn(),
|
||||
_unstable_sendDelayedStateEvent: jest.fn(),
|
||||
@ -402,6 +404,7 @@ type MakeEventProps = MakeEventPassThruProps & {
|
||||
// eslint-disable-next-line camelcase
|
||||
prev_content?: IContent;
|
||||
unsigned?: IUnsigned;
|
||||
status?: EventStatus;
|
||||
};
|
||||
|
||||
export const mkRoomCreateEvent = (userId: string, roomId: string, content?: IContent): MatrixEvent => {
|
||||
@ -482,6 +485,9 @@ export function mkEvent(opts: MakeEventProps): MatrixEvent {
|
||||
getMxcAvatarUrl: () => {},
|
||||
} as unknown as RoomMember;
|
||||
}
|
||||
if (opts.status !== undefined) {
|
||||
mxEvent.status = opts.status;
|
||||
}
|
||||
return mxEvent;
|
||||
}
|
||||
|
||||
@ -702,7 +708,7 @@ export function mkStubRoom(
|
||||
getMembersWithMembership: jest.fn().mockReturnValue([]),
|
||||
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
|
||||
getMyMembership: jest.fn().mockReturnValue(KnownMembership.Join),
|
||||
getPendingEvents: () => [] as MatrixEvent[],
|
||||
getPendingEvents: jest.fn().mockReturnValue([]),
|
||||
getReceiptsForEvent: jest.fn().mockReturnValue([]),
|
||||
getRecommendedVersion: jest.fn().mockReturnValue(Promise.resolve("")),
|
||||
getThreads: jest.fn().mockReturnValue([]),
|
||||
|
||||
@ -26,10 +26,73 @@ import {
|
||||
RoomNotifState,
|
||||
getUnreadNotificationCount,
|
||||
determineUnreadState,
|
||||
getUnsentMessages,
|
||||
} from "../../src/RoomNotifs";
|
||||
import { NotificationLevel } from "../../src/stores/notifications/NotificationLevel";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { mkThread } from "../test-utils/threads";
|
||||
|
||||
describe("getUnsentMessages", () => {
|
||||
const ROOM_ID = "!roomId";
|
||||
let room: Room;
|
||||
let event: MatrixEvent;
|
||||
let client: MatrixClient;
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
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;
|
||||
});
|
||||
|
||||
it("returns no unsent messages", () => {
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("checks the event status", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(1);
|
||||
event.status = EventStatus.SENT;
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("only returns events related to a thread", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
const { rootEvent, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
rootEvent.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(rootEvent, rootEvent.getId()!);
|
||||
for (const event of events) {
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(event, Date.now() + Math.random() + "");
|
||||
}
|
||||
|
||||
const pendingEvents = getUnsentMessages(room, rootEvent.getId());
|
||||
|
||||
expect(pendingEvents[0].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[1].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[2].threadRootId).toBe(rootEvent.getId());
|
||||
|
||||
// Filters out the non thread events
|
||||
expect(pendingEvents.every((ev) => ev.getId() !== event.getId())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RoomNotifs test", () => {
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
|
||||
@ -1,150 +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, { getUnsentMessages } from "../../../../src/components/structures/RoomStatusBar";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { mkEvent, stubClient } from "../../../test-utils/test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
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} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
describe("getUnsentMessages", () => {
|
||||
it("returns no unsent messages", () => {
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("checks the event status", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(1);
|
||||
event.status = EventStatus.SENT;
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("only returns events related to a thread", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
const { rootEvent, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
rootEvent.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(rootEvent, rootEvent.getId()!);
|
||||
for (const event of events) {
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(event, Date.now() + Math.random() + "");
|
||||
}
|
||||
|
||||
const pendingEvents = getUnsentMessages(room, rootEvent.getId());
|
||||
|
||||
expect(pendingEvents[0].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[1].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[2].threadRootId).toBe(rootEvent.getId());
|
||||
|
||||
// Filters out the non thread events
|
||||
expect(pendingEvents.every((ev) => ev.getId() !== event.getId())).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<RoomStatusBar />", () => {
|
||||
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 <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 mx_RoomStatusBar_unsentRetry"
|
||||
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 <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 mx_RoomStatusBar_unsentRetry"
|
||||
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>
|
||||
`;
|
||||
@ -212,53 +212,61 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
|
||||
aria-labelledby="_r_ti_"
|
||||
class="_banner_48r66_20"
|
||||
data-type="critical"
|
||||
role="status"
|
||||
>
|
||||
<div
|
||||
role="alert"
|
||||
class="_icon_48r66_63"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentBadge"
|
||||
<svg
|
||||
fill="currentColor"
|
||||
font-size="24"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
Some of your messages have not been sent
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_RoomStatusBar_unsentButtonBar"
|
||||
<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_48r66_51"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-semibold_6v6n8_55 _container_mqidv_1"
|
||||
id="_r_ti_"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
Could not start a chat with this user
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="_actions_48r66_83"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 _container_mqidv_1 _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"
|
||||
>
|
||||
<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>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -409,7 +417,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="_r_qd_"
|
||||
aria-labelledby="_r_qm_"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@ -1373,7 +1381,7 @@ exports[`RoomView should hide the header when hideHeader=true 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="_r_10_"
|
||||
aria-labelledby="_r_12_"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@ -1682,7 +1690,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_5b_"
|
||||
aria-labelledby="_r_5e_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -1709,7 +1717,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_5g_"
|
||||
aria-labelledby="_r_5j_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -1724,7 +1732,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="_r_5l_"
|
||||
aria-labelledby="_r_5o_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -1751,7 +1759,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="_r_5q_"
|
||||
aria-labelledby="_r_5t_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -1781,7 +1789,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="_r_5v_"
|
||||
aria-labelledby="_r_62_"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@ -1848,7 +1856,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="_r_6a_"
|
||||
aria-labelledby="_r_6e_"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@ -2157,7 +2165,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_2e_"
|
||||
aria-labelledby="_r_2g_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -2184,7 +2192,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_2j_"
|
||||
aria-labelledby="_r_2l_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -2199,7 +2207,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="_r_2o_"
|
||||
aria-labelledby="_r_2q_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -2226,7 +2234,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="_r_2t_"
|
||||
aria-labelledby="_r_2v_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -2256,7 +2264,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="_r_32_"
|
||||
aria-labelledby="_r_34_"
|
||||
class="mx_AccessibleButton mx_FacePile mx_FacePile_toggled"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@ -2323,7 +2331,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="_r_3d_"
|
||||
aria-labelledby="_r_3g_"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@ -2632,7 +2640,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_f5_"
|
||||
aria-labelledby="_r_fc_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -2659,7 +2667,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_fa_"
|
||||
aria-labelledby="_r_fh_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -2674,7 +2682,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="_r_ff_"
|
||||
aria-labelledby="_r_fm_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -2701,7 +2709,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="_r_fk_"
|
||||
aria-labelledby="_r_fr_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -2731,7 +2739,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="_r_fp_"
|
||||
aria-labelledby="_r_g0_"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@ -2846,7 +2854,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_f5_"
|
||||
aria-labelledby="_r_fc_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -2873,7 +2881,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_fa_"
|
||||
aria-labelledby="_r_fh_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@ -2888,7 +2896,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="_r_ff_"
|
||||
aria-labelledby="_r_fm_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -2915,7 +2923,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="_r_fk_"
|
||||
aria-labelledby="_r_fr_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -2945,7 +2953,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="_r_fp_"
|
||||
aria-labelledby="_r_g0_"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@ -3014,7 +3022,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="_r_g4_"
|
||||
aria-labelledby="_r_gc_"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
data-testid="e2e-icon"
|
||||
style="width: 12px; height: 12px;"
|
||||
@ -3349,7 +3357,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Chat"
|
||||
aria-labelledby="_r_k7_"
|
||||
aria-labelledby="_r_kg_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -3376,7 +3384,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="_r_kc_"
|
||||
aria-labelledby="_r_kl_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -3403,7 +3411,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="_r_kh_"
|
||||
aria-labelledby="_r_kq_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@ -3433,7 +3441,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="_r_km_"
|
||||
aria-labelledby="_r_kv_"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@ -3518,7 +3526,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby="_r_kv_"
|
||||
aria-labelledby="_r_l8_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="secondary"
|
||||
data-testid="base-card-close-button"
|
||||
@ -3578,7 +3586,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="_r_l4_"
|
||||
aria-labelledby="_r_ld_"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
|
||||
@ -25,7 +25,6 @@ import { NotificationStateEvents } from "../../../../src/stores/notifications/No
|
||||
import { NotificationLevel } from "../../../../src/stores/notifications/NotificationLevel";
|
||||
import { createMessageEventContent } from "../../../test-utils/events";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import * as RoomStatusBarModule from "../../../../src/components/structures/RoomStatusBar";
|
||||
import * as UnreadModule from "../../../../src/Unread";
|
||||
|
||||
describe("RoomNotificationState", () => {
|
||||
@ -210,7 +209,7 @@ describe("RoomNotificationState", () => {
|
||||
|
||||
describe("computed attributes", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([]);
|
||||
jest.spyOn(room, "getPendingEvents").mockReturnValue([]);
|
||||
jest.spyOn(UnreadModule, "doesRoomHaveUnreadMessages").mockReturnValue(false);
|
||||
});
|
||||
|
||||
@ -221,7 +220,9 @@ describe("RoomNotificationState", () => {
|
||||
});
|
||||
|
||||
it("should has isUnsetMessage at true", () => {
|
||||
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
||||
jest.spyOn(room, "getPendingEvents").mockReturnValue([
|
||||
mkEvent({ status: EventStatus.NOT_SENT, user: "@foobar:example.org", type: "any.event", content: {} }),
|
||||
]);
|
||||
const roomNotifState = new RoomNotificationState(room, false);
|
||||
expect(roomNotifState.isUnsentMessage).toBe(true);
|
||||
});
|
||||
@ -239,7 +240,9 @@ describe("RoomNotificationState", () => {
|
||||
room.updateMyMembership(KnownMembership.Knock);
|
||||
expect(roomNotifState.isMention).toBe(false);
|
||||
|
||||
jest.spyOn(RoomStatusBarModule, "getUnsentMessages").mockReturnValue([{} as MatrixEvent]);
|
||||
jest.spyOn(room, "getPendingEvents").mockReturnValue([
|
||||
mkEvent({ status: EventStatus.NOT_SENT, user: "@foobar:example.org", type: "any.event", content: {} }),
|
||||
]);
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
expect(roomNotifState.isMention).toBe(false);
|
||||
});
|
||||
|
||||
166
test/viewmodels/room/RoomStatusBar-test.ts
Normal file
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 {
|
||||
SyncState,
|
||||
MatrixError,
|
||||
ClientEvent,
|
||||
type MatrixClient,
|
||||
type Room,
|
||||
type MatrixEvent,
|
||||
EventStatus,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { RoomStatusBarState } from "@element-hq/web-shared-components";
|
||||
import { type MockedObject } from "jest-mock";
|
||||
|
||||
import { mkEvent, mkRoom, stubClient } from "../../test-utils";
|
||||
import { RoomStatusBarViewModel } from "../../../src/viewmodels/room/RoomStatusBar";
|
||||
import { LocalRoom, LocalRoomState } from "../../../src/models/LocalRoom";
|
||||
|
||||
const userId = "@example:example.org";
|
||||
|
||||
function mkEventWithError(error: MatrixError): MatrixEvent {
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
user: userId,
|
||||
type: "org.example.test",
|
||||
content: {},
|
||||
status: EventStatus.NOT_SENT,
|
||||
});
|
||||
event.error = error;
|
||||
return event;
|
||||
}
|
||||
|
||||
describe("RoomStatusBarViewModel", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
let vm: RoomStatusBarViewModel;
|
||||
let room: MockedObject<Room>;
|
||||
let roomEmitFn!: () => void;
|
||||
beforeEach(() => {
|
||||
client = stubClient() as MockedObject<MatrixClient>;
|
||||
room = mkRoom(client, "!example");
|
||||
room.on.mockImplementationOnce((_event, fn) => {
|
||||
roomEmitFn = fn as any;
|
||||
return room;
|
||||
});
|
||||
vm = new RoomStatusBarViewModel({
|
||||
room,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should not be visible by default", () => {
|
||||
expect(vm.getSnapshot()).toEqual({ state: null });
|
||||
});
|
||||
|
||||
it("should resolve state to ConnectionLost on failed sync", () => {
|
||||
client.getSyncState.mockReturnValue(SyncState.Error);
|
||||
client.emit(ClientEvent.Sync, SyncState.Error, null);
|
||||
expect(vm.getSnapshot()).toEqual({ state: RoomStatusBarState.ConnectionLost });
|
||||
});
|
||||
|
||||
// Because we expect LoggedInView to pop a toast
|
||||
it("should resolve state to nothing if sync error is M_RESOURCE_LIMIT_EXCEEDED", () => {
|
||||
client.getSyncState.mockReturnValue(SyncState.Error);
|
||||
client.getSyncStateData.mockReturnValue({ error: new MatrixError({ errcode: "M_RESOURCE_LIMIT_EXCEEDED" }) });
|
||||
client.emit(ClientEvent.Sync, SyncState.Error, null);
|
||||
expect(vm.getSnapshot()).toEqual({ state: null });
|
||||
});
|
||||
|
||||
it("should resolve state to NeedsConsent if a pending event has a M_CONSENT_NOT_GIVEN error", () => {
|
||||
room.getPendingEvents.mockReturnValue([
|
||||
mkEventWithError(new MatrixError({ errcode: "M_CONSENT_NOT_GIVEN", consent_uri: "https://example.org" })),
|
||||
]);
|
||||
roomEmitFn();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.NeedsConsent,
|
||||
consentUri: "https://example.org",
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve state to UnsentMessages once onTermsAndConditionsClicked is called", () => {
|
||||
room.getPendingEvents.mockReturnValue([mkEventWithError(new MatrixError({ errcode: "M_CONSENT_NOT_GIVEN" }))]);
|
||||
roomEmitFn();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.NeedsConsent,
|
||||
});
|
||||
vm.onTermsAndConditionsClicked();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve state to ResourceLimited if a pending event has a M_RESOURCE_LIMIT_EXCEEDED error", () => {
|
||||
room.getPendingEvents.mockReturnValue([
|
||||
mkEventWithError(
|
||||
new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
limit_type: "hs_disabled",
|
||||
admin_contact: "https://example.org",
|
||||
}),
|
||||
),
|
||||
]);
|
||||
roomEmitFn();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.ResourceLimited,
|
||||
adminContactHref: "https://example.org",
|
||||
resourceLimit: "hs_disabled",
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve state to UnsentMessages if there are any other events", () => {
|
||||
room.getPendingEvents.mockReturnValue([mkEventWithError(new MatrixError({ errcode: "M_UNKNOWN" }))]);
|
||||
roomEmitFn();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve state to isResending=true once onResendAllClick is called", async () => {
|
||||
room.getPendingEvents.mockReturnValue([mkEventWithError(new MatrixError({ errcode: "M_UNKNOWN" }))]);
|
||||
roomEmitFn();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending: false,
|
||||
});
|
||||
const promise = vm.onResendAllClick();
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: RoomStatusBarState.UnsentMessages,
|
||||
isResending: true,
|
||||
});
|
||||
room.getPendingEvents.mockReturnValue([]);
|
||||
await promise;
|
||||
expect(client.resendEvent).toHaveBeenCalledTimes(1);
|
||||
expect(vm.getSnapshot()).toEqual({
|
||||
state: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("Local rooms", () => {
|
||||
it("should resolve state to LocalRoomFailed if room fails to be created", () => {
|
||||
const localRoom = new LocalRoom("!example", client, userId);
|
||||
localRoom.state = LocalRoomState.ERROR;
|
||||
vm = new RoomStatusBarViewModel({
|
||||
room: localRoom,
|
||||
});
|
||||
expect(vm.getSnapshot()).toEqual({ state: RoomStatusBarState.LocalRoomFailed });
|
||||
});
|
||||
it("should resolve state to nothing for any other state for localroom", () => {
|
||||
const localRoom = new LocalRoom("!example", client, userId);
|
||||
localRoom.state = LocalRoomState.NEW;
|
||||
vm = new RoomStatusBarViewModel({
|
||||
room: localRoom,
|
||||
});
|
||||
expect(vm.getSnapshot()).toEqual({ state: null });
|
||||
});
|
||||
});
|
||||
});
|
||||