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;
|
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])
|
}, [roomContextData])
|
||||||
|
const onClick = (evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (roomContextData.focusedEventRowID) {
|
||||||
|
roomContextData.setFocusedEventRowID(null)
|
||||||
|
evt.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
return <RoomContext value={roomContextData}>
|
return <RoomContext value={roomContextData}>
|
||||||
<div className="room-view">
|
<div className="room-view" onClick={onClick}>
|
||||||
|
<div id="mobile-event-menu-container"/>
|
||||||
<RoomViewHeader room={room}/>
|
<RoomViewHeader room={room}/>
|
||||||
<TimelineView/>
|
<TimelineView/>
|
||||||
<MessageComposer/>
|
<MessageComposer/>
|
||||||
|
|
|
@ -5,6 +5,7 @@ div.room-header {
|
||||||
padding-left: .5rem;
|
padding-left: .5rem;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
grid-area: header;
|
||||||
|
|
||||||
> div.room-name-and-topic {
|
> div.room-name-and-topic {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { RefObject, createContext, createRef, use } from "react"
|
import { RefObject, createContext, createRef, use } from "react"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
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 { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||||
import { escapeMarkdown } from "@/util/markdown.ts"
|
import { escapeMarkdown } from "@/util/markdown.ts"
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ export class RoomContextData {
|
||||||
public setReplyTo: (eventID: EventID | null) => void = noop("setReplyTo")
|
public setReplyTo: (eventID: EventID | null) => void = noop("setReplyTo")
|
||||||
public setEditing: (evt: MemDBEvent | null) => void = noop("setEditing")
|
public setEditing: (evt: MemDBEvent | null) => void = noop("setEditing")
|
||||||
public insertText: (text: string) => void = noop("insertText")
|
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 readonly isEditing = new NonNullCachedEventDispatcher<boolean>(false)
|
||||||
public scrolledToBottom = true
|
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>) => {
|
appendMentionToComposer = (evt: React.MouseEvent<HTMLSpanElement>) => {
|
||||||
const targetUser = evt.currentTarget.getAttribute("data-target-user")
|
const targetUser = evt.currentTarget.getAttribute("data-target-user")
|
||||||
if (!targetUser) {
|
if (!targetUser) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ div.timeline-event {
|
||||||
transition: background-color 1s;
|
transition: background-color 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(.no-hover), &.focused-event {
|
||||||
background-color: var(--timeline-hover-bg-color);
|
background-color: var(--timeline-hover-bg-color);
|
||||||
|
|
||||||
&.highlight {
|
&.highlight {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { JSX, use, useState } from "react"
|
import React, { JSX, use, useState } from "react"
|
||||||
|
import { createPortal } from "react-dom"
|
||||||
import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
|
import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts"
|
||||||
import { useRoomMember } from "@/api/statestore"
|
import { useRoomMember } from "@/api/statestore"
|
||||||
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||||
|
@ -38,6 +39,7 @@ export interface TimelineEventProps {
|
||||||
prevEvt: MemDBEvent | null
|
prevEvt: MemDBEvent | null
|
||||||
disableMenu?: boolean
|
disableMenu?: boolean
|
||||||
smallReplies?: boolean
|
smallReplies?: boolean
|
||||||
|
isFocused?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "medium" })
|
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 roomCtx = useRoomContext()
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
|
@ -107,21 +109,8 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mouseEvt.preventDefault()
|
mouseEvt.preventDefault()
|
||||||
if (window.hackyOpenEventContextMenu === evt.event_id) {
|
mouseEvt.stopPropagation()
|
||||||
window.closeModal()
|
roomCtx.setFocusedEventRowID(roomCtx.focusedEventRowID === evt.rowid ? null : evt.rowid)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
||||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
|
@ -144,6 +133,12 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
if (evt.sender === client.userID) {
|
if (evt.sender === client.userID) {
|
||||||
wrapperClassNames.push("own-event")
|
wrapperClassNames.push("own-event")
|
||||||
}
|
}
|
||||||
|
if (isMobileDevice || disableMenu) {
|
||||||
|
wrapperClassNames.push("no-hover")
|
||||||
|
}
|
||||||
|
if (isFocused) {
|
||||||
|
wrapperClassNames.push("focused-event")
|
||||||
|
}
|
||||||
let dateSeparator = null
|
let dateSeparator = null
|
||||||
const prevEvtDate = prevEvt ? new Date(prevEvt.timestamp) : null
|
const prevEvtDate = prevEvt ? new Date(prevEvt.timestamp) : null
|
||||||
if (prevEvtDate && (
|
if (prevEvtDate && (
|
||||||
|
@ -207,6 +202,10 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
>
|
>
|
||||||
<EventHoverMenu evt={evt} roomCtx={roomCtx} setForceOpen={setForceContextMenuOpen}/>
|
<EventHoverMenu evt={evt} roomCtx={roomCtx} setForceOpen={setForceContextMenuOpen}/>
|
||||||
</div>}
|
</div>}
|
||||||
|
{isMobileDevice && isFocused && createPortal(
|
||||||
|
<EventFixedMenu evt={evt} roomCtx={roomCtx} />,
|
||||||
|
document.getElementById("mobile-event-menu-container")!,
|
||||||
|
)}
|
||||||
{replyAboveMessage}
|
{replyAboveMessage}
|
||||||
{renderAvatar && <div
|
{renderAvatar && <div
|
||||||
className="sender-avatar"
|
className="sender-avatar"
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import { use, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
|
import { use, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||||
import { ScaleLoader } from "react-spinners"
|
import { ScaleLoader } from "react-spinners"
|
||||||
import { usePreference, useRoomTimeline } from "@/api/statestore"
|
import { usePreference, useRoomTimeline } from "@/api/statestore"
|
||||||
import { MemDBEvent } from "@/api/types"
|
import { EventRowID, MemDBEvent } from "@/api/types"
|
||||||
import useFocus from "@/util/focus.ts"
|
import useFocus from "@/util/focus.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||||
|
@ -29,6 +29,7 @@ const TimelineView = () => {
|
||||||
const timeline = useRoomTimeline(room)
|
const timeline = useRoomTimeline(room)
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const [isLoadingHistory, setLoadingHistory] = useState(false)
|
const [isLoadingHistory, setLoadingHistory] = useState(false)
|
||||||
|
const [focusedEventRowID, directSetFocusedEventRowID] = useState<EventRowID | null>(null)
|
||||||
const loadHistory = useCallback(() => {
|
const loadHistory = useCallback(() => {
|
||||||
setLoadingHistory(true)
|
setLoadingHistory(true)
|
||||||
client.loadMoreHistory(room.roomID)
|
client.loadMoreHistory(room.roomID)
|
||||||
|
@ -68,6 +69,9 @@ const TimelineView = () => {
|
||||||
}
|
}
|
||||||
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
||||||
}, [client.userID, roomCtx, timeline])
|
}, [client.userID, roomCtx, timeline])
|
||||||
|
useEffect(() => {
|
||||||
|
roomCtx.directSetFocusedEventRowID = directSetFocusedEventRowID
|
||||||
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newestEvent = timeline[timeline.length - 1]
|
const newestEvent = timeline[timeline.length - 1]
|
||||||
if (
|
if (
|
||||||
|
@ -126,7 +130,11 @@ const TimelineView = () => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const thisEvt = <TimelineEvent
|
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
|
prevEvt = entry
|
||||||
return thisEvt
|
return thisEvt
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { Blurhash } from "react-blurhash"
|
||||||
import { GridLoader } from "react-spinners"
|
import { GridLoader } from "react-spinners"
|
||||||
import { usePreference } from "@/api/statestore"
|
import { usePreference } from "@/api/statestore"
|
||||||
|
@ -49,7 +49,7 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => {
|
||||||
const supportsClickToShow = supportsLoadingPlaceholder || content.msgtype === "m.video"
|
const supportsClickToShow = supportsLoadingPlaceholder || content.msgtype === "m.video"
|
||||||
const showPreviewsByDefault = usePreference(client.store, room, "show_media_previews")
|
const showPreviewsByDefault = usePreference(client.store, room, "show_media_previews")
|
||||||
const [loaded, onLoad] = useReducer(switchToTrue, !supportsLoadingPlaceholder)
|
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"]
|
let contentWarning = content["town.robin.msc3725.content_warning"]
|
||||||
if (content["page.codeberg.everypizza.msc4193.spoiler"]) {
|
if (content["page.codeberg.everypizza.msc4193.spoiler"]) {
|
||||||
|
@ -72,8 +72,12 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => {
|
||||||
const blurhash = ensureString(
|
const blurhash = ensureString(
|
||||||
content.info?.["xyz.amorgan.blurhash"] ?? content.info?.thumbnail_info?.["xyz.amorgan.blurhash"],
|
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
|
placeholderElem = <div
|
||||||
onClick={onClickShow}
|
onClick={onClick}
|
||||||
className="placeholder"
|
className="placeholder"
|
||||||
>
|
>
|
||||||
{(blurhash && containerStyle.width) ? <Blurhash
|
{(blurhash && containerStyle.width) ? <Blurhash
|
||||||
|
|
|
@ -20,9 +20,12 @@ import { RoomContextData } from "../../roomview/roomcontext.ts"
|
||||||
import { usePrimaryItems } from "./usePrimaryItems.tsx"
|
import { usePrimaryItems } from "./usePrimaryItems.tsx"
|
||||||
import { useSecondaryItems } from "./useSecondaryItems.tsx"
|
import { useSecondaryItems } from "./useSecondaryItems.tsx"
|
||||||
|
|
||||||
interface EventHoverMenuProps {
|
interface BaseEventMenuProps {
|
||||||
evt: MemDBEvent
|
evt: MemDBEvent
|
||||||
roomCtx: RoomContextData
|
roomCtx: RoomContextData
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventHoverMenuProps extends BaseEventMenuProps {
|
||||||
setForceOpen: (forceOpen: boolean) => void
|
setForceOpen: (forceOpen: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,9 +34,7 @@ export const EventHoverMenu = ({ evt, roomCtx, setForceOpen }: EventHoverMenuPro
|
||||||
return <div className="event-hover-menu">{elements}</div>
|
return <div className="event-hover-menu">{elements}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventContextMenuProps {
|
interface EventContextMenuProps extends BaseEventMenuProps {
|
||||||
evt: MemDBEvent
|
|
||||||
roomCtx: RoomContextData
|
|
||||||
style: CSSProperties
|
style: CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,12 +54,13 @@ export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) =>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventFixedMenu = ({ evt, roomCtx }: Omit<EventContextMenuProps, "style">) => {
|
export const EventFixedMenu = ({ evt, roomCtx }: BaseEventMenuProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const primary = usePrimaryItems(client, roomCtx, evt, false, true, undefined, undefined)
|
const primary = usePrimaryItems(client, roomCtx, evt, false, true, undefined, undefined)
|
||||||
const secondary = useSecondaryItems(client, roomCtx, evt, false)
|
const secondary = useSecondaryItems(client, roomCtx, evt, false)
|
||||||
return <div className="event-fixed-menu">
|
return <div className="event-fixed-menu">
|
||||||
{primary}
|
{primary}
|
||||||
|
<div className="vertical-line"/>
|
||||||
{secondary}
|
{secondary}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,8 @@ div.event-hover-menu, div.event-fixed-menu {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.event-fixed-menu {
|
div.event-fixed-menu {
|
||||||
position: fixed;
|
|
||||||
inset: 0 0 auto;
|
|
||||||
height: 3.5rem;
|
|
||||||
padding: .25rem;
|
padding: .25rem;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
box-sizing: border-box;
|
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
Loading…
Add table
Reference in a new issue