mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 18:43:41 -05:00
Add moderation actions to user panel
This commit is contained in:
parent
05460e8130
commit
00c97fb5df
6 changed files with 177 additions and 4 deletions
|
@ -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
1
web/src/icons/gavel.svg
Normal 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 |
|
@ -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 {
|
div.errors {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import UserExtendedProfile from "./UserExtendedProfile.tsx"
|
||||||
import DeviceList from "./UserInfoDeviceList.tsx"
|
import DeviceList from "./UserInfoDeviceList.tsx"
|
||||||
import UserInfoError from "./UserInfoError.tsx"
|
import UserInfoError from "./UserInfoError.tsx"
|
||||||
import MutualRooms from "./UserInfoMutualRooms.tsx"
|
import MutualRooms from "./UserInfoMutualRooms.tsx"
|
||||||
|
import UserModeration from "./UserModeration.tsx"
|
||||||
|
|
||||||
interface UserInfoProps {
|
interface UserInfoProps {
|
||||||
userID: UserID
|
userID: UserID
|
||||||
|
@ -77,6 +78,8 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
</>}
|
</>}
|
||||||
<DeviceList client={client} room={roomCtx?.store} userID={userID}/>
|
<DeviceList client={client} room={roomCtx?.store} userID={userID}/>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
<UserModeration client={client} room={roomCtx?.store} member={memberEvt} userID={userID}/>
|
||||||
|
<hr/>
|
||||||
{errors?.length ? <>
|
{errors?.length ? <>
|
||||||
<UserInfoError errors={errors}/>
|
<UserInfoError errors={errors}/>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
142
web/src/ui/rightpanel/UserModeration.tsx
Normal file
142
web/src/ui/rightpanel/UserModeration.tsx
Normal 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
|
|
@ -20,7 +20,7 @@ import { ModalCloseContext } from "../../modal"
|
||||||
import TimelineEvent from "../TimelineEvent.tsx"
|
import TimelineEvent from "../TimelineEvent.tsx"
|
||||||
|
|
||||||
interface ConfirmWithMessageProps {
|
interface ConfirmWithMessageProps {
|
||||||
evt: MemDBEvent
|
evt?: MemDBEvent
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
placeholder: string
|
placeholder: string
|
||||||
|
@ -40,9 +40,9 @@ const ConfirmWithMessageModal = ({
|
||||||
}
|
}
|
||||||
return <form onSubmit={onConfirmWrapped}>
|
return <form onSubmit={onConfirmWrapped}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<div className="timeline-event-container">
|
{evt && <div className="timeline-event-container">
|
||||||
<TimelineEvent evt={evt} prevEvt={null} disableMenu={true} />
|
<TimelineEvent evt={evt} prevEvt={null} disableMenu={true} />
|
||||||
</div>
|
</div>}
|
||||||
<div className="confirm-description">
|
<div className="confirm-description">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue