From df551fe4cb0486c10ee8c117845de3e9340c510b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 10:54:13 +0200 Subject: [PATCH] web/timeline: use custom message focus state on mobile to match context menu state --- web/src/ui/roomview/RoomView.css | 12 +++++++ web/src/ui/roomview/RoomView.tsx | 9 +++++- web/src/ui/roomview/RoomViewHeader.css | 1 + web/src/ui/roomview/roomcontext.ts | 9 +++++- web/src/ui/timeline/TimelineEvent.css | 2 +- web/src/ui/timeline/TimelineEvent.tsx | 31 +++++++++---------- web/src/ui/timeline/TimelineView.tsx | 12 +++++-- .../ui/timeline/content/MediaMessageBody.tsx | 10 ++++-- web/src/ui/timeline/menu/EventMenu.tsx | 12 ++++--- web/src/ui/timeline/menu/index.css | 4 --- 10 files changed, 69 insertions(+), 33 deletions(-) diff --git a/web/src/ui/roomview/RoomView.css b/web/src/ui/roomview/RoomView.css index 49b93f4..4dca406 100644 --- a/web/src/ui/roomview/RoomView.css +++ b/web/src/ui/roomview/RoomView.css @@ -19,3 +19,15 @@ div.room-view { align-items: center; } } + +div#mobile-event-menu-container { + grid-area: header; + + &:empty { + display: none; + } + + &:not(:empty) + div.room-header { + display: none; + } +} diff --git a/web/src/ui/roomview/RoomView.tsx b/web/src/ui/roomview/RoomView.tsx index d06da39..a646395 100644 --- a/web/src/ui/roomview/RoomView.tsx +++ b/web/src/ui/roomview/RoomView.tsx @@ -41,8 +41,15 @@ const RoomView = ({ room, rightPanelResizeHandle, rightPanel }: RoomViewProps) = } } }, [roomContextData]) + const onClick = (evt: React.MouseEvent) => { + if (roomContextData.focusedEventRowID) { + roomContextData.setFocusedEventRowID(null) + evt.stopPropagation() + } + } return -
+
+
diff --git a/web/src/ui/roomview/RoomViewHeader.css b/web/src/ui/roomview/RoomViewHeader.css index aab85cd..2f4c4d6 100644 --- a/web/src/ui/roomview/RoomViewHeader.css +++ b/web/src/ui/roomview/RoomViewHeader.css @@ -5,6 +5,7 @@ div.room-header { padding-left: .5rem; border-bottom: 1px solid var(--border-color); overflow: hidden; + grid-area: header; > div.room-name-and-topic { flex: 1; diff --git a/web/src/ui/roomview/roomcontext.ts b/web/src/ui/roomview/roomcontext.ts index d38ff54..0758612 100644 --- a/web/src/ui/roomview/roomcontext.ts +++ b/web/src/ui/roomview/roomcontext.ts @@ -15,7 +15,7 @@ // along with this program. If not, see . import { RefObject, createContext, createRef, use } from "react" import { RoomStateStore } from "@/api/statestore" -import { EventID, MemDBEvent } from "@/api/types" +import { EventID, EventRowID, MemDBEvent } from "@/api/types" import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts" import { escapeMarkdown } from "@/util/markdown.ts" @@ -28,6 +28,8 @@ export class RoomContextData { public setReplyTo: (eventID: EventID | null) => void = noop("setReplyTo") public setEditing: (evt: MemDBEvent | null) => void = noop("setEditing") public insertText: (text: string) => void = noop("insertText") + public directSetFocusedEventRowID: (eventRowID: EventRowID | null) => void = noop("setFocusedEventRowID") + public focusedEventRowID: EventRowID | null = null public readonly isEditing = new NonNullCachedEventDispatcher(false) public scrolledToBottom = true @@ -39,6 +41,11 @@ export class RoomContextData { } } + setFocusedEventRowID = (eventRowID: number | null) => { + this.directSetFocusedEventRowID(eventRowID) + this.focusedEventRowID = eventRowID + } + appendMentionToComposer = (evt: React.MouseEvent) => { const targetUser = evt.currentTarget.getAttribute("data-target-user") if (!targetUser) { diff --git a/web/src/ui/timeline/TimelineEvent.css b/web/src/ui/timeline/TimelineEvent.css index 1bc1acf..c5b38b4 100644 --- a/web/src/ui/timeline/TimelineEvent.css +++ b/web/src/ui/timeline/TimelineEvent.css @@ -24,7 +24,7 @@ div.timeline-event { transition: background-color 1s; } - &:hover { + &:hover:not(.no-hover), &.focused-event { background-color: var(--timeline-hover-bg-color); &.highlight { diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 38b15d7..524d84b 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { JSX, use, useState } from "react" +import { createPortal } from "react-dom" import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" @@ -38,6 +39,7 @@ export interface TimelineEventProps { prevEvt: MemDBEvent | null disableMenu?: boolean smallReplies?: boolean + isFocused?: boolean } const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "medium" }) @@ -73,7 +75,7 @@ const EventSendStatus = ({ evt }: { evt: MemDBEvent }) => { } } -const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEventProps) => { +const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: TimelineEventProps) => { const roomCtx = useRoomContext() const client = use(ClientContext)! const mainScreen = use(MainScreenContext) @@ -107,21 +109,8 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven return } mouseEvt.preventDefault() - if (window.hackyOpenEventContextMenu === evt.event_id) { - window.closeModal() - window.hackyOpenEventContextMenu = undefined - } else { - openModal({ - content: , - captureInput: false, - onClose: () => { - if (window.hackyOpenEventContextMenu === evt.event_id) { - window.hackyOpenEventContextMenu = undefined - } - }, - }) - window.hackyOpenEventContextMenu = evt.event_id - } + mouseEvt.stopPropagation() + roomCtx.setFocusedEventRowID(roomCtx.focusedEventRowID === evt.rowid ? null : evt.rowid) } const memberEvt = useRoomMember(client, roomCtx.store, evt.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined @@ -144,6 +133,12 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven if (evt.sender === client.userID) { wrapperClassNames.push("own-event") } + if (isMobileDevice || disableMenu) { + wrapperClassNames.push("no-hover") + } + if (isFocused) { + wrapperClassNames.push("focused-event") + } let dateSeparator = null const prevEvtDate = prevEvt ? new Date(prevEvt.timestamp) : null if (prevEvtDate && ( @@ -207,6 +202,10 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven >
} + {isMobileDevice && isFocused && createPortal( + , + document.getElementById("mobile-event-menu-container")!, + )} {replyAboveMessage} {renderAvatar &&
{ const timeline = useRoomTimeline(room) const client = use(ClientContext)! const [isLoadingHistory, setLoadingHistory] = useState(false) + const [focusedEventRowID, directSetFocusedEventRowID] = useState(null) const loadHistory = useCallback(() => { setLoadingHistory(true) client.loadMoreHistory(room.roomID) @@ -68,6 +69,9 @@ const TimelineView = () => { } prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0 }, [client.userID, roomCtx, timeline]) + useEffect(() => { + roomCtx.directSetFocusedEventRowID = directSetFocusedEventRowID + }, []) useEffect(() => { const newestEvent = timeline[timeline.length - 1] if ( @@ -126,7 +130,11 @@ const TimelineView = () => { return null } const thisEvt = prevEvt = entry return thisEvt diff --git a/web/src/ui/timeline/content/MediaMessageBody.tsx b/web/src/ui/timeline/content/MediaMessageBody.tsx index 9a8401a..fbe267a 100644 --- a/web/src/ui/timeline/content/MediaMessageBody.tsx +++ b/web/src/ui/timeline/content/MediaMessageBody.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 { CSSProperties, JSX, use, useReducer } from "react" +import React, { CSSProperties, JSX, use, useReducer, useState } from "react" import { Blurhash } from "react-blurhash" import { GridLoader } from "react-spinners" import { usePreference } from "@/api/statestore" @@ -49,7 +49,7 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => { const supportsClickToShow = supportsLoadingPlaceholder || content.msgtype === "m.video" const showPreviewsByDefault = usePreference(client.store, room, "show_media_previews") const [loaded, onLoad] = useReducer(switchToTrue, !supportsLoadingPlaceholder) - const [clickedShow, onClickShow] = useReducer(switchToTrue, false) + const [clickedShow, setClickedShow] = useState(false) let contentWarning = content["town.robin.msc3725.content_warning"] if (content["page.codeberg.everypizza.msc4193.spoiler"]) { @@ -72,8 +72,12 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => { const blurhash = ensureString( content.info?.["xyz.amorgan.blurhash"] ?? content.info?.thumbnail_info?.["xyz.amorgan.blurhash"], ) + const onClick = !clickedShow ? (evt: React.MouseEvent) => { + setClickedShow(true) + evt.stopPropagation() + } : undefined placeholderElem =
{(blurhash && containerStyle.width) ? void } @@ -31,9 +34,7 @@ export const EventHoverMenu = ({ evt, roomCtx, setForceOpen }: EventHoverMenuPro return
{elements}
} -interface EventContextMenuProps { - evt: MemDBEvent - roomCtx: RoomContextData +interface EventContextMenuProps extends BaseEventMenuProps { style: CSSProperties } @@ -53,12 +54,13 @@ export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) =>
} -export const EventFixedMenu = ({ evt, roomCtx }: Omit) => { +export const EventFixedMenu = ({ evt, roomCtx }: BaseEventMenuProps) => { const client = use(ClientContext)! const primary = usePrimaryItems(client, roomCtx, evt, false, true, undefined, undefined) const secondary = useSecondaryItems(client, roomCtx, evt, false) return
{primary} +
{secondary}
} diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index 44bb8c0..f42eac3 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -20,12 +20,8 @@ div.event-hover-menu, div.event-fixed-menu { } div.event-fixed-menu { - position: fixed; - inset: 0 0 auto; - height: 3.5rem; padding: .25rem; border-bottom: 1px solid var(--border-color); - box-sizing: border-box; justify-content: right; flex-direction: row-reverse; overflow-x: auto;