diff --git a/web/src/icons/group.svg b/web/src/icons/group.svg new file mode 100644 index 0000000..a1bdcd1 --- /dev/null +++ b/web/src/icons/group.svg @@ -0,0 +1 @@ + diff --git a/web/src/icons/settings.svg b/web/src/icons/settings.svg new file mode 100644 index 0000000..1e009ad --- /dev/null +++ b/web/src/icons/settings.svg @@ -0,0 +1 @@ + diff --git a/web/src/ui/MainScreen.css b/web/src/ui/MainScreen.css index bcdd17c..0b8e349 100644 --- a/web/src/ui/MainScreen.css +++ b/web/src/ui/MainScreen.css @@ -1,31 +1,49 @@ main.matrix-main { + --room-list-width: 300px; + --right-panel-width: 300px; + position: fixed; inset: 0; display: grid; - grid-template: "roomlist roomview" 1fr / 300px 1fr; + grid-template: + " roomlist rh1 roomview" 1fr + / var(--room-list-width) 0 1fr; - @media screen and (max-width: 1000px) { - grid-template: "roomlist roomview" 1fr / 250px 1fr; - } - - @media screen and (max-width: 900px) { - grid-template: "roomlist roomview" 1fr / 200px 1fr; + &.right-panel-open { + grid-template: + " roomlist rh1 roomview rh2 rightpanel " 1fr + / var(--room-list-width) 0 1fr 0 var(--right-panel-width); } @media screen and (max-width: 750px) { - &.room-selected { + &.right-panel-open { + grid-template: "rightpanel" 1fr / 1fr; + > div.room-list-wrapper { + display: none; + } + > div.room-view { + display: none; + } + } + + &.room-selected:not(.right-panel-open) { grid-template: "roomview" 1fr / 1fr; > div.room-list-wrapper { display: none; } } - &:not(.room-selected) { + &:not(.room-selected):not(.right-panel-open) { grid-template: "roomlist" 1fr / 1fr; - > div.room-view { - display: none; - } } } + + > div.room-list-resizer { + grid-area: rh1; + } + + > div.right-panel-resizer { + grid-area: rh2; + } } diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 9fdfbe0..1eae557 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -13,32 +13,64 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback, useLayoutEffect, useState } from "react" +import { use, useCallback, useLayoutEffect, useMemo, useState } from "react" import type { RoomID } from "@/api/types" import ClientContext from "./ClientContext.ts" +import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts" import RoomView from "./RoomView.tsx" +import RightPanel, { RightPanelProps } from "./rightpanel/RightPanel.tsx" import RoomList from "./roomlist/RoomList.tsx" +import { useResizeHandle } from "./useResizeHandle.tsx" import "./MainScreen.css" const MainScreen = () => { const [activeRoomID, setActiveRoomID] = useState(null) + const [rightPanel, setRightPanel] = useState(null) const client = use(ClientContext)! const activeRoom = activeRoomID && client.store.rooms.get(activeRoomID) const setActiveRoom = useCallback((roomID: RoomID) => { console.log("Switching to room", roomID) setActiveRoomID(roomID) + setRightPanel(null) if (client.store.rooms.get(roomID)?.stateLoaded === false) { client.loadRoomState(roomID) .catch(err => console.error("Failed to load room state", err)) } }, [client]) + const context: MainScreenContextFields = useMemo(() => ({ + setActiveRoom, + clickRoom: (evt: React.MouseEvent) => { + const roomID = evt.currentTarget.getAttribute("data-room-id") + if (roomID) { + setActiveRoom(roomID) + } else { + console.warn("No room ID :(", evt.currentTarget) + } + }, + clearActiveRoom: () => setActiveRoomID(null), + setRightPanel, + }), [setRightPanel, setActiveRoom]) useLayoutEffect(() => { client.store.switchRoom = setActiveRoom }, [client, setActiveRoom]) - const clearActiveRoom = useCallback(() => setActiveRoomID(null), []) - return
- - {activeRoom && } + const [roomListWidth, resizeHandle1] = useResizeHandle( + 300, 48, 900, "roomListWidth", { className: "room-list-resizer" }, + ) + const [rightPanelWidth, resizeHandle2] = useResizeHandle( + 300, 100, 900, "rightPanelWidth", { className: "right-panel-resizer" }, + ) + const extraStyle = { + ["--room-list-width" as string]: `${roomListWidth}px`, + ["--right-panel-width" as string]: `${rightPanelWidth}px`, + } + return
+ + + {resizeHandle1} + {activeRoom && } + {resizeHandle2} + {rightPanel && } +
} diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts new file mode 100644 index 0000000..e4876d9 --- /dev/null +++ b/web/src/ui/MainScreenContext.ts @@ -0,0 +1,43 @@ +// 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 . +import { createContext } from "react" +import type { RoomID } from "@/api/types" +import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" + +export interface MainScreenContextFields { + setActiveRoom: (roomID: RoomID) => void + clickRoom: (evt: React.MouseEvent) => void + clearActiveRoom: () => void + + setRightPanel: (props: RightPanelProps) => void +} + +const MainScreenContext = createContext({ + get setActiveRoom(): never { + throw new Error("MainScreenContext used outside main screen") + }, + get clickRoom(): never { + throw new Error("MainScreenContext used outside main screen") + }, + get clearActiveRoom(): never { + throw new Error("MainScreenContext used outside main screen") + }, + get setRightPanel(): never { + throw new Error("MainScreenContext used outside main screen") + }, +}) + +export default MainScreenContext diff --git a/web/src/ui/ResizeHandle.css b/web/src/ui/ResizeHandle.css new file mode 100644 index 0000000..29e7b0b --- /dev/null +++ b/web/src/ui/ResizeHandle.css @@ -0,0 +1,12 @@ +div.resize-handle-outer { + position: relative; + + > div.resize-handle-inner { + position: absolute; + top: 0; + bottom: 0; + left: -.25rem; + right: -.25rem; + cursor: col-resize; + } +} diff --git a/web/src/ui/ResizeHandle.tsx b/web/src/ui/ResizeHandle.tsx new file mode 100644 index 0000000..09fbe35 --- /dev/null +++ b/web/src/ui/ResizeHandle.tsx @@ -0,0 +1,50 @@ +// 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 . +import React, { CSSProperties } from "react" +import useEvent from "@/util/useEvent.ts" +import "./ResizeHandle.css" + +export interface ResizeHandleProps { + width: number + minWidth: number + maxWidth: number + setWidth: (width: number) => void + className?: string + style?: CSSProperties +} + +const ResizeHandle = ({ width, minWidth, maxWidth, setWidth, style, className }: ResizeHandleProps) => { + const onMouseDown = useEvent((evt: React.MouseEvent) => { + const origWidth = width + const startPos = evt.clientX + const onMouseMove = (evt: MouseEvent) => { + setWidth(Math.max(minWidth, Math.min(maxWidth, origWidth + evt.clientX - startPos))) + evt.preventDefault() + } + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove) + document.removeEventListener("mouseup", onMouseUp) + } + document.addEventListener("mousemove", onMouseMove) + document.addEventListener("mouseup", onMouseUp) + evt.preventDefault() + }) + return
+
+
+} + +export default ResizeHandle diff --git a/web/src/ui/RoomView.css b/web/src/ui/RoomView.css index f7a0eb0..8ed804f 100644 --- a/web/src/ui/RoomView.css +++ b/web/src/ui/RoomView.css @@ -1,4 +1,5 @@ div.room-view { + grid-area: roomview; overflow: hidden; height: 100%; display: grid; @@ -8,21 +9,4 @@ div.room-view { "messageview" 1fr "input" auto / 1fr; - - > div.room-header { - display: flex; - align-items: center; - gap: .5rem; - padding-left: .5rem; - border-bottom: 1px solid var(--border-color); - - > span.room-name { - font-weight: bold; - } - - > button.back { - height: 2.5rem; - width: 2.5rem; - } - } } diff --git a/web/src/ui/RoomView.tsx b/web/src/ui/RoomView.tsx index 59ff7e0..4790e78 100644 --- a/web/src/ui/RoomView.tsx +++ b/web/src/ui/RoomView.tsx @@ -13,39 +13,16 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useRef } from "react" -import { getAvatarURL } from "@/api/media.ts" +import { useRef } from "react" import { RoomStateStore } from "@/api/statestore" -import { useEventAsState } from "@/util/eventdispatcher.ts" +import RoomViewHeader from "./RoomViewHeader.tsx" import MessageComposer from "./composer/MessageComposer.tsx" -import { LightboxContext } from "./modal/Lightbox.tsx" import { RoomContext, RoomContextData } from "./roomcontext.ts" import TimelineView from "./timeline/TimelineView.tsx" -import BackIcon from "@/icons/back.svg?react" import "./RoomView.css" interface RoomViewProps { room: RoomStateStore - clearActiveRoom: () => void -} - -const RoomHeader = ({ room, clearActiveRoom }: RoomViewProps) => { - const roomMeta = useEventAsState(room.meta) - const avatarSourceID = roomMeta.lazy_load_summary?.heroes?.length === 1 - ? roomMeta.lazy_load_summary.heroes[0] : room.roomID - return
- - - - {roomMeta.name ?? roomMeta.room_id} - -
} const onKeyDownRoomView = (evt: React.KeyboardEvent) => { @@ -54,14 +31,14 @@ const onKeyDownRoomView = (evt: React.KeyboardEvent) => { } } -const RoomView = ({ room, clearActiveRoom }: RoomViewProps) => { +const RoomView = ({ room }: RoomViewProps) => { const roomContextDataRef = useRef(undefined) if (roomContextDataRef.current === undefined) { roomContextDataRef.current = new RoomContextData(room) } return
- + diff --git a/web/src/ui/RoomViewHeader.css b/web/src/ui/RoomViewHeader.css new file mode 100644 index 0000000..dcb94f3 --- /dev/null +++ b/web/src/ui/RoomViewHeader.css @@ -0,0 +1,31 @@ +div.room-header { + display: flex; + align-items: center; + gap: .5rem; + padding-left: .5rem; + border-bottom: 1px solid var(--border-color); + + > span.room-name { + font-weight: bold; + } + + > div.divider { + flex: 1; + } + + > button.back { + height: 2.5rem; + width: 2.5rem; + } + + > div.right-buttons { + display: flex; + align-items: center; + margin-right: .5rem; + + > button { + width: 2.5rem; + height: 2.5rem; + } + } +} diff --git a/web/src/ui/RoomViewHeader.tsx b/web/src/ui/RoomViewHeader.tsx new file mode 100644 index 0000000..5022929 --- /dev/null +++ b/web/src/ui/RoomViewHeader.tsx @@ -0,0 +1,58 @@ +// 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 . +import { use } from "react" +import { getAvatarURL } from "@/api/media.ts" +import { RoomStateStore } from "@/api/statestore" +import { useEventAsState } from "@/util/eventdispatcher.ts" +import MainScreenContext from "./MainScreenContext.ts" +import { LightboxContext } from "./modal/Lightbox.tsx" +import BackIcon from "@/icons/back.svg?react" +// import PeopleIcon from "@/icons/group.svg?react" +// import PinIcon from "@/icons/pin.svg?react" +// import SettingsIcon from "@/icons/settings.svg?react" +import "./RoomViewHeader.css" + +interface RoomViewHeaderProps { + room: RoomStateStore +} + +const RoomViewHeader = ({ room }: RoomViewHeaderProps) => { + const roomMeta = useEventAsState(room.meta) + const avatarSourceID = roomMeta.lazy_load_summary?.heroes?.length === 1 + ? roomMeta.lazy_load_summary.heroes[0] : room.roomID + const mainScreen = use(MainScreenContext) + return
+ + + + {roomMeta.name ?? roomMeta.room_id} + +
+ {/*
+ + + +
*/} +
+} + +export default RoomViewHeader diff --git a/web/src/ui/rightpanel/RightPanel.css b/web/src/ui/rightpanel/RightPanel.css new file mode 100644 index 0000000..24fbb72 --- /dev/null +++ b/web/src/ui/rightpanel/RightPanel.css @@ -0,0 +1,3 @@ +div.right-panel { + +} diff --git a/web/src/ui/rightpanel/RightPanel.tsx b/web/src/ui/rightpanel/RightPanel.tsx new file mode 100644 index 0000000..09920e5 --- /dev/null +++ b/web/src/ui/rightpanel/RightPanel.tsx @@ -0,0 +1,27 @@ +// 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 . + +export interface RightPanelProps { + meow?: never +} + +const RightPanel = ({ meow }: RightPanelProps) => { + return
+ {meow} +
+} + +export default RightPanel diff --git a/web/src/ui/roomlist/Entry.tsx b/web/src/ui/roomlist/Entry.tsx index 55e6866..456eca6 100644 --- a/web/src/ui/roomlist/Entry.tsx +++ b/web/src/ui/roomlist/Entry.tsx @@ -19,10 +19,10 @@ import type { RoomListEntry } from "@/api/statestore" import type { MemDBEvent, MemberEventContent } from "@/api/types" import useContentVisibility from "@/util/contentvisibility.ts" import ClientContext from "../ClientContext.ts" +import MainScreenContext from "../MainScreenContext.ts" export interface RoomListEntryProps { room: RoomListEntry - setActiveRoom: (evt: React.MouseEvent) => void isActive: boolean hidden: boolean } @@ -77,12 +77,12 @@ const EntryInner = ({ room }: InnerProps) => { } -const Entry = ({ room, setActiveRoom, isActive, hidden }: RoomListEntryProps) => { +const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => { const [isVisible, divRef] = useContentVisibility() return
{isVisible ? : null} diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index b5eac6d..fc7841c 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -22,23 +22,14 @@ import Entry from "./Entry.tsx" import "./RoomList.css" interface RoomListProps { - setActiveRoom: (room_id: RoomID) => void activeRoomID: RoomID | null } -const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => { +const RoomList = ({ activeRoomID }: RoomListProps) => { const roomList = useEventAsState(use(ClientContext)!.store.roomList) const roomFilterRef = useRef(null) const [roomFilter, setRoomFilter] = useState("") const [realRoomFilter, setRealRoomFilter] = useState("") - const clickRoom = useCallback((evt: React.MouseEvent) => { - const roomID = evt.currentTarget.getAttribute("data-room-id") - if (roomID) { - setActiveRoom(roomID) - } else { - console.warn("No room ID :(", evt.currentTarget) - } - }, [setActiveRoom]) const updateRoomFilter = useCallback((evt: React.ChangeEvent) => { setRoomFilter(evt.target.value) @@ -61,7 +52,6 @@ const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => { isActive={room.room_id === activeRoomID} hidden={roomFilter ? !room.search_name.includes(realRoomFilter) : false} room={room} - setActiveRoom={clickRoom} />, )}
diff --git a/web/src/ui/useResizeHandle.tsx b/web/src/ui/useResizeHandle.tsx new file mode 100644 index 0000000..9c4d060 --- /dev/null +++ b/web/src/ui/useResizeHandle.tsx @@ -0,0 +1,43 @@ +// 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 . +import { useEffect, useState } from "react" +import ResizeHandle, { ResizeHandleProps } from "./ResizeHandle.tsx" + +export const useResizeHandle = ( + defaultSize: number, + minWidth: number, + maxWidth: number, + localStorageKey: string, + extraProps: Partial, +) => { + const [width, setWidth] = useState(() => { + const savedWidth = localStorage.getItem(localStorageKey) + if (savedWidth) { + return Number(savedWidth) + } + return defaultSize + }) + useEffect(() => { + localStorage.setItem(localStorageKey, width.toString()) + }, [localStorageKey, width]) + return [width, ] as const +}