mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/roomlist: add context menu for entries
This commit is contained in:
parent
2203c18e15
commit
bbce3df381
10 changed files with 135 additions and 18 deletions
|
@ -17,6 +17,7 @@ import (
|
|||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/pushrules"
|
||||
|
||||
"go.mau.fi/gomuks/pkg/hicli/database"
|
||||
)
|
||||
|
@ -167,6 +168,20 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
|||
return unmarshalAndCall(req.Data, func(params *leaveRoomParams) (*mautrix.RespLeaveRoom, error) {
|
||||
return h.Client.LeaveRoom(ctx, params.RoomID, &mautrix.ReqLeave{Reason: params.Reason})
|
||||
})
|
||||
case "create_room":
|
||||
return unmarshalAndCall(req.Data, func(params *mautrix.ReqCreateRoom) (*mautrix.RespCreateRoom, error) {
|
||||
return h.Client.CreateRoom(ctx, params)
|
||||
})
|
||||
case "mute_room":
|
||||
return unmarshalAndCall(req.Data, func(params *muteRoomParams) (bool, error) {
|
||||
if params.Muted {
|
||||
return true, h.Client.PutPushRule(ctx, "global", pushrules.RoomRule, string(params.RoomID), &mautrix.ReqPutPushRule{
|
||||
Actions: []pushrules.PushActionType{},
|
||||
})
|
||||
} else {
|
||||
return false, h.Client.DeletePushRule(ctx, "global", pushrules.RoomRule, string(params.RoomID))
|
||||
}
|
||||
})
|
||||
case "ensure_group_session_shared":
|
||||
return unmarshalAndCall(req.Data, func(params *ensureGroupSessionSharedParams) (bool, error) {
|
||||
return true, h.EnsureGroupSessionShared(ctx, params.RoomID)
|
||||
|
@ -223,10 +238,6 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
|||
return unmarshalAndCall(req.Data, func(params *database.PushRegistration) (bool, error) {
|
||||
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:
|
||||
return nil, fmt.Errorf("unknown command %q", req.Command)
|
||||
}
|
||||
|
@ -393,3 +404,8 @@ type getReceiptsParams struct {
|
|||
RoomID id.RoomID `json:"room_id"`
|
||||
EventIDs []id.EventID `json:"event_ids"`
|
||||
}
|
||||
|
||||
type muteRoomParams struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
|
|
@ -248,6 +248,14 @@ export default abstract class RPCClient {
|
|||
return this.request("leave_room", { room_id, reason })
|
||||
}
|
||||
|
||||
createRoom(request: ReqCreateRoom): Promise<RespCreateRoom> {
|
||||
return this.request("create_room", request)
|
||||
}
|
||||
|
||||
muteRoom(room_id: RoomID, muted: boolean): Promise<boolean> {
|
||||
return this.request("mute_room", { room_id, muted })
|
||||
}
|
||||
|
||||
resolveAlias(alias: RoomAlias): Promise<ResolveAliasResponse> {
|
||||
return this.request("resolve_alias", { alias })
|
||||
}
|
||||
|
@ -279,8 +287,4 @@ export default abstract class RPCClient {
|
|||
registerPush(reg: DBPushRegistration): Promise<boolean> {
|
||||
return this.request("register_push", reg)
|
||||
}
|
||||
|
||||
createRoom(request: ReqCreateRoom): Promise<RespCreateRoom> {
|
||||
return this.request("create_room", request)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -568,7 +568,7 @@ const MessageComposer = () => {
|
|||
style.left = style.right
|
||||
delete style.right
|
||||
openModal({
|
||||
content: <div className="event-context-menu" style={style}>
|
||||
content: <div className="context-menu event-context-menu" style={style}>
|
||||
{makeAttachmentButtons(true)}
|
||||
</div>,
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
//
|
||||
// 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 { JSX, memo, use } from "react"
|
||||
import React, { JSX, memo, use } from "react"
|
||||
import { getRoomAvatarThumbnailURL } from "@/api/media.ts"
|
||||
import type { RoomListEntry } from "@/api/statestore"
|
||||
import type { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
|
@ -21,6 +21,8 @@ import useContentVisibility from "@/util/contentvisibility.ts"
|
|||
import { getDisplayname } from "@/util/validation.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import MainScreenContext from "../MainScreenContext.ts"
|
||||
import { ModalContext } from "../modal"
|
||||
import { RoomMenu, getModalStyleFromMouse } from "../timeline/menu"
|
||||
import UnreadCount from "./UnreadCount.tsx"
|
||||
|
||||
export interface RoomListEntryProps {
|
||||
|
@ -77,10 +79,29 @@ function renderEntry(room: RoomListEntry) {
|
|||
|
||||
const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => {
|
||||
const [isVisible, divRef] = useContentVisibility<HTMLDivElement>()
|
||||
const openModal = use(ModalContext)
|
||||
const mainScreen = use(MainScreenContext)
|
||||
const client = use(ClientContext)!
|
||||
const onContextMenu = (evt: React.MouseEvent<HTMLDivElement>) => {
|
||||
const realRoom = client.store.rooms.get(room.room_id)
|
||||
if (!realRoom) {
|
||||
console.error("Room state store not found for", room.room_id)
|
||||
return
|
||||
}
|
||||
openModal({
|
||||
content: <RoomMenu
|
||||
room={realRoom}
|
||||
entry={room}
|
||||
style={getModalStyleFromMouse(evt, 6 * 40)}
|
||||
/>,
|
||||
})
|
||||
evt.preventDefault()
|
||||
}
|
||||
return <div
|
||||
ref={divRef}
|
||||
className={`room-entry ${isActive ? "active" : ""} ${hidden ? "hidden" : ""}`}
|
||||
onClick={use(MainScreenContext).clickRoom}
|
||||
onClick={mainScreen.clickRoom}
|
||||
onContextMenu={onContextMenu}
|
||||
data-room-id={room.room_id}
|
||||
>
|
||||
{isVisible ? renderEntry(room) : null}
|
||||
|
|
|
@ -41,14 +41,14 @@ interface EventContextMenuProps extends BaseEventMenuProps {
|
|||
|
||||
export const EventExtraMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => {
|
||||
const elements = useSecondaryItems(use(ClientContext)!, roomCtx, evt)
|
||||
return <div style={style} className="event-context-menu extra">{elements}</div>
|
||||
return <div style={style} className="context-menu event-context-menu extra">{elements}</div>
|
||||
}
|
||||
|
||||
export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => {
|
||||
const client = use(ClientContext)!
|
||||
const primary = usePrimaryItems(client, roomCtx, evt, false, false, style, undefined)
|
||||
const secondary = useSecondaryItems(client, roomCtx, evt)
|
||||
return <div style={style} className="event-context-menu full">
|
||||
return <div style={style} className="context-menu event-context-menu full">
|
||||
{primary}
|
||||
<hr/>
|
||||
{secondary}
|
||||
|
|
3
web/src/ui/timeline/menu/RoomMenu.css
Normal file
3
web/src/ui/timeline/menu/RoomMenu.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
div.room-list-menu {
|
||||
|
||||
}
|
68
web/src/ui/timeline/menu/RoomMenu.tsx
Normal file
68
web/src/ui/timeline/menu/RoomMenu.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
// 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 { CSSProperties, use } from "react"
|
||||
import { RoomListEntry, RoomStateStore, useAccountData } from "@/api/statestore"
|
||||
import { RoomID } from "@/api/types"
|
||||
import ClientContext from "../../ClientContext.ts"
|
||||
import { ModalContext } from "../../modal"
|
||||
import SettingsView from "../../settings/SettingsView.tsx"
|
||||
import NotificationsOffIcon from "@/icons/notifications-off.svg?react"
|
||||
import NotificationsIcon from "@/icons/notifications.svg?react"
|
||||
import SettingsIcon from "@/icons/settings.svg?react"
|
||||
import "./RoomMenu.css"
|
||||
|
||||
interface RoomMenuProps {
|
||||
room: RoomStateStore
|
||||
entry: RoomListEntry
|
||||
style: CSSProperties
|
||||
}
|
||||
|
||||
const hasNotifyingActions = (actions: unknown) => {
|
||||
return Array.isArray(actions) && actions.length > 0 && actions.includes("notify")
|
||||
}
|
||||
|
||||
const MuteButton = ({ roomID }: { roomID: RoomID }) => {
|
||||
const client = use(ClientContext)!
|
||||
const roomRules = useAccountData(client.store, "m.push_rules")?.global?.room
|
||||
const pushRule = Array.isArray(roomRules) ? roomRules.find(rule => rule?.rule_id === roomID) : null
|
||||
const muted = pushRule?.enabled === true && !hasNotifyingActions(pushRule.actions)
|
||||
const toggleMute = () => {
|
||||
client.rpc.muteRoom(roomID, !muted).catch(err => {
|
||||
console.error("Failed to mute room", err)
|
||||
window.alert(`Failed to ${muted ? "unmute" : "mute"} room: ${err}`)
|
||||
})
|
||||
}
|
||||
return <button onClick={toggleMute}>
|
||||
{muted ? <NotificationsIcon/> : <NotificationsOffIcon/>}
|
||||
{muted ? "Unmute" : "Mute"}
|
||||
</button>
|
||||
}
|
||||
|
||||
export const RoomMenu = ({ room, style }: RoomMenuProps) => {
|
||||
const openModal = use(ModalContext)
|
||||
const openSettings = () => {
|
||||
openModal({
|
||||
dimmed: true,
|
||||
boxed: true,
|
||||
innerBoxClass: "settings-view",
|
||||
content: <SettingsView room={room} />,
|
||||
})
|
||||
}
|
||||
return <div className="context-menu room-list-menu" style={style}>
|
||||
<MuteButton roomID={room.roomID}/>
|
||||
<button onClick={openSettings}><SettingsIcon /> Settings</button>
|
||||
</div>
|
||||
}
|
|
@ -43,7 +43,7 @@ div.event-fixed-menu {
|
|||
}
|
||||
}
|
||||
|
||||
div.event-context-menu {
|
||||
div.context-menu {
|
||||
position: fixed;
|
||||
background-color: var(--background-color);
|
||||
border-radius: .5rem;
|
||||
|
@ -80,6 +80,10 @@ div.event-context-menu {
|
|||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.event-context-menu, &.room-list-menu {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
div.confirm-message-modal > form {
|
||||
|
|
|
@ -14,4 +14,5 @@
|
|||
// 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/>.
|
||||
export { EventExtraMenu, EventFixedMenu, EventFullMenu, EventHoverMenu } from "./EventMenu.tsx"
|
||||
export { RoomMenu } from "./RoomMenu.tsx"
|
||||
export { getModalStyleFromMouse } from "./util.ts"
|
||||
|
|
|
@ -39,10 +39,10 @@ export const getEncryption = (room: RoomStateStore): boolean =>{
|
|||
export function getModalStyleFromMouse(
|
||||
evt: React.MouseEvent, modalHeight: number, modalWidth = 10 * 16,
|
||||
): CSSProperties {
|
||||
const style: CSSProperties = { right: window.innerWidth - evt.clientX }
|
||||
if (evt.clientX - modalWidth < 4) {
|
||||
delete style.right
|
||||
style.left = "4px"
|
||||
const style: CSSProperties = { left: evt.clientX }
|
||||
if (evt.clientX + modalWidth > window.innerWidth) {
|
||||
delete style.left
|
||||
style.right = "4px"
|
||||
}
|
||||
if (evt.clientY + modalHeight > window.innerHeight) {
|
||||
style.bottom = window.innerHeight - evt.clientY
|
||||
|
|
Loading…
Add table
Reference in a new issue