From f58da1802d6b49ccd9d79c3a6c6ae9d5403b2144 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 30 Dec 2025 11:16:33 +0000 Subject: [PATCH] Add safety views. --- .../RoomStatusBarView.stories.tsx | 68 +++++++ .../room/RoomStatusBar/RoomStatusBarView.tsx | 170 +++++++++++++++++- 2 files changed, 237 insertions(+), 1 deletion(-) diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx index 38f8d86f1d..c25201bb9a 100644 --- a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx @@ -103,3 +103,71 @@ export const WithLocalRoomRetry = Template.bind({}); WithLocalRoomRetry.args = { state: RoomStatusBarState.LocalRoomFailed, }; + +/** + * Rendered when a message was rejected by the server, and cannot be reattempted. + */ +export const WithMessageRejected = Template.bind({}); +WithMessageRejected.args = { + state: RoomStatusBarState.MessageRejected, + onResendAllClick: undefined, + harms: ["org.matrix.msc4387.harassment"], +}; + +/** + * Rendered when a message was rejected by the server, and can be reattempted later. + */ +export const WithMessageRejectedCanRetryInTime = Template.bind({}); +WithMessageRejectedCanRetryInTime.args = { + state: RoomStatusBarState.MessageRejected, + onResendAllClick: undefined, + canRetryInSeconds: 5, + harms: [], +}; + +/** + * Rendered when a message was rejected by the server, and can be reattempted. + */ +export const WithMessageRejectedCanRetry = Template.bind({}); +WithMessageRejectedCanRetry.args = { + state: RoomStatusBarState.MessageRejected, + harms: [], +}; + +/** + * Rendered when a message was rejected by the server, and is being resent. + */ +export const WithMessageRejectedSending = Template.bind({}); +WithMessageRejectedSending.args = { + state: RoomStatusBarState.MessageRejected, + harms: [], + isResending: true, +}; + +/** + * Rendered when a message was rejected by the server, and we use the generic message. + */ +export const WithMessageRejectedWithKnownHarm = Template.bind({}); +WithMessageRejectedWithKnownHarm.args = { + state: RoomStatusBarState.MessageRejected, + harms: ["org.matrix.msc4387.spam"], +}; + +/** + * Rendered when a message was rejected by the server, and we use the generic message. + */ +export const WithMessageRejectedWithUnknownHarm = Template.bind({}); +WithMessageRejectedWithUnknownHarm.args = { + state: RoomStatusBarState.MessageRejected, + harms: ["any.old.harm"], +}; + +/** + * Rendered when a message was rejected by the server with a specific message. + */ +export const WithMessageRejectedWithServerMessage = Template.bind({}); +WithMessageRejectedWithServerMessage.args = { + state: RoomStatusBarState.MessageRejected, + harms: ["any.old.harm"], + serverError: "OurServer rejects this content", +}; diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx index 57bf028d24..71e2f52e06 100644 --- a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.tsx @@ -42,6 +42,7 @@ export enum RoomStatusBarState { ResourceLimited, UnsentMessages, LocalRoomFailed, + MessageRejected, } export interface RoomStatusBarNotVisible { @@ -71,13 +72,22 @@ export interface RoomStatusBarLocalRoomError { state: RoomStatusBarState.LocalRoomFailed; } +export interface RoomStatusBarMessageRejected { + state: RoomStatusBarState.MessageRejected; + canRetryInSeconds?: number; + isResending: boolean; + harms: string[]; + serverError?: string; +} + export type RoomStatusBarViewSnapshot = | RoomStatusBarNoConnection | RoomStatusBarConsentState | RoomStatusBarResourceLimitedState | RoomStatusBarUnsentMessagesState | RoomStatusBarLocalRoomError - | RoomStatusBarNotVisible; + | RoomStatusBarNotVisible + | RoomStatusBarMessageRejected; /** * The view model for the banner. @@ -91,6 +101,162 @@ interface RoomStatusBarViewProps { vm: RoomStatusBarViewModel; } +function translateHarmsToText(harms: string[], serverProvidedText?: string): string { + const { translate: _t } = useI18n(); + const translatedStrings = []; + for (const harmCategory of harms) { + switch (harmCategory) { + // case "m.spam" once the MSC passes. + case "org.matrix.msc4387.spam": + translatedStrings.push(_t("safety|harms|spam")); + break; + case "org.matrix.msc4387.spam.fraud": + translatedStrings.push(_t("safety|harms|spam.fraud")); + break; + case "org.matrix.msc4387.spam.impersonation": + translatedStrings.push(_t("safety|harms|spam.impersonation")); + break; + case "org.matrix.msc4387.spam.election_interference": + translatedStrings.push(_t("safety|harms|spam.election_interference")); + break; + case "org.matrix.msc4387.spam.flooding": + translatedStrings.push(_t("safety|harms|spam.flooding")); + break; + case "org.matrix.msc4387.adult": + translatedStrings.push(_t("safety|harms|spam.adult")); + break; + case "org.matrix.msc4387.harassment": + translatedStrings.push(_t("safety|harms|harassment")); + break; + case "org.matrix.msc4387.harassment.trolling": + translatedStrings.push(_t("safety|harms|harassment.trolling")); + break; + case "org.matrix.msc4387.harassment.targeted": + translatedStrings.push(_t("safety|harms|harassment.targeted")); + break; + case "org.matrix.msc4387.harassment.hate": + translatedStrings.push(_t("safety|harms|harassment.hate")); + break; + case "org.matrix.msc4387.harassment.doxxing": + translatedStrings.push(_t("safety|harms|harassment.doxxing")); + break; + case "org.matrix.msc4387.violence": + translatedStrings.push(_t("safety|harms|violence")); + break; + case "org.matrix.msc4387.child_safety": + translatedStrings.push(_t("safety|harms|child_safety")); + break; + case "org.matrix.msc4387.danger": + translatedStrings.push(_t("safety|harms|danger")); + break; + case "org.matrix.msc4387.tos": + translatedStrings.push(_t("safety|harms|tos")); + break; + case "org.matrix.msc4387.tos.hacking": + translatedStrings.push(_t("safety|harms|tos.hacking")); + break; + case "org.matrix.msc4387.tos.prohibited": + translatedStrings.push(_t("safety|harms|tos.prohibited")); + break; + case "org.matrix.msc4387.tos.ban_evasion": + translatedStrings.push(_t("safety|harms|tos.ban_evasion")); + break; + } + } + if (translatedStrings.length > 1) { + return _t("safety|harms|multiple"); + } else if (translatedStrings.length === 0) { + return serverProvidedText ?? _t("safety|harms|generic"); + } + return translatedStrings[0]; +} + +function RoomStatusBarViewMessageRejected({ + snapshot, + actions: { onDeleteAllClick, onResendAllClick }, +}: { + snapshot: RoomStatusBarMessageRejected; + actions: RoomStatusBarViewActions; +}): JSX.Element { + const { translate: _t } = useI18n(); + const bannerTitleId = useId(); + const deleteAllClick = useCallback>( + (ev) => { + ev.preventDefault(); + onDeleteAllClick?.(); + }, + [onDeleteAllClick], + ); + + const resendClick = useCallback>( + (ev) => { + ev.preventDefault(); + onResendAllClick?.(); + }, + [onResendAllClick], + ); + + let subtitleText: string; + if (onResendAllClick) { + subtitleText = _t("room|status_bar|select_messages_to_retry"); + } else if (!onResendAllClick && snapshot.canRetryInSeconds !== undefined) { + subtitleText = _t("room|status_bar|message_rejected|can_retry_in", { count: snapshot.canRetryInSeconds }); + } else { + subtitleText = _t("room|status_bar|message_rejected|cannot_retry"); + } + + return ( + + ) : ( + <> + {onDeleteAllClick && ( + + )} + {(onResendAllClick || snapshot.canRetryInSeconds) && ( + + )} + + ) + } + aria-labelledby={bannerTitleId} + > +
+ + {_t("room|status_bar|message_rejected|title", { + harm: translateHarmsToText(snapshot.harms, snapshot.serverError), + })} + + {subtitleText} +
+
+ ); +} + /** * A component to alert to a failure in the context of a room. * @@ -282,6 +448,8 @@ export function RoomStatusBarView({ vm }: Readonly): JSX ); + case RoomStatusBarState.MessageRejected: + return ; default: throw Error(`Unexpected unknown state for RoomStatusBar ${snapshot["state"]}`); }