mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/timeline: add special message menu for mobile
This commit is contained in:
parent
e750c19e8a
commit
a1bddd6b6b
6 changed files with 68 additions and 20 deletions
|
@ -27,7 +27,7 @@ import ReadReceipts from "./ReadReceipts.tsx"
|
||||||
import { ReplyIDBody } from "./ReplyBody.tsx"
|
import { ReplyIDBody } from "./ReplyBody.tsx"
|
||||||
import URLPreviews from "./URLPreviews.tsx"
|
import URLPreviews from "./URLPreviews.tsx"
|
||||||
import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content"
|
import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content"
|
||||||
import { EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu"
|
import { EventFixedMenu, EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu"
|
||||||
import ErrorIcon from "@/icons/error.svg?react"
|
import ErrorIcon from "@/icons/error.svg?react"
|
||||||
import PendingIcon from "@/icons/pending.svg?react"
|
import PendingIcon from "@/icons/pending.svg?react"
|
||||||
import SentIcon from "@/icons/sent.svg?react"
|
import SentIcon from "@/icons/sent.svg?react"
|
||||||
|
@ -98,6 +98,19 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
/>,
|
/>,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const onClick = (mouseEvt: React.MouseEvent) => {
|
||||||
|
const targetElem = mouseEvt.target as HTMLElement
|
||||||
|
if (
|
||||||
|
targetElem.tagName === "A"
|
||||||
|
|| targetElem.tagName === "IMG"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mouseEvt.preventDefault()
|
||||||
|
openModal({
|
||||||
|
content: <EventFixedMenu evt={evt} roomCtx={roomCtx} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
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
|
||||||
const BodyType = getBodyType(evt)
|
const BodyType = getBodyType(evt)
|
||||||
|
@ -175,11 +188,12 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven
|
||||||
data-event-id={evt.event_id}
|
data-event-id={evt.event_id}
|
||||||
className={wrapperClassNames.join(" ")}
|
className={wrapperClassNames.join(" ")}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
onClick={!disableMenu && isMobileDevice ? onClick : undefined}
|
||||||
>
|
>
|
||||||
{!disableMenu && !isMobileDevice && <div
|
{!disableMenu && !isMobileDevice && <div
|
||||||
className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}
|
className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}
|
||||||
>
|
>
|
||||||
<EventHoverMenu evt={evt} setForceOpen={setForceContextMenuOpen}/>
|
<EventHoverMenu evt={evt} roomCtx={roomCtx} setForceOpen={setForceContextMenuOpen}/>
|
||||||
</div>}
|
</div>}
|
||||||
{replyAboveMessage}
|
{replyAboveMessage}
|
||||||
{renderAvatar && <div
|
{renderAvatar && <div
|
||||||
|
|
|
@ -16,17 +16,18 @@
|
||||||
import { CSSProperties, use } from "react"
|
import { CSSProperties, use } from "react"
|
||||||
import { MemDBEvent } from "@/api/types"
|
import { MemDBEvent } from "@/api/types"
|
||||||
import ClientContext from "../../ClientContext.ts"
|
import ClientContext from "../../ClientContext.ts"
|
||||||
import { RoomContextData, useRoomContext } from "../../roomview/roomcontext.ts"
|
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 EventHoverMenuProps {
|
||||||
evt: MemDBEvent
|
evt: MemDBEvent
|
||||||
|
roomCtx: RoomContextData
|
||||||
setForceOpen: (forceOpen: boolean) => void
|
setForceOpen: (forceOpen: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventHoverMenu = ({ evt, setForceOpen }: EventHoverMenuProps) => {
|
export const EventHoverMenu = ({ evt, roomCtx, setForceOpen }: EventHoverMenuProps) => {
|
||||||
const elements = usePrimaryItems(use(ClientContext)!, useRoomContext(), evt, true, undefined, setForceOpen)
|
const elements = usePrimaryItems(use(ClientContext)!, roomCtx, evt, true, false, undefined, setForceOpen)
|
||||||
return <div className="event-hover-menu">{elements}</div>
|
return <div className="event-hover-menu">{elements}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ export const EventExtraMenu = ({ evt, roomCtx, style }: EventContextMenuProps) =
|
||||||
|
|
||||||
export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => {
|
export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const primary = usePrimaryItems(client, roomCtx, evt, false, style, undefined)
|
const primary = usePrimaryItems(client, roomCtx, evt, false, false, style, undefined)
|
||||||
const secondary = useSecondaryItems(client, roomCtx, evt)
|
const secondary = useSecondaryItems(client, roomCtx, evt)
|
||||||
return <div style={style} className="event-context-menu full">
|
return <div style={style} className="event-context-menu full">
|
||||||
{primary}
|
{primary}
|
||||||
|
@ -51,3 +52,13 @@ export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) =>
|
||||||
{secondary}
|
{secondary}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EventFixedMenu = ({ evt, roomCtx }: Omit<EventContextMenuProps, "style">) => {
|
||||||
|
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}
|
||||||
|
{secondary}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
|
@ -2,13 +2,9 @@ div.event-hover-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: .5rem;
|
right: .5rem;
|
||||||
top: -1.5rem;
|
top: -1.5rem;
|
||||||
background-color: var(--background-color);
|
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
display: flex;
|
|
||||||
gap: .25rem;
|
|
||||||
padding: .125rem;
|
padding: .125rem;
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
|
@ -16,6 +12,28 @@ div.event-hover-menu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.event-hover-menu, div.event-fixed-menu {
|
||||||
|
display: flex;
|
||||||
|
gap: .25rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.event-fixed-menu {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
height: 3rem;
|
||||||
|
padding: .25rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
justify-content: right;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div.event-context-menu {
|
div.event-context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
|
|
|
@ -13,5 +13,5 @@
|
||||||
//
|
//
|
||||||
// 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/>.
|
||||||
export { EventExtraMenu, EventFullMenu, EventHoverMenu } from "./EventMenu.tsx"
|
export { EventExtraMenu, EventFixedMenu, EventFullMenu, EventHoverMenu } from "./EventMenu.tsx"
|
||||||
export { getModalStyleFromMouse } from "./util.ts"
|
export { getModalStyleFromMouse } from "./util.ts"
|
||||||
|
|
|
@ -37,9 +37,11 @@ export const usePrimaryItems = (
|
||||||
roomCtx: RoomContextData,
|
roomCtx: RoomContextData,
|
||||||
evt: MemDBEvent,
|
evt: MemDBEvent,
|
||||||
isHover: boolean,
|
isHover: boolean,
|
||||||
|
isFixed: boolean,
|
||||||
style?: CSSProperties,
|
style?: CSSProperties,
|
||||||
setForceOpen?: (forceOpen: boolean) => void,
|
setForceOpen?: (forceOpen: boolean) => void,
|
||||||
) => {
|
) => {
|
||||||
|
const names = !isHover && !isFixed
|
||||||
const closeModal = !isHover ? use(ModalCloseContext) : noop
|
const closeModal = !isHover ? use(ModalCloseContext) : noop
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
|
|
||||||
|
@ -108,11 +110,11 @@ export const usePrimaryItems = (
|
||||||
return <>
|
return <>
|
||||||
{didFail && <button onClick={onClickResend} title="Resend message">
|
{didFail && <button onClick={onClickResend} title="Resend message">
|
||||||
<RefreshIcon/>
|
<RefreshIcon/>
|
||||||
{!isHover && "Resend"}
|
{names && "Resend"}
|
||||||
</button>}
|
</button>}
|
||||||
{canReact && <button disabled={isPending} title={pendingTitle} onClick={onClickReact}>
|
{canReact && <button disabled={isPending} title={pendingTitle} onClick={onClickReact}>
|
||||||
<ReactIcon/>
|
<ReactIcon/>
|
||||||
{!isHover && "React"}
|
{names && "React"}
|
||||||
</button>}
|
</button>}
|
||||||
{canSend && <button
|
{canSend && <button
|
||||||
disabled={isEditing || isPending}
|
disabled={isEditing || isPending}
|
||||||
|
@ -120,11 +122,11 @@ export const usePrimaryItems = (
|
||||||
onClick={onClickReply}
|
onClick={onClickReply}
|
||||||
>
|
>
|
||||||
<ReplyIcon/>
|
<ReplyIcon/>
|
||||||
{!isHover && "Reply"}
|
{names && "Reply"}
|
||||||
</button>}
|
</button>}
|
||||||
{canEdit && <button onClick={onClickEdit} disabled={isPending} title={pendingTitle}>
|
{canEdit && <button onClick={onClickEdit} disabled={isPending} title={pendingTitle}>
|
||||||
<EditIcon/>
|
<EditIcon/>
|
||||||
{!isHover && "Edit"}
|
{names && "Edit"}
|
||||||
</button>}
|
</button>}
|
||||||
{isHover && <button onClick={onClickMore}><MoreIcon/></button>}
|
{isHover && <button onClick={onClickMore}><MoreIcon/></button>}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const useSecondaryItems = (
|
||||||
client: Client,
|
client: Client,
|
||||||
roomCtx: RoomContextData,
|
roomCtx: RoomContextData,
|
||||||
evt: MemDBEvent,
|
evt: MemDBEvent,
|
||||||
|
names = true,
|
||||||
) => {
|
) => {
|
||||||
const closeModal = use(ModalCloseContext)
|
const closeModal = use(ModalCloseContext)
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
|
@ -102,20 +103,22 @@ export const useSecondaryItems = (
|
||||||
&& (evt.sender === client.userID || ownPL >= redactOtherPL)
|
&& (evt.sender === client.userID || ownPL >= redactOtherPL)
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<button onClick={onClickViewSource}><ViewSourceIcon/>View source</button>
|
<button onClick={onClickViewSource}><ViewSourceIcon/>{names && "View source"}</button>
|
||||||
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
||||||
? <button onClick={onClickPin(false)}>
|
? <button onClick={onClickPin(false)}>
|
||||||
<UnpinIcon/>Unpin message
|
<UnpinIcon/>{names && "Unpin message"}
|
||||||
</button>
|
</button>
|
||||||
: <button onClick={onClickPin(true)} title={pendingTitle} disabled={isPending}>
|
: <button onClick={onClickPin(true)} title={pendingTitle} disabled={isPending}>
|
||||||
<PinIcon/>Pin message
|
<PinIcon/>{names && "Pin message"}
|
||||||
</button>)}
|
</button>)}
|
||||||
<button onClick={onClickReport} disabled={isPending} title={pendingTitle}><ReportIcon/>Report</button>
|
<button onClick={onClickReport} disabled={isPending} title={pendingTitle}>
|
||||||
|
<ReportIcon/>{names && "Report"}
|
||||||
|
</button>
|
||||||
{canRedact && <button
|
{canRedact && <button
|
||||||
onClick={onClickRedact}
|
onClick={onClickRedact}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
title={pendingTitle}
|
title={pendingTitle}
|
||||||
className="redact-button"
|
className="redact-button"
|
||||||
><DeleteIcon/>Remove</button>}
|
><DeleteIcon/>{names && "Remove"}</button>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue