diff --git a/assets/tag.png b/assets/tag.png index c8edfc7..7a5ad4a 100644 Binary files a/assets/tag.png and b/assets/tag.png differ diff --git a/go.mod b/go.mod index effcb3b..8168425 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.25.5 require ( github.com/BurntSushi/toml v1.6.0 + github.com/boxes-ltd/imaging v1.7.5 + github.com/crazy3lf/colorconv v1.2.0 github.com/diamondburned/gotk4/pkg v0.3.1 github.com/gen2brain/beeep v0.11.2 github.com/go-analyze/charts v0.5.24 @@ -12,6 +14,10 @@ require ( github.com/jasonlovesdoggo/gopen v0.0.0-20250130105607-39c98c645030 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/kr/pretty v0.2.0 + github.com/mskrha/svg2png v0.0.0-20240706085601-64fa78f4eb07 + github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef golang.org/x/net v0.29.0 gosrc.io/xmpp v0.5.1 mellium.im/xmpp v0.22.0 @@ -33,10 +39,10 @@ require ( github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect - golang.org/x/image v0.24.0 // indirect - golang.org/x/sync v0.11.0 // indirect + golang.org/x/image v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect mellium.im/reader v0.1.0 // indirect mellium.im/xmlstream v0.15.4 // indirect diff --git a/go.sum b/go.sum index fb3e577..d42bef5 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,16 @@ github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/KarpelesLab/weak v0.1.1 h1:fNnlPo3aypS9tBzoEQluY13XyUfd/eWaSE/vMvo9s4g= github.com/KarpelesLab/weak v0.1.1/go.mod h1:pzXsWs5f2bf+fpgHayTlBE1qJpO3MpJKo5sRaLu1XNw= github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/boxes-ltd/imaging v1.7.5 h1:k4kYxJEhysoGhEEN1IEeKoSbnG8/8snjj7M48Ok0fnk= +github.com/boxes-ltd/imaging v1.7.5/go.mod h1:+8H+oRvis3InOFtTpcoCCB1RDXqo6p9tQBtjZfWnrC8= github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= +github.com/crazy3lf/colorconv v1.2.0 h1:UM7kSZWnwFMGiC+PpYrjxQSOd6sEyWb+dRKKTd3KslA= +github.com/crazy3lf/colorconv v1.2.0/go.mod h1:2jTJ7QCWCj2sSLOhF4Gzi0J5/hoX8/VY8VzNvXAlD1I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -80,6 +84,8 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mskrha/svg2png v0.0.0-20240706085601-64fa78f4eb07 h1:7fan6wzUXasMPMHho2ePSkB+QTEb0Rh/f6B+IkkP1Sc= +github.com/mskrha/svg2png v0.0.0-20240706085601-64fa78f4eb07/go.mod h1:KFdfdIgpr48ODxdkxKvpcYwuyLpQ6rfkAsFB2UQ6jD4= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -89,6 +95,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.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= +github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs= +github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d/go.mod h1:TbpErkob6SY7cyozRVSGoB3OlO2qOAgVN8O3KAJ4fMI= github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M= github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY= github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ= @@ -97,6 +105,10 @@ github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjM github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -123,11 +135,11 @@ golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -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.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= +golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -137,8 +149,8 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20190911185100-cd5d95a43a6e/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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -152,14 +164,14 @@ golang.org/x/sys v0.1.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/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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= diff --git a/gtk-helpers.go b/gtk-helpers.go index 1321b8f..5b292f1 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -1,13 +1,22 @@ package main import ( + "bytes" "context" "fmt" + "github.com/boxes-ltd/imaging" + "github.com/crazy3lf/colorconv" "github.com/diamondburned/gotk4/pkg/gdk/v4" + "github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/diamondburned/gotk4/pkg/pango" + "github.com/rrivera/identicon" "gosrc.io/xmpp/stanza" + "image" + "image/png" + xmpp_color "mellium.im/xmpp/color" + "strconv" ) func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) { @@ -62,7 +71,7 @@ func switchToTab(jid string, w *gtk.Window) { gen := gtk.NewBox(gtk.OrientationVertical, 0) i := 0 - mm.Range(func(k, v any) bool { + rangeOrdered(&mm, (func(k, v any) bool { i++ userbox := gtk.NewBox(gtk.OrientationHorizontal, 0) @@ -71,6 +80,13 @@ func switchToTab(jid string, w *gtk.Window) { var ocu OccupantID u.Get(&mu) u.Get(&ocu) + + if mu.MucUserItem.Role == "moderator" { + gen.Prepend(userbox) + } else { + gen.Append(userbox) + } + //id := ocu.ID //if id == "" { id := JidMustParse(u.From).Resource @@ -90,7 +106,29 @@ func switchToTab(jid string, w *gtk.Window) { ok := u.Get(&hats) if ok { for _, hat := range hats.Hats { - tag := gtk.NewImageFromPaintable(clientAssets["tag"]) + var val float64 + if hat.Hue != "" { + tval, _ := strconv.Atoi(hat.Hue) + val = float64(tval) + } else { + xc := xmpp_color.String(hat.URI, 255, loadedConfig.CVD) + r, g, b, _ := xc.RGBA() + val, _, _ = colorconv.RGBToHSV(uint8(r), uint8(g), uint8(b)) + + } + tB := tagBytes + img, _, _ := image.Decode(bytes.NewReader(tB)) + i_rgba := imaging.AdjustHue(img, val) + + var buf bytes.Buffer + png.Encode(&buf, i_rgba) + + tB = buf.Bytes() + + loader := gdkpixbuf.NewPixbufLoader() + loader.Write(tB) + loader.Close() + tag := gtk.NewPictureForPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf())) tag.SetTooltipText(hat.Title) userbox.Prepend(tag) } @@ -262,10 +300,12 @@ func switchToTab(jid string, w *gtk.Window) { win.SetTitle(JidMustParse(u.From).Resource) nick.AddCSSClass("author") + nick.SetSelectable(true) profile_box.Append(nick) profile_box.Append(ver_text) fr := gtk.NewLabel(u.From) fr.AddCSSClass("jid") + fr.SetSelectable(true) profile_box.Append(fr) profile_box.Append(ver_text) @@ -300,6 +340,7 @@ func switchToTab(jid string, w *gtk.Window) { if mu.MucUserItem.JID != "" { ji := (gtk.NewLabel(mu.MucUserItem.JID)) ji.AddCSSClass("jid") + ji.SetSelectable(true) profile_box.Append(ji) } profile_box.Append(gtk.NewLabel("Connected with role " + mu.MucUserItem.Role)) @@ -397,13 +438,13 @@ func switchToTab(jid string, w *gtk.Window) { im.AddCSSClass("author_img") profile_box.Prepend(im) } else { - im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + im := createIdenticon(u.From) im.SetPixelSize(80) im.AddCSSClass("author_img") profile_box.Prepend(im) } } else { - im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + im := createIdenticon(u.From) im.SetPixelSize(80) im.AddCSSClass("author_img") profile_box.Prepend(im) @@ -418,13 +459,8 @@ func switchToTab(jid string, w *gtk.Window) { userbox.AddController(gesture) userbox.AddController(mod_gesture) - if mu.MucUserItem.Role == "moderator" { - gen.Prepend(userbox) - } else { - gen.Append(userbox) - } return true - }) + })) headerBox := gtk.NewBox(gtk.OrientationHorizontal, 0) if i >= 500 { @@ -450,3 +486,29 @@ func switchToTab(jid string, w *gtk.Window) { func showErrorDialog(err error) { fmt.Println(err.Error()) } + +func createIdenticon(word string) *gtk.Image { // This function generates an identicon + if !loadedConfig.Identicons { + i := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + i.AddCSSClass(loadedConfig.CVD.String()+"_CVD") + return i + } + + gen, _ := identicon.New("github", 5, 3) + ii, _ := gen.Draw(word) + im := ii.Image(25) + + buf := new(bytes.Buffer) + err := png.Encode(buf, im) + if err != nil { + panic(err) + } + + loader := gdkpixbuf.NewPixbufLoader() + loader.Write(buf.Bytes()) + loader.Close() + i := gtk.NewImageFromPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf())) + i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") + return i + +} diff --git a/gtk-message.go b/gtk-message.go index afb43d2..0b809e9 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -16,6 +16,7 @@ import ( "os" "path/filepath" "runtime" + "strings" ) func generatePresenceWidget(p stanza.Packet) gtk.Widgetter { @@ -67,7 +68,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { if ok { b := gtk.NewBox(gtk.OrientationHorizontal, 0) b.Append(gtk.NewLabel(fmt.Sprintf("%s is typing...", JidMustParse(m.From).Resource))) - return b + return b } if m.Error.Type != "" { @@ -95,7 +96,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { rc_box := gtk.NewBox(gtk.OrientationVertical, 0) reactions := gtk.NewBox(gtk.OrientationHorizontal, 0) - reaction := []string{"👍", "👎", "♥️", "🤣", "😭"} + reaction := []string{"👍", "👎", "♥️", "🤣", "💀"} for _, v := range reaction { like := gtk.NewButton() like.SetLabel(v) @@ -117,7 +118,15 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { quote := gtk.NewButtonWithLabel("Quote") quote.ConnectClicked(func() { - message_en.SetText("> " + m.Body + "\n") + lines := strings.Split(m.Body, "\n") + for i, line := range lines { + quoteline:= "> " + line + lines[i] = quoteline + } + + newstr := strings.Join(lines, "\n") + "\n\n" + + message_en.SetText(newstr) }) rc_box.Append(quote) @@ -170,13 +179,13 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { im.AddCSSClass("author_img") authorBox.Append(im) } else { - im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + im := createIdenticon(m.From) im.SetPixelSize(40) im.AddCSSClass("author_img") authorBox.Append(im) } } else { - im := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + im := createIdenticon(m.From) im.SetPixelSize(40) im.AddCSSClass("author_img") authorBox.Append(im) @@ -251,25 +260,27 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou oghash := hash p, err := ensureCache() if err != nil { - return gtk.NewImageFromPaintable(clientAssets["FailedAvatar"]) + return createIdenticon(j) } if hash == "" { fmt.Println("Hash is nil!") - return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + return createIdenticon(j) } _, ok := invalidImages[hash] if ok { fmt.Println("Image is invalid") - return gtk.NewImageFromPaintable(clientAssets["FailedAvatar"]) + return createIdenticon(j) } hash = filepath.Join(p, sanitizefilename.Sanitize(hash)) _, err = os.ReadFile(hash) if err == nil { - return newImageFromPath(hash) + i := newImageFromPath(hash) + i.AddCSSClass(loadedConfig.CVD.String()+"_CVD") + return i } iqResp, err := stanza.NewIQ(stanza.Attrs{ @@ -294,14 +305,14 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou result := <-mychan card, ok := result.Payload.(*VCard) if !ok { - return gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) + return createIdenticon(j) } base64_data := card.Photo.Binval if card.Photo.Binval == "" || ((card.Photo.Type == "image/svg+xml" || card.Photo.Type == "image/webp") && (runtime.GOOS == "windows" || runtime.GOOS == "netbsd")) { fmt.Println("Blocking image") invalidImages[oghash] = true - return gtk.NewImageFromPaintable(clientAssets["FailedAvatar"]) + return createIdenticon(j) } data, err := base64.StdEncoding.DecodeString(base64_data) @@ -314,5 +325,7 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou panic(err) } - return newImageFromPath(hash) + i := newImageFromPath(hash) + i.AddCSSClass(loadedConfig.CVD.String()+"_CVD") + return i } diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..043c338 --- /dev/null +++ b/helpers.go @@ -0,0 +1,26 @@ +// Generic helpers +package main + +import ( + "sync" + "sort" +) + +func rangeOrdered(m *sync.Map, fn func(k, v any) bool) { + var keys []string + + m.Range(func(k, v any) bool { + keys = append(keys, k.(string)) + return true + }) + + sort.Strings(keys) + + for _, k := range keys { + v, _ := m.Load(k) + if !fn(k, v) { + break + } + } +} + diff --git a/main.go b/main.go index 7d6a360..79d074f 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,8 @@ var connectionIcon *gtk.Image var mStatus *gtk.Label var mIcon *gtk.Image +var typingStatus *gtk.Label + var pingStatus *gtk.Label // var msgs *gtk.ListBox @@ -225,7 +227,9 @@ func main() { if ok { if JidMustParse(fm.From).Bare() == JidMustParse(m.From).Bare() { p = sc.Forwarded.Stanza + orig := m.To m = sc.Forwarded.Stanza.(stanza.Message) + m.To = orig } else { panic(fmt.Sprintln("Impersonation: ", fm.From, m.From)) } @@ -245,6 +249,19 @@ func main() { } } } + composing := stanza.StateComposing{} + ok = m.Get(&composing) + if ok && current == JidMustParse(m.From).Bare() { + typingStatus.SetText(fmt.Sprintf("%s is typing...", m.From)) + return + } + + inactive := stanza.StateInactive{} + ok = m.Get(&inactive) + if ok && current == JidMustParse(m.From).Bare() { + typingStatus.SetText("") + return + } glib.IdleAdd(func() { //uiQueue <- func() { @@ -674,22 +691,97 @@ func activate(app *gtk.Application) { btn.ConnectClicked(func() { t := jid_entry.Text() _, ok := tabs.Load(t) + jm := func() { + + err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text()) + if err != nil { + panic(err) + } + + createTab(t, true) + b := gtk.NewLabel(t) + gesture1 := gtk.NewGestureClick() + gesture1.SetButton(1) + gesture1.Connect("pressed", func() { + switchToTab(t, &window.Window) + }) + + b.AddController(gesture1) + menu.Append(b) + } if !ok { - err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text()) + // First check the MUC's disco and see if it's semianon + allowed := true + fmt.Println("Attempting to get Disco info") + + myIQ, err := stanza.NewIQ(stanza.Attrs{ + Type: "get", + From: clientroot.Session.BindJid, + To: t, + Id: "dicks", + Lang: "en", + }) + if err != nil { panic(err) } - createTab(t, true) - b := gtk.NewLabel(t) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(t, &window.Window) - }) + myIQ.Payload = &stanza.DiscoInfo{} - b.AddController(gesture1) - menu.Append(b) + ctx := context.TODO() + mychan, err := client.SendIQ(ctx, myIQ) + if err == nil { + result := <-mychan + res, ok := result.Payload.(*stanza.DiscoInfo) + if ok { + semianon := false + features := res.Features + for _, feature := range features { + if feature.Var == "muc_nonanonymous" { + semianon = false + break + } else if feature.Var == "muc_semianonymous" { + semianon = true + break + } + } + + if !semianon { + allowed = false + warning_win := gtk.NewWindow() + warning_win.SetTitle("Warning") + warning_win.SetDefaultSize(400, 400) + warning_win.SetResizable(false) + + warning_box := gtk.NewBox(gtk.OrientationVertical, 0) + warning_label := gtk.NewLabel("This muc is not semi-anonymous. Your JID will be revealed to non-moderators if you join. Continue?") + + buttons := gtk.NewBox(gtk.OrientationHorizontal, 0) + join_button := gtk.NewButtonWithLabel("Join") + join_button.ConnectClicked(func() { + warning_win.SetVisible(false) + jm() + }) + + cancel_button := gtk.NewButtonWithLabel("Cancel") + cancel_button.ConnectClicked(func() { + warning_win.SetVisible(false) + }) + + buttons.Append(join_button) + buttons.Append(cancel_button) + + warning_box.Append(warning_label) + warning_box.Append(buttons) + warning_win.SetChild(warning_box) + warning_win.Present() + } + } + } + + if allowed { + jm() + } } win.SetVisible(false) }) @@ -884,7 +976,7 @@ func activate(app *gtk.Application) { end := start + len("@everyone") new_mention := new(Mention) - new_mention.Type = "urn:xmpp:mentions:0#channel" + new_mention.Mentions = "urn:xmpp:mentions:0#channel" new_mention.Begin = start new_mention.End = end @@ -915,6 +1007,9 @@ func activate(app *gtk.Application) { box.Append(entry_box) + typingStatus = gtk.NewLabel("") + box.Append(typingStatus) + window.SetChild(box) window.SetVisible(true) diff --git a/style.css b/style.css index 9356f90..5d59713 100644 --- a/style.css +++ b/style.css @@ -61,3 +61,15 @@ color: white; background-color: red; } + +.RedGreen_CVD { + filter: hue-rotate(30deg) saturate(120%) contrast(110%); +} + +.Blue_CVD { + filter: hue-rotate(30deg); +} + +.None_CVD { + +} diff --git a/svg-conv.go b/svg-conv.go new file mode 100644 index 0000000..22db2e4 --- /dev/null +++ b/svg-conv.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "fmt" + "image" + "image/png" + "github.com/srwiley/oksvg" + "github.com/srwiley/rasterx" +) + +func SVGToPNG(svgData []byte) ([]byte, error) { + // Parse SVG + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) + if err != nil { + return nil, fmt.Errorf("failed to parse SVG: %w", err) + } + + w := int(icon.ViewBox.W) + h := int(icon.ViewBox.H) + if w == 0 || h == 0 { + w, h = 100, 100 + } + + // Rasterize into an RGBA image + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) + scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds()) + dasher := rasterx.NewDasher(w, h, scanner) + + icon.SetTarget(0, 0, float64(w), float64(h)) + icon.Draw(dasher, 1.0) + + // Encode to PNG bytes + var buf bytes.Buffer + if err := png.Encode(&buf, rgba); err != nil { + return nil, fmt.Errorf("failed to encode PNG: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/types.go b/types.go index 1e2e64d..cf8bea5 100644 --- a/types.go +++ b/types.go @@ -20,6 +20,7 @@ type lambdaConfig struct { Nick string JoinBookmarks bool CVD color.CVD + Identicons bool } type mucUnit struct { diff --git a/version.go b/version.go index a838089..29bd199 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -var lambda_version string = "26w11a" +var lambda_version string = "26w15a" diff --git a/xmpp-mentions.go b/xmpp-mentions.go index 29dde9e..72ef7d3 100644 --- a/xmpp-mentions.go +++ b/xmpp-mentions.go @@ -5,17 +5,22 @@ import ( "gosrc.io/xmpp/stanza" ) -// Experimental implementation of XEP-XXXX: Explicit Mentions -// https://git.isekai.rocks/snit/protoxeps/tree/explicit-mentions.xml +// Implementation of XEP-0513: Explicit Mentions +// https://xmpp.org/extensions/xep-0513.html + +type NoPing struct{} +type Active struct{} type Mention struct { stanza.MsgExtension - XMLName xml.Name `xml:"urn:xmpp:mentions:0 mention"` - URI string `xml:"uri,attr,omitempty"` - Begin int `xml:"begin,attr,omitempty"` - End int `xml:"end,attr,omitempty"` - Type string `xml:"type,attr"` - Target string `xml:"target,attr,omitempty"` + XMLName xml.Name `xml:"urn:xmpp:mentions:0 mention"` + URI string `xml:"uri,attr,omitempty"` + Begin int `xml:"begin,attr,omitempty"` + End int `xml:"end,attr,omitempty"` + Mentions string `xml:"mentions,attr"` + OccupantID string `xml:"occupantid,attr,omitempty"` + NoPing NoPing `xml:"noping,omitempty"` + Active Active `xml:"active,omitempty"` } func init() {