From 4c4744eba87a6c467928d7f59284cf8f911bb5cc Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Fri, 20 Dec 2024 15:44:00 -0700 Subject: [PATCH 01/85] web/timeline: render MSC4095 URL previews This commit implements rending of MSC4095[1] bundled URL previews and includes a preference for disabling rendering of the previews. [1]: https://github.com/matrix-org/matrix-spec-proposals/pull/4095 Signed-off-by: Sumner Evans --- pkg/hicli/sync.go | 16 ++++ web/src/api/types/mxtypes.ts | 15 ++++ web/src/api/types/preferences/preferences.ts | 6 ++ web/src/ui/timeline/TimelineEvent.tsx | 2 + web/src/ui/timeline/URLPreviews.css | 48 +++++++++++ web/src/ui/timeline/URLPreviews.tsx | 87 ++++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 web/src/ui/timeline/URLPreviews.css create mode 100644 web/src/ui/timeline/URLPreviews.tsx diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index fd6aea1..4c8a6b5 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -411,6 +411,22 @@ func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID datab } else if content.GetInfo().ThumbnailURL != "" { h.addMediaCache(ctx, rowID, content.Info.ThumbnailURL, nil, content.Info.ThumbnailInfo, "") } + + for _, image := range content.BeeperGalleryImages { + h.cacheMedia(ctx, &event.Event{ + Type: event.EventMessage, + Content: event.Content{Parsed: image}, + }, rowID) + } + + for _, preview := range content.BeeperLinkPreviews { + info := &event.FileInfo{MimeType: preview.ImageType} + if preview.ImageEncryption != nil { + h.addMediaCache(ctx, rowID, preview.ImageEncryption.URL, preview.ImageEncryption, info, "") + } else if preview.ImageURL != "" { + h.addMediaCache(ctx, rowID, preview.ImageURL, nil, info, "") + } + } case event.StateRoomAvatar: _ = evt.Content.ParseRaw(evt.Type) content, ok := evt.Content.Parsed.(*event.RoomAvatarEventContent) diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 78e6146..45fe52c 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -140,6 +140,19 @@ export interface ContentWarning { description?: string } +export interface URLPreview { + matched_url: string + "beeper:image:encryption"?: EncryptedFile + "matrix:image:size": number + "og:image"?: ContentURI + "og:url": string + "og:image:width"?: number + "og:image:height"?: number + "og:image:type"?: string + "og:title"?: string + "og:description"?: string +} + export interface BaseMessageEventContent { msgtype: string body: string @@ -150,6 +163,8 @@ export interface BaseMessageEventContent { "town.robin.msc3725.content_warning"?: ContentWarning "page.codeberg.everypizza.msc4193.spoiler"?: boolean "page.codeberg.everypizza.msc4193.spoiler.reason"?: string + "m.url_previews"?: URLPreview[] + "com.beeper.linkpreviews"?: URLPreview[] } export interface TextMessageEventContent extends BaseMessageEventContent { diff --git a/web/src/api/types/preferences/preferences.ts b/web/src/api/types/preferences/preferences.ts index 6952424..4d55ac3 100644 --- a/web/src/api/types/preferences/preferences.ts +++ b/web/src/api/types/preferences/preferences.ts @@ -102,6 +102,12 @@ export const preferences = { allowedContexts: anyContext, defaultValue: true, }), + render_url_previews: new Preference({ + displayName: "Render URL previews", + description: "Whether to render MSC4095 URL previews in the room timeline.", + allowedContexts: anyContext, + defaultValue: true, + }), show_date_separators: new Preference({ displayName: "Show date separators", description: "Whether messages in different days should have a date separator between them in the room timeline.", diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index c79b63c..18a63e3 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -25,6 +25,7 @@ import { ModalContext } from "../modal/Modal.tsx" import { useRoomContext } from "../roomview/roomcontext.ts" import ReadReceipts from "./ReadReceipts.tsx" import { ReplyIDBody } from "./ReplyBody.tsx" +import URLPreviews from "./URLPreviews.tsx" import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content" import { EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu" import ErrorIcon from "@/icons/error.svg?react" @@ -196,6 +197,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => { /> : null} + {evt.reactions ? : null} diff --git a/web/src/ui/timeline/URLPreviews.css b/web/src/ui/timeline/URLPreviews.css new file mode 100644 index 0000000..aee9709 --- /dev/null +++ b/web/src/ui/timeline/URLPreviews.css @@ -0,0 +1,48 @@ +div.url-previews { + display: flex; + flex-direction: row; + gap: 1rem; + overflow-x: scroll; + + > div.url-preview { + margin: 0.5rem 0; + border-radius: 0.5rem; + background-color: var(--pill-background-color); + border: 1px solid var(--border-color); + + div.title { + margin: 0.5rem 0.5rem 0 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + } + + div.description { + margin: 0 0.5rem 0.5rem 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + color: var(--semisecondary-text-color); + } + + > div.media-container { + border-radius: 0 0 0.5rem 0.5rem; + } + + &.inline { + display: flex; + flex-direction: row; + min-width: 320px; + max-width: 320px; + + > div.media-container { + border-radius: none; + margin: 0.5rem; + } + } + } +} diff --git a/web/src/ui/timeline/URLPreviews.tsx b/web/src/ui/timeline/URLPreviews.tsx new file mode 100644 index 0000000..32b0cb2 --- /dev/null +++ b/web/src/ui/timeline/URLPreviews.tsx @@ -0,0 +1,87 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Sumner Evans +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { use } from "react" +import { getEncryptedMediaURL, getMediaURL } from "@/api/media" +import { RoomStateStore, usePreference } from "@/api/statestore" +import { MemDBEvent, URLPreview } from "@/api/types" +import { ImageContainerSize, calculateMediaSize } from "@/util/mediasize" +import ClientContext from "../ClientContext" +import "./URLPreviews.css" + +const URLPreviews = ({ event, room }: { + room: RoomStateStore + event: MemDBEvent +}) => { + const client = use(ClientContext)! + const renderPreviews = usePreference(client.store, room, "render_url_previews") + if (event.redacted_by || !renderPreviews) { + return null + } + + const previews = (event.content["com.beeper.linkpreviews"] ?? event.content["m.url_previews"]) as URLPreview[] + if (!previews) { + return null + } + return
+ {previews + .filter(p => p["og:title"] || p["og:image"] || p["beeper:image:encryption"]) + .map(p => { + const mediaURL = p["beeper:image:encryption"] + ? getEncryptedMediaURL(p["beeper:image:encryption"].url) + : getMediaURL(p["og:image"]) + const aspectRatio = (p["og:image:width"] ?? 1) / (p["og:image:height"] ?? 1) + let containerSize: ImageContainerSize | undefined + let inline = false + if (aspectRatio < 1.2) { + containerSize = { width: 70, height: 70 } + inline = true + } + const style = calculateMediaSize(p["og:image:width"], p["og:image:height"], containerSize) + + const title = p["og:title"] ?? p["og:url"] ?? p.matched_url + return
+ {mediaURL && inline &&
+ {p["og:title"]} +
} +
+
+ {title} +
+
{p["og:description"]}
+
+ {mediaURL && !inline &&
+ {p["og:title"]} +
} +
+ })} +
+} + +export default React.memo(URLPreviews) From cf857e459ecaefc2a3d14863fd1961b3bf4ad413 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sat, 21 Dec 2024 13:30:11 -0700 Subject: [PATCH 02/85] hicli/sync: fix caching media edits in encrypted rooms Signed-off-by: Sumner Evans --- pkg/hicli/sync.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index 4c8a6b5..eeaf27e 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -395,12 +395,7 @@ func (h *HiClient) addMediaCache( } func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID database.EventRowID) { - switch evt.Type { - case event.EventMessage, event.EventSticker: - content, ok := evt.Content.Parsed.(*event.MessageEventContent) - if !ok { - return - } + cacheMessageEventContent := func(content *event.MessageEventContent) { if content.File != nil { h.addMediaCache(ctx, rowID, content.File.URL, content.File, content.Info, content.GetFileName()) } else if content.URL != "" { @@ -427,6 +422,19 @@ func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID datab h.addMediaCache(ctx, rowID, preview.ImageURL, nil, info, "") } } + } + + switch evt.Type { + case event.EventMessage, event.EventSticker: + content, ok := evt.Content.Parsed.(*event.MessageEventContent) + if !ok { + return + } + + cacheMessageEventContent(content) + if content.NewContent != nil { + cacheMessageEventContent(content.NewContent) + } case event.StateRoomAvatar: _ = evt.Content.ParseRaw(evt.Type) content, ok := evt.Content.Parsed.(*event.RoomAvatarEventContent) From 74842707b314c77ef007c070dbb82ee591059518 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 00:50:07 +0200 Subject: [PATCH 03/85] web/timeline: use css grid for url previews and improve inline style --- web/src/index.css | 2 ++ web/src/ui/timeline/URLPreviews.css | 54 +++++++++++++++++++---------- web/src/ui/timeline/URLPreviews.tsx | 46 +++++++++++------------- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/web/src/index.css b/web/src/index.css index 9d7f938..2c3a0cc 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -22,6 +22,7 @@ --border-color: #ccc; --pill-background-color: #ccc; + --url-preview-background-color: #ddd; --highlight-pill-background-color: #c00; --highlight-pill-text-color: #fff; --button-hover-color: rgba(0, 0, 0, .2); @@ -109,6 +110,7 @@ --border-color: #222; --pill-background-color: #222; + --url-preview-background-color: #222; --button-hover-color: rgba(255, 255, 255, .2); --light-hover-color: rgba(255, 255, 255, .1); diff --git a/web/src/ui/timeline/URLPreviews.css b/web/src/ui/timeline/URLPreviews.css index aee9709..feba09d 100644 --- a/web/src/ui/timeline/URLPreviews.css +++ b/web/src/ui/timeline/URLPreviews.css @@ -7,41 +7,57 @@ div.url-previews { > div.url-preview { margin: 0.5rem 0; border-radius: 0.5rem; - background-color: var(--pill-background-color); - border: 1px solid var(--border-color); + background-color: var(--url-preview-background-color); + border: 1px solid var(--url-preview-background-color); + display: grid; + + grid-template: + "title" auto + "description" auto + "media" auto + / 1fr; div.title { - margin: 0.5rem 0.5rem 0 0.5rem; - display: -webkit-box; - -webkit-line-clamp: 1; - line-clamp: 1; - -webkit-box-orient: vertical; + grid-area: title; + margin: 0.5rem 0.5rem 0; overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: bold; } div.description { - margin: 0 0.5rem 0.5rem 0.5rem; + grid-area: description; + margin: 0 0.5rem 0.5rem; display: -webkit-box; - -webkit-line-clamp: 3; - line-clamp: 3; - -webkit-box-orient: vertical; overflow: hidden; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; color: var(--semisecondary-text-color); } > div.media-container { - border-radius: 0 0 0.5rem 0.5rem; + grid-area: media; + border-radius: 0 0 .5rem .5rem; + background-color: var(--background-color); } &.inline { - display: flex; - flex-direction: row; - min-width: 320px; - max-width: 320px; + grid-template: + "media title" auto + "media description" auto + / auto auto; + width: 100%; + max-width: 20rem; - > div.media-container { - border-radius: none; - margin: 0.5rem; + > div.inline-media-wrapper { + grid-area: media; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--background-color); + border-radius: .5rem 0 0 .5rem; + padding: .5rem; } } } diff --git a/web/src/ui/timeline/URLPreviews.tsx b/web/src/ui/timeline/URLPreviews.tsx index 32b0cb2..032b18c 100644 --- a/web/src/ui/timeline/URLPreviews.tsx +++ b/web/src/ui/timeline/URLPreviews.tsx @@ -46,39 +46,33 @@ const URLPreviews = ({ event, room }: { let containerSize: ImageContainerSize | undefined let inline = false if (aspectRatio < 1.2) { - containerSize = { width: 70, height: 70 } + containerSize = { width: 80, height: 80 } inline = true } const style = calculateMediaSize(p["og:image:width"], p["og:image:height"], containerSize) - const title = p["og:title"] ?? p["og:url"] ?? p.matched_url + const url = p["og:url"] ?? p.matched_url + const title = p["og:title"] ?? p["og:url"] ?? url + const mediaContainer =
+ +
return
- {mediaURL && inline &&
- {p["og:title"]} -
} -
-
- {title} -
-
{p["og:description"]}
+ style={inline ? {} : { width: style.container.width }} + > + - {mediaURL && !inline &&
- {p["og:title"]} -
} +
{p["og:description"]}
+ {mediaURL && (inline + ?
{mediaContainer}
+ : mediaContainer)}
})}
From bb26bc4e64997c75731ff6df892390723633eaf9 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sat, 21 Dec 2024 15:56:04 -0700 Subject: [PATCH 04/85] web/{composer,timeline}: fix a couple loader colors (#561) Signed-off-by: Sumner Evans --- web/src/ui/composer/MessageComposer.tsx | 2 +- web/src/ui/timeline/TimelineView.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index c7bc8c3..55775d7 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -602,7 +602,7 @@ const MessageComposer = () => { isThread={false} onClose={stopEditing} />} - {loadingMedia &&
} + {loadingMedia &&
} {state.media && } {state.location && { return
{room.hasMoreHistory ? : "No more history available in this room"}
From fa4d4144ba882c4d2a5bba35673fc4609e949d13 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 01:33:40 +0200 Subject: [PATCH 05/85] web/timeline: add option for compact replies Closes #549 --- web/src/api/types/preferences/preferences.ts | 6 +++ web/src/index.css | 1 + web/src/ui/timeline/ReplyBody.css | 53 ++++++++++++++++---- web/src/ui/timeline/ReplyBody.tsx | 12 +++-- web/src/ui/timeline/TimelineEvent.css | 11 +++- web/src/ui/timeline/TimelineEvent.tsx | 46 +++++++++++------ web/src/ui/timeline/TimelineView.tsx | 5 +- 7 files changed, 103 insertions(+), 31 deletions(-) diff --git a/web/src/api/types/preferences/preferences.ts b/web/src/api/types/preferences/preferences.ts index 4d55ac3..a535817 100644 --- a/web/src/api/types/preferences/preferences.ts +++ b/web/src/api/types/preferences/preferences.ts @@ -108,6 +108,12 @@ export const preferences = { allowedContexts: anyContext, defaultValue: true, }), + small_replies: new Preference({ + displayName: "Compact reply style", + description: "Whether to use a Discord-like compact style for replies instead of the traditional style.", + allowedContexts: anyContext, + defaultValue: false, + }), show_date_separators: new Preference({ displayName: "Show date separators", description: "Whether messages in different days should have a date separator between them in the room timeline.", diff --git a/web/src/index.css b/web/src/index.css index 2c3a0cc..a79ca3f 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11,6 +11,7 @@ --semisecondary-text-color: #555; --link-text-color: #0467dd; --visited-link-text-color: var(--link-text-color); + --small-font-size: .875rem; --code-background-color: rgba(0, 0, 0, 0.15); --media-placeholder-default-background: rgba(0, 0, 0, .1); diff --git a/web/src/ui/timeline/ReplyBody.css b/web/src/ui/timeline/ReplyBody.css index 1efc20b..1895960 100644 --- a/web/src/ui/timeline/ReplyBody.css +++ b/web/src/ui/timeline/ReplyBody.css @@ -1,18 +1,43 @@ blockquote.reply-body { margin: 0 0 .25rem; - border-left: 2px solid var(--blockquote-border-color); + border-left: 2px solid var(--reply-border-color); padding: .25rem .5rem; - &.sender-color-0 { border-color: var(--sender-color-0); } - &.sender-color-1 { border-color: var(--sender-color-1); } - &.sender-color-2 { border-color: var(--sender-color-2); } - &.sender-color-3 { border-color: var(--sender-color-3); } - &.sender-color-4 { border-color: var(--sender-color-4); } - &.sender-color-5 { border-color: var(--sender-color-5); } - &.sender-color-6 { border-color: var(--sender-color-6); } - &.sender-color-7 { border-color: var(--sender-color-7); } - &.sender-color-8 { border-color: var(--sender-color-8); } - &.sender-color-9 { border-color: var(--sender-color-9); } + &.sender-color-0 { --reply-border-color: var(--sender-color-0); } + &.sender-color-1 { --reply-border-color: var(--sender-color-1); } + &.sender-color-2 { --reply-border-color: var(--sender-color-2); } + &.sender-color-3 { --reply-border-color: var(--sender-color-3); } + &.sender-color-4 { --reply-border-color: var(--sender-color-4); } + &.sender-color-5 { --reply-border-color: var(--sender-color-5); } + &.sender-color-6 { --reply-border-color: var(--sender-color-6); } + &.sender-color-7 { --reply-border-color: var(--sender-color-7); } + &.sender-color-8 { --reply-border-color: var(--sender-color-8); } + &.sender-color-9 { --reply-border-color: var(--sender-color-9); } + + &.small { + grid-area: reply; + display: flex; + gap: .25rem; + font-size: var(--small-font-size); + height: calc(var(--small-font-size) * 1.5); + border-left: none; + padding-left: 0; + padding-bottom: 0; + + > div.reply-spine { + margin-top: calc(var(--small-font-size) * 0.75 - 1px); + margin-left: calc(var(--timeline-avatar-size) / 2 - 1px); + width: calc(var(--timeline-avatar-size)/2 + var(--timeline-avatar-gap)); + border-left: 2px solid var(--reply-border-color); + border-top: 2px solid var(--reply-border-color); + border-top-left-radius: .5rem; + flex-shrink: 0; + } + + > div.message-text { + -webkit-line-clamp: 1; + } + } pre { display: inline; @@ -38,6 +63,7 @@ blockquote.reply-body { -webkit-box-orient: vertical; overflow: hidden; color: var(--semisecondary-text-color); + user-select: none; } &.thread > div.reply-sender > span.event-sender::after { @@ -60,6 +86,11 @@ blockquote.reply-body { width: 1rem; height: 1rem; margin-right: .25rem; + + > img { + width: 100%; + height: 100%; + } } > div.buttons { diff --git a/web/src/ui/timeline/ReplyBody.tsx b/web/src/ui/timeline/ReplyBody.tsx index ae179b9..bf7dc36 100644 --- a/web/src/ui/timeline/ReplyBody.tsx +++ b/web/src/ui/timeline/ReplyBody.tsx @@ -32,6 +32,7 @@ interface ReplyBodyProps { room: RoomStateStore event: MemDBEvent isThread: boolean + small?: boolean isEditing?: boolean onClose?: (evt: React.MouseEvent) => void isSilent?: boolean @@ -44,9 +45,10 @@ interface ReplyIDBodyProps { room: RoomStateStore eventID: EventID isThread: boolean + small: boolean } -export const ReplyIDBody = ({ room, eventID, isThread }: ReplyIDBodyProps) => { +export const ReplyIDBody = ({ room, eventID, isThread, small }: ReplyIDBodyProps) => { const event = useRoomEvent(room, eventID) if (!event) { // This caches whether the event is requested or not, so it doesn't need to be wrapped in an effect. @@ -55,7 +57,7 @@ export const ReplyIDBody = ({ room, eventID, isThread }: ReplyIDBodyProps) => { Reply to unknown event
{eventID} } - return + return } const onClickReply = (evt: React.MouseEvent) => { @@ -78,7 +80,7 @@ const onClickReply = (evt: React.MouseEvent) => { } export const ReplyBody = ({ - room, event, onClose, isThread, isEditing, isSilent, onSetSilent, isExplicitInThread, onSetExplicitInThread, + room, event, onClose, isThread, isEditing, isSilent, onSetSilent, isExplicitInThread, onSetExplicitInThread, small, }: ReplyBodyProps) => { const client = use(ClientContext) const memberEvt = useRoomMember(client, room, event.sender) @@ -94,9 +96,13 @@ export const ReplyBody = ({ if (isEditing) { classNames.push("editing") } + if (small) { + classNames.push("small") + } const userColorIndex = getUserColorIndex(event.sender) classNames.push(`sender-color-${userColorIndex}`) return
+ {small &&
}
div.timeline-event { diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 18a63e3..e01dbb8 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback, useState } from "react" +import React, { JSX, use, useCallback, useState } from "react" import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" @@ -37,6 +37,7 @@ export interface TimelineEventProps { evt: MemDBEvent prevEvt: MemDBEvent | null disableMenu?: boolean + smallReplies?: boolean } const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "medium" }) @@ -72,7 +73,7 @@ const EventSendStatus = ({ evt }: { evt: MemDBEvent }) => { } } -const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => { +const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEventProps) => { const roomCtx = useRoomContext() const client = use(ClientContext)! const mainScreen = use(MainScreenContext) @@ -130,17 +131,39 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
} + const isSmallBodyType = isSmallEvent(BodyType) + const relatesTo = (evt.orig_content ?? evt.content)?.["m.relates_to"] + const replyTo = relatesTo?.["m.in_reply_to"]?.event_id + let replyAboveMessage: JSX.Element | null = null + let replyInMessage: JSX.Element | null = null + if (isEventID(replyTo) && BodyType !== HiddenEvent && !evt.redacted_by) { + const replyElem = + if (smallReplies && !isSmallBodyType) { + replyAboveMessage = replyElem + wrapperClassNames.push("reply-above") + } else { + replyInMessage = replyElem + } + } let smallAvatar = false let renderAvatar = true let eventTimeOnly = false - if (isSmallEvent(BodyType)) { + if (isSmallBodyType) { wrapperClassNames.push("small-event") smallAvatar = true eventTimeOnly = true - } else if (prevEvt?.sender === evt.sender && - prevEvt.timestamp + 15 * 60 * 1000 > evt.timestamp && - !isSmallEvent(getBodyType(prevEvt)) && - dateSeparator === null) { + } else if ( + prevEvt?.sender === evt.sender + && prevEvt.timestamp + 15 * 60 * 1000 > evt.timestamp + && dateSeparator === null + && !replyAboveMessage + && !isSmallEvent(getBodyType(prevEvt)) + ) { wrapperClassNames.push("same-sender") eventTimeOnly = true renderAvatar = false @@ -148,8 +171,6 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => { const fullTime = fullTimeFormatter.format(eventTS) const shortTime = formatShortTime(eventTS) const editTime = editEventTS ? `Edited at ${fullTimeFormatter.format(editEventTS)}` : null - const relatesTo = (evt.orig_content ?? evt.content)?.["m.relates_to"] - const replyTo = relatesTo?.["m.in_reply_to"]?.event_id const mainEvent =
{ >
} + {replyAboveMessage} {renderAvatar &&
{ {shortTime}
}
- {isEventID(replyTo) && BodyType !== HiddenEvent && !evt.redacted_by ? : null} + {replyInMessage} diff --git a/web/src/ui/timeline/TimelineView.tsx b/web/src/ui/timeline/TimelineView.tsx index 7a8c987..b2c01ec 100644 --- a/web/src/ui/timeline/TimelineView.tsx +++ b/web/src/ui/timeline/TimelineView.tsx @@ -15,7 +15,7 @@ // along with this program. If not, see . import { use, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react" import { ScaleLoader } from "react-spinners" -import { useRoomTimeline } from "@/api/statestore" +import { usePreference, useRoomTimeline } from "@/api/statestore" import { MemDBEvent } from "@/api/types" import useFocus from "@/util/focus.ts" import ClientContext from "../ClientContext.ts" @@ -42,6 +42,7 @@ const TimelineView = () => { const oldestTimelineRow = timeline[0]?.timeline_rowid const oldScrollHeight = useRef(0) const focused = useFocus() + const smallReplies = usePreference(client.store, room, "small_replies") // When the user scrolls the timeline manually, remember if they were at the bottom, // so that we can keep them at the bottom when new events are added. @@ -131,7 +132,7 @@ const TimelineView = () => { return null } const thisEvt = prevEvt = entry return thisEvt From 6830e5d1cc68c6a3a1b07652ad4d8f79902b0ace Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 01:41:06 +0200 Subject: [PATCH 06/85] web/timeline: don't render locations in replies --- web/src/ui/timeline/content/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/ui/timeline/content/index.ts b/web/src/ui/timeline/content/index.ts index 49fb3b2..108d44a 100644 --- a/web/src/ui/timeline/content/index.ts +++ b/web/src/ui/timeline/content/index.ts @@ -55,6 +55,9 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo } return MediaMessageBody case "m.location": + if (forReply) { + return TextMessageBody + } return LocationMessageBody default: return UnknownMessageBody From 588a994b55ba418f86510b8558dc164eee938c8c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 02:03:51 +0200 Subject: [PATCH 07/85] web/timeline: allow opening lightbox from url preview image --- web/src/ui/timeline/URLPreviews.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/ui/timeline/URLPreviews.tsx b/web/src/ui/timeline/URLPreviews.tsx index 032b18c..0103621 100644 --- a/web/src/ui/timeline/URLPreviews.tsx +++ b/web/src/ui/timeline/URLPreviews.tsx @@ -19,6 +19,7 @@ import { RoomStateStore, usePreference } from "@/api/statestore" import { MemDBEvent, URLPreview } from "@/api/types" import { ImageContainerSize, calculateMediaSize } from "@/util/mediasize" import ClientContext from "../ClientContext" +import { LightboxContext } from "../modal/Lightbox.tsx" import "./URLPreviews.css" const URLPreviews = ({ event, room }: { @@ -58,6 +59,7 @@ const URLPreviews = ({ event, room }: { loading="lazy" style={style.media} src={mediaURL} + onClick={use(LightboxContext)!} alt="" />
From cac6db390938bf93492ccc697f3ca6083ba4fb2e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 02:07:41 +0200 Subject: [PATCH 08/85] web/timeline: remove small reply padding entirely --- web/src/ui/timeline/ReplyBody.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/ui/timeline/ReplyBody.css b/web/src/ui/timeline/ReplyBody.css index 1895960..f173866 100644 --- a/web/src/ui/timeline/ReplyBody.css +++ b/web/src/ui/timeline/ReplyBody.css @@ -21,8 +21,7 @@ blockquote.reply-body { font-size: var(--small-font-size); height: calc(var(--small-font-size) * 1.5); border-left: none; - padding-left: 0; - padding-bottom: 0; + padding: 0; > div.reply-spine { margin-top: calc(var(--small-font-size) * 0.75 - 1px); From e4182fc2d55c3bf6cca50e335013623b940fcfa7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 12:57:02 +0200 Subject: [PATCH 09/85] web/timeline: fix some things that make replies too big --- web/src/ui/timeline/ReplyBody.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/ui/timeline/ReplyBody.css b/web/src/ui/timeline/ReplyBody.css index f173866..d6f96c4 100644 --- a/web/src/ui/timeline/ReplyBody.css +++ b/web/src/ui/timeline/ReplyBody.css @@ -35,6 +35,7 @@ blockquote.reply-body { > div.message-text { -webkit-line-clamp: 1; + font-size: var(--small-font-size); } } @@ -63,6 +64,15 @@ blockquote.reply-body { overflow: hidden; color: var(--semisecondary-text-color); user-select: none; + + h1, h2, h3, h4, h5, h6 { + font-size: 1em; + } + + img { + vertical-align: baseline; + height: 1em; + } } &.thread > div.reply-sender > span.event-sender::after { From 388be09795067f335eab817f83562bae67c457ce Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 14:05:24 +0200 Subject: [PATCH 10/85] web/timeline: don't shrink url previews --- web/src/ui/timeline/URLPreviews.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/timeline/URLPreviews.css b/web/src/ui/timeline/URLPreviews.css index feba09d..bd5b921 100644 --- a/web/src/ui/timeline/URLPreviews.css +++ b/web/src/ui/timeline/URLPreviews.css @@ -10,6 +10,7 @@ div.url-previews { background-color: var(--url-preview-background-color); border: 1px solid var(--url-preview-background-color); display: grid; + flex-shrink: 0; grid-template: "title" auto From 4160a33edbc688fe299aec685c8afb7fb9bba990 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 22 Dec 2024 14:54:57 +0200 Subject: [PATCH 11/85] web/all: remove unnecessary uses of useCallback --- web/src/ui/composer/Autocompleter.tsx | 4 +- web/src/ui/composer/ComposerMedia.tsx | 63 ++++++ web/src/ui/composer/MessageComposer.tsx | 190 +++++++----------- web/src/ui/emojipicker/EmojiGroup.tsx | 21 +- web/src/ui/emojipicker/EmojiPicker.tsx | 14 +- web/src/ui/emojipicker/GIFPicker.tsx | 12 +- web/src/ui/emojipicker/StickerPicker.tsx | 10 +- web/src/ui/login/BeeperLogin.tsx | 19 +- web/src/ui/login/LoginScreen.tsx | 23 +-- web/src/ui/login/VerificationScreen.tsx | 6 +- web/src/ui/modal/Modal.tsx | 4 +- web/src/ui/rightpanel/MemberList.tsx | 13 +- web/src/ui/rightpanel/UserInfoDeviceList.tsx | 14 +- web/src/ui/roomlist/RoomList.tsx | 14 +- web/src/ui/roomview/RoomPreview.tsx | 10 +- web/src/ui/roomview/RoomViewHeader.tsx | 6 +- web/src/ui/settings/SettingsView.tsx | 61 +++--- web/src/ui/timeline/TimelineEvent.tsx | 6 +- web/src/ui/timeline/TimelineView.tsx | 4 +- .../timeline/menu/ConfirmWithMessageModal.tsx | 19 +- web/src/ui/timeline/menu/usePrimaryItems.tsx | 22 +- .../ui/timeline/menu/useSecondaryItems.tsx | 35 ++-- web/src/ui/util/ResizeHandle.tsx | 5 +- 23 files changed, 285 insertions(+), 290 deletions(-) create mode 100644 web/src/ui/composer/ComposerMedia.tsx diff --git a/web/src/ui/composer/Autocompleter.tsx b/web/src/ui/composer/Autocompleter.tsx index 78e1a0d..432d327 100644 --- a/web/src/ui/composer/Autocompleter.tsx +++ b/web/src/ui/composer/Autocompleter.tsx @@ -80,13 +80,13 @@ function useAutocompleter({ }) document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" }) }) - const onClick = useEvent((evt: React.MouseEvent) => { + const onClick = (evt: React.MouseEvent) => { const idx = evt.currentTarget.getAttribute("data-index") if (idx) { onSelect(+idx) setAutocomplete(null) } - }) + } useEffect(() => { if (params.selected !== undefined) { onSelect(params.selected) diff --git a/web/src/ui/composer/ComposerMedia.tsx b/web/src/ui/composer/ComposerMedia.tsx new file mode 100644 index 0000000..44fe952 --- /dev/null +++ b/web/src/ui/composer/ComposerMedia.tsx @@ -0,0 +1,63 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import Client from "@/api/client.ts" +import { RoomStateStore, usePreference } from "@/api/statestore" +import type { MediaMessageEventContent } from "@/api/types" +import { LeafletPicker } from "../maps/async.tsx" +import { useMediaContent } from "../timeline/content/useMediaContent.tsx" +import CloseIcon from "@/icons/close.svg?react" +import "./MessageComposer.css" + +export interface ComposerMediaProps { + content: MediaMessageEventContent + clearMedia: false | (() => void) +} + +export const ComposerMedia = ({ content, clearMedia }: ComposerMediaProps) => { + const [mediaContent, containerClass, containerStyle] = useMediaContent( + content, "m.room.message", { height: 120, width: 360 }, + ) + return
+
+ {mediaContent} +
+ {clearMedia && } +
+} + +export interface ComposerLocationValue { + lat: number + long: number + prec?: number +} + +export interface ComposerLocationProps { + room: RoomStateStore + client: Client + location: ComposerLocationValue + onChange: (location: ComposerLocationValue) => void + clearLocation: () => void +} + +export const ComposerLocation = ({ client, room, location, onChange, clearLocation }: ComposerLocationProps) => { + const tileTemplate = usePreference(client.store, room, "leaflet_tile_template") + return
+
+ +
+ +
+} diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index 55775d7..701b07d 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -15,8 +15,7 @@ // along with this program. If not, see . import React, { CSSProperties, use, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react" import { ScaleLoader } from "react-spinners" -import Client from "@/api/client.ts" -import { RoomStateStore, usePreference, useRoomEvent } from "@/api/statestore" +import { useRoomEvent } from "@/api/statestore" import type { EventID, MediaMessageEventContent, @@ -29,21 +28,18 @@ import type { import { PartialEmoji, emojiToMarkdown } from "@/util/emoji" import { isMobileDevice } from "@/util/ismobile.ts" import { escapeMarkdown } from "@/util/markdown.ts" -import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import EmojiPicker from "../emojipicker/EmojiPicker.tsx" import GIFPicker from "../emojipicker/GIFPicker.tsx" import StickerPicker from "../emojipicker/StickerPicker.tsx" import { keyToString } from "../keybindings.ts" -import { LeafletPicker } from "../maps/async.tsx" import { ModalContext } from "../modal/Modal.tsx" import { useRoomContext } from "../roomview/roomcontext.ts" import { ReplyBody } from "../timeline/ReplyBody.tsx" -import { useMediaContent } from "../timeline/content/useMediaContent.tsx" import type { AutocompleteQuery } from "./Autocompleter.tsx" +import { ComposerLocation, ComposerLocationValue, ComposerMedia } from "./ComposerMedia.tsx" import { charToAutocompleteType, emojiQueryRegex, getAutocompleter } from "./getAutocompleter.ts" import AttachIcon from "@/icons/attach.svg?react" -import CloseIcon from "@/icons/close.svg?react" import EmojiIcon from "@/icons/emoji-categories/smileys-emotion.svg?react" import GIFIcon from "@/icons/gif.svg?react" import LocationIcon from "@/icons/location.svg?react" @@ -52,12 +48,6 @@ import SendIcon from "@/icons/send.svg?react" import StickerIcon from "@/icons/sticker.svg?react" import "./MessageComposer.css" -export interface ComposerLocationValue { - lat: number - long: number - prec?: number -} - export interface ComposerState { text: string media: MediaMessageEventContent | null @@ -174,13 +164,13 @@ const MessageComposer = () => { textInput.current?.focus() }, [room.roomID]) const canSend = Boolean(state.text || state.media || state.location) - const sendMessage = useEvent((evt: React.FormEvent) => { + const onClickSend = (evt: React.FormEvent) => { evt.preventDefault() if (!canSend) { return } doSendMessage(state) - }) + } const doSendMessage = (state: ComposerState) => { if (editing) { setState(draftStore.get(room.roomID) ?? emptyComposer) @@ -245,7 +235,7 @@ const MessageComposer = () => { mentions, }).catch(err => window.alert("Failed to send message: " + err)) } - const onComposerCaretChange = useEvent((evt: CaretEvent, newText?: string) => { + const onComposerCaretChange = (evt: CaretEvent, newText?: string) => { const area = evt.currentTarget if (area.selectionStart <= (autocomplete?.startPos ?? 0)) { if (autocomplete) { @@ -281,8 +271,8 @@ const MessageComposer = () => { }) } } - }) - const onComposerKeyDown = useEvent((evt: React.KeyboardEvent) => { + } + const onComposerKeyDown = (evt: React.KeyboardEvent) => { const inp = evt.currentTarget const fullKey = keyToString(evt) const sendKey = fullKey === "Enter" || fullKey === "Ctrl+Enter" @@ -295,7 +285,7 @@ const MessageComposer = () => { || autocomplete.selected !== undefined || !document.getElementById("composer-autocompletions")?.classList.contains("has-items") )) { - sendMessage(evt) + onClickSend(evt) } else if (autocomplete) { let autocompleteUpdate: Partial | null | undefined if (fullKey === "Tab" || fullKey === "ArrowDown") { @@ -340,8 +330,8 @@ const MessageComposer = () => { evt.stopPropagation() roomCtx.setEditing(null) } - }) - const onChange = useEvent((evt: React.ChangeEvent) => { + } + const onChange = (evt: React.ChangeEvent) => { setState({ text: evt.target.value }) const now = Date.now() if (evt.target.value !== "" && typingSentAt.current + 5_000 < now) { @@ -358,7 +348,7 @@ const MessageComposer = () => { } } onComposerCaretChange(evt, evt.target.value) - }) + } const doUploadFile = useCallback((file: File | null | undefined) => { if (!file) { return @@ -380,10 +370,7 @@ const MessageComposer = () => { .catch(err => window.alert("Failed to upload file: " + err)) .finally(() => setLoadingMedia(false)) }, [room]) - const onAttachFile = useEvent( - (evt: React.ChangeEvent) => doUploadFile(evt.target.files?.[0]), - ) - const onPaste = useEvent((evt: React.ClipboardEvent) => { + const onPaste = (evt: React.ClipboardEvent) => { const file = evt.clipboardData?.files?.[0] const text = evt.clipboardData.getData("text/plain") const input = evt.currentTarget @@ -400,7 +387,7 @@ const MessageComposer = () => { return } evt.preventDefault() - }) + } // To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState // To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect useLayoutEffect(() => { @@ -450,7 +437,6 @@ const MessageComposer = () => { draftStore.set(room.roomID, state) } }, [roomCtx, room, state, editing]) - const openFilePicker = useCallback(() => fileInput.current!.click(), []) const clearMedia = useCallback(() => setState({ media: null, location: null }), []) const onChangeLocation = useCallback((location: ComposerLocationValue) => setState({ location }), []) const closeReply = useCallback((evt: React.MouseEvent) => { @@ -461,56 +447,6 @@ const MessageComposer = () => { evt.stopPropagation() roomCtx.setEditing(null) }, [roomCtx]) - const getEmojiPickerStyle = () => ({ - bottom: (composerRef.current?.clientHeight ?? 32) + 4 + 24, - right: "var(--timeline-horizontal-padding)", - }) - const openEmojiPicker = useEvent(() => { - openModal({ - content: { - const mdEmoji = emojiToMarkdown(emoji) - setState({ - text: state.text.slice(0, textInput.current?.selectionStart ?? 0) - + mdEmoji - + state.text.slice(textInput.current?.selectionEnd ?? 0), - }) - if (textInput.current) { - textInput.current.setSelectionRange(textInput.current.selectionStart + mdEmoji.length, 0) - } - }} - // TODO allow keeping open on select on non-mobile devices - // (requires onSelect to be able to keep track of the state after updating it) - closeOnSelect={true} - />, - onClose: () => !isMobileDevice && textInput.current?.focus(), - }) - }) - const openGIFPicker = useEvent(() => { - openModal({ - content: setState({ media })} - />, - onClose: () => !isMobileDevice && textInput.current?.focus(), - }) - }) - const openStickerPicker = useEvent(() => { - openModal({ - content: doSendMessage({ ...state, media, text: "" })} - />, - onClose: () => !isMobileDevice && textInput.current?.focus(), - }) - }) - const openLocationPicker = useEvent(() => { - setState({ location: { lat: 0, long: 0, prec: 1 }, media: null }) - }) const Autocompleter = getAutocompleter(autocomplete, client, room) let mediaDisabledTitle: string | undefined let stickerDisabledTitle: string | undefined @@ -533,7 +469,57 @@ const MessageComposer = () => { } else if (state.text && !editing) { stickerDisabledTitle = "You can't attach a sticker to a message with text" } + const getEmojiPickerStyle = () => ({ + bottom: (composerRef.current?.clientHeight ?? 32) + 4 + 24, + right: "var(--timeline-horizontal-padding)", + }) const makeAttachmentButtons = (includeText = false) => { + const openEmojiPicker = () => { + openModal({ + content: { + const mdEmoji = emojiToMarkdown(emoji) + setState({ + text: state.text.slice(0, textInput.current?.selectionStart ?? 0) + + mdEmoji + + state.text.slice(textInput.current?.selectionEnd ?? 0), + }) + if (textInput.current) { + textInput.current.setSelectionRange(textInput.current.selectionStart + mdEmoji.length, 0) + } + }} + // TODO allow keeping open on select on non-mobile devices + // (requires onSelect to be able to keep track of the state after updating it) + closeOnSelect={true} + />, + onClose: () => !isMobileDevice && textInput.current?.focus(), + }) + } + const openGIFPicker = () => { + openModal({ + content: setState({ media })} + />, + onClose: () => !isMobileDevice && textInput.current?.focus(), + }) + } + const openStickerPicker = () => { + openModal({ + content: doSendMessage({ ...state, media, text: "" })} + />, + onClose: () => !isMobileDevice && textInput.current?.focus(), + }) + } + const openLocationPicker = () => { + setState({ location: { lat: 0, long: 0, prec: 1 }, media: null }) + } return <> } - const openButtonsModal = useEvent(() => { + const openButtonsModal = () => { const style: CSSProperties = getEmojiPickerStyle() style.left = style.right delete style.right @@ -571,7 +557,7 @@ const MessageComposer = () => { {makeAttachmentButtons(true)}
, }) - }) + } const inlineButtons = state.text === "" || window.innerWidth > 720 const showSendButton = canSend || window.innerWidth > 720 const disableClearMedia = editing && state.media?.msgtype === "m.sticker" @@ -625,49 +611,19 @@ const MessageComposer = () => { /> {inlineButtons && makeAttachmentButtons()} {showSendButton && } - + doUploadFile(evt.target.files?.[0])} + type="file" + value="" + />
} -interface ComposerMediaProps { - content: MediaMessageEventContent - clearMedia: false | (() => void) -} - -const ComposerMedia = ({ content, clearMedia }: ComposerMediaProps) => { - const [mediaContent, containerClass, containerStyle] = useMediaContent( - content, "m.room.message", { height: 120, width: 360 }, - ) - return
-
- {mediaContent} -
- {clearMedia && } -
-} - -interface ComposerLocationProps { - room: RoomStateStore - client: Client - location: ComposerLocationValue - onChange: (location: ComposerLocationValue) => void - clearLocation: () => void -} - -const ComposerLocation = ({ client, room, location, onChange, clearLocation }: ComposerLocationProps) => { - const tileTemplate = usePreference(client.store, room, "leaflet_tile_template") - return
-
- -
- -
-} - export default MessageComposer diff --git a/web/src/ui/emojipicker/EmojiGroup.tsx b/web/src/ui/emojipicker/EmojiGroup.tsx index da3ed3a..dfb993a 100644 --- a/web/src/ui/emojipicker/EmojiGroup.tsx +++ b/web/src/ui/emojipicker/EmojiGroup.tsx @@ -13,11 +13,10 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback } from "react" +import React, { use } from "react" import { stringToRoomStateGUID } from "@/api/types" import useContentVisibility from "@/util/contentvisibility.ts" import { CATEGORY_FREQUENTLY_USED, CustomEmojiPack, Emoji, categories } from "@/util/emoji" -import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import renderEmoji from "./renderEmoji.tsx" @@ -56,27 +55,25 @@ export const EmojiGroup = ({ } return emoji } - const onClickEmoji = useEvent((evt: React.MouseEvent) => - onSelect(getEmojiFromAttrs(evt.currentTarget))) - const onMouseOverEmoji = useEvent((evt: React.MouseEvent) => - setPreviewEmoji?.(getEmojiFromAttrs(evt.currentTarget))) - const onMouseOutEmoji = useCallback(() => setPreviewEmoji?.(undefined), [setPreviewEmoji]) - const onClickSubscribePack = useEvent((evt: React.MouseEvent) => { + const onMouseOverEmoji = setPreviewEmoji && ((evt: React.MouseEvent) => + setPreviewEmoji(getEmojiFromAttrs(evt.currentTarget))) + const onMouseOutEmoji = setPreviewEmoji && (() => setPreviewEmoji(undefined)) + const onClickSubscribePack = (evt: React.MouseEvent) => { const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id")) if (!guid) { return } client.subscribeToEmojiPack(guid, true) .catch(err => window.alert(`Failed to subscribe to emoji pack: ${err}`)) - }) - const onClickUnsubscribePack = useEvent((evt: React.MouseEvent) => { + } + const onClickUnsubscribePack = (evt: React.MouseEvent) => { const guid = stringToRoomStateGUID(evt.currentTarget.getAttribute("data-pack-id")) if (!guid) { return } client.subscribeToEmojiPack(guid, false) .catch(err => window.alert(`Failed to unsubscribe from emoji pack: ${err}`)) - }) + } let categoryName: string if (typeof categoryID === "number") { @@ -112,7 +109,7 @@ export const EmojiGroup = ({ data-emoji-index={idx} onMouseOver={onMouseOverEmoji} onMouseOut={onMouseOutEmoji} - onClick={onClickEmoji} + onClick={evt => onSelect(getEmojiFromAttrs(evt.currentTarget))} title={emoji.t} >{renderEmoji(emoji)}) : null}
diff --git a/web/src/ui/emojipicker/EmojiPicker.tsx b/web/src/ui/emojipicker/EmojiPicker.tsx index 0a96251..940d371 100644 --- a/web/src/ui/emojipicker/EmojiPicker.tsx +++ b/web/src/ui/emojipicker/EmojiPicker.tsx @@ -19,7 +19,6 @@ import { RoomStateStore, useCustomEmojis } from "@/api/statestore" import { roomStateGUIDToString } from "@/api/types" import { CATEGORY_FREQUENTLY_USED, Emoji, PartialEmoji, categories, useFilteredEmojis } from "@/util/emoji" import { isMobileDevice } from "@/util/ismobile.ts" -import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import { ModalCloseContext } from "../modal/Modal.tsx" import { EmojiGroup } from "./EmojiGroup.tsx" @@ -72,7 +71,6 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe frequentlyUsed: client.store.frequentlyUsedEmoji, customEmojiPacks, }) - const clearQuery = useCallback(() => setQuery(""), []) const close = closeOnSelect ? use(ModalCloseContext) : null const onSelectWrapped = useCallback((emoji?: PartialEmoji) => { if (!emoji) { @@ -85,12 +83,10 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe } close?.() }, [onSelect, selected, client, close]) - const onClickFreeformReact = useEvent(() => onSelectWrapped({ u: query })) - const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) - const onClickCategoryButton = useCallback((evt: React.MouseEvent) => { + const onClickCategoryButton = (evt: React.MouseEvent) => { const categoryID = evt.currentTarget.getAttribute("data-category-id")! document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView() - }, []) + } return
@@ -155,7 +151,7 @@ const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSe })} {allowFreeform && query && }
diff --git a/web/src/ui/emojipicker/GIFPicker.tsx b/web/src/ui/emojipicker/GIFPicker.tsx index 87181d2..f7ef45e 100644 --- a/web/src/ui/emojipicker/GIFPicker.tsx +++ b/web/src/ui/emojipicker/GIFPicker.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { CSSProperties, use, useCallback, useEffect, useState } from "react" +import React, { CSSProperties, use, useEffect, useState } from "react" import { RoomStateStore, usePreference } from "@/api/statestore" import { MediaMessageEventContent } from "@/api/types" import { isMobileDevice } from "@/util/ismobile.ts" @@ -36,13 +36,11 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => { const [results, setResults] = useState([]) const [error, setError] = useState() const close = use(ModalCloseContext) - const clearQuery = useCallback(() => setQuery(""), []) - const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) const client = use(ClientContext)! const provider = usePreference(client.store, room, "gif_provider") const providerName = provider.slice(0, 1).toUpperCase() + provider.slice(1) // const reuploadGIFs = room.preferences.reupload_gifs - const onSelectGIF = useCallback((evt: React.MouseEvent) => { + const onSelectGIF = (evt: React.MouseEvent) => { const idx = evt.currentTarget.getAttribute("data-gif-index") if (!idx) { return @@ -64,7 +62,7 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => { url: gif.proxied_mxc, }) close() - }, [onSelect, close, results]) + } useEffect(() => { if (!query) { if (trendingCache.has(provider)) { @@ -106,12 +104,12 @@ const GIFPicker = ({ style, onSelect, room }: MediaPickerProps) => {
setQuery(evt.target.value)} value={query} type="search" placeholder={`Search ${providerName}`} /> -
diff --git a/web/src/ui/emojipicker/StickerPicker.tsx b/web/src/ui/emojipicker/StickerPicker.tsx index dbb6488..2a3922c 100644 --- a/web/src/ui/emojipicker/StickerPicker.tsx +++ b/web/src/ui/emojipicker/StickerPicker.tsx @@ -39,7 +39,6 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => { customEmojiPacks, stickers: true, }) - const clearQuery = useCallback(() => setQuery(""), []) const close = use(ModalCloseContext) const onSelectWrapped = useCallback((emoji?: Emoji) => { if (!emoji) { @@ -53,11 +52,10 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => { }) close() }, [onSelect, close]) - const onChangeQuery = useCallback((evt: React.ChangeEvent) => setQuery(evt.target.value), []) - const onClickCategoryButton = useCallback((evt: React.MouseEvent) => { + const onClickCategoryButton = (evt: React.MouseEvent) => { const categoryID = evt.currentTarget.getAttribute("data-category-id")! document.getElementById(`emoji-category-${categoryID}`)?.scrollIntoView() - }, []) + } return
@@ -76,12 +74,12 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => {
setQuery(evt.target.value)} value={query} type="search" placeholder="Search stickers" /> -
diff --git a/web/src/ui/login/BeeperLogin.tsx b/web/src/ui/login/BeeperLogin.tsx index ac0bc90..95b2d4d 100644 --- a/web/src/ui/login/BeeperLogin.tsx +++ b/web/src/ui/login/BeeperLogin.tsx @@ -13,10 +13,9 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { useCallback, useState } from "react" +import React, { useState } from "react" import * as beeper from "@/api/beeper.ts" import type Client from "@/api/client.ts" -import useEvent from "@/util/useEvent.ts" interface BeeperLoginProps { domain: string @@ -29,18 +28,18 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => { const [code, setCode] = useState("") const [error, setError] = useState("") - const onChangeEmail = useCallback((evt: React.ChangeEvent) => { + const onChangeEmail = (evt: React.ChangeEvent) => { setEmail(evt.target.value) - }, []) - const onChangeCode = useCallback((evt: React.ChangeEvent) => { + } + const onChangeCode = (evt: React.ChangeEvent) => { let codeDigits = evt.target.value.replace(/\D/g, "").slice(0, 6) if (codeDigits.length > 3) { codeDigits = codeDigits.slice(0, 3) + " " + codeDigits.slice(3) } setCode(codeDigits) - }, []) + } - const requestCode = useEvent((evt: React.FormEvent) => { + const requestCode = (evt: React.FormEvent) => { evt.preventDefault() beeper.doStartLogin(domain).then( request => beeper.doRequestCode(domain, request, email).then( @@ -49,8 +48,8 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => { ), err => setError(`Failed to start login: ${err}`), ) - }) - const submitCode = useEvent((evt: React.FormEvent) => { + } + const submitCode = (evt: React.FormEvent) => { evt.preventDefault() beeper.doSubmitCode(domain, requestID, code).then( token => { @@ -61,7 +60,7 @@ const BeeperLogin = ({ domain, client }: BeeperLoginProps) => { }, err => setError(`Failed to submit code: ${err}`), ) - }) + } return

Beeper email login

diff --git a/web/src/ui/login/LoginScreen.tsx b/web/src/ui/login/LoginScreen.tsx index 58a90f5..3de5a55 100644 --- a/web/src/ui/login/LoginScreen.tsx +++ b/web/src/ui/login/LoginScreen.tsx @@ -16,7 +16,6 @@ import React, { useCallback, useEffect, useState } from "react" import type Client from "@/api/client.ts" import type { ClientState } from "@/api/types" -import useEvent from "@/util/useEvent.ts" import BeeperLogin from "./BeeperLogin.tsx" import "./LoginScreen.css" @@ -34,7 +33,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { const [loginFlows, setLoginFlows] = useState(null) const [error, setError] = useState("") - const loginSSO = useEvent(() => { + const loginSSO = () => { fetch("_gomuks/sso", { method: "POST", body: JSON.stringify({ homeserver_url: homeserverURL }), @@ -53,9 +52,9 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { }, err => setError(`Failed to start SSO login: ${err}`), ) - }) + } - const login = useEvent((evt: React.FormEvent) => { + const login = (evt: React.FormEvent) => { evt.preventDefault() if (!loginFlows) { // do nothing @@ -67,7 +66,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { err => setError(err.toString()), ) } - }) + } const resolveLoginFlows = useCallback((serverURL: string) => { client.rpc.getLoginFlows(serverURL).then( @@ -108,16 +107,10 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { clearTimeout(timeout) } }, [homeserverURL, loginFlows, resolveLoginFlows]) - const onChangeUsername = useCallback((evt: React.ChangeEvent) => { - setUsername(evt.target.value) - }, []) - const onChangePassword = useCallback((evt: React.ChangeEvent) => { - setPassword(evt.target.value) - }, []) - const onChangeHomeserverURL = useCallback((evt: React.ChangeEvent) => { + const onChangeHomeserverURL = (evt: React.ChangeEvent) => { setLoginFlows(null) setHomeserverURL(evt.target.value) - }, []) + } const supportsSSO = loginFlows?.includes("m.login.sso") ?? false const supportsPassword = loginFlows?.includes("m.login.password") @@ -130,7 +123,7 @@ export const LoginScreen = ({ client }: LoginScreenProps) => { id="mxlogin-username" placeholder="User ID" value={username} - onChange={onChangeUsername} + onChange={evt => setUsername(evt.target.value)} /> { id="mxlogin-password" placeholder="Password" value={password} - onChange={onChangePassword} + onChange={evt => setPassword(evt.target.value)} />}
{supportsSSO && : null}
diff --git a/web/src/ui/rightpanel/UserInfoDeviceList.tsx b/web/src/ui/rightpanel/UserInfoDeviceList.tsx index 0c668fc..6b60456 100644 --- a/web/src/ui/rightpanel/UserInfoDeviceList.tsx +++ b/web/src/ui/rightpanel/UserInfoDeviceList.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { useCallback, useEffect, useState, useTransition } from "react" +import { useEffect, useState, useTransition } from "react" import { ScaleLoader } from "react-spinners" import Client from "@/api/client.ts" import { RoomStateStore } from "@/api/statestore" @@ -34,17 +34,19 @@ const DeviceList = ({ client, room, userID }: DeviceListProps) => { const [view, setEncryptionInfo] = useState(null) const [errors, setErrors] = useState(null) const [trackChangePending, startTransition] = useTransition() - const doTrackDeviceList = useCallback(() => { + const doTrackDeviceList = () => { startTransition(async () => { try { const resp = await client.rpc.trackUserDevices(userID) - setEncryptionInfo(resp) - setErrors(resp.errors) + startTransition(() => { + setEncryptionInfo(resp) + setErrors(resp.errors) + }) } catch (err) { - setErrors([`${err}`]) + startTransition(() => setErrors([`${err}`])) } }) - }, [client, userID]) + } useEffect(() => { setEncryptionInfo(null) setErrors(null) diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 075a1d9..93eb074 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback, useRef, useState } from "react" +import React, { use, useRef, useState } from "react" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -38,18 +38,18 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { const [roomFilter, setRoomFilter] = useState("") const [realRoomFilter, setRealRoomFilter] = useState("") - const updateRoomFilter = useCallback((evt: React.ChangeEvent) => { + const updateRoomFilter = (evt: React.ChangeEvent) => { setRoomFilter(evt.target.value) client.store.currentRoomListFilter = toSearchableString(evt.target.value) setRealRoomFilter(client.store.currentRoomListFilter) - }, [client]) - const clearQuery = useCallback(() => { + } + const clearQuery = () => { setRoomFilter("") client.store.currentRoomListFilter = "" setRealRoomFilter("") roomFilterRef.current?.focus() - }, [client]) - const onKeyDown = useCallback((evt: React.KeyboardEvent) => { + } + const onKeyDown = (evt: React.KeyboardEvent) => { const key = keyToString(evt) if (key === "Escape") { clearQuery() @@ -62,7 +62,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { evt.stopPropagation() evt.preventDefault() } - }, [mainScreen, client.store, clearQuery]) + } return
diff --git a/web/src/ui/roomview/RoomPreview.tsx b/web/src/ui/roomview/RoomPreview.tsx index 8718682..d72bd6f 100644 --- a/web/src/ui/roomview/RoomPreview.tsx +++ b/web/src/ui/roomview/RoomPreview.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback, useEffect, useState } from "react" +import { use, useEffect, useState } from "react" import { ScaleLoader } from "react-spinners" import { getAvatarURL, getRoomAvatarURL } from "@/api/media.ts" import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts" @@ -41,7 +41,7 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => { const [loading, setLoading] = useState(false) const [buttonClicked, setButtonClicked] = useState(false) const [error, setError] = useState(null) - const doJoinRoom = useCallback(() => { + const doJoinRoom = () => { let realVia = via if (!via?.length && invite?.invited_by) { realVia = [getServerName(invite.invited_by)] @@ -54,8 +54,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => { setButtonClicked(false) }, ) - }, [client, roomID, via, alias, invite]) - const doRejectInvite = useCallback(() => { + } + const doRejectInvite = () => { setButtonClicked(true) client.rpc.leaveRoom(roomID).then( () => { @@ -67,7 +67,7 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => { setButtonClicked(false) }, ) - }, [client, mainScreen, roomID]) + } useEffect(() => { setSummary(null) setError(null) diff --git a/web/src/ui/roomview/RoomViewHeader.tsx b/web/src/ui/roomview/RoomViewHeader.tsx index 8dd9ea2..4aa0f1d 100644 --- a/web/src/ui/roomview/RoomViewHeader.tsx +++ b/web/src/ui/roomview/RoomViewHeader.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback } from "react" +import { use } from "react" import { getRoomAvatarURL } from "@/api/media.ts" import { RoomStateStore } from "@/api/statestore" import { useEventAsState } from "@/util/eventdispatcher.ts" @@ -35,14 +35,14 @@ const RoomViewHeader = ({ room }: RoomViewHeaderProps) => { const roomMeta = useEventAsState(room.meta) const mainScreen = use(MainScreenContext) const openModal = use(ModalContext) - const openSettings = useCallback(() => { + const openSettings = () => { openModal({ dimmed: true, boxed: true, innerBoxClass: "settings-view", content: , }) - }, [room, openModal]) + } return
{ inheritedValue: T } -const useRemover = ( +const makeRemover = ( context: PreferenceContext, setPref: SetPrefFunc, name: keyof Preferences, value: PreferenceValueType | undefined, ) => { - const onClear = useCallback(() => { - setPref(context, name, undefined) - }, [setPref, context, name]) if (value === undefined) { return null } - return + return } const BooleanPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps) => { - const onChange = useCallback((evt: React.ChangeEvent) => { - setPref(context, name, evt.target.checked) - }, [setPref, context, name]) return
- - {useRemover(context, setPref, name, value)} + setPref(context, name, evt.target.checked)}/> + {makeRemover(context, setPref, name, value)}
} const TextPreferenceCell = ({ context, name, setPref, value, inheritedValue }: PreferenceCellProps) => { - const onChange = useCallback((evt: React.ChangeEvent) => { - setPref(context, name, evt.target.value) - }, [setPref, context, name]) return
- - {useRemover(context, setPref, name, value)} + setPref(context, name, evt.target.value)}/> + {makeRemover(context, setPref, name, value)}
} const SelectPreferenceCell = ({ context, name, pref, setPref, value, inheritedValue }: PreferenceCellProps) => { - const onChange = useCallback((evt: React.ChangeEvent) => { - setPref(context, name, evt.target.value) - }, [setPref, context, name]) - const remover = useRemover(context, setPref, name, value) if (!pref.allowedValues) { return null } return
- setPref(context, name, evt.target.value)}> {pref.allowedValues.map(value => )} - {remover} + {makeRemover(context, setPref, name, value)}
} @@ -186,7 +173,7 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta const client = use(ClientContext)! const appliedContext = getActiveCSSContext(client, room) const [context, setContext] = useState(appliedContext) - const getContextText = useCallback((context: PreferenceContext) => { + const getContextText = (context: PreferenceContext) => { if (context === PreferenceContext.Account) { return client.store.serverPreferenceCache.custom_css } else if (context === PreferenceContext.Device) { @@ -196,17 +183,17 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta } else if (context === PreferenceContext.RoomDevice) { return room.localPreferenceCache.custom_css } - }, [client, room]) + } const origText = getContextText(context) const [text, setText] = useState(origText ?? "") - const onChangeContext = useCallback((evt: React.ChangeEvent) => { + const onChangeContext = (evt: React.ChangeEvent) => { const newContext = evt.target.value as PreferenceContext setContext(newContext) setText(getContextText(newContext) ?? "") - }, [getContextText]) - const onChangeText = useCallback((evt: React.ChangeEvent) => { + } + const onChangeText = (evt: React.ChangeEvent) => { setText(evt.target.value) - }, []) + } const onSave = useEvent(() => { if (vscodeOpen) { setText(vscodeContentRef.current) @@ -215,18 +202,18 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta setPref(context, "custom_css", text) } }) - const onDelete = useEvent(() => { + const onDelete = () => { setPref(context, "custom_css", undefined) setText("") - }) + } const [vscodeOpen, setVSCodeOpen] = useState(false) const vscodeContentRef = useRef("") const vscodeInitialContentRef = useRef("") - const onClickVSCode = useEvent(() => { + const onClickVSCode = () => { vscodeContentRef.current = text vscodeInitialContentRef.current = text setVSCodeOpen(true) - }) + } const closeVSCode = useCallback(() => { setVSCodeOpen(false) setText(vscodeContentRef.current) @@ -296,7 +283,9 @@ const SettingsView = ({ room }: SettingsViewProps) => { const roomMeta = useEventAsState(room.meta) const client = use(ClientContext)! const closeModal = use(ModalCloseContext) - const setPref = useCallback((context: PreferenceContext, key: keyof Preferences, value: PreferenceValueType | undefined) => { + const setPref = useCallback(( + context: PreferenceContext, key: keyof Preferences, value: PreferenceValueType | undefined, + ) => { if (context === PreferenceContext.Account) { client.rpc.setAccountData("fi.mau.gomuks.preferences", { ...client.store.serverPreferenceCache, @@ -321,15 +310,15 @@ const SettingsView = ({ room }: SettingsViewProps) => { } } }, [client, room]) - const onClickLogout = useCallback(() => { + const onClickLogout = () => { if (window.confirm("Really log out and delete all local data?")) { client.logout().then( () => console.info("Successfully logged out"), err => window.alert(`Failed to log out: ${err}`), ) } - }, [client]) - const onClickLeave = useCallback(() => { + } + const onClickLeave = () => { if (window.confirm(`Really leave ${room.meta.current.name}?`)) { client.rpc.leaveRoom(room.roomID).then( () => { @@ -339,7 +328,7 @@ const SettingsView = ({ room }: SettingsViewProps) => { err => window.alert(`Failed to leave room: ${err}`), ) } - }, [client, room, closeModal]) + } usePreferences(client.store, room) const globalServer = client.store.serverPreferenceCache const globalLocal = client.store.localPreferenceCache diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index e01dbb8..7e34e18 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { JSX, use, useCallback, useState } from "react" +import React, { JSX, use, useState } from "react" import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" @@ -79,7 +79,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven const mainScreen = use(MainScreenContext) const openModal = use(ModalContext) const [forceContextMenuOpen, setForceContextMenuOpen] = useState(false) - const onContextMenu = useCallback((mouseEvt: React.MouseEvent) => { + const onContextMenu = (mouseEvt: React.MouseEvent) => { const targetElem = mouseEvt.target as HTMLElement if ( !roomCtx.store.preferences.message_context_menu @@ -97,7 +97,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven style={getModalStyleFromMouse(mouseEvt, 9 * 40)} />, }) - }, [openModal, evt, roomCtx]) + } const memberEvt = useRoomMember(client, roomCtx.store, evt.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const BodyType = getBodyType(evt) diff --git a/web/src/ui/timeline/TimelineView.tsx b/web/src/ui/timeline/TimelineView.tsx index b2c01ec..9bb8903 100644 --- a/web/src/ui/timeline/TimelineView.tsx +++ b/web/src/ui/timeline/TimelineView.tsx @@ -46,13 +46,13 @@ const TimelineView = () => { // When the user scrolls the timeline manually, remember if they were at the bottom, // so that we can keep them at the bottom when new events are added. - const handleScroll = useCallback(() => { + const handleScroll = () => { if (!timelineViewRef.current) { return } const timelineView = timelineViewRef.current roomCtx.scrolledToBottom = timelineView.scrollTop + timelineView.clientHeight + 1 >= timelineView.scrollHeight - }, [roomCtx]) + } // Save the scroll height prior to updating the timeline, so that we can adjust the scroll position if needed. if (timelineViewRef.current) { oldScrollHeight.current = timelineViewRef.current.scrollHeight diff --git a/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx b/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx index f59b2f5..ff3ed8b 100644 --- a/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx +++ b/web/src/ui/timeline/menu/ConfirmWithMessageModal.tsx @@ -13,9 +13,9 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback, useState } from "react" +import React, { use, useState } from "react" import { MemDBEvent } from "@/api/types" -import useEvent from "@/util/useEvent.ts" +import { isMobileDevice } from "@/util/ismobile.ts" import { ModalCloseContext } from "../../modal/Modal.tsx" import TimelineEvent from "../TimelineEvent.tsx" @@ -33,14 +33,11 @@ const ConfirmWithMessageModal = ({ }: ConfirmWithMessageProps) => { const [reason, setReason] = useState("") const closeModal = use(ModalCloseContext) - const onConfirmWrapped = useEvent((evt: React.FormEvent) => { + const onConfirmWrapped = (evt: React.FormEvent) => { evt.preventDefault() closeModal() onConfirm(reason) - }) - const onChangeReason = useCallback((evt: React.ChangeEvent) => { - setReason(evt.target.value) - }, []) + } return

{title}

@@ -49,7 +46,13 @@ const ConfirmWithMessageModal = ({
{description}
- + setReason(evt.target.value)} + />
diff --git a/web/src/ui/timeline/menu/usePrimaryItems.tsx b/web/src/ui/timeline/menu/usePrimaryItems.tsx index b53a64a..f570f40 100644 --- a/web/src/ui/timeline/menu/usePrimaryItems.tsx +++ b/web/src/ui/timeline/menu/usePrimaryItems.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { CSSProperties, use, useCallback } from "react" +import React, { CSSProperties, use } from "react" import Client from "@/api/client.ts" import { MemDBEvent } from "@/api/types" import { emojiToReactionContent } from "@/util/emoji" @@ -43,11 +43,11 @@ export const usePrimaryItems = ( const closeModal = !isHover ? use(ModalCloseContext) : noop const openModal = use(ModalContext) - const onClickReply = useCallback(() => { + const onClickReply = () => { roomCtx.setReplyTo(evt.event_id) closeModal() - }, [roomCtx, evt.event_id, closeModal]) - const onClickReact = useCallback((mevt: React.MouseEvent) => { + } + const onClickReact = (mevt: React.MouseEvent) => { const emojiPickerHeight = 34 * 16 setForceOpen?.(true) openModal({ @@ -63,20 +63,20 @@ export const usePrimaryItems = ( />, onClose: () => setForceOpen?.(false), }) - }, [client, roomCtx, evt, style, setForceOpen, openModal]) - const onClickEdit = useCallback(() => { + } + const onClickEdit = () => { closeModal() roomCtx.setEditing(evt) - }, [roomCtx, evt, closeModal]) - const onClickResend = useCallback(() => { + } + const onClickResend = () => { if (!evt.transaction_id) { return } closeModal() client.resendEvent(evt.transaction_id) .catch(err => window.alert(`Failed to resend message: ${err}`)) - }, [client, evt.transaction_id, closeModal]) - const onClickMore = useCallback((mevt: React.MouseEvent) => { + } + const onClickMore = (mevt: React.MouseEvent) => { const moreMenuHeight = 4 * 40 setForceOpen!(true) openModal({ @@ -87,7 +87,7 @@ export const usePrimaryItems = ( />, onClose: () => setForceOpen!(false), }) - }, [evt, roomCtx, setForceOpen, openModal]) + } const isEditing = useEventAsState(roomCtx.isEditing) const [isPending, pendingTitle] = getPending(evt) const isEncrypted = getEncryption(roomCtx.store) diff --git a/web/src/ui/timeline/menu/useSecondaryItems.tsx b/web/src/ui/timeline/menu/useSecondaryItems.tsx index 1aca07d..046aff4 100644 --- a/web/src/ui/timeline/menu/useSecondaryItems.tsx +++ b/web/src/ui/timeline/menu/useSecondaryItems.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useCallback } from "react" +import { use } from "react" import Client from "@/api/client.ts" import { useRoomState } from "@/api/statestore" import { MemDBEvent } from "@/api/types" @@ -35,14 +35,14 @@ export const useSecondaryItems = ( ) => { const closeModal = use(ModalCloseContext) const openModal = use(ModalContext) - const onClickViewSource = useCallback(() => { + const onClickViewSource = () => { openModal({ dimmed: true, boxed: true, content: , }) - }, [evt, openModal]) - const onClickReport = useCallback(() => { + } + const onClickReport = () => { openModal({ dimmed: true, boxed: true, @@ -61,8 +61,8 @@ export const useSecondaryItems = ( /> , }) - }, [evt, roomCtx, openModal, client]) - const onClickRedact = useCallback(() => { + } + const onClickRedact = () => { openModal({ dimmed: true, boxed: true, @@ -81,17 +81,12 @@ export const useSecondaryItems = ( /> , }) - }, [evt, roomCtx, openModal, client]) - const onClickPin = useCallback(() => { + } + const onClickPin = (pin: boolean) => () => { closeModal() - client.pinMessage(roomCtx.store, evt.event_id, true) - .catch(err => window.alert(`Failed to pin message: ${err}`)) - }, [closeModal, client, roomCtx, evt.event_id]) - const onClickUnpin = useCallback(() => { - closeModal() - client.pinMessage(roomCtx.store, evt.event_id, false) - .catch(err => window.alert(`Failed to unpin message: ${err}`)) - }, [closeModal, client, roomCtx, evt.event_id]) + client.pinMessage(roomCtx.store, evt.event_id, pin) + .catch(err => window.alert(`Failed to ${pin ? "pin" : "unpin"} message: ${err}`)) + } const [isPending, pendingTitle] = getPending(evt) useRoomState(roomCtx.store, "m.room.power_levels", "") @@ -109,8 +104,12 @@ export const useSecondaryItems = ( return <> {ownPL >= pinPL && (pins.includes(evt.event_id) - ? - : )} + ? + : )} {canRedact &&
} From 277732efd95a3b009b0de0751380f7376273d4f2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 10:56:33 +0200 Subject: [PATCH 17/85] web/app: add missing return to effect --- web/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 8de3b8e..e6d986f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -38,7 +38,7 @@ function App() { const clientState = useEventAsState(client.state) useEffect(() => { window.client = client - client.start() + return client.start() }, [client]) const afterConnectError = Boolean(connState?.error && connState.reconnecting && clientState?.is_verified) From 5fae26480217e50a13bdbad67beaf574395fa591 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 11:35:23 +0200 Subject: [PATCH 18/85] web/polyfill: make toArray work on map iterators --- web/src/util/polyfill.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/util/polyfill.js b/web/src/util/polyfill.js index c98c5d9..d123cde 100644 --- a/web/src/util/polyfill.js +++ b/web/src/util/polyfill.js @@ -15,7 +15,8 @@ // along with this program. If not, see . if (!window.Iterator?.prototype.map) { - ;(new Map([])).keys().__proto__.map = function(callbackFn) { + const iterProto = (new Map([])).keys().__proto__ + iterProto.map = function(callbackFn) { const output = [] let i = 0 for (const item of this) { @@ -24,7 +25,7 @@ if (!window.Iterator?.prototype.map) { } return output } - ;(new Map([])).keys().__proto__.filter = function(callbackFn) { + iterProto.filter = function(callbackFn) { const output = [] let i = 0 for (const item of this) { @@ -35,6 +36,10 @@ if (!window.Iterator?.prototype.map) { } return output } + const identity = x => x + iterProto.toArray = function() { + return this.map(identity) + } Array.prototype.toArray = function() { return this } From 266116f2372729c59e300aab224873cf8ee4f638 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 12:03:06 +0200 Subject: [PATCH 19/85] web/timeline: don't render url previews on hidden events --- web/src/ui/timeline/TimelineEvent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 4065934..54c0726 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -215,7 +215,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven {replyInMessage} - + {!isSmallBodyType && } {evt.reactions ? : null}
From 4c8497e5d985d6e7fd7fe6b649c398be8e04eee9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 13:25:35 +0200 Subject: [PATCH 20/85] web/preferences: add option to change window title Fixes #564 --- web/src/api/statestore/main.ts | 2 +- web/src/api/statestore/room.ts | 2 +- web/src/api/types/preferences/preferences.ts | 14 +++++++++++++- web/src/api/types/preferences/proxy.ts | 4 ++-- web/src/ui/MainScreen.tsx | 11 +++++++++-- web/src/ui/settings/SettingsView.tsx | 3 +++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 9ffa945..65dac61 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -76,7 +76,7 @@ export class StateStore { readonly accountData: Map = new Map() readonly accountDataSubs = new MultiSubscribable() readonly emojiRoomsSub = new Subscribable() - readonly preferences: Preferences = getPreferenceProxy(this) + readonly preferences = getPreferenceProxy(this) #frequentlyUsedEmoji: Map | null = null #emojiPackKeys: RoomStateGUID[] | null = null #watchedRoomEmojiPacks: Record | null = null diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 23fc555..3896de7 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -112,7 +112,7 @@ export class RoomStateStore { readonly accountDataSubs = new MultiSubscribable() readonly openNotifications: Map = new Map() readonly #emojiPacksCache: Map = new Map() - readonly preferences: Preferences + readonly preferences: Required readonly localPreferenceCache: Preferences readonly preferenceSub = new NoDataSubscribable() serverPreferenceCache: Preferences = {} diff --git a/web/src/api/types/preferences/preferences.ts b/web/src/api/types/preferences/preferences.ts index a535817..9f7e253 100644 --- a/web/src/api/types/preferences/preferences.ts +++ b/web/src/api/types/preferences/preferences.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import type { ContentURI } from "../../types" -import { Preference, anyContext } from "./types.ts" +import { Preference, anyContext, anyGlobalContext } from "./types.ts" export const codeBlockStyles = [ "auto", "abap", "algol_nu", "algol", "arduino", "autumn", "average", "base16-snazzy", "borland", "bw", @@ -171,6 +171,18 @@ export const preferences = { allowedContexts: anyContext, defaultValue: "", }), + room_window_title: new Preference({ + displayName: "In-room window title", + description: "The title to use for the window when viewing a room. $room will be replaced with the room name", + allowedContexts: anyContext, + defaultValue: "$room - gomuks web", + }), + window_title: new Preference({ + displayName: "Default window title", + description: "The title to use for the window when not in a room.", + allowedContexts: anyGlobalContext, + defaultValue: "gomuks web", + }), } as const export const existingPreferenceKeys = new Set(Object.keys(preferences)) diff --git a/web/src/api/types/preferences/proxy.ts b/web/src/api/types/preferences/proxy.ts index d9cd11a..f7f0b68 100644 --- a/web/src/api/types/preferences/proxy.ts +++ b/web/src/api/types/preferences/proxy.ts @@ -19,7 +19,7 @@ import { PreferenceContext, PreferenceValueType } from "./types.ts" const prefKeys = Object.keys(preferences) -export function getPreferenceProxy(store: StateStore, room?: RoomStateStore): Preferences { +export function getPreferenceProxy(store: StateStore, room?: RoomStateStore): Required { return new Proxy({}, { set(): boolean { throw new Error("The preference proxy is read-only") @@ -61,5 +61,5 @@ export function getPreferenceProxy(store: StateStore, room?: RoomStateStore): Pr writable: false, } : undefined }, - }) + }) as Required } diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index ca2c381..2ae5194 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -124,6 +124,13 @@ class ContextFields implements MainScreenContextFields { } } + #getWindowTitle(room?: RoomStateStore, name?: string) { + if (!room) { + return this.client.store.preferences.window_title + } + return room.preferences.room_window_title.replace("$room", name!) + } + #setActiveRoom(room: RoomStateStore, pushState: boolean) { window.activeRoom = room this.directSetActiveRoom(room) @@ -147,7 +154,7 @@ class ContextFields implements MainScreenContextFields { if (roomNameForTitle && roomNameForTitle.length > 48) { roomNameForTitle = roomNameForTitle.slice(0, 45) + "…" } - document.title = `${roomNameForTitle} - gomuks web` + document.title = this.#getWindowTitle(room, roomNameForTitle) } #closeActiveRoom(pushState: boolean) { @@ -161,7 +168,7 @@ class ContextFields implements MainScreenContextFields { if (pushState) { history.pushState({}, "") } - document.title = "gomuks web" + document.title = this.#getWindowTitle() } clickRoom = (evt: React.MouseEvent) => { diff --git a/web/src/ui/settings/SettingsView.tsx b/web/src/ui/settings/SettingsView.tsx index e2b6b86..6f9aefc 100644 --- a/web/src/ui/settings/SettingsView.tsx +++ b/web/src/ui/settings/SettingsView.tsx @@ -110,6 +110,9 @@ const PreferenceRow = ({ val: PreferenceValueType | undefined, inheritedVal: PreferenceValueType, ) => { + if (!pref.allowedContexts.includes(context)) { + return null + } if (prefType === "boolean") { return Date: Mon, 23 Dec 2024 13:30:20 +0200 Subject: [PATCH 21/85] web/preferences: add option to change favicon --- web/index.html | 2 +- web/src/api/types/preferences/preferences.ts | 6 ++++++ web/src/ui/StylePreferences.tsx | 7 ++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/web/index.html b/web/index.html index 5de864d..40252d6 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,7 @@ - + gomuks web diff --git a/web/src/api/types/preferences/preferences.ts b/web/src/api/types/preferences/preferences.ts index 9f7e253..94e8764 100644 --- a/web/src/api/types/preferences/preferences.ts +++ b/web/src/api/types/preferences/preferences.ts @@ -183,6 +183,12 @@ export const preferences = { allowedContexts: anyGlobalContext, defaultValue: "gomuks web", }), + favicon: new Preference({ + displayName: "Favicon", + description: "The URL to use for the favicon.", + allowedContexts: anyContext, + defaultValue: "gomuks.png", + }), } as const export const existingPreferenceKeys = new Set(Object.keys(preferences)) diff --git a/web/src/ui/StylePreferences.tsx b/web/src/ui/StylePreferences.tsx index b410598..8c2da2b 100644 --- a/web/src/ui/StylePreferences.tsx +++ b/web/src/ui/StylePreferences.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { useInsertionEffect } from "react" +import React, { useEffect, useInsertionEffect } from "react" import type Client from "@/api/client.ts" import { RoomStateStore, usePreferences } from "@/api/statestore" @@ -128,7 +128,12 @@ const StylePreferences = ({ client, activeRoom }: StylePreferencesProps) => { @import url("_gomuks/codeblock/${preferences.code_block_theme}.css"); `, [preferences.code_block_theme], "gomuks-pref-code-block-theme") useAsyncStyle(() => preferences.custom_css, [preferences.custom_css], "gomuks-pref-custom-css") + useEffect(() => { + favicon.href = preferences.favicon + }, [preferences.favicon]) return null } +const favicon = document.getElementById("favicon") as HTMLLinkElement + export default React.memo(StylePreferences) From a0ce4f8cfe10006a3eeb74da3630b6f4f815c6c9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 16:08:29 +0200 Subject: [PATCH 22/85] web/mainscreen: ensure sidebar resize handles are on top --- web/src/ui/MainScreen.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/ui/MainScreen.css b/web/src/ui/MainScreen.css index b1604fb..bfe308d 100644 --- a/web/src/ui/MainScreen.css +++ b/web/src/ui/MainScreen.css @@ -40,10 +40,12 @@ main.matrix-main { > div.room-list-resizer { grid-area: rh1; + z-index: 1; } > div.right-panel-resizer { grid-area: rh2; + z-index: 1; } } From b563c31a2723bbeee8fa0c4cc2257ef78231448a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 21:27:15 +0200 Subject: [PATCH 23/85] web/composer: fix sticker button size --- web/src/ui/composer/MessageComposer.css | 5 +++++ web/src/ui/timeline/menu/index.css | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/web/src/ui/composer/MessageComposer.css b/web/src/ui/composer/MessageComposer.css index 46e320a..f6e83b9 100644 --- a/web/src/ui/composer/MessageComposer.css +++ b/web/src/ui/composer/MessageComposer.css @@ -34,6 +34,11 @@ div.message-composer { height: 2rem; width: 2rem; padding: .25rem; + + > svg { + width: 1.5rem; + height: 1.5rem; + } } > input[type="file"] { diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index f4685dc..9005345 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -37,6 +37,11 @@ div.event-context-menu { justify-content: left; gap: .5rem; + > svg { + width: 1.5rem; + height: 1.5rem; + } + &:first-of-type { border-radius: .5rem .5rem 0 0; } From 5fbb8a21ab177145eefd7a94ae18ec33bc6ef992 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 23 Dec 2024 22:46:10 +0200 Subject: [PATCH 24/85] hicli/sync: fix detecting db lock errors --- pkg/hicli/syncwrap.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/hicli/syncwrap.go b/pkg/hicli/syncwrap.go index e479e1c..8492da2 100644 --- a/pkg/hicli/syncwrap.go +++ b/pkg/hicli/syncwrap.go @@ -13,6 +13,7 @@ import ( "time" "github.com/mattn/go-sqlite3" + "github.com/rs/zerolog" "maunium.net/go/mautrix" "maunium.net/go/mautrix/id" @@ -46,7 +47,9 @@ func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync, err = c.DB.DoTxn(ctx, nil, func(ctx context.Context) error { return c.processSyncResponse(ctx, resp, since) }) - if errors.Is(err, sqlite3.ErrLocked) && i < 24 { + var sqliteErr sqlite3.Error + if errors.As(err, &sqliteErr) && sqliteErr.Code == sqlite3.ErrBusy && i < 24 { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Database is busy, retrying") c.markSyncErrored(err, false) continue } else if err != nil { From 7cad53bb7d03c6a84fa247444df31b57a0613524 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 25 Dec 2024 13:56:19 +0200 Subject: [PATCH 25/85] web/css: ensure body background color applies to whole height --- web/src/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/index.css b/web/src/index.css index f7b637f..48a3a6e 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -163,6 +163,7 @@ body { font-size: 16px; touch-action: none; color: var(--text-color); + min-height: 100vh; } html { From eda7d6790e4b9bbbbea48710d81b6efb1089158e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 25 Dec 2024 14:04:18 +0200 Subject: [PATCH 26/85] web/timeline: fix media spoilers Closes #567 --- web/src/ui/timeline/content/MediaMessageBody.tsx | 5 ++++- web/src/ui/timeline/content/index.css | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/ui/timeline/content/MediaMessageBody.tsx b/web/src/ui/timeline/content/MediaMessageBody.tsx index 01fc95f..9a8401a 100644 --- a/web/src/ui/timeline/content/MediaMessageBody.tsx +++ b/web/src/ui/timeline/content/MediaMessageBody.tsx @@ -59,7 +59,10 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => { } } const renderMediaElem = !supportsClickToShow || showPreviewsByDefault || clickedShow - const renderPlaceholderElem = supportsClickToShow && (!renderMediaElem || !!contentWarning || !loaded) + const renderPlaceholderElem = supportsClickToShow + && (!renderMediaElem + || (contentWarning && !clickedShow) + || !loaded) const isLoadingOnlyCover = !loaded && !contentWarning && renderMediaElem const [mediaContent, containerClass, containerStyle] = useMediaContent(content, event.type, undefined, onLoad) diff --git a/web/src/ui/timeline/content/index.css b/web/src/ui/timeline/content/index.css index 0a403a2..bac8ff7 100644 --- a/web/src/ui/timeline/content/index.css +++ b/web/src/ui/timeline/content/index.css @@ -215,6 +215,10 @@ div.media-container { border-radius: .25rem; } + &:has(> div.empty-placeholder) + img { + filter: blur(16px); + } + & + img { /* In order loading=lazy to work, the image has to be visible, so put it behind the placeholder instead of below */ From 6aa4e91c0a8f342332e0529c496d089ffc5f33d9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 25 Dec 2024 14:20:04 +0200 Subject: [PATCH 27/85] web/css: fix background colors again --- web/src/App.tsx | 8 ++++---- web/src/index.css | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index e6d986f..816db3c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -70,18 +70,18 @@ function App() {
: null if (connState?.error && !afterConnectError) { - return errorOverlay + return
{errorOverlay}
} else if ((!connState?.connected && !afterConnectError) || !clientState) { const msg = connState?.connected ? "Waiting for client state..." : "Connecting to backend..." - return
+ return
{msg}
} else if (!clientState.is_logged_in) { - return + return
} else if (!clientState.is_verified) { - return + return
} else { return diff --git a/web/src/index.css b/web/src/index.css index 48a3a6e..6517adc 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -158,7 +158,7 @@ body { font-family: var(--font-stack); margin: 0; padding: 0; - background-color: var(--login-background-color); + background-color: var(--background-color); line-height: 1.5; font-size: 16px; touch-action: none; @@ -248,9 +248,15 @@ div.connection-error-wrapper { } } -div.pre-connect { - margin-top: 2rem; - text-align: center; +div.pre-main { + position: fixed; + inset: 0; + background-color: var(--login-background-color); + + &.waiting-to-connect { + padding-top: 2rem; + text-align: center; + } } a { From 57e067b67195dfd69e1778a89616a8f0395cf461 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 26 Dec 2024 17:16:05 +0200 Subject: [PATCH 28/85] web/stickerpicker: always include info Fixes #568 --- web/src/ui/emojipicker/StickerPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/emojipicker/StickerPicker.tsx b/web/src/ui/emojipicker/StickerPicker.tsx index 2c6e0ce..3f772fe 100644 --- a/web/src/ui/emojipicker/StickerPicker.tsx +++ b/web/src/ui/emojipicker/StickerPicker.tsx @@ -47,7 +47,7 @@ const StickerPicker = ({ style, onSelect, room }: MediaPickerProps) => { onSelect({ msgtype: "m.sticker", body: emoji.t, - info: emoji.i, + info: emoji.i ?? {}, url: emoji.u, }) close() From 8b1354b4a77ca0a68ef4dabb5a875e0fb8bd92b6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 27 Dec 2024 16:48:45 +0200 Subject: [PATCH 29/85] hicli/send: add /rawstate command --- pkg/hicli/send.go | 11 +++++++++++ web/src/api/client.ts | 4 +++- web/src/api/rpc.ts | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/hicli/send.go b/pkg/hicli/send.go index 9670fac..998382f 100644 --- a/pkg/hicli/send.go +++ b/pkg/hicli/send.go @@ -91,6 +91,17 @@ func (h *HiClient) SendMessage( return nil, fmt.Errorf("invalid JSON in /raw command") } return h.send(ctx, roomID, event.Type{Type: parts[1]}, content, "", unencrypted) + } else if strings.HasPrefix(text, "/rawstate ") { + parts := strings.SplitN(text, " ", 4) + if len(parts) < 4 || len(parts[1]) == 0 { + return nil, fmt.Errorf("invalid /rawstate command") + } + content := json.RawMessage(parts[3]) + if !json.Valid(content) { + return nil, fmt.Errorf("invalid JSON in /rawstate command") + } + _, err := h.SetState(ctx, roomID, event.Type{Type: parts[1], Class: event.StateEventType}, parts[2], content) + return nil, err } var content event.MessageEventContent msgType := event.MsgText diff --git a/web/src/api/client.ts b/web/src/api/client.ts index cbdefd0..89e78a0 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -211,7 +211,9 @@ export default class Client { throw new Error("Room not found") } const dbEvent = await this.rpc.sendMessage(params) - this.#handleOutgoingEvent(dbEvent, room) + if (dbEvent) { + this.#handleOutgoingEvent(dbEvent, room) + } } async subscribeToEmojiPack(pack: RoomStateGUID, subscribe: boolean = true) { diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index 7f9004f..b967010 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -138,7 +138,7 @@ export default abstract class RPCClient { return this.request("logout", {}) } - sendMessage(params: SendMessageParams): Promise { + sendMessage(params: SendMessageParams): Promise { return this.request("send_message", params) } From 8f46121413c9129a76791bf27c889060eda9f7f7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 27 Dec 2024 20:04:38 +0200 Subject: [PATCH 30/85] web/login: fix login screen sizing --- web/src/ui/login/LoginScreen.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/ui/login/LoginScreen.css b/web/src/ui/login/LoginScreen.css index 2ea8b7d..09a6865 100644 --- a/web/src/ui/login/LoginScreen.css +++ b/web/src/ui/login/LoginScreen.css @@ -1,19 +1,23 @@ main.matrix-login { - max-width: 30rem; + max-width: 42rem; width: 100%; padding: 3rem 6rem; + box-sizing: border-box; box-shadow: 0 0 1rem var(--modal-box-shadow-color); - margin: 2rem; + margin: 2rem auto; @media (width < 800px) { + max-width: 38rem; padding: 2rem 4rem; + width: calc(100% - 4rem); } @media (width < 500px) { padding: 1rem; box-shadow: none; margin: 0 !important; + width: 100%; } h1 { From 0b424e59bf0f85616a9ddbaedc7cea182fae7416 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Dec 2024 17:19:33 +0200 Subject: [PATCH 31/85] web/roomlist: add bidi isolate for sender names --- web/src/ui/roomlist/Entry.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/ui/roomlist/Entry.tsx b/web/src/ui/roomlist/Entry.tsx index efe235a..c26f2fa 100644 --- a/web/src/ui/roomlist/Entry.tsx +++ b/web/src/ui/roomlist/Entry.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { memo, use } from "react" +import { JSX, memo, use } from "react" import { getRoomAvatarURL } from "@/api/media.ts" import type { RoomListEntry } from "@/api/statestore" import type { MemDBEvent, MemberEventContent } from "@/api/types" @@ -28,9 +28,9 @@ export interface RoomListEntryProps { hidden: boolean } -function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): [string, string] { +function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): [string, JSX.Element | null] { if (!evt) { - return ["", ""] + return ["", null] } if ((evt.type === "m.room.message" || evt.type === "m.sticker") && typeof evt.content.body === "string") { // eslint-disable-next-line react-hooks/rules-of-hooks @@ -44,10 +44,14 @@ function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): } return [ `${displayname}: ${evt.content.body}`, - `${displayname.length > 16 ? displayname.slice(0, 12) + "…" : displayname}: ${previewText}`, + <> + + {displayname.length > 16 ? displayname.slice(0, 12) + "…" : displayname} + : {previewText} + , ] } - return ["", ""] + return ["", null] } function renderEntry(room: RoomListEntry) { From a0bc1b0d17f2badab4689165f8acecca9c647d40 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 14:39:43 +0200 Subject: [PATCH 32/85] web/statestore: fix dm_user_id field in room list entries --- web/src/api/media.ts | 4 ++-- web/src/api/statestore/main.ts | 4 ++-- web/src/api/statestore/room.ts | 2 +- web/src/api/types/mxtypes.ts | 2 +- web/src/ui/rightpanel/UserInfoMutualRooms.tsx | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 5ece955..5c97ac1 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -103,8 +103,8 @@ export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: Conten if ("dm_user_id" in room) { dmUserID = room.dm_user_id } else if ("lazy_load_summary" in room) { - dmUserID = room.lazy_load_summary?.heroes?.length === 1 - ? room.lazy_load_summary.heroes[0] : undefined + dmUserID = room.lazy_load_summary?.["m.heroes"]?.length === 1 + ? room.lazy_load_summary["m.heroes"][0] : undefined } return getAvatarURL(dmUserID ?? room.room_id, { displayname: room.name, diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 65dac61..f4332f8 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -143,8 +143,8 @@ export class StateStore { const name = entry.meta.name ?? "Unnamed room" return { room_id: entry.meta.room_id, - dm_user_id: entry.meta.lazy_load_summary?.heroes?.length === 1 - ? entry.meta.lazy_load_summary.heroes[0] : undefined, + dm_user_id: entry.meta.lazy_load_summary?.["m.heroes"]?.length === 1 + ? entry.meta.lazy_load_summary["m.heroes"][0] : undefined, sorting_timestamp: entry.meta.sorting_timestamp, preview_event, preview_sender, diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 3896de7..20b4ae3 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -62,7 +62,7 @@ function arraysAreEqual(arr1?: T[], arr2?: T[]): boolean { function llSummaryIsEqual(ll1?: LazyLoadSummary, ll2?: LazyLoadSummary): boolean { return ll1?.["m.joined_member_count"] === ll2?.["m.joined_member_count"] && ll1?.["m.invited_member_count"] === ll2?.["m.invited_member_count"] && - arraysAreEqual(ll1?.heroes, ll2?.heroes) + arraysAreEqual(ll1?.["m.heroes"], ll2?.["m.heroes"]) } function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean { diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 45fe52c..5932e3e 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -43,7 +43,7 @@ export interface TombstoneEventContent { } export interface LazyLoadSummary { - heroes?: UserID[] + "m.heroes"?: UserID[] "m.joined_member_count"?: number "m.invited_member_count"?: number } diff --git a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx index 3238699..fadd041 100644 --- a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx +++ b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx @@ -40,8 +40,8 @@ const MutualRooms = ({ client, userID }: MutualRoomsProps) => { } return { room_id: roomID, - dm_user_id: roomData.meta.current.lazy_load_summary?.heroes?.length === 1 - ? roomData.meta.current.lazy_load_summary.heroes[0] : undefined, + dm_user_id: roomData.meta.current.lazy_load_summary?.["m.heroes"]?.length === 1 + ? roomData.meta.current.lazy_load_summary["m.heroes"][0] : undefined, name: roomData.meta.current.name ?? "Unnamed room", avatar: roomData.meta.current.avatar, search_name: "", From e750c19e8a55df2fad1288bdc3d3f9667d7f3337 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 15:07:18 +0200 Subject: [PATCH 33/85] web/mainscreen: fix handling popstate event to null state --- web/src/ui/MainScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 2ae5194..63173ed 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -294,8 +294,8 @@ const MainScreen = () => { const roomID = evt.state?.room_id ?? null if (roomID !== client.store.activeRoomID) { context.setActiveRoom(roomID, { - alias: ensureString(evt?.state.source_alias) || undefined, - via: ensureStringArray(evt?.state.source_via), + alias: ensureString(evt.state?.source_alias) || undefined, + via: ensureStringArray(evt.state?.source_via), }, false) } context.setRightPanel(evt.state?.right_panel ?? null, false) From a1bddd6b6b2bc599d6c0ac062653351e8a77e2da Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 16:37:14 +0200 Subject: [PATCH 34/85] web/timeline: add special message menu for mobile --- web/src/ui/timeline/TimelineEvent.tsx | 18 +++++++++++-- web/src/ui/timeline/menu/EventMenu.tsx | 19 +++++++++++--- web/src/ui/timeline/menu/index.css | 26 ++++++++++++++++--- web/src/ui/timeline/menu/index.ts | 2 +- web/src/ui/timeline/menu/usePrimaryItems.tsx | 10 ++++--- .../ui/timeline/menu/useSecondaryItems.tsx | 13 ++++++---- 6 files changed, 68 insertions(+), 20 deletions(-) diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 54c0726..760428e 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -27,7 +27,7 @@ import ReadReceipts from "./ReadReceipts.tsx" import { ReplyIDBody } from "./ReplyBody.tsx" import URLPreviews from "./URLPreviews.tsx" 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 PendingIcon from "@/icons/pending.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: , + }) + } const memberEvt = useRoomMember(client, roomCtx.store, evt.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const BodyType = getBodyType(evt) @@ -175,11 +188,12 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven data-event-id={evt.event_id} className={wrapperClassNames.join(" ")} onContextMenu={onContextMenu} + onClick={!disableMenu && isMobileDevice ? onClick : undefined} > {!disableMenu && !isMobileDevice &&
- +
} {replyAboveMessage} {renderAvatar &&
void } -export const EventHoverMenu = ({ evt, setForceOpen }: EventHoverMenuProps) => { - const elements = usePrimaryItems(use(ClientContext)!, useRoomContext(), evt, true, undefined, setForceOpen) +export const EventHoverMenu = ({ evt, roomCtx, setForceOpen }: EventHoverMenuProps) => { + const elements = usePrimaryItems(use(ClientContext)!, roomCtx, evt, true, false, undefined, setForceOpen) return
{elements}
} @@ -43,7 +44,7 @@ export const EventExtraMenu = ({ evt, roomCtx, style }: EventContextMenuProps) = export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => { 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) return
{primary} @@ -51,3 +52,13 @@ export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) => {secondary}
} + +export const EventFixedMenu = ({ evt, roomCtx }: Omit) => { + const client = use(ClientContext)! + const primary = usePrimaryItems(client, roomCtx, evt, false, true, undefined, undefined) + const secondary = useSecondaryItems(client, roomCtx, evt, false) + return
+ {primary} + {secondary} +
+} diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index 9005345..da720c7 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -2,13 +2,9 @@ div.event-hover-menu { position: absolute; right: .5rem; top: -1.5rem; - background-color: var(--background-color); border: 1px solid var(--border-color); border-radius: .5rem; - display: flex; - gap: .25rem; padding: .125rem; - z-index: 1; > button { 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 { position: fixed; background-color: var(--background-color); diff --git a/web/src/ui/timeline/menu/index.ts b/web/src/ui/timeline/menu/index.ts index fc25eb0..4f89140 100644 --- a/web/src/ui/timeline/menu/index.ts +++ b/web/src/ui/timeline/menu/index.ts @@ -13,5 +13,5 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -export { EventExtraMenu, EventFullMenu, EventHoverMenu } from "./EventMenu.tsx" +export { EventExtraMenu, EventFixedMenu, EventFullMenu, EventHoverMenu } from "./EventMenu.tsx" export { getModalStyleFromMouse } from "./util.ts" diff --git a/web/src/ui/timeline/menu/usePrimaryItems.tsx b/web/src/ui/timeline/menu/usePrimaryItems.tsx index 1ebb0a9..d1f374c 100644 --- a/web/src/ui/timeline/menu/usePrimaryItems.tsx +++ b/web/src/ui/timeline/menu/usePrimaryItems.tsx @@ -37,9 +37,11 @@ export const usePrimaryItems = ( roomCtx: RoomContextData, evt: MemDBEvent, isHover: boolean, + isFixed: boolean, style?: CSSProperties, setForceOpen?: (forceOpen: boolean) => void, ) => { + const names = !isHover && !isFixed const closeModal = !isHover ? use(ModalCloseContext) : noop const openModal = use(ModalContext) @@ -108,11 +110,11 @@ export const usePrimaryItems = ( return <> {didFail && } {canReact && } {canSend && } {canEdit && } {isHover && } diff --git a/web/src/ui/timeline/menu/useSecondaryItems.tsx b/web/src/ui/timeline/menu/useSecondaryItems.tsx index f577aca..134d4f7 100644 --- a/web/src/ui/timeline/menu/useSecondaryItems.tsx +++ b/web/src/ui/timeline/menu/useSecondaryItems.tsx @@ -32,6 +32,7 @@ export const useSecondaryItems = ( client: Client, roomCtx: RoomContextData, evt: MemDBEvent, + names = true, ) => { const closeModal = use(ModalCloseContext) const openModal = use(ModalContext) @@ -102,20 +103,22 @@ export const useSecondaryItems = ( && (evt.sender === client.userID || ownPL >= redactOtherPL) return <> - + {ownPL >= pinPL && (pins.includes(evt.event_id) ? : )} - + {canRedact && } + >{names && "Remove"}} } From 8fa54a5beabc47534e3fa08828e276b1ff8c9620 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 16:44:33 +0200 Subject: [PATCH 35/85] web/timeline: fix fixed menu redact button color --- web/src/ui/timeline/menu/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index da720c7..e5abf5c 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -31,6 +31,10 @@ div.event-fixed-menu { > button { width: 3rem; height: 3rem; + + &.redact-button { + color: var(--error-color); + } } } From f83b914af096ac129cc4f5463ddde9b662ca1579 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 17:01:08 +0200 Subject: [PATCH 36/85] web/timeline: align fixed menu size with room header --- web/src/ui/timeline/menu/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index e5abf5c..2750816 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -22,9 +22,10 @@ div.event-hover-menu, div.event-fixed-menu { div.event-fixed-menu { position: fixed; inset: 0 0 auto; - height: 3rem; + height: 3.5rem; padding: .25rem; border-bottom: 1px solid var(--border-color); + box-sizing: border-box; justify-content: right; flex-direction: row-reverse; From 08a1712850c97f475dde7aa540c3a6f32f907ddb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 17:30:27 +0200 Subject: [PATCH 37/85] web/modal: don't capture input in context menu modal --- web/src/ui/modal/Modal.tsx | 31 ++++++++++++++++----------- web/src/ui/modal/contexts.ts | 1 + web/src/ui/timeline/TimelineEvent.tsx | 18 +++++++++++++--- web/src/ui/timeline/menu/index.css | 1 + web/src/vite-env.d.ts | 2 ++ 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/web/src/ui/modal/Modal.tsx b/web/src/ui/modal/Modal.tsx index 83a1176..1fc3ab6 100644 --- a/web/src/ui/modal/Modal.tsx +++ b/web/src/ui/modal/Modal.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { JSX, useCallback, useLayoutEffect, useReducer, useRef } from "react" +import React, { JSX, useCallback, useEffect, useLayoutEffect, useReducer, useRef } from "react" import { ModalCloseContext, ModalContext, ModalState } from "./contexts.ts" const ModalWrapper = ({ children }: { children: React.ReactNode }) => { @@ -40,7 +40,7 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => { evt.stopPropagation() } const openModal = useCallback((newState: ModalState) => { - if (!history.state?.modal) { + if (!history.state?.modal && newState.captureInput !== false) { history.pushState({ ...(history.state ?? {}), modal: true }, "") } setState(newState) @@ -50,6 +50,9 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => { if (wrapperRef.current && (!document.activeElement || !wrapperRef.current.contains(document.activeElement))) { wrapperRef.current.focus() } + }, [state]) + useEffect(() => { + window.closeModal = onClickWrapper const listener = (evt: PopStateEvent) => { if (!evt.state?.modal) { setState(null) @@ -57,7 +60,7 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => { } window.addEventListener("popstate", listener) return () => window.removeEventListener("popstate", listener) - }, [state]) + }, []) let modal: JSX.Element | null = null if (state) { let content = {state.content} @@ -68,15 +71,19 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => {
} - modal =
- {content} -
+ if (state.captureInput !== false) { + modal =
+ {content} +
+ } else { + modal = content + } } return {children} diff --git a/web/src/ui/modal/contexts.ts b/web/src/ui/modal/contexts.ts index e7fa7c7..dad5963 100644 --- a/web/src/ui/modal/contexts.ts +++ b/web/src/ui/modal/contexts.ts @@ -32,6 +32,7 @@ export interface ModalState { boxClass?: string innerBoxClass?: string onClose?: () => void + captureInput?: boolean } type openModal = (state: ModalState) => void diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 760428e..38b15d7 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -107,9 +107,21 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven return } mouseEvt.preventDefault() - openModal({ - content: , - }) + if (window.hackyOpenEventContextMenu === evt.event_id) { + window.closeModal() + window.hackyOpenEventContextMenu = undefined + } else { + openModal({ + content: , + 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 memberEvtContent = memberEvt?.content as MemberEventContent | undefined diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index 2750816..535273b 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -28,6 +28,7 @@ div.event-fixed-menu { box-sizing: border-box; justify-content: right; flex-direction: row-reverse; + overflow-x: auto; > button { width: 3rem; diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts index ceffa1a..9cef2d8 100644 --- a/web/src/vite-env.d.ts +++ b/web/src/vite-env.d.ts @@ -14,5 +14,7 @@ declare global { mainScreenContext: MainScreenContextFields openLightbox: (params: { src: string, alt: string }) => void gcSettings: GCSettings + hackyOpenEventContextMenu?: string + closeModal: () => void } } From 622bc5d8042bcd844c27eef8c280b8c74fb9e249 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 29 Dec 2024 17:51:40 +0200 Subject: [PATCH 38/85] web/timeline: fix scrolling mobile context menu --- web/src/ui/timeline/menu/index.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index 535273b..44bb8c0 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -29,10 +29,12 @@ div.event-fixed-menu { justify-content: right; flex-direction: row-reverse; overflow-x: auto; + overflow-y: hidden; > button { width: 3rem; height: 3rem; + flex-shrink: 0; &.redact-button { color: var(--error-color); From 326b06c702f6b003afe6ebf18485863c23c8c0a3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Dec 2024 18:36:41 +0200 Subject: [PATCH 39/85] hicli/database: store spaces edges --- pkg/hicli/database/database.go | 46 ++-- pkg/hicli/database/room.go | 3 +- pkg/hicli/database/space.go | 250 ++++++++++++++++++ .../database/upgrades/00-latest-revision.sql | 27 +- pkg/hicli/database/upgrades/10-spaces.sql | 113 ++++++++ pkg/hicli/events.go | 1 + pkg/hicli/init.go | 10 + pkg/hicli/paginate.go | 9 +- pkg/hicli/sync.go | 100 ++++++- pkg/hicli/syncwrap.go | 1 + 10 files changed, 533 insertions(+), 27 deletions(-) create mode 100644 pkg/hicli/database/space.go create mode 100644 pkg/hicli/database/upgrades/10-spaces.sql diff --git a/pkg/hicli/database/database.go b/pkg/hicli/database/database.go index 7299f21..ed1a1b4 100644 --- a/pkg/hicli/database/database.go +++ b/pkg/hicli/database/database.go @@ -17,16 +17,17 @@ import ( type Database struct { *dbutil.Database - Account AccountQuery - AccountData AccountDataQuery - Room RoomQuery - InvitedRoom InvitedRoomQuery - Event EventQuery - CurrentState CurrentStateQuery - Timeline TimelineQuery - SessionRequest SessionRequestQuery - Receipt ReceiptQuery - Media MediaQuery + Account *AccountQuery + AccountData *AccountDataQuery + Room *RoomQuery + InvitedRoom *InvitedRoomQuery + Event *EventQuery + CurrentState *CurrentStateQuery + Timeline *TimelineQuery + SessionRequest *SessionRequestQuery + Receipt *ReceiptQuery + Media *MediaQuery + SpaceEdge *SpaceEdgeQuery } func New(rawDB *dbutil.Database) *Database { @@ -35,16 +36,17 @@ func New(rawDB *dbutil.Database) *Database { return &Database{ Database: rawDB, - Account: AccountQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccount)}, - AccountData: AccountDataQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccountData)}, - Room: RoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newRoom)}, - InvitedRoom: InvitedRoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newInvitedRoom)}, - Event: EventQuery{QueryHelper: eventQH}, - CurrentState: CurrentStateQuery{QueryHelper: eventQH}, - Timeline: TimelineQuery{QueryHelper: eventQH}, - SessionRequest: SessionRequestQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newSessionRequest)}, - Receipt: ReceiptQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newReceipt)}, - Media: MediaQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newMedia)}, + Account: &AccountQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccount)}, + AccountData: &AccountDataQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccountData)}, + Room: &RoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newRoom)}, + InvitedRoom: &InvitedRoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newInvitedRoom)}, + Event: &EventQuery{QueryHelper: eventQH}, + CurrentState: &CurrentStateQuery{QueryHelper: eventQH}, + Timeline: &TimelineQuery{QueryHelper: eventQH}, + SessionRequest: &SessionRequestQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newSessionRequest)}, + Receipt: &ReceiptQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newReceipt)}, + Media: &MediaQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newMedia)}, + SpaceEdge: &SpaceEdgeQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newSpaceEdge)}, } } @@ -79,3 +81,7 @@ func newAccountData(_ *dbutil.QueryHelper[*AccountData]) *AccountData { func newAccount(_ *dbutil.QueryHelper[*Account]) *Account { return &Account{} } + +func newSpaceEdge(_ *dbutil.QueryHelper[*SpaceEdge]) *SpaceEdge { + return &SpaceEdge{} +} diff --git a/pkg/hicli/database/room.go b/pkg/hicli/database/room.go index f26ef15..234f00f 100644 --- a/pkg/hicli/database/room.go +++ b/pkg/hicli/database/room.go @@ -34,7 +34,8 @@ const ( ` upsertRoomFromSyncQuery = ` UPDATE room - SET creation_content = COALESCE(room.creation_content, $2), + SET room_type = COALESCE(room.room_type, json($2)->>'$.type'), + creation_content = COALESCE(room.creation_content, $2), tombstone_content = COALESCE(room.tombstone_content, $3), name = COALESCE($4, room.name), name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END, diff --git a/pkg/hicli/database/space.go b/pkg/hicli/database/space.go new file mode 100644 index 0000000..8ff6710 --- /dev/null +++ b/pkg/hicli/database/space.go @@ -0,0 +1,250 @@ +// Copyright (c) 2024 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package database + +import ( + "context" + "database/sql" + + "go.mau.fi/util/dbutil" + "maunium.net/go/mautrix/id" +) + +const ( + getAllSpaceChildren = ` + SELECT space_id, child_id, depth, child_event_rowid, "order", suggested, parent_event_rowid, canonical, parent_validated + FROM space_edge + WHERE (space_id = $1 OR $1 = '') AND depth IS NOT NULL AND (child_event_rowid IS NOT NULL OR parent_validated) + ORDER BY depth, space_id, "order", child_id + ` + // language=sqlite - for some reason GoLand doesn't auto-detect SQL when using WITH RECURSIVE + recalculateAllSpaceChildDepths = ` + UPDATE space_edge SET depth = NULL; + WITH RECURSIVE + top_level_spaces AS ( + SELECT space_id + FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge + INNER JOIN room ON outeredge.space_id = room.room_id AND room.room_type = 'm.space' + WHERE NOT EXISTS( + SELECT 1 + FROM space_edge inneredge + INNER JOIN room ON inneredge.space_id = room.room_id + WHERE inneredge.child_id=outeredge.space_id + AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated) + ) + ), + children AS ( + SELECT space_id, child_id, 1 AS depth, space_id AS path + FROM space_edge + WHERE space_id IN top_level_spaces AND (child_event_rowid IS NOT NULL OR parent_validated) + UNION + SELECT se.space_id, se.child_id, c.depth+1, c.path || se.space_id + FROM space_edge se + INNER JOIN children c ON se.space_id = c.child_id + WHERE instr(c.path, se.space_id) = 0 + AND c.depth < 10 + AND (child_event_rowid IS NOT NULL OR parent_validated) + ) + UPDATE space_edge + SET depth = c.depth + FROM children c + WHERE space_edge.space_id = c.space_id AND space_edge.child_id = c.child_id; + ` + revalidateAllParents = ` + UPDATE space_edge + SET parent_validated=(SELECT EXISTS( + SELECT 1 + FROM room + INNER JOIN current_state cs ON cs.room_id = room.room_id AND cs.event_type = 'm.room.power_levels' AND cs.state_key = '' + INNER JOIN event pls ON cs.event_rowid = pls.rowid + INNER JOIN event edgeevt ON space_edge.parent_event_rowid = edgeevt.rowid + WHERE room.room_id = space_edge.space_id + AND room.room_type = 'm.space' + AND COALESCE( + ( + SELECT value + FROM json_each(pls.content, 'users') + WHERE key=edgeevt.sender AND type='integer' + ), + pls.content->>'$.users_default', + 0 + ) >= COALESCE( + pls.content->>'$.events."m.space.child"', + pls.content->>'$.state_default', + 50 + ) + )) + WHERE parent_event_rowid IS NOT NULL + ` + revalidateAllParentsPointingAtSpaceQuery = revalidateAllParents + ` AND space_id=$1` + revalidateAllParentsOfRoomQuery = revalidateAllParents + ` AND child_id=$1` + revalidateSpecificParentQuery = revalidateAllParents + ` AND space_id=$1 AND child_id=$2` + clearSpaceChildrenQuery = ` + UPDATE space_edge SET child_event_rowid=NULL, "order"=NULL, suggested=false + WHERE space_id=$1 + ` + clearSpaceParentsQuery = ` + UPDATE space_edge SET parent_event_rowid=NULL, canonical=false, parent_validated=false + WHERE child_id=$1 + ` + deleteEmptySpaceEdgeRowsQuery = ` + DELETE FROM space_edge WHERE child_event_rowid IS NULL AND parent_event_rowid IS NULL + ` + addSpaceChildQuery = ` + INSERT INTO space_edge (space_id, child_id, child_event_rowid, "order", suggested) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (space_id, child_id) DO UPDATE + SET child_event_rowid=EXCLUDED.child_event_rowid, + "order"=EXCLUDED."order", + suggested=EXCLUDED.suggested + ` + addSpaceParentQuery = ` + INSERT INTO space_edge (space_id, child_id, parent_event_rowid, canonical) + VALUES ($1, $2, $3, $4) + ON CONFLICT (space_id, child_id) DO UPDATE + SET parent_event_rowid=EXCLUDED.parent_event_rowid, + canonical=EXCLUDED.canonical, + parent_validated=false + ` +) + +var massInsertSpaceParentBuilder = dbutil.NewMassInsertBuilder[SpaceParentEntry, [1]any](addSpaceParentQuery, "($%d, $1, $%d, $%d)") +var massInsertSpaceChildBuilder = dbutil.NewMassInsertBuilder[SpaceChildEntry, [1]any](addSpaceChildQuery, "($1, $%d, $%d, $%d, $%d)") + +type SpaceEdgeQuery struct { + *dbutil.QueryHelper[*SpaceEdge] +} + +func (seq *SpaceEdgeQuery) AddChild(ctx context.Context, spaceID, childID id.RoomID, childEventRowID EventRowID, order string, suggested bool) error { + return seq.Exec(ctx, addSpaceChildQuery, spaceID, childID, childEventRowID, order, suggested) +} + +func (seq *SpaceEdgeQuery) AddParent(ctx context.Context, spaceID, childID id.RoomID, parentEventRowID EventRowID, canonical bool) error { + return seq.Exec(ctx, addSpaceParentQuery, spaceID, childID, parentEventRowID, canonical) +} + +type SpaceParentEntry struct { + ParentID id.RoomID + EventRowID EventRowID + Canonical bool +} + +func (spe SpaceParentEntry) GetMassInsertValues() [3]any { + return [...]any{spe.ParentID, spe.EventRowID, spe.Canonical} +} + +type SpaceChildEntry struct { + ChildID id.RoomID + EventRowID EventRowID + Order string + Suggested bool +} + +func (sce SpaceChildEntry) GetMassInsertValues() [4]any { + return [...]any{sce.ChildID, sce.EventRowID, sce.Order, sce.Suggested} +} + +func (seq *SpaceEdgeQuery) SetChildren(ctx context.Context, spaceID id.RoomID, children []SpaceChildEntry, removedChildren []id.RoomID, clear bool) error { + if clear { + err := seq.Exec(ctx, clearSpaceChildrenQuery, spaceID) + if err != nil { + return err + } + } else { + + } + if len(removedChildren) > 0 { + err := seq.Exec(ctx, deleteEmptySpaceEdgeRowsQuery, spaceID) + if err != nil { + return err + } + } + if len(children) == 0 { + return nil + } + query, params := massInsertSpaceChildBuilder.Build([1]any{spaceID}, children) + return seq.Exec(ctx, query, params) +} + +func (seq *SpaceEdgeQuery) SetParents(ctx context.Context, childID id.RoomID, parents []SpaceParentEntry, removedParents []id.RoomID, clear bool) error { + if clear { + err := seq.Exec(ctx, clearSpaceParentsQuery, childID) + if err != nil { + return err + } + } + if len(removedParents) > 0 { + err := seq.Exec(ctx, deleteEmptySpaceEdgeRowsQuery) + if err != nil { + return err + } + } + if len(parents) == 0 { + return nil + } + query, params := massInsertSpaceParentBuilder.Build([1]any{childID}, parents) + return seq.Exec(ctx, query, params) +} + +func (seq *SpaceEdgeQuery) RevalidateAllChildrenOfParentValidity(ctx context.Context, spaceID id.RoomID) error { + return seq.Exec(ctx, revalidateAllParentsPointingAtSpaceQuery, spaceID) +} + +func (seq *SpaceEdgeQuery) RevalidateAllParentsOfRoomValidity(ctx context.Context, childID id.RoomID) error { + return seq.Exec(ctx, revalidateAllParentsOfRoomQuery, childID) +} + +func (seq *SpaceEdgeQuery) RevalidateSpecificParentValidity(ctx context.Context, spaceID, childID id.RoomID) error { + return seq.Exec(ctx, revalidateSpecificParentQuery, spaceID, childID) +} + +func (seq *SpaceEdgeQuery) RecalculateAllChildDepths(ctx context.Context) error { + return seq.Exec(ctx, recalculateAllSpaceChildDepths) +} + +func (seq *SpaceEdgeQuery) GetAll(ctx context.Context, spaceID id.RoomID) (map[id.RoomID][]*SpaceEdge, error) { + edges := make(map[id.RoomID][]*SpaceEdge) + err := seq.QueryManyIter(ctx, getAllSpaceChildren, spaceID).Iter(func(edge *SpaceEdge) (bool, error) { + edges[edge.SpaceID] = append(edges[edge.SpaceID], edge) + edge.SpaceID = "" + if !edge.ParentValidated { + edge.ParentEventRowID = 0 + edge.Canonical = false + } + return true, nil + }) + return edges, err +} + +type SpaceEdge struct { + SpaceID id.RoomID `json:"space_id,omitempty"` + ChildID id.RoomID `json:"child_id"` + Depth int `json:"-"` + + ChildEventRowID EventRowID `json:"child_event_rowid,omitempty"` + Order string `json:"order,omitempty"` + Suggested bool `json:"suggested,omitempty"` + + ParentEventRowID EventRowID `json:"parent_event_rowid,omitempty"` + Canonical bool `json:"canonical,omitempty"` + ParentValidated bool `json:"-"` +} + +func (se *SpaceEdge) Scan(row dbutil.Scannable) (*SpaceEdge, error) { + var childRowID, parentRowID sql.NullInt64 + err := row.Scan( + &se.SpaceID, &se.ChildID, &se.Depth, + &childRowID, &se.Order, &se.Suggested, + &parentRowID, &se.Canonical, &se.ParentValidated, + ) + if err != nil { + return nil, err + } + se.ChildEventRowID = EventRowID(childRowID.Int64) + se.ParentEventRowID = EventRowID(parentRowID.Int64) + return se, nil +} diff --git a/pkg/hicli/database/upgrades/00-latest-revision.sql b/pkg/hicli/database/upgrades/00-latest-revision.sql index af2d8b9..36ca532 100644 --- a/pkg/hicli/database/upgrades/00-latest-revision.sql +++ b/pkg/hicli/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v9 (compatible with v5+): Latest revision +-- v0 -> v10 (compatible with v10+): Latest revision CREATE TABLE account ( user_id TEXT NOT NULL PRIMARY KEY, device_id TEXT NOT NULL, @@ -10,6 +10,7 @@ CREATE TABLE account ( CREATE TABLE room ( room_id TEXT NOT NULL PRIMARY KEY, + room_type TEXT, creation_content TEXT, tombstone_content TEXT, @@ -35,7 +36,7 @@ CREATE TABLE room ( CONSTRAINT room_preview_event_fkey FOREIGN KEY (preview_event_rowid) REFERENCES event (rowid) ON DELETE SET NULL ) STRICT; -CREATE INDEX room_type_idx ON room (creation_content ->> 'type'); +CREATE INDEX room_type_idx ON room (room_type); CREATE INDEX room_sorting_timestamp_idx ON room (sorting_timestamp DESC); CREATE INDEX room_preview_idx ON room (preview_event_rowid); -- CREATE INDEX room_sorting_timestamp_idx ON room (unread_notifications > 0); @@ -278,3 +279,25 @@ CREATE TABLE receipt ( CONSTRAINT receipt_room_fkey FOREIGN KEY (room_id) REFERENCES room (room_id) ON DELETE CASCADE -- note: there's no foreign key on event ID because receipts could point at events that are too far in history. ) STRICT; + +CREATE TABLE space_edge ( + space_id TEXT NOT NULL, + child_id TEXT NOT NULL, + depth INTEGER, + + -- m.space.child fields + child_event_rowid INTEGER, + "order" TEXT NOT NULL DEFAULT '', + suggested INTEGER NOT NULL DEFAULT false CHECK ( suggested IN (false, true) ), + -- m.space.parent fields + parent_event_rowid INTEGER, + canonical INTEGER NOT NULL DEFAULT false CHECK ( canonical IN (false, true) ), + parent_validated INTEGER NOT NULL DEFAULT false CHECK ( parent_validated IN (false, true) ), + + PRIMARY KEY (space_id, child_id), + CONSTRAINT space_edge_child_event_fkey FOREIGN KEY (child_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE, + CONSTRAINT space_edge_parent_event_fkey FOREIGN KEY (parent_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE, + CONSTRAINT space_edge_child_event_unique UNIQUE (child_event_rowid), + CONSTRAINT space_edge_parent_event_unique UNIQUE (parent_event_rowid) +) STRICT; +CREATE INDEX space_edge_child_idx ON space_edge (child_id); diff --git a/pkg/hicli/database/upgrades/10-spaces.sql b/pkg/hicli/database/upgrades/10-spaces.sql new file mode 100644 index 0000000..a2ef780 --- /dev/null +++ b/pkg/hicli/database/upgrades/10-spaces.sql @@ -0,0 +1,113 @@ +-- v10 (compatible with v10+): Add support for spaces +ALTER TABLE room ADD COLUMN room_type TEXT; +UPDATE room SET room_type=COALESCE(creation_content->>'$.type', ''); +DROP INDEX room_type_idx; +CREATE INDEX room_type_idx ON room (room_type); + +CREATE TABLE space_edge ( + space_id TEXT NOT NULL, + child_id TEXT NOT NULL, + depth INTEGER, + + -- m.space.child fields + child_event_rowid INTEGER, + "order" TEXT NOT NULL DEFAULT '', + suggested INTEGER NOT NULL DEFAULT false CHECK ( suggested IN (false, true) ), + -- m.space.parent fields + parent_event_rowid INTEGER, + canonical INTEGER NOT NULL DEFAULT false CHECK ( canonical IN (false, true) ), + parent_validated INTEGER NOT NULL DEFAULT false CHECK ( parent_validated IN (false, true) ), + + PRIMARY KEY (space_id, child_id), + CONSTRAINT space_edge_child_event_fkey FOREIGN KEY (child_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE, + CONSTRAINT space_edge_parent_event_fkey FOREIGN KEY (parent_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE, + CONSTRAINT space_edge_child_event_unique UNIQUE (child_event_rowid), + CONSTRAINT space_edge_parent_event_unique UNIQUE (parent_event_rowid) +) STRICT; +CREATE INDEX space_edge_child_idx ON space_edge (child_id); + +INSERT INTO space_edge (space_id, child_id, child_event_rowid, "order", suggested) +SELECT + event.room_id, + event.state_key, + event.rowid, + CASE WHEN typeof(content->>'$.order')='TEXT' THEN content->>'$.order' ELSE '' END, + CASE WHEN json_type(content, '$.suggested') IN ('true', 'false') THEN content->>'$.suggested' ELSE false END +FROM current_state + INNER JOIN event ON current_state.event_rowid = event.rowid + LEFT JOIN room ON current_state.room_id = room.room_id +WHERE type = 'm.space.child' + AND json_array_length(event.content, '$.via') > 0 + AND event.state_key LIKE '!%' + AND (room.room_id IS NULL OR room.room_type = 'm.space'); + +INSERT INTO space_edge (space_id, child_id, parent_event_rowid, canonical) +SELECT + event.state_key, + event.room_id, + event.rowid, + CASE WHEN json_type(content, '$.canonical') IN ('true', 'false') THEN content->>'$.canonical' ELSE false END +FROM current_state + INNER JOIN event ON current_state.event_rowid = event.rowid + LEFT JOIN room ON event.state_key = room.room_id +WHERE type = 'm.space.parent' + AND json_array_length(event.content, '$.via') > 0 + AND event.state_key LIKE '!%' + AND (room.room_id IS NULL OR room.room_type = 'm.space') +ON CONFLICT (space_id, child_id) DO UPDATE + SET parent_event_rowid = excluded.parent_event_rowid, + canonical = excluded.canonical; + +UPDATE space_edge +SET parent_validated=(SELECT EXISTS( + SELECT 1 + FROM room + INNER JOIN current_state cs ON cs.room_id = room.room_id AND cs.event_type = 'm.room.power_levels' AND cs.state_key = '' + INNER JOIN event pls ON cs.event_rowid = pls.rowid + INNER JOIN event edgeevt ON space_edge.parent_event_rowid = edgeevt.rowid + WHERE room.room_id = space_edge.space_id + AND room.room_type = 'm.space' + AND COALESCE( + ( + SELECT value + FROM json_each(pls.content, '$.users') + WHERE key=edgeevt.sender AND type='integer' + ), + pls.content->>'$.users_default', + 0 + ) >= COALESCE( + pls.content->>'$.events."m.space.child"', + pls.content->>'$.state_default', + 50 + ) +)) +WHERE parent_event_rowid IS NOT NULL; + +WITH RECURSIVE + top_level_spaces AS ( + SELECT space_id + FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge + WHERE NOT EXISTS( + SELECT 1 + FROM space_edge inneredge + INNER JOIN room ON inneredge.space_id = room.room_id + WHERE inneredge.child_id=outeredge.space_id + AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated) + ) + ), + children AS ( + SELECT space_id, child_id, 1 AS depth, space_id AS path + FROM space_edge + WHERE space_id IN top_level_spaces AND (child_event_rowid IS NOT NULL OR parent_validated) + UNION + SELECT se.space_id, se.child_id, c.depth+1, c.path || se.space_id + FROM space_edge se + INNER JOIN children c ON se.space_id=c.child_id + WHERE instr(c.path, se.space_id)=0 + AND c.depth < 10 + AND (child_event_rowid IS NOT NULL OR parent_validated) + ) +UPDATE space_edge +SET depth = c.depth +FROM children c +WHERE space_edge.space_id = c.space_id AND space_edge.child_id = c.child_id; diff --git a/pkg/hicli/events.go b/pkg/hicli/events.go index 2bd8534..c15c8b1 100644 --- a/pkg/hicli/events.go +++ b/pkg/hicli/events.go @@ -37,6 +37,7 @@ type SyncComplete struct { Rooms map[id.RoomID]*SyncRoom `json:"rooms"` LeftRooms []id.RoomID `json:"left_rooms"` InvitedRooms []*database.InvitedRoom `json:"invited_rooms"` + SpaceEdges map[id.RoomID][]*database.SpaceEdge `json:"space_edges"` } func (c *SyncComplete) IsEmpty() bool { diff --git a/pkg/hicli/init.go b/pkg/hicli/init.go index 5c02292..a50023d 100644 --- a/pkg/hicli/init.go +++ b/pkg/hicli/init.go @@ -91,11 +91,21 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* } return } + payload.SpaceEdges, err = h.DB.SpaceEdge.GetAll(ctx, "") + if err != nil { + if ctx.Err() == nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get space edges to send to client") + } + return + } payload.ClearState = true } if payload.InvitedRooms == nil { payload.InvitedRooms = make([]*database.InvitedRoom, 0) } + if payload.SpaceEdges == nil { + payload.SpaceEdges = make(map[id.RoomID][]*database.SpaceEdge) + } for _, room := range rooms { if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp { break diff --git a/pkg/hicli/paginate.go b/pkg/hicli/paginate.go index ecb2ec5..542ff5b 100644 --- a/pkg/hicli/paginate.go +++ b/pkg/hicli/paginate.go @@ -121,13 +121,14 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe if err != nil { return fmt.Errorf("failed to save events: %w", err) } + sdc := &spaceDataCollector{} for i := range currentStateEntries { currentStateEntries[i].EventRowID = dbEvts[i].RowID if mediaReferenceEntries[i] != nil { mediaReferenceEntries[i].EventRowID = dbEvts[i].RowID } if evts[i].Type != event.StateMember { - processImportantEvent(ctx, evts[i], room, updatedRoom) + processImportantEvent(ctx, evts[i], room, updatedRoom, dbEvts[i].RowID, sdc) } } err = h.DB.Media.AddMany(ctx, mediaCacheEntries) @@ -146,6 +147,10 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe return fmt.Errorf("failed to save current state entries: %w", err) } roomChanged := updatedRoom.CheckChangesAndCopyInto(room) + err = sdc.Apply(ctx, room, h.DB.SpaceEdge) + if err != nil { + return err + } if roomChanged { err = h.DB.Room.Upsert(ctx, updatedRoom) if err != nil { @@ -168,6 +173,8 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe InvitedRooms: make([]*database.InvitedRoom, 0), AccountData: make(map[event.Type]*database.AccountData), LeftRooms: make([]id.RoomID, 0), + // TODO dispatch space edge changes if something changed? (fairly unlikely though) + SpaceEdges: make(map[id.RoomID][]*database.SpaceEdge), }) } } diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index e46532a..a591e91 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -667,6 +667,7 @@ func (h *HiClient) processStateAndTimeline( updatedRoom.LazyLoadSummary = summary heroesChanged = true } + sdc := &spaceDataCollector{} decryptionQueue := make(map[id.SessionID]*database.SessionRequest) allNewEvents := make([]*database.Event, 0, len(state.Events)+len(timeline.Events)) addedEvents := make(map[database.EventRowID]struct{}) @@ -750,7 +751,7 @@ func (h *HiClient) processStateAndTimeline( if err != nil { return -1, fmt.Errorf("failed to save current state event ID %s for %s/%s: %w", evt.ID, evt.Type.Type, *evt.StateKey, err) } - processImportantEvent(ctx, evt, room, updatedRoom) + processImportantEvent(ctx, evt, room, updatedRoom, dbEvt.RowID, sdc) } allNewEvents = append(allNewEvents, dbEvt) addedEvents[dbEvt.RowID] = struct{}{} @@ -932,6 +933,10 @@ func (h *HiClient) processStateAndTimeline( return fmt.Errorf("failed to save room data: %w", err) } } + err = sdc.Apply(ctx, room, h.DB.SpaceEdge) + if err != nil { + return err + } // TODO why is *old* unread count sometimes zero when processing the read receipt that is making it zero? if roomChanged || len(accountData) > 0 || len(newOwnReceipts) > 0 || len(receipts) > 0 || len(timelineRowTuples) > 0 || len(allNewEvents) > 0 { for _, receipt := range receipts { @@ -1033,13 +1038,101 @@ func intPtrEqual(a, b *int) bool { return *a == *b } -func processImportantEvent(ctx context.Context, evt *event.Event, existingRoomData, updatedRoom *database.Room) (roomDataChanged bool) { +type spaceDataCollector struct { + Children []database.SpaceChildEntry + Parents []database.SpaceParentEntry + RemovedChildren []id.RoomID + RemovedParents []id.RoomID + PowerLevelChanged bool + IsFullState bool +} + +func (sdc *spaceDataCollector) Collect(evt *event.Event, rowID database.EventRowID) { + switch evt.Type { + case event.StatePowerLevels: + sdc.PowerLevelChanged = true + case event.StateCreate: + sdc.IsFullState = true + case event.StateSpaceChild: + content := evt.Content.AsSpaceChild() + if len(content.Via) == 0 { + sdc.RemovedChildren = append(sdc.RemovedChildren, id.RoomID(*evt.StateKey)) + } else { + sdc.Children = append(sdc.Children, database.SpaceChildEntry{ + ChildID: id.RoomID(*evt.StateKey), + EventRowID: rowID, + Order: content.Order, + Suggested: content.Suggested, + }) + } + case event.StateSpaceParent: + content := evt.Content.AsSpaceParent() + if len(content.Via) == 0 { + sdc.RemovedParents = append(sdc.RemovedParents, id.RoomID(*evt.StateKey)) + } else { + sdc.Parents = append(sdc.Parents, database.SpaceParentEntry{ + ParentID: id.RoomID(*evt.StateKey), + EventRowID: rowID, + Canonical: content.Canonical, + }) + } + } +} + +func (sdc *spaceDataCollector) Apply(ctx context.Context, room *database.Room, seq *database.SpaceEdgeQuery) error { + if room.CreationContent == nil || room.CreationContent.Type != event.RoomTypeSpace { + sdc.Children = nil + sdc.RemovedChildren = nil + sdc.PowerLevelChanged = false + } + if len(sdc.Children) == 0 && len(sdc.RemovedChildren) == 0 && + len(sdc.Parents) == 0 && len(sdc.RemovedParents) == 0 && + !sdc.PowerLevelChanged { + return nil + } + return seq.GetDB().DoTxn(ctx, nil, func(ctx context.Context) error { + if len(sdc.Children) > 0 || len(sdc.RemovedChildren) > 0 { + err := seq.SetChildren(ctx, room.ID, sdc.Children, sdc.RemovedChildren, sdc.IsFullState) + if err != nil { + return fmt.Errorf("failed to set space children: %w", err) + } + } + if len(sdc.Parents) > 0 || len(sdc.RemovedParents) > 0 { + err := seq.SetParents(ctx, room.ID, sdc.Parents, sdc.RemovedParents, sdc.IsFullState) + if err != nil { + return fmt.Errorf("failed to set space parents: %w", err) + } + if len(sdc.Parents) > 0 { + err = seq.RevalidateAllParentsOfRoomValidity(ctx, room.ID) + if err != nil { + return fmt.Errorf("failed to revalidate own parent references: %w", err) + } + } + } + if sdc.PowerLevelChanged { + err := seq.RevalidateAllChildrenOfParentValidity(ctx, room.ID) + if err != nil { + return fmt.Errorf("failed to revalidate child parent references to self: %w", err) + } + } + return nil + }) +} + +func processImportantEvent( + ctx context.Context, + evt *event.Event, + existingRoomData, updatedRoom *database.Room, + rowID database.EventRowID, + sdc *spaceDataCollector, +) (roomDataChanged bool) { if evt.StateKey == nil { return } switch evt.Type { case event.StateCreate, event.StateTombstone, event.StateRoomName, event.StateCanonicalAlias, - event.StateRoomAvatar, event.StateTopic, event.StateEncryption: + event.StateRoomAvatar, event.StateTopic, event.StateEncryption, + event.StateSpaceChild, event.StateSpaceParent, event.StatePowerLevels: if *evt.StateKey != "" { return } @@ -1047,6 +1140,7 @@ func processImportantEvent(ctx context.Context, evt *event.Event, existingRoomDa return } err := evt.Content.ParseRaw(evt.Type) + sdc.Collect(evt, rowID) if err != nil && !errors.Is(err, event.ErrContentAlreadyParsed) { zerolog.Ctx(ctx).Warn().Err(err). Stringer("event_type", &evt.Type). diff --git a/pkg/hicli/syncwrap.go b/pkg/hicli/syncwrap.go index 8492da2..5b34f9b 100644 --- a/pkg/hicli/syncwrap.go +++ b/pkg/hicli/syncwrap.go @@ -38,6 +38,7 @@ func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync, Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)), InvitedRooms: make([]*database.InvitedRoom, 0, len(resp.Rooms.Invite)), LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)), + SpaceEdges: make(map[id.RoomID][]*database.SpaceEdge), }}) err := c.preProcessSyncResponse(ctx, resp, since) if err != nil { From 2b206bb32f15e6c16d3c19c68ff4977a847dd390 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Dec 2024 18:51:39 +0200 Subject: [PATCH 40/85] hicli/database: don't store space depth --- pkg/hicli/database/space.go | 57 ++++++------------- .../database/upgrades/00-latest-revision.sql | 1 - pkg/hicli/database/upgrades/10-spaces.sql | 30 ---------- 3 files changed, 16 insertions(+), 72 deletions(-) diff --git a/pkg/hicli/database/space.go b/pkg/hicli/database/space.go index 8ff6710..1139c69 100644 --- a/pkg/hicli/database/space.go +++ b/pkg/hicli/database/space.go @@ -16,43 +16,23 @@ import ( const ( getAllSpaceChildren = ` - SELECT space_id, child_id, depth, child_event_rowid, "order", suggested, parent_event_rowid, canonical, parent_validated + SELECT space_id, child_id, child_event_rowid, "order", suggested, parent_event_rowid, canonical, parent_validated FROM space_edge - WHERE (space_id = $1 OR $1 = '') AND depth IS NOT NULL AND (child_event_rowid IS NOT NULL OR parent_validated) - ORDER BY depth, space_id, "order", child_id + -- This check should be redundant thanks to parent_validated and validation before insert for children + --INNER JOIN room ON space_id = room.room_id AND room.room_type = 'm.space' + WHERE (space_id = $1 OR $1 = '') AND (child_event_rowid IS NOT NULL OR parent_validated) + ORDER BY space_id, "order", child_id ` - // language=sqlite - for some reason GoLand doesn't auto-detect SQL when using WITH RECURSIVE - recalculateAllSpaceChildDepths = ` - UPDATE space_edge SET depth = NULL; - WITH RECURSIVE - top_level_spaces AS ( - SELECT space_id - FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge - INNER JOIN room ON outeredge.space_id = room.room_id AND room.room_type = 'm.space' - WHERE NOT EXISTS( - SELECT 1 - FROM space_edge inneredge - INNER JOIN room ON inneredge.space_id = room.room_id - WHERE inneredge.child_id=outeredge.space_id - AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated) - ) - ), - children AS ( - SELECT space_id, child_id, 1 AS depth, space_id AS path - FROM space_edge - WHERE space_id IN top_level_spaces AND (child_event_rowid IS NOT NULL OR parent_validated) - UNION - SELECT se.space_id, se.child_id, c.depth+1, c.path || se.space_id - FROM space_edge se - INNER JOIN children c ON se.space_id = c.child_id - WHERE instr(c.path, se.space_id) = 0 - AND c.depth < 10 - AND (child_event_rowid IS NOT NULL OR parent_validated) - ) - UPDATE space_edge - SET depth = c.depth - FROM children c - WHERE space_edge.space_id = c.space_id AND space_edge.child_id = c.child_id; + getTopLevelSpaces = ` + SELECT space_id + FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge + WHERE NOT EXISTS( + SELECT 1 + FROM space_edge inneredge + INNER JOIN room ON inneredge.space_id = room.room_id + WHERE inneredge.child_id = outeredge.space_id + AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated) + ) ` revalidateAllParents = ` UPDATE space_edge @@ -202,10 +182,6 @@ func (seq *SpaceEdgeQuery) RevalidateSpecificParentValidity(ctx context.Context, return seq.Exec(ctx, revalidateSpecificParentQuery, spaceID, childID) } -func (seq *SpaceEdgeQuery) RecalculateAllChildDepths(ctx context.Context) error { - return seq.Exec(ctx, recalculateAllSpaceChildDepths) -} - func (seq *SpaceEdgeQuery) GetAll(ctx context.Context, spaceID id.RoomID) (map[id.RoomID][]*SpaceEdge, error) { edges := make(map[id.RoomID][]*SpaceEdge) err := seq.QueryManyIter(ctx, getAllSpaceChildren, spaceID).Iter(func(edge *SpaceEdge) (bool, error) { @@ -223,7 +199,6 @@ func (seq *SpaceEdgeQuery) GetAll(ctx context.Context, spaceID id.RoomID) (map[i type SpaceEdge struct { SpaceID id.RoomID `json:"space_id,omitempty"` ChildID id.RoomID `json:"child_id"` - Depth int `json:"-"` ChildEventRowID EventRowID `json:"child_event_rowid,omitempty"` Order string `json:"order,omitempty"` @@ -237,7 +212,7 @@ type SpaceEdge struct { func (se *SpaceEdge) Scan(row dbutil.Scannable) (*SpaceEdge, error) { var childRowID, parentRowID sql.NullInt64 err := row.Scan( - &se.SpaceID, &se.ChildID, &se.Depth, + &se.SpaceID, &se.ChildID, &childRowID, &se.Order, &se.Suggested, &parentRowID, &se.Canonical, &se.ParentValidated, ) diff --git a/pkg/hicli/database/upgrades/00-latest-revision.sql b/pkg/hicli/database/upgrades/00-latest-revision.sql index 36ca532..2df49d2 100644 --- a/pkg/hicli/database/upgrades/00-latest-revision.sql +++ b/pkg/hicli/database/upgrades/00-latest-revision.sql @@ -283,7 +283,6 @@ CREATE TABLE receipt ( CREATE TABLE space_edge ( space_id TEXT NOT NULL, child_id TEXT NOT NULL, - depth INTEGER, -- m.space.child fields child_event_rowid INTEGER, diff --git a/pkg/hicli/database/upgrades/10-spaces.sql b/pkg/hicli/database/upgrades/10-spaces.sql index a2ef780..429973c 100644 --- a/pkg/hicli/database/upgrades/10-spaces.sql +++ b/pkg/hicli/database/upgrades/10-spaces.sql @@ -7,7 +7,6 @@ CREATE INDEX room_type_idx ON room (room_type); CREATE TABLE space_edge ( space_id TEXT NOT NULL, child_id TEXT NOT NULL, - depth INTEGER, -- m.space.child fields child_event_rowid INTEGER, @@ -82,32 +81,3 @@ SET parent_validated=(SELECT EXISTS( ) )) WHERE parent_event_rowid IS NOT NULL; - -WITH RECURSIVE - top_level_spaces AS ( - SELECT space_id - FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge - WHERE NOT EXISTS( - SELECT 1 - FROM space_edge inneredge - INNER JOIN room ON inneredge.space_id = room.room_id - WHERE inneredge.child_id=outeredge.space_id - AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated) - ) - ), - children AS ( - SELECT space_id, child_id, 1 AS depth, space_id AS path - FROM space_edge - WHERE space_id IN top_level_spaces AND (child_event_rowid IS NOT NULL OR parent_validated) - UNION - SELECT se.space_id, se.child_id, c.depth+1, c.path || se.space_id - FROM space_edge se - INNER JOIN children c ON se.space_id=c.child_id - WHERE instr(c.path, se.space_id)=0 - AND c.depth < 10 - AND (child_event_rowid IS NOT NULL OR parent_validated) - ) -UPDATE space_edge -SET depth = c.depth -FROM children c -WHERE space_edge.space_id = c.space_id AND space_edge.child_id = c.child_id; From 2ea80dac6f823e49f93cab6d3a7b0f0b7b12d741 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Dec 2024 18:58:49 +0200 Subject: [PATCH 41/85] web/statestore: allow sync event fields to be null --- pkg/hicli/init.go | 26 ++++++-------------------- pkg/hicli/paginate.go | 15 ++------------- pkg/hicli/syncwrap.go | 1 - web/src/api/statestore/main.ts | 14 +++++++------- web/src/api/statestore/room.ts | 12 ++++++------ web/src/api/types/hievents.ts | 22 ++++++++++++---------- web/src/api/types/hitypes.ts | 12 ++++++++++++ 7 files changed, 45 insertions(+), 57 deletions(-) diff --git a/pkg/hicli/init.go b/pkg/hicli/init.go index a50023d..3c2b40f 100644 --- a/pkg/hicli/init.go +++ b/pkg/hicli/init.go @@ -14,12 +14,9 @@ import ( func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room) *SyncRoom { syncRoom := &SyncRoom{ - Meta: room, - Events: make([]*database.Event, 0, 2), - Timeline: make([]database.TimelineRowTuple, 0), - State: map[event.Type]map[string]database.EventRowID{}, - Notifications: make([]SyncNotification, 0), - Receipts: make(map[id.EventID][]*database.Receipt), + Meta: room, + Events: make([]*database.Event, 0, 2), + State: map[event.Type]map[string]database.EventRowID{}, } ad, err := h.DB.AccountData.GetAllRoom(ctx, h.Account.UserID, room.ID) if err != nil { @@ -27,7 +24,6 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room) if ctx.Err() != nil { return nil } - syncRoom.AccountData = make(map[event.Type]*database.AccountData) } else { syncRoom.AccountData = make(map[event.Type]*database.AccountData, len(ad)) for _, data := range ad { @@ -79,9 +75,7 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* return } payload := SyncComplete{ - Rooms: make(map[id.RoomID]*SyncRoom, len(rooms)-1), - LeftRooms: make([]id.RoomID, 0), - AccountData: make(map[event.Type]*database.AccountData), + Rooms: make(map[id.RoomID]*SyncRoom, len(rooms)), } if i == 0 { payload.InvitedRooms, err = h.DB.InvitedRoom.GetAll(ctx) @@ -91,6 +85,7 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* } return } + // TODO include space rooms in first batch too? payload.SpaceEdges, err = h.DB.SpaceEdge.GetAll(ctx, "") if err != nil { if ctx.Err() == nil { @@ -100,12 +95,6 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* } payload.ClearState = true } - if payload.InvitedRooms == nil { - payload.InvitedRooms = make([]*database.InvitedRoom, 0) - } - if payload.SpaceEdges == nil { - payload.SpaceEdges = make(map[id.RoomID][]*database.SpaceEdge) - } for _, room := range rooms { if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp { break @@ -127,10 +116,7 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* return } payload := SyncComplete{ - Rooms: make(map[id.RoomID]*SyncRoom), - InvitedRooms: make([]*database.InvitedRoom, 0), - LeftRooms: make([]id.RoomID, 0), - AccountData: make(map[event.Type]*database.AccountData, len(ad)), + AccountData: make(map[event.Type]*database.AccountData, len(ad)), } for _, data := range ad { payload.AccountData[event.Type{Type: data.Type, Class: event.AccountDataEventType}] = data diff --git a/pkg/hicli/paginate.go b/pkg/hicli/paginate.go index 542ff5b..f41ceee 100644 --- a/pkg/hicli/paginate.go +++ b/pkg/hicli/paginate.go @@ -147,6 +147,7 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe return fmt.Errorf("failed to save current state entries: %w", err) } roomChanged := updatedRoom.CheckChangesAndCopyInto(room) + // TODO dispatch space edge changes if something changed? (fairly unlikely though) err = sdc.Apply(ctx, room, h.DB.SpaceEdge) if err != nil { return err @@ -160,21 +161,9 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe h.EventHandler(&SyncComplete{ Rooms: map[id.RoomID]*SyncRoom{ roomID: { - Meta: room, - Timeline: make([]database.TimelineRowTuple, 0), - State: make(map[event.Type]map[string]database.EventRowID), - AccountData: make(map[event.Type]*database.AccountData), - Events: make([]*database.Event, 0), - Reset: false, - Notifications: make([]SyncNotification, 0), - Receipts: make(map[id.EventID][]*database.Receipt), + Meta: room, }, }, - InvitedRooms: make([]*database.InvitedRoom, 0), - AccountData: make(map[event.Type]*database.AccountData), - LeftRooms: make([]id.RoomID, 0), - // TODO dispatch space edge changes if something changed? (fairly unlikely though) - SpaceEdges: make(map[id.RoomID][]*database.SpaceEdge), }) } } diff --git a/pkg/hicli/syncwrap.go b/pkg/hicli/syncwrap.go index 5b34f9b..8492da2 100644 --- a/pkg/hicli/syncwrap.go +++ b/pkg/hicli/syncwrap.go @@ -38,7 +38,6 @@ func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync, Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)), InvitedRooms: make([]*database.InvitedRoom, 0, len(resp.Rooms.Invite)), LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)), - SpaceEdges: make(map[id.RoomID][]*database.SpaceEdge), }}) err := c.preProcessSyncResponse(ctx, resp, since) if err != nil { diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index f4332f8..1bfde16 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -122,7 +122,7 @@ export class StateStore { entry.meta.unread_highlights !== oldEntry.meta.current.unread_highlights || entry.meta.marked_unread !== oldEntry.meta.current.marked_unread || entry.meta.preview_event_rowid !== oldEntry.meta.current.preview_event_rowid || - entry.events.findIndex(evt => evt.rowid === entry.meta.preview_event_rowid) !== -1 + (entry.events ?? []).findIndex(evt => evt.rowid === entry.meta.preview_event_rowid) !== -1 } #makeRoomListEntry(entry: SyncRoom, room?: RoomStateStore): RoomListEntry | null { @@ -165,7 +165,7 @@ export class StateStore { } const resyncRoomList = this.roomList.current.length === 0 const changedRoomListEntries = new Map() - for (const data of sync.invited_rooms) { + for (const data of sync.invited_rooms ?? []) { const room = new InvitedRoomStore(data, this) this.inviteRooms.set(room.room_id, room) if (!resyncRoomList) { @@ -176,7 +176,7 @@ export class StateStore { } } const hasInvites = this.inviteRooms.size > 0 - for (const [roomID, data] of Object.entries(sync.rooms)) { + for (const [roomID, data] of Object.entries(sync.rooms ?? {})) { let isNewRoom = false let room = this.rooms.get(roomID) if (!room) { @@ -203,7 +203,7 @@ export class StateStore { } } - if (window.Notification?.permission === "granted" && !focused.current) { + if (window.Notification?.permission === "granted" && !focused.current && data.notifications) { for (const notification of data.notifications) { this.showNotification(room, notification.event_rowid, notification.sound) } @@ -212,7 +212,7 @@ export class StateStore { this.switchRoom?.(roomID) } } - for (const ad of Object.values(sync.account_data)) { + for (const ad of Object.values(sync.account_data ?? {})) { if (ad.type === "io.element.recent_emoji") { this.#frequentlyUsedEmoji = null } else if (ad.type === "fi.mau.gomuks.preferences") { @@ -222,7 +222,7 @@ export class StateStore { this.accountData.set(ad.type, ad.content) this.accountDataSubs.notify(ad.type) } - for (const roomID of sync.left_rooms) { + for (const roomID of sync.left_rooms ?? []) { if (this.activeRoomID === roomID) { this.switchRoom?.(null) } @@ -233,7 +233,7 @@ export class StateStore { let updatedRoomList: RoomListEntry[] | undefined if (resyncRoomList) { updatedRoomList = this.inviteRooms.values().toArray() - updatedRoomList = updatedRoomList.concat(Object.values(sync.rooms) + updatedRoomList = updatedRoomList.concat(Object.values(sync.rooms ?? {}) .map(entry => this.#makeRoomListEntry(entry)) .filter(entry => entry !== null)) updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp) diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 20b4ae3..7419a55 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -390,7 +390,7 @@ export class RoomStateStore { } else { this.meta.emit(sync.meta) } - for (const ad of Object.values(sync.account_data)) { + for (const ad of Object.values(sync.account_data ?? {})) { if (ad.type === "fi.mau.gomuks.preferences") { this.serverPreferenceCache = ad.content this.preferenceSub.notify() @@ -398,10 +398,10 @@ export class RoomStateStore { this.accountData.set(ad.type, ad.content) this.accountDataSubs.notify(ad.type) } - for (const evt of sync.events) { + for (const evt of sync.events ?? []) { this.applyEvent(evt) } - for (const [evtType, changedEvts] of Object.entries(sync.state)) { + for (const [evtType, changedEvts] of Object.entries(sync.state ?? {})) { let stateMap = this.state.get(evtType) if (!stateMap) { stateMap = new Map() @@ -414,9 +414,9 @@ export class RoomStateStore { this.stateSubs.notify(evtType) } if (sync.reset) { - this.timeline = sync.timeline + this.timeline = sync.timeline ?? [] this.pendingEvents.splice(0, this.pendingEvents.length) - } else { + } else if (sync.timeline) { this.timeline.push(...sync.timeline) } if (sync.meta.unread_notifications === 0 && sync.meta.unread_highlights === 0) { @@ -426,7 +426,7 @@ export class RoomStateStore { this.openNotifications.clear() } this.notifyTimelineSubscribers() - for (const [evtID, receipts] of Object.entries(sync.receipts)) { + for (const [evtID, receipts] of Object.entries(sync.receipts ?? {})) { this.applyReceipts(receipts, evtID, false) } } diff --git a/web/src/api/types/hievents.ts b/web/src/api/types/hievents.ts index f05296e..f7d85d3 100644 --- a/web/src/api/types/hievents.ts +++ b/web/src/api/types/hievents.ts @@ -19,6 +19,7 @@ import { DBReceipt, DBRoom, DBRoomAccountData, + DBSpaceEdge, EventRowID, RawDBEvent, TimelineRowTuple, @@ -71,13 +72,13 @@ export interface ImageAuthTokenEvent extends BaseRPCCommand { export interface SyncRoom { meta: DBRoom - timeline: TimelineRowTuple[] - events: RawDBEvent[] - state: Record> + timeline: TimelineRowTuple[] | null + events: RawDBEvent[] | null + state: Record> | null reset: boolean - notifications: SyncNotification[] - account_data: Record - receipts: Record + notifications: SyncNotification[] | null + account_data: Record | null + receipts: Record | null } export interface SyncNotification { @@ -86,10 +87,11 @@ export interface SyncNotification { } export interface SyncCompleteData { - rooms: Record - invited_rooms: DBInvitedRoom[] - left_rooms: RoomID[] - account_data: Record + rooms: Record | null + invited_rooms: DBInvitedRoom[] | null + left_rooms: RoomID[] | null + account_data: Record | null + space_edges: Record[]> | null since?: string clear_state?: boolean } diff --git a/web/src/api/types/hitypes.ts b/web/src/api/types/hitypes.ts index 7796e82..a859a47 100644 --- a/web/src/api/types/hitypes.ts +++ b/web/src/api/types/hitypes.ts @@ -71,6 +71,18 @@ export interface DBRoom { prev_batch: string } +export interface DBSpaceEdge { + space_id: RoomID + child_id: RoomID + + child_event_rowid?: EventRowID + order?: string + suggested?: true + + parent_event_rowid?: EventRowID + canonical?: true +} + //eslint-disable-next-line @typescript-eslint/no-explicit-any export type UnknownEventContent = Record From 5483b077c752f739d84424dd67dc77bb18b9d356 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Dec 2024 19:13:21 +0200 Subject: [PATCH 42/85] hicli/init: send spaces in first payload --- pkg/hicli/database/room.go | 5 +++ pkg/hicli/database/space.go | 13 ++++++- pkg/hicli/events.go | 15 ++++---- pkg/hicli/init.go | 65 +++++++++++++++++++++++++---------- web/src/api/types/hievents.ts | 1 + 5 files changed, 72 insertions(+), 27 deletions(-) diff --git a/pkg/hicli/database/room.go b/pkg/hicli/database/room.go index 234f00f..a27de72 100644 --- a/pkg/hicli/database/room.go +++ b/pkg/hicli/database/room.go @@ -27,6 +27,7 @@ const ( FROM room ` getRoomsBySortingTimestampQuery = getRoomBaseQuery + `WHERE sorting_timestamp < $1 AND sorting_timestamp > 0 ORDER BY sorting_timestamp DESC LIMIT $2` + getRoomsByTypeQuery = getRoomBaseQuery + `WHERE room_type = $1` getRoomByIDQuery = getRoomBaseQuery + `WHERE room_id = $1` ensureRoomExistsQuery = ` INSERT INTO room (room_id) VALUES ($1) @@ -96,6 +97,10 @@ func (rq *RoomQuery) GetBySortTS(ctx context.Context, maxTS time.Time, limit int return rq.QueryMany(ctx, getRoomsBySortingTimestampQuery, maxTS.UnixMilli(), limit) } +func (rq *RoomQuery) GetAllSpaces(ctx context.Context) ([]*Room, error) { + return rq.QueryMany(ctx, getRoomsByTypeQuery, event.RoomTypeSpace) +} + func (rq *RoomQuery) Upsert(ctx context.Context, room *Room) error { return rq.Exec(ctx, upsertRoomFromSyncQuery, room.sqlVariables()...) } diff --git a/pkg/hicli/database/space.go b/pkg/hicli/database/space.go index 1139c69..b0b86ef 100644 --- a/pkg/hicli/database/space.go +++ b/pkg/hicli/database/space.go @@ -26,13 +26,18 @@ const ( getTopLevelSpaces = ` SELECT space_id FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge + LEFT JOIN room_account_data ON + room_account_data.user_id = $1 + AND room_account_data.room_id = outeredge.space_id + AND room_account_data.type = 'org.matrix.msc3230.space_order' WHERE NOT EXISTS( SELECT 1 FROM space_edge inneredge INNER JOIN room ON inneredge.space_id = room.room_id WHERE inneredge.child_id = outeredge.space_id AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated) - ) + ) AND EXISTS(SELECT 1 FROM room WHERE room_id = space_id AND room_type = 'm.space') + ORDER BY room_account_data.content->>'$.order' NULLS LAST, space_id ` revalidateAllParents = ` UPDATE space_edge @@ -196,6 +201,12 @@ func (seq *SpaceEdgeQuery) GetAll(ctx context.Context, spaceID id.RoomID) (map[i return edges, err } +var roomIDScanner = dbutil.ConvertRowFn[id.RoomID](dbutil.ScanSingleColumn[id.RoomID]) + +func (seq *SpaceEdgeQuery) GetTopLevelIDs(ctx context.Context, userID id.UserID) ([]id.RoomID, error) { + return roomIDScanner.NewRowIter(seq.GetDB().Query(ctx, getTopLevelSpaces, userID)).AsList() +} + type SpaceEdge struct { SpaceID id.RoomID `json:"space_id,omitempty"` ChildID id.RoomID `json:"child_id"` diff --git a/pkg/hicli/events.go b/pkg/hicli/events.go index c15c8b1..e45a4e8 100644 --- a/pkg/hicli/events.go +++ b/pkg/hicli/events.go @@ -31,13 +31,14 @@ type SyncNotification struct { } type SyncComplete struct { - Since *string `json:"since,omitempty"` - ClearState bool `json:"clear_state,omitempty"` - AccountData map[event.Type]*database.AccountData `json:"account_data"` - Rooms map[id.RoomID]*SyncRoom `json:"rooms"` - LeftRooms []id.RoomID `json:"left_rooms"` - InvitedRooms []*database.InvitedRoom `json:"invited_rooms"` - SpaceEdges map[id.RoomID][]*database.SpaceEdge `json:"space_edges"` + Since *string `json:"since,omitempty"` + ClearState bool `json:"clear_state,omitempty"` + AccountData map[event.Type]*database.AccountData `json:"account_data"` + Rooms map[id.RoomID]*SyncRoom `json:"rooms"` + LeftRooms []id.RoomID `json:"left_rooms"` + InvitedRooms []*database.InvitedRoom `json:"invited_rooms"` + SpaceEdges map[id.RoomID][]*database.SpaceEdge `json:"space_edges"` + TopLevelSpaces []id.RoomID `json:"top_level_spaces"` } func (c *SyncComplete) IsEmpty() bool { diff --git a/pkg/hicli/init.go b/pkg/hicli/init.go index 3c2b40f..5b08de5 100644 --- a/pkg/hicli/init.go +++ b/pkg/hicli/init.go @@ -66,6 +66,49 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room) func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*SyncComplete] { return func(yield func(*SyncComplete) bool) { maxTS := time.Now().Add(1 * time.Hour) + { + spaces, err := h.DB.Room.GetAllSpaces(ctx) + if err != nil { + if ctx.Err() == nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get initial spaces to send to client") + } + return + } + payload := SyncComplete{ + Rooms: make(map[id.RoomID]*SyncRoom, len(spaces)), + } + for _, room := range spaces { + payload.Rooms[room.ID] = h.getInitialSyncRoom(ctx, room) + if ctx.Err() != nil { + return + } + } + payload.TopLevelSpaces, err = h.DB.SpaceEdge.GetTopLevelIDs(ctx, h.Account.UserID) + if err != nil { + if ctx.Err() == nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get top-level space IDs to send to client") + } + return + } + payload.SpaceEdges, err = h.DB.SpaceEdge.GetAll(ctx, "") + if err != nil { + if ctx.Err() == nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get space edges to send to client") + } + return + } + payload.InvitedRooms, err = h.DB.InvitedRoom.GetAll(ctx) + if err != nil { + if ctx.Err() == nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get invited rooms to send to client") + } + return + } + payload.ClearState = true + if !yield(&payload) { + return + } + } for i := 0; ; i++ { rooms, err := h.DB.Room.GetBySortTS(ctx, maxTS, batchSize) if err != nil { @@ -77,24 +120,6 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* payload := SyncComplete{ Rooms: make(map[id.RoomID]*SyncRoom, len(rooms)), } - if i == 0 { - payload.InvitedRooms, err = h.DB.InvitedRoom.GetAll(ctx) - if err != nil { - if ctx.Err() == nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to get invited rooms to send to client") - } - return - } - // TODO include space rooms in first batch too? - payload.SpaceEdges, err = h.DB.SpaceEdge.GetAll(ctx, "") - if err != nil { - if ctx.Err() == nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to get space edges to send to client") - } - return - } - payload.ClearState = true - } for _, room := range rooms { if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp { break @@ -105,7 +130,9 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[* return } } - if !yield(&payload) || len(rooms) < batchSize { + if !yield(&payload) { + return + } else if len(rooms) < batchSize { break } } diff --git a/web/src/api/types/hievents.ts b/web/src/api/types/hievents.ts index f7d85d3..7c64596 100644 --- a/web/src/api/types/hievents.ts +++ b/web/src/api/types/hievents.ts @@ -92,6 +92,7 @@ export interface SyncCompleteData { left_rooms: RoomID[] | null account_data: Record | null space_edges: Record[]> | null + top_level_spaces: RoomID[] | null since?: string clear_state?: boolean } From 5a8139685dc28c3d67cc37064980edb233874ea8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 28 Dec 2024 21:05:16 +0200 Subject: [PATCH 43/85] web/roomlist: add space bar Fixes #518 --- web/index.html | 11 ++++ web/src/api/statestore/main.ts | 53 +++++++++++++-- web/src/api/statestore/space.ts | 109 +++++++++++++++++++++++++++++++ web/src/api/types/hievents.ts | 2 +- web/src/api/types/hitypes.ts | 2 +- web/src/icons/home.svg | 1 + web/src/ui/MainScreen.css | 2 +- web/src/ui/MainScreen.tsx | 2 +- web/src/ui/roomlist/RoomList.css | 46 ++++++++++++- web/src/ui/roomlist/RoomList.tsx | 58 +++++++++++----- web/src/ui/roomlist/Space.tsx | 40 ++++++++++++ web/src/util/eventdispatcher.ts | 11 ++-- 12 files changed, 302 insertions(+), 35 deletions(-) create mode 100644 web/src/api/statestore/space.ts create mode 100644 web/src/icons/home.svg create mode 100644 web/src/ui/roomlist/Space.tsx diff --git a/web/index.html b/web/index.html index 40252d6..483ad69 100644 --- a/web/index.html +++ b/web/index.html @@ -12,5 +12,16 @@
+ + + + + + + diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 1bfde16..71db297 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -39,6 +39,7 @@ import { } from "../types" import { InvitedRoomStore } from "./invitedroom.ts" import { RoomStateStore } from "./room.ts" +import { RoomListFilter, SpaceEdgeStore } from "./space.ts" export interface RoomListEntry { room_id: RoomID @@ -72,7 +73,10 @@ export class StateStore { readonly rooms: Map = new Map() readonly inviteRooms: Map = new Map() readonly roomList = new NonNullCachedEventDispatcher([]) - currentRoomListFilter: string = "" + readonly topLevelSpaces = new NonNullCachedEventDispatcher([]) + readonly spaceEdges: Map = new Map() + currentRoomListQuery: string = "" + currentRoomListFilter: RoomListFilter | null = null readonly accountData: Map = new Map() readonly accountDataSubs = new MultiSubscribable() readonly emojiRoomsSub = new Subscribable() @@ -89,11 +93,25 @@ export class StateStore { activeRoomIsPreview: boolean = false imageAuthToken?: string - getFilteredRoomList(): RoomListEntry[] { - if (!this.currentRoomListFilter) { - return this.roomList.current + #roomListFilterFunc = (entry: RoomListEntry) => { + if (this.currentRoomListQuery && !entry.search_name.includes(this.currentRoomListQuery)) { + return false + } else if (this.currentRoomListFilter && !this.currentRoomListFilter.include(entry)) { + return false } - return this.roomList.current.filter(entry => entry.search_name.includes(this.currentRoomListFilter)) + return true + } + + get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null { + if (!this.currentRoomListFilter && !this.currentRoomListQuery) { + return null + } + return this.#roomListFilterFunc + } + + getFilteredRoomList(): RoomListEntry[] { + const fn = this.roomListFilterFunc + return fn ? this.roomList.current.filter(fn) : this.roomList.current } #shouldHideRoom(entry: SyncRoom): boolean { @@ -259,6 +277,12 @@ export class StateStore { if (updatedRoomList) { this.roomList.emit(updatedRoomList) } + for (const [spaceID, children] of Object.entries(sync.space_edges ?? {})) { + this.getSpaceStore(spaceID, true).children = children + } + if (sync.top_level_spaces) { + this.topLevelSpaces.emit(sync.top_level_spaces) + } } invalidateEmojiPackKeyCache() { @@ -324,6 +348,20 @@ export class StateStore { return this.#watchedRoomEmojiPacks ?? {} } + getSpaceStore(spaceID: RoomID, force: true): SpaceEdgeStore + getSpaceStore(spaceID: RoomID): SpaceEdgeStore | null + getSpaceStore(spaceID: RoomID, force?: true): SpaceEdgeStore | null { + let store = this.spaceEdges.get(spaceID) + if (!store) { + if (!force && this.rooms.get(spaceID)?.meta.current.creation_content?.type !== "m.space") { + return null + } + store = new SpaceEdgeStore(spaceID, this) + this.spaceEdges.set(spaceID, store) + } + return store + } + get frequentlyUsedEmoji(): Map { if (this.#frequentlyUsedEmoji === null) { const emojiData = this.accountData.get("io.element.recent_emoji") @@ -433,9 +471,12 @@ export class StateStore { clear() { this.rooms.clear() this.inviteRooms.clear() + this.spaceEdges.clear() this.roomList.emit([]) + this.topLevelSpaces.emit([]) this.accountData.clear() - this.currentRoomListFilter = "" + this.currentRoomListQuery = "" + this.currentRoomListFilter = null this.#frequentlyUsedEmoji = null this.#emojiPackKeys = null this.#watchedRoomEmojiPacks = null diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts new file mode 100644 index 0000000..f3a3fc0 --- /dev/null +++ b/web/src/api/statestore/space.ts @@ -0,0 +1,109 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { RoomListEntry, StateStore } from "@/api/statestore/main.ts" +import { DBSpaceEdge, RoomID } from "@/api/types" + +export interface RoomListFilter { + id: unknown + include(room: RoomListEntry): boolean +} + +export class SpaceEdgeStore { + #children: DBSpaceEdge[] = [] + #childRooms: Set = new Set() + #flattenedRooms: Set = new Set() + #childSpaces: Set = new Set() + readonly #parentSpaces: Set = new Set() + + constructor(public id: RoomID, private parent: StateStore) { + } + + addParent(parent: SpaceEdgeStore) { + this.#parentSpaces.add(parent) + } + + removeParent(parent: SpaceEdgeStore) { + this.#parentSpaces.delete(parent) + } + + include(room: RoomListEntry) { + return this.#flattenedRooms.has(room.room_id) + } + + get children() { + return this.#children + } + + #updateFlattened(recalculate: boolean, added: Set) { + if (recalculate) { + let flattened = new Set(this.#childRooms) + for (const space of this.#childSpaces) { + flattened = flattened.union(space.#flattenedRooms) + } + this.#flattenedRooms = flattened + } else if (added.size > 50) { + this.#flattenedRooms = this.#flattenedRooms.union(added) + } else if (added.size > 0) { + for (const room of added) { + this.#flattenedRooms.add(room) + } + } + } + + #notifyParentsOfChange(recalculate: boolean, added: Set, stack: WeakSet) { + if (stack.has(this)) { + return + } + stack.add(this) + for (const parent of this.#parentSpaces) { + parent.#updateFlattened(recalculate, added) + parent.#notifyParentsOfChange(recalculate, added, stack) + } + stack.delete(this) + } + + set children(newChildren: DBSpaceEdge[]) { + const newChildRooms = new Set() + const newChildSpaces = new Set() + for (const child of newChildren) { + const spaceStore = this.parent.getSpaceStore(child.child_id) + if (spaceStore) { + newChildSpaces.add(spaceStore) + spaceStore.addParent(this) + } else { + newChildRooms.add(child.child_id) + } + } + for (const space of this.#childSpaces) { + if (!newChildSpaces.has(space)) { + space.removeParent(this) + } + } + const addedRooms = newChildRooms.difference(this.#childRooms) + const removedRooms = this.#childRooms.difference(newChildRooms) + this.#children = newChildren + this.#childRooms = newChildRooms + this.#childSpaces = newChildSpaces + if (this.#childSpaces.size > 0) { + this.#updateFlattened(removedRooms.size > 0, addedRooms) + } else { + this.#flattenedRooms = newChildRooms + } + if (this.#parentSpaces.size > 0) { + this.#notifyParentsOfChange(removedRooms.size > 0, addedRooms, new WeakSet()) + } + } +} diff --git a/web/src/api/types/hievents.ts b/web/src/api/types/hievents.ts index 7c64596..125eb57 100644 --- a/web/src/api/types/hievents.ts +++ b/web/src/api/types/hievents.ts @@ -91,7 +91,7 @@ export interface SyncCompleteData { invited_rooms: DBInvitedRoom[] | null left_rooms: RoomID[] | null account_data: Record | null - space_edges: Record[]> | null + space_edges: Record | null top_level_spaces: RoomID[] | null since?: string clear_state?: boolean diff --git a/web/src/api/types/hitypes.ts b/web/src/api/types/hitypes.ts index a859a47..521a9dd 100644 --- a/web/src/api/types/hitypes.ts +++ b/web/src/api/types/hitypes.ts @@ -72,7 +72,7 @@ export interface DBRoom { } export interface DBSpaceEdge { - space_id: RoomID + // space_id: RoomID child_id: RoomID child_event_rowid?: EventRowID diff --git a/web/src/icons/home.svg b/web/src/icons/home.svg new file mode 100644 index 0000000..cc29681 --- /dev/null +++ b/web/src/icons/home.svg @@ -0,0 +1 @@ + diff --git a/web/src/ui/MainScreen.css b/web/src/ui/MainScreen.css index bfe308d..4e9bc21 100644 --- a/web/src/ui/MainScreen.css +++ b/web/src/ui/MainScreen.css @@ -1,5 +1,5 @@ main.matrix-main { - --room-list-width: 300px; + --room-list-width: 350px; --right-panel-width: 300px; position: fixed; diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 63173ed..05f8b59 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -318,7 +318,7 @@ const MainScreen = () => { }, [context, client]) useEffect(() => context.keybindings.listen(), [context]) const [roomListWidth, resizeHandle1] = useResizeHandle( - 300, 48, Math.min(900, window.innerWidth * 0.4), + 350, 96, Math.min(900, window.innerWidth * 0.4), "roomListWidth", { className: "room-list-resizer" }, ) const [rightPanelWidth, resizeHandle2] = useResizeHandle( diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index 933fba2..ca6504a 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -5,14 +5,53 @@ div.room-list-wrapper { box-sizing: border-box; overflow: hidden; scrollbar-color: var(--room-list-scrollbar-color); - display: flex; - flex-direction: column; + display: grid; + grid-template: + "spacebar search" 3.5rem + "spacebar roomlist" 1fr + / 3rem 1fr; } div.room-list { background-color: var(--room-list-background-overlay); overflow-y: auto; - flex: 1; + grid-area: roomlist; +} + +div.space-bar { + background-color: var(--space-list-background-overlay); + grid-area: spacebar; + overflow: auto; + scrollbar-width: none; + + > div.space-entry { + width: 2rem; + height: 2rem; + padding: .25rem; + margin: .25rem; + border-radius: .25rem; + cursor: var(--clickable-cursor); + + &:hover, &:focus { + background-color: var(--room-list-entry-hover-color); + } + + &.active { + background-color: var(--room-list-entry-selected-color); + } + + > svg { + width: 100%; + height: 100%; + } + + > img.avatar { + border-radius: 0; + clip-path: url(#squircle); + width: 100%; + height: 100%; + } + } } div.room-search-wrapper { @@ -21,6 +60,7 @@ div.room-search-wrapper { align-items: center; height: 3.5rem; background-color: var(--room-list-search-background-overlay); + grid-area: search; > input { padding: 0 0 0 1rem; diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 93eb074..324c144 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -13,7 +13,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useRef, useState } from "react" +import React, { use, useCallback, useRef, useState } from "react" +import type { RoomListFilter } from "@/api/statestore/space.ts" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -22,7 +23,9 @@ import ClientContext from "../ClientContext.ts" import MainScreenContext from "../MainScreenContext.ts" import { keyToString } from "../keybindings.ts" import Entry from "./Entry.tsx" +import Space from "./Space.tsx" import CloseIcon from "@/icons/close.svg?react" +import HomeIcon from "@/icons/home.svg?react" import SearchIcon from "@/icons/search.svg?react" import "./RoomList.css" @@ -34,20 +37,27 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { const client = use(ClientContext)! const mainScreen = use(MainScreenContext) const roomList = useEventAsState(client.store.roomList) - const roomFilterRef = useRef(null) - const [roomFilter, setRoomFilter] = useState("") - const [realRoomFilter, setRealRoomFilter] = useState("") + const spaces = useEventAsState(client.store.topLevelSpaces) + const searchInputRef = useRef(null) + const [query, directSetQuery] = useState("") + const [space, directSetSpace] = useState(null) - const updateRoomFilter = (evt: React.ChangeEvent) => { - setRoomFilter(evt.target.value) - client.store.currentRoomListFilter = toSearchableString(evt.target.value) - setRealRoomFilter(client.store.currentRoomListFilter) + const setQuery = (evt: React.ChangeEvent) => { + client.store.currentRoomListQuery = toSearchableString(evt.target.value) + directSetQuery(evt.target.value) } + const setSpace = useCallback((space: RoomListFilter | null) => { + directSetSpace(space) + client.store.currentRoomListFilter = space + }, [client]) + const onClickSpace = useCallback((evt: React.MouseEvent) => { + const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) + setSpace(store) + }, [setSpace, client]) const clearQuery = () => { - setRoomFilter("") - client.store.currentRoomListFilter = "" - setRealRoomFilter("") - roomFilterRef.current?.focus() + client.store.currentRoomListQuery = "" + directSetQuery("") + searchInputRef.current?.focus() } const onKeyDown = (evt: React.KeyboardEvent) => { const key = keyToString(evt) @@ -64,28 +74,40 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { } } + const roomListFilter = client.store.roomListFilterFunc return
-
+
+
setSpace(null)}> + +
+ {spaces.map(roomID => )} +
{reverseMap(roomList, room =>
-
setSpace(null)}> - -
+ + + {spaces.map(roomID => Date: Sun, 29 Dec 2024 14:55:18 +0200 Subject: [PATCH 45/85] web/roomlist: add pseudo-space for space orphans --- web/src/api/statestore/main.ts | 14 +++++++++++--- web/src/api/statestore/space.ts | 26 ++++++++++++++++++++------ web/src/ui/roomlist/RoomList.tsx | 15 ++++++++++++--- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 71db297..724754a 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -39,7 +39,7 @@ import { } from "../types" import { InvitedRoomStore } from "./invitedroom.ts" import { RoomStateStore } from "./room.ts" -import { RoomListFilter, SpaceEdgeStore } from "./space.ts" +import { RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace } from "./space.ts" export interface RoomListEntry { room_id: RoomID @@ -75,6 +75,7 @@ export class StateStore { readonly roomList = new NonNullCachedEventDispatcher([]) readonly topLevelSpaces = new NonNullCachedEventDispatcher([]) readonly spaceEdges: Map = new Map() + readonly spaceOrphans = new SpaceOrphansSpace(this) currentRoomListQuery: string = "" currentRoomListFilter: RoomListFilter | null = null readonly accountData: Map = new Map() @@ -277,11 +278,18 @@ export class StateStore { if (updatedRoomList) { this.roomList.emit(updatedRoomList) } - for (const [spaceID, children] of Object.entries(sync.space_edges ?? {})) { - this.getSpaceStore(spaceID, true).children = children + if (sync.space_edges) { + // Ensure all space stores exist first + for (const spaceID of Object.keys(sync.space_edges)) { + this.getSpaceStore(spaceID, true) + } + for (const [spaceID, children] of Object.entries(sync.space_edges ?? {})) { + this.getSpaceStore(spaceID, true).children = children + } } if (sync.top_level_spaces) { this.topLevelSpaces.emit(sync.top_level_spaces) + this.spaceOrphans.children = sync.top_level_spaces.map(child_id => ({ child_id })) } } diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts index 22d354d..1815533 100644 --- a/web/src/api/statestore/space.ts +++ b/web/src/api/statestore/space.ts @@ -44,11 +44,11 @@ export class SpaceEdgeStore implements RoomListFilter { constructor(public id: RoomID, private parent: StateStore) { } - addParent(parent: SpaceEdgeStore) { + #addParent(parent: SpaceEdgeStore) { this.#parentSpaces.add(parent) } - removeParent(parent: SpaceEdgeStore) { + #removeParent(parent: SpaceEdgeStore) { this.#parentSpaces.delete(parent) } @@ -95,28 +95,42 @@ export class SpaceEdgeStore implements RoomListFilter { const spaceStore = this.parent.getSpaceStore(child.child_id) if (spaceStore) { newChildSpaces.add(spaceStore) - spaceStore.addParent(this) + spaceStore.#addParent(this) } else { newChildRooms.add(child.child_id) } } for (const space of this.#childSpaces) { if (!newChildSpaces.has(space)) { - space.removeParent(this) + space.#removeParent(this) } } const addedRooms = newChildRooms.difference(this.#childRooms) const removedRooms = this.#childRooms.difference(newChildRooms) + const didAddChildren = newChildSpaces.difference(this.#childSpaces).size > 0 + const recalculateFlattened = removedRooms.size > 0 || didAddChildren this.#children = newChildren this.#childRooms = newChildRooms this.#childSpaces = newChildSpaces if (this.#childSpaces.size > 0) { - this.#updateFlattened(removedRooms.size > 0, addedRooms) + this.#updateFlattened(recalculateFlattened, addedRooms) } else { this.#flattenedRooms = newChildRooms } if (this.#parentSpaces.size > 0) { - this.#notifyParentsOfChange(removedRooms.size > 0, addedRooms, new WeakSet()) + this.#notifyParentsOfChange(recalculateFlattened, addedRooms, new WeakSet()) } } } + +export class SpaceOrphansSpace extends SpaceEdgeStore { + static id = "fi.mau.gomuks.space_orphans" + + constructor(parent: StateStore) { + super(SpaceOrphansSpace.id, parent) + } + + include(room: RoomListEntry): boolean { + return !super.include(room) && !room.dm_user_id + } +} diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 3289526..587ac8d 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -75,6 +75,12 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { } const roomListFilter = client.store.roomListFilterFunc + const pseudoSpaces = [ + null, + DirectChatSpace, + UnreadsSpace, + client.store.spaceOrphans, + ] return
{
- - - + {pseudoSpaces.map(pseudoSpace => )} {spaces.map(roomID => Date: Sun, 29 Dec 2024 15:28:29 +0200 Subject: [PATCH 46/85] media: use rect instead of circle in fallback avatar Normal avatars already have border-radius. The raw svg being a rectangle allows space squircles to work properly too. --- pkg/gomuks/media.go | 2 +- web/src/api/media.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/gomuks/media.go b/pkg/gomuks/media.go index c6d847f..fe03c96 100644 --- a/pkg/gomuks/media.go +++ b/pkg/gomuks/media.go @@ -125,7 +125,7 @@ func (new *noErrorWriter) Write(p []byte) (n int, err error) { // note: this should stay in sync with makeAvatarFallback in web/src/api/media.ts const fallbackAvatarTemplate = ` - + %s diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 5c97ac1..33ad9ea 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -54,7 +54,7 @@ export const getUserColor = (userID: UserID) => { // note: this should stay in sync with fallbackAvatarTemplate in cmd/gomuks.media.go function makeFallbackAvatar(backgroundColor: string, fallbackCharacter: string): string { return "data:image/svg+xml," + encodeURIComponent(` - + ${escapeHTMLChar(fallbackCharacter)} From 7a8d29b6de95758a12ba5a0cfd681c596b28330d Mon Sep 17 00:00:00 2001 From: Derry Tutt <82726593+everypizza1@users.noreply.github.com> Date: Sun, 29 Dec 2024 11:12:47 -0700 Subject: [PATCH 47/85] web/composer: fix streched custom emojis in autocomplete (#565) --- web/src/ui/composer/Autocompleter.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/composer/Autocompleter.css b/web/src/ui/composer/Autocompleter.css index fb8b4bd..571532c 100644 --- a/web/src/ui/composer/Autocompleter.css +++ b/web/src/ui/composer/Autocompleter.css @@ -35,6 +35,7 @@ div.autocompletions { > img { width: 1.5rem; height: 1.5rem; + object-fit: contain; } } } From c25ab057dc9f2e3106450fb589d43d23b284b266 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 10:18:36 +0200 Subject: [PATCH 48/85] web/roomlist: add react key for real spaces --- web/src/ui/roomlist/RoomList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 587ac8d..2e84442 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -105,6 +105,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { isActive={space?.id === pseudoSpace?.id} />)} {spaces.map(roomID => Date: Mon, 30 Dec 2024 10:54:13 +0200 Subject: [PATCH 49/85] web/timeline: use custom message focus state on mobile to match context menu state --- web/src/ui/roomview/RoomView.css | 12 +++++++ web/src/ui/roomview/RoomView.tsx | 9 +++++- web/src/ui/roomview/RoomViewHeader.css | 1 + web/src/ui/roomview/roomcontext.ts | 9 +++++- web/src/ui/timeline/TimelineEvent.css | 2 +- web/src/ui/timeline/TimelineEvent.tsx | 31 +++++++++---------- web/src/ui/timeline/TimelineView.tsx | 12 +++++-- .../ui/timeline/content/MediaMessageBody.tsx | 10 ++++-- web/src/ui/timeline/menu/EventMenu.tsx | 12 ++++--- web/src/ui/timeline/menu/index.css | 4 --- 10 files changed, 69 insertions(+), 33 deletions(-) diff --git a/web/src/ui/roomview/RoomView.css b/web/src/ui/roomview/RoomView.css index 49b93f4..4dca406 100644 --- a/web/src/ui/roomview/RoomView.css +++ b/web/src/ui/roomview/RoomView.css @@ -19,3 +19,15 @@ div.room-view { align-items: center; } } + +div#mobile-event-menu-container { + grid-area: header; + + &:empty { + display: none; + } + + &:not(:empty) + div.room-header { + display: none; + } +} diff --git a/web/src/ui/roomview/RoomView.tsx b/web/src/ui/roomview/RoomView.tsx index d06da39..a646395 100644 --- a/web/src/ui/roomview/RoomView.tsx +++ b/web/src/ui/roomview/RoomView.tsx @@ -41,8 +41,15 @@ const RoomView = ({ room, rightPanelResizeHandle, rightPanel }: RoomViewProps) = } } }, [roomContextData]) + const onClick = (evt: React.MouseEvent) => { + if (roomContextData.focusedEventRowID) { + roomContextData.setFocusedEventRowID(null) + evt.stopPropagation() + } + } return -
+
+
diff --git a/web/src/ui/roomview/RoomViewHeader.css b/web/src/ui/roomview/RoomViewHeader.css index aab85cd..2f4c4d6 100644 --- a/web/src/ui/roomview/RoomViewHeader.css +++ b/web/src/ui/roomview/RoomViewHeader.css @@ -5,6 +5,7 @@ div.room-header { padding-left: .5rem; border-bottom: 1px solid var(--border-color); overflow: hidden; + grid-area: header; > div.room-name-and-topic { flex: 1; diff --git a/web/src/ui/roomview/roomcontext.ts b/web/src/ui/roomview/roomcontext.ts index d38ff54..0758612 100644 --- a/web/src/ui/roomview/roomcontext.ts +++ b/web/src/ui/roomview/roomcontext.ts @@ -15,7 +15,7 @@ // along with this program. If not, see . import { RefObject, createContext, createRef, use } from "react" 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 { escapeMarkdown } from "@/util/markdown.ts" @@ -28,6 +28,8 @@ export class RoomContextData { public setReplyTo: (eventID: EventID | null) => void = noop("setReplyTo") public setEditing: (evt: MemDBEvent | null) => void = noop("setEditing") 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(false) public scrolledToBottom = true @@ -39,6 +41,11 @@ export class RoomContextData { } } + setFocusedEventRowID = (eventRowID: number | null) => { + this.directSetFocusedEventRowID(eventRowID) + this.focusedEventRowID = eventRowID + } + appendMentionToComposer = (evt: React.MouseEvent) => { const targetUser = evt.currentTarget.getAttribute("data-target-user") if (!targetUser) { diff --git a/web/src/ui/timeline/TimelineEvent.css b/web/src/ui/timeline/TimelineEvent.css index 1bc1acf..c5b38b4 100644 --- a/web/src/ui/timeline/TimelineEvent.css +++ b/web/src/ui/timeline/TimelineEvent.css @@ -24,7 +24,7 @@ div.timeline-event { transition: background-color 1s; } - &:hover { + &:hover:not(.no-hover), &.focused-event { background-color: var(--timeline-hover-bg-color); &.highlight { diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 38b15d7..524d84b 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { JSX, use, useState } from "react" +import { createPortal } from "react-dom" import { getAvatarURL, getMediaURL, getUserColorIndex } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types" @@ -38,6 +39,7 @@ export interface TimelineEventProps { prevEvt: MemDBEvent | null disableMenu?: boolean smallReplies?: boolean + isFocused?: boolean } 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 client = use(ClientContext)! const mainScreen = use(MainScreenContext) @@ -107,21 +109,8 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven return } mouseEvt.preventDefault() - if (window.hackyOpenEventContextMenu === evt.event_id) { - window.closeModal() - window.hackyOpenEventContextMenu = undefined - } else { - openModal({ - content: , - captureInput: false, - onClose: () => { - if (window.hackyOpenEventContextMenu === evt.event_id) { - window.hackyOpenEventContextMenu = undefined - } - }, - }) - window.hackyOpenEventContextMenu = evt.event_id - } + mouseEvt.stopPropagation() + roomCtx.setFocusedEventRowID(roomCtx.focusedEventRowID === evt.rowid ? null : evt.rowid) } const memberEvt = useRoomMember(client, roomCtx.store, evt.sender) const memberEvtContent = memberEvt?.content as MemberEventContent | undefined @@ -144,6 +133,12 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven if (evt.sender === client.userID) { wrapperClassNames.push("own-event") } + if (isMobileDevice || disableMenu) { + wrapperClassNames.push("no-hover") + } + if (isFocused) { + wrapperClassNames.push("focused-event") + } let dateSeparator = null const prevEvtDate = prevEvt ? new Date(prevEvt.timestamp) : null if (prevEvtDate && ( @@ -207,6 +202,10 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies }: TimelineEven >
} + {isMobileDevice && isFocused && createPortal( + , + document.getElementById("mobile-event-menu-container")!, + )} {replyAboveMessage} {renderAvatar &&
{ const timeline = useRoomTimeline(room) const client = use(ClientContext)! const [isLoadingHistory, setLoadingHistory] = useState(false) + const [focusedEventRowID, directSetFocusedEventRowID] = useState(null) const loadHistory = useCallback(() => { setLoadingHistory(true) client.loadMoreHistory(room.roomID) @@ -68,6 +69,9 @@ const TimelineView = () => { } prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0 }, [client.userID, roomCtx, timeline]) + useEffect(() => { + roomCtx.directSetFocusedEventRowID = directSetFocusedEventRowID + }, []) useEffect(() => { const newestEvent = timeline[timeline.length - 1] if ( @@ -126,7 +130,11 @@ const TimelineView = () => { return null } const thisEvt = prevEvt = entry return thisEvt diff --git a/web/src/ui/timeline/content/MediaMessageBody.tsx b/web/src/ui/timeline/content/MediaMessageBody.tsx index 9a8401a..fbe267a 100644 --- a/web/src/ui/timeline/content/MediaMessageBody.tsx +++ b/web/src/ui/timeline/content/MediaMessageBody.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { CSSProperties, JSX, use, useReducer } from "react" +import React, { CSSProperties, JSX, use, useReducer, useState } from "react" import { Blurhash } from "react-blurhash" import { GridLoader } from "react-spinners" import { usePreference } from "@/api/statestore" @@ -49,7 +49,7 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => { const supportsClickToShow = supportsLoadingPlaceholder || content.msgtype === "m.video" const showPreviewsByDefault = usePreference(client.store, room, "show_media_previews") 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"] if (content["page.codeberg.everypizza.msc4193.spoiler"]) { @@ -72,8 +72,12 @@ const MediaMessageBody = ({ event, room, sender }: EventContentProps) => { const blurhash = ensureString( content.info?.["xyz.amorgan.blurhash"] ?? content.info?.thumbnail_info?.["xyz.amorgan.blurhash"], ) + const onClick = !clickedShow ? (evt: React.MouseEvent) => { + setClickedShow(true) + evt.stopPropagation() + } : undefined placeholderElem =
{(blurhash && containerStyle.width) ? void } @@ -31,9 +34,7 @@ export const EventHoverMenu = ({ evt, roomCtx, setForceOpen }: EventHoverMenuPro return
{elements}
} -interface EventContextMenuProps { - evt: MemDBEvent - roomCtx: RoomContextData +interface EventContextMenuProps extends BaseEventMenuProps { style: CSSProperties } @@ -53,12 +54,13 @@ export const EventFullMenu = ({ evt, roomCtx, style }: EventContextMenuProps) =>
} -export const EventFixedMenu = ({ evt, roomCtx }: Omit) => { +export const EventFixedMenu = ({ evt, roomCtx }: BaseEventMenuProps) => { const client = use(ClientContext)! const primary = usePrimaryItems(client, roomCtx, evt, false, true, undefined, undefined) const secondary = useSecondaryItems(client, roomCtx, evt, false) return
{primary} +
{secondary}
} diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index 44bb8c0..f42eac3 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -20,12 +20,8 @@ div.event-hover-menu, div.event-fixed-menu { } div.event-fixed-menu { - position: fixed; - inset: 0 0 auto; - height: 3.5rem; padding: .25rem; border-bottom: 1px solid var(--border-color); - box-sizing: border-box; justify-content: right; flex-direction: row-reverse; overflow-x: auto; From b3d63b72011bc9c6df7caf033676e54c7fd2e2c0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 11:01:22 +0200 Subject: [PATCH 50/85] web/roomview: fix mobile context menu scroll --- web/src/ui/roomview/RoomView.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/roomview/RoomView.css b/web/src/ui/roomview/RoomView.css index 4dca406..b0aad91 100644 --- a/web/src/ui/roomview/RoomView.css +++ b/web/src/ui/roomview/RoomView.css @@ -22,6 +22,7 @@ div.room-view { div#mobile-event-menu-container { grid-area: header; + overflow: hidden; &:empty { display: none; From 248a218eed3d6efe53dbfcbe58793f42735612a6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 12:40:03 +0200 Subject: [PATCH 51/85] web/timeline: fix mobile context menu bottom border again --- web/src/ui/roomview/RoomView.css | 1 + web/src/ui/timeline/menu/index.css | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/roomview/RoomView.css b/web/src/ui/roomview/RoomView.css index b0aad91..ec525b1 100644 --- a/web/src/ui/roomview/RoomView.css +++ b/web/src/ui/roomview/RoomView.css @@ -23,6 +23,7 @@ div.room-view { div#mobile-event-menu-container { grid-area: header; overflow: hidden; + border-bottom: 1px solid var(--border-color); &:empty { display: none; diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index f42eac3..d729fad 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -21,7 +21,6 @@ div.event-hover-menu, div.event-fixed-menu { div.event-fixed-menu { padding: .25rem; - border-bottom: 1px solid var(--border-color); justify-content: right; flex-direction: row-reverse; overflow-x: auto; From b30025746d58c0eda968bcc39652eabda118bdac Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 12:53:51 +0200 Subject: [PATCH 52/85] web/timeline: add close button to mobile context menu --- web/src/ui/timeline/menu/EventMenu.tsx | 3 +++ web/src/ui/timeline/menu/index.css | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/web/src/ui/timeline/menu/EventMenu.tsx b/web/src/ui/timeline/menu/EventMenu.tsx index 5c5a8ea..13a839b 100644 --- a/web/src/ui/timeline/menu/EventMenu.tsx +++ b/web/src/ui/timeline/menu/EventMenu.tsx @@ -19,6 +19,7 @@ import ClientContext from "../../ClientContext.ts" import { RoomContextData } from "../../roomview/roomcontext.ts" import { usePrimaryItems } from "./usePrimaryItems.tsx" import { useSecondaryItems } from "./useSecondaryItems.tsx" +import CloseIcon from "@/icons/close.svg?react" interface BaseEventMenuProps { evt: MemDBEvent @@ -62,5 +63,7 @@ export const EventFixedMenu = ({ evt, roomCtx }: BaseEventMenuProps) => { {primary}
{secondary} +
+
} diff --git a/web/src/ui/timeline/menu/index.css b/web/src/ui/timeline/menu/index.css index d729fad..a78c222 100644 --- a/web/src/ui/timeline/menu/index.css +++ b/web/src/ui/timeline/menu/index.css @@ -26,6 +26,12 @@ div.event-fixed-menu { overflow-x: auto; overflow-y: hidden; + > div.vertical-line { + width: 1px; + flex-shrink: 0; + background-color: var(--border-color); + } + > button { width: 3rem; height: 3rem; From e0f107f0285936964afeeec8f4efbb312d9e3c22 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 16:43:11 +0200 Subject: [PATCH 53/85] web/roomlist: add unread counters for spaces Fixes #570 --- web/src/api/statestore/main.ts | 39 +++++++++++++- web/src/api/statestore/room.ts | 3 +- web/src/api/statestore/space.ts | 74 ++++++++++++++++++++++---- web/src/index.css | 6 +++ web/src/ui/roomlist/Entry.tsx | 19 +------ web/src/ui/roomlist/FakeSpace.tsx | 9 ++-- web/src/ui/roomlist/RoomList.css | 82 +++++++++++++++++++---------- web/src/ui/roomlist/RoomList.tsx | 15 ++---- web/src/ui/roomlist/Space.tsx | 3 ++ web/src/ui/roomlist/UnreadCount.tsx | 73 +++++++++++++++++++++++++ 10 files changed, 251 insertions(+), 72 deletions(-) create mode 100644 web/src/ui/roomlist/UnreadCount.tsx diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 724754a..50b07d1 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -39,7 +39,7 @@ import { } from "../types" import { InvitedRoomStore } from "./invitedroom.ts" import { RoomStateStore } from "./room.ts" -import { RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace } from "./space.ts" +import { DirectChatSpace, RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts" export interface RoomListEntry { room_id: RoomID @@ -76,6 +76,13 @@ export class StateStore { readonly topLevelSpaces = new NonNullCachedEventDispatcher([]) readonly spaceEdges: Map = new Map() readonly spaceOrphans = new SpaceOrphansSpace(this) + readonly directChatsSpace = new DirectChatSpace() + readonly unreadsSpace = new UnreadsSpace() + readonly pseudoSpaces = [ + this.spaceOrphans, + this.directChatsSpace, + this.unreadsSpace, + ] as const currentRoomListQuery: string = "" currentRoomListFilter: RoomListFilter | null = null readonly accountData: Map = new Map() @@ -177,6 +184,25 @@ export class StateStore { } } + #applyUnreadModification(meta: RoomListEntry | null, oldMeta: RoomListEntry | undefined | null) { + const someMeta = meta ?? oldMeta + if (!someMeta) { + return + } + if (this.spaceOrphans.include(someMeta)) { + this.spaceOrphans.applyUnreads(meta, oldMeta) + return + } + if (this.directChatsSpace.include(someMeta)) { + this.directChatsSpace.applyUnreads(meta, oldMeta) + } + for (const space of this.spaceEdges.values()) { + if (space.include(someMeta)) { + space.applyUnreads(meta, oldMeta) + } + } + } + applySync(sync: SyncCompleteData) { if (sync.clear_state && this.rooms.size > 0) { console.info("Clearing state store as sync told to reset and there are rooms in the store") @@ -186,9 +212,11 @@ export class StateStore { const changedRoomListEntries = new Map() for (const data of sync.invited_rooms ?? []) { const room = new InvitedRoomStore(data, this) + const oldEntry = this.inviteRooms.get(room.room_id) this.inviteRooms.set(room.room_id, room) if (!resyncRoomList) { changedRoomListEntries.set(room.room_id, room) + this.#applyUnreadModification(room, oldEntry) } if (this.activeRoomID === room.room_id) { this.switchRoom?.(room.room_id) @@ -209,7 +237,10 @@ export class StateStore { const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room)) room.applySync(data) if (roomListEntryChanged) { - changedRoomListEntries.set(roomID, this.#makeRoomListEntry(data, room)) + const entry = this.#makeRoomListEntry(data, room) + changedRoomListEntries.set(roomID, entry) + this.#applyUnreadModification(entry, room.roomListEntry) + room.roomListEntry = entry } if (!resyncRoomList) { // When we join a valid replacement room, hide the tombstoned room. @@ -256,6 +287,10 @@ export class StateStore { .map(entry => this.#makeRoomListEntry(entry)) .filter(entry => entry !== null)) updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp) + for (const entry of updatedRoomList) { + this.#applyUnreadModification(entry, undefined) + this.rooms.get(entry.room_id)!.roomListEntry = entry + } } else if (changedRoomListEntries.size > 0) { updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id)) for (const entry of changedRoomListEntries.values()) { diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 7419a55..73903a8 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -42,7 +42,7 @@ import { UserID, roomStateGUIDToString, } from "../types" -import type { StateStore } from "./main.ts" +import type { RoomListEntry, StateStore } from "./main.ts" function arraysAreEqual(arr1?: T[], arr2?: T[]): boolean { if (!arr1 || !arr2) { @@ -126,6 +126,7 @@ export class RoomStateStore { readUpToRow = -1 hasMoreHistory = true hidden = false + roomListEntry: RoomListEntry | undefined | null constructor(meta: DBRoom, private parent: StateStore) { this.roomID = meta.room_id diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts index 1815533..8e7eb02 100644 --- a/web/src/api/statestore/space.ts +++ b/web/src/api/statestore/space.ts @@ -15,26 +15,79 @@ // along with this program. If not, see . import { RoomListEntry, StateStore } from "@/api/statestore/main.ts" import { DBSpaceEdge, RoomID } from "@/api/types" +import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts" export interface RoomListFilter { id: string include(room: RoomListEntry): boolean } -export const DirectChatSpace: RoomListFilter = { - id: "fi.mau.gomuks.direct_chats", - include: room => !!room.dm_user_id, +export interface SpaceUnreadCounts { + unread_messages: number + unread_notifications: number + unread_highlights: number } -export const UnreadsSpace: RoomListFilter = { - id: "fi.mau.gomuks.unreads", - include: room => Boolean(room.unread_messages - || room.unread_notifications - || room.unread_highlights - || room.marked_unread), +const emptyUnreadCounts: SpaceUnreadCounts = { + unread_messages: 0, + unread_notifications: 0, + unread_highlights: 0, } -export class SpaceEdgeStore implements RoomListFilter { +export abstract class Space implements RoomListFilter { + counts = new NonNullCachedEventDispatcher(emptyUnreadCounts) + + abstract id: string + abstract include(room: RoomListEntry): boolean + + applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) { + const mergedCounts: SpaceUnreadCounts = { + unread_messages: this.counts.current.unread_messages + + (newCounts?.unread_messages ?? 0) - (oldCounts?.unread_messages ?? 0), + unread_notifications: this.counts.current.unread_notifications + + (newCounts?.unread_notifications ?? 0) - (oldCounts?.unread_notifications ?? 0), + unread_highlights: this.counts.current.unread_highlights + + (newCounts?.unread_highlights ?? 0) - (oldCounts?.unread_highlights ?? 0), + } + if (mergedCounts.unread_messages < 0) { + mergedCounts.unread_messages = 0 + } + if (mergedCounts.unread_notifications < 0) { + mergedCounts.unread_notifications = 0 + } + if (mergedCounts.unread_highlights < 0) { + mergedCounts.unread_highlights = 0 + } + if ( + mergedCounts.unread_messages !== this.counts.current.unread_messages + || mergedCounts.unread_notifications !== this.counts.current.unread_notifications + || mergedCounts.unread_highlights !== this.counts.current.unread_highlights + ) { + this.counts.emit(mergedCounts) + } + } +} + +export class DirectChatSpace extends Space { + id = "fi.mau.gomuks.direct_chats" + + include(room: RoomListEntry): boolean { + return Boolean(room.dm_user_id) + } +} + +export class UnreadsSpace extends Space { + id = "fi.mau.gomuks.unreads" + + include(room: RoomListEntry): boolean { + return Boolean(room.unread_messages + || room.unread_notifications + || room.unread_highlights + || room.marked_unread) + } +} + +export class SpaceEdgeStore extends Space { #children: DBSpaceEdge[] = [] #childRooms: Set = new Set() #flattenedRooms: Set = new Set() @@ -42,6 +95,7 @@ export class SpaceEdgeStore implements RoomListFilter { readonly #parentSpaces: Set = new Set() constructor(public id: RoomID, private parent: StateStore) { + super() } #addParent(parent: SpaceEdgeStore) { diff --git a/web/src/index.css b/web/src/index.css index 6517adc..5d6f39d 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -55,6 +55,9 @@ --unread-counter-notification-bg: rgba(50, 150, 0, 0.7); --unread-counter-marked-unread-bg: var(--unread-counter-notification-bg); --unread-counter-highlight-bg: rgba(200, 0, 0, 0.7); + --space-unread-counter-message-bg: rgb(100, 100, 100, 0.9); + --space-unread-counter-notification-bg: rgb(50, 150, 0); + --space-unread-counter-highlight-bg: rgb(200, 0, 0); --sender-color-0: #a4041d; --sender-color-1: #9b2200; @@ -136,6 +139,9 @@ --unread-counter-message-bg: rgba(255, 255, 255, 0.5); --unread-counter-notification-bg: rgba(150, 255, 0, 0.7); --unread-counter-highlight-bg: rgba(255, 50, 50, 0.7); + --space-unread-counter-message-bg: rgb(200, 200, 200, 0.8); + --space-unread-counter-notification-bg: rgb(150, 255, 0); + --space-unread-counter-highlight-bg: rgb(255, 50, 50); --sender-color-0: #ff877c; --sender-color-1: #f6913d; diff --git a/web/src/ui/roomlist/Entry.tsx b/web/src/ui/roomlist/Entry.tsx index c26f2fa..901d64f 100644 --- a/web/src/ui/roomlist/Entry.tsx +++ b/web/src/ui/roomlist/Entry.tsx @@ -21,6 +21,7 @@ import useContentVisibility from "@/util/contentvisibility.ts" import { getDisplayname } from "@/util/validation.ts" import ClientContext from "../ClientContext.ts" import MainScreenContext from "../MainScreenContext.ts" +import UnreadCount from "./UnreadCount.tsx" export interface RoomListEntryProps { room: RoomListEntry @@ -56,14 +57,6 @@ function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): function renderEntry(room: RoomListEntry) { const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender) - const unreadCount = room.unread_messages || room.unread_notifications || room.unread_highlights - const countIsBig = Boolean(room.unread_notifications || room.unread_highlights) - let unreadCountDisplay = unreadCount.toString() - if (unreadCount > 999 && countIsBig) { - unreadCountDisplay = "99+" - } else if (unreadCount > 9999 && countIsBig) { - unreadCountDisplay = "999+" - } return <>
@@ -78,15 +71,7 @@ function renderEntry(room: RoomListEntry) {
{room.name}
{previewText &&
{croppedPreviewText}
}
- {(room.unread_messages || room.marked_unread) ?
-
- {unreadCountDisplay} -
-
: null} + } diff --git a/web/src/ui/roomlist/FakeSpace.tsx b/web/src/ui/roomlist/FakeSpace.tsx index f3ffa53..f8c595a 100644 --- a/web/src/ui/roomlist/FakeSpace.tsx +++ b/web/src/ui/roomlist/FakeSpace.tsx @@ -14,7 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { JSX } from "react" -import type { RoomListFilter } from "@/api/statestore/space.ts" +import { RoomListFilter, Space } from "@/api/statestore/space.ts" +import { useEventAsState } from "@/util/eventdispatcher.ts" +import UnreadCount from "./UnreadCount.tsx" import HomeIcon from "@/icons/home.svg?react" import NotificationsIcon from "@/icons/notifications.svg?react" import PersonIcon from "@/icons/person.svg?react" @@ -22,7 +24,7 @@ import TagIcon from "@/icons/tag.svg?react" import "./RoomList.css" export interface FakeSpaceProps { - space: RoomListFilter | null + space: Space | null setSpace: (space: RoomListFilter | null) => void isActive: boolean } @@ -43,10 +45,11 @@ const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => { } const FakeSpace = ({ space, setSpace, isActive }: FakeSpaceProps) => { + const unreads = useEventAsState(space?.counts) return
setSpace(space)}> + {getFakeSpaceIcon(space)}
- } export default FakeSpace diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index ca6504a..cc83edf 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -51,6 +51,17 @@ div.space-bar { width: 100%; height: 100%; } + + > div.room-entry-unreads { + z-index: 2; + height: 0; + position: relative; + + > div.unread-count { + position: absolute; + top: 0; + } + } } } @@ -138,51 +149,64 @@ div.room-entry { } } } +} + +div.room-entry-unreads { + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + margin-right: .25rem; + + > div.unread-count { + --unread-count-size: 1rem; + --unread-count-padding-inline: calc(var(--unread-count-size)/4); + --unread-count-padding-block: calc(var(--unread-count-size)/8); - > div.room-entry-unreads { display: flex; align-items: center; justify-content: center; - width: 3rem; - margin-right: .25rem; + border-radius: var(--unread-count-size); + color: var(--unread-counter-text-color); - > div.unread-count { - --unread-count-size: 1rem; - --unread-count-padding-inline: calc(var(--unread-count-size)/4); - --unread-count-padding-block: calc(var(--unread-count-size)/8); + background-color: var(--unread-counter-message-bg); + height: var(--unread-count-size); + min-width: calc(var(--unread-count-size) - 2*(var(--unread-count-padding-inline) - var(--unread-count-padding-block))); - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--unread-count-size); - color: var(--unread-counter-text-color); + line-height: 1; + font-size: .75em; - background-color: var(--unread-counter-message-bg); - height: var(--unread-count-size); - min-width: calc(var(--unread-count-size) - 2*(var(--unread-count-padding-inline) - var(--unread-count-padding-block))); + padding-inline: var(--unread-count-padding-inline); + padding-block: var(--unread-count-padding-block); - line-height: 1; - font-size: .75em; + &.big { + --unread-count-size: 1.5rem; + font-size: 1em; + font-weight: bold; + } - padding-inline: var(--unread-count-padding-inline); - padding-block: var(--unread-count-padding-block); + &.marked-unread { + background-color: var(--unread-counter-marked-unread-bg); + } - &.notified, &.marked-unread, &.highlighted { - --unread-count-size: 1.5rem; - font-size: 1em; - font-weight: bold; - } + &.notified { + background-color: var(--unread-counter-notification-bg); + } - &.marked-unread { - background-color: var(--unread-counter-marked-unread-bg); - } + &.highlighted { + background-color: var(--unread-counter-highlight-bg); + } + + &.space { + --unread-count-size: .75rem; + background-color: var(--space-unread-counter-message-bg); &.notified { - background-color: var(--unread-counter-notification-bg); + background-color: var(--space-unread-counter-notification-bg); } &.highlighted { - background-color: var(--unread-counter-highlight-bg); + background-color: var(--space-unread-counter-highlight-bg); } } } diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 2e84442..9b6ace3 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { use, useCallback, useRef, useState } from "react" -import { DirectChatSpace, RoomListFilter, UnreadsSpace } from "@/api/statestore/space.ts" +import { RoomListFilter } from "@/api/statestore/space.ts" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -75,12 +75,6 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { } const roomListFilter = client.store.roomListFilterFunc - const pseudoSpaces = [ - null, - DirectChatSpace, - UnreadsSpace, - client.store.spaceOrphans, - ] return
{
- {pseudoSpaces.map(pseudoSpace => + {client.store.pseudoSpaces.map(pseudoSpace => )} {spaces.map(roomID => { + const unreads = useEventAsState(client.store.spaceEdges.get(roomID)?.counts) const room = useEventAsState(client.store.rooms.get(roomID)?.meta) if (!room) { return } return
+ {room.name}
} diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx new file mode 100644 index 0000000..1c4f9c1 --- /dev/null +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -0,0 +1,73 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { SpaceUnreadCounts } from "@/api/statestore/space.ts" + +interface UnreadCounts extends SpaceUnreadCounts { + marked_unread?: boolean +} + +interface UnreadCountProps { + counts: UnreadCounts | null + space?: true +} + +const UnreadCount = ({ counts, space }: UnreadCountProps) => { + if (!counts) { + return null + } + const unreadCount = space + ? counts.unread_highlights || counts.unread_notifications || counts.unread_messages + : counts.unread_messages || counts.unread_notifications || counts.unread_highlights + if (!unreadCount && !counts.marked_unread) { + return null + } + const countIsBig = !space && Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread) + let unreadCountDisplay = unreadCount.toString() + if (unreadCount > 999 && countIsBig) { + unreadCountDisplay = "99+" + } else if (unreadCount > 9999 && countIsBig) { + unreadCountDisplay = "999+" + } + const classNames = ["unread-count"] + if (countIsBig) { + classNames.push("big") + } + let unreadCountTitle = unreadCount.toString() + if (space) { + classNames.push("space") + unreadCountTitle = [ + counts.unread_highlights && `${counts.unread_highlights} highlights`, + counts.unread_notifications && `${counts.unread_notifications} notifications`, + counts.unread_messages && `${counts.unread_messages} messages`, + ].filter(x => !!x).join("\n") + } + if (counts.marked_unread) { + classNames.push("marked-unread") + } + if (counts.unread_notifications) { + classNames.push("notified") + } + if (counts.unread_highlights) { + classNames.push("highlighted") + } + return
+
+ {unreadCountDisplay} +
+
+} + +export default UnreadCount From 572ef41b80d2c9675093b11c0ff9a931c4830845 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 22:26:27 +0200 Subject: [PATCH 54/85] hicli/sync: fix processing space events --- pkg/hicli/database/space.go | 24 +++++++++++++++++++----- pkg/hicli/sync.go | 7 +++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/pkg/hicli/database/space.go b/pkg/hicli/database/space.go index b0b86ef..7af1563 100644 --- a/pkg/hicli/database/space.go +++ b/pkg/hicli/database/space.go @@ -69,13 +69,15 @@ const ( revalidateAllParentsOfRoomQuery = revalidateAllParents + ` AND child_id=$1` revalidateSpecificParentQuery = revalidateAllParents + ` AND space_id=$1 AND child_id=$2` clearSpaceChildrenQuery = ` - UPDATE space_edge SET child_event_rowid=NULL, "order"=NULL, suggested=false + UPDATE space_edge SET child_event_rowid=NULL, "order"='', suggested=false WHERE space_id=$1 ` clearSpaceParentsQuery = ` UPDATE space_edge SET parent_event_rowid=NULL, canonical=false, parent_validated=false WHERE child_id=$1 ` + removeSpaceChildQuery = clearSpaceChildrenQuery + ` AND child_id=$2` + removeSpaceParentQuery = clearSpaceParentsQuery + ` AND space_id=$2` deleteEmptySpaceEdgeRowsQuery = ` DELETE FROM space_edge WHERE child_event_rowid IS NULL AND parent_event_rowid IS NULL ` @@ -139,8 +141,13 @@ func (seq *SpaceEdgeQuery) SetChildren(ctx context.Context, spaceID id.RoomID, c if err != nil { return err } - } else { - + } else if len(removedChildren) > 0 { + for _, child := range removedChildren { + err := seq.Exec(ctx, removeSpaceChildQuery, spaceID, child) + if err != nil { + return err + } + } } if len(removedChildren) > 0 { err := seq.Exec(ctx, deleteEmptySpaceEdgeRowsQuery, spaceID) @@ -152,7 +159,7 @@ func (seq *SpaceEdgeQuery) SetChildren(ctx context.Context, spaceID id.RoomID, c return nil } query, params := massInsertSpaceChildBuilder.Build([1]any{spaceID}, children) - return seq.Exec(ctx, query, params) + return seq.Exec(ctx, query, params...) } func (seq *SpaceEdgeQuery) SetParents(ctx context.Context, childID id.RoomID, parents []SpaceParentEntry, removedParents []id.RoomID, clear bool) error { @@ -161,6 +168,13 @@ func (seq *SpaceEdgeQuery) SetParents(ctx context.Context, childID id.RoomID, pa if err != nil { return err } + } else if len(removedParents) > 0 { + for _, parent := range removedParents { + err := seq.Exec(ctx, removeSpaceParentQuery, childID, parent) + if err != nil { + return err + } + } } if len(removedParents) > 0 { err := seq.Exec(ctx, deleteEmptySpaceEdgeRowsQuery) @@ -172,7 +186,7 @@ func (seq *SpaceEdgeQuery) SetParents(ctx context.Context, childID id.RoomID, pa return nil } query, params := massInsertSpaceParentBuilder.Build([1]any{childID}, parents) - return seq.Exec(ctx, query, params) + return seq.Exec(ctx, query, params...) } func (seq *SpaceEdgeQuery) RevalidateAllChildrenOfParentValidity(ctx context.Context, spaceID id.RoomID) error { diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index a591e91..4813f1b 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -1131,11 +1131,14 @@ func processImportantEvent( } switch evt.Type { case event.StateCreate, event.StateTombstone, event.StateRoomName, event.StateCanonicalAlias, - event.StateRoomAvatar, event.StateTopic, event.StateEncryption, - event.StateSpaceChild, event.StateSpaceParent, event.StatePowerLevels: + event.StateRoomAvatar, event.StateTopic, event.StateEncryption, event.StatePowerLevels: if *evt.StateKey != "" { return } + case event.StateSpaceChild, event.StateSpaceParent: + if !strings.HasPrefix(*evt.StateKey, "!") { + return + } default: return } From d82f5404ec6d67788f3678d95a246a8d8888cebe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 22:27:13 +0200 Subject: [PATCH 55/85] web/roomlist: fix unread count positioning --- web/src/ui/roomlist/RoomList.css | 6 +++++- web/src/ui/roomlist/UnreadCount.tsx | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index cc83edf..f7f8e33 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -55,11 +55,15 @@ div.space-bar { > div.room-entry-unreads { z-index: 2; height: 0; + width: 0; + margin-left: auto; position: relative; > div.unread-count { position: absolute; - top: 0; + /* This positioning doesn't feel very precise, but it looks correct enough */ + margin-top: .75rem; + margin-right: .25rem; } } } diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index 1c4f9c1..c563ae3 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -34,7 +34,8 @@ const UnreadCount = ({ counts, space }: UnreadCountProps) => { if (!unreadCount && !counts.marked_unread) { return null } - const countIsBig = !space && Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread) + const countIsBig = !space + && Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread) let unreadCountDisplay = unreadCount.toString() if (unreadCount > 999 && countIsBig) { unreadCountDisplay = "99+" From c44ab253f8d11497fee47d39f671b9b5d5fc1332 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 22:40:14 +0200 Subject: [PATCH 56/85] web/timeline: fix rendering replies to unknown events in compact style --- web/src/ui/timeline/ReplyBody.css | 1 + web/src/ui/timeline/ReplyBody.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/ui/timeline/ReplyBody.css b/web/src/ui/timeline/ReplyBody.css index d6f96c4..391b456 100644 --- a/web/src/ui/timeline/ReplyBody.css +++ b/web/src/ui/timeline/ReplyBody.css @@ -3,6 +3,7 @@ blockquote.reply-body { border-left: 2px solid var(--reply-border-color); padding: .25rem .5rem; + &.sender-color-null { --reply-border-color: var(--blockquote-border-color); } &.sender-color-0 { --reply-border-color: var(--sender-color-0); } &.sender-color-1 { --reply-border-color: var(--sender-color-1); } &.sender-color-2 { --reply-border-color: var(--sender-color-2); } diff --git a/web/src/ui/timeline/ReplyBody.tsx b/web/src/ui/timeline/ReplyBody.tsx index bf7dc36..1788baa 100644 --- a/web/src/ui/timeline/ReplyBody.tsx +++ b/web/src/ui/timeline/ReplyBody.tsx @@ -53,8 +53,11 @@ export const ReplyIDBody = ({ room, eventID, isThread, small }: ReplyIDBodyProps if (!event) { // This caches whether the event is requested or not, so it doesn't need to be wrapped in an effect. use(ClientContext)!.requestEvent(room, eventID) - return
- Reply to unknown event
{eventID} + return
+ {small &&
} + Reply to unknown event + {!small &&
} + {eventID}
} return From c9807df660c39d0228cdce170efc23512425205e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 22:57:15 +0200 Subject: [PATCH 57/85] web/roomlist: jump to first unread when clicking space unread counter Fixes #577 --- web/src/ui/roomlist/FakeSpace.tsx | 6 +++-- web/src/ui/roomlist/RoomList.tsx | 36 ++++++++++++++++++++++++++++- web/src/ui/roomlist/Space.tsx | 5 ++-- web/src/ui/roomlist/UnreadCount.tsx | 17 +++++++------- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/web/src/ui/roomlist/FakeSpace.tsx b/web/src/ui/roomlist/FakeSpace.tsx index f8c595a..00ac3a7 100644 --- a/web/src/ui/roomlist/FakeSpace.tsx +++ b/web/src/ui/roomlist/FakeSpace.tsx @@ -27,6 +27,7 @@ export interface FakeSpaceProps { space: Space | null setSpace: (space: RoomListFilter | null) => void isActive: boolean + onClickUnread?: (evt: React.MouseEvent | null, space: Space | null) => void } const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => { @@ -44,10 +45,11 @@ const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => { } } -const FakeSpace = ({ space, setSpace, isActive }: FakeSpaceProps) => { +const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => { const unreads = useEventAsState(space?.counts) + const onClickUnreadWrapped = onClickUnread ? () => onClickUnread(null, space) : undefined return
setSpace(space)}> - + {getFakeSpaceIcon(space)}
} diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 9b6ace3..04aff8d 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { use, useCallback, useRef, useState } from "react" -import { RoomListFilter } from "@/api/statestore/space.ts" +import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore/space.ts" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -54,6 +54,38 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) setSpace(store) }, [setSpace, client]) + const onClickSpaceUnread = useCallback(( + evt: React.MouseEvent | null, space?: SpaceStore | null, + ) => { + if (evt) { + const targetSpace = evt.currentTarget.closest("div.space-entry")?.getAttribute("data-target-space") + if (!targetSpace) { + return + } + space = client.store.getSpaceStore(targetSpace) + } + if (!space) { + return + } + const counts = space.counts.current + let wantedField: keyof SpaceUnreadCounts + if (counts.unread_highlights > 0) { + wantedField = "unread_highlights" + } else if (counts.unread_notifications > 0) { + wantedField = "unread_notifications" + } else if (counts.unread_messages > 0) { + wantedField = "unread_messages" + } else { + return + } + for (let i = client.store.roomList.current.length - 1; i >= 0; i--) { + const entry = client.store.roomList.current[i] + if (entry[wantedField] > 0 && space.include(entry)) { + mainScreen.setActiveRoom(entry.room_id) + break + } + } + }, [mainScreen, client]) const clearQuery = () => { client.store.currentRoomListQuery = "" directSetQuery("") @@ -97,6 +129,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { key={pseudoSpace.id} space={pseudoSpace} setSpace={setSpace} + onClickUnread={onClickSpaceUnread} isActive={space?.id === pseudoSpace.id} />)} {spaces.map(roomID => { client={client} onClick={onClickSpace} isActive={space?.id === roomID} + onClickUnread={onClickSpaceUnread} />)}
diff --git a/web/src/ui/roomlist/Space.tsx b/web/src/ui/roomlist/Space.tsx index 579915b..daeb35c 100644 --- a/web/src/ui/roomlist/Space.tsx +++ b/web/src/ui/roomlist/Space.tsx @@ -25,17 +25,18 @@ export interface SpaceProps { roomID: RoomID client: Client onClick: (evt: React.MouseEvent) => void + onClickUnread: (evt: React.MouseEvent) => void isActive: boolean } -const Space = ({ roomID, client, onClick, isActive }: SpaceProps) => { +const Space = ({ roomID, client, onClick, isActive, onClickUnread }: SpaceProps) => { const unreads = useEventAsState(client.store.spaceEdges.get(roomID)?.counts) const room = useEventAsState(client.store.rooms.get(roomID)?.meta) if (!room) { return } return
- + {room.name}
} diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index c563ae3..82807c0 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -22,9 +22,10 @@ interface UnreadCounts extends SpaceUnreadCounts { interface UnreadCountProps { counts: UnreadCounts | null space?: true + onClick?: (evt: React.MouseEvent) => void } -const UnreadCount = ({ counts, space }: UnreadCountProps) => { +const UnreadCount = ({ counts, space, onClick }: UnreadCountProps) => { if (!counts) { return null } @@ -46,15 +47,15 @@ const UnreadCount = ({ counts, space }: UnreadCountProps) => { if (countIsBig) { classNames.push("big") } - let unreadCountTitle = unreadCount.toString() if (space) { classNames.push("space") - unreadCountTitle = [ - counts.unread_highlights && `${counts.unread_highlights} highlights`, - counts.unread_notifications && `${counts.unread_notifications} notifications`, - counts.unread_messages && `${counts.unread_messages} messages`, - ].filter(x => !!x).join("\n") } + const unreadCountTitle = [ + counts.unread_highlights && `${counts.unread_highlights} highlights`, + counts.unread_notifications && `${counts.unread_notifications} notifications`, + counts.unread_messages && `${counts.unread_messages} messages`, + counts.marked_unread && "Marked unread", + ].filter(x => !!x).join("\n") if (counts.marked_unread) { classNames.push("marked-unread") } @@ -65,7 +66,7 @@ const UnreadCount = ({ counts, space }: UnreadCountProps) => { classNames.push("highlighted") } return
-
+
{unreadCountDisplay}
From f9b5fcc863b7496837617841e609fb027dafd560 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 23:29:34 +0200 Subject: [PATCH 58/85] hicli/database: fix space parent revalidation query --- pkg/hicli/database/space.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/hicli/database/space.go b/pkg/hicli/database/space.go index 7af1563..3e801e1 100644 --- a/pkg/hicli/database/space.go +++ b/pkg/hicli/database/space.go @@ -52,7 +52,7 @@ const ( AND COALESCE( ( SELECT value - FROM json_each(pls.content, 'users') + FROM json_each(pls.content, '$.users') WHERE key=edgeevt.sender AND type='integer' ), pls.content->>'$.users_default', From 7afcc37326a21584211418b2cb1e3decc7ea1ebe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 30 Dec 2024 23:29:42 +0200 Subject: [PATCH 59/85] web/timeline: add missing dependency to effect --- web/src/ui/timeline/TimelineView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/timeline/TimelineView.tsx b/web/src/ui/timeline/TimelineView.tsx index 2c896a6..2148258 100644 --- a/web/src/ui/timeline/TimelineView.tsx +++ b/web/src/ui/timeline/TimelineView.tsx @@ -71,7 +71,7 @@ const TimelineView = () => { }, [client.userID, roomCtx, timeline]) useEffect(() => { roomCtx.directSetFocusedEventRowID = directSetFocusedEventRowID - }, []) + }, [roomCtx]) useEffect(() => { const newestEvent = timeline[timeline.length - 1] if ( From d0c35dda756cdd2473eee828e0c304105b788b42 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 31 Dec 2024 13:51:21 +0200 Subject: [PATCH 60/85] web/roomlist: close room when switching space --- web/src/api/statestore/main.ts | 2 +- web/src/api/statestore/space.ts | 7 ++++++- web/src/ui/roomlist/RoomList.tsx | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 50b07d1..84ad5ff 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -77,7 +77,7 @@ export class StateStore { readonly spaceEdges: Map = new Map() readonly spaceOrphans = new SpaceOrphansSpace(this) readonly directChatsSpace = new DirectChatSpace() - readonly unreadsSpace = new UnreadsSpace() + readonly unreadsSpace = new UnreadsSpace(this) readonly pseudoSpaces = [ this.spaceOrphans, this.directChatsSpace, diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts index 8e7eb02..f54cc4c 100644 --- a/web/src/api/statestore/space.ts +++ b/web/src/api/statestore/space.ts @@ -79,8 +79,13 @@ export class DirectChatSpace extends Space { export class UnreadsSpace extends Space { id = "fi.mau.gomuks.unreads" + constructor(private parent: StateStore) { + super() + } + include(room: RoomListEntry): boolean { - return Boolean(room.unread_messages + return Boolean(room.room_id === this.parent.activeRoomID + || room.unread_messages || room.unread_notifications || room.unread_highlights || room.marked_unread) diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 04aff8d..4353524 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -49,7 +49,13 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { const setSpace = useCallback((space: RoomListFilter | null) => { directSetSpace(space) client.store.currentRoomListFilter = space - }, [client]) + if (client.store.activeRoomID && space) { + const entry = client.store.rooms.get(client.store.activeRoomID)?.roomListEntry + if (entry && !space.include(entry)) { + mainScreen.setActiveRoom(null) + } + } + }, [client, mainScreen]) const onClickSpace = useCallback((evt: React.MouseEvent) => { const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) setSpace(store) From 43f25727e64c68c82f11b8d28cabbf8d92279d27 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 31 Dec 2024 13:51:59 +0200 Subject: [PATCH 61/85] web/roomlist: add title for pseudo-spaces --- web/src/ui/roomlist/FakeSpace.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web/src/ui/roomlist/FakeSpace.tsx b/web/src/ui/roomlist/FakeSpace.tsx index 00ac3a7..59120e8 100644 --- a/web/src/ui/roomlist/FakeSpace.tsx +++ b/web/src/ui/roomlist/FakeSpace.tsx @@ -30,27 +30,28 @@ export interface FakeSpaceProps { onClickUnread?: (evt: React.MouseEvent | null, space: Space | null) => void } -const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => { +const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JSX.Element | null] => { switch (space?.id) { case undefined: - return + return ["Home", ] case "fi.mau.gomuks.direct_chats": - return + return ["Direct chats", ] case "fi.mau.gomuks.unreads": - return + return ["Unread chats", ] case "fi.mau.gomuks.space_orphans": - return + return ["Rooms outside spaces", ] default: - return null + return [undefined, null] } } const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => { const unreads = useEventAsState(space?.counts) const onClickUnreadWrapped = onClickUnread ? () => onClickUnread(null, space) : undefined - return
setSpace(space)}> + const [title, icon] = getFakeSpaceMeta(space) + return
setSpace(space)} title={title}> - {getFakeSpaceIcon(space)} + {icon}
} From 59e1b760d6f69d928a41db715c211bf19ec6691d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 01:22:33 +0200 Subject: [PATCH 62/85] web/statestore: fix clearing unread count after accepting invite --- web/src/api/statestore/main.ts | 15 ++++++++++----- web/src/api/statestore/room.ts | 3 +-- web/src/ui/roomlist/RoomList.tsx | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 84ad5ff..3274b5e 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -73,6 +73,7 @@ export class StateStore { readonly rooms: Map = new Map() readonly inviteRooms: Map = new Map() readonly roomList = new NonNullCachedEventDispatcher([]) + readonly roomListEntries = new Map() readonly topLevelSpaces = new NonNullCachedEventDispatcher([]) readonly spaceEdges: Map = new Map() readonly spaceOrphans = new SpaceOrphansSpace(this) @@ -212,11 +213,11 @@ export class StateStore { const changedRoomListEntries = new Map() for (const data of sync.invited_rooms ?? []) { const room = new InvitedRoomStore(data, this) - const oldEntry = this.inviteRooms.get(room.room_id) this.inviteRooms.set(room.room_id, room) if (!resyncRoomList) { changedRoomListEntries.set(room.room_id, room) - this.#applyUnreadModification(room, oldEntry) + this.#applyUnreadModification(room, this.roomListEntries.get(room.room_id)) + this.roomListEntries.set(room.room_id, room) } if (this.activeRoomID === room.room_id) { this.switchRoom?.(room.room_id) @@ -239,8 +240,12 @@ export class StateStore { if (roomListEntryChanged) { const entry = this.#makeRoomListEntry(data, room) changedRoomListEntries.set(roomID, entry) - this.#applyUnreadModification(entry, room.roomListEntry) - room.roomListEntry = entry + this.#applyUnreadModification(entry, this.roomListEntries.get(roomID)) + if (entry) { + this.roomListEntries.set(roomID, entry) + } else { + this.roomListEntries.delete(roomID) + } } if (!resyncRoomList) { // When we join a valid replacement room, hide the tombstoned room. @@ -289,7 +294,7 @@ export class StateStore { updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp) for (const entry of updatedRoomList) { this.#applyUnreadModification(entry, undefined) - this.rooms.get(entry.room_id)!.roomListEntry = entry + this.roomListEntries.set(entry.room_id, entry) } } else if (changedRoomListEntries.size > 0) { updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id)) diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 73903a8..7419a55 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -42,7 +42,7 @@ import { UserID, roomStateGUIDToString, } from "../types" -import type { RoomListEntry, StateStore } from "./main.ts" +import type { StateStore } from "./main.ts" function arraysAreEqual(arr1?: T[], arr2?: T[]): boolean { if (!arr1 || !arr2) { @@ -126,7 +126,6 @@ export class RoomStateStore { readUpToRow = -1 hasMoreHistory = true hidden = false - roomListEntry: RoomListEntry | undefined | null constructor(meta: DBRoom, private parent: StateStore) { this.roomID = meta.room_id diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 4353524..2cbd9be 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -50,7 +50,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { directSetSpace(space) client.store.currentRoomListFilter = space if (client.store.activeRoomID && space) { - const entry = client.store.rooms.get(client.store.activeRoomID)?.roomListEntry + const entry = client.store.roomListEntries.get(client.store.activeRoomID) if (entry && !space.include(entry)) { mainScreen.setActiveRoom(null) } From 8b7d0fe6b6dc9ec92cc7ac1c39411e773a2d3d97 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 12:21:10 +0200 Subject: [PATCH 63/85] web/roomlist: restore open space when using browser history --- web/src/api/statestore/index.ts | 1 + web/src/api/statestore/main.ts | 17 ++++++++++++++ web/src/ui/MainScreen.tsx | 35 ++++++++++++++++++++++++----- web/src/ui/MainScreenContext.ts | 5 +++++ web/src/ui/roomlist/RoomList.tsx | 24 ++++++-------------- web/src/ui/roomlist/UnreadCount.tsx | 2 +- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/web/src/api/statestore/index.ts b/web/src/api/statestore/index.ts index 3bbe512..106a3f4 100644 --- a/web/src/api/statestore/index.ts +++ b/web/src/api/statestore/index.ts @@ -1,3 +1,4 @@ export * from "./main.ts" export * from "./room.ts" export * from "./hooks.ts" +export * from "./space.ts" diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 3274b5e..78dadf0 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -111,6 +111,23 @@ export class StateStore { return true } + getSpaceByID(spaceID: string | undefined): RoomListFilter | null { + if (!spaceID) { + return null + } + const realSpace = this.spaceEdges.get(spaceID) + if (realSpace) { + return realSpace + } + for (const pseudoSpace of this.pseudoSpaces) { + if (pseudoSpace.id === spaceID) { + return pseudoSpace + } + } + console.warn("Failed to find space", spaceID) + return null + } + get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null { if (!this.currentRoomListFilter && !this.currentRoomListQuery) { return null diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 05f8b59..c735548 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -16,7 +16,7 @@ import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react" import { SyncLoader } from "react-spinners" import Client from "@/api/client.ts" -import { RoomStateStore } from "@/api/statestore" +import { RoomListFilter, RoomStateStore } from "@/api/statestore" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts" @@ -52,6 +52,7 @@ class ContextFields implements MainScreenContextFields { constructor( private directSetRightPanel: (props: RightPanelProps | null) => void, private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void, + private directSetSpace: (space: RoomListFilter | null) => void, private client: Client, ) { this.keybindings = new Keybindings(client.store, this) @@ -109,6 +110,24 @@ class ContextFields implements MainScreenContextFields { } } + setSpace = (space: RoomListFilter | null, pushState = true) => { + if (space === this.client.store.currentRoomListFilter) { + return + } + console.log("Switching to space", space?.id) + this.directSetSpace(space) + this.client.store.currentRoomListFilter = space + if (pushState) { + if (this.client.store.activeRoomID && space) { + const entry = this.client.store.roomListEntries.get(this.client.store.activeRoomID) + if (entry && !space.include(entry)) { + this.setActiveRoom(null) + } + } + history.replaceState({ ...(history.state || {}), space_id: space?.id }, "") + } + } + #setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial) { const invite = this.client.store.inviteRooms.get(roomID) this.#closeActiveRoom(false) @@ -120,6 +139,7 @@ class ContextFields implements MainScreenContextFields { room_id: roomID, source_via: meta?.via, source_alias: meta?.alias, + space_id: history.state.space_id, }, "") } } @@ -148,7 +168,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID }, "") + history.pushState({ room_id: room.roomID, space_id: history.state.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -166,7 +186,7 @@ class ContextFields implements MainScreenContextFields { this.client.store.activeRoomIsPreview = false this.keybindings.activeRoom = null if (pushState) { - history.pushState({}, "") + history.pushState({ space_id: history.state.space_id }, "") } document.title = this.#getWindowTitle() } @@ -279,12 +299,13 @@ const activeRoomReducer = ( const MainScreen = () => { const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null]) + const [space, directSetSpace] = useState(null) const skipNextTransitionRef = useRef(false) const [rightPanel, directSetRightPanel] = useState(null) const client = use(ClientContext)! const syncStatus = useEventAsState(client.syncStatus) const context = useMemo( - () => new ContextFields(directSetRightPanel, directSetActiveRoom, client), + () => new ContextFields(directSetRightPanel, directSetActiveRoom, directSetSpace, client), [client], ) useEffect(() => { @@ -292,6 +313,10 @@ const MainScreen = () => { const listener = (evt: PopStateEvent) => { skipNextTransitionRef.current = evt.hasUAVisualTransition const roomID = evt.state?.room_id ?? null + const spaceID = evt.state?.space_id ?? undefined + if (spaceID !== client.store.currentRoomListFilter?.id) { + context.setSpace(client.store.getSpaceByID(spaceID), false) + } if (roomID !== client.store.activeRoomID) { context.setActiveRoom(roomID, { alias: ensureString(evt.state?.source_alias) || undefined, @@ -372,7 +397,7 @@ const MainScreen = () => {
- + {resizeHandle1} {renderedRoom ? renderedRoom instanceof RoomStateStore diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts index 67c0b0b..28aa97d 100644 --- a/web/src/ui/MainScreenContext.ts +++ b/web/src/ui/MainScreenContext.ts @@ -14,12 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { createContext } from "react" +import { RoomListFilter } from "@/api/statestore" import type { RoomID } from "@/api/types" import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx" export interface MainScreenContextFields { setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial) => void + setSpace: (space: RoomListFilter | null, pushState?: boolean) => void clickRoom: (evt: React.MouseEvent) => void clearActiveRoom: () => void @@ -32,6 +34,9 @@ const stubContext = { get setActiveRoom(): never { throw new Error("MainScreenContext used outside main screen") }, + get setSpace(): never { + throw new Error("MainScreenContext used outside main screen") + }, get clickRoom(): never { throw new Error("MainScreenContext used outside main screen") }, diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 2cbd9be..b4c0b78 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { use, useCallback, useRef, useState } from "react" -import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore/space.ts" +import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -31,35 +31,25 @@ import "./RoomList.css" interface RoomListProps { activeRoomID: RoomID | null + space: RoomListFilter | null } -const RoomList = ({ activeRoomID }: RoomListProps) => { +const RoomList = ({ activeRoomID, space }: RoomListProps) => { const client = use(ClientContext)! const mainScreen = use(MainScreenContext) const roomList = useEventAsState(client.store.roomList) const spaces = useEventAsState(client.store.topLevelSpaces) const searchInputRef = useRef(null) const [query, directSetQuery] = useState("") - const [space, directSetSpace] = useState(null) const setQuery = (evt: React.ChangeEvent) => { client.store.currentRoomListQuery = toSearchableString(evt.target.value) directSetQuery(evt.target.value) } - const setSpace = useCallback((space: RoomListFilter | null) => { - directSetSpace(space) - client.store.currentRoomListFilter = space - if (client.store.activeRoomID && space) { - const entry = client.store.roomListEntries.get(client.store.activeRoomID) - if (entry && !space.include(entry)) { - mainScreen.setActiveRoom(null) - } - } - }, [client, mainScreen]) const onClickSpace = useCallback((evt: React.MouseEvent) => { const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) - setSpace(store) - }, [setSpace, client]) + mainScreen.setSpace(store) + }, [mainScreen, client]) const onClickSpaceUnread = useCallback(( evt: React.MouseEvent | null, space?: SpaceStore | null, ) => { @@ -130,11 +120,11 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
- + {client.store.pseudoSpaces.map(pseudoSpace => )} diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index 82807c0..b1fb141 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { SpaceUnreadCounts } from "@/api/statestore/space.ts" +import { SpaceUnreadCounts } from "@/api/statestore" interface UnreadCounts extends SpaceUnreadCounts { marked_unread?: boolean From 6d1c5f6277395e5fa2c463b5e200ca99e4439820 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 13:11:31 +0200 Subject: [PATCH 64/85] web/roomlist: use margin instead of padding for room avatars This allows adding a background for avatars using ```css .avatar { background-color: var(--background-color); } ``` --- web/src/ui/roomlist/RoomList.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index f7f8e33..6241a65 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -119,7 +119,7 @@ div.room-entry { width: 3rem; > img.room-avatar { - padding: 4px; + margin: .25rem; } } From ddf20b34d26bbb17aecd611561f78ba3505ac44f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 15:44:44 +0200 Subject: [PATCH 65/85] web/mainscreen: fix pushing history states when outside a space --- web/src/ui/MainScreen.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index c735548..3bb15e6 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -139,7 +139,7 @@ class ContextFields implements MainScreenContextFields { room_id: roomID, source_via: meta?.via, source_alias: meta?.alias, - space_id: history.state.space_id, + space_id: history.state?.space_id, }, "") } } @@ -168,7 +168,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID, space_id: history.state.space_id }, "") + history.pushState({ room_id: room.roomID, space_id: history.state?.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -186,7 +186,7 @@ class ContextFields implements MainScreenContextFields { this.client.store.activeRoomIsPreview = false this.keybindings.activeRoom = null if (pushState) { - history.pushState({ space_id: history.state.space_id }, "") + history.pushState({ space_id: history.state?.space_id }, "") } document.title = this.#getWindowTitle() } From 8c9925959afc261ba2ff9890003640e2bae75583 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 15:47:08 +0200 Subject: [PATCH 66/85] web/roomlist: don't allow selecting unread counter text --- web/src/ui/roomlist/RoomList.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index 6241a65..0d084d2 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -172,6 +172,7 @@ div.room-entry-unreads { justify-content: center; border-radius: var(--unread-count-size); color: var(--unread-counter-text-color); + user-select: none; background-color: var(--unread-counter-message-bg); height: var(--unread-count-size); From 7f94bbf39ea5cec471b4a36228e27d70372bb508 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 15:51:15 +0200 Subject: [PATCH 67/85] web/timeline: add background for read receipt avatars --- web/src/ui/timeline/ReadReceipts.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/timeline/ReadReceipts.css b/web/src/ui/timeline/ReadReceipts.css index 7e8579d..df23657 100644 --- a/web/src/ui/timeline/ReadReceipts.css +++ b/web/src/ui/timeline/ReadReceipts.css @@ -20,6 +20,7 @@ div.timeline-event > div.read-receipts { > img { margin-left: -.35rem; border: 1px solid var(--background-color); + background-color: var(--background-color); } } From c3899d0b50dddeda9bbb18a642ae5e4abcca40b5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 18:07:57 +0200 Subject: [PATCH 68/85] web/settings: add button to log into css.gomuks.app --- desktop/go.mod | 2 +- desktop/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- pkg/hicli/json-commands.go | 2 ++ web/src/api/rpc.ts | 5 +++++ web/src/api/types/mxtypes.ts | 7 +++++++ web/src/ui/settings/SettingsView.tsx | 11 +++++++++++ 8 files changed, 31 insertions(+), 6 deletions(-) diff --git a/desktop/go.mod b/desktop/go.mod index 70137e5..8291c8f 100644 --- a/desktop/go.mod +++ b/desktop/go.mod @@ -75,7 +75,7 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 // indirect + maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c // indirect mvdan.cc/xurls/v2 v2.5.0 // indirect ) diff --git a/desktop/go.sum b/desktop/go.sum index ef86ad9..c04dd75 100644 --- a/desktop/go.sum +++ b/desktop/go.sum @@ -250,7 +250,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 h1:3aZCApUF3wlboN1BcAX6u6MGhju4OpsmhyfvxdO6tO0= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/go.mod b/go.mod index bc8de98..a7347ed 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.org/x/text v0.21.0 gopkg.in/yaml.v3 v3.0.1 maunium.net/go/mauflag v1.0.0 - maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 + maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c mvdan.cc/xurls/v2 v2.5.0 ) diff --git a/go.sum b/go.sum index 1897c4d..51e82a9 100644 --- a/go.sum +++ b/go.sum @@ -91,7 +91,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 h1:3aZCApUF3wlboN1BcAX6u6MGhju4OpsmhyfvxdO6tO0= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/pkg/hicli/json-commands.go b/pkg/hicli/json-commands.go index 0a07c25..26b8e89 100644 --- a/pkg/hicli/json-commands.go +++ b/pkg/hicli/json-commands.go @@ -153,6 +153,8 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) { return h.Client.ResolveAlias(ctx, params.Alias) }) + case "request_openid_token": + return h.Client.RequestOpenIDToken(ctx) case "logout": if h.LogoutFunc == nil { return nil, errors.New("logout not supported") diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index b967010..a52a3c3 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -32,6 +32,7 @@ import type { ReceiptType, RelatesTo, ResolveAliasResponse, + RespOpenIDToken, RespRoomJoin, RoomAlias, RoomID, @@ -257,4 +258,8 @@ export default abstract class RPCClient { verify(recovery_key: string): Promise { return this.request("verify", { recovery_key }) } + + requestOpenIDToken(): Promise { + return this.request("request_openid_token", {}) + } } diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 5932e3e..81261ea 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -279,3 +279,10 @@ export interface RoomSummary { export interface RespRoomJoin { room_id: RoomID } + +export interface RespOpenIDToken { + access_token: string + expires_in: number + matrix_server_name: string + token_type: "Bearer" +} diff --git a/web/src/ui/settings/SettingsView.tsx b/web/src/ui/settings/SettingsView.tsx index 6f9aefc..02c5ff3 100644 --- a/web/src/ui/settings/SettingsView.tsx +++ b/web/src/ui/settings/SettingsView.tsx @@ -332,6 +332,16 @@ const SettingsView = ({ room }: SettingsViewProps) => { ) } } + const onClickOpenCSSApp = () => { + client.rpc.requestOpenIDToken().then( + resp => window.open( + `https://css.gomuks.app/login?token=${resp.access_token}&server_name=${resp.matrix_server_name}`, + "_blank", + "noreferrer noopener", + ), + err => window.alert(`Failed to request OpenID token: ${err}`), + ) + } usePreferences(client.store, room) const globalServer = client.store.serverPreferenceCache const globalLocal = client.store.localPreferenceCache @@ -381,6 +391,7 @@ const SettingsView = ({ room }: SettingsViewProps) => {
+ {window.Notification && } From 021236592f3ac59d3814df1a38dc2429875851fd Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:07:04 +0200 Subject: [PATCH 69/85] web/statestore: clear unread counts when clearing state --- web/src/api/statestore/main.ts | 1 + web/src/api/statestore/space.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 78dadf0..ec1ecb5 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -537,6 +537,7 @@ export class StateStore { this.rooms.clear() this.inviteRooms.clear() this.spaceEdges.clear() + this.pseudoSpaces.forEach(space => space.clearUnreads()) this.roomList.emit([]) this.topLevelSpaces.emit([]) this.accountData.clear() diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts index f54cc4c..96b37b8 100644 --- a/web/src/api/statestore/space.ts +++ b/web/src/api/statestore/space.ts @@ -40,6 +40,10 @@ export abstract class Space implements RoomListFilter { abstract id: string abstract include(room: RoomListEntry): boolean + clearUnreads() { + this.counts.emit(emptyUnreadCounts) + } + applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) { const mergedCounts: SpaceUnreadCounts = { unread_messages: this.counts.current.unread_messages From 3c3e2456e26c4157d8f41547eae8eed0f06dec74 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:16:26 +0200 Subject: [PATCH 70/85] web/roomlist: switch space in unread click handler On desktop it worked without this via event propagation, but that doesn't work on mobile (possibly because the room view opens immediately and hides the space bar?) --- web/src/ui/roomlist/FakeSpace.tsx | 6 ++++-- web/src/ui/roomlist/RoomList.tsx | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/web/src/ui/roomlist/FakeSpace.tsx b/web/src/ui/roomlist/FakeSpace.tsx index 59120e8..bb46319 100644 --- a/web/src/ui/roomlist/FakeSpace.tsx +++ b/web/src/ui/roomlist/FakeSpace.tsx @@ -27,7 +27,7 @@ export interface FakeSpaceProps { space: Space | null setSpace: (space: RoomListFilter | null) => void isActive: boolean - onClickUnread?: (evt: React.MouseEvent | null, space: Space | null) => void + onClickUnread?: (evt: React.MouseEvent, space: Space | null) => void } const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JSX.Element | null] => { @@ -47,7 +47,9 @@ const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JS const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => { const unreads = useEventAsState(space?.counts) - const onClickUnreadWrapped = onClickUnread ? () => onClickUnread(null, space) : undefined + const onClickUnreadWrapped = onClickUnread + ? (evt: React.MouseEvent) => onClickUnread(evt, space) + : undefined const [title, icon] = getFakeSpaceMeta(space) return
setSpace(space)} title={title}> diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index b4c0b78..39d4cb5 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -51,17 +51,17 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => { mainScreen.setSpace(store) }, [mainScreen, client]) const onClickSpaceUnread = useCallback(( - evt: React.MouseEvent | null, space?: SpaceStore | null, + evt: React.MouseEvent, space?: SpaceStore | null, ) => { - if (evt) { + if (!space) { const targetSpace = evt.currentTarget.closest("div.space-entry")?.getAttribute("data-target-space") if (!targetSpace) { return } space = client.store.getSpaceStore(targetSpace) - } - if (!space) { - return + if (!space) { + return + } } const counts = space.counts.current let wantedField: keyof SpaceUnreadCounts @@ -78,6 +78,8 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => { const entry = client.store.roomList.current[i] if (entry[wantedField] > 0 && space.include(entry)) { mainScreen.setActiveRoom(entry.room_id) + mainScreen.setSpace(space) + evt.stopPropagation() break } } From d8f0a82ffc79b9c1706da5c68f33526ebe8d4706 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:17:22 +0200 Subject: [PATCH 71/85] web/roomlist: fix unread counter overflow condition --- web/src/ui/roomlist/UnreadCount.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index b1fb141..4ad48f5 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -38,9 +38,9 @@ const UnreadCount = ({ counts, space, onClick }: UnreadCountProps) => { const countIsBig = !space && Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread) let unreadCountDisplay = unreadCount.toString() - if (unreadCount > 999 && countIsBig) { + if (unreadCount > 999 && (countIsBig || space)) { unreadCountDisplay = "99+" - } else if (unreadCount > 9999 && countIsBig) { + } else if (unreadCount > 9999) { unreadCountDisplay = "999+" } const classNames = ["unread-count"] From ac6f2713e55e5d72df4547438ec1274e86eeeed5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:34:14 +0200 Subject: [PATCH 72/85] web/eslint: make line max length an error --- web/eslint.config.js | 2 +- web/src/ui/modal/Modal.tsx | 2 +- web/src/ui/settings/SettingsView.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/eslint.config.js b/web/eslint.config.js index 7654e8d..41ba85f 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -74,7 +74,7 @@ export default tseslint.config( "quotes": ["error", "double", {allowTemplateLiterals: true}], "semi": ["error", "never"], "comma-dangle": ["error", "always-multiline"], - "max-len": ["warn", 120], + "max-len": ["error", 120], "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", diff --git a/web/src/ui/modal/Modal.tsx b/web/src/ui/modal/Modal.tsx index 1fc3ab6..4d0baf9 100644 --- a/web/src/ui/modal/Modal.tsx +++ b/web/src/ui/modal/Modal.tsx @@ -60,7 +60,7 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => { } window.addEventListener("popstate", listener) return () => window.removeEventListener("popstate", listener) - }, []) + }, [onClickWrapper]) let modal: JSX.Element | null = null if (state) { let content = {state.content} diff --git a/web/src/ui/settings/SettingsView.tsx b/web/src/ui/settings/SettingsView.tsx index 02c5ff3..daf5c44 100644 --- a/web/src/ui/settings/SettingsView.tsx +++ b/web/src/ui/settings/SettingsView.tsx @@ -237,7 +237,9 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta }
{vscodeOpen ?
-
}> +
+ }> Date: Thu, 2 Jan 2025 23:18:00 +0200 Subject: [PATCH 73/85] hicli/database: store DM user ID in database --- pkg/hicli/database/room.go | 35 ++++++++++++------- .../database/upgrades/00-latest-revision.sql | 3 +- pkg/hicli/database/upgrades/11-dm-user-id.sql | 19 ++++++++++ pkg/hicli/sync.go | 33 ++++++++++++----- web/src/api/media.ts | 12 ++----- web/src/api/statestore/main.ts | 3 +- web/src/api/statestore/room.ts | 1 + web/src/api/types/hitypes.ts | 1 + web/src/ui/rightpanel/UserInfoMutualRooms.tsx | 3 +- 9 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 pkg/hicli/database/upgrades/11-dm-user-id.sql diff --git a/pkg/hicli/database/room.go b/pkg/hicli/database/room.go index a27de72..6e46001 100644 --- a/pkg/hicli/database/room.go +++ b/pkg/hicli/database/room.go @@ -21,7 +21,8 @@ import ( const ( getRoomBaseQuery = ` - SELECT room_id, creation_content, tombstone_content, name, name_quality, avatar, explicit_avatar, topic, canonical_alias, + SELECT room_id, creation_content, tombstone_content, name, name_quality, + avatar, explicit_avatar, dm_user_id, topic, canonical_alias, lazy_load_summary, encryption_event, has_member_list, preview_event_rowid, sorting_timestamp, unread_highlights, unread_notifications, unread_messages, marked_unread, prev_batch FROM room @@ -42,18 +43,19 @@ const ( name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END, avatar = COALESCE($6, room.avatar), explicit_avatar = CASE WHEN $6 IS NOT NULL THEN $7 ELSE room.explicit_avatar END, - topic = COALESCE($8, room.topic), - canonical_alias = COALESCE($9, room.canonical_alias), - lazy_load_summary = COALESCE($10, room.lazy_load_summary), - encryption_event = COALESCE($11, room.encryption_event), - has_member_list = room.has_member_list OR $12, - preview_event_rowid = COALESCE($13, room.preview_event_rowid), - sorting_timestamp = COALESCE($14, room.sorting_timestamp), - unread_highlights = COALESCE($15, room.unread_highlights), - unread_notifications = COALESCE($16, room.unread_notifications), - unread_messages = COALESCE($17, room.unread_messages), - marked_unread = COALESCE($18, room.marked_unread), - prev_batch = COALESCE($19, room.prev_batch) + dm_user_id = COALESCE($8, room.dm_user_id), + topic = COALESCE($9, room.topic), + canonical_alias = COALESCE($10, room.canonical_alias), + lazy_load_summary = COALESCE($11, room.lazy_load_summary), + encryption_event = COALESCE($12, room.encryption_event), + has_member_list = room.has_member_list OR $13, + preview_event_rowid = COALESCE($14, room.preview_event_rowid), + sorting_timestamp = COALESCE($15, room.sorting_timestamp), + unread_highlights = COALESCE($16, room.unread_highlights), + unread_notifications = COALESCE($17, room.unread_notifications), + unread_messages = COALESCE($18, room.unread_messages), + marked_unread = COALESCE($19, room.marked_unread), + prev_batch = COALESCE($20, room.prev_batch) WHERE room_id = $1 ` setRoomPrevBatchQuery = ` @@ -153,6 +155,7 @@ type Room struct { NameQuality NameQuality `json:"name_quality"` Avatar *id.ContentURI `json:"avatar,omitempty"` ExplicitAvatar bool `json:"explicit_avatar"` + DMUserID *id.UserID `json:"dm_user_id,omitempty"` Topic *string `json:"topic,omitempty"` CanonicalAlias *id.RoomAlias `json:"canonical_alias,omitempty"` @@ -188,6 +191,10 @@ func (r *Room) CheckChangesAndCopyInto(other *Room) (hasChanges bool) { other.ExplicitAvatar = r.ExplicitAvatar hasChanges = true } + if r.DMUserID != nil { + other.DMUserID = r.DMUserID + hasChanges = true + } if r.Topic != nil { other.Topic = r.Topic hasChanges = true @@ -250,6 +257,7 @@ func (r *Room) Scan(row dbutil.Scannable) (*Room, error) { &r.NameQuality, &r.Avatar, &r.ExplicitAvatar, + &r.DMUserID, &r.Topic, &r.CanonicalAlias, dbutil.JSON{Data: &r.LazyLoadSummary}, @@ -281,6 +289,7 @@ func (r *Room) sqlVariables() []any { r.NameQuality, r.Avatar, r.ExplicitAvatar, + r.DMUserID, r.Topic, r.CanonicalAlias, dbutil.JSONPtr(r.LazyLoadSummary), diff --git a/pkg/hicli/database/upgrades/00-latest-revision.sql b/pkg/hicli/database/upgrades/00-latest-revision.sql index 2df49d2..97f98d4 100644 --- a/pkg/hicli/database/upgrades/00-latest-revision.sql +++ b/pkg/hicli/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v10 (compatible with v10+): Latest revision +-- v0 -> v11 (compatible with v10+): Latest revision CREATE TABLE account ( user_id TEXT NOT NULL PRIMARY KEY, device_id TEXT NOT NULL, @@ -18,6 +18,7 @@ CREATE TABLE room ( name_quality INTEGER NOT NULL DEFAULT 0, avatar TEXT, explicit_avatar INTEGER NOT NULL DEFAULT 0, + dm_user_id TEXT, topic TEXT, canonical_alias TEXT, lazy_load_summary TEXT, diff --git a/pkg/hicli/database/upgrades/11-dm-user-id.sql b/pkg/hicli/database/upgrades/11-dm-user-id.sql new file mode 100644 index 0000000..3377f0c --- /dev/null +++ b/pkg/hicli/database/upgrades/11-dm-user-id.sql @@ -0,0 +1,19 @@ +-- v11 (compatible with v10+): Store direct chat user ID in database +ALTER TABLE room ADD COLUMN dm_user_id TEXT; +WITH dm_user_ids AS ( + SELECT room_id, value + FROM room + INNER JOIN json_each(lazy_load_summary, '$."m.heroes"') + WHERE value NOT IN (SELECT value FROM json_each(( + SELECT event.content + FROM current_state cs + INNER JOIN event ON cs.event_rowid = event.rowid + WHERE cs.room_id=room.room_id AND cs.event_type='io.element.functional_members' AND cs.state_key='' + ), '$.service_members')) + GROUP BY room_id + HAVING COUNT(*) = 1 +) +UPDATE room +SET dm_user_id=value +FROM dm_user_ids du +WHERE room.room_id=du.room_id; diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index 4813f1b..316da7e 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -894,10 +894,11 @@ func (h *HiClient) processStateAndTimeline( } // Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil { - name, dmAvatarURL, err := h.calculateRoomParticipantName(ctx, room.ID, summary) + name, dmAvatarURL, dmUserID, err := h.calculateRoomParticipantName(ctx, room.ID, summary) if err != nil { return fmt.Errorf("failed to calculate room name: %w", err) } + updatedRoom.DMUserID = &dmUserID updatedRoom.Name = &name updatedRoom.NameQuality = database.NameQualityParticipants if !dmAvatarURL.IsEmpty() && !room.ExplicitAvatar { @@ -966,15 +967,15 @@ func joinMemberNames(names []string, totalCount int) string { } } -func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, error) { +func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, id.UserID, error) { var primaryAvatarURL id.ContentURI if summary == nil || len(summary.Heroes) == 0 { - return "Empty room", primaryAvatarURL, nil + return "Empty room", primaryAvatarURL, "", nil } var functionalMembers []id.UserID functionalMembersEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateElementFunctionalMembers, "") if err != nil { - return "", primaryAvatarURL, fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err) + return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err) } else if functionalMembersEvt != nil { mautrixEvt := functionalMembersEvt.AsRawMautrix() _ = mautrixEvt.Content.ParseRaw(mautrixEvt.Type) @@ -990,16 +991,21 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R } else if summary.InvitedMemberCount != nil { memberCount = *summary.InvitedMemberCount } + var dmUserID id.UserID for _, hero := range summary.Heroes { if slices.Contains(functionalMembers, hero) { + // TODO save member count so push rule evaluation would use the subtracted one? memberCount-- continue } else if len(members) >= 5 { break } + if dmUserID == "" { + dmUserID = hero + } heroEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateMember, hero.String()) if err != nil { - return "", primaryAvatarURL, fmt.Errorf("failed to get %s's member event: %w", hero, err) + return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s's member event: %w", hero, err) } else if heroEvt == nil { leftMembers = append(leftMembers, hero.String()) continue @@ -1015,19 +1021,28 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R } if membership == "join" || membership == "invite" { members = append(members, name) + dmUserID = hero } else { leftMembers = append(leftMembers, name) } } - if len(members)+len(leftMembers) > 1 || !primaryAvatarURL.IsValid() { + if !primaryAvatarURL.IsValid() { primaryAvatarURL = id.ContentURI{} } if len(members) > 0 { - return joinMemberNames(members, memberCount), primaryAvatarURL, nil + if len(members) > 1 { + primaryAvatarURL = id.ContentURI{} + dmUserID = "" + } + return joinMemberNames(members, memberCount), primaryAvatarURL, dmUserID, nil } else if len(leftMembers) > 0 { - return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, nil + if len(leftMembers) > 1 { + primaryAvatarURL = id.ContentURI{} + dmUserID = "" + } + return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, "", nil } else { - return "Empty room", primaryAvatarURL, nil + return "Empty room", primaryAvatarURL, "", nil } } diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 33ad9ea..5e3180d 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { parseMXC } from "@/util/validation.ts" -import { ContentURI, LazyLoadSummary, RoomID, UserID, UserProfile } from "./types" +import { ContentURI, RoomID, UserID, UserProfile } from "./types" export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => { const [server, mediaID] = parseMXC(mxc) @@ -93,20 +93,12 @@ interface RoomForAvatarURL { room_id: RoomID name?: string dm_user_id?: UserID - lazy_load_summary?: LazyLoadSummary avatar?: ContentURI avatar_url?: ContentURI } export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => { - let dmUserID: UserID | undefined - if ("dm_user_id" in room) { - dmUserID = room.dm_user_id - } else if ("lazy_load_summary" in room) { - dmUserID = room.lazy_load_summary?.["m.heroes"]?.length === 1 - ? room.lazy_load_summary["m.heroes"][0] : undefined - } - return getAvatarURL(dmUserID ?? room.room_id, { + return getAvatarURL(room.dm_user_id ?? room.room_id, { displayname: room.name, avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url, }) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index ec1ecb5..e81ff2a 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -187,8 +187,7 @@ export class StateStore { const name = entry.meta.name ?? "Unnamed room" return { room_id: entry.meta.room_id, - dm_user_id: entry.meta.lazy_load_summary?.["m.heroes"]?.length === 1 - ? entry.meta.lazy_load_summary["m.heroes"][0] : undefined, + dm_user_id: entry.meta.dm_user_id, sorting_timestamp: entry.meta.sorting_timestamp, preview_event, preview_sender, diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 7419a55..5252a3c 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -70,6 +70,7 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean { meta1.avatar === meta2.avatar && meta1.topic === meta2.topic && meta1.canonical_alias === meta2.canonical_alias && + meta1.dm_user_id === meta2.dm_user_id && llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) && meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm && meta1.has_member_list === meta2.has_member_list diff --git a/web/src/api/types/hitypes.ts b/web/src/api/types/hitypes.ts index 521a9dd..637f38a 100644 --- a/web/src/api/types/hitypes.ts +++ b/web/src/api/types/hitypes.ts @@ -54,6 +54,7 @@ export interface DBRoom { name_quality: RoomNameQuality avatar?: ContentURI explicit_avatar: boolean + dm_user_id?: UserID topic?: string canonical_alias?: RoomAlias lazy_load_summary?: LazyLoadSummary diff --git a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx index fadd041..1a11b55 100644 --- a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx +++ b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx @@ -40,8 +40,7 @@ const MutualRooms = ({ client, userID }: MutualRoomsProps) => { } return { room_id: roomID, - dm_user_id: roomData.meta.current.lazy_load_summary?.["m.heroes"]?.length === 1 - ? roomData.meta.current.lazy_load_summary["m.heroes"][0] : undefined, + dm_user_id: roomData.meta.current.dm_user_id, name: roomData.meta.current.name ?? "Unnamed room", avatar: roomData.meta.current.avatar, search_name: "", From 5d25d839f8c89e85b7917746fcf0ac23614187af Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 3 Jan 2025 12:12:33 +0200 Subject: [PATCH 74/85] web: switch to first matching space when opening room Fixes #582 --- web/src/api/statestore/main.ts | 18 +++++++++++++++++- web/src/ui/MainScreen.tsx | 32 ++++++++++++++++++++++++-------- web/src/ui/MainScreenContext.ts | 4 ++-- web/src/ui/roomlist/RoomList.tsx | 3 +-- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index e81ff2a..31fe657 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -39,7 +39,7 @@ import { } from "../types" import { InvitedRoomStore } from "./invitedroom.ts" import { RoomStateStore } from "./room.ts" -import { DirectChatSpace, RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts" +import { DirectChatSpace, RoomListFilter, Space, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts" export interface RoomListEntry { room_id: RoomID @@ -128,6 +128,22 @@ export class StateStore { return null } + findMatchingSpace(room: RoomListEntry): Space | null { + if (this.spaceOrphans.include(room)) { + return this.spaceOrphans + } + for (const spaceID of this.topLevelSpaces.current) { + const space = this.spaceEdges.get(spaceID) + if (space?.include(room)) { + return space + } + } + if (this.directChatsSpace.include(room)) { + return this.directChatsSpace + } + return null + } + get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null { if (!this.currentRoomListFilter && !this.currentRoomListQuery) { return null diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 3bb15e6..e3220a6 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -96,12 +96,17 @@ class ContextFields implements MainScreenContextFields { } } - setActiveRoom = (roomID: RoomID | null, previewMeta?: Partial, pushState = true) => { + setActiveRoom = ( + roomID: RoomID | null, + previewMeta?: Partial, + toSpace?: RoomListFilter, + pushState = true, + ) => { console.log("Switching to room", roomID) if (roomID) { const room = this.client.store.rooms.get(roomID) if (room) { - this.#setActiveRoom(room, pushState) + this.#setActiveRoom(room, toSpace, pushState) } else { this.#setPreviewRoom(roomID, pushState, previewMeta) } @@ -151,10 +156,21 @@ class ContextFields implements MainScreenContextFields { return room.preferences.room_window_title.replace("$room", name!) } - #setActiveRoom(room: RoomStateStore, pushState: boolean) { + #setActiveRoom(room: RoomStateStore, space: RoomListFilter | undefined | null, pushState: boolean) { window.activeRoom = room this.directSetActiveRoom(room) this.directSetRightPanel(null) + if (!space && this.client.store.currentRoomListFilter) { + const roomListEntry = this.client.store.roomListEntries.get(room.roomID) + if (roomListEntry && !this.client.store.currentRoomListFilter.include(roomListEntry)) { + space = this.client.store.findMatchingSpace(roomListEntry) + } + } + if (space && space !== this.client.store.currentRoomListFilter) { + console.log("Switching to space", space?.id) + this.directSetSpace(space) + this.client.store.currentRoomListFilter = space + } this.rightPanelStack = [] this.client.store.activeRoomID = room.roomID this.client.store.activeRoomIsPreview = false @@ -168,7 +184,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID, space_id: history.state?.space_id }, "") + history.pushState({ room_id: room.roomID, space_id: space?.id ?? history.state?.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -217,7 +233,7 @@ class ContextFields implements MainScreenContextFields { const SYNC_ERROR_HIDE_DELAY = 30 * 1000 -const handleURLHash = (client: Client) => { +const handleURLHash = (client: Client, context: MainScreenContextFields) => { if (!location.hash.startsWith("#/uri/")) { if (location.search) { const currentETag = ( @@ -268,7 +284,7 @@ const handleURLHash = (client: Client) => { // TODO loading indicator or something for this? client.rpc.resolveAlias(uri.identifier).then( res => { - window.mainScreenContext.setActiveRoom(res.room_id, { + context.setActiveRoom(res.room_id, { alias: uri.identifier, via: res.servers.slice(0, 3), }) @@ -321,13 +337,13 @@ const MainScreen = () => { context.setActiveRoom(roomID, { alias: ensureString(evt.state?.source_alias) || undefined, via: ensureStringArray(evt.state?.source_via), - }, false) + }, undefined, false) } context.setRightPanel(evt.state?.right_panel ?? null, false) } window.addEventListener("popstate", listener) const initHandle = () => { - const state = handleURLHash(client) + const state = handleURLHash(client, context) listener({ state } as PopStateEvent) } let cancel = () => {} diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts index 28aa97d..de71425 100644 --- a/web/src/ui/MainScreenContext.ts +++ b/web/src/ui/MainScreenContext.ts @@ -13,14 +13,14 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { createContext } from "react" +import React, { createContext } from "react" import { RoomListFilter } from "@/api/statestore" import type { RoomID } from "@/api/types" import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx" export interface MainScreenContextFields { - setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial) => void + setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial, toSpace?: RoomListFilter) => void setSpace: (space: RoomListFilter | null, pushState?: boolean) => void clickRoom: (evt: React.MouseEvent) => void clearActiveRoom: () => void diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 39d4cb5..c83be1d 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -77,8 +77,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => { for (let i = client.store.roomList.current.length - 1; i >= 0; i--) { const entry = client.store.roomList.current[i] if (entry[wantedField] > 0 && space.include(entry)) { - mainScreen.setActiveRoom(entry.room_id) - mainScreen.setSpace(space) + mainScreen.setActiveRoom(entry.room_id, undefined, space) evt.stopPropagation() break } From f766b786eea71cd1178d26bea170ae4247aa92f8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 3 Jan 2025 14:20:00 +0200 Subject: [PATCH 75/85] web/eslint: add curly rule --- web/eslint.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/eslint.config.js b/web/eslint.config.js index 41ba85f..40156d5 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -73,6 +73,7 @@ export default tseslint.config( "one-var-declaration-per-line": ["error", "initializations"], "quotes": ["error", "double", {allowTemplateLiterals: true}], "semi": ["error", "never"], + "curly": ["error", "all"], "comma-dangle": ["error", "always-multiline"], "max-len": ["error", 120], "space-before-function-paren": ["error", { From a1a006bf6b9a63a4fc7410686dbc79f74cea4c29 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Fri, 3 Jan 2025 12:27:02 +0000 Subject: [PATCH 76/85] web/rightpanel: show extended profile info for users (#574) Co-authored-by: Tulir Asokan --- pkg/hicli/json-commands.go | 9 ++ web/src/api/rpc.ts | 5 + web/src/api/types/mxtypes.ts | 18 +++ web/src/ui/rightpanel/RightPanel.css | 14 +++ web/src/ui/rightpanel/UserExtendedProfile.tsx | 114 ++++++++++++++++++ web/src/ui/rightpanel/UserInfo.tsx | 17 ++- 6 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 web/src/ui/rightpanel/UserExtendedProfile.tsx diff --git a/pkg/hicli/json-commands.go b/pkg/hicli/json-commands.go index 26b8e89..2ef0ab4 100644 --- a/pkg/hicli/json-commands.go +++ b/pkg/hicli/json-commands.go @@ -86,6 +86,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespUserProfile, error) { return h.Client.GetProfile(ctx, params.UserID) }) + case "set_profile_field": + return unmarshalAndCall(req.Data, func(params *setProfileFieldParams) (bool, error) { + return true, h.Client.UnstableSetProfileField(ctx, params.Field, params.Value) + }) case "get_mutual_rooms": return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) { return h.GetMutualRooms(ctx, params.UserID) @@ -275,6 +279,11 @@ type getProfileParams struct { UserID id.UserID `json:"user_id"` } +type setProfileFieldParams struct { + Field string `json:"field"` + Value any `json:"value"` +} + type getEventParams struct { RoomID id.RoomID `json:"room_id"` EventID id.EventID `json:"event_id"` diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index a52a3c3..083ca75 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -20,6 +20,7 @@ import type { EventID, EventRowID, EventType, + JSONValue, LoginFlowsResponse, LoginRequest, Mentions, @@ -181,6 +182,10 @@ export default abstract class RPCClient { return this.request("get_profile", { user_id }) } + setProfileField(field: string, value: JSONValue): Promise { + return this.request("set_profile_field", { field, value }) + } + getMutualRooms(user_id: UserID): Promise { return this.request("get_mutual_rooms", { user_id }) } diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 81261ea..8605239 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -25,6 +25,14 @@ export type RoomVersion = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | export type RoomType = "" | "m.space" export type RelationType = "m.annotation" | "m.reference" | "m.replace" | "m.thread" +export type JSONValue = + | string + | number + | boolean + | null + | JSONValue[] + | {[key: string]: JSONValue} + export interface RoomPredecessor { room_id: RoomID event_id: EventID @@ -68,6 +76,16 @@ export interface UserProfile { [custom: string]: unknown } +export interface PronounSet { + subject?: string + object?: string + possessive_determiner?: string + possessive_pronoun?: string + reflexive?: string + summary: string + language: string +} + export type Membership = "join" | "leave" | "ban" | "invite" | "knock" export interface MemberEventContent extends UserProfile { diff --git a/web/src/ui/rightpanel/RightPanel.css b/web/src/ui/rightpanel/RightPanel.css index 76be03f..50a3c44 100644 --- a/web/src/ui/rightpanel/RightPanel.css +++ b/web/src/ui/rightpanel/RightPanel.css @@ -91,6 +91,20 @@ div.right-panel-content.user { word-break: break-word; } + div.extended-profile { + display: grid; + gap: 0.25rem; + grid-template-columns: 1fr 1fr; + + > input { + border: 0; + padding: 0; /* Necessary to prevent alignment issues with other cells */ + width: 100%; + box-sizing: border-box; + border-bottom: 1px solid var(--blockquote-border-color); + } + } + hr { width: 100%; opacity: .2; diff --git a/web/src/ui/rightpanel/UserExtendedProfile.tsx b/web/src/ui/rightpanel/UserExtendedProfile.tsx new file mode 100644 index 0000000..236e1ef --- /dev/null +++ b/web/src/ui/rightpanel/UserExtendedProfile.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react" +import Client from "@/api/client.ts" +import { PronounSet, UserProfile } from "@/api/types" +import { ensureArray, ensureString } from "@/util/validation.ts" + +interface ExtendedProfileProps { + profile: UserProfile + refreshProfile: () => void + client: Client + userID: string +} + +interface SetTimezoneProps { + tz?: string + client: Client + refreshProfile: () => void +} + +const getCurrentTimezone = () => new Intl.DateTimeFormat().resolvedOptions().timeZone + +const currentTimeAdjusted = (tz: string) => { + try { + return new Intl.DateTimeFormat("en-GB", { + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZoneName: "short", + timeZone: tz, + }).format(new Date()) + } catch (e) { + return `${e}` + } +} + +const ClockElement = ({ tz }: { tz: string }) => { + const [time, setTime] = useState(currentTimeAdjusted(tz)) + useEffect(() => { + let interval: number | undefined + const updateTime = () => setTime(currentTimeAdjusted(tz)) + const timeout = setTimeout(() => { + interval = setInterval(updateTime, 1000) + updateTime() + }, (1001 - Date.now() % 1000)) + return () => interval ? clearInterval(interval) : clearTimeout(timeout) + }, [tz]) + + return <> +
Time:
+
{time}
+ +} + +const SetTimeZoneElement = ({ tz, client, refreshProfile }: SetTimezoneProps) => { + const zones = Intl.supportedValuesOf("timeZone") + const saveTz = (newTz: string) => { + if (!zones.includes(newTz)) { + return + } + client.rpc.setProfileField("us.cloke.msc4175.tz", newTz).then( + () => refreshProfile(), + err => { + console.error("Failed to set time zone:", err) + window.alert(`Failed to set time zone: ${err}`) + }, + ) + } + + const defaultValue = tz || getCurrentTimezone() + return <> + + evt.key === "Enter" && saveTz(evt.currentTarget.value)} + onBlur={evt => evt.currentTarget.value !== defaultValue && saveTz(evt.currentTarget.value)} + /> + + {zones.map((zone) => + +} + + +const UserExtendedProfile = ({ profile, refreshProfile, client, userID }: ExtendedProfileProps)=> { + if (!profile) { + return null + } + + const extendedProfileKeys = ["us.cloke.msc4175.tz", "io.fsky.nyx.pronouns"] + const hasExtendedProfile = extendedProfileKeys.some((key) => profile[key]) + if (!hasExtendedProfile && client.userID !== userID) { + return null + } + // Explicitly only return something if the profile has the keys we're looking for. + // otherwise there's an ugly and pointless
for no real reason. + + const pronouns = ensureArray(profile["io.fsky.nyx.pronouns"]) as PronounSet[] + const userTimeZone = ensureString(profile["us.cloke.msc4175.tz"]) + return <> +
+
+ {userTimeZone && } + {userID === client.userID && + } + {pronouns.length > 0 && <> +
Pronouns:
+
{pronouns.map(pronounSet => ensureString(pronounSet.summary)).join(" / ")}
+ } +
+ +} + +export default UserExtendedProfile diff --git a/web/src/ui/rightpanel/UserInfo.tsx b/web/src/ui/rightpanel/UserInfo.tsx index 4e93ec7..0181904 100644 --- a/web/src/ui/rightpanel/UserInfo.tsx +++ b/web/src/ui/rightpanel/UserInfo.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { use, useEffect, useState } from "react" +import { use, useCallback, useEffect, useState } from "react" import { PuffLoader } from "react-spinners" import { getAvatarURL } from "@/api/media.ts" import { useRoomMember } from "@/api/statestore" @@ -22,6 +22,7 @@ import { getLocalpart } from "@/util/validation.ts" import ClientContext from "../ClientContext.ts" import { LightboxContext } from "../modal" import { RoomContext } from "../roomview/roomcontext.ts" +import UserExtendedProfile from "./UserExtendedProfile.tsx" import DeviceList from "./UserInfoDeviceList.tsx" import UserInfoError from "./UserInfoError.tsx" import MutualRooms from "./UserInfoMutualRooms.tsx" @@ -38,14 +39,17 @@ const UserInfo = ({ userID }: UserInfoProps) => { const member = (memberEvt?.content ?? null) as MemberEventContent | null const [globalProfile, setGlobalProfile] = useState(null) const [errors, setErrors] = useState(null) - useEffect(() => { - setErrors(null) - setGlobalProfile(null) + const refreshProfile = useCallback((clearState = false) => { + if (clearState) { + setErrors(null) + setGlobalProfile(null) + } client.rpc.getProfile(userID).then( setGlobalProfile, err => setErrors([`${err}`]), ) - }, [roomCtx, userID, client]) + }, [userID, client]) + useEffect(() => refreshProfile(true), [refreshProfile]) const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID) return <> @@ -63,6 +67,9 @@ const UserInfo = ({ userID }: UserInfoProps) => {
{displayname}
{userID}
+ {globalProfile && }
{userID !== client.userID && <> From cb08f435357ef342f00b33deb2eec6da702215cc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 3 Jan 2025 16:29:21 +0200 Subject: [PATCH 77/85] web/rightpanel: use comma instead of slash as separator for pronoun sets --- web/src/ui/rightpanel/UserExtendedProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/rightpanel/UserExtendedProfile.tsx b/web/src/ui/rightpanel/UserExtendedProfile.tsx index 236e1ef..e745f53 100644 --- a/web/src/ui/rightpanel/UserExtendedProfile.tsx +++ b/web/src/ui/rightpanel/UserExtendedProfile.tsx @@ -105,7 +105,7 @@ const UserExtendedProfile = ({ profile, refreshProfile, client, userID }: Extend } {pronouns.length > 0 && <>
Pronouns:
-
{pronouns.map(pronounSet => ensureString(pronounSet.summary)).join(" / ")}
+
{pronouns.map(pronounSet => ensureString(pronounSet.summary)).join(", ")}
}
From 158745b7a0429ff56339210554106c2fb95d27a3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 5 Jan 2025 01:30:42 +0200 Subject: [PATCH 78/85] web/statestore: clear unreads when rejecting invite or leaving room --- web/src/api/statestore/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 31fe657..c430fa7 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -315,6 +315,7 @@ export class StateStore { } this.rooms.delete(roomID) changedRoomListEntries.set(roomID, null) + this.#applyUnreadModification(null, this.roomListEntries.get(roomID)) } let updatedRoomList: RoomListEntry[] | undefined From bdc823742e20ad2bb1d9f8b8c95be69d0e0b8b82 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Mon, 6 Jan 2025 12:51:58 +0000 Subject: [PATCH 79/85] web/timeline: render policy list events (#586) Co-authored-by: Tulir Asokan --- web/src/api/types/mxtypes.ts | 6 ++ web/src/ui/MainScreen.tsx | 1 + .../ui/timeline/content/PolicyRuleBody.tsx | 61 +++++++++++++++++++ web/src/ui/timeline/content/index.ts | 9 +++ 4 files changed, 77 insertions(+) create mode 100644 web/src/ui/timeline/content/PolicyRuleBody.tsx diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 8605239..77f036a 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -111,6 +111,12 @@ export interface ACLEventContent { deny?: string[] } +export interface PolicyRuleContent { + entity: string + reason: string + recommendation: string +} + export interface PowerLevelEventContent { users?: Record users_default?: number diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index e3220a6..13edde4 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -217,6 +217,7 @@ class ContextFields implements MainScreenContextFields { } clickRightPanelOpener = (evt: React.MouseEvent) => { + evt.preventDefault() const type = evt.currentTarget.getAttribute("data-target-panel") if (type === "pinned-messages" || type === "members") { this.setRightPanel({ type }) diff --git a/web/src/ui/timeline/content/PolicyRuleBody.tsx b/web/src/ui/timeline/content/PolicyRuleBody.tsx new file mode 100644 index 0000000..8da2217 --- /dev/null +++ b/web/src/ui/timeline/content/PolicyRuleBody.tsx @@ -0,0 +1,61 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { JSX, use } from "react" +import { PolicyRuleContent } from "@/api/types" +import { getDisplayname } from "@/util/validation.ts" +import MainScreenContext from "../../MainScreenContext.ts" +import EventContentProps from "./props.ts" + +const PolicyRuleBody = ({ event, sender }: EventContentProps) => { + const content = event.content as PolicyRuleContent + const prevContent = event.unsigned.prev_content as PolicyRuleContent | undefined + const mainScreen = use(MainScreenContext) + + const entity = content.entity ?? prevContent?.entity + const recommendation = content.recommendation ?? prevContent?.recommendation + if (!entity || !recommendation) { + return
+ {getDisplayname(event.sender, sender?.content)} sent an invalid policy rule +
+ } + let entityElement = <>{entity} + if(event.type === "m.policy.rule.user" && !entity?.includes("*") && !entity?.includes("?")) { + entityElement = ( + + {entity} + + ) + } + let recommendationElement: JSX.Element | string = {recommendation} + if (recommendation === "m.ban") { + recommendationElement = "ban" + } + const action = prevContent ? ((content.entity && content.recommendation) ? "updated" : "removed") : "added" + const target = event.type.replace(/^m\.policy\.rule\./, "") + return
+ {getDisplayname(event.sender, sender?.content)} {action} a {recommendationElement} rule + for {target}s matching {entityElement} + {content.reason ? <> for {content.reason} : null} +
+} + +export default PolicyRuleBody diff --git a/web/src/ui/timeline/content/index.ts b/web/src/ui/timeline/content/index.ts index 108d44a..b3c77f7 100644 --- a/web/src/ui/timeline/content/index.ts +++ b/web/src/ui/timeline/content/index.ts @@ -7,6 +7,7 @@ import LocationMessageBody from "./LocationMessageBody.tsx" import MediaMessageBody from "./MediaMessageBody.tsx" import MemberBody from "./MemberBody.tsx" import PinnedEventsBody from "./PinnedEventsBody.tsx" +import PolicyRuleBody from "./PolicyRuleBody.tsx" import PowerLevelBody from "./PowerLevelBody.tsx" import RedactedBody from "./RedactedBody.tsx" import RoomAvatarBody from "./RoomAvatarBody.tsx" @@ -24,6 +25,7 @@ export { default as MediaMessageBody } from "./MediaMessageBody.tsx" export { default as LocationMessageBody } from "./LocationMessageBody.tsx" export { default as MemberBody } from "./MemberBody.tsx" export { default as PinnedEventsBody } from "./PinnedEventsBody.tsx" +export { default as PolicyRuleBody } from "./PolicyRuleBody.tsx" export { default as PowerLevelBody } from "./PowerLevelBody.tsx" export { default as RedactedBody } from "./RedactedBody.tsx" export { default as RoomAvatarBody } from "./RoomAvatarBody.tsx" @@ -82,6 +84,12 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo return RoomAvatarBody case "m.room.server_acl": return ACLBody + case "m.policy.rule.user": + return PolicyRuleBody + case "m.policy.rule.room": + return PolicyRuleBody + case "m.policy.rule.server": + return PolicyRuleBody case "m.room.pinned_events": return PinnedEventsBody case "m.room.power_levels": @@ -97,6 +105,7 @@ export function isSmallEvent(bodyType: React.FunctionComponent Date: Mon, 6 Jan 2025 06:08:34 -0700 Subject: [PATCH 80/85] web/timeline: render MSC4144 per-message profiles (#566) Signed-off-by: Sumner Evans --- web/src/api/media.ts | 5 ++-- web/src/api/types/mxtypes.ts | 6 +++++ web/src/ui/timeline/ReplyBody.css | 10 ++++++++ web/src/ui/timeline/ReplyBody.tsx | 37 ++++++++++++++++++++++----- web/src/ui/timeline/TimelineEvent.css | 15 +++++++++++ web/src/ui/timeline/TimelineEvent.tsx | 37 ++++++++++++++++++++++----- web/src/ui/timeline/content/index.ts | 9 ++++++- 7 files changed, 104 insertions(+), 15 deletions(-) diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 5e3180d..028ab6f 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -81,12 +81,13 @@ function getFallbackCharacter(from: unknown, idx: number): string { export const getAvatarURL = (userID: UserID, content?: UserProfile | null): string | undefined => { const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1) const backgroundColor = getUserColor(userID) - const [server, mediaID] = parseMXC(content?.avatar_url) + const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url) if (!mediaID) { return makeFallbackAvatar(backgroundColor, fallbackCharacter) } + const encrypted = !!content?.avatar_file const fallback = `${backgroundColor}:${fallbackCharacter}` - return `_gomuks/media/${server}/${mediaID}?encrypted=false&fallback=${encodeURIComponent(fallback)}` + return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}&fallback=${encodeURIComponent(fallback)}` } interface RoomForAvatarURL { diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 77f036a..51a22d4 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -73,6 +73,7 @@ export interface EncryptedEventContent { export interface UserProfile { displayname?: string avatar_url?: ContentURI + avatar_file?: EncryptedFile [custom: string]: unknown } @@ -177,6 +178,10 @@ export interface URLPreview { "og:description"?: string } +export interface BeeperPerMessageProfile extends UserProfile { + id: string +} + export interface BaseMessageEventContent { msgtype: string body: string @@ -189,6 +194,7 @@ export interface BaseMessageEventContent { "page.codeberg.everypizza.msc4193.spoiler.reason"?: string "m.url_previews"?: URLPreview[] "com.beeper.linkpreviews"?: URLPreview[] + "com.beeper.per_message_profile"?: BeeperPerMessageProfile } export interface TextMessageEventContent extends BaseMessageEventContent { diff --git a/web/src/ui/timeline/ReplyBody.css b/web/src/ui/timeline/ReplyBody.css index 391b456..d932627 100644 --- a/web/src/ui/timeline/ReplyBody.css +++ b/web/src/ui/timeline/ReplyBody.css @@ -103,6 +103,16 @@ blockquote.reply-body { } } + > div.per-message-event-sender { + color: var(--secondary-text-color); + font-size: .75rem; + margin: 0 .25rem; + + > span.via { + margin-right: .25rem; + } + } + > div.buttons { margin-left: auto; display: flex; diff --git a/web/src/ui/timeline/ReplyBody.tsx b/web/src/ui/timeline/ReplyBody.tsx index 1788baa..65c20c8 100644 --- a/web/src/ui/timeline/ReplyBody.tsx +++ b/web/src/ui/timeline/ReplyBody.tsx @@ -20,7 +20,7 @@ import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types" import { getDisplayname } from "@/util/validation.ts" import ClientContext from "../ClientContext.ts" import TooltipButton from "../util/TooltipButton.tsx" -import { ContentErrorBoundary, getBodyType } from "./content" +import { ContentErrorBoundary, getBodyType, getPerMessageProfile } from "./content" import CloseIcon from "@/icons/close.svg?react" import NotificationsOffIcon from "@/icons/notifications-off.svg?react" import NotificationsIcon from "@/icons/notifications.svg?react" @@ -102,22 +102,47 @@ export const ReplyBody = ({ if (small) { classNames.push("small") } - const userColorIndex = getUserColorIndex(event.sender) + const perMessageSender = getPerMessageProfile(event) + let renderMemberEvtContent = memberEvtContent + if (perMessageSender) { + renderMemberEvtContent = { + membership: "join", + displayname: perMessageSender.displayname ?? memberEvtContent?.displayname, + avatar_url: perMessageSender.avatar_url ?? memberEvtContent?.avatar_url, + avatar_file: perMessageSender.avatar_file ?? memberEvtContent?.avatar_file, + } + } + const userColorIndex = getUserColorIndex(perMessageSender?.id ?? event.sender) classNames.push(`sender-color-${userColorIndex}`) return
{small &&
}
-
+
- - {getDisplayname(event.sender, memberEvtContent)} + + {getDisplayname(event.sender, renderMemberEvtContent)} + {perMessageSender &&
+ via + + {getDisplayname(event.sender, memberEvtContent)} + +
} {onClose &&
{onSetSilent && (isExplicitInThread || !isThread) && div.per-message-event-sender { + color: var(--secondary-text-color); + font-size: .75rem; + + > span.via { + margin-right: .25rem; + } + + > span.event-sender { + font-weight: bold; + user-select: none; + cursor: var(--clickable-cursor); + } + } + > span.event-time, > span.event-edited { font-size: .7rem; color: var(--secondary-text-color); diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 524d84b..55c83ed 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -27,7 +27,7 @@ import { useRoomContext } from "../roomview/roomcontext.ts" import ReadReceipts from "./ReadReceipts.tsx" import { ReplyIDBody } from "./ReplyBody.tsx" import URLPreviews from "./URLPreviews.tsx" -import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content" +import { ContentErrorBoundary, HiddenEvent, getBodyType, getPerMessageProfile, isSmallEvent } from "./content" import { EventFixedMenu, EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu" import ErrorIcon from "@/icons/error.svg?react" import PendingIcon from "@/icons/pending.svg?react" @@ -170,6 +170,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T replyInMessage = replyElem } } + const perMessageSender = getPerMessageProfile(evt) + const prevPerMessageSender = getPerMessageProfile(prevEvt) + let renderMemberEvtContent = memberEvtContent + if (perMessageSender) { + renderMemberEvtContent = { + membership: "join", + displayname: perMessageSender.displayname ?? memberEvtContent?.displayname, + avatar_url: perMessageSender.avatar_url ?? memberEvtContent?.avatar_url, + avatar_file: perMessageSender.avatar_file ?? memberEvtContent?.avatar_file, + } + } + let smallAvatar = false let renderAvatar = true let eventTimeOnly = false @@ -183,6 +195,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T && dateSeparator === null && !replyAboveMessage && !isSmallEvent(getBodyType(prevEvt)) + && prevPerMessageSender?.id === perMessageSender?.id ) { wrapperClassNames.push("same-sender") eventTimeOnly = true @@ -209,7 +222,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T {replyAboveMessage} {renderAvatar &&
} {!eventTimeOnly ?
- {getDisplayname(evt.sender, memberEvtContent)} + {getDisplayname(evt.sender, renderMemberEvtContent)} + {perMessageSender &&
+ via + + {getDisplayname(evt.sender, memberEvtContent)} + +
} {shortTime} {(editEventTS && editTime) ? (edited at {formatShortTime(editEventTS)}) diff --git a/web/src/ui/timeline/content/index.ts b/web/src/ui/timeline/content/index.ts index b3c77f7..7bd981b 100644 --- a/web/src/ui/timeline/content/index.ts +++ b/web/src/ui/timeline/content/index.ts @@ -1,5 +1,5 @@ import React from "react" -import { MemDBEvent } from "@/api/types" +import { BeeperPerMessageProfile, MemDBEvent, MessageEventContent } from "@/api/types" import ACLBody from "./ACLBody.tsx" import EncryptedBody from "./EncryptedBody.tsx" import HiddenEvent from "./HiddenEvent.tsx" @@ -113,3 +113,10 @@ export function isSmallEvent(bodyType: React.FunctionComponent Date: Mon, 6 Jan 2025 17:25:51 +0200 Subject: [PATCH 81/85] dependencies: update --- desktop/go.mod | 18 +- desktop/go.sum | 48 +-- go.mod | 18 +- go.sum | 40 +-- web/package-lock.json | 805 +++++++++++++++++++++++------------------- 5 files changed, 502 insertions(+), 427 deletions(-) diff --git a/desktop/go.mod b/desktop/go.mod index 8291c8f..28fd97e 100644 --- a/desktop/go.mod +++ b/desktop/go.mod @@ -8,7 +8,7 @@ require github.com/wailsapp/wails/v3 v3.0.0-alpha.7 require ( go.mau.fi/gomuks v0.3.1 - go.mau.fi/util v0.8.4-0.20241217231624-e3dc7ee01c86 + go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a ) require ( @@ -16,7 +16,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect - github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/alecthomas/chroma/v2 v2.15.0 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/buckket/go-blurhash v1.1.0 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -24,10 +24,10 @@ require ( github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/ebitengine/purego v0.4.0-alpha.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.11.0 // indirect @@ -63,20 +63,20 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/yuin/goldmark v1.7.8 // indirect go.mau.fi/zeroconfig v0.1.3 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect golang.org/x/image v0.23.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.28.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c // indirect - mvdan.cc/xurls/v2 v2.5.0 // indirect + maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f // indirect + mvdan.cc/xurls/v2 v2.6.0 // indirect ) replace go.mau.fi/gomuks => ../ diff --git a/desktop/go.sum b/desktop/go.sum index c04dd75..40e5e98 100644 --- a/desktop/go.sum +++ b/desktop/go.sum @@ -9,10 +9,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -42,16 +42,16 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= -github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -121,8 +121,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= +github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= @@ -160,8 +160,8 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.8.4-0.20241217231624-e3dc7ee01c86 h1:pIeLc83N03ect2L06lDg+4MQ11oH0phEzRZ/58FdHMo= -go.mau.fi/util v0.8.4-0.20241217231624-e3dc7ee01c86/go.mod h1:c00Db8xog70JeIsEvhdHooylTkTkakgnAOsZ04hplQY= +go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a h1:D9RCHBFjxah9F/YB7amvRJjT2IEOFWcz8jpcEY8dBV0= +go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -169,10 +169,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -213,15 +213,15 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -250,7 +250,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM= -maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= +maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f h1:+nAznNCSCm+P4am9CmguSfNfcbpA7qtTzJQrwCP8aHM= +maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f/go.mod h1:FmwzK7RSzrd1OfGDgJzFWXl7nYmYm8/P0Y77sy/A1Uw= +mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= +mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= diff --git a/go.mod b/go.mod index a7347ed..c9e7622 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.23.0 toolchain go1.23.4 require ( - github.com/alecthomas/chroma/v2 v2.14.0 + github.com/alecthomas/chroma/v2 v2.15.0 github.com/buckket/go-blurhash v1.1.0 github.com/chzyer/readline v1.5.1 github.com/coder/websocket v1.8.12 - github.com/gabriel-vasile/mimetype v1.4.7 + github.com/gabriel-vasile/mimetype v1.4.8 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-sqlite3 v1.14.24 github.com/rivo/uniseg v0.4.7 @@ -17,29 +17,29 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/yuin/goldmark v1.7.8 - go.mau.fi/util v0.8.4-0.20241217231624-e3dc7ee01c86 + go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a go.mau.fi/zeroconfig v0.1.3 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.32.0 golang.org/x/image v0.23.0 golang.org/x/net v0.33.0 golang.org/x/text v0.21.0 gopkg.in/yaml.v3 v3.0.1 maunium.net/go/mauflag v1.0.0 - maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c - mvdan.cc/xurls/v2 v2.5.0 + maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f + mvdan.cc/xurls/v2 v2.6.0 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect github.com/rs/xid v1.6.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect + golang.org/x/sys v0.29.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 51e82a9..ee83488 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do= @@ -22,10 +22,10 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= -github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= @@ -63,14 +63,14 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.8.4-0.20241217231624-e3dc7ee01c86 h1:pIeLc83N03ect2L06lDg+4MQ11oH0phEzRZ/58FdHMo= -go.mau.fi/util v0.8.4-0.20241217231624-e3dc7ee01c86/go.mod h1:c00Db8xog70JeIsEvhdHooylTkTkakgnAOsZ04hplQY= +go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a h1:D9RCHBFjxah9F/YB7amvRJjT2IEOFWcz8jpcEY8dBV0= +go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= @@ -79,8 +79,8 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= @@ -91,7 +91,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM= -maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= +maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f h1:+nAznNCSCm+P4am9CmguSfNfcbpA7qtTzJQrwCP8aHM= +maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f/go.mod h1:FmwzK7RSzrd1OfGDgJzFWXl7nYmYm8/P0Y77sy/A1Uw= +mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= +mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= diff --git a/web/package-lock.json b/web/package-lock.json index b322427..804656b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1031,9 +1031,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.0.tgz", + "integrity": "sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ==", "cpu": [ "arm" ], @@ -1045,9 +1045,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.0.tgz", + "integrity": "sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A==", "cpu": [ "arm64" ], @@ -1059,9 +1059,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.0.tgz", + "integrity": "sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==", "cpu": [ "arm64" ], @@ -1073,9 +1073,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.0.tgz", + "integrity": "sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw==", "cpu": [ "x64" ], @@ -1087,9 +1087,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.0.tgz", + "integrity": "sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA==", "cpu": [ "arm64" ], @@ -1101,9 +1101,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.0.tgz", + "integrity": "sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg==", "cpu": [ "x64" ], @@ -1115,9 +1115,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.0.tgz", + "integrity": "sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==", "cpu": [ "arm" ], @@ -1129,9 +1129,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.0.tgz", + "integrity": "sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==", "cpu": [ "arm" ], @@ -1143,9 +1143,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.0.tgz", + "integrity": "sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==", "cpu": [ "arm64" ], @@ -1157,9 +1157,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.0.tgz", + "integrity": "sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==", "cpu": [ "arm64" ], @@ -1171,9 +1171,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.0.tgz", + "integrity": "sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==", "cpu": [ "loong64" ], @@ -1185,9 +1185,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.0.tgz", + "integrity": "sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==", "cpu": [ "ppc64" ], @@ -1199,9 +1199,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.0.tgz", + "integrity": "sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==", "cpu": [ "riscv64" ], @@ -1213,9 +1213,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.0.tgz", + "integrity": "sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==", "cpu": [ "s390x" ], @@ -1227,9 +1227,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.0.tgz", + "integrity": "sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==", "cpu": [ "x64" ], @@ -1241,9 +1241,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.0.tgz", + "integrity": "sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==", "cpu": [ "x64" ], @@ -1255,9 +1255,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.0.tgz", + "integrity": "sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==", "cpu": [ "arm64" ], @@ -1269,9 +1269,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.0.tgz", + "integrity": "sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g==", "cpu": [ "ia32" ], @@ -1283,9 +1283,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.0.tgz", + "integrity": "sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg==", "cpu": [ "x64" ], @@ -1529,9 +1529,9 @@ } }, "node_modules/@swc/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz", - "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.4.tgz", + "integrity": "sha512-ut3zfiTLORMxhr6y/GBxkHmzcGuVpwJYX4qyXWuBKkpw/0g0S5iO1/wW7RnLnZbAi8wS/n0atRZoaZlXWBkeJg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1547,16 +1547,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.1", - "@swc/core-darwin-x64": "1.10.1", - "@swc/core-linux-arm-gnueabihf": "1.10.1", - "@swc/core-linux-arm64-gnu": "1.10.1", - "@swc/core-linux-arm64-musl": "1.10.1", - "@swc/core-linux-x64-gnu": "1.10.1", - "@swc/core-linux-x64-musl": "1.10.1", - "@swc/core-win32-arm64-msvc": "1.10.1", - "@swc/core-win32-ia32-msvc": "1.10.1", - "@swc/core-win32-x64-msvc": "1.10.1" + "@swc/core-darwin-arm64": "1.10.4", + "@swc/core-darwin-x64": "1.10.4", + "@swc/core-linux-arm-gnueabihf": "1.10.4", + "@swc/core-linux-arm64-gnu": "1.10.4", + "@swc/core-linux-arm64-musl": "1.10.4", + "@swc/core-linux-x64-gnu": "1.10.4", + "@swc/core-linux-x64-musl": "1.10.4", + "@swc/core-win32-arm64-msvc": "1.10.4", + "@swc/core-win32-ia32-msvc": "1.10.4", + "@swc/core-win32-x64-msvc": "1.10.4" }, "peerDependencies": { "@swc/helpers": "*" @@ -1568,9 +1568,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz", - "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.4.tgz", + "integrity": "sha512-sV/eurLhkjn/197y48bxKP19oqcLydSel42Qsy2zepBltqUx+/zZ8+/IS0Bi7kaWVFxerbW1IPB09uq8Zuvm3g==", "cpu": [ "arm64" ], @@ -1585,9 +1585,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz", - "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.4.tgz", + "integrity": "sha512-gjYNU6vrAUO4+FuovEo9ofnVosTFXkF0VDuo1MKPItz6e2pxc2ale4FGzLw0Nf7JB1sX4a8h06CN16/pLJ8Q2w==", "cpu": [ "x64" ], @@ -1602,9 +1602,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz", - "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.4.tgz", + "integrity": "sha512-zd7fXH5w8s+Sfvn2oO464KDWl+ZX1MJiVmE4Pdk46N3PEaNwE0koTfgx2vQRqRG4vBBobzVvzICC3618WcefOA==", "cpu": [ "arm" ], @@ -1619,9 +1619,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz", - "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.4.tgz", + "integrity": "sha512-+UGfoHDxsMZgFD3tABKLeEZHqLNOkxStu+qCG7atGBhS4Slri6h6zijVvf4yI5X3kbXdvc44XV/hrP/Klnui2A==", "cpu": [ "arm64" ], @@ -1636,9 +1636,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz", - "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.4.tgz", + "integrity": "sha512-cDDj2/uYsOH0pgAnDkovLZvKJpFmBMyXkxEG6Q4yw99HbzO6QzZ5HDGWGWVq/6dLgYKlnnmpjZCPPQIu01mXEg==", "cpu": [ "arm64" ], @@ -1653,9 +1653,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz", - "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.4.tgz", + "integrity": "sha512-qJXh9D6Kf5xSdGWPINpLGixAbB5JX8JcbEJpRamhlDBoOcQC79dYfOMEIxWPhTS1DGLyFakAx2FX/b2VmQmj0g==", "cpu": [ "x64" ], @@ -1670,9 +1670,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz", - "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.4.tgz", + "integrity": "sha512-A76lIAeyQnHCVt0RL/pG+0er8Qk9+acGJqSZOZm67Ve3B0oqMd871kPtaHBM0BW3OZAhoILgfHW3Op9Q3mx3Cw==", "cpu": [ "x64" ], @@ -1687,9 +1687,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz", - "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.4.tgz", + "integrity": "sha512-e6j5kBu4fIY7fFxFxnZI0MlEovRvp50Lg59Fw+DVbtqHk3C85dckcy5xKP+UoXeuEmFceauQDczUcGs19SRGSQ==", "cpu": [ "arm64" ], @@ -1704,9 +1704,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz", - "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.4.tgz", + "integrity": "sha512-RSYHfdKgNXV/amY5Tqk1EWVsyQnhlsM//jeqMLw5Fy9rfxP592W9UTumNikNRPdjI8wKKzNMXDb1U29tQjN0dg==", "cpu": [ "ia32" ], @@ -1721,9 +1721,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz", - "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.4.tgz", + "integrity": "sha512-1ujYpaqfqNPYdwKBlvJnOqcl+Syn3UrQ4XE0Txz6zMYgyh6cdU6a3pxqLqIUSJ12MtXRA9ZUhEz1ekU3LfLWXw==", "cpu": [ "x64" ], @@ -1800,9 +1800,9 @@ } }, "node_modules/@types/react": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.1.tgz", - "integrity": "sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==", + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.3.tgz", + "integrity": "sha512-UavfHguIjnnuq9O67uXfgy/h3SRJbidAYvNjLceB+2RIKVRBzVsh0QO+Pw6BCSQqFS9xwzKfwstXx0m6AbAREA==", "dev": true, "license": "MIT", "dependencies": { @@ -1830,17 +1830,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", - "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", + "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.0", - "@typescript-eslint/type-utils": "8.18.0", - "@typescript-eslint/utils": "8.18.0", - "@typescript-eslint/visitor-keys": "8.18.0", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/type-utils": "8.19.0", + "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1860,16 +1860,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", - "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", + "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", "dev": true, - "license": "MITClause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.0", - "@typescript-eslint/types": "8.18.0", - "@typescript-eslint/typescript-estree": "8.18.0", - "@typescript-eslint/visitor-keys": "8.18.0", + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4" }, "engines": { @@ -1885,14 +1885,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", - "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", + "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.0", - "@typescript-eslint/visitor-keys": "8.18.0" + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1903,14 +1903,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", - "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", + "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.0", - "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/utils": "8.19.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1927,9 +1927,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", - "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", "dev": true, "license": "MIT", "engines": { @@ -1941,14 +1941,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", - "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", + "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.0", - "@typescript-eslint/visitor-keys": "8.18.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2007,16 +2007,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", - "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", + "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.0", - "@typescript-eslint/types": "8.18.0", - "@typescript-eslint/typescript-estree": "8.18.0" + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2031,13 +2031,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", - "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", + "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/types": "8.19.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2062,9 +2062,9 @@ } }, "node_modules/@wailsio/runtime": { - "version": "3.0.0-alpha.29", - "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.29.tgz", - "integrity": "sha512-gap5qxcw3fgDBYBN75X65XZoo3vEPyJ9L+cqRd8I133Bf0kPT6XVVchm8Gc693eDqH7djyhXmCB7zJfosVH0fA==", + "version": "3.0.0-alpha.36", + "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.36.tgz", + "integrity": "sha512-IPxzYLxgX8tOWcB1x2RHzx3VwRFTLAUrdeMQL2wZyaV7Xvtybt1h1WYaEp0iZiiNB/KCuCKIrnhnrN5sNDoDYg==", "license": "MIT", "dependencies": { "nanoid": "^5.0.7" @@ -2134,14 +2134,14 @@ "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -2412,9 +2412,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001689", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", - "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -2542,15 +2542,15 @@ "license": "MIT" }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2560,31 +2560,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -2714,9 +2714,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.1.tgz", + "integrity": "sha512-xWXmuRnN9OMP6ptPd2+H0cCbcYBULa5YDTbMm/2lvkWvNA3O4wcW+GvzooqBuNM8yy6pl3VIAeJTUUWUbfI5Fw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2740,13 +2740,13 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -2755,9 +2755,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.73", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", - "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", + "version": "1.5.76", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", + "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", "dev": true, "license": "ISC" }, @@ -2785,28 +2785,29 @@ } }, "node_modules/es-abstract": { - "version": "1.23.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.6.tgz", - "integrity": "sha512-Ifco6n3yj2tMZDWNLyloZrytt9lqqlwvS83P3HtaETR0NUOYnIULGGHpktqYGObGy+8wc1okO25p8TjemhImvA==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", + "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.7", - "get-intrinsic": "^1.2.6", - "get-symbol-description": "^1.0.2", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", @@ -2814,31 +2815,33 @@ "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.4", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.3", + "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", - "is-typed-array": "^1.1.13", + "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", - "math-intrinsics": "^1.0.0", + "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.3", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.16" + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -2881,15 +2884,16 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3271,9 +3275,9 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3281,7 +3285,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3315,9 +3319,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, "license": "ISC", "dependencies": { @@ -3424,13 +3428,14 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.7.tgz", - "integrity": "sha512-2g4x+HqTJKM9zcJqBSpjoRmdcPFtJM60J3xJisTQSXBWka5XqyBN/2tNUgma1mztTXyDuUsEtYe5qcs7xYzYQA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", @@ -3464,22 +3469,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3488,16 +3493,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3520,9 +3539,9 @@ } }, "node_modules/globals": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", - "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", "dev": true, "license": "MIT", "engines": { @@ -3570,11 +3589,14 @@ "license": "MIT" }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3733,14 +3755,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -3757,13 +3780,16 @@ "license": "MIT" }, "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz", + "integrity": "sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3819,9 +3845,9 @@ } }, "node_modules/is-core-module": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", - "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -3880,13 +3906,13 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz", - "integrity": "sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3896,13 +3922,16 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3937,19 +3966,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4010,13 +4026,13 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4061,13 +4077,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4106,14 +4122,14 @@ } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4211,9 +4227,9 @@ } }, "node_modules/katex": { - "version": "0.16.15", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.15.tgz", - "integrity": "sha512-yE9YJIEAk2aZ+FL/G8r+UGw0CTUzEA8ZFy6E+8tc3spHUKq3qBnzCkI1CQwGoI9atJhVyFPEypQsTY7mJ1Pi9w==", + "version": "0.16.19", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.19.tgz", + "integrity": "sha512-3IA6DYVhxhBabjSLTNO9S4+OliA3Qvb8pBQXMfC4WxXJgLwZgnfDl0BmB4z6nBMdznBsZ+CGM8DrGZ5hcguDZg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -4307,9 +4323,9 @@ } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -4443,15 +4459,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -4496,13 +4514,14 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -4531,6 +4550,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4793,20 +4830,20 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz", - "integrity": "sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "dunder-proto": "^1.0.0", - "es-abstract": "^1.23.5", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.2.0", - "which-builtin-type": "^1.2.0" + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -4816,15 +4853,17 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "set-function-name": "^2.0.2" }, "engines": { @@ -4835,9 +4874,9 @@ } }, "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { @@ -4848,6 +4887,9 @@ "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4874,9 +4916,9 @@ } }, "node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.0.tgz", + "integrity": "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA==", "dev": true, "license": "MIT", "dependencies": { @@ -4890,25 +4932,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.30.0", + "@rollup/rollup-android-arm64": "4.30.0", + "@rollup/rollup-darwin-arm64": "4.30.0", + "@rollup/rollup-darwin-x64": "4.30.0", + "@rollup/rollup-freebsd-arm64": "4.30.0", + "@rollup/rollup-freebsd-x64": "4.30.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.0", + "@rollup/rollup-linux-arm-musleabihf": "4.30.0", + "@rollup/rollup-linux-arm64-gnu": "4.30.0", + "@rollup/rollup-linux-arm64-musl": "4.30.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0", + "@rollup/rollup-linux-riscv64-gnu": "4.30.0", + "@rollup/rollup-linux-s390x-gnu": "4.30.0", + "@rollup/rollup-linux-x64-gnu": "4.30.0", + "@rollup/rollup-linux-x64-musl": "4.30.0", + "@rollup/rollup-win32-arm64-msvc": "4.30.0", + "@rollup/rollup-win32-ia32-msvc": "4.30.0", + "@rollup/rollup-win32-x64-msvc": "4.30.0", "fsevents": "~2.3.2" } }, @@ -4956,6 +4998,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -5024,6 +5083,21 @@ "node": ">= 0.4" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5319,32 +5393,32 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -5354,19 +5428,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz", - "integrity": "sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "reflect.getprototypeof": "^1.0.6" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -5411,15 +5485,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", - "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", + "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.18.0", - "@typescript-eslint/parser": "8.18.0", - "@typescript-eslint/utils": "8.18.0" + "@typescript-eslint/eslint-plugin": "8.19.0", + "@typescript-eslint/parser": "8.19.0", + "@typescript-eslint/utils": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5658,16 +5732,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", - "integrity": "sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "for-each": "^0.3.3", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { From b4ad603ea3472bc70915181ad1e4a5d9c0a5b258 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 9 Jan 2025 01:49:41 +0200 Subject: [PATCH 82/85] web/index: add user-scalable=no --- web/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index 483ad69..7a59ab1 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ - + gomuks web From 40e7d6345334f6178fe8327e1d9a4f984a6c674f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Jan 2025 01:57:35 +0200 Subject: [PATCH 83/85] desktop/dependencies: update wails --- desktop/go.mod | 25 ++++++++++---------- desktop/go.sum | 62 ++++++++++++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/desktop/go.mod b/desktop/go.mod index 28fd97e..2e38a85 100644 --- a/desktop/go.mod +++ b/desktop/go.mod @@ -4,7 +4,7 @@ go 1.23.0 toolchain go1.23.3 -require github.com/wailsapp/wails/v3 v3.0.0-alpha.7 +require github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3 require ( go.mau.fi/gomuks v0.3.1 @@ -12,29 +12,30 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/adrg/xdg v0.5.0 // indirect github.com/alecthomas/chroma/v2 v2.15.0 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/buckket/go-blurhash v1.1.0 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.3.8 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/ebitengine/purego v0.4.0-alpha.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.11.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-git/go-billy/v5 v5.6.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect @@ -52,13 +53,13 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/rs/zerolog v1.33.0 // indirect github.com/samber/lo v1.38.1 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/wailsapp/go-webview2 v1.0.15 // indirect + github.com/wailsapp/go-webview2 v1.0.18 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/yuin/goldmark v1.7.8 // indirect diff --git a/desktop/go.sum b/desktop/go.sum index 40e5e98..c0fdeb3 100644 --- a/desktop/go.sum +++ b/desktop/go.sum @@ -1,5 +1,5 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -7,8 +7,10 @@ github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= +github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= @@ -31,14 +33,14 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= +github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,18 +54,18 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= +github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -71,8 +73,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -106,8 +108,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+ocrg/ZSgZ6k8bOk+kcZQ7fnyx6UvOm4= github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -130,11 +132,11 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -149,12 +151,12 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/wailsapp/go-webview2 v1.0.15 h1:IeQFoWmsHp32y64I41J+Zod3SopjHs918KSO4jUqEnY= -github.com/wailsapp/go-webview2 v1.0.15/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= +github.com/wailsapp/go-webview2 v1.0.18 h1:SSSCoLA+MYikSp1U0WmvELF/4c3x5kH8Vi31TKyZ4yk= +github.com/wailsapp/go-webview2 v1.0.18/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v3 v3.0.0-alpha.7 h1:LNX2EnbxTEYJYICJT8UkuzoGVNalRizTNGBY47endmk= -github.com/wailsapp/wails/v3 v3.0.0-alpha.7/go.mod h1:lBz4zedFxreJBoVpMe9u89oo4IE3IlyHJg5rOWnGNR0= +github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3 h1:9aCL0IXD60A5iscQ/ps6f3ti3IlaoG6LQe0RZ9JkueU= +github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3/go.mod h1:9Ca1goy5oqxmy8Oetb8Tchkezcx4tK03DK+SqYByu5Y= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -195,7 +197,6 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -208,6 +209,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -247,7 +249,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f h1:+nAznNCSCm+P4am9CmguSfNfcbpA7qtTzJQrwCP8aHM= From 5ab60cb816d6e935fd57341bb2e0f81da733ba3d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Jan 2025 01:58:55 +0200 Subject: [PATCH 84/85] web/mainscreen: handle url fragment change --- web/src/ui/MainScreen.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 13edde4..c0a5b48 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -234,8 +234,11 @@ class ContextFields implements MainScreenContextFields { const SYNC_ERROR_HIDE_DELAY = 30 * 1000 -const handleURLHash = (client: Client, context: MainScreenContextFields) => { +const handleURLHash = (client: Client, context: MainScreenContextFields, hashOnly = false) => { if (!location.hash.startsWith("#/uri/")) { + if (hashOnly) { + return null + } if (location.search) { const currentETag = ( document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement @@ -261,7 +264,7 @@ const handleURLHash = (client: Client, context: MainScreenContextFields) => { const uri = parseMatrixURI(decodedURI) if (!uri) { console.error("Invalid matrix URI", decodedURI) - return history.state + return hashOnly ? null : history.state } console.log("Handling URI", uri) const newURL = new URL(location.href) @@ -295,8 +298,9 @@ const handleURLHash = (client: Client, context: MainScreenContextFields) => { return null } else { console.error("Invalid matrix URI", uri) + history.replaceState(history.state, "", newURL.toString()) } - return history.state + return hashOnly ? null : history.state } type ActiveRoomType = [RoomStateStore | RoomPreviewProps | null, RoomStateStore | RoomPreviewProps | null] @@ -327,7 +331,7 @@ const MainScreen = () => { ) useEffect(() => { window.mainScreenContext = context - const listener = (evt: PopStateEvent) => { + const listener = (evt: Pick) => { skipNextTransitionRef.current = evt.hasUAVisualTransition const roomID = evt.state?.room_id ?? null const spaceID = evt.state?.space_id ?? undefined @@ -342,6 +346,13 @@ const MainScreen = () => { } context.setRightPanel(evt.state?.right_panel ?? null, false) } + const hashListener = () => { + const state = handleURLHash(client, context, true) + if (state !== null) { + listener({ state, hasUAVisualTransition: false }) + } + } + window.addEventListener("hashchange", hashListener) window.addEventListener("popstate", listener) const initHandle = () => { const state = handleURLHash(client, context) @@ -355,6 +366,7 @@ const MainScreen = () => { } return () => { window.removeEventListener("popstate", listener) + window.removeEventListener("hashchange", hashListener) cancel() } }, [context, client]) From d4fc8837362d3f44c96ec2f9fb4398c33ed9923f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 11 Jan 2025 20:45:39 +0200 Subject: [PATCH 85/85] web/timeline: fix typo in power level body --- web/src/ui/timeline/content/PowerLevelBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/timeline/content/PowerLevelBody.tsx b/web/src/ui/timeline/content/PowerLevelBody.tsx index 3598075..88e6c46 100644 --- a/web/src/ui/timeline/content/PowerLevelBody.tsx +++ b/web/src/ui/timeline/content/PowerLevelBody.tsx @@ -34,7 +34,7 @@ function renderPowerLevels(content: PowerLevelEventContent, prevContent?: PowerL intDiff`the ban power level from ${prevContent?.ban ?? 50} to ${content.ban ?? 50}`, intDiff`the kick power level from ${prevContent?.kick ?? 50} to ${content.kick ?? 50}`, intDiff`the redact power level from ${prevContent?.redact ?? 50} to ${content.redact ?? 50}`, - intDiff`the invite power level from ${prevContent?.redact ?? 0} to ${content.redact ?? 0}`, + intDiff`the invite power level from ${prevContent?.invite ?? 0} to ${content.invite ?? 0}`, intDiff`the @room notification power level from ${prevContent?.notifications?.room ?? 50} to ${content.notifications?.room ?? 50}`, ] /* eslint-enable max-len */