From f2f89b728f486c7bf83a84342248d8655a92a473 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 21 Oct 2024 20:52:59 +0300 Subject: [PATCH] web/timeline: add error boundary for event content rendering --- web/src/ui/timeline/ReplyBody.tsx | 6 ++- web/src/ui/timeline/TimelineEvent.css | 5 +++ web/src/ui/timeline/TimelineEvent.tsx | 6 ++- .../timeline/content/ContentErrorBoundary.tsx | 40 +++++++++++++++++++ web/src/ui/timeline/content/index.ts | 1 + 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 web/src/ui/timeline/content/ContentErrorBoundary.tsx diff --git a/web/src/ui/timeline/ReplyBody.tsx b/web/src/ui/timeline/ReplyBody.tsx index 8fb4426..455b635 100644 --- a/web/src/ui/timeline/ReplyBody.tsx +++ b/web/src/ui/timeline/ReplyBody.tsx @@ -18,7 +18,7 @@ import { getAvatarURL } from "@/api/media.ts" import { RoomStateStore, useRoomEvent } from "@/api/statestore" import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types" import { ClientContext } from "../ClientContext.ts" -import getBodyType from "./content" +import getBodyType, { ContentErrorBoundary } from "./content" import CloseButton from "@/icons/close.svg?react" import "./ReplyBody.css" @@ -83,6 +83,8 @@ export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => { {memberEvtContent?.displayname || event.sender} {onClose && } - + + + } diff --git a/web/src/ui/timeline/TimelineEvent.css b/web/src/ui/timeline/TimelineEvent.css index 339e19b..8101d49 100644 --- a/web/src/ui/timeline/TimelineEvent.css +++ b/web/src/ui/timeline/TimelineEvent.css @@ -150,6 +150,11 @@ div.date-separator { } } +div.render-error-body { + font-style: italic; + color: #555; +} + div.decryption-error-body { display: flex; align-items: center; diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index abd4836..401ae4e 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -21,7 +21,7 @@ import { isEventID } from "@/util/validation.ts" import { ClientContext } from "../ClientContext.ts" import { LightboxContext } from "../Lightbox.tsx" import { ReplyIDBody } from "./ReplyBody.tsx" -import getBodyType, { EventContentProps, HiddenEvent, MemberBody } from "./content" +import getBodyType, { ContentErrorBoundary, EventContentProps, HiddenEvent, MemberBody } from "./content" import ErrorIcon from "../../icons/error.svg?react" import PendingIcon from "../../icons/pending.svg?react" import SentIcon from "../../icons/sent.svg?react" @@ -111,7 +111,9 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyToRef }: TimelineEventProps
{isEventID(replyTo) && BodyType !== HiddenEvent ? : null} - + + + {evt.reactions ? : null}
{evt.sender === client.userID && evt.transaction_id ? : null} diff --git a/web/src/ui/timeline/content/ContentErrorBoundary.tsx b/web/src/ui/timeline/content/ContentErrorBoundary.tsx new file mode 100644 index 0000000..7265c81 --- /dev/null +++ b/web/src/ui/timeline/content/ContentErrorBoundary.tsx @@ -0,0 +1,40 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" + +export default class ContentErrorBoundary extends React.Component<{ children: React.ReactNode }, { error?: Error }> { + constructor(props: { children: React.ReactNode }) { + super(props) + this.state = { error: undefined } + } + + static getDerivedStateFromError(error: unknown) { + if (error instanceof Error) { + error = new Error(`${error}`) + } + return { error } + } + + render() { + if (this.state.error) { + return
+ Failed to render event: {this.state.error.message.replace(/^Error: /, "")} +
+ } + + return this.props.children + } +} diff --git a/web/src/ui/timeline/content/index.ts b/web/src/ui/timeline/content/index.ts index 0eca18c..58d49c6 100644 --- a/web/src/ui/timeline/content/index.ts +++ b/web/src/ui/timeline/content/index.ts @@ -9,6 +9,7 @@ import TextMessageBody from "./TextMessageBody.tsx" import UnknownMessageBody from "./UnknownMessageBody.tsx" import EventContentProps from "./props.ts" +export { default as ContentErrorBoundary } from "./ContentErrorBoundary.tsx" export { default as EncryptedBody } from "./EncryptedBody.tsx" export { default as HiddenEvent } from "./HiddenEvent.tsx" export { default as MediaMessageBody } from "./MediaMessageBody.tsx"