Implement user ignores

This commit is contained in:
nexy7574 2025-01-19 21:43:20 +00:00
parent bef3e43de8
commit 6f177e0524
No known key found for this signature in database
5 changed files with 83 additions and 13 deletions

View file

@ -66,6 +66,15 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
return unmarshalAndCall(req.Data, func(params *sendStateEventParams) (id.EventID, error) { return unmarshalAndCall(req.Data, func(params *sendStateEventParams) (id.EventID, error) {
return h.SetState(ctx, params.RoomID, params.EventType, params.StateKey, params.Content) return h.SetState(ctx, params.RoomID, params.EventType, params.StateKey, params.Content)
}) })
case "get_account_data":
return unmarshalAndCall(req.Data, func(params *getAccountDataParams) (*map[string]any, error) {
var result map[string]any
if params.RoomID != "" {
return &result, h.Client.GetRoomAccountData(ctx, params.RoomID, params.Type, &result)
} else {
return &result, h.Client.GetAccountData(ctx, params.Type, &result)
}
})
case "set_account_data": case "set_account_data":
return unmarshalAndCall(req.Data, func(params *setAccountDataParams) (bool, error) { return unmarshalAndCall(req.Data, func(params *setAccountDataParams) (bool, error) {
if params.RoomID != "" { if params.RoomID != "" {
@ -262,6 +271,11 @@ type sendStateEventParams struct {
Content json.RawMessage `json:"content"` Content json.RawMessage `json:"content"`
} }
type getAccountDataParams struct {
RoomID id.RoomID `json:"room_id,omitempty"`
Type string `json:"type"`
}
type setAccountDataParams struct { type setAccountDataParams struct {
RoomID id.RoomID `json:"room_id,omitempty"` RoomID id.RoomID `json:"room_id,omitempty"`
Type string `json:"type"` Type string `json:"type"`

View file

@ -167,6 +167,10 @@ export default abstract class RPCClient {
return this.request("set_state", { room_id, type, state_key, content }) return this.request("set_state", { room_id, type, state_key, content })
} }
getAccountData(type: EventType, roomID?: RoomID): Promise<unknown> {
return this.request("get_account_data", { type, roomID })
}
setAccountData(type: EventType, content: unknown, room_id?: RoomID): Promise<boolean> { setAccountData(type: EventType, content: unknown, room_id?: RoomID): Promise<boolean> {
return this.request("set_account_data", { type, content, room_id }) return this.request("set_account_data", { type, content, room_id })
} }

1
web/src/icons/block.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="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q54 0 104-17.5t92-50.5L228-676q-33 42-50.5 92T160-480q0 134 93 227t227 93Zm252-124q33-42 50.5-92T800-480q0-134-93-227t-227-93q-54 0-104 17.5T284-732l448 448Z"/></svg>

After

Width:  |  Height:  |  Size: 477 B

View file

@ -205,7 +205,7 @@ div.right-panel-content.user {
fill: var(--error-color) fill: var(--error-color)
} }
} }
.invite { .positive {
color: var(--primary-color); color: var(--primary-color);
svg { svg {
fill: var(--primary-color) fill: var(--primary-color)

View file

@ -13,7 +13,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use } from "react" import { use, useEffect, useState } from "react"
import Client from "@/api/client.ts" import Client from "@/api/client.ts"
import { RoomStateStore } from "@/api/statestore" import { RoomStateStore } from "@/api/statestore"
import { MemDBEvent, MemberEventContent, Membership } from "@/api/types" import { MemDBEvent, MemberEventContent, Membership } from "@/api/types"
@ -21,6 +21,7 @@ import { ModalContext } from "@/ui/modal"
import { RoomContext } from "@/ui/roomview/roomcontext.ts" import { RoomContext } from "@/ui/roomview/roomcontext.ts"
import ConfirmWithMessageModal from "@/ui/timeline/menu/ConfirmWithMessageModal.tsx" import ConfirmWithMessageModal from "@/ui/timeline/menu/ConfirmWithMessageModal.tsx"
import { getPowerLevels } from "@/ui/timeline/menu/util.ts" import { getPowerLevels } from "@/ui/timeline/menu/util.ts"
import Block from "@/icons/block.svg?react"
import Gavel from "@/icons/gavel.svg?react" import Gavel from "@/icons/gavel.svg?react"
import PersonAdd from "@/icons/person-add.svg?react" import PersonAdd from "@/icons/person-add.svg?react"
import PersonRemove from "@/icons/person-remove.svg?react" import PersonRemove from "@/icons/person-remove.svg?react"
@ -32,13 +33,62 @@ interface UserModerationProps {
member: MemDBEvent | null; member: MemDBEvent | null;
} }
interface IgnoredUsersType {
ignored_users: Record<string, object>;
}
const UserIgnoreButton = ({ userID, client }: { userID: string; client: Client }) => {
const [ignoredUsers, setIgnoredUsers] = useState<IgnoredUsersType | null>(null)
useEffect(() => {
// Get blocked user list
client.rpc.getAccountData("m.ignored_user_list").then((data) => {
const parsedData = data as IgnoredUsersType
if (data !== ignoredUsers || !("ignored_users" in parsedData)) {
return
}
setIgnoredUsers(parsedData)
}).catch((e) => {
console.error("Failed to get ignored users", e)
})
})
const isIgnored = ignoredUsers?.ignored_users[userID]
const ignoreUser = () => {
const newIgnoredUsers = { ...(ignoredUsers || { ignored_users: {}}) }
newIgnoredUsers.ignored_users[userID] = {}
client.rpc.setAccountData("m.ignored_user_list", newIgnoredUsers).then(() => {
setIgnoredUsers(newIgnoredUsers)
}).catch((e) => {
console.error("Failed to ignore user", e)
})
}
const unignoreUser = () => {
const newIgnoredUsers = { ...(ignoredUsers || { ignored_users: {}}) }
delete newIgnoredUsers.ignored_users[userID]
client.rpc.setAccountData("m.ignored_user_list", newIgnoredUsers).then(() => {
setIgnoredUsers(newIgnoredUsers)
}).catch((e) => {
console.error("Failed to unignore user", e)
})
}
return (
<button
className={"moderation-actions " + (isIgnored ? "positive" : "dangerous")}
onClick={isIgnored ? unignoreUser : ignoreUser}>
<Block/>
<span>{isIgnored ? "Unignore" : "Ignore"}</span>
</button>
)
}
const UserModeration = ({ userID, client, member }: UserModerationProps) => { const UserModeration = ({ userID, client, member }: UserModerationProps) => {
const roomCtx = use(RoomContext) const roomCtx = use(RoomContext)
if(!roomCtx) {
return null // There is no room context, moderation is not an applicable context.
}
const openModal = use(ModalContext) const openModal = use(ModalContext)
const hasPl = (action: "invite" | "kick" | "ban") => { const hasPl = (action: "invite" | "kick" | "ban") => {
if(!roomCtx) {
return false // no room context
}
const [pls, ownPL] = getPowerLevels(roomCtx.store, client) const [pls, ownPL] = getPowerLevels(roomCtx.store, client)
const actionPL = pls[action] ?? pls.state_default ?? 50 const actionPL = pls[action] ?? pls.state_default ?? 50
const otherUserPl = pls.users?.[userID] ?? pls.users_default ?? 0 const otherUserPl = pls.users?.[userID] ?? pls.users_default ?? 0
@ -88,42 +138,43 @@ const UserModeration = ({ userID, client, member }: UserModerationProps) => {
}) })
} }
} }
const membership = member?.content.membership || "leave" const membership = member?.content.membership || "leave"
return ( return (
<div className="user-moderation"> <div className="user-moderation">
<h4>Moderation</h4> <h4>Moderation</h4>
<div className="moderation-actions"> <div className="moderation-actions">
{(["knock", "leave"].includes(membership) || !member) && hasPl("invite") && ( {roomCtx && (["knock", "leave"].includes(membership) || !member) && hasPl("invite") && (
<button className="moderation-action invite" onClick={runAction("invite")}> <button className="moderation-action positive" onClick={runAction("invite")}>
<PersonAdd /> <PersonAdd />
<span>{membership === "knock" ? "Accept request to join" : "Invite"}</span> <span>{membership === "knock" ? "Accept request to join" : "Invite"}</span>
</button> </button>
)} )}
{["knock", "invite"].includes(membership) && hasPl("kick") && ( {roomCtx && ["knock", "invite"].includes(membership) && hasPl("kick") && (
<button className="moderation-action dangerous" onClick={runAction("leave")}> <button className="moderation-action dangerous" onClick={runAction("leave")}>
<PersonRemove /> <PersonRemove />
<span>{membership === "invite" ? "Revoke invitation" : "Reject join request"}</span> <span>{membership === "invite" ? "Revoke invitation" : "Reject join request"}</span>
</button> </button>
)} )}
{membership === "join" && hasPl("kick") && ( {roomCtx && membership === "join" && hasPl("kick") && (
<button className="moderation-action dangerous" onClick={runAction("leave")}> <button className="moderation-action dangerous" onClick={runAction("leave")}>
<PersonRemove /> <PersonRemove />
<span>Kick</span> <span>Kick</span>
</button> </button>
)} )}
{membership !== "ban" && hasPl("ban") && ( {roomCtx && membership !== "ban" && hasPl("ban") && (
<button className="moderation-action dangerous" onClick={runAction("ban")}> <button className="moderation-action dangerous" onClick={runAction("ban")}>
<Gavel /> <Gavel />
<span>Ban</span> <span>Ban</span>
</button> </button>
)} )}
{membership === "ban" && hasPl("ban") && ( {roomCtx && membership === "ban" && hasPl("ban") && (
<button className="moderation-action invite" onClick={runAction("leave")}> <button className="moderation-action positive" onClick={runAction("leave")}>
<Gavel /> <Gavel />
<span>Unban</span> <span>Unban</span>
</button> </button>
)} )}
<UserIgnoreButton userID={userID} client={client} />
</div> </div>
</div> </div>
) )