mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
parent
a3873643ec
commit
bf7769ee95
8 changed files with 129 additions and 23 deletions
|
@ -13,6 +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 type { MouseEvent } from "react"
|
||||
import { CachedEventDispatcher, NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
||||
import { RoomStateStore, StateStore } from "./statestore"
|
||||
|
@ -66,8 +67,20 @@ export default class Client {
|
|||
}
|
||||
console.log("Successfully authenticated, connecting to websocket")
|
||||
this.rpc.start()
|
||||
window.Notification?.requestPermission()
|
||||
.then(permission => console.log("Notification permission:", permission))
|
||||
this.requestNotificationPermission()
|
||||
}
|
||||
|
||||
requestNotificationPermission = (evt?: MouseEvent) => {
|
||||
window.Notification?.requestPermission().then(permission => {
|
||||
console.log("Notification permission:", permission)
|
||||
if (evt) {
|
||||
window.alert(`Notification permission: ${permission}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerURIHandler = () => {
|
||||
navigator.registerProtocolHandler("matrix", "#/uri/%s")
|
||||
}
|
||||
|
||||
start(): () => void {
|
||||
|
|
|
@ -37,15 +37,18 @@ export function useRoomState(
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
export function useRoomMembers(room?: RoomStateStore): MemDBEvent[] {
|
||||
return useSyncExternalStore(
|
||||
room ? room.stateSubs.getSubscriber("m.room.member") : noopSubscribe,
|
||||
room ? room.getMembers : () => [],
|
||||
room ? room.getMembers : returnEmptyArray,
|
||||
)
|
||||
}
|
||||
|
||||
const noopSubscribe = () => () => {}
|
||||
const returnNull = () => null
|
||||
const emptyArray: never[] = []
|
||||
const returnEmptyArray = () => emptyArray
|
||||
|
||||
export function useRoomEvent(room: RoomStateStore, eventID: EventID | null): MemDBEvent | null {
|
||||
return useSyncExternalStore(
|
||||
|
|
|
@ -19,6 +19,7 @@ import Client from "@/api/client.ts"
|
|||
import { RoomStateStore } from "@/api/statestore"
|
||||
import type { RoomID } from "@/api/types"
|
||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||
import { parseMatrixURI } from "@/util/validation.ts"
|
||||
import ClientContext from "./ClientContext.ts"
|
||||
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
|
||||
import StylePreferences from "./StylePreferences.tsx"
|
||||
|
@ -61,6 +62,9 @@ class ContextFields implements MainScreenContextFields {
|
|||
}
|
||||
|
||||
setRightPanel = (props: RightPanelProps | null, pushState = true) => {
|
||||
if ((props?.type === "members" || props?.type === "pinned-messages") && !this.client.store.activeRoomID) {
|
||||
props = null
|
||||
}
|
||||
const isEqual = objectIsEqual(this.currentRightPanel, props)
|
||||
if (isEqual && !pushState) {
|
||||
return
|
||||
|
@ -145,6 +149,42 @@ class ContextFields implements MainScreenContextFields {
|
|||
|
||||
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
||||
|
||||
const handleURLHash = (client: Client, context: ContextFields) => {
|
||||
if (!location.hash.startsWith("#/uri/")) {
|
||||
return
|
||||
}
|
||||
const decodedURI = decodeURIComponent(location.hash.slice("#/uri/".length))
|
||||
const uri = parseMatrixURI(decodedURI)
|
||||
if (!uri) {
|
||||
console.error("Invalid matrix URI", decodedURI)
|
||||
return
|
||||
}
|
||||
const newURL = new URL(location.href)
|
||||
newURL.hash = ""
|
||||
if (uri.identifier.startsWith("@")) {
|
||||
const right_panel = {
|
||||
type: "user",
|
||||
userID: uri.identifier,
|
||||
} as RightPanelProps
|
||||
history.replaceState({ right_panel }, "", newURL.toString())
|
||||
context.setRightPanel(right_panel, false)
|
||||
} else if (uri.identifier.startsWith("!")) {
|
||||
history.replaceState({ room_id: uri.identifier }, "", newURL.toString())
|
||||
context.setActiveRoom(uri.identifier, false)
|
||||
} else if (uri.identifier.startsWith("#")) {
|
||||
// TODO loading indicator or something for this?
|
||||
client.rpc.resolveAlias(uri.identifier).then(
|
||||
res => {
|
||||
history.replaceState({ room_id: res.room_id }, "", newURL.toString())
|
||||
context.setActiveRoom(res.room_id, false)
|
||||
},
|
||||
err => window.alert(`Failed to resolve room alias ${uri.identifier}: ${err}`),
|
||||
)
|
||||
} else {
|
||||
console.error("Invalid matrix URI", uri)
|
||||
}
|
||||
}
|
||||
|
||||
const MainScreen = () => {
|
||||
const [activeRoom, directSetActiveRoom] = useState<RoomStateStore | null>(null)
|
||||
const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null)
|
||||
|
@ -166,6 +206,8 @@ const MainScreen = () => {
|
|||
context.setRightPanel(evt.state?.right_panel ?? null, false)
|
||||
}
|
||||
window.addEventListener("popstate", listener)
|
||||
listener({ state: history.state } as PopStateEvent)
|
||||
handleURLHash(client, context)
|
||||
return () => window.removeEventListener("popstate", listener)
|
||||
}, [context, client])
|
||||
useEffect(() => context.keybindings.listen(), [context])
|
||||
|
@ -228,6 +270,7 @@ const MainScreen = () => {
|
|||
rightPanelResizeHandle={resizeHandle2}
|
||||
/>
|
||||
: rightPanel && <>
|
||||
<div className="room-view placeholder"/>
|
||||
{resizeHandle2}
|
||||
{rightPanel && <RightPanel {...rightPanel}/>}
|
||||
</>}
|
||||
|
|
|
@ -59,11 +59,12 @@ function renderRightPanelContent(props: RightPanelProps): JSX.Element | null {
|
|||
}
|
||||
|
||||
const RightPanel = (props: RightPanelProps) => {
|
||||
const mainScreen = use(MainScreenContext)
|
||||
let backButton: JSX.Element | null = null
|
||||
if (props.type === "user") {
|
||||
backButton = <button
|
||||
data-target-panel="members"
|
||||
onClick={use(MainScreenContext).clickRightPanelOpener}
|
||||
onClick={mainScreen.clickRightPanelOpener}
|
||||
><BackIcon/></button>
|
||||
}
|
||||
return <div className="right-panel">
|
||||
|
@ -72,7 +73,7 @@ const RightPanel = (props: RightPanelProps) => {
|
|||
{backButton}
|
||||
<div className="panel-name">{getTitle(props.type)}</div>
|
||||
</div>
|
||||
<button onClick={use(MainScreenContext).closeRightPanel}><CloseIcon/></button>
|
||||
<button onClick={mainScreen.closeRightPanel}><CloseIcon/></button>
|
||||
</div>
|
||||
<div className={`right-panel-content ${props.type}`}>
|
||||
{renderRightPanelContent(props)}
|
||||
|
|
|
@ -88,13 +88,17 @@ div.settings-view {
|
|||
}
|
||||
}
|
||||
|
||||
button.logout {
|
||||
margin-top: 1rem;
|
||||
> div.misc-buttons > button {
|
||||
padding: .5rem 1rem;
|
||||
display: block;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--error-color);
|
||||
color: var(--inverted-text-color);
|
||||
&.logout {
|
||||
margin-top: 2rem;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--error-color);
|
||||
color: var(--inverted-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -357,7 +357,13 @@ const SettingsView = ({ room }: SettingsViewProps) => {
|
|||
</table>
|
||||
<CustomCSSInput setPref={setPref} room={room} />
|
||||
<AppliedSettingsView room={room} />
|
||||
<button className="logout" onClick={onClickLogout}>Logout</button>
|
||||
<div className="misc-buttons">
|
||||
{window.Notification && <button onClick={client.requestNotificationPermission}>
|
||||
Request notification permission
|
||||
</button>}
|
||||
<button onClick={client.registerURIHandler}>Register <code>matrix:</code> URI handler</button>
|
||||
<button className="logout" onClick={onClickLogout}>Logout</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +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 { MessageEventContent } from "@/api/types"
|
||||
import { getDisplayname } from "@/util/validation.ts"
|
||||
import { getDisplayname, parseMatrixURI } from "@/util/validation.ts"
|
||||
import EventContentProps from "./props.ts"
|
||||
|
||||
function isImageElement(elem: EventTarget): elem is HTMLImageElement {
|
||||
|
@ -26,21 +26,19 @@ function isAnchorElement(elem: EventTarget): elem is HTMLAnchorElement {
|
|||
}
|
||||
|
||||
function onClickMatrixURI(href: string) {
|
||||
const url = new URL(href)
|
||||
const pathParts = url.pathname.split("/")
|
||||
const decodedPart = decodeURIComponent(pathParts[1])
|
||||
switch (pathParts[0]) {
|
||||
case "u":
|
||||
const uri = parseMatrixURI(href)
|
||||
switch (uri?.identifier[0]) {
|
||||
case "@":
|
||||
return window.mainScreenContext.setRightPanel({
|
||||
type: "user",
|
||||
userID: `@${decodedPart}`,
|
||||
userID: uri.identifier,
|
||||
})
|
||||
case "roomid":
|
||||
return window.mainScreenContext.setActiveRoom(`!${decodedPart}`)
|
||||
case "r":
|
||||
return window.client.rpc.resolveAlias(`#${decodedPart}`).then(
|
||||
case "!":
|
||||
return window.mainScreenContext.setActiveRoom(uri.identifier)
|
||||
case "#":
|
||||
return window.client.rpc.resolveAlias(uri.identifier).then(
|
||||
res => window.mainScreenContext.setActiveRoom(res.room_id),
|
||||
err => window.alert(`Failed to resolve room alias #${decodedPart}: ${err}`),
|
||||
err => window.alert(`Failed to resolve room alias ${uri.identifier}: ${err}`),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,44 @@ export const isRoomID = (roomID: unknown) => isIdentifier<RoomID>(roomID, "!", t
|
|||
export const isRoomAlias = (roomAlias: unknown) => isIdentifier<RoomAlias>(roomAlias, "#", true)
|
||||
export const isMXC = (mxc: unknown): mxc is ContentURI => typeof mxc === "string" && mediaRegex.test(mxc)
|
||||
|
||||
export interface ParsedMatrixURI {
|
||||
identifier: UserID | RoomID | RoomAlias
|
||||
eventID?: EventID
|
||||
params: URLSearchParams
|
||||
}
|
||||
|
||||
export function parseMatrixURI(uri: unknown): ParsedMatrixURI | undefined {
|
||||
if (typeof uri !== "string") {
|
||||
return
|
||||
}
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(uri)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (parsed.protocol !== "matrix:") {
|
||||
return
|
||||
}
|
||||
const [type, ident1, subtype, ident2] = parsed.pathname.split("/")
|
||||
const output: Partial<ParsedMatrixURI> = {
|
||||
params: parsed.searchParams,
|
||||
}
|
||||
if (type === "u") {
|
||||
output.identifier = `@${ident1}`
|
||||
} else if (type === "r") {
|
||||
output.identifier = `#${ident1}`
|
||||
} else if (type === "roomid") {
|
||||
output.identifier = `!${ident1}`
|
||||
if (subtype === "e") {
|
||||
output.eventID = `$${ident2}`
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
return output as ParsedMatrixURI
|
||||
}
|
||||
|
||||
export function getLocalpart(userID: UserID): string {
|
||||
const idx = userID.indexOf(":")
|
||||
return idx > 0 ? userID.slice(1, idx) : userID.slice(1)
|
||||
|
|
Loading…
Add table
Reference in a new issue