1
0
Fork 0
forked from Mirrors/gomuks

Compare commits

...
Sign in to create a new pull request.

32 commits

Author SHA1 Message Date
n
d5666f9f00 Test a runner
Signed-off-by: n <me@everypizza.im>
2025-03-19 13:53:22 -05:00
n
338c7d9adc Remove button for unredact
Signed-off-by: n <me@everypizza.im>
2025-03-19 13:46:30 -05:00
n
7c5ab68cf2 Update README.md
Signed-off-by: n <me@everypizza.im>
2025-03-19 13:43:09 -05:00
Tulir Asokan
295d1f156e
readme: update 2025-03-16 23:02:32 +02:00
Tulir Asokan
c312a2b523 dependencies: update 2025-03-16 21:36:00 +02:00
Tulir Asokan
99b3fd0e5e web/index: move font size to html root 2025-03-16 21:31:58 +02:00
Tulir Asokan
c48f24d2de web/timeline: hide overflow in small replies 2025-03-15 12:32:43 +02:00
Tulir Asokan
696687f60c hicli/sync: ignore failing to recalculate preview 2025-03-15 12:32:31 +02:00
Tulir Asokan
4262b5abfa dependencies: update 2025-03-14 02:07:37 +02:00
Tulir Asokan
87ec9d60a5 hicli/paginate: fix handling non-empty final chunk 2025-03-14 02:06:19 +02:00
Tulir Asokan
9c17ce001d readme: update room alias 2025-03-14 02:00:57 +02:00
Tulir Asokan
0f2263ce8d web/timeline: implement MSC4205 for policy list rendering 2025-03-13 02:25:18 +02:00
Tulir Asokan
92d3ab64bf web/timeline: limit name size in small replies 2025-03-11 19:30:40 +02:00
Tulir Asokan
746e26fbc1 web/timeline: fix avatars in replies shrinking with long names 2025-03-11 19:28:25 +02:00
Tulir Asokan
3041fb18e3 web/rightpanel: fix displayname overflow in user info 2025-03-11 19:26:47 +02:00
Tulir Asokan
ede1c92906 dependencies: update mautrix-go 2025-03-10 01:26:33 +02:00
Tulir Asokan
0ed5dfcc12 hicli/sync: fix updating preview event rowid on redaction 2025-03-09 18:12:34 +02:00
Tulir Asokan
7f80301276 web/statestore: reload room view if state store is cleared 2025-03-09 18:05:39 +02:00
Tulir Asokan
3f4333003d web/preferences: fix letter 2025-03-09 17:31:04 +02:00
Tulir Asokan
218481f3a4 web/timeline: disable url preview images if image previews are disabled 2025-03-09 17:21:21 +02:00
Tulir Asokan
ef05bc71f9 web/roomlist: add option to hide invite avatars 2025-03-09 17:18:27 +02:00
Tulir Asokan
86843d61f6 web/preferences: add option to disable inline images 2025-03-09 16:55:55 +02:00
Tulir Asokan
6b9f6bebd5 web/widget: move iframe css outside right panel content 2025-03-09 16:41:50 +02:00
Tulir Asokan
aee4cff572 hicli/sync: clear outbound session on invite accept if history visibility is set to joined 2025-03-08 19:22:36 +02:00
Tulir Asokan
678940618f hicli/send: fix panic if history visibility event is not found 2025-03-08 18:43:19 +02:00
Tulir Asokan
6425f68c88 web/widget: add basic permission prompt
Fixes #605
2025-03-08 18:15:09 +02:00
Tulir Asokan
0db18f1e94 web/modal: allow non-dismissable modals 2025-03-08 16:37:27 +02:00
Tulir Asokan
fbad48129b web/widget: implement handling close event 2025-03-08 15:59:46 +02:00
Sumner Evans
b3b255e71c
web/modal: fix scrollbars (#606)
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2025-03-08 13:55:42 +02:00
Tulir Asokan
5da85acfe0 web/timeline: fix message for rejecting invites 2025-03-07 22:53:06 +02:00
Tulir Asokan
f79678a87f web/widget: implement delayed state events 2025-03-06 01:29:40 +02:00
Tulir Asokan
23f2699909 push: log number of events in push payloads 2025-03-05 22:55:17 +02:00
43 changed files with 871 additions and 569 deletions

11
.forgeo/workflows Normal file
View file

@ -0,0 +1,11 @@
on: [push]
jobs:
test:
runs-on: docker
steps:
- uses: actions/cascading-pr@v1.0.1
with:
GOPATH="$CI_PROJECT_DIR/.cache"
GOCACHE="$CI_PROJECT_DIR/.cache/build"
MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
GO_LDFLAGS="-s -w -linkmode external -extldflags -static -X go.mau.fi/gomuks/version.Tag=$CI_COMMIT_TAG -X go.mau.fi/gomuks/version.Commit=$CI_COMMIT_SHA -X 'go.mau.fi/gomuks/version.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"

View file

@ -1,18 +1,7 @@
# gomuks # nyxmuks
![Languages](https://img.shields.io/github/languages/top/tulir/gomuks.svg)
[![License](https://img.shields.io/github/license/tulir/gomuks.svg)](LICENSE)
[![Release](https://img.shields.io/github/release/tulir/gomuks/all.svg)](https://github.com/tulir/gomuks/releases)
[![GitLab CI](https://mau.dev/tulir/gomuks/badges/main/pipeline.svg)](https://mau.dev/tulir/gomuks/pipelines)
A Matrix client written in Go using [mautrix](https://github.com/mautrix/go). Soft fork of Tulir's Gomuks.
This branch contains gomuks web. For legacy gomuks terminal, see the # why?
[master branch](https://github.com/tulir/gomuks/tree/master). The new
version will get a terminal frontend in the future. See also:
<https://github.com/tulir/gomuks/issues/476>.
## Docs Gomuks is adding unneccesary features, and the developer is acting maliciously. This fork aims to remove a few features that seem to be made with a malicious intent, like the "un-redact" button.
For installation and usage instructions, see [docs.mau.fi](https://docs.mau.fi/gomuks/).
## Discussion
Matrix room: [#gomuks:maunium.net](https://matrix.to/#/#gomuks:maunium.net)

View file

@ -8,7 +8,7 @@ require github.com/wailsapp/wails/v3 v3.0.0-alpha.9
require ( require (
go.mau.fi/gomuks v0.4.0 go.mau.fi/gomuks v0.4.0
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95 go.mau.fi/util v0.8.6
) )
require ( require (
@ -22,7 +22,7 @@ require (
github.com/buckket/go-blurhash v1.1.0 // indirect github.com/buckket/go-blurhash v1.1.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect github.com/cloudflare/circl v1.3.8 // indirect
github.com/coder/websocket v1.8.12 // indirect github.com/coder/websocket v1.8.13 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/disintegration/imaging v1.6.2 // indirect github.com/disintegration/imaging v1.6.2 // indirect
@ -47,7 +47,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a // indirect github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
@ -66,20 +66,20 @@ require (
github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark v1.7.8 // indirect
go.mau.fi/webp v0.2.0 // indirect go.mau.fi/webp v0.2.0 // indirect
go.mau.fi/zeroconfig v0.1.3 // indirect go.mau.fi/zeroconfig v0.1.3 // indirect
golang.org/x/crypto v0.34.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/image v0.24.0 // indirect golang.org/x/image v0.25.0 // indirect
golang.org/x/mod v0.23.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.30.0 // indirect golang.org/x/tools v0.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 // indirect maunium.net/go/mautrix v0.23.2 // indirect
mvdan.cc/xurls/v2 v2.6.0 // indirect mvdan.cc/xurls/v2 v2.6.0 // indirect
) )

View file

@ -35,8 +35,8 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= 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.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13/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 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
@ -113,8 +113,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a h1:ckxP/kGzsxvxXo8jO6E/0QJ8MMmwI7IRj4Fys9QbAZA= github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@ -166,8 +166,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.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 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95 h1:5EfVWWjU2Hte9uE6B/hBgvjnVfBx/7SYDZBnsuo+EBs= go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg= go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q= go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
@ -177,17 +177,17 @@ 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.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.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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -196,13 +196,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -222,15 +222,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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -238,14 +238,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -261,7 +261,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 h1:AeNHqITptzOpmfMxnqmQRw6xN7DUDCgsN00BaPyRd4k= maunium.net/go/mautrix v0.23.2 h1:Bo3tPrQJwkxyL7aMmy/T+d2tqIrypZjHqeHe8fyeAOg=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7/go.mod h1:IHMaSJh7YIxMrZSDVefS+nLdr3RbeLowsCSa6ibONZ0= maunium.net/go/mautrix v0.23.2/go.mod h1:pCYLHmo02Jauak/9VlTkbGPrBMvLXsGqTGMNOx+L2PE=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=

22
go.mod
View file

@ -2,13 +2,13 @@ module go.mau.fi/gomuks
go 1.23.0 go 1.23.0
toolchain go1.24.0 toolchain go1.24.1
require ( require (
github.com/alecthomas/chroma/v2 v2.15.0 github.com/alecthomas/chroma/v2 v2.15.0
github.com/buckket/go-blurhash v1.1.0 github.com/buckket/go-blurhash v1.1.0
github.com/chzyer/readline v1.5.1 github.com/chzyer/readline v1.5.1
github.com/coder/websocket v1.8.12 github.com/coder/websocket v1.8.13
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/gabriel-vasile/mimetype v1.4.8 github.com/gabriel-vasile/mimetype v1.4.8
github.com/lucasb-eyer/go-colorful v1.2.0 github.com/lucasb-eyer/go-colorful v1.2.0
@ -18,16 +18,16 @@ require (
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5 github.com/tidwall/sjson v1.2.5
github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark v1.7.8
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95 go.mau.fi/util v0.8.6
go.mau.fi/webp v0.2.0 go.mau.fi/webp v0.2.0
go.mau.fi/zeroconfig v0.1.3 go.mau.fi/zeroconfig v0.1.3
golang.org/x/crypto v0.34.0 golang.org/x/crypto v0.36.0
golang.org/x/image v0.24.0 golang.org/x/image v0.25.0
golang.org/x/net v0.35.0 golang.org/x/net v0.37.0
golang.org/x/text v0.22.0 golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mauflag v1.0.0 maunium.net/go/mauflag v1.0.0
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 maunium.net/go/mautrix v0.23.2
mvdan.cc/xurls/v2 v2.6.0 mvdan.cc/xurls/v2 v2.6.0
) )
@ -37,11 +37,11 @@ require (
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a // indirect github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.31.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
) )

40
go.sum
View file

@ -16,8 +16,8 @@ github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 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/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13/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 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -42,8 +42,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-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 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a h1:ckxP/kGzsxvxXo8jO6E/0QJ8MMmwI7IRj4Fys9QbAZA= github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -68,30 +68,30 @@ 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/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 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95 h1:5EfVWWjU2Hte9uE6B/hBgvjnVfBx/7SYDZBnsuo+EBs= go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M= go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg= go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q= go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@ -100,7 +100,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 h1:AeNHqITptzOpmfMxnqmQRw6xN7DUDCgsN00BaPyRd4k= maunium.net/go/mautrix v0.23.2 h1:Bo3tPrQJwkxyL7aMmy/T+d2tqIrypZjHqeHe8fyeAOg=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7/go.mod h1:IHMaSJh7YIxMrZSDVefS+nLdr3RbeLowsCSa6ibONZ0= maunium.net/go/mautrix v0.23.2/go.mod h1:pCYLHmo02Jauak/9VlTkbGPrBMvLXsGqTGMNOx+L2PE=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=

View file

@ -157,12 +157,18 @@ func (pn *PushNotification) Split(yield func(*PushNotification) bool) {
} }
func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*database.PushRegistration, notif *PushNotification) { func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*database.PushRegistration, notif *PushNotification) {
log := zerolog.Ctx(ctx).With().
Bool("important", notif.HasImportant).
Int("message_count", len(notif.RawMessages)).
Int("dismiss_count", len(notif.Dismiss)).
Logger()
ctx = log.WithContext(ctx)
rawPayload, err := json.Marshal(notif) rawPayload, err := json.Marshal(notif)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to marshal push notification") log.Err(err).Msg("Failed to marshal push notification")
return return
} else if base64.StdEncoding.EncodedLen(len(rawPayload)) >= 4000 { } else if base64.StdEncoding.EncodedLen(len(rawPayload)) >= 4000 {
zerolog.Ctx(ctx).Error().Msg("Generated push payload too long") log.Error().Msg("Generated push payload too long")
return return
} }
for _, reg := range pushRegs { for _, reg := range pushRegs {
@ -172,7 +178,7 @@ func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*databas
var err error var err error
devicePayload, err = encryptPush(rawPayload, reg.Encryption.Key) devicePayload, err = encryptPush(rawPayload, reg.Encryption.Key)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Str("device_id", reg.DeviceID).Msg("Failed to encrypt push payload") log.Err(err).Str("device_id", reg.DeviceID).Msg("Failed to encrypt push payload")
continue continue
} }
encrypted = true encrypted = true
@ -180,7 +186,7 @@ func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*databas
switch reg.Type { switch reg.Type {
case database.PushTypeFCM: case database.PushTypeFCM:
if !encrypted { if !encrypted {
zerolog.Ctx(ctx).Warn(). log.Warn().
Str("device_id", reg.DeviceID). Str("device_id", reg.DeviceID).
Msg("FCM push registration doesn't have encryption key") Msg("FCM push registration doesn't have encryption key")
continue continue
@ -188,7 +194,7 @@ func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*databas
var token string var token string
err = json.Unmarshal(reg.Data, &token) err = json.Unmarshal(reg.Data, &token)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Str("device_id", reg.DeviceID).Msg("Failed to unmarshal FCM token") log.Err(err).Str("device_id", reg.DeviceID).Msg("Failed to unmarshal FCM token")
continue continue
} }
gmx.SendFCMPush(ctx, token, devicePayload, notif.HasImportant) gmx.SendFCMPush(ctx, token, devicePayload, notif.HasImportant)

View file

@ -80,7 +80,7 @@ const (
AND (type IN ('m.room.message', 'm.sticker') AND (type IN ('m.room.message', 'm.sticker')
OR (type = 'm.room.encrypted' OR (type = 'm.room.encrypted'
AND decrypted_type IN ('m.room.message', 'm.sticker'))) AND decrypted_type IN ('m.room.message', 'm.sticker')))
AND relation_type <> 'm.replace' AND (relation_type IS NULL OR relation_type <> 'm.replace')
AND redacted_by IS NULL AND redacted_by IS NULL
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT 1 LIMIT 1
@ -132,6 +132,9 @@ func (rq *RoomQuery) UpdatePreviewIfLaterOnTimeline(ctx context.Context, roomID
func (rq *RoomQuery) RecalculatePreview(ctx context.Context, roomID id.RoomID) (rowID EventRowID, err error) { func (rq *RoomQuery) RecalculatePreview(ctx context.Context, roomID id.RoomID) (rowID EventRowID, err error) {
err = rq.GetDB().QueryRow(ctx, recalculateRoomPreviewEventQuery, roomID).Scan(&rowID) err = rq.GetDB().QueryRow(ctx, recalculateRoomPreviewEventQuery, roomID).Scan(&rowID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return return
} }
@ -215,7 +218,7 @@ func (r *Room) CheckChangesAndCopyInto(other *Room) (hasChanges bool) {
hasChanges = true hasChanges = true
other.HasMemberList = true other.HasMemberList = true
} }
if r.PreviewEventRowID > other.PreviewEventRowID { if r.PreviewEventRowID != 0 {
other.PreviewEventRowID = r.PreviewEventRowID other.PreviewEventRowID = r.PreviewEventRowID
hasChanges = true hasChanges = true
} }

View file

@ -414,24 +414,42 @@ var HTMLSanitizerImgSrcTemplate = "mxc://%s/%s"
func writeImg(w *strings.Builder, attr []html.Attribute) id.ContentURI { func writeImg(w *strings.Builder, attr []html.Attribute) id.ContentURI {
src, alt, title, isCustomEmoji, width, height := parseImgAttributes(attr) src, alt, title, isCustomEmoji, width, height := parseImgAttributes(attr)
mxc := id.ContentURIString(src).ParseOrIgnore()
if !mxc.IsValid() {
w.WriteString("<span")
writeAttribute(w, "class", "hicli-inline-img-fallback hicli-invalid-inline-img")
w.WriteString(">")
writeEscapedString(w, alt)
w.WriteString("</span>")
return id.ContentURI{}
}
url := fmt.Sprintf(HTMLSanitizerImgSrcTemplate, mxc.Homeserver, mxc.FileID)
w.WriteString("<a")
writeAttribute(w, "class", "hicli-inline-img-fallback hicli-mxc-url")
writeAttribute(w, "title", title)
writeAttribute(w, "style", "display: none;")
writeAttribute(w, "target", "_blank")
writeAttribute(w, "data-mxc", mxc.String())
writeAttribute(w, "href", url)
w.WriteString(">")
writeEscapedString(w, alt)
w.WriteString("</a>")
w.WriteString("<img") w.WriteString("<img")
writeAttribute(w, "alt", alt) writeAttribute(w, "alt", alt)
if title != "" { if title != "" {
writeAttribute(w, "title", title) writeAttribute(w, "title", title)
} }
mxc := id.ContentURIString(src).ParseOrIgnore() writeAttribute(w, "src", url)
if !mxc.IsValid() {
return id.ContentURI{}
}
writeAttribute(w, "src", fmt.Sprintf(HTMLSanitizerImgSrcTemplate, mxc.Homeserver, mxc.FileID))
writeAttribute(w, "loading", "lazy") writeAttribute(w, "loading", "lazy")
if isCustomEmoji { if isCustomEmoji {
writeAttribute(w, "class", "hicli-custom-emoji") writeAttribute(w, "class", "hicli-inline-img hicli-custom-emoji")
} else if cWidth, cHeight, sizeOK := calculateMediaSize(width, height); sizeOK { } else if cWidth, cHeight, sizeOK := calculateMediaSize(width, height); sizeOK {
writeAttribute(w, "class", "hicli-sized-inline-img") writeAttribute(w, "class", "hicli-inline-img hicli-sized-inline-img")
writeAttribute(w, "style", fmt.Sprintf("width: %.2fpx; height: %.2fpx;", cWidth, cHeight)) writeAttribute(w, "style", fmt.Sprintf("width: %.2fpx; height: %.2fpx;", cWidth, cHeight))
} else { } else {
writeAttribute(w, "class", "hicli-sizeless-inline-img") writeAttribute(w, "class", "hicli-inline-img hicli-sizeless-inline-img")
} }
return mxc return mxc
} }

View file

@ -65,7 +65,16 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
}) })
case "set_state": case "set_state":
return unmarshalAndCall(req.Data, func(params *sendStateEventParams) (id.EventID, error) { return unmarshalAndCall(req.Data, func(params *sendStateEventParams) (id.EventID, error) {
return h.SetState(ctx, params.RoomID, params.EventType, params.StateKey, params.Content) return h.SetState(ctx, params.RoomID, params.EventType, params.StateKey, params.Content, mautrix.ReqSendEvent{
UnstableDelay: time.Duration(params.DelayMS) * time.Millisecond,
})
})
case "update_delayed_event":
return unmarshalAndCall(req.Data, func(params *updateDelayedEventParams) (*mautrix.RespUpdateDelayedEvent, error) {
return h.Client.UpdateDelayedEvent(ctx, &mautrix.ReqUpdateDelayedEvent{
DelayID: params.DelayID,
Action: params.Action,
})
}) })
case "set_membership": case "set_membership":
return unmarshalAndCall(req.Data, func(params *setMembershipParams) (any, error) { return unmarshalAndCall(req.Data, func(params *setMembershipParams) (any, error) {
@ -308,6 +317,12 @@ type sendStateEventParams struct {
EventType event.Type `json:"type"` EventType event.Type `json:"type"`
StateKey string `json:"state_key"` StateKey string `json:"state_key"`
Content json.RawMessage `json:"content"` Content json.RawMessage `json:"content"`
DelayMS int `json:"delay_ms"`
}
type updateDelayedEventParams struct {
DelayID string `json:"delay_id"`
Action string `json:"action"`
} }
type setMembershipParams struct { type setMembershipParams struct {

View file

@ -296,12 +296,12 @@ func (h *HiClient) PaginateServer(ctx context.Context, roomID id.RoomID, limit i
if resp.End == "" { if resp.End == "" {
resp.End = database.PrevBatchPaginationComplete resp.End = database.PrevBatchPaginationComplete
} }
if resp.End == database.PrevBatchPaginationComplete || len(resp.Chunk) == 0 { if len(resp.Chunk) == 0 {
err = h.DB.Room.SetPrevBatch(ctx, room.ID, resp.End) err = h.DB.Room.SetPrevBatch(ctx, room.ID, resp.End)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to set prev_batch: %w", err) return nil, fmt.Errorf("failed to set prev_batch: %w", err)
} }
return &PaginationResponse{Events: events, HasMore: resp.End != ""}, nil return &PaginationResponse{Events: events, HasMore: resp.End != database.PrevBatchPaginationComplete}, nil
} }
wakeupSessionRequests := false wakeupSessionRequests := false
err = h.DB.DoTxn(ctx, nil, func(ctx context.Context) error { err = h.DB.DoTxn(ctx, nil, func(ctx context.Context) error {
@ -366,5 +366,5 @@ func (h *HiClient) PaginateServer(ctx context.Context, roomID id.RoomID, limit i
if err == nil && wakeupSessionRequests { if err == nil && wakeupSessionRequests {
h.WakeupRequestQueue() h.WakeupRequestQueue()
} }
return &PaginationResponse{Events: events, HasMore: true}, err return &PaginationResponse{Events: events, HasMore: resp.End != database.PrevBatchPaginationComplete}, err
} }

View file

@ -226,6 +226,7 @@ func (h *HiClient) SetState(
evtType event.Type, evtType event.Type,
stateKey string, stateKey string,
content any, content any,
extra ...mautrix.ReqSendEvent,
) (id.EventID, error) { ) (id.EventID, error) {
room, err := h.DB.Room.Get(ctx, roomID) room, err := h.DB.Room.Get(ctx, roomID)
if err != nil { if err != nil {
@ -233,10 +234,14 @@ func (h *HiClient) SetState(
} else if room == nil { } else if room == nil {
return "", fmt.Errorf("unknown room") return "", fmt.Errorf("unknown room")
} }
resp, err := h.Client.SendStateEvent(ctx, room.ID, evtType, stateKey, content) resp, err := h.Client.SendStateEvent(ctx, room.ID, evtType, stateKey, content, extra...)
if err != nil { if err != nil {
return "", err return "", err
} }
if resp.UnstableDelayID != "" {
// Mildly hacky, but it's fine'
return id.EventID(resp.UnstableDelayID), nil
}
return resp.EventID, nil return resp.EventID, nil
} }
@ -511,6 +516,9 @@ func (h *HiClient) shouldShareKeysToInvitedUsers(ctx context.Context, roomID id.
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get history visibility event") zerolog.Ctx(ctx).Err(err).Msg("Failed to get history visibility event")
return false return false
} else if historyVisibility == nil {
zerolog.Ctx(ctx).Warn().Msg("History visibility event not found")
return false
} }
mautrixEvt := historyVisibility.AsRawMautrix() mautrixEvt := historyVisibility.AsRawMautrix()
err = mautrixEvt.Content.ParseRaw(mautrixEvt.Type) err = mautrixEvt.Content.ParseRaw(mautrixEvt.Type)

View file

@ -137,7 +137,7 @@ func (h *HiClient) maybeDiscardOutboundSession(ctx context.Context, newMembershi
prevMembership = event.Membership(gjson.GetBytes(cs.Content, "membership").Str) prevMembership = event.Membership(gjson.GetBytes(cs.Content, "membership").Str)
} }
if prevMembership == newMembership || if prevMembership == newMembership ||
(prevMembership == event.MembershipInvite && newMembership == event.MembershipJoin) || (prevMembership == event.MembershipInvite && newMembership == event.MembershipJoin && h.shouldShareKeysToInvitedUsers(ctx, evt.RoomID)) ||
(prevMembership == event.MembershipJoin && newMembership == event.MembershipInvite) || (prevMembership == event.MembershipJoin && newMembership == event.MembershipInvite) ||
(prevMembership == event.MembershipBan && newMembership == event.MembershipLeave) || (prevMembership == event.MembershipBan && newMembership == event.MembershipLeave) ||
(prevMembership == event.MembershipLeave && newMembership == event.MembershipBan) { (prevMembership == event.MembershipLeave && newMembership == event.MembershipBan) {
@ -598,7 +598,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
return nil, nil return nil, nil
} }
const CurrentHTMLSanitizerVersion = 8 const CurrentHTMLSanitizerVersion = 10
func (h *HiClient) ReprocessExistingEvent(ctx context.Context, evt *database.Event) { func (h *HiClient) ReprocessExistingEvent(ctx context.Context, evt *database.Event) {
if (evt.Type != event.EventMessage.Type && evt.DecryptedType != event.EventMessage.Type) || if (evt.Type != event.EventMessage.Type && evt.DecryptedType != event.EventMessage.Type) ||
@ -785,7 +785,7 @@ func (h *HiClient) processStateAndTimeline(
return fmt.Errorf("failed to get relation target of redaction target: %w", err) return fmt.Errorf("failed to get relation target of redaction target: %w", err)
} }
} }
if updatedRoom.PreviewEventRowID == dbEvt.RowID { if updatedRoom.PreviewEventRowID == dbEvt.RowID || (updatedRoom.PreviewEventRowID == 0 && room.PreviewEventRowID == dbEvt.RowID) {
updatedRoom.PreviewEventRowID = 0 updatedRoom.PreviewEventRowID = 0
recalculatePreviewEvent = true recalculatePreviewEvent = true
} }
@ -969,12 +969,13 @@ func (h *HiClient) processStateAndTimeline(
updatedRoom.PreviewEventRowID, err = h.DB.Room.RecalculatePreview(ctx, room.ID) updatedRoom.PreviewEventRowID, err = h.DB.Room.RecalculatePreview(ctx, room.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to recalculate preview event: %w", err) return fmt.Errorf("failed to recalculate preview event: %w", err)
} } else if updatedRoom.PreviewEventRowID != 0 {
_, err = addOldEvent(updatedRoom.PreviewEventRowID, "") _, err = addOldEvent(updatedRoom.PreviewEventRowID, "")
if err != nil { if err != nil {
return fmt.Errorf("failed to get preview event: %w", err) return fmt.Errorf("failed to get preview event: %w", err)
} }
} }
}
// Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset // 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 { if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil {
name, dmAvatarURL, dmUserID, err := h.calculateRoomParticipantName(ctx, room.ID, summary) name, dmAvatarURL, dmUserID, err := h.calculateRoomParticipantName(ctx, room.ID, summary)

771
web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -78,11 +78,16 @@ function getFallbackCharacter(from: unknown, idx: number): string {
return Array.from(from.slice(0, (idx + 1) * 2))[idx]?.toUpperCase().toWellFormed() ?? "" return Array.from(from.slice(0, (idx + 1) * 2))[idx]?.toUpperCase().toWellFormed() ?? ""
} }
export const getAvatarURL = (userID: UserID, content?: UserProfile | null, thumbnail = false): string | undefined => { export const getAvatarURL = (
userID: UserID,
content?: UserProfile | null,
thumbnail = false,
forceFallback = false,
): string | undefined => {
const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1) const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1)
const backgroundColor = getUserColor(userID) const backgroundColor = getUserColor(userID)
const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url) const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url)
if (!mediaID) { if (!mediaID || forceFallback) {
return makeFallbackAvatar(backgroundColor, fallbackCharacter) return makeFallbackAvatar(backgroundColor, fallbackCharacter)
} }
const encrypted = !!content?.avatar_file const encrypted = !!content?.avatar_file
@ -91,8 +96,12 @@ export const getAvatarURL = (userID: UserID, content?: UserProfile | null, thumb
return thumbnail ? `${url}&thumbnail=avatar` : url return thumbnail ? `${url}&thumbnail=avatar` : url
} }
export const getAvatarThumbnailURL = (userID: UserID, content?: UserProfile | null): string | undefined => { export const getAvatarThumbnailURL = (
return getAvatarURL(userID, content, true) userID: UserID,
content?: UserProfile | null,
forceFallback = false,
): string | undefined => {
return getAvatarURL(userID, content, true, forceFallback)
} }
interface RoomForAvatarURL { interface RoomForAvatarURL {
@ -104,14 +113,21 @@ interface RoomForAvatarURL {
} }
export const getRoomAvatarURL = ( export const getRoomAvatarURL = (
room: RoomForAvatarURL, avatarOverride?: ContentURI, thumbnail = false, room: RoomForAvatarURL,
avatarOverride?: ContentURI,
thumbnail = false,
forceFallback = false,
): string | undefined => { ): string | undefined => {
return getAvatarURL(room.dm_user_id ?? room.room_id, { return getAvatarURL(room.dm_user_id ?? room.room_id, {
displayname: room.name, displayname: room.name,
avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url, avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url,
}, thumbnail) }, thumbnail, forceFallback)
} }
export const getRoomAvatarThumbnailURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => { export const getRoomAvatarThumbnailURL = (
return getRoomAvatarURL(room, avatarOverride, true) room: RoomForAvatarURL,
avatarOverride?: ContentURI,
forceFallback = false,
): string | undefined => {
return getRoomAvatarURL(room, avatarOverride, true, forceFallback)
} }

View file

@ -174,8 +174,13 @@ export default abstract class RPCClient {
setState( setState(
room_id: RoomID, type: EventType, state_key: string, content: Record<string, unknown>, room_id: RoomID, type: EventType, state_key: string, content: Record<string, unknown>,
extra: { delay_ms?: number } = {},
): Promise<EventID> { ): Promise<EventID> {
return this.request("set_state", { room_id, type, state_key, content }) return this.request("set_state", { room_id, type, state_key, content, ...extra })
}
updateDelayedEvent(delay_id: string, action: string): Promise<void> {
return this.request("update_delayed_event", { delay_id, action })
} }
setMembership(room_id: RoomID, user_id: UserID, action: MembershipAction, reason?: string): Promise<void> { setMembership(room_id: RoomID, user_id: UserID, action: MembershipAction, reason?: string): Promise<void> {

View file

@ -45,6 +45,7 @@ export class InvitedRoomStore implements RoomListEntry, RoomSummary {
readonly invited_by?: UserID readonly invited_by?: UserID
readonly inviter_profile?: MemberEventContent readonly inviter_profile?: MemberEventContent
readonly is_direct: boolean readonly is_direct: boolean
readonly is_invite = true
constructor(public readonly meta: DBInvitedRoom, parent: StateStore) { constructor(public readonly meta: DBInvitedRoom, parent: StateStore) {
this.room_id = meta.room_id this.room_id = meta.room_id

View file

@ -55,6 +55,7 @@ export interface RoomListEntry {
unread_notifications: number unread_notifications: number
unread_highlights: number unread_highlights: number
marked_unread: boolean marked_unread: boolean
is_invite?: boolean
} }
export interface GCSettings { export interface GCSettings {
@ -255,8 +256,10 @@ export class StateStore {
} }
applySync(sync: SyncCompleteData) { applySync(sync: SyncCompleteData) {
let prevActiveRoom: RoomID | null = null
if (sync.clear_state && this.rooms.size > 0) { 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") console.info("Clearing state store as sync told to reset and there are rooms in the store")
prevActiveRoom = this.activeRoomID
this.clear() this.clear()
} }
const resyncRoomList = this.roomList.current.length === 0 const resyncRoomList = this.roomList.current.length === 0
@ -387,6 +390,10 @@ export class StateStore {
this.topLevelSpaces.emit(sync.top_level_spaces) this.topLevelSpaces.emit(sync.top_level_spaces)
this.spaceOrphans.children = sync.top_level_spaces.map(child_id => ({ child_id })) this.spaceOrphans.children = sync.top_level_spaces.map(child_id => ({ child_id }))
} }
if (prevActiveRoom) {
// TODO this will fail if the room is not in the top 100 recent rooms
this.switchRoom?.(prevActiveRoom)
}
} }
invalidateEmojiPackKeyCache() { invalidateEmojiPackKeyCache() {

View file

@ -116,6 +116,9 @@ export interface PolicyRuleContent {
entity: string entity: string
reason: string reason: string
recommendation: string recommendation: string
"org.matrix.msc4205.hashes"?: {
sha256: string
}
} }
export interface PowerLevelEventContent { export interface PowerLevelEventContent {

View file

@ -55,10 +55,22 @@ export const preferences = {
}), }),
show_media_previews: new Preference<boolean>({ show_media_previews: new Preference<boolean>({
displayName: "Show image and video previews", displayName: "Show image and video previews",
description: "If disabled, images and videos will only be visible after clicking and will not be downloaded automatically.", description: "If disabled, images and videos will only be visible after clicking and will not be downloaded automatically. This will also disable images in URL previews.",
allowedContexts: anyContext, allowedContexts: anyContext,
defaultValue: true, defaultValue: true,
}), }),
show_inline_images: new Preference<boolean>({
displayName: "Show inline images",
description: "If disabled, custom emojis and other inline images will not be rendered and the alt attribute will be shown instead.",
allowedContexts: anyContext,
defaultValue: true,
}),
show_invite_avatars: new Preference<boolean>({
displayName: "Show avatars in invites",
description: "If disabled, the avatar of the room or inviter will not be shown in the invite view.",
allowedContexts: anyGlobalContext,
defaultValue: true,
}),
code_block_line_wrap: new Preference<boolean>({ code_block_line_wrap: new Preference<boolean>({
displayName: "Code block line wrap", displayName: "Code block line wrap",
description: "Whether to wrap long lines in code blocks instead of scrolling horizontally.", description: "Whether to wrap long lines in code blocks instead of scrolling horizontally.",

View file

@ -167,7 +167,6 @@ body {
padding: 0; padding: 0;
background-color: var(--background-color); background-color: var(--background-color);
line-height: 1.5; line-height: 1.5;
font-size: 16px;
touch-action: none; touch-action: none;
color: var(--text-color); color: var(--text-color);
min-height: 100vh; min-height: 100vh;
@ -176,6 +175,7 @@ body {
html { html {
touch-action: none; touch-action: none;
background-color: var(--background-color); background-color: var(--background-color);
font-size: 16px;
} }
#root { #root {

View file

@ -117,6 +117,15 @@ const StylePreferences = ({ client, activeRoom }: StylePreferencesProps) => {
--timeline-status-size: 2rem; --timeline-status-size: 2rem;
} }
`, [preferences.display_read_receipts]) `, [preferences.display_read_receipts])
useStyle(() => !preferences.show_inline_images && css`
a.hicli-inline-img-fallback {
display: inline !important;
}
img.hicli-inline-img {
display: none;
}
`, [preferences.show_inline_images])
useAsyncStyle(() => preferences.code_block_theme === "auto" ? ` useAsyncStyle(() => preferences.code_block_theme === "auto" ? `
@import url("_gomuks/codeblock/github.css") (prefers-color-scheme: light); @import url("_gomuks/codeblock/github.css") (prefers-color-scheme: light);
@import url("_gomuks/codeblock/github-dark.css") (prefers-color-scheme: dark); @import url("_gomuks/codeblock/github-dark.css") (prefers-color-scheme: dark);

View file

@ -181,10 +181,5 @@ export const useSecondaryItems = (
title={pendingTitle} title={pendingTitle}
className="redact-button" className="redact-button"
><DeleteIcon/>{names && "Remove"}</button>} ><DeleteIcon/>{names && "Remove"}</button>}
{canUnredact && (evt.viewing_redacted ? <button onClick={onClickHideUnredacted}>
<DeleteIcon/>{names && "Hide content"}
</button> : <button onClick={onClickUnredact}>
<RestoreTrashIcon/>{names && "View content"}
</button>)}
</> </>
} }

View file

@ -30,7 +30,7 @@ div.overlay {
} }
> div.modal-box-inner { > div.modal-box-inner {
overflow: scroll; overflow: auto;
} }
} }
} }

View file

@ -29,7 +29,7 @@ const ModalWrapper = ({ children, ContextType, historyStateKey }: ModalWrapperPr
return newState return newState
}, null) }, null)
const onClickWrapper = useCallback((evt?: React.MouseEvent) => { const onClickWrapper = useCallback((evt?: React.MouseEvent) => {
if (evt && evt.target !== evt.currentTarget) { if (evt && (evt.target !== evt.currentTarget || state?.noDismiss)) {
return return
} }
evt?.stopPropagation() evt?.stopPropagation()
@ -37,9 +37,9 @@ const ModalWrapper = ({ children, ContextType, historyStateKey }: ModalWrapperPr
if (history.state?.[historyStateKey]) { if (history.state?.[historyStateKey]) {
history.back() history.back()
} }
}, [historyStateKey]) }, [historyStateKey, state])
const onKeyWrapper = (evt: React.KeyboardEvent<HTMLDivElement>) => { const onKeyWrapper = (evt: React.KeyboardEvent<HTMLDivElement>) => {
if (evt.key === "Escape") { if (evt.key === "Escape" && !state?.noDismiss) {
setState(null) setState(null)
if (history.state?.[historyStateKey]) { if (history.state?.[historyStateKey]) {
history.back() history.back()
@ -55,12 +55,17 @@ const ModalWrapper = ({ children, ContextType, historyStateKey }: ModalWrapperPr
}, [historyStateKey]) }, [historyStateKey])
const wrapperRef = useRef<HTMLDivElement>(null) const wrapperRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => { useLayoutEffect(() => {
window.closeModal = onClickWrapper
if (historyStateKey === "nestable_modal") {
window.openNestableModal = openModal
} else {
window.openModal = openModal
}
if (wrapperRef.current && (!document.activeElement || !wrapperRef.current.contains(document.activeElement))) { if (wrapperRef.current && (!document.activeElement || !wrapperRef.current.contains(document.activeElement))) {
wrapperRef.current.focus() wrapperRef.current.focus()
} }
}, [state]) }, [state, onClickWrapper, historyStateKey, openModal])
useEffect(() => { useEffect(() => {
window.closeModal = onClickWrapper
const listener = (evt: PopStateEvent) => { const listener = (evt: PopStateEvent) => {
if (!evt.state?.[historyStateKey]) { if (!evt.state?.[historyStateKey]) {
setState(null) setState(null)
@ -68,7 +73,7 @@ const ModalWrapper = ({ children, ContextType, historyStateKey }: ModalWrapperPr
} }
window.addEventListener("popstate", listener) window.addEventListener("popstate", listener)
return () => window.removeEventListener("popstate", listener) return () => window.removeEventListener("popstate", listener)
}, [historyStateKey, onClickWrapper]) }, [historyStateKey])
let modal: JSX.Element | null = null let modal: JSX.Element | null = null
if (state) { if (state) {
let content = <ModalCloseContext value={onClickWrapper}> let content = <ModalCloseContext value={onClickWrapper}>
@ -97,9 +102,6 @@ const ModalWrapper = ({ children, ContextType, historyStateKey }: ModalWrapperPr
modal = content modal = content
} }
} }
if (historyStateKey === "nestable_modal") {
window.openNestableModal = openModal
}
return <ContextType value={openModal}> return <ContextType value={openModal}>
{children} {children}
{modal} {modal}

View file

@ -33,6 +33,7 @@ export interface ModalState {
innerBoxClass?: string innerBoxClass?: string
onClose?: () => void onClose?: () => void
captureInput?: boolean captureInput?: boolean
noDismiss?: boolean
} }
export type openModal = (state: ModalState) => void export type openModal = (state: ModalState) => void

View file

@ -68,8 +68,6 @@ div.right-panel-content.widgets {
} }
div.right-panel-content.user { div.right-panel-content.user {
display: flex;
flex-direction: column;
padding: 1rem; padding: 1rem;
div.avatar-container { div.avatar-container {
@ -79,7 +77,6 @@ div.right-panel-content.user {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 1rem; padding: 1rem;
flex-shrink: 0;
margin: 0 auto; margin: 0 auto;
> img { > img {
@ -105,6 +102,7 @@ div.right-panel-content.user {
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
word-break: break-word; word-break: break-word;
overflow: hidden;
} }
div.userid, div.extended-profile, div.devices, div.user-moderation, div.mutual-rooms, div.errors { div.userid, div.extended-profile, div.devices, div.user-moderation, div.mutual-rooms, div.errors {

View file

@ -16,7 +16,7 @@
import type { IWidget } from "matrix-widget-api" import type { IWidget } from "matrix-widget-api"
import { JSX, use } from "react" import { JSX, use } from "react"
import type { UserID } from "@/api/types" import type { UserID } from "@/api/types"
import MainScreenContext from "../MainScreenContext.ts" import MainScreenContext, { MainScreenContextFields } from "../MainScreenContext.ts"
import ErrorBoundary from "../util/ErrorBoundary.tsx" import ErrorBoundary from "../util/ErrorBoundary.tsx"
import ElementCall from "../widget/ElementCall.tsx" import ElementCall from "../widget/ElementCall.tsx"
import LazyWidget from "../widget/LazyWidget.tsx" import LazyWidget from "../widget/LazyWidget.tsx"
@ -63,7 +63,7 @@ function getTitle(props: RightPanelProps): string {
} }
} }
function renderRightPanelContent(props: RightPanelProps): JSX.Element | null { function renderRightPanelContent(props: RightPanelProps, mainScreen: MainScreenContextFields): JSX.Element | null {
switch (props.type) { switch (props.type) {
case "pinned-messages": case "pinned-messages":
return <PinnedMessages /> return <PinnedMessages />
@ -72,9 +72,9 @@ function renderRightPanelContent(props: RightPanelProps): JSX.Element | null {
case "widgets": case "widgets":
return <WidgetList /> return <WidgetList />
case "element-call": case "element-call":
return <ElementCall /> return <ElementCall onClose={mainScreen.closeRightPanel} />
case "widget": case "widget":
return <LazyWidget info={props.info} /> return <LazyWidget info={props.info} onClose={mainScreen.closeRightPanel} />
case "user": case "user":
return <UserInfo userID={props.userID} /> return <UserInfo userID={props.userID} />
} }
@ -104,7 +104,7 @@ const RightPanel = (props: RightPanelProps) => {
</div> </div>
<div className={`right-panel-content ${props.type}`}> <div className={`right-panel-content ${props.type}`}>
<ErrorBoundary thing="right panel content"> <ErrorBoundary thing="right panel content">
{renderRightPanelContent(props)} {renderRightPanelContent(props, mainScreen)}
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</div> </div>

View file

@ -29,6 +29,7 @@ export interface RoomListEntryProps {
room: RoomListEntry room: RoomListEntry
isActive: boolean isActive: boolean
hidden: boolean hidden: boolean
hideAvatar?: boolean
} }
function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): [string, JSX.Element | null] { function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null): [string, JSX.Element | null] {
@ -57,7 +58,7 @@ function getPreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent | null):
return ["", null] return ["", null]
} }
function renderEntry(room: RoomListEntry) { function renderEntry(room: RoomListEntry, hideAvatar: boolean | undefined) {
const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender) const [previewText, croppedPreviewText] = getPreviewText(room.preview_event, room.preview_sender)
return <> return <>
@ -65,7 +66,7 @@ function renderEntry(room: RoomListEntry) {
<img <img
loading="lazy" loading="lazy"
className="avatar room-avatar" className="avatar room-avatar"
src={getRoomAvatarThumbnailURL(room)} src={getRoomAvatarThumbnailURL(room, undefined, hideAvatar)}
alt="" alt=""
/> />
</div> </div>
@ -77,7 +78,7 @@ function renderEntry(room: RoomListEntry) {
</> </>
} }
const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => { const Entry = ({ room, isActive, hidden, hideAvatar }: RoomListEntryProps) => {
const [isVisible, divRef] = useContentVisibility<HTMLDivElement>() const [isVisible, divRef] = useContentVisibility<HTMLDivElement>()
const openModal = use(ModalContext) const openModal = use(ModalContext)
const mainScreen = use(MainScreenContext) const mainScreen = use(MainScreenContext)
@ -105,7 +106,7 @@ const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => {
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
data-room-id={room.room_id} data-room-id={room.room_id}
> >
{isVisible ? renderEntry(room) : null} {isVisible ? renderEntry(room, hideAvatar) : null}
</div> </div>
} }

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { use, useCallback, useRef, useState } from "react" import React, { use, useCallback, useRef, useState } from "react"
import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore" import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts, usePreference } from "@/api/statestore"
import type { RoomID } from "@/api/types" import type { RoomID } from "@/api/types"
import { useEventAsState } from "@/util/eventdispatcher.ts" import { useEventAsState } from "@/util/eventdispatcher.ts"
import reverseMap from "@/util/reversemap.ts" import reverseMap from "@/util/reversemap.ts"
@ -103,6 +103,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
} }
} }
const showInviteAvatars = usePreference(client.store, null, "show_invite_avatars")
const roomListFilter = client.store.roomListFilterFunc const roomListFilter = client.store.roomListFilterFunc
return <div className="room-list-wrapper"> return <div className="room-list-wrapper">
<div className="room-search-wrapper"> <div className="room-search-wrapper">
@ -145,6 +146,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
isActive={room.room_id === activeRoomID} isActive={room.room_id === activeRoomID}
hidden={roomListFilter ? !roomListFilter(room) : false} hidden={roomListFilter ? !roomListFilter(room) : false}
room={room} room={room}
hideAvatar={room.is_invite && !showInviteAvatars}
/>, />,
)} )}
</div> </div>

View file

@ -16,6 +16,7 @@
import { use, useEffect, useState } from "react" import { use, useEffect, useState } from "react"
import { ScaleLoader } from "react-spinners" import { ScaleLoader } from "react-spinners"
import { getAvatarThumbnailURL, getAvatarURL, getRoomAvatarURL } from "@/api/media.ts" import { getAvatarThumbnailURL, getAvatarURL, getRoomAvatarURL } from "@/api/media.ts"
import { usePreference } from "@/api/statestore/hooks.ts"
import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts" import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts"
import { RoomID, RoomSummary } from "@/api/types" import { RoomID, RoomSummary } from "@/api/types"
import { getDisplayname, getServerName } from "@/util/validation.ts" import { getDisplayname, getServerName } from "@/util/validation.ts"
@ -84,13 +85,15 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
const name = summary?.name ?? summary?.canonical_alias ?? invite?.name ?? invite?.canonical_alias ?? alias ?? roomID const name = summary?.name ?? summary?.canonical_alias ?? invite?.name ?? invite?.canonical_alias ?? alias ?? roomID
const memberCount = summary?.num_joined_members || null const memberCount = summary?.num_joined_members || null
const topic = summary?.topic ?? invite?.topic ?? "" const topic = summary?.topic ?? invite?.topic ?? ""
const showInviteAvatars = usePreference(client.store, null, "show_invite_avatars")
const noAvatarPreview = invite && !showInviteAvatars
return <div className="room-view preview"> return <div className="room-view preview">
<div className="preview-inner"> <div className="preview-inner">
{invite?.invited_by && !invite.dm_user_id ? <div className="inviter-info"> {invite?.invited_by && !invite.dm_user_id ? <div className="inviter-info">
<img <img
className="small avatar" className="small avatar"
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
src={getAvatarThumbnailURL(invite.invited_by, invite.inviter_profile)} src={getAvatarThumbnailURL(invite.invited_by, invite.inviter_profile, noAvatarPreview)}
data-full-src={getAvatarURL(invite.invited_by, invite.inviter_profile)} data-full-src={getAvatarURL(invite.invited_by, invite.inviter_profile)}
alt="" alt=""
/> />
@ -102,7 +105,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
<h2 className="room-name">{name}</h2> <h2 className="room-name">{name}</h2>
<img <img
// this is a big avatar (120px), use full resolution // this is a big avatar (120px), use full resolution
src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID })} src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID }, undefined, false, noAvatarPreview)}
data-full-src={getRoomAvatarURL(invite ?? summary ?? { room_id: roomID })}
className="large avatar" className="large avatar"
onClick={use(LightboxContext)} onClick={use(LightboxContext)}
alt="" alt=""

View file

@ -23,6 +23,7 @@ blockquote.reply-body {
height: calc(var(--small-font-size) * 1.5); height: calc(var(--small-font-size) * 1.5);
border-left: none; border-left: none;
padding: 0; padding: 0;
overflow: hidden;
> div.reply-spine { > div.reply-spine {
margin-top: calc(var(--small-font-size) * 0.75 - 1px); margin-top: calc(var(--small-font-size) * 0.75 - 1px);

View file

@ -246,10 +246,15 @@ div.event-content > div.event-reactions {
} }
} }
blockquote.reply-body.small > div.reply-sender > span.event-sender {
max-width: 10rem;
}
div.small-event > div.sender-avatar, blockquote.reply-body > div.reply-sender > div.sender-avatar { div.small-event > div.sender-avatar, blockquote.reply-body > div.reply-sender > div.sender-avatar {
margin-top: 0; margin-top: 0;
display: flex; display: flex;
align-items: center; align-items: center;
flex-shrink: 0;
} }
div.date-separator { div.date-separator {

View file

@ -28,6 +28,8 @@ const URLPreviews = ({ event, room }: {
}) => { }) => {
const client = use(ClientContext)! const client = use(ClientContext)!
const renderPreviews = usePreference(client.store, room, "render_url_previews") const renderPreviews = usePreference(client.store, room, "render_url_previews")
// TODO support blurhashes and clicking to view image previews here?
const showPreviewImages = usePreference(client.store, room, "show_media_previews")
if (event.redacted_by || !renderPreviews) { if (event.redacted_by || !renderPreviews) {
return null return null
} }
@ -72,7 +74,7 @@ const URLPreviews = ({ event, room }: {
<a href={url} title={title} target="_blank" rel="noreferrer noopener">{title}</a> <a href={url} title={title} target="_blank" rel="noreferrer noopener">{title}</a>
</div> </div>
<div className="description" title={p["og:description"]}>{p["og:description"]}</div> <div className="description" title={p["og:description"]}>{p["og:description"]}</div>
{mediaURL && (inline {mediaURL && showPreviewImages && (inline
? <div className="inline-media-wrapper">{mediaContainer}</div> ? <div className="inline-media-wrapper">{mediaContainer}</div>
: mediaContainer)} : mediaContainer)}
</div> </div>

View file

@ -99,6 +99,8 @@ function useChangeDescription(
if (sender === target) { if (sender === target) {
if (prevContent?.membership === "knock") { if (prevContent?.membership === "knock") {
return "cancelled their join request" return "cancelled their join request"
} else if (prevContent?.membership === "invite") {
return "rejected the invite"
} }
return "left the room" return "left the room"
} }

View file

@ -25,14 +25,21 @@ const PolicyRuleBody = ({ event, sender }: EventContentProps) => {
const mainScreen = use(MainScreenContext) const mainScreen = use(MainScreenContext)
const entity = content.entity ?? prevContent?.entity const entity = content.entity ?? prevContent?.entity
const hashedEntity = content["org.matrix.msc4205.hashes"]?.sha256
?? prevContent?.["org.matrix.msc4205.hashes"]?.sha256
const recommendation = content.recommendation ?? prevContent?.recommendation const recommendation = content.recommendation ?? prevContent?.recommendation
if (!entity || !recommendation) { if ((!entity && !hashedEntity) || !recommendation) {
return <div className="policy-body"> return <div className="policy-body">
{getDisplayname(event.sender, sender?.content)} sent an invalid policy rule {getDisplayname(event.sender, sender?.content)} sent an invalid policy rule
</div> </div>
} }
const target = event.type.replace(/^m\.policy\.rule\./, "")
let entityElement = <>{entity}</> let entityElement = <>{entity}</>
if(event.type === "m.policy.rule.user" && !entity?.includes("*") && !entity?.includes("?")) { let matchingWord = `${target}s matching`
if (!entity && hashedEntity) {
matchingWord = `the ${target} with hash`
entityElement = <>{hashedEntity}</>
} else if (event.type === "m.policy.rule.user" && entity && !entity.includes("*") && !entity.includes("?")) {
entityElement = ( entityElement = (
<a <a
className="hicli-matrix-uri hicli-matrix-uri-user" className="hicli-matrix-uri hicli-matrix-uri-user"
@ -44,16 +51,18 @@ const PolicyRuleBody = ({ event, sender }: EventContentProps) => {
{entity} {entity}
</a> </a>
) )
matchingWord = "user"
} }
let recommendationElement: JSX.Element | string = <code>{recommendation}</code> let recommendationElement: JSX.Element | string = <code>{recommendation}</code>
if (recommendation === "m.ban") { if (recommendation === "m.ban") {
recommendationElement = "ban" recommendationElement = "ban"
} else if (recommendation === "org.matrix.msc4204.takedown") {
recommendationElement = "takedown"
} }
const action = prevContent ? ((content.entity && content.recommendation) ? "updated" : "removed") : "added" const action = prevContent ? ((content.entity && content.recommendation) ? "updated" : "removed") : "added"
const target = event.type.replace(/^m\.policy\.rule\./, "")
return <div className="policy-body"> return <div className="policy-body">
{getDisplayname(event.sender, sender?.content)} {action} a {recommendationElement} rule {getDisplayname(event.sender, sender?.content)} {action} a {recommendationElement} rule
for {target}s matching <code>{entityElement}</code> for {matchingWord} <code>{entityElement}</code>
{content.reason ? <> for <code>{content.reason}</code></> : null} {content.reason ? <> for <code>{content.reason}</code></> : null}
</div> </div>
} }

View file

@ -33,7 +33,7 @@ const elementCallParams = new URLSearchParams({
appPrompt: "false", appPrompt: "false",
}).toString().replaceAll("%24", "$") }).toString().replaceAll("%24", "$")
const ElementCall = () => { const ElementCall = ({ onClose }: { onClose?: () => void }) => {
const room = use(RoomContext)?.store ?? null const room = use(RoomContext)?.store ?? null
const client = use(ClientContext)! const client = use(ClientContext)!
const baseURL = usePreference(client.store, room, "element_call_base_url") const baseURL = usePreference(client.store, room, "element_call_base_url")
@ -51,7 +51,7 @@ const ElementCall = () => {
if (!room || !client) { if (!room || !client) {
return null return null
} }
return <LazyWidget info={widgetInfo} /> return <LazyWidget info={widgetInfo} onClose={onClose} />
} }
export default ElementCall export default ElementCall

View file

@ -28,9 +28,10 @@ const widgetLoader = <div className="widget-container widget-loading">
export interface LazyWidgetProps { export interface LazyWidgetProps {
info: IWidget info: IWidget
onClose?: () => void
} }
const LazyWidget = ({ info }: LazyWidgetProps) => { const LazyWidget = ({ info, onClose }: LazyWidgetProps) => {
const room = use(RoomContext)?.store const room = use(RoomContext)?.store
const client = use(ClientContext) const client = use(ClientContext)
if (!room || !client) { if (!room || !client) {
@ -38,7 +39,7 @@ const LazyWidget = ({ info }: LazyWidgetProps) => {
} }
return ( return (
<Suspense fallback={widgetLoader}> <Suspense fallback={widgetLoader}>
<Widget info={info} room={room} client={client} /> <Widget info={info} room={room} client={client} onClose={onClose} />
</Suspense> </Suspense>
) )
} }

View file

@ -0,0 +1,111 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
import { MatrixCapabilities } from "matrix-widget-api"
import { use, useState } from "react"
import { ModalCloseContext } from "../modal"
interface PermissionPromptProps {
capabilities: Set<string>
onConfirm: (approvedCapabilities: Set<string>) => void
}
const getCapabilityName = (capability: string): string => {
const paramIdx = capability.indexOf(":")
const capabilityID = paramIdx === -1 ? capability : capability.slice(0, paramIdx)
const parameter = paramIdx === -1 ? null : capability.slice(paramIdx + 1)
// Map capability IDs to human-readable names
const capabilityNames: Record<string, string> = {
[MatrixCapabilities.MSC2931Navigate]: "Navigate to other rooms",
[MatrixCapabilities.MSC3846TurnServers]: "Request TURN servers from the homeserver",
[MatrixCapabilities.MSC4157SendDelayedEvent]: "Send delayed events",
[MatrixCapabilities.MSC4157UpdateDelayedEvent]: "Update delayed events",
[MatrixCapabilities.MSC4039UploadFile]: "Upload files",
[MatrixCapabilities.MSC4039DownloadFile]: "Download files",
"org.matrix.msc2762.timeline": "Read room history",
"org.matrix.msc2762.send.event": "Send timeline events",
"org.matrix.msc2762.receive.event": "Receive timeline events",
"org.matrix.msc2762.send.state_event": "Send state events",
"org.matrix.msc2762.receive.state_event": "Receive state events",
"org.matrix.msc3819.send.to_device": "Send to-device events",
"org.matrix.msc3819.receive.to_device": "Receive to-device events",
}
const name = capabilityNames[capabilityID] || capabilityID
if (parameter) {
return `${name} (${parameter})`
}
return name
}
const PermissionPrompt = ({ capabilities, onConfirm }: PermissionPromptProps) => {
const [selectedCapabilities, setSelectedCapabilities] = useState<Set<string>>(() => new Set(capabilities))
const closeModal = use(ModalCloseContext)
const handleToggleCapability = (capability: string) => {
const newCapabilities = new Set(selectedCapabilities)
if (newCapabilities.has(capability)) {
newCapabilities.delete(capability)
} else {
newCapabilities.add(capability)
}
setSelectedCapabilities(newCapabilities)
}
const doConfirm = () => {
onConfirm(selectedCapabilities)
closeModal()
}
const doReject = () => {
onConfirm(new Set())
closeModal()
}
return <>
<h2>Widget Permissions</h2>
<p>This widget is requesting the following permissions:</p>
<div className="capability-list">
{Array.from(capabilities).map((capability) => (
<div key={capability} className="capability-item">
<label>
<input
type="checkbox"
checked={selectedCapabilities.has(capability)}
onChange={() => handleToggleCapability(capability)}
/>
{getCapabilityName(capability)}
</label>
</div>
))}
</div>
<div className="permission-actions">
<button onClick={doReject}>Reject all</button>
<button
onClick={doConfirm}
className="confirm-button"
>
Accept selected
</button>
</div>
</>
}
export default PermissionPrompt

View file

@ -1,9 +1,25 @@
div.right-panel-content.widget, div.right-panel-content.element-call { div.right-panel-content.widget, div.right-panel-content.element-call {
overflow: hidden !important; overflow: hidden !important;
> iframe.widget-iframe { }
iframe.widget-iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
}
div.permission-prompt {
> h2 {
margin: 0;
}
> div.permission-actions {
display: flex;
justify-content: space-between;
> button {
padding: .5rem 1rem;
}
} }
} }

View file

@ -19,6 +19,7 @@ import type Client from "@/api/client"
import type { RoomStateStore, WidgetListener } from "@/api/statestore" import type { RoomStateStore, WidgetListener } from "@/api/statestore"
import type { MemDBEvent, RoomID, SyncToDevice } from "@/api/types" import type { MemDBEvent, RoomID, SyncToDevice } from "@/api/types"
import { getDisplayname } from "@/util/validation" import { getDisplayname } from "@/util/validation"
import PermissionPrompt from "./PermissionPrompt"
import { memDBEventToIRoomEvent } from "./util" import { memDBEventToIRoomEvent } from "./util"
import GomuksWidgetDriver from "./widgetDriver" import GomuksWidgetDriver from "./widgetDriver"
import "./Widget.css" import "./Widget.css"
@ -27,6 +28,7 @@ export interface WidgetProps {
info: IWidget info: IWidget
room: RoomStateStore room: RoomStateStore
client: Client client: Client
onClose?: () => void
} }
// TODO remove this after widgets start using a parameter for it // TODO remove this after widgets start using a parameter for it
@ -68,9 +70,24 @@ class WidgetListenerImpl implements WidgetListener {
} }
} }
const ReactWidget = ({ room, info, client }: WidgetProps) => { const openPermissionPrompt = (requested: Set<string>): Promise<Set<string>> => {
return new Promise(resolve => {
window.openModal({
content: <PermissionPrompt
capabilities={requested}
onConfirm={resolve}
/>,
dimmed: true,
boxed: true,
noDismiss: true,
innerBoxClass: "permission-prompt",
})
})
}
const ReactWidget = ({ room, info, client, onClose }: WidgetProps) => {
const wrappedWidget = new WrappedWidget(info) const wrappedWidget = new WrappedWidget(info)
const driver = new GomuksWidgetDriver(client, room) const driver = new GomuksWidgetDriver(client, room, openPermissionPrompt)
const widgetURL = addLegacyParams(wrappedWidget.getCompleteUrl({ const widgetURL = addLegacyParams(wrappedWidget.getCompleteUrl({
widgetRoomId: room.roomID, widgetRoomId: room.roomID,
currentUserId: client.userID, currentUserId: client.userID,
@ -92,13 +109,16 @@ const ReactWidget = ({ room, info, client }: WidgetProps) => {
evt.preventDefault() evt.preventDefault()
clientAPI.transport.reply(evt.detail, {}) clientAPI.transport.reply(evt.detail, {})
} }
const closeWidget = (evt: CustomEvent) => {
noopReply(evt)
onClose?.()
}
clientAPI.on("action:io.element.join", noopReply) clientAPI.on("action:io.element.join", noopReply)
clientAPI.on("action:im.vector.hangup", noopReply) clientAPI.on("action:im.vector.hangup", noopReply)
clientAPI.on("action:io.element.device_mute", noopReply) clientAPI.on("action:io.element.device_mute", noopReply)
clientAPI.on("action:io.element.tile_layout", noopReply) clientAPI.on("action:io.element.tile_layout", noopReply)
clientAPI.on("action:io.element.spotlight_layout", noopReply) clientAPI.on("action:io.element.spotlight_layout", noopReply)
// TODO handle this one? clientAPI.on("action:io.element.close", closeWidget)
clientAPI.on("action:io.element.close", noopReply)
clientAPI.on("action:set_always_on_screen", noopReply) clientAPI.on("action:set_always_on_screen", noopReply)
const removeListener = client.addWidgetListener(new WidgetListenerImpl(clientAPI)) const removeListener = client.addWidgetListener(new WidgetListenerImpl(clientAPI))

View file

@ -19,11 +19,13 @@ import {
IOpenIDUpdate, IOpenIDUpdate,
IRoomAccountData, IRoomAccountData,
IRoomEvent, IRoomEvent,
ISendDelayedEventDetails,
ISendEventDetails, ISendEventDetails,
ITurnServer, ITurnServer,
OpenIDRequestState, OpenIDRequestState,
SimpleObservable, SimpleObservable,
Symbols, Symbols,
UpdateDelayedEventAction,
WidgetDriver, WidgetDriver,
} from "matrix-widget-api" } from "matrix-widget-api"
import Client from "@/api/client.ts" import Client from "@/api/client.ts"
@ -35,12 +37,16 @@ class GomuksWidgetDriver extends WidgetDriver {
private openIDToken: IOpenIDCredentials | null = null private openIDToken: IOpenIDCredentials | null = null
private openIDExpiry: number | null = null private openIDExpiry: number | null = null
constructor(private client: Client, private room: RoomStateStore) { constructor(
private client: Client,
private room: RoomStateStore,
private openPermissionPrompt: (requested: Set<string>) => Promise<Set<string>>,
) {
super() super()
} }
async validateCapabilities(requested: Set<string>): Promise<Set<string>> { async validateCapabilities(requested: Set<string>): Promise<Set<string>> {
return new Set(requested) return this.openPermissionPrompt(requested)
} }
async sendEvent( async sendEvent(
@ -62,23 +68,31 @@ class GomuksWidgetDriver extends WidgetDriver {
} }
} }
// async sendDelayedEvent( async sendDelayedEvent(
// delay: number | null, delay: number | null,
// parentDelayID: string | null, parentDelayID: string | null,
// eventType: string, eventType: string,
// content: unknown, content: unknown,
// stateKey: string | null = null, stateKey: string | null = null,
// roomID: string | null = null, roomID: string | null = null,
// ): Promise<ISendDelayedEventDetails> { ): Promise<ISendDelayedEventDetails> {
// if (!isRecord(content)) { if (!isRecord(content)) {
// throw new Error("Content must be an object") throw new Error("Content must be an object")
// } } else if (stateKey === null) {
// throw new Error("Delayed events are not supported") throw new Error("Non-state delayed events are not supported")
// } } else if (parentDelayID !== null) {
throw new Error("Parent delayed events are not supported")
} else if (!delay) {
throw new Error("Delay must be a number")
}
roomID = roomID ?? this.room.roomID
const delayID = await this.client.rpc.setState(roomID, eventType, stateKey, content, { delay_ms: delay })
return { delayId: delayID, roomId: roomID }
}
// async updateDelayedEvent(delayID: string, action: UpdateDelayedEventAction): Promise<void> { async updateDelayedEvent(delayID: string, action: UpdateDelayedEventAction): Promise<void> {
// throw new Error("Delayed events are not supported") await this.client.rpc.updateDelayedEvent(delayID, action)
// } }
async sendToDevice( async sendToDevice(
eventType: string, eventType: string,

View file

@ -17,6 +17,7 @@ declare global {
gcSettings: GCSettings gcSettings: GCSettings
hackyOpenEventContextMenu?: string hackyOpenEventContextMenu?: string
closeModal: () => void closeModal: () => void
openModal: openModal
openNestableModal: openModal openNestableModal: openModal
gomuksAndroid?: true gomuksAndroid?: true
} }