From bbce3df3815eadbe344278b0e469662aef3b64e0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 25 Feb 2025 21:44:01 +0200 Subject: [PATCH] web/roomlist: add context menu for entries --- pkg/hicli/json-commands.go | 24 +++++++-- web/src/api/rpc.ts | 12 +++-- web/src/ui/composer/MessageComposer.tsx | 2 +- web/src/ui/roomlist/Entry.tsx | 25 ++++++++- web/src/ui/timeline/menu/EventMenu.tsx | 4 +- web/src/ui/timeline/menu/RoomMenu.css | 3 ++ web/src/ui/timeline/menu/RoomMenu.tsx | 68 +++++++++++++++++++++++++ web/src/ui/timeline/menu/index.css | 6 ++- web/src/ui/timeline/menu/index.ts | 1 + web/src/ui/timeline/menu/util.ts | 8 +-- 10 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 web/src/ui/timeline/menu/RoomMenu.css create mode 100644 web/src/ui/timeline/menu/RoomMenu.tsx diff --git a/pkg/hicli/json-commands.go b/pkg/hicli/json-commands.go index d6fdcf6..61399af 100644 --- a/pkg/hicli/json-commands.go +++ b/pkg/hicli/json-commands.go @@ -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"` +} diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index e93b420..aeec7eb 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -248,6 +248,14 @@ export default abstract class RPCClient { return this.request("leave_room", { room_id, reason }) } + createRoom(request: ReqCreateRoom): Promise { + return this.request("create_room", request) + } + + muteRoom(room_id: RoomID, muted: boolean): Promise { + return this.request("mute_room", { room_id, muted }) + } + resolveAlias(alias: RoomAlias): Promise { return this.request("resolve_alias", { alias }) } @@ -279,8 +287,4 @@ export default abstract class RPCClient { registerPush(reg: DBPushRegistration): Promise { return this.request("register_push", reg) } - - createRoom(request: ReqCreateRoom): Promise { - return this.request("create_room", request) - } } diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index 67152d9..34c8a86 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -568,7 +568,7 @@ const MessageComposer = () => { style.left = style.right delete style.right openModal({ - content:
+ content:
{makeAttachmentButtons(true)}
, }) diff --git a/web/src/ui/roomlist/Entry.tsx b/web/src/ui/roomlist/Entry.tsx index a69c957..c456bc3 100644 --- a/web/src/ui/roomlist/Entry.tsx +++ b/web/src/ui/roomlist/Entry.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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() + const openModal = use(ModalContext) + const mainScreen = use(MainScreenContext) + const client = use(ClientContext)! + const onContextMenu = (evt: React.MouseEvent) => { + 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: , + }) + evt.preventDefault() + } return
{isVisible ? renderEntry(room) : null} diff --git a/web/src/ui/timeline/menu/EventMenu.tsx b/web/src/ui/timeline/menu/EventMenu.tsx index 13a839b..d0c7a74 100644 --- a/web/src/ui/timeline/menu/EventMenu.tsx +++ b/web/src/ui/timeline/menu/EventMenu.tsx @@ -41,14 +41,14 @@ interface EventContextMenuProps extends BaseEventMenuProps { export const EventExtraMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => { const elements = useSecondaryItems(use(ClientContext)!, roomCtx, evt) - return
{elements}
+ return
{elements}
} 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
+ return
{primary}
{secondary} diff --git a/web/src/ui/timeline/menu/RoomMenu.css b/web/src/ui/timeline/menu/RoomMenu.css new file mode 100644 index 0000000..684a877 --- /dev/null +++ b/web/src/ui/timeline/menu/RoomMenu.css @@ -0,0 +1,3 @@ +div.room-list-menu { + +} diff --git a/web/src/ui/timeline/menu/RoomMenu.tsx b/web/src/ui/timeline/menu/RoomMenu.tsx new file mode 100644 index 0000000..46cad47 --- /dev/null +++ b/web/src/ui/timeline/menu/RoomMenu.tsx @@ -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 . +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 +} + +export const RoomMenu = ({ room, style }: RoomMenuProps) => { + const openModal = use(ModalContext) + const openSettings = () => { + openModal({ + dimmed: true, + boxed: true, + innerBoxClass: "settings-view", + content: , + }) + } + return
+ + +
+} diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index b9f8bcc..14b4727 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -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 { diff --git a/web/src/ui/timeline/menu/index.ts b/web/src/ui/timeline/menu/index.ts index 4f89140..07a73d0 100644 --- a/web/src/ui/timeline/menu/index.ts +++ b/web/src/ui/timeline/menu/index.ts @@ -14,4 +14,5 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . export { EventExtraMenu, EventFixedMenu, EventFullMenu, EventHoverMenu } from "./EventMenu.tsx" +export { RoomMenu } from "./RoomMenu.tsx" export { getModalStyleFromMouse } from "./util.ts" diff --git a/web/src/ui/timeline/menu/util.ts b/web/src/ui/timeline/menu/util.ts index 1692a12..556d729 100644 --- a/web/src/ui/timeline/menu/util.ts +++ b/web/src/ui/timeline/menu/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