diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 86052398..77f036a0 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -111,6 +111,12 @@ export interface ACLEventContent { deny?: string[] } +export interface PolicyRuleContent { + entity: string + reason: string + recommendation: string +} + export interface PowerLevelEventContent { users?: Record users_default?: number diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index e3220a64..13edde4d 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -217,6 +217,7 @@ class ContextFields implements MainScreenContextFields { } clickRightPanelOpener = (evt: React.MouseEvent) => { + evt.preventDefault() const type = evt.currentTarget.getAttribute("data-target-panel") if (type === "pinned-messages" || type === "members") { this.setRightPanel({ type }) diff --git a/web/src/ui/timeline/content/PolicyRuleBody.tsx b/web/src/ui/timeline/content/PolicyRuleBody.tsx new file mode 100644 index 00000000..8da22178 --- /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/index.ts b/web/src/ui/timeline/content/index.ts index 108d44a0..b3c77f7b 100644 --- a/web/src/ui/timeline/content/index.ts +++ b/web/src/ui/timeline/content/index.ts @@ -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" @@ -82,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": @@ -97,6 +105,7 @@ export function isSmallEvent(bodyType: React.FunctionComponent