mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/timeline: use custom message focus state on mobile to match context menu state
This commit is contained in:
parent
c25ab057dc
commit
df551fe4cb
10 changed files with 69 additions and 33 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,15 @@ const RoomView = ({ room, rightPanelResizeHandle, rightPanel }: RoomViewProps) =
|
|||
}
|
||||
}
|
||||
}, [roomContextData])
|
||||
const onClick = (evt: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (roomContextData.focusedEventRowID) {
|
||||
roomContextData.setFocusedEventRowID(null)
|
||||
evt.stopPropagation()
|
||||
}
|
||||
}
|
||||
return <RoomContext value={roomContextData}>
|
||||
<div className="room-view">
|
||||
<div className="room-view" onClick={onClick}>
|
||||
<div id="mobile-event-menu-container"/>
|
||||
<RoomViewHeader room={room}/>
|
||||
<TimelineView/>
|
||||
<MessageComposer/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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<boolean>(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<HTMLSpanElement>) => {
|
||||
const targetUser = evt.currentTarget.getAttribute("data-target-user")
|
||||
if (!targetUser) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,6 +14,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 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: <EventFixedMenu evt={evt} roomCtx={roomCtx} />,
|
||||
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
|
|||
>
|
||||
<EventHoverMenu evt={evt} roomCtx={roomCtx} setForceOpen={setForceContextMenuOpen}/>
|
||||
</div>}
|
||||
{isMobileDevice && isFocused && createPortal(
|
||||
<EventFixedMenu evt={evt} roomCtx={roomCtx} />,
|
||||
document.getElementById("mobile-event-menu-container")!,
|
||||
)}
|
||||
{replyAboveMessage}
|
||||
{renderAvatar && <div
|
||||
className="sender-avatar"
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import { use, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
import { ScaleLoader } from "react-spinners"
|
||||
import { usePreference, useRoomTimeline } from "@/api/statestore"
|
||||
import { MemDBEvent } from "@/api/types"
|
||||
import { EventRowID, MemDBEvent } from "@/api/types"
|
||||
import useFocus from "@/util/focus.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||
|
@ -29,6 +29,7 @@ const TimelineView = () => {
|
|||
const timeline = useRoomTimeline(room)
|
||||
const client = use(ClientContext)!
|
||||
const [isLoadingHistory, setLoadingHistory] = useState(false)
|
||||
const [focusedEventRowID, directSetFocusedEventRowID] = useState<EventRowID | null>(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 = <TimelineEvent
|
||||
key={entry.rowid} evt={entry} prevEvt={prevEvt} smallReplies={smallReplies}
|
||||
key={entry.rowid}
|
||||
evt={entry}
|
||||
prevEvt={prevEvt}
|
||||
smallReplies={smallReplies}
|
||||
isFocused={focusedEventRowID === entry.rowid}
|
||||
/>
|
||||
prevEvt = entry
|
||||
return thisEvt
|
||||
|
|
|
@ -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 { 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<HTMLDivElement>) => {
|
||||
setClickedShow(true)
|
||||
evt.stopPropagation()
|
||||
} : undefined
|
||||
placeholderElem = <div
|
||||
onClick={onClickShow}
|
||||
onClick={onClick}
|
||||
className="placeholder"
|
||||
>
|
||||
{(blurhash && containerStyle.width) ? <Blurhash
|
||||
|
|
|
@ -20,9 +20,12 @@ import { RoomContextData } from "../../roomview/roomcontext.ts"
|
|||
import { usePrimaryItems } from "./usePrimaryItems.tsx"
|
||||
import { useSecondaryItems } from "./useSecondaryItems.tsx"
|
||||
|
||||
interface EventHoverMenuProps {
|
||||
interface BaseEventMenuProps {
|
||||
evt: MemDBEvent
|
||||
roomCtx: RoomContextData
|
||||
}
|
||||
|
||||
interface EventHoverMenuProps extends BaseEventMenuProps {
|
||||
setForceOpen: (forceOpen: boolean) => void
|
||||
}
|
||||
|
||||
|
@ -31,9 +34,7 @@ export const EventHoverMenu = ({ evt, roomCtx, setForceOpen }: EventHoverMenuPro
|
|||
return <div className="event-hover-menu">{elements}</div>
|
||||
}
|
||||
|
||||
interface EventContextMenuProps {
|
||||
evt: MemDBEvent
|
||||
roomCtx: RoomContextData
|
||||
interface EventContextMenuProps extends BaseEventMenuProps {
|
||||
style: CSSProperties
|
||||
}
|
||||
|
||||
|
@ -53,12 +54,13 @@ export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) =>
|
|||
</div>
|
||||
}
|
||||
|
||||
export const EventFixedMenu = ({ evt, roomCtx }: Omit<EventContextMenuProps, "style">) => {
|
||||
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 <div className="event-fixed-menu">
|
||||
{primary}
|
||||
<div className="vertical-line"/>
|
||||
{secondary}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue