refactor to use enum

This commit is contained in:
Half-Shot 2025-12-19 15:03:41 +00:00
parent e6aca3fb7b
commit 1b744d09d6
3 changed files with 192 additions and 185 deletions

View File

@ -8,7 +8,12 @@ import { type Meta, type StoryFn } from "@storybook/react-vite";
import React, { type JSX } from "react";
import { useMockedViewModel } from "../../useMockedViewModel";
import { RoomStatusBarView, type RoomStatusBarViewActions, type RoomStatusBarViewSnapshot } from "./RoomStatusBarView";
import {
RoomStatusBarState,
RoomStatusBarView,
type RoomStatusBarViewActions,
type RoomStatusBarViewSnapshot,
} from "./RoomStatusBarView";
import { fn } from "storybook/test";
type RoomStatusBarProps = RoomStatusBarViewSnapshot & RoomStatusBarViewActions;
@ -49,9 +54,7 @@ const Template: StoryFn<typeof RoomStatusBarViewWrapper> = (args) => <RoomStatus
*/
export const WithConnectionLost = Template.bind({});
WithConnectionLost.args = {
state: {
connectionLost: true,
},
state: RoomStatusBarState.ConnectionLost,
};
/**
@ -60,9 +63,8 @@ WithConnectionLost.args = {
*/
export const WithConsentLink = Template.bind({});
WithConsentLink.args = {
state: {
consentUri: "#example",
},
state: RoomStatusBarState.NeedsConsent,
consentUri: "#example",
};
/**
@ -71,10 +73,9 @@ WithConsentLink.args = {
*/
export const WithResourceLimit = Template.bind({});
WithResourceLimit.args = {
state: {
resourceLimit: "hs_disabled",
adminContactHref: "#example",
},
state: RoomStatusBarState.ResourceLimited,
resourceLimit: "hs_disabled",
adminContactHref: "#example",
};
/**
@ -82,9 +83,8 @@ WithResourceLimit.args = {
*/
export const WithUnsentMessages = Template.bind({});
WithUnsentMessages.args = {
state: {
isResending: false,
},
state: RoomStatusBarState.UnsentMessages,
isResending: false,
};
/**
@ -93,16 +93,13 @@ WithUnsentMessages.args = {
*/
export const WithUnsentMessagesSending = Template.bind({});
WithUnsentMessagesSending.args = {
state: {
isResending: true,
},
state: RoomStatusBarState.UnsentMessages,
isResending: true,
};
/**
* Rendered when a local room has failed to be created.
*/
export const WithLocalRoomRetry = Template.bind({});
WithLocalRoomRetry.args = {
state: {
shouldRetryRoomCreation: false,
},
state: RoomStatusBarState.LocalRoomFailed,
};

View File

@ -36,35 +36,48 @@ export interface RoomStatusBarViewActions {
onTermsAndConditionsClicked?: () => void;
}
export enum RoomStatusBarState {
ConnectionLost,
NeedsConsent,
ResourceLimited,
UnsentMessages,
LocalRoomFailed,
}
export interface RoomStatusBarNotVisible {
state: null;
}
export interface RoomStatusBarNoConnection {
connectionLost: true;
state: RoomStatusBarState.ConnectionLost;
}
export interface RoomStatusBarConsentState {
state: RoomStatusBarState.NeedsConsent;
consentUri: string;
}
export interface RoomStatusBarResourceLimitedState {
state: RoomStatusBarState.ResourceLimited;
resourceLimit: "monthly_active_user" | "hs_disabled" | string;
adminContactHref?: string;
}
export interface RoomStatusBarUnsentMessagesState {
state: RoomStatusBarState.UnsentMessages;
isResending: boolean;
}
export interface RoomStatusBarLocalRoomError {
shouldRetryRoomCreation: boolean;
state: RoomStatusBarState.LocalRoomFailed;
}
export interface RoomStatusBarViewSnapshot {
state:
| RoomStatusBarNoConnection
| RoomStatusBarConsentState
| RoomStatusBarResourceLimitedState
| RoomStatusBarUnsentMessagesState
| RoomStatusBarLocalRoomError
| null;
}
export type RoomStatusBarViewSnapshot =
| RoomStatusBarNoConnection
| RoomStatusBarConsentState
| RoomStatusBarResourceLimitedState
| RoomStatusBarUnsentMessagesState
| RoomStatusBarLocalRoomError
| RoomStatusBarNotVisible;
/**
* The view model for the banner.
@ -88,7 +101,7 @@ interface RoomStatusBarViewProps {
*/
export function RoomStatusBarView({ vm }: Readonly<RoomStatusBarViewProps>): JSX.Element {
const { translate: _t } = useI18n();
const { state } = useViewModel(vm);
const snapshot = useViewModel(vm);
const bannerTitleId = useId();
const deleteAllClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
@ -120,158 +133,154 @@ export function RoomStatusBarView({ vm }: Readonly<RoomStatusBarViewProps>): JSX
vm.onTermsAndConditionsClicked?.();
}, [vm.onTermsAndConditionsClicked]);
if (state === null) {
if (snapshot.state === null) {
// Nothing to show!
return <></>;
}
if ("connectionLost" in state) {
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}>
{_t("room|status_bar|server_connectivity_lost_description")}
</Text>
</div>
</Banner>
);
}
if ("consentUri" in state) {
return (
<Banner
type="critical"
role="status"
aria-labelledby={bannerTitleId}
actions={
<Button
onClick={termsAndConditionsClicked}
kind="secondary"
size="sm"
as="a"
href={state.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>
);
}
if ("resourceLimit" in state) {
const title =
{
monthly_active_user: _t("room|status_bar|monthly_user_limit_reached_title"),
hs_disabled: _t("room|status_bar|homeserver_blocked_title"),
}[state.resourceLimit] || _t("room|status_bar|exceeded_resource_limit_title");
return (
<Banner
type="critical"
role="status"
aria-labelledby={bannerTitleId}
actions={
state.adminContactHref && (
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}>
{_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={state.adminContactHref}
href={snapshot.consentUri}
target="_blank"
rel="noreferrer noopener"
>
Contact admin
{_t("terms|tac_button")}
</Button>
)
}
>
<div className={styles.container}>
<Text id={bannerTitleId} weight="semibold">
{title}
</Text>
<Text className={styles.description}>
{_t("room|status_bar|exceeded_resource_limit_description")}
</Text>
</div>
</Banner>
);
}
if ("shouldRetryRoomCreation" in state) {
return (
<Banner
role="status"
type="critical"
aria-labelledby={bannerTitleId}
actions={
<Button
size="sm"
kind="secondary"
className={styles.container}
Icon={RestartIcon}
disabled={state.shouldRetryRoomCreation}
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>
);
}
const actions = state.isResending ? (
<InlineSpinner />
) : (
<>
{vm.onDeleteAllClick && (
<Button
size="sm"
kind="destructive"
Icon={DeleteIcon}
disabled={state.isResending}
onClick={deleteAllClick}
}
>
{_t("room|status_bar|delete_all")}
</Button>
)}
{vm.onResendAllClick && (
<Button
size="sm"
kind="secondary"
Icon={RestartIcon}
disabled={state.isResending}
onClick={resendClick}
className={styles.container}
>
{_t("room|status_bar|retry_all")}
</Button>
)}
</>
);
<div className={styles.container}>
<Text id={bannerTitleId} weight="semibold">
{_t("room|status_bar|requires_consent_agreement_title")}
</Text>
</div>
</Banner>
);
case RoomStatusBarState.ResourceLimited:
const title =
{
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");
return (
<Banner role="status" type="critical" actions={actions} 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}>{_t("room|status_bar|select_messages_to_retry")}</Text>
</div>
</Banner>
);
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">
{title}
</Text>
<Text className={styles.description}>
{_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:
const 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>
)}
</>
);
return (
<Banner role="status" type="critical" actions={actions} 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}>{_t("room|status_bar|select_messages_to_retry")}</Text>
</div>
</Banner>
);
default:
throw Error(`Unexpected unknown state for RoomStatusBar ${snapshot["state"]}`);
}
}

View File

@ -7,6 +7,7 @@
import {
BaseViewModel,
RoomStatusBarState,
type RoomStatusBarViewModel as RoomStatusBarViewModelInterface,
type RoomStatusBarViewSnapshot,
} from "@element-hq/web-shared-components";
@ -49,14 +50,17 @@ export class RoomStatusBarViewModel
private static readonly determineStateForUnreadMessages = (
room: Room,
hasClickedTermsAndConditions: boolean,
): RoomStatusBarViewSnapshot["state"] => {
): RoomStatusBarViewSnapshot => {
const unsentMessages = room.getPendingEvents().filter((ev) => ev.status === EventStatus.NOT_SENT);
if (unsentMessages.length === 0) {
return null;
return {
state: null,
};
}
if (hasClickedTermsAndConditions) {
// The user has just clicked (and we assume accepted) the terms and contitions, so show them the retry buttons
return {
state: RoomStatusBarState.UnsentMessages,
isResending: false,
};
}
@ -66,7 +70,7 @@ export class RoomStatusBarViewModel
if (m.error.errcode === "M_CONSENT_NOT_GIVEN") {
// This is the most important thing to show, so break here if we find one.
return {
// This MUST exist.
state: RoomStatusBarState.NeedsConsent,
consentUri: m.error.data.consent_uri,
};
}
@ -77,11 +81,13 @@ export class RoomStatusBarViewModel
}
if (resourceLimitError) {
return {
state: RoomStatusBarState.ResourceLimited,
resourceLimit: resourceLimitError.data.limit_type ?? "",
adminContactHref: resourceLimitError.data.admin_contact,
};
}
return {
state: RoomStatusBarState.UnsentMessages,
isResending: false,
};
};
@ -95,9 +101,7 @@ export class RoomStatusBarViewModel
if (room instanceof LocalRoom) {
if (room.isError) {
return {
state: {
shouldRetryRoomCreation: true,
},
state: RoomStatusBarState.LocalRoomFailed,
};
} else {
// Local rooms do not have to worry about these other conditions :)
@ -108,9 +112,8 @@ export class RoomStatusBarViewModel
// If we're in the process of resending, don't flicker.
if (isResending) {
return {
state: {
isResending,
},
state: RoomStatusBarState.UnsentMessages,
isResending,
};
}
const syncState = client.getSyncState();
@ -128,15 +131,13 @@ export class RoomStatusBarViewModel
};
} else {
return {
state: {
connectionLost: true,
},
state: RoomStatusBarState.ConnectionLost,
};
}
}
// Then check messages.
return { state: this.determineStateForUnreadMessages(room, hasClickedTermsAndConditions) };
return this.determineStateForUnreadMessages(room, hasClickedTermsAndConditions);
};
private readonly client: MatrixClient;