From 3045068f25eec897af7b0b33c462db852a2ad353 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Wed, 22 Apr 2026 23:49:19 +0530 Subject: [PATCH] Show banner for message rejection --- apps/web/src/viewmodels/room/RoomStatusBar.ts | 47 ++++++++++++++-- .../room/RoomStatusBar/RoomStatusBarView.tsx | 55 ++++++++++++++++++- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/apps/web/src/viewmodels/room/RoomStatusBar.ts b/apps/web/src/viewmodels/room/RoomStatusBar.ts index 799b3c4cec..6b4f6e5cb5 100644 --- a/apps/web/src/viewmodels/room/RoomStatusBar.ts +++ b/apps/web/src/viewmodels/room/RoomStatusBar.ts @@ -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(); + }; } diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx index 4d9d79014a..a5bbb63994 100644 --- a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx @@ -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): JSX [vm], ); + const dismissClick = useCallback>( + (ev) => { + ev.preventDefault(); + vm.onDismissClick?.(); + }, + [vm], + ); + const resendClick = useCallback>( (ev) => { ev.preventDefault(); @@ -302,6 +323,36 @@ export function RoomStatusBarView({ vm }: Readonly): JSX ); + case RoomStatusBarState.MessageRejected: + return ( + + {vm.onDismissClick && ( + + )} + + } + aria-labelledby={bannerTitleId} + > +
+ + Message Rejected + + {snapshot.errorMessage} +
+
+ ); default: // We should never get into this state. return null;