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
![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)
# nyxmuks
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
[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>.
# why?
## Docs
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)
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.

View file

@ -8,7 +8,7 @@ require github.com/wailsapp/wails/v3 v3.0.0-alpha.9
require (
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 (
@ -22,7 +22,7 @@ require (
github.com/buckket/go-blurhash v1.1.0 // indirect
github.com/chzyer/readline v1.5.1 // 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/cyphar/filepath-securejoin v0.2.5 // 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-isatty v0.0.20 // 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/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@ -66,20 +66,20 @@ require (
github.com/yuin/goldmark v1.7.8 // indirect
go.mau.fi/webp v0.2.0 // indirect
go.mau.fi/zeroconfig v0.1.3 // indirect
golang.org/x/crypto v0.34.0 // indirect
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.30.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/image v0.25.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 // indirect
maunium.net/go/mautrix v0.23.2 // 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.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
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/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a h1:ckxP/kGzsxvxXo8jO6E/0QJ8MMmwI7IRj4Fys9QbAZA=
github.com/petermattis/goid v0.0.0-20250211185408-f2b9d978cd7a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
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/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
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.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
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-0.20250227184636-7ff63b0b9d95/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M=
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
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/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
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.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.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
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.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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.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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
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-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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
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-20191026070338-33540a1f6037/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.6.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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
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.3/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.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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
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=
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=
@ -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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 h1:AeNHqITptzOpmfMxnqmQRw6xN7DUDCgsN00BaPyRd4k=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7/go.mod h1:IHMaSJh7YIxMrZSDVefS+nLdr3RbeLowsCSa6ibONZ0=
maunium.net/go/mautrix v0.23.2 h1:Bo3tPrQJwkxyL7aMmy/T+d2tqIrypZjHqeHe8fyeAOg=
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/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=

22
go.mod
View file

@ -2,13 +2,13 @@ module go.mau.fi/gomuks
go 1.23.0
toolchain go1.24.0
toolchain go1.24.1
require (
github.com/alecthomas/chroma/v2 v2.15.0
github.com/buckket/go-blurhash v1.1.0
github.com/chzyer/readline v1.5.1
github.com/coder/websocket v1.8.12
github.com/coder/websocket v1.8.13
github.com/disintegration/imaging v1.6.2
github.com/gabriel-vasile/mimetype v1.4.8
github.com/lucasb-eyer/go-colorful v1.2.0
@ -18,16 +18,16 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/yuin/goldmark v1.7.8
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95
go.mau.fi/util v0.8.6
go.mau.fi/webp v0.2.0
go.mau.fi/zeroconfig v0.1.3
golang.org/x/crypto v0.34.0
golang.org/x/image v0.24.0
golang.org/x/net v0.35.0
golang.org/x/text v0.22.0
golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.0
golang.org/x/net v0.37.0
golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1
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
)
@ -37,11 +37,11 @@ require (
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/mattn/go-colorable v0.1.14 // 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/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // 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/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
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.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
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/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
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-20250211185408-f2b9d978cd7a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95 h1:5EfVWWjU2Hte9uE6B/hBgvjnVfBx/7SYDZBnsuo+EBs=
go.mau.fi/util v0.8.6-0.20250227184636-7ff63b0b9d95/go.mod h1:Ycug9mrbztlahHPEJ6H5r8Nu/xqZaWbE5vPHVWmfz6M=
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
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/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
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.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7 h1:AeNHqITptzOpmfMxnqmQRw6xN7DUDCgsN00BaPyRd4k=
maunium.net/go/mautrix v0.23.2-0.20250304004736-7d3791ace3b7/go.mod h1:IHMaSJh7YIxMrZSDVefS+nLdr3RbeLowsCSa6ibONZ0=
maunium.net/go/mautrix v0.23.2 h1:Bo3tPrQJwkxyL7aMmy/T+d2tqIrypZjHqeHe8fyeAOg=
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/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) {
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)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to marshal push notification")
log.Err(err).Msg("Failed to marshal push notification")
return
} 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
}
for _, reg := range pushRegs {
@ -172,7 +178,7 @@ func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*databas
var err error
devicePayload, err = encryptPush(rawPayload, reg.Encryption.Key)
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
}
encrypted = true
@ -180,7 +186,7 @@ func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*databas
switch reg.Type {
case database.PushTypeFCM:
if !encrypted {
zerolog.Ctx(ctx).Warn().
log.Warn().
Str("device_id", reg.DeviceID).
Msg("FCM push registration doesn't have encryption key")
continue
@ -188,7 +194,7 @@ func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*databas
var token string
err = json.Unmarshal(reg.Data, &token)
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
}
gmx.SendFCMPush(ctx, token, devicePayload, notif.HasImportant)

View file

@ -80,7 +80,7 @@ const (
AND (type IN ('m.room.message', 'm.sticker')
OR (type = 'm.room.encrypted'
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
ORDER BY timestamp DESC
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) {
err = rq.GetDB().QueryRow(ctx, recalculateRoomPreviewEventQuery, roomID).Scan(&rowID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
@ -215,7 +218,7 @@ func (r *Room) CheckChangesAndCopyInto(other *Room) (hasChanges bool) {
hasChanges = true
other.HasMemberList = true
}
if r.PreviewEventRowID > other.PreviewEventRowID {
if r.PreviewEventRowID != 0 {
other.PreviewEventRowID = r.PreviewEventRowID
hasChanges = true
}

View file

@ -414,24 +414,42 @@ var HTMLSanitizerImgSrcTemplate = "mxc://%s/%s"
func writeImg(w *strings.Builder, attr []html.Attribute) id.ContentURI {
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")
writeAttribute(w, "alt", alt)
if title != "" {
writeAttribute(w, "title", title)
}
mxc := id.ContentURIString(src).ParseOrIgnore()
if !mxc.IsValid() {
return id.ContentURI{}
}
writeAttribute(w, "src", fmt.Sprintf(HTMLSanitizerImgSrcTemplate, mxc.Homeserver, mxc.FileID))
writeAttribute(w, "src", url)
writeAttribute(w, "loading", "lazy")
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 {
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))
} else {
writeAttribute(w, "class", "hicli-sizeless-inline-img")
writeAttribute(w, "class", "hicli-inline-img hicli-sizeless-inline-img")
}
return mxc
}

View file

@ -65,7 +65,16 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
})
case "set_state":
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":
return unmarshalAndCall(req.Data, func(params *setMembershipParams) (any, error) {
@ -308,6 +317,12 @@ type sendStateEventParams struct {
EventType event.Type `json:"type"`
StateKey string `json:"state_key"`
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 {

View file

@ -296,12 +296,12 @@ func (h *HiClient) PaginateServer(ctx context.Context, roomID id.RoomID, limit i
if resp.End == "" {
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)
if err != nil {
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
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 {
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,
stateKey string,
content any,
extra ...mautrix.ReqSendEvent,
) (id.EventID, error) {
room, err := h.DB.Room.Get(ctx, roomID)
if err != nil {
@ -233,10 +234,14 @@ func (h *HiClient) SetState(
} else if room == nil {
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 {
return "", err
}
if resp.UnstableDelayID != "" {
// Mildly hacky, but it's fine'
return id.EventID(resp.UnstableDelayID), nil
}
return resp.EventID, nil
}
@ -511,6 +516,9 @@ func (h *HiClient) shouldShareKeysToInvitedUsers(ctx context.Context, roomID id.
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get history visibility event")
return false
} else if historyVisibility == nil {
zerolog.Ctx(ctx).Warn().Msg("History visibility event not found")
return false
}
mautrixEvt := historyVisibility.AsRawMautrix()
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)
}
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.MembershipBan && newMembership == event.MembershipLeave) ||
(prevMembership == event.MembershipLeave && newMembership == event.MembershipBan) {
@ -598,7 +598,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
return nil, nil
}
const CurrentHTMLSanitizerVersion = 8
const CurrentHTMLSanitizerVersion = 10
func (h *HiClient) ReprocessExistingEvent(ctx context.Context, evt *database.Event) {
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)
}
}
if updatedRoom.PreviewEventRowID == dbEvt.RowID {
if updatedRoom.PreviewEventRowID == dbEvt.RowID || (updatedRoom.PreviewEventRowID == 0 && room.PreviewEventRowID == dbEvt.RowID) {
updatedRoom.PreviewEventRowID = 0
recalculatePreviewEvent = true
}
@ -969,10 +969,11 @@ func (h *HiClient) processStateAndTimeline(
updatedRoom.PreviewEventRowID, err = h.DB.Room.RecalculatePreview(ctx, room.ID)
if err != nil {
return fmt.Errorf("failed to recalculate preview event: %w", err)
}
_, err = addOldEvent(updatedRoom.PreviewEventRowID, "")
if err != nil {
return fmt.Errorf("failed to get preview event: %w", err)
} else if updatedRoom.PreviewEventRowID != 0 {
_, err = addOldEvent(updatedRoom.PreviewEventRowID, "")
if err != nil {
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

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() ?? ""
}
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 backgroundColor = getUserColor(userID)
const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url)
if (!mediaID) {
if (!mediaID || forceFallback) {
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
}
const encrypted = !!content?.avatar_file
@ -91,8 +96,12 @@ export const getAvatarURL = (userID: UserID, content?: UserProfile | null, thumb
return thumbnail ? `${url}&thumbnail=avatar` : url
}
export const getAvatarThumbnailURL = (userID: UserID, content?: UserProfile | null): string | undefined => {
return getAvatarURL(userID, content, true)
export const getAvatarThumbnailURL = (
userID: UserID,
content?: UserProfile | null,
forceFallback = false,
): string | undefined => {
return getAvatarURL(userID, content, true, forceFallback)
}
interface RoomForAvatarURL {
@ -104,14 +113,21 @@ interface RoomForAvatarURL {
}
export const getRoomAvatarURL = (
room: RoomForAvatarURL, avatarOverride?: ContentURI, thumbnail = false,
room: RoomForAvatarURL,
avatarOverride?: ContentURI,
thumbnail = false,
forceFallback = false,
): string | undefined => {
return getAvatarURL(room.dm_user_id ?? room.room_id, {
displayname: room.name,
avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url,
}, thumbnail)
}, thumbnail, forceFallback)
}
export const getRoomAvatarThumbnailURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => {
return getRoomAvatarURL(room, avatarOverride, true)
export const getRoomAvatarThumbnailURL = (
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(
room_id: RoomID, type: EventType, state_key: string, content: Record<string, unknown>,
extra: { delay_ms?: number } = {},
): 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> {

View file

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

View file

@ -55,6 +55,7 @@ export interface RoomListEntry {
unread_notifications: number
unread_highlights: number
marked_unread: boolean
is_invite?: boolean
}
export interface GCSettings {
@ -255,8 +256,10 @@ export class StateStore {
}
applySync(sync: SyncCompleteData) {
let prevActiveRoom: RoomID | null = null
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")
prevActiveRoom = this.activeRoomID
this.clear()
}
const resyncRoomList = this.roomList.current.length === 0
@ -387,6 +390,10 @@ export class StateStore {
this.topLevelSpaces.emit(sync.top_level_spaces)
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() {

View file

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

View file

@ -55,10 +55,22 @@ export const preferences = {
}),
show_media_previews: new Preference<boolean>({
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,
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>({
displayName: "Code block line wrap",
description: "Whether to wrap long lines in code blocks instead of scrolling horizontally.",

View file

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

View file

@ -117,6 +117,15 @@ const StylePreferences = ({ client, activeRoom }: StylePreferencesProps) => {
--timeline-status-size: 2rem;
}
`, [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" ? `
@import url("_gomuks/codeblock/github.css") (prefers-color-scheme: light);
@import url("_gomuks/codeblock/github-dark.css") (prefers-color-scheme: dark);

View file

@ -181,10 +181,5 @@ export const useSecondaryItems = (
title={pendingTitle}
className="redact-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 {
overflow: scroll;
overflow: auto;
}
}
}

View file

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

View file

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

View file

@ -68,8 +68,6 @@ div.right-panel-content.widgets {
}
div.right-panel-content.user {
display: flex;
flex-direction: column;
padding: 1rem;
div.avatar-container {
@ -79,7 +77,6 @@ div.right-panel-content.user {
justify-content: center;
align-items: center;
padding: 1rem;
flex-shrink: 0;
margin: 0 auto;
> img {
@ -105,6 +102,7 @@ div.right-panel-content.user {
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
word-break: break-word;
overflow: hidden;
}
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 { JSX, use } from "react"
import type { UserID } from "@/api/types"
import MainScreenContext from "../MainScreenContext.ts"
import MainScreenContext, { MainScreenContextFields } from "../MainScreenContext.ts"
import ErrorBoundary from "../util/ErrorBoundary.tsx"
import ElementCall from "../widget/ElementCall.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) {
case "pinned-messages":
return <PinnedMessages />
@ -72,9 +72,9 @@ function renderRightPanelContent(props: RightPanelProps): JSX.Element | null {
case "widgets":
return <WidgetList />
case "element-call":
return <ElementCall />
return <ElementCall onClose={mainScreen.closeRightPanel} />
case "widget":
return <LazyWidget info={props.info} />
return <LazyWidget info={props.info} onClose={mainScreen.closeRightPanel} />
case "user":
return <UserInfo userID={props.userID} />
}
@ -104,7 +104,7 @@ const RightPanel = (props: RightPanelProps) => {
</div>
<div className={`right-panel-content ${props.type}`}>
<ErrorBoundary thing="right panel content">
{renderRightPanelContent(props)}
{renderRightPanelContent(props, mainScreen)}
</ErrorBoundary>
</div>
</div>

View file

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

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 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 { useEventAsState } from "@/util/eventdispatcher.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
return <div className="room-list-wrapper">
<div className="room-search-wrapper">
@ -145,6 +146,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => {
isActive={room.room_id === activeRoomID}
hidden={roomListFilter ? !roomListFilter(room) : false}
room={room}
hideAvatar={room.is_invite && !showInviteAvatars}
/>,
)}
</div>

View file

@ -16,6 +16,7 @@
import { use, useEffect, useState } from "react"
import { ScaleLoader } from "react-spinners"
import { getAvatarThumbnailURL, getAvatarURL, getRoomAvatarURL } from "@/api/media.ts"
import { usePreference } from "@/api/statestore/hooks.ts"
import { InvitedRoomStore } from "@/api/statestore/invitedroom.ts"
import { RoomID, RoomSummary } from "@/api/types"
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 memberCount = summary?.num_joined_members || null
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">
<div className="preview-inner">
{invite?.invited_by && !invite.dm_user_id ? <div className="inviter-info">
<img
className="small avatar"
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)}
alt=""
/>
@ -102,7 +105,8 @@ const RoomPreview = ({ roomID, via, alias, invite }: RoomPreviewProps) => {
<h2 className="room-name">{name}</h2>
<img
// 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"
onClick={use(LightboxContext)}
alt=""

View file

@ -23,6 +23,7 @@ blockquote.reply-body {
height: calc(var(--small-font-size) * 1.5);
border-left: none;
padding: 0;
overflow: hidden;
> div.reply-spine {
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 {
margin-top: 0;
display: flex;
align-items: center;
flex-shrink: 0;
}
div.date-separator {

View file

@ -28,6 +28,8 @@ const URLPreviews = ({ event, room }: {
}) => {
const client = use(ClientContext)!
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) {
return null
}
@ -72,7 +74,7 @@ const URLPreviews = ({ event, room }: {
<a href={url} title={title} target="_blank" rel="noreferrer noopener">{title}</a>
</div>
<div className="description" title={p["og:description"]}>{p["og:description"]}</div>
{mediaURL && (inline
{mediaURL && showPreviewImages && (inline
? <div className="inline-media-wrapper">{mediaContainer}</div>
: mediaContainer)}
</div>

View file

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

View file

@ -25,14 +25,21 @@ const PolicyRuleBody = ({ event, sender }: EventContentProps) => {
const mainScreen = use(MainScreenContext)
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
if (!entity || !recommendation) {
if ((!entity && !hashedEntity) || !recommendation) {
return <div className="policy-body">
{getDisplayname(event.sender, sender?.content)} sent an invalid policy rule
</div>
}
const target = event.type.replace(/^m\.policy\.rule\./, "")
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 = (
<a
className="hicli-matrix-uri hicli-matrix-uri-user"
@ -44,16 +51,18 @@ const PolicyRuleBody = ({ event, sender }: EventContentProps) => {
{entity}
</a>
)
matchingWord = "user"
}
let recommendationElement: JSX.Element | string = <code>{recommendation}</code>
if (recommendation === "m.ban") {
recommendationElement = "ban"
} else if (recommendation === "org.matrix.msc4204.takedown") {
recommendationElement = "takedown"
}
const action = prevContent ? ((content.entity && content.recommendation) ? "updated" : "removed") : "added"
const target = event.type.replace(/^m\.policy\.rule\./, "")
return <div className="policy-body">
{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}
</div>
}

View file

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

View file

@ -28,9 +28,10 @@ const widgetLoader = <div className="widget-container widget-loading">
export interface LazyWidgetProps {
info: IWidget
onClose?: () => void
}
const LazyWidget = ({ info }: LazyWidgetProps) => {
const LazyWidget = ({ info, onClose }: LazyWidgetProps) => {
const room = use(RoomContext)?.store
const client = use(ClientContext)
if (!room || !client) {
@ -38,7 +39,7 @@ const LazyWidget = ({ info }: LazyWidgetProps) => {
}
return (
<Suspense fallback={widgetLoader}>
<Widget info={info} room={room} client={client} />
<Widget info={info} room={room} client={client} onClose={onClose} />
</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 {
overflow: hidden !important;
> iframe.widget-iframe {
width: 100%;
height: 100%;
border: none;
}
iframe.widget-iframe {
width: 100%;
height: 100%;
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 { MemDBEvent, RoomID, SyncToDevice } from "@/api/types"
import { getDisplayname } from "@/util/validation"
import PermissionPrompt from "./PermissionPrompt"
import { memDBEventToIRoomEvent } from "./util"
import GomuksWidgetDriver from "./widgetDriver"
import "./Widget.css"
@ -27,6 +28,7 @@ export interface WidgetProps {
info: IWidget
room: RoomStateStore
client: Client
onClose?: () => void
}
// 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 driver = new GomuksWidgetDriver(client, room)
const driver = new GomuksWidgetDriver(client, room, openPermissionPrompt)
const widgetURL = addLegacyParams(wrappedWidget.getCompleteUrl({
widgetRoomId: room.roomID,
currentUserId: client.userID,
@ -92,13 +109,16 @@ const ReactWidget = ({ room, info, client }: WidgetProps) => {
evt.preventDefault()
clientAPI.transport.reply(evt.detail, {})
}
const closeWidget = (evt: CustomEvent) => {
noopReply(evt)
onClose?.()
}
clientAPI.on("action:io.element.join", noopReply)
clientAPI.on("action:im.vector.hangup", noopReply)
clientAPI.on("action:io.element.device_mute", noopReply)
clientAPI.on("action:io.element.tile_layout", noopReply)
clientAPI.on("action:io.element.spotlight_layout", noopReply)
// TODO handle this one?
clientAPI.on("action:io.element.close", noopReply)
clientAPI.on("action:io.element.close", closeWidget)
clientAPI.on("action:set_always_on_screen", noopReply)
const removeListener = client.addWidgetListener(new WidgetListenerImpl(clientAPI))

View file

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

View file

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