diff --git a/web/src/api/media.ts b/web/src/api/media.ts
index 0de06ad..933c654 100644
--- a/web/src/api/media.ts
+++ b/web/src/api/media.ts
@@ -13,20 +13,15 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-
+import { parseMXC } from "@/util/validation.ts"
import { UserID } from "./types"
-const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
-
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
- if (!mxc) {
+ const [server, mediaID] = parseMXC(mxc)
+ if (!mediaID) {
return undefined
}
- const match = mxc.match(mediaRegex)
- if (!match) {
- return undefined
- }
- return `_gomuks/media/${match[1]}/${match[2]}?encrypted=${encrypted}`
+ return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}`
}
export const getEncryptedMediaURL = (mxc?: string): string | undefined => {
@@ -34,15 +29,11 @@ export const getEncryptedMediaURL = (mxc?: string): string | undefined => {
}
export const getAvatarURL = (_userID: UserID, mxc?: string): string | undefined => {
- if (!mxc) {
+ const [server, mediaID] = parseMXC(mxc)
+ if (!mediaID) {
return undefined
// return `_gomuks/avatar/${encodeURIComponent(userID)}`
}
- const match = mxc.match(mediaRegex)
- if (!match) {
- return undefined
- // return `_gomuks/avatar/${encodeURIComponent(userID)}`
- }
- return `_gomuks/media/${match[1]}/${match[2]}`
- // return `_gomuks/avatar/${encodeURIComponent(userID)}/${match[1]}/${match[2]}`
+ return `_gomuks/media/${server}/${mediaID}`
+ // return `_gomuks/avatar/${encodeURIComponent(userID)}/${server}/${mediaID}`
}
diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx
index 33ba74f..d608467 100644
--- a/web/src/ui/timeline/TimelineEvent.tsx
+++ b/web/src/ui/timeline/TimelineEvent.tsx
@@ -17,6 +17,7 @@ import React, { use, useCallback } from "react"
import { getAvatarURL } from "@/api/media.ts"
import { RoomStateStore } from "@/api/statestore"
import { MemDBEvent, MemberEventContent } from "@/api/types"
+import { isEventID } from "@/util/validation.ts"
import { ClientContext } from "../ClientContext.ts"
import { LightboxContext } from "../Lightbox.tsx"
import { ReplyIDBody } from "./ReplyBody.tsx"
@@ -152,8 +153,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
{shortTime}
- {typeof replyTo === "string" && BodyType !== HiddenEvent
- ? : null}
+ {isEventID(replyTo) && BodyType !== HiddenEvent ? : null}
{evt.reactions ? : null}
diff --git a/web/src/util/validation.ts b/web/src/util/validation.ts
new file mode 100644
index 0000000..5917575
--- /dev/null
+++ b/web/src/util/validation.ts
@@ -0,0 +1,51 @@
+// 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 { ContentURI, EventID, RoomAlias, RoomID, UserID } from "@/api/types"
+
+const simpleHomeserverRegex = /^[a-zA-Z0-9.:-]+$/
+const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
+
+function isIdentifier(identifier: unknown, sigil: string, requiresServer: boolean): identifier is T {
+ if (typeof identifier !== "string" || !identifier.startsWith(sigil)) {
+ return false
+ }
+ if (requiresServer) {
+ const idx = identifier.indexOf(":")
+ return idx > 0 && simpleHomeserverRegex.test(identifier.slice(idx+1))
+ }
+ return true
+}
+
+export function validated(value: T | undefined, validator: (value: T) => boolean): value is T {
+ return value !== undefined && validator(value)
+}
+
+export const isEventID = (eventID: unknown) => isIdentifier(eventID, "$", false)
+export const isUserID = (userID: unknown) => isIdentifier(userID, "@", true)
+export const isRoomID = (roomID: unknown) => isIdentifier(roomID, "!", true)
+export const isRoomAlias = (roomAlias: unknown) => isIdentifier(roomAlias, "#", true)
+export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc)
+
+export function parseMXC(mxc: unknown): [string, string] | [] {
+ if (typeof mxc !== "string") {
+ return []
+ }
+ const match = mxc.match(mediaRegex)
+ if (!match) {
+ return []
+ }
+ return [match[1], match[2]]
+}