Add moderation actions to user panel

This commit is contained in:
nexy7574 2025-01-12 20:52:56 +00:00
parent 05460e8130
commit 00c97fb5df
6 changed files with 177 additions and 4 deletions

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5E6267"><path d="M200-200v-80h560v80H200Zm14-160 266-400 266 400H214Zm266-80Zm-118 0h236L480-616 362-440Z"/></svg>

Before

Width:  |  Height:  |  Size: 213 B

1
web/src/icons/gavel.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5E6267"><path d="M160-120v-80h480v80H160Zm226-194L160-540l84-86 228 226-86 86Zm254-254L414-796l86-84 226 226-86 86Zm184 408L302-682l56-56 522 522-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 260 B

View file

@ -192,6 +192,34 @@ div.right-panel-content.user {
}
}
div.user-moderation {
display: flex;
flex-direction: column;
gap: .5rem;
div.moderation-actions {
.dangerous {
color: var(--error-color);
svg {
fill: var(--error-color)
}
}
.invite {
color: var(--primary-color);
svg {
fill: var(--primary-color)
}
}
}
button {
padding: .25rem 0;
width: 100%;
gap: .25rem;
justify-content: left;
}
}
div.errors {
display: flex;
flex-direction: column;

View file

@ -26,6 +26,7 @@ import UserExtendedProfile from "./UserExtendedProfile.tsx"
import DeviceList from "./UserInfoDeviceList.tsx"
import UserInfoError from "./UserInfoError.tsx"
import MutualRooms from "./UserInfoMutualRooms.tsx"
import UserModeration from "./UserModeration.tsx"
interface UserInfoProps {
userID: UserID
@ -77,6 +78,8 @@ const UserInfo = ({ userID }: UserInfoProps) => {
</>}
<DeviceList client={client} room={roomCtx?.store} userID={userID}/>
<hr/>
<UserModeration client={client} room={roomCtx?.store} member={memberEvt} userID={userID}/>
<hr/>
{errors?.length ? <>
<UserInfoError errors={errors}/>
<hr/>

View file

@ -0,0 +1,142 @@
// 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 <https://www.gnu.org/licenses/>.
import { use, useState } from "react"
import Client from "@/api/client.ts"
import { RoomStateStore } from "@/api/statestore"
import { MemDBEvent, MemberEventContent, Membership } from "@/api/types"
import { ModalContext } from "@/ui/modal"
import { RoomContext } from "@/ui/roomview/roomcontext.ts"
import ConfirmWithMessageModal from "@/ui/timeline/menu/ConfirmWithMessageModal.tsx"
import Gavel from "@/icons/gavel.svg?react"
import PersonAdd from "@/icons/person-add.svg?react"
import PersonRemove from "@/icons/person-remove.svg?react"
interface UserModerationProps {
userID: string;
client: Client;
room?: RoomStateStore;
member: MemDBEvent | null;
}
const UserModeration = ({ userID, client, member }: UserModerationProps) => {
const [actionInProgress, setActionInProgress] = useState(false)
const roomCtx = use(RoomContext)
const openModal = use(ModalContext)
const runAction = (mode: Membership) => {
const callback = (reason: string) => {
if (!roomCtx?.store.roomID) {
console.error("Cannot action user without a room")
return
}
const payload: MemberEventContent = {
membership: mode,
}
if (reason) {
payload["reason"] = reason
}
setActionInProgress(true)
client.rpc
.setState(roomCtx?.store.roomID, "m.room.member", userID, payload)
.then(() => {
console.debug("Actioned", userID)
setActionInProgress(false)
})
.catch((e) => {
console.error("Failed to action", e)
setActionInProgress(false)
})
}
return () => {
openModal({
dimmed: true,
boxed: true,
innerBoxClass: "confirm-message-modal",
content: (
<RoomContext value={roomCtx}>
<ConfirmWithMessageModal
title={`${mode.charAt(0).toUpperCase() + mode.slice(1)} User`}
description={`Are you sure you want to ${mode} this user?`}
placeholder="Optional reason"
confirmButton={mode.charAt(0).toUpperCase() + mode.slice(1)}
onConfirm={callback}
/>
</RoomContext>
),
})
}
}
const membership = member?.content.membership || "leave"
return (
<div className="user-moderation">
<h4>Moderation</h4>
<div className="moderation-actions">
{(["knock", "leave"].includes(membership) || !member) && (
<button
className="moderation-action invite"
onClick={runAction("invite")}
disabled={actionInProgress}
>
<PersonAdd />
<span>Invite</span>
</button>
)}
{["knock", "invite"].includes(membership) && (
<button
className="moderation-action dangerous"
onClick={runAction("leave")}
disabled={actionInProgress}
>
<PersonRemove />
<span>{membership === "invite" ? "Revoke invitation" : "Reject join request"}</span>
</button>
)}
{membership === "join" && (
<button
className="moderation-action dangerous"
onClick={runAction("leave")}
disabled={actionInProgress}
>
<PersonRemove />
<span>Kick</span>
</button>
)}
{membership !== "ban" && (
<button
className="moderation-action dangerous"
onClick={runAction("ban")}
disabled={actionInProgress}
>
<Gavel />
<span>Ban</span>
</button>
)}
{membership === "ban" && (
<button
className="moderation-action invite"
onClick={runAction("leave")}
disabled={actionInProgress}
>
<Gavel />
<span>Unban</span>
</button>
)}
</div>
</div>
)
}
export default UserModeration

View file

@ -20,7 +20,7 @@ import { ModalCloseContext } from "../../modal"
import TimelineEvent from "../TimelineEvent.tsx"
interface ConfirmWithMessageProps {
evt: MemDBEvent
evt?: MemDBEvent
title: string
description: string
placeholder: string
@ -40,9 +40,9 @@ const ConfirmWithMessageModal = ({
}
return <form onSubmit={onConfirmWrapped}>
<h3>{title}</h3>
<div className="timeline-event-container">
{evt && <div className="timeline-event-container">
<TimelineEvent evt={evt} prevEvt={null} disableMenu={true} />
</div>
</div>}
<div className="confirm-description">
{description}
</div>