{(blurhash && containerStyle.width) ?
const targetElem = <>
- {content.avatar_url && targetAvatar}
-
{content.displayname ?? target}
+ {content.avatar_url && targetAvatar}
+ {content.displayname ?? target}
+
>
if (content.membership === prevContent?.membership) {
if (content.displayname !== prevContent.displayname) {
@@ -43,10 +44,9 @@ function useChangeDescription(
return <>set their displayname to
{content.displayname}>
}
return <>
- changed their displayname from
-
{prevContent.displayname}
- to
-
{content.displayname}
+ changed their displayname from
+ {prevContent.displayname}
+ to
{content.displayname}
>
} else if (content.avatar_url !== prevContent.avatar_url) {
if (!content.avatar_url) {
@@ -95,11 +95,12 @@ const MemberBody = ({ event, sender }: EventContentProps) => {
const content = event.content as MemberEventContent
const prevContent = event.unsigned.prev_content as MemberEventContent | undefined
return
- {sender?.content.displayname ?? event.sender}
-
+
+ {sender?.content.displayname ?? event.sender}
+
{useChangeDescription(event.sender, event.state_key as UserID, content, prevContent)}
- {content.reason ? for {content.reason} : null}
+ {content.reason ? for {content.reason} : null}
}
diff --git a/web/src/ui/timeline/content/PolicyRuleBody.tsx b/web/src/ui/timeline/content/PolicyRuleBody.tsx
new file mode 100644
index 0000000..8da2217
--- /dev/null
+++ b/web/src/ui/timeline/content/PolicyRuleBody.tsx
@@ -0,0 +1,61 @@
+// 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 { JSX, use } from "react"
+import { PolicyRuleContent } from "@/api/types"
+import { getDisplayname } from "@/util/validation.ts"
+import MainScreenContext from "../../MainScreenContext.ts"
+import EventContentProps from "./props.ts"
+
+const PolicyRuleBody = ({ event, sender }: EventContentProps) => {
+ const content = event.content as PolicyRuleContent
+ const prevContent = event.unsigned.prev_content as PolicyRuleContent | undefined
+ const mainScreen = use(MainScreenContext)
+
+ const entity = content.entity ?? prevContent?.entity
+ const recommendation = content.recommendation ?? prevContent?.recommendation
+ if (!entity || !recommendation) {
+ return
+ {getDisplayname(event.sender, sender?.content)} sent an invalid policy rule
+
+ }
+ let entityElement = <>{entity}>
+ if(event.type === "m.policy.rule.user" && !entity?.includes("*") && !entity?.includes("?")) {
+ entityElement = (
+
+ {entity}
+
+ )
+ }
+ let recommendationElement: JSX.Element | string =
{recommendation}
+ if (recommendation === "m.ban") {
+ recommendationElement = "ban"
+ }
+ const action = prevContent ? ((content.entity && content.recommendation) ? "updated" : "removed") : "added"
+ const target = event.type.replace(/^m\.policy\.rule\./, "")
+ return
+ {getDisplayname(event.sender, sender?.content)} {action} a {recommendationElement} rule
+ for {target}s matching {entityElement}
+ {content.reason ? <> for {content.reason}
> : null}
+
+}
+
+export default PolicyRuleBody
diff --git a/web/src/ui/timeline/content/PowerLevelBody.tsx b/web/src/ui/timeline/content/PowerLevelBody.tsx
index 3598075..88e6c46 100644
--- a/web/src/ui/timeline/content/PowerLevelBody.tsx
+++ b/web/src/ui/timeline/content/PowerLevelBody.tsx
@@ -34,7 +34,7 @@ function renderPowerLevels(content: PowerLevelEventContent, prevContent?: PowerL
intDiff`the ban power level from ${prevContent?.ban ?? 50} to ${content.ban ?? 50}`,
intDiff`the kick power level from ${prevContent?.kick ?? 50} to ${content.kick ?? 50}`,
intDiff`the redact power level from ${prevContent?.redact ?? 50} to ${content.redact ?? 50}`,
- intDiff`the invite power level from ${prevContent?.redact ?? 0} to ${content.redact ?? 0}`,
+ intDiff`the invite power level from ${prevContent?.invite ?? 0} to ${content.invite ?? 0}`,
intDiff`the @room notification power level from ${prevContent?.notifications?.room ?? 50} to ${content.notifications?.room ?? 50}`,
]
/* eslint-enable max-len */
diff --git a/web/src/ui/timeline/content/RoomAvatarBody.tsx b/web/src/ui/timeline/content/RoomAvatarBody.tsx
index d996bba..7878bbb 100644
--- a/web/src/ui/timeline/content/RoomAvatarBody.tsx
+++ b/web/src/ui/timeline/content/RoomAvatarBody.tsx
@@ -17,7 +17,7 @@ import { JSX, use } from "react"
import { getRoomAvatarURL } from "@/api/media.ts"
import { ContentURI, RoomAvatarEventContent } from "@/api/types"
import { ensureString } from "@/util/validation.ts"
-import { LightboxContext } from "../../modal/Lightbox.tsx"
+import { LightboxContext } from "../../modal"
import EventContentProps from "./props.ts"
const RoomAvatarBody = ({ event, sender, room }: EventContentProps) => {
diff --git a/web/src/ui/timeline/content/TextMessageBody.tsx b/web/src/ui/timeline/content/TextMessageBody.tsx
index e8b0a79..03cfdd2 100644
--- a/web/src/ui/timeline/content/TextMessageBody.tsx
+++ b/web/src/ui/timeline/content/TextMessageBody.tsx
@@ -34,10 +34,15 @@ function onClickMatrixURI(href: string) {
userID: uri.identifier,
})
case "!":
- return window.mainScreenContext.setActiveRoom(uri.identifier)
+ return window.mainScreenContext.setActiveRoom(uri.identifier, {
+ via: uri.params.getAll("via"),
+ })
case "#":
return window.client.rpc.resolveAlias(uri.identifier).then(
- res => window.mainScreenContext.setActiveRoom(res.room_id),
+ res => window.mainScreenContext.setActiveRoom(res.room_id, {
+ alias: uri.identifier,
+ via: res.servers.slice(0, 3),
+ }),
err => window.alert(`Failed to resolve room alias ${uri.identifier}: ${err}`),
)
}
diff --git a/web/src/ui/timeline/content/index.css b/web/src/ui/timeline/content/index.css
index 7d0de13..bac8ff7 100644
--- a/web/src/ui/timeline/content/index.css
+++ b/web/src/ui/timeline/content/index.css
@@ -24,23 +24,13 @@ div.redacted-body, div.decryption-pending-body {
}
div.member-body {
- display: flex;
- align-items: center;
- gap: .25rem;
-
- span.name {
+ span.name, span.reason {
unicode-bidi: isolate;
- max-width: 40ch;
- text-wrap: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
}
- > span.change-description {
- display: flex;
- align-items: center;
- gap: .25rem;
- text-wrap: nowrap;
+ img {
+ /* Hacky vertical align with text. Can't use flex because it breaks line wrapping */
+ margin-bottom: -.125rem;
}
}
@@ -225,6 +215,10 @@ div.media-container {
border-radius: .25rem;
}
+ &:has(> div.empty-placeholder) + img {
+ filter: blur(16px);
+ }
+
& + img {
/* In order loading=lazy to work, the image has to be visible,
so put it behind the placeholder instead of below */
diff --git a/web/src/ui/timeline/content/index.ts b/web/src/ui/timeline/content/index.ts
index 49fb3b2..7bd981b 100644
--- a/web/src/ui/timeline/content/index.ts
+++ b/web/src/ui/timeline/content/index.ts
@@ -1,5 +1,5 @@
import React from "react"
-import { MemDBEvent } from "@/api/types"
+import { BeeperPerMessageProfile, MemDBEvent, MessageEventContent } from "@/api/types"
import ACLBody from "./ACLBody.tsx"
import EncryptedBody from "./EncryptedBody.tsx"
import HiddenEvent from "./HiddenEvent.tsx"
@@ -7,6 +7,7 @@ import LocationMessageBody from "./LocationMessageBody.tsx"
import MediaMessageBody from "./MediaMessageBody.tsx"
import MemberBody from "./MemberBody.tsx"
import PinnedEventsBody from "./PinnedEventsBody.tsx"
+import PolicyRuleBody from "./PolicyRuleBody.tsx"
import PowerLevelBody from "./PowerLevelBody.tsx"
import RedactedBody from "./RedactedBody.tsx"
import RoomAvatarBody from "./RoomAvatarBody.tsx"
@@ -24,6 +25,7 @@ export { default as MediaMessageBody } from "./MediaMessageBody.tsx"
export { default as LocationMessageBody } from "./LocationMessageBody.tsx"
export { default as MemberBody } from "./MemberBody.tsx"
export { default as PinnedEventsBody } from "./PinnedEventsBody.tsx"
+export { default as PolicyRuleBody } from "./PolicyRuleBody.tsx"
export { default as PowerLevelBody } from "./PowerLevelBody.tsx"
export { default as RedactedBody } from "./RedactedBody.tsx"
export { default as RoomAvatarBody } from "./RoomAvatarBody.tsx"
@@ -55,6 +57,9 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo
}
return MediaMessageBody
case "m.location":
+ if (forReply) {
+ return TextMessageBody
+ }
return LocationMessageBody
default:
return UnknownMessageBody
@@ -79,6 +84,12 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo
return RoomAvatarBody
case "m.room.server_acl":
return ACLBody
+ case "m.policy.rule.user":
+ return PolicyRuleBody
+ case "m.policy.rule.room":
+ return PolicyRuleBody
+ case "m.policy.rule.server":
+ return PolicyRuleBody
case "m.room.pinned_events":
return PinnedEventsBody
case "m.room.power_levels":
@@ -94,6 +105,7 @@ export function isSmallEvent(bodyType: React.FunctionComponent
.
-import React, { use, useCallback, useState } from "react"
+import React, { use, useState } from "react"
import { MemDBEvent } from "@/api/types"
-import useEvent from "@/util/useEvent.ts"
-import { ModalCloseContext } from "../../modal/Modal.tsx"
+import { isMobileDevice } from "@/util/ismobile.ts"
+import { ModalCloseContext } from "../../modal"
import TimelineEvent from "../TimelineEvent.tsx"
interface ConfirmWithMessageProps {
@@ -33,14 +33,11 @@ const ConfirmWithMessageModal = ({
}: ConfirmWithMessageProps) => {
const [reason, setReason] = useState("")
const closeModal = use(ModalCloseContext)
- const onConfirmWrapped = useEvent((evt: React.FormEvent) => {
+ const onConfirmWrapped = (evt: React.FormEvent) => {
evt.preventDefault()
closeModal()
onConfirm(reason)
- })
- const onChangeReason = useCallback((evt: React.ChangeEvent
) => {
- setReason(evt.target.value)
- }, [])
+ }
return