mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web: add share event button (#589)
Co-authored-by: Tulir Asokan <tulir@maunium.net>
This commit is contained in:
parent
865b2e4fdf
commit
b7f939f480
5 changed files with 154 additions and 2 deletions
|
@ -18,7 +18,7 @@ import { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji"
|
||||||
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||||
import toSearchableString from "@/util/searchablestring.ts"
|
import toSearchableString from "@/util/searchablestring.ts"
|
||||||
import Subscribable, { MultiSubscribable, NoDataSubscribable } from "@/util/subscribable.ts"
|
import Subscribable, { MultiSubscribable, NoDataSubscribable } from "@/util/subscribable.ts"
|
||||||
import { getDisplayname } from "@/util/validation.ts"
|
import { getDisplayname, getServerName } from "@/util/validation.ts"
|
||||||
import {
|
import {
|
||||||
ContentURI,
|
ContentURI,
|
||||||
DBReceipt,
|
DBReceipt,
|
||||||
|
@ -246,6 +246,35 @@ export class RoomStateStore {
|
||||||
return this.#membersCache ?? []
|
return this.#membersCache ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getViaServers(): string[] {
|
||||||
|
const ownServerName = getServerName(this.parent.userID)
|
||||||
|
const vias = [ownServerName]
|
||||||
|
const members = this.getMembers()
|
||||||
|
const memberCount = new Map<string, number>()
|
||||||
|
const powerLevels: PowerLevelEventContent = this.getStateEvent("m.room.power_levels", "")?.content ?? {}
|
||||||
|
const usersDefault = powerLevels.users_default ?? 0
|
||||||
|
let powerServer: string | undefined = undefined
|
||||||
|
for (const member of members) {
|
||||||
|
const serverName = getServerName(member.userID)
|
||||||
|
if (serverName !== ownServerName) {
|
||||||
|
if (!powerServer && (powerLevels?.users?.[member.userID] ?? usersDefault) > usersDefault) {
|
||||||
|
powerServer = serverName
|
||||||
|
vias.push(powerServer)
|
||||||
|
}
|
||||||
|
memberCount.set(serverName, (memberCount.get(serverName) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const servers = Array.from(memberCount.entries())
|
||||||
|
servers.sort(([, a], [, b]) => b - a)
|
||||||
|
for (const [serverName] of servers) {
|
||||||
|
if (serverName !== ownServerName && serverName !== powerServer) {
|
||||||
|
vias.push(serverName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vias
|
||||||
|
}
|
||||||
|
|
||||||
getPinnedEvents(): EventID[] {
|
getPinnedEvents(): EventID[] {
|
||||||
const pinnedList = this.getStateEvent("m.room.pinned_events", "")?.content?.pinned
|
const pinnedList = this.getStateEvent("m.room.pinned_events", "")?.content?.pinned
|
||||||
if (Array.isArray(pinnedList)) {
|
if (Array.isArray(pinnedList)) {
|
||||||
|
|
1
web/src/icons/share.svg
Normal file
1
web/src/icons/share.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M680-80q-50 0-85-35t-35-85q0-6 3-28L282-392q-16 15-37 23.5t-45 8.5q-50 0-85-35t-35-85q0-50 35-85t85-35q24 0 45 8.5t37 23.5l281-164q-2-7-2.5-13.5T560-760q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35q-24 0-45-8.5T598-672L317-508q2 7 2.5 13.5t.5 14.5q0 8-.5 14.5T317-452l281 164q16-15 37-23.5t45-8.5q50 0 85 35t35 85q0 50-35 85t-85 35Zm0-80q17 0 28.5-11.5T720-200q0-17-11.5-28.5T680-240q-17 0-28.5 11.5T640-200q0 17 11.5 28.5T680-160ZM200-440q17 0 28.5-11.5T240-480q0-17-11.5-28.5T200-520q-17 0-28.5 11.5T160-480q0 17 11.5 28.5T200-440Zm480-280q17 0 28.5-11.5T720-760q0-17-11.5-28.5T680-800q-17 0-28.5 11.5T640-760q0 17 11.5 28.5T680-720Zm0 520ZM200-480Zm480-280Z"/></svg>
|
After Width: | Height: | Size: 793 B |
65
web/src/ui/timeline/menu/ShareModal.tsx
Normal file
65
web/src/ui/timeline/menu/ShareModal.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import React, { use, useState } from "react"
|
||||||
|
import { MemDBEvent } from "@/api/types"
|
||||||
|
import { ModalCloseContext } from "@/ui/modal"
|
||||||
|
import TimelineEvent from "@/ui/timeline/TimelineEvent.tsx"
|
||||||
|
import Toggle from "@/ui/util/Toggle.tsx"
|
||||||
|
|
||||||
|
interface ConfirmWithMessageProps {
|
||||||
|
evt: MemDBEvent
|
||||||
|
title: string
|
||||||
|
confirmButton: string
|
||||||
|
onConfirm: (useMatrixTo: boolean, includeEvent: boolean) => void
|
||||||
|
generateLink: (useMatrixTo: boolean, includeEvent: boolean) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShareModal = ({ evt, title, confirmButton, onConfirm, generateLink }: ConfirmWithMessageProps) => {
|
||||||
|
const [useMatrixTo, setUseMatrixTo] = useState(false)
|
||||||
|
const [includeEvent, setIncludeEvent] = useState(true)
|
||||||
|
const closeModal = use(ModalCloseContext)
|
||||||
|
const onConfirmWrapped = (evt: React.FormEvent) => {
|
||||||
|
evt.preventDefault()
|
||||||
|
closeModal()
|
||||||
|
onConfirm(useMatrixTo, includeEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = generateLink(useMatrixTo, includeEvent)
|
||||||
|
return <form onSubmit={onConfirmWrapped}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<div className="timeline-event-container">
|
||||||
|
<TimelineEvent evt={evt} prevEvt={null} disableMenu={true}/>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Use matrix.to link</td>
|
||||||
|
<td>
|
||||||
|
<Toggle
|
||||||
|
id="useMatrixTo"
|
||||||
|
checked={useMatrixTo}
|
||||||
|
onChange={evt => setUseMatrixTo(evt.target.checked)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Link to this specific event</td>
|
||||||
|
<td>
|
||||||
|
<Toggle
|
||||||
|
id="shareEvent"
|
||||||
|
checked={includeEvent}
|
||||||
|
onChange={evt => setIncludeEvent(evt.target.checked)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="output-preview">
|
||||||
|
<span className="no-select">Preview: </span><code>{link}</code>
|
||||||
|
</div>
|
||||||
|
<div className="confirm-buttons">
|
||||||
|
<button type="button" onClick={closeModal}>Cancel</button>
|
||||||
|
<button type="submit">{confirmButton}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShareModal
|
|
@ -101,6 +101,7 @@ div.confirm-message-modal > form {
|
||||||
|
|
||||||
> div.timeline-event {
|
> div.timeline-event {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,4 +119,14 @@ div.confirm-message-modal > form {
|
||||||
padding: .5rem 1rem;
|
padding: .5rem 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> div.output-preview {
|
||||||
|
> span.no-select {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> code {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,13 @@ import { ModalCloseContext, ModalContext } from "../../modal"
|
||||||
import { RoomContext, RoomContextData } from "../../roomview/roomcontext.ts"
|
import { RoomContext, RoomContextData } from "../../roomview/roomcontext.ts"
|
||||||
import JSONView from "../../util/JSONView.tsx"
|
import JSONView from "../../util/JSONView.tsx"
|
||||||
import ConfirmWithMessageModal from "./ConfirmWithMessageModal.tsx"
|
import ConfirmWithMessageModal from "./ConfirmWithMessageModal.tsx"
|
||||||
|
import ShareModal from "./ShareModal.tsx"
|
||||||
import { getPending, getPowerLevels } from "./util.ts"
|
import { getPending, getPowerLevels } from "./util.ts"
|
||||||
import ViewSourceIcon from "@/icons/code.svg?react"
|
import ViewSourceIcon from "@/icons/code.svg?react"
|
||||||
import DeleteIcon from "@/icons/delete.svg?react"
|
import DeleteIcon from "@/icons/delete.svg?react"
|
||||||
import PinIcon from "@/icons/pin.svg?react"
|
import PinIcon from "@/icons/pin.svg?react"
|
||||||
import ReportIcon from "@/icons/report.svg?react"
|
import ReportIcon from "@/icons/report.svg?react"
|
||||||
|
import ShareIcon from "@/icons/share.svg?react"
|
||||||
import UnpinIcon from "@/icons/unpin.svg?react"
|
import UnpinIcon from "@/icons/unpin.svg?react"
|
||||||
|
|
||||||
export const useSecondaryItems = (
|
export const useSecondaryItems = (
|
||||||
|
@ -40,7 +42,7 @@ export const useSecondaryItems = (
|
||||||
openModal({
|
openModal({
|
||||||
dimmed: true,
|
dimmed: true,
|
||||||
boxed: true,
|
boxed: true,
|
||||||
content: <JSONView data={evt} />,
|
content: <JSONView data={evt}/>,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const onClickReport = () => {
|
const onClickReport = () => {
|
||||||
|
@ -89,6 +91,49 @@ export const useSecondaryItems = (
|
||||||
.catch(err => window.alert(`Failed to ${pin ? "pin" : "unpin"} message: ${err}`))
|
.catch(err => window.alert(`Failed to ${pin ? "pin" : "unpin"} message: ${err}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClickShareEvent = () => {
|
||||||
|
const generateLink = (useMatrixTo: boolean, includeEvent: boolean) => {
|
||||||
|
const isRoomIDLink = true
|
||||||
|
let generatedURL = useMatrixTo ? "https://matrix.to/#/" : "matrix:roomid/"
|
||||||
|
if (useMatrixTo) {
|
||||||
|
generatedURL += evt.room_id
|
||||||
|
} else {
|
||||||
|
generatedURL += `${evt.room_id.slice(1)}`
|
||||||
|
}
|
||||||
|
if (includeEvent) {
|
||||||
|
if (useMatrixTo) {
|
||||||
|
generatedURL += `/${evt.event_id}`
|
||||||
|
} else {
|
||||||
|
generatedURL += `/e/${evt.event_id.slice(1)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRoomIDLink) {
|
||||||
|
generatedURL += "?" + new URLSearchParams(
|
||||||
|
roomCtx.store.getViaServers().map(server => ["via", server]),
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
return generatedURL
|
||||||
|
}
|
||||||
|
openModal({
|
||||||
|
dimmed: true,
|
||||||
|
boxed: true,
|
||||||
|
innerBoxClass: "confirm-message-modal",
|
||||||
|
content: <RoomContext value={roomCtx}>
|
||||||
|
<ShareModal
|
||||||
|
evt={evt}
|
||||||
|
title="Share Message"
|
||||||
|
confirmButton="Copy to clipboard"
|
||||||
|
onConfirm={(useMatrixTo: boolean, includeEvent: boolean) => {
|
||||||
|
navigator.clipboard.writeText(generateLink(useMatrixTo, includeEvent)).catch(
|
||||||
|
err => window.alert(`Failed to copy link: ${err}`),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
generateLink={generateLink}
|
||||||
|
/>
|
||||||
|
</RoomContext>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const [isPending, pendingTitle] = getPending(evt)
|
const [isPending, pendingTitle] = getPending(evt)
|
||||||
useRoomState(roomCtx.store, "m.room.power_levels", "")
|
useRoomState(roomCtx.store, "m.room.power_levels", "")
|
||||||
// We get pins from getPinnedEvents, but use the hook anyway to subscribe to changes
|
// We get pins from getPinnedEvents, but use the hook anyway to subscribe to changes
|
||||||
|
@ -104,6 +149,7 @@ export const useSecondaryItems = (
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<button onClick={onClickViewSource}><ViewSourceIcon/>{names && "View source"}</button>
|
<button onClick={onClickViewSource}><ViewSourceIcon/>{names && "View source"}</button>
|
||||||
|
<button onClick={onClickShareEvent}><ShareIcon/>{names && "Share"}</button>
|
||||||
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
{ownPL >= pinPL && (pins.includes(evt.event_id)
|
||||||
? <button onClick={onClickPin(false)}>
|
? <button onClick={onClickPin(false)}>
|
||||||
<UnpinIcon/>{names && "Unpin message"}
|
<UnpinIcon/>{names && "Unpin message"}
|
||||||
|
|
Loading…
Add table
Reference in a new issue