diff --git a/web/src/ui/JSONView.css b/web/src/ui/JSONView.css
new file mode 100644
index 0000000..0cb4724
--- /dev/null
+++ b/web/src/ui/JSONView.css
@@ -0,0 +1,36 @@
+pre.json-view {
+ white-space: wrap;
+ overflow-wrap: anywhere;
+ margin: 0;
+
+ ul, ol {
+ margin: 0;
+
+ > li {
+ list-style-type: none;
+ }
+ }
+
+ span.json-collapsed {
+ user-select: none;
+ }
+
+ button {
+ padding: 0 .25rem;
+ }
+
+ /* If the screen is wide enough, make line-wrapped strings aligned after the object key */
+ @media screen and (min-width: 800px) {
+ li.json-object-entry:has(> span.json-comma-container > span.json-string) {
+ display: flex;
+
+ span.json-object-key {
+ white-space: nowrap;
+ }
+
+ span.json-object-entry-colon {
+ white-space: pre;
+ }
+ }
+ }
+}
diff --git a/web/src/ui/JSONView.tsx b/web/src/ui/JSONView.tsx
new file mode 100644
index 0000000..8bd165a
--- /dev/null
+++ b/web/src/ui/JSONView.tsx
@@ -0,0 +1,120 @@
+// 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 { useReducer } from "react"
+import "./JSONView.css"
+
+interface JSONViewProps {
+ data: unknown
+}
+
+interface JSONViewPropsWithKey extends JSONViewProps {
+ objectKey?: string
+ trailingComma?: boolean
+ noCollapse?: boolean
+}
+
+function renderJSONString(data: string, styleClass: string = "s2") {
+ return {JSON.stringify(data)}
+}
+
+function renderJSONValue(data: unknown, collapsed: boolean) {
+ switch (typeof data) {
+ case "object":
+ if (data === null) {
+ return null
+ } else if (Array.isArray(data)) {
+ if (data.length === 0) {
+ return null
+ } else if (collapsed) {
+ return …
+ }
+ return
+}
diff --git a/web/src/ui/timeline/menu/ViewSourceModal.tsx b/web/src/ui/timeline/menu/ViewSourceModal.tsx
index fd20fbc..69da420 100644
--- a/web/src/ui/timeline/menu/ViewSourceModal.tsx
+++ b/web/src/ui/timeline/menu/ViewSourceModal.tsx
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import { MemDBEvent } from "@/api/types"
+import JSONView from "../../JSONView.tsx"
interface ViewSourceModalProps {
evt: MemDBEvent
@@ -21,9 +22,7 @@ interface ViewSourceModalProps {
const ViewSourceModal = ({ evt }: ViewSourceModalProps) => {
return