Show banner for message rejection

This commit is contained in:
R Midhun Suresh 2026-04-22 23:49:19 +05:30
parent 07dd1ab2f7
commit 3045068f25
No known key found for this signature in database
2 changed files with 95 additions and 7 deletions

View File

@ -98,6 +98,17 @@ export class RoomStatusBarViewModel
adminContactHref: resourceLimitError.data.admin_contact,
};
}
// Check if any of the unsent messages are because the server rejected them.
const serverRejectedEvent = unsentMessages.find((event) => event.error?.errcode === "M_FORBIDDEN");
const errorMessage = serverRejectedEvent?.error?.error;
if (errorMessage) {
return {
state: RoomStatusBarState.MessageRejected,
errorMessage,
};
}
// Otherwise, we know there are unsent messages but the error is not special.
return {
state: RoomStatusBarState.UnsentMessages,
@ -151,6 +162,8 @@ export class RoomStatusBarViewModel
private readonly client: MatrixClient;
private isMessageRejectedByServer: boolean = false;
public constructor(props: Props) {
const client = MatrixClientPeg.safeGet();
super(props, RoomStatusBarViewModel.computeSnapshot(props.room, client, false, false));
@ -164,21 +177,40 @@ export class RoomStatusBarViewModel
};
private readonly onRoomLocalEchoUpdated = (): void => {
this.setSnapshot();
const newSnapshot = RoomStatusBarViewModel.computeSnapshot(
this.props.room,
this.client,
this.isResending,
this.hasClickedTermsAndConditions,
);
this.setSnapshot(newSnapshot);
if (newSnapshot.state === RoomStatusBarState.MessageRejected) {
// When a message is rejected, there's not much to do except
// cancel the message. So why bother waiting until the user
// clicks the button?
this.isMessageRejectedByServer = true;
setTimeout(() => {
Resend.cancelUnsentEvents(this.props.room);
}, 1000);
}
};
private isResending = false;
private hasClickedTermsAndConditions = false;
private setSnapshot(): void {
this.snapshot.set(
private setSnapshot(newSnapshot?: RoomStatusBarViewSnapshot): void {
if (this.isMessageRejectedByServer) {
return;
}
const snapshot =
newSnapshot ??
RoomStatusBarViewModel.computeSnapshot(
this.props.room,
this.client,
this.isResending,
this.hasClickedTermsAndConditions,
),
);
);
this.snapshot.set(snapshot);
// Reset `hasClickedTermsAndConditions` once the state has cleared.
if (this.hasClickedTermsAndConditions && !this.snapshot.current.state) {
this.hasClickedTermsAndConditions = false;
@ -220,4 +252,9 @@ export class RoomStatusBarViewModel
roomId: this.props.room.roomId,
});
};
public onDismissClick = (): void => {
this.isMessageRejectedByServer = false;
this.setSnapshot();
};
}

View File

@ -6,7 +6,7 @@
*/
import React, { useCallback, useId, type JSX } from "react";
import { RestartIcon, DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { RestartIcon, DeleteIcon, CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Button, InlineSpinner, Text } from "@vector-im/compound-web";
import styles from "./RoomStatusBarView.module.css";
@ -33,6 +33,8 @@ export interface RoomStatusBarViewActions {
* Called when the user clicks on the 'Review Terms and Conditions' button.
*/
onTermsAndConditionsClicked?: () => void;
onDismissClick?: () => void;
}
export const RoomStatusBarState = {
@ -60,6 +62,10 @@ export const RoomStatusBarState = {
* There was an error creating a room. The user may retry creation.
*/
LocalRoomFailed: "LocalRoomFailed",
/**
* The homeserver rejected this message for some reason.
*/
MessageRejected: "MessageRejected",
} as const;
export interface RoomStatusBarNotVisible {
@ -85,6 +91,12 @@ export interface RoomStatusBarUnsentMessagesState {
state: "UnsentMessages";
isResending: boolean;
}
export interface RoomStatusBarMessageRejectedState {
state: "MessageRejected";
errorMessage: string;
}
export interface RoomStatusBarLocalRoomError {
state: "LocalRoomFailed";
}
@ -95,7 +107,8 @@ export type RoomStatusBarViewSnapshot =
| RoomStatusBarResourceLimitedState
| RoomStatusBarUnsentMessagesState
| RoomStatusBarLocalRoomError
| RoomStatusBarNotVisible;
| RoomStatusBarNotVisible
| RoomStatusBarMessageRejectedState;
/**
* The view model for RoomStatusBarView.
@ -130,6 +143,14 @@ export function RoomStatusBarView({ vm }: Readonly<RoomStatusBarViewProps>): JSX
[vm],
);
const dismissClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(ev) => {
ev.preventDefault();
vm.onDismissClick?.();
},
[vm],
);
const resendClick = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
(ev) => {
ev.preventDefault();
@ -302,6 +323,36 @@ export function RoomStatusBarView({ vm }: Readonly<RoomStatusBarViewProps>): JSX
</div>
</Banner>
);
case RoomStatusBarState.MessageRejected:
return (
<Banner
role="status"
type="critical"
actions={
<>
{vm.onDismissClick && (
<Button
size="sm"
kind="primary"
Icon={CloseIcon}
onClick={dismissClick}
className={styles.primaryAction}
>
Dismiss
</Button>
)}
</>
}
aria-labelledby={bannerTitleId}
>
<div className={styles.container}>
<Text className={styles.title} id={bannerTitleId} weight="medium">
Message Rejected
</Text>
<Text className={styles.description}>{snapshot.errorMessage}</Text>
</div>
</Banner>
);
default:
// We should never get into this state.
return null;