Skip to content

Commit

Permalink
web: add share event button (#589)
Browse files Browse the repository at this point in the history
Co-authored-by: Tulir Asokan <[email protected]>
  • Loading branch information
nexy7574 and tulir authored Jan 23, 2025
1 parent 865b2e4 commit b7f939f
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 2 deletions.
31 changes: 30 additions & 1 deletion web/src/api/statestore/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { CustomEmojiPack, parseCustomEmojiPack } from "@/util/emoji"
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
import toSearchableString from "@/util/searchablestring.ts"
import Subscribable, { MultiSubscribable, NoDataSubscribable } from "@/util/subscribable.ts"
import { getDisplayname } from "@/util/validation.ts"
import { getDisplayname, getServerName } from "@/util/validation.ts"
import {
ContentURI,
DBReceipt,
Expand Down Expand Up @@ -246,6 +246,35 @@ export class RoomStateStore {
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[] {
const pinnedList = this.getStateEvent("m.room.pinned_events", "")?.content?.pinned
if (Array.isArray(pinnedList)) {
Expand Down
1 change: 1 addition & 0 deletions web/src/icons/share.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions web/src/ui/timeline/menu/ShareModal.tsx
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions web/src/ui/timeline/menu/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ div.confirm-message-modal > form {

> div.timeline-event {
margin: 0;
padding: 0;
}
}

Expand All @@ -118,4 +119,14 @@ div.confirm-message-modal > form {
padding: .5rem 1rem;
}
}

> div.output-preview {
> span.no-select {
user-select: none;
}

> code {
word-break: break-word;
}
}
}
48 changes: 47 additions & 1 deletion web/src/ui/timeline/menu/useSecondaryItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import { ModalCloseContext, ModalContext } from "../../modal"
import { RoomContext, RoomContextData } from "../../roomview/roomcontext.ts"
import JSONView from "../../util/JSONView.tsx"
import ConfirmWithMessageModal from "./ConfirmWithMessageModal.tsx"
import ShareModal from "./ShareModal.tsx"
import { getPending, getPowerLevels } from "./util.ts"
import ViewSourceIcon from "@/icons/code.svg?react"
import DeleteIcon from "@/icons/delete.svg?react"
import PinIcon from "@/icons/pin.svg?react"
import ReportIcon from "@/icons/report.svg?react"
import ShareIcon from "@/icons/share.svg?react"
import UnpinIcon from "@/icons/unpin.svg?react"

export const useSecondaryItems = (
Expand All @@ -40,7 +42,7 @@ export const useSecondaryItems = (
openModal({
dimmed: true,
boxed: true,
content: <JSONView data={evt} />,
content: <JSONView data={evt}/>,
})
}
const onClickReport = () => {
Expand Down Expand Up @@ -89,6 +91,49 @@ export const useSecondaryItems = (
.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)
useRoomState(roomCtx.store, "m.room.power_levels", "")
// We get pins from getPinnedEvents, but use the hook anyway to subscribe to changes
Expand All @@ -104,6 +149,7 @@ export const useSecondaryItems = (

return <>
<button onClick={onClickViewSource}><ViewSourceIcon/>{names && "View source"}</button>
<button onClick={onClickShareEvent}><ShareIcon/>{names && "Share"}</button>
{ownPL >= pinPL && (pins.includes(evt.event_id)
? <button onClick={onClickPin(false)}>
<UnpinIcon/>{names && "Unpin message"}
Expand Down

0 comments on commit b7f939f

Please sign in to comment.