forked from Mirrors/gomuks
parent
bdae0c416f
commit
2203c18e15
7 changed files with 200 additions and 35 deletions
|
@ -223,6 +223,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *database.PushRegistration) (bool, error) {
|
return unmarshalAndCall(req.Data, func(params *database.PushRegistration) (bool, error) {
|
||||||
return true, h.DB.PushRegistration.Put(ctx, params)
|
return true, h.DB.PushRegistration.Put(ctx, params)
|
||||||
})
|
})
|
||||||
|
case "create_room":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *mautrix.ReqCreateRoom) (*mautrix.RespCreateRoom, error) {
|
||||||
|
return h.Client.CreateRoom(ctx, params)
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown command %q", req.Command)
|
return nil, fmt.Errorf("unknown command %q", req.Command)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,9 @@ import type {
|
||||||
ReceiptType,
|
ReceiptType,
|
||||||
RelatesTo,
|
RelatesTo,
|
||||||
RelationType,
|
RelationType,
|
||||||
|
ReqCreateRoom,
|
||||||
ResolveAliasResponse,
|
ResolveAliasResponse,
|
||||||
|
RespCreateRoom,
|
||||||
RespOpenIDToken,
|
RespOpenIDToken,
|
||||||
RespRoomJoin,
|
RespRoomJoin,
|
||||||
RoomAlias,
|
RoomAlias,
|
||||||
|
@ -277,4 +279,8 @@ export default abstract class RPCClient {
|
||||||
registerPush(reg: DBPushRegistration): Promise<boolean> {
|
registerPush(reg: DBPushRegistration): Promise<boolean> {
|
||||||
return this.request("register_push", reg)
|
return this.request("register_push", reg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createRoom(request: ReqCreateRoom): Promise<RespCreateRoom> {
|
||||||
|
return this.request("create_room", request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -320,3 +320,29 @@ export interface RespOpenIDToken {
|
||||||
matrix_server_name: string
|
matrix_server_name: string
|
||||||
token_type: "Bearer"
|
token_type: "Bearer"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomVisibility = "public" | "private"
|
||||||
|
export type RoomPreset = "private_chat" | "public_chat" | "trusted_private_chat"
|
||||||
|
|
||||||
|
export interface ReqCreateRoom {
|
||||||
|
visibility?: RoomVisibility
|
||||||
|
room_alias_name?: string
|
||||||
|
name?: string
|
||||||
|
topic?: string
|
||||||
|
invite?: UserID[]
|
||||||
|
preset?: RoomPreset
|
||||||
|
is_direct?: boolean
|
||||||
|
initial_state?: {
|
||||||
|
type: EventType
|
||||||
|
state_key?: string
|
||||||
|
content: Record<string, unknown>
|
||||||
|
}[]
|
||||||
|
room_version?: string
|
||||||
|
creation_content?: Record<string, unknown>
|
||||||
|
power_level_content_override?: Record<string, unknown>
|
||||||
|
"fi.mau.room_id"?: RoomID
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespCreateRoom {
|
||||||
|
room_id: RoomID
|
||||||
|
}
|
||||||
|
|
1
web/src/icons/chat.svg
Normal file
1
web/src/icons/chat.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M240-400h320v-80H240v80Zm0-120h480v-80H240v80Zm0-120h480v-80H240v80ZM80-80v-720q0-33 23.5-56.5T160-880h640q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H240L80-80Zm126-240h594v-480H160v525l46-45Zm-46 0v-480 480Z"/></svg>
|
After Width: | Height: | Size: 341 B |
102
web/src/ui/rightpanel/StartDMButton.tsx
Normal file
102
web/src/ui/rightpanel/StartDMButton.tsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2025 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, useMemo, useState } from "react"
|
||||||
|
import Client from "@/api/client.ts"
|
||||||
|
import { UserID } from "@/api/types"
|
||||||
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
|
import ChatIcon from "@/icons/chat.svg?react"
|
||||||
|
|
||||||
|
const StartDMButton = ({ userID, client }: { userID: UserID; client: Client }) => {
|
||||||
|
const mainScreen = use(MainScreenContext)!
|
||||||
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
|
|
||||||
|
const findExistingRoom = () => {
|
||||||
|
for (const room of client.store.rooms.values()) {
|
||||||
|
if (room.meta.current.dm_user_id === userID) {
|
||||||
|
return room.roomID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const existingRoom = useMemo(findExistingRoom, [userID, client])
|
||||||
|
|
||||||
|
const startDM = async () => {
|
||||||
|
if (existingRoom) {
|
||||||
|
mainScreen.setActiveRoom(existingRoom)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!window.confirm(`Are you sure you want to start a chat with ${userID}?`)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const existingRoomRelookup = findExistingRoom()
|
||||||
|
if (existingRoomRelookup) {
|
||||||
|
mainScreen.setActiveRoom(existingRoomRelookup)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCreating(true)
|
||||||
|
|
||||||
|
let shouldEncrypt = false
|
||||||
|
const initialState = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
shouldEncrypt = (await client.rpc.trackUserDevices(userID)).devices.length > 0
|
||||||
|
|
||||||
|
if (shouldEncrypt) {
|
||||||
|
console.log("User has encryption devices, creating encrypted room")
|
||||||
|
initialState.push({
|
||||||
|
type: "m.room.encryption",
|
||||||
|
content: {
|
||||||
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to check user encryption status:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the room with encryption if needed
|
||||||
|
const response = await client.rpc.createRoom({
|
||||||
|
is_direct: true,
|
||||||
|
preset: "trusted_private_chat",
|
||||||
|
invite: [userID],
|
||||||
|
initial_state: initialState,
|
||||||
|
})
|
||||||
|
console.log("Created DM room:", response.room_id)
|
||||||
|
|
||||||
|
// FIXME this is a hacky way to work around the room taking time to come down /sync
|
||||||
|
setTimeout(() => {
|
||||||
|
mainScreen.setActiveRoom(response.room_id)
|
||||||
|
}, 1000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to create DM room:", err)
|
||||||
|
window.alert(`Failed to create DM room: ${err}`)
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button
|
||||||
|
className="moderation-action positive"
|
||||||
|
onClick={startDM}
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<ChatIcon />
|
||||||
|
<span>{existingRoom ? "Go to DM" : isCreating ? "Creating..." : "Create DM"}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StartDMButton
|
55
web/src/ui/rightpanel/UserIgnoreButton.tsx
Normal file
55
web/src/ui/rightpanel/UserIgnoreButton.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2025 Nexus Nicholson
|
||||||
|
//
|
||||||
|
// 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 Client from "@/api/client.ts"
|
||||||
|
import { useAccountData } from "@/api/statestore"
|
||||||
|
import { IgnoredUsersEventContent } from "@/api/types"
|
||||||
|
import IgnoreIcon from "@/icons/block.svg?react"
|
||||||
|
|
||||||
|
const UserIgnoreButton = ({ userID, client }: { userID: string; client: Client }) => {
|
||||||
|
const ignoredUsers = useAccountData(client.store, "m.ignored_user_list") as IgnoredUsersEventContent | null
|
||||||
|
|
||||||
|
const isIgnored = Boolean(ignoredUsers?.ignored_users?.[userID])
|
||||||
|
const ignoreUser = () => {
|
||||||
|
if (!window.confirm(`Are you sure you want to ignore ${userID}?`)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newIgnoredUsers = { ...(ignoredUsers || { ignored_users: {}}) }
|
||||||
|
newIgnoredUsers.ignored_users[userID] = {}
|
||||||
|
client.rpc.setAccountData("m.ignored_user_list", newIgnoredUsers).catch(err => {
|
||||||
|
console.error("Failed to ignore user", err)
|
||||||
|
window.alert(`Failed to ignore ${userID}: ${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const unignoreUser = () => {
|
||||||
|
const newIgnoredUsers = { ...(ignoredUsers || { ignored_users: {}}) }
|
||||||
|
delete newIgnoredUsers.ignored_users[userID]
|
||||||
|
client.rpc.setAccountData("m.ignored_user_list", newIgnoredUsers).catch(err => {
|
||||||
|
console.error("Failed to unignore user", err)
|
||||||
|
window.alert(`Failed to unignore ${userID}: ${err}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={"moderation-action " + (isIgnored ? "positive" : "dangerous")}
|
||||||
|
onClick={isIgnored ? unignoreUser : ignoreUser}>
|
||||||
|
<IgnoreIcon/>
|
||||||
|
<span>{isIgnored ? "Unignore" : "Ignore"}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserIgnoreButton
|
|
@ -15,12 +15,13 @@
|
||||||
// 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 } from "react"
|
||||||
import Client from "@/api/client.ts"
|
import Client from "@/api/client.ts"
|
||||||
import { RoomStateStore, useAccountData } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import { IgnoredUsersEventContent, MemDBEvent, MembershipAction } from "@/api/types"
|
import { MemDBEvent, MembershipAction } from "@/api/types"
|
||||||
import { ModalContext } from "../modal"
|
import { ModalContext } from "../modal"
|
||||||
import ConfirmWithMessageModal from "../timeline/menu/ConfirmWithMessageModal.tsx"
|
import ConfirmWithMessageModal from "../timeline/menu/ConfirmWithMessageModal.tsx"
|
||||||
import { getPowerLevels } from "../timeline/menu/util.ts"
|
import { getPowerLevels } from "../timeline/menu/util.ts"
|
||||||
import IgnoreIcon from "@/icons/block.svg?react"
|
import StartDMButton from "./StartDMButton.tsx"
|
||||||
|
import UserIgnoreButton from "./UserIgnoreButton.tsx"
|
||||||
import BanIcon from "@/icons/gavel.svg?react"
|
import BanIcon from "@/icons/gavel.svg?react"
|
||||||
import InviteIcon from "@/icons/person-add.svg?react"
|
import InviteIcon from "@/icons/person-add.svg?react"
|
||||||
import KickIcon from "@/icons/person-remove.svg?react"
|
import KickIcon from "@/icons/person-remove.svg?react"
|
||||||
|
@ -32,37 +33,6 @@ interface UserModerationProps {
|
||||||
member: MemDBEvent | null;
|
member: MemDBEvent | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserIgnoreButton = ({ userID, client }: { userID: string; client: Client }) => {
|
|
||||||
const ignoredUsers = useAccountData(client.store, "m.ignored_user_list") as IgnoredUsersEventContent | null
|
|
||||||
|
|
||||||
const isIgnored = Boolean(ignoredUsers?.ignored_users?.[userID])
|
|
||||||
const ignoreUser = () => {
|
|
||||||
const newIgnoredUsers = { ...(ignoredUsers || { ignored_users: {}}) }
|
|
||||||
newIgnoredUsers.ignored_users[userID] = {}
|
|
||||||
client.rpc.setAccountData("m.ignored_user_list", newIgnoredUsers).catch(err => {
|
|
||||||
console.error("Failed to ignore user", err)
|
|
||||||
window.alert(`Failed to ignore ${userID}: ${err}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const unignoreUser = () => {
|
|
||||||
const newIgnoredUsers = { ...(ignoredUsers || { ignored_users: {}}) }
|
|
||||||
delete newIgnoredUsers.ignored_users[userID]
|
|
||||||
client.rpc.setAccountData("m.ignored_user_list", newIgnoredUsers).catch(err => {
|
|
||||||
console.error("Failed to unignore user", err)
|
|
||||||
window.alert(`Failed to unignore ${userID}: ${err}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={"moderation-action " + (isIgnored ? "positive" : "dangerous")}
|
|
||||||
onClick={isIgnored ? unignoreUser : ignoreUser}>
|
|
||||||
<IgnoreIcon/>
|
|
||||||
<span>{isIgnored ? "Unignore" : "Ignore"}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserModeration = ({ userID, client, member, room }: UserModerationProps) => {
|
const UserModeration = ({ userID, client, member, room }: UserModerationProps) => {
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
const hasPL = (action: "invite" | "kick" | "ban") => {
|
const hasPL = (action: "invite" | "kick" | "ban") => {
|
||||||
|
@ -109,7 +79,8 @@ const UserModeration = ({ userID, client, member, room }: UserModerationProps) =
|
||||||
const membership = member?.content.membership || "leave"
|
const membership = member?.content.membership || "leave"
|
||||||
|
|
||||||
return <div className="user-moderation">
|
return <div className="user-moderation">
|
||||||
<h4>Moderation</h4>
|
<h4>Actions</h4>
|
||||||
|
{!room || room.meta.current.dm_user_id !== userID ? <StartDMButton userID={userID} client={client} /> : null}
|
||||||
{room && (["knock", "leave"].includes(membership) || !member) && hasPL("invite") && (
|
{room && (["knock", "leave"].includes(membership) || !member) && hasPL("invite") && (
|
||||||
<button className="moderation-action positive" onClick={runAction("invite")}>
|
<button className="moderation-action positive" onClick={runAction("invite")}>
|
||||||
<InviteIcon />
|
<InviteIcon />
|
||||||
|
|
Loading…
Add table
Reference in a new issue