Files
lambda/gtk-helpers.go
T
2026-06-01 10:37:22 +01:00

714 lines
18 KiB
Go

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/color"
"image/png"
xmpp_color "mellium.im/xmpp/color"
"strconv"
)
func init() {
}
func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) {
glib.IdleAdd(func() bool {
vAdj := scrolledWindow.VAdjustment()
vAdj.SetValue(vAdj.Upper() - vAdj.PageSize())
return false // Return false to run only once
})
}
func createTab(jid string, isMuc bool, name string) bool {
if name == "" {
name = jid
}
_, ok := tabs.Load(jid)
_, uok := userdevices.Load(jid)
_, mok := mucmembers.Load(jid)
if !ok && !uok && !mok {
newTab := new(chatTab)
newTab.isMuc = isMuc
newTab.msgs = gtk.NewListBox()
glib.IdleAdd(func() {
newTab.msgs.SetVExpand(true)
newTab.msgs.SetShowSeparators(true)
newTab.msgs.Append(gtk.NewButtonWithLabel(loadedLocale["getPastMessages"]))
})
newTab.name = name
tabs.Store(jid, newTab)
return true
}
return false
}
func removeTab(jid string, w *gtk.Window) {
t, ok := tabs.Load(jid)
if ok {
tab := t.(*chatTab)
tab.msgs.RemoveAll()
if tab.isMuc {
client.SendRaw(fmt.Sprintf(`
<presence from='%s' to='%s' type='unavailable'>
<x xmlns='http://jabber.org/protocol/muc'/>
</presence>
`, clientroot.Session.BindJid, jid+"/"+tab.current_nick))
}
tabs.Delete(jid)
mucmembers.Delete(jid)
userdevices.Delete(jid)
if current == jid {
current = ""
scroller.SetChild(empty_dialog)
typingStatus.SetText("")
memberList.SetChild(gtk.NewLabel(""))
}
mucmembers.Delete(jid)
}
}
func switchToTab(jid string, w *gtk.Window) {
current = jid
tab, ok := tabs.Load(current)
if !ok {
return
}
typed_tab := tab.(*chatTab)
scroller.SetChild(typed_tab.msgs)
typingStatus.SetText("")
if typed_tab.isMuc {
m, ok := mucmembers.Load(jid)
if !ok {
box := gtk.NewBox(gtk.OrientationVertical, 10)
failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail"))
failed_icon.SetPixelSize(100)
box.Append(failed_icon)
label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.")
label.SetWrap(true)
box.Append(label)
memberList.SetChild(box)
return
}
ma, ok := m.(mucUnit)
if !ok {
box := gtk.NewBox(gtk.OrientationVertical, 10)
failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail"))
failed_icon.SetPixelSize(100)
box.Append(failed_icon)
label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.")
label.SetWrap(true)
box.Append(label)
memberList.SetChild(box)
return
}
mm := ma.Members
gen := gtk.NewBox(gtk.OrientationVertical, 10)
i := 0
rangeOrdered(&mm, (func(k, v any) bool {
i++
u, ok := v.(stanza.Presence)
if !ok {
return true
}
userbox := gtk.NewBox(gtk.OrientationHorizontal, 2)
var mu MucUser
var ocu OccupantID
u.Get(&mu)
u.Get(&ocu)
if mu.MucUserItem.Role == "moderator" {
gen.Prepend(gtk.NewSeparator(gtk.OrientationHorizontal))
gen.Prepend(userbox)
} else {
gen.Append(userbox)
gen.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}
//id := ocu.ID
//if id == "" {
id := JidMustParse(u.From).Resource
//}
nick_label := gtk.NewLabel(JidMustParse(u.From).Resource)
custom_nick, ok := loadedConfig.CustomNicks[ocu.ID]
if ok {
nick_label.SetText(custom_nick)
}
nick_label.SetEllipsize(pango.EllipsizeEnd)
nick_label.AddCSSClass(mu.MucUserItem.Role)
if mu.MucUserItem.Role == "visitor" {
nick_label.SetOpacity(0.5)
}
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\n%s", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation, loadedLocale["clickForMoreInfo"]))
userbox.Append(nick_label)
var hats Hats
ok = u.Get(&hats)
if ok {
for _, hat := range hats.Hats {
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)
}
}
status := gtk.NewImageFromPaintable(clientAssetsLoad("status_" + string(u.Show)))
status.SetTooltipText(string(u.Show))
status.SetHAlign(gtk.AlignEnd)
// medal.SetHExpand(true)
userbox.Prepend(status)
if u.Status != "" {
status_message := gtk.NewImageFromPaintable(clientAssetsLoad("comment"))
status_message.SetTooltipText(u.Status)
status_message.SetHAlign(gtk.AlignEnd)
userbox.Append(status_message)
}
medal := gtk.NewImageFromPaintable(clientAssetsLoad(mu.MucUserItem.Affiliation))
medal.SetTooltipText(mu.MucUserItem.Affiliation)
medal.SetHAlign(gtk.AlignEnd)
medal.SetHExpand(true)
userbox.Append(medal)
if loadedConfig.ShowAvatarsInMemberList {
default_av := createIdenticon(u.From, false)
userbox.Prepend(default_av)
var vcu VCardUpdate
ok = u.Get(&vcu)
if ok {
photo := vcu.Photo
go func() {
new_im := getAvatar(u.From, photo)
glib.IdleAdd(func() {
userbox.Remove(default_av)
userbox.Prepend(new_im)
})
}()
}
}
gesture := gtk.NewGestureClick()
gesture.SetButton(1)
mod_gesture := gtk.NewGestureClick()
mod_gesture.SetButton(3)
popover := gtk.NewPopover()
popover.SetHasArrow(false)
popover.SetParent(userbox)
rc_box := gtk.NewBox(gtk.OrientationVertical, 0)
bb := gtk.NewButtonWithLabel(loadedLocale["ban"])
kb := gtk.NewButtonWithLabel(loadedLocale["kick"])
ab := gtk.NewButtonWithLabel(loadedLocale["setAffil"])
rb := gtk.NewButtonWithLabel(loadedLocale["setRole"])
kb.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='kick1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item nick='%s' role='none'>
</item>
</query>
</iq>
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource))
})
bb.ConnectClicked(func() {
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='ban1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='outcast' jid='%s'/>
</query>
</iq>
`, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare()))
}
}
})
ab.ConnectClicked(func() {
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
win := gtk.NewWindow()
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel(loadedLocale["setAffilDescPartOne"] + JidMustParse(u.From).Resource + loadedLocale["setAffilDescPartTwo"]))
the_entry := gtk.NewEntry()
the_entry.SetText(mu.MucUserItem.Affiliation)
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='ba1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='%s' jid='%s'/>
</query>
</iq>
`, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare()))
win.SetVisible(false)
})
box.Append(the_entry)
box.Append(submit)
win.SetChild(box)
win.SetVisible(true)
}
}
})
rb.ConnectClicked(func() {
var mu MucUser
ok = u.Get(&mu)
if ok {
if mu.MucUserItem.JID != "" {
win := gtk.NewWindow()
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel(loadedLocale["setRoleDescPartOne"] + JidMustParse(u.From).Resource + loadedLocale["setRoleDescPartTwo"]))
box.Append(gtk.NewLabel(loadedLocale["setRoleWarning"]))
the_entry := gtk.NewEntry()
the_entry.SetText(mu.MucUserItem.Role)
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
id='kick1'
to='%s'
type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item nick='%s' role='%s'>
</item>
</query>
</iq>
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text()))
})
box.Append(the_entry)
box.Append(submit)
win.SetChild(box)
win.SetVisible(true)
}
}
})
rc_box.Append(bb)
rc_box.Append(kb)
rc_box.Append(ab)
rc_box.Append(rb)
popover.SetChild(rc_box)
mod_gesture.Connect("pressed", func(n_press, x, y int) {
rect := gdk.NewRectangle(x, y, 1, 1)
popover.SetPointingTo(&rect)
popover.Popup()
})
gesture.Connect("pressed", func(n_press, x, y int) {
win := gtk.NewWindow()
win.SetDefaultSize(400, 400)
win.SetResizable(false)
profile_box := gtk.NewBox(gtk.OrientationVertical, 0)
nick := gtk.NewLabel(JidMustParse(u.From).Resource)
if custom_nick != "" {
nick.SetText(custom_nick)
}
ver_text := gtk.NewLabel(loadedLocale["gettingVersion"])
ver_text.AddCSSClass("visitor")
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)
iqResp, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
From: clientroot.Session.BindJid,
To: u.From,
Id: "vc2",
Lang: "en",
})
if err != nil {
panic(err)
}
iqResp.Payload = &stanza.Version{}
loading_version_text := gtk.NewLabel("...")
var hats Hats
ok := u.Get(&hats)
if ok {
for _, hat := range hats.Hats {
l := gtk.NewLabel(hat.Title)
l.AddCSSClass("hat")
profile_box.Append(l)
}
}
var mu MucUser
ok = u.Get(&mu)
if ok {
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(loadedLocale["connectedWithRole"] + mu.MucUserItem.Role))
profile_box.Append(gtk.NewLabel(loadedLocale["affiliatedAs"] + mu.MucUserItem.Affiliation))
}
if ocu.ID != "" {
ocu_label := gtk.NewLabel(ocu.ID)
ocu_label.AddCSSClass("jid")
ocu_label.SetSelectable(true)
profile_box.Append(ocu_label)
}
go func() {
myIQ, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
From: clientroot.Session.BindJid,
To: u.From,
Id: "dicks",
Lang: "en",
})
if err != nil {
panic(err)
}
myIQ.Payload = &stanza.DiscoInfo{}
ctx := context.TODO()
mychan, err := client.SendIQ(ctx, myIQ)
if err == nil {
result := <-mychan
res, ok := result.Payload.(*stanza.DiscoInfo)
if ok {
idents := res.Identity
for i, ident := range idents {
profile_box.Append(gtk.NewLabel(fmt.Sprintf("%d: Name: %s, Category: %s, Type: %s", i+1, ident.Name, ident.Category, ident.Type)))
}
/*
s := fmt.Sprintf("%v", res.Features)
h := sha1.New()
h.Write([]byte(s))
sha1_hash := hex.EncodeToString(h.Sum(nil))
*/
sw := gtk.NewScrolledWindow()
s := ""
for _, feature := range res.Features {
s = s + feature.Var + "\n"
}
sw.SetChild(gtk.NewLabel(s))
profile_box.Append(sw)
}
}
}()
go func() {
ctx := context.TODO()
mychan, err := client.SendIQ(ctx, iqResp)
if err == nil {
result := <-mychan
ver, ok := result.Payload.(*stanza.Version)
if ok {
loading_version_text.SetVisible(false)
name := ver.Name
version := ver.Version
os := ver.OS
vr := fmt.Sprintf("%s %s %s", name, version, os)
if name == "" && version == "" && os == "" {
ver_text.SetText(loadedLocale["versionQueryEmpty"])
} else {
ver_text.SetText(vr)
ver_text.RemoveCSSClass("visitor")
}
} else if result.Error != nil && result.Error.Type != "" {
ver_text.SetText(loadedLocale["versionQueryError"])
ver_text.SetTooltipText(result.Error.Reason + ": " + result.Error.Text)
ver_text.RemoveCSSClass("visitor")
ver_text.AddCSSClass("error")
}
}
}()
go func() {
mo, _ := mucmembers.Load(JidMustParse(u.From).Bare())
mm := mo.(mucUnit)
mmm := mm.Members
mmmm, ok := mmm.Load(id)
if ok {
pres := mmmm.(stanza.Presence)
var vu VCardUpdate
pres.Get(&vu)
if vu.Photo != "" {
im := getAvatar(u.From, vu.Photo)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
} else {
im := createIdenticon(u.From, false)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
}
} else {
im := createIdenticon(u.From, false)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
}
}()
win.SetChild(profile_box)
win.SetTransientFor(win)
win.Present()
})
userbox.AddController(gesture)
userbox.AddController(mod_gesture)
return true
}))
headerBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
if i >= 500 {
headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("world")))
} else if i >= 50 {
headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("large_group")))
} else {
headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("group")))
}
headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, loadedLocale["participants"])))
gen.Prepend(headerBox)
muc_presence := typed_tab.muc_presence
if muc_presence != nil {
vc := &VCardUpdate{}
ok = muc_presence.Get(vc)
if ok {
muci := getAvatar(jid, vc.Photo)
muci.SetPixelSize(80)
gen.Prepend(muci)
}
}
muc_name := gtk.NewLabel(typed_tab.name)
muc_name.AddCSSClass("author")
muc_name.SetWrap(true)
gen.Prepend(muc_name)
memberList.SetChild(gen)
} else {
memberList.SetChild(gtk.NewLabel(jid))
}
}
func showErrorDialog(err error, w *gtk.Window) {
err_win := gtk.NewWindow()
err_win.SetTitle(loadedLocale["error"])
err_win.SetDefaultSize(400, 200)
err_win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
err_label := gtk.NewLabel(err.Error())
err_label.SetSelectable(true)
box.Append(err_label)
close_btn := gtk.NewButtonWithLabel(loadedLocale["close"])
close_btn.ConnectClicked(func() {
err_win.SetVisible(false)
})
box.Append(close_btn)
err_win.SetChild(box)
err_win.SetTransientFor(w)
err_win.Present()
}
func createIdenticon(word string, always_create bool) *gtk.Image { // This function generates an identicon
if !loadedConfig.Identicons && !always_create {
return gtk.NewImageFromPaintable(clientAssetsLoad("DefaultAvatar"))
}
identicon.SetBackgroundColorFunction(func([]byte, color.Color) color.Color {
return color.Transparent
})
gen, _ := identicon.New("", 5, 3)
ii, _ := gen.Draw(word)
im := ii.Image(250)
buf := new(bytes.Buffer)
err := png.Encode(buf, im)
if err != nil {
panic(err)
}
loader := gdkpixbuf.NewPixbufLoader()
loader.Write(buf.Bytes())
loader.Close()
p := loader.Pixbuf()
gt := gdk.NewTextureForPixbuf(p)
i := gtk.NewImageFromPaintable(gt)
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
return i
}
func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows the user to interactively build a JID
// TODO: Localise this
win := gtk.NewWindow()
win.SetTitle("Build-A-JID")
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 2)
header := gtk.NewLabel("Build-A-JID")
header.AddCSSClass("author")
box.Append(header)
box.Append(gtk.NewLabel("All fields except for domain are optional"))
jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2)
localPartEntry := gtk.NewEntry()
localPartEntry.SetPlaceholderText("localpart")
jid_builder.Append(localPartEntry)
at_sign := gtk.NewLabel("@")
at_sign.AddCSSClass("author")
jid_builder.Append(at_sign)
domainEntry := gtk.NewEntry()
domainEntry.SetPlaceholderText("domain")
jid_builder.Append(domainEntry)
resource_sign := gtk.NewLabel("/")
resource_sign.AddCSSClass("author")
jid_builder.Append(resource_sign)
resourceEntry := gtk.NewEntry()
resourceEntry.SetPlaceholderText("resource")
jid_builder.Append(resourceEntry)
box.Append(jid_builder)
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
localPart := localPartEntry.Text()
domain := domainEntry.Text()
resource := resourceEntry.Text()
at := "@"
slash := "/"
if localPart == "" {
at = ""
}
if resource == "" {
slash = ""
}
jid := localPart + at + domain + slash + resource
en.SetText(jid)
win.SetVisible(false)
})
box.Append(submit)
win.SetChild(box)
win.SetVisible(true)
}