{renderRightPanelContent(props)}
diff --git a/web/src/ui/settings/SettingsView.css b/web/src/ui/settings/SettingsView.css
index 1cda143..b680e22 100644
--- a/web/src/ui/settings/SettingsView.css
+++ b/web/src/ui/settings/SettingsView.css
@@ -88,13 +88,17 @@ div.settings-view {
}
}
- button.logout {
- margin-top: 1rem;
+ > div.misc-buttons > button {
padding: .5rem 1rem;
+ display: block;
- &:hover, &:focus {
- background-color: var(--error-color);
- color: var(--inverted-text-color);
+ &.logout {
+ margin-top: 2rem;
+
+ &:hover, &:focus {
+ background-color: var(--error-color);
+ color: var(--inverted-text-color);
+ }
}
}
}
diff --git a/web/src/ui/settings/SettingsView.tsx b/web/src/ui/settings/SettingsView.tsx
index 131a542..99a8b3e 100644
--- a/web/src/ui/settings/SettingsView.tsx
+++ b/web/src/ui/settings/SettingsView.tsx
@@ -357,7 +357,13 @@ const SettingsView = ({ room }: SettingsViewProps) => {
-
+
+ {window.Notification && }
+
+
+
>
}
diff --git a/web/src/ui/timeline/content/TextMessageBody.tsx b/web/src/ui/timeline/content/TextMessageBody.tsx
index 90655bc..e8b0a79 100644
--- a/web/src/ui/timeline/content/TextMessageBody.tsx
+++ b/web/src/ui/timeline/content/TextMessageBody.tsx
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
.
import { MessageEventContent } from "@/api/types"
-import { getDisplayname } from "@/util/validation.ts"
+import { getDisplayname, parseMatrixURI } from "@/util/validation.ts"
import EventContentProps from "./props.ts"
function isImageElement(elem: EventTarget): elem is HTMLImageElement {
@@ -26,21 +26,19 @@ function isAnchorElement(elem: EventTarget): elem is HTMLAnchorElement {
}
function onClickMatrixURI(href: string) {
- const url = new URL(href)
- const pathParts = url.pathname.split("/")
- const decodedPart = decodeURIComponent(pathParts[1])
- switch (pathParts[0]) {
- case "u":
+ const uri = parseMatrixURI(href)
+ switch (uri?.identifier[0]) {
+ case "@":
return window.mainScreenContext.setRightPanel({
type: "user",
- userID: `@${decodedPart}`,
+ userID: uri.identifier,
})
- case "roomid":
- return window.mainScreenContext.setActiveRoom(`!${decodedPart}`)
- case "r":
- return window.client.rpc.resolveAlias(`#${decodedPart}`).then(
+ case "!":
+ return window.mainScreenContext.setActiveRoom(uri.identifier)
+ case "#":
+ return window.client.rpc.resolveAlias(uri.identifier).then(
res => window.mainScreenContext.setActiveRoom(res.room_id),
- err => window.alert(`Failed to resolve room alias #${decodedPart}: ${err}`),
+ err => window.alert(`Failed to resolve room alias ${uri.identifier}: ${err}`),
)
}
}
diff --git a/web/src/util/validation.ts b/web/src/util/validation.ts
index 0eca5fd..8554282 100644
--- a/web/src/util/validation.ts
+++ b/web/src/util/validation.ts
@@ -39,6 +39,44 @@ export const isRoomID = (roomID: unknown) => isIdentifier
(roomID, "!", t
export const isRoomAlias = (roomAlias: unknown) => isIdentifier(roomAlias, "#", true)
export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc)
+export interface ParsedMatrixURI {
+ identifier: UserID | RoomID | RoomAlias
+ eventID?: EventID
+ params: URLSearchParams
+}
+
+export function parseMatrixURI(uri: unknown): ParsedMatrixURI | undefined {
+ if (typeof uri !== "string") {
+ return
+ }
+ let parsed: URL
+ try {
+ parsed = new URL(uri)
+ } catch {
+ return
+ }
+ if (parsed.protocol !== "matrix:") {
+ return
+ }
+ const [type, ident1, subtype, ident2] = parsed.pathname.split("/")
+ const output: Partial = {
+ params: parsed.searchParams,
+ }
+ if (type === "u") {
+ output.identifier = `@${ident1}`
+ } else if (type === "r") {
+ output.identifier = `#${ident1}`
+ } else if (type === "roomid") {
+ output.identifier = `!${ident1}`
+ if (subtype === "e") {
+ output.eventID = `$${ident2}`
+ }
+ } else {
+ return
+ }
+ return output as ParsedMatrixURI
+}
+
export function getLocalpart(userID: UserID): string {
const idx = userID.indexOf(":")
return idx > 0 ? userID.slice(1, idx) : userID.slice(1)