2026-01-29 21:35:36 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
2026-01-30 10:40:38 +00:00
|
|
|
"sync"
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
|
|
|
|
"github.com/diamondburned/gotk4/pkg/gio/v2"
|
|
|
|
|
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
|
|
|
|
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
2026-02-02 13:23:08 +00:00
|
|
|
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
|
2026-02-01 13:49:30 +00:00
|
|
|
_ "github.com/kr/pretty"
|
2026-01-31 21:35:26 +00:00
|
|
|
"path/filepath"
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
|
"gosrc.io/xmpp"
|
|
|
|
|
"gosrc.io/xmpp/stanza"
|
|
|
|
|
"mellium.im/xmpp/jid"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
_ "embed"
|
|
|
|
|
"encoding/xml"
|
2026-01-31 23:32:26 +00:00
|
|
|
"math/rand/v2"
|
2026-01-30 15:56:58 +00:00
|
|
|
"runtime"
|
2026-02-02 13:23:08 +00:00
|
|
|
"encoding/base64"
|
2026-01-29 21:35:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var loadedConfig lambdaConfig
|
|
|
|
|
|
|
|
|
|
var empty_dialog *gtk.Label
|
|
|
|
|
|
|
|
|
|
// var msgs *gtk.ListBox
|
|
|
|
|
var content *gtk.Widgetter
|
|
|
|
|
|
2026-01-30 21:31:33 +00:00
|
|
|
// var tabs map[string]*chatTab = make(map[string]*chatTab)
|
|
|
|
|
var tabs sync.Map
|
2026-01-29 21:35:36 +00:00
|
|
|
var current string
|
|
|
|
|
|
|
|
|
|
var scroller *gtk.ScrolledWindow
|
2026-01-30 10:40:38 +00:00
|
|
|
var memberList *gtk.ScrolledWindow
|
2026-01-30 21:31:33 +00:00
|
|
|
var menu *gtk.Box
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
//go:embed style.css
|
|
|
|
|
var styleCSS string
|
|
|
|
|
var client xmpp.Sender
|
|
|
|
|
var clientroot *xmpp.Client
|
|
|
|
|
|
|
|
|
|
var uiQueue = make(chan func(), 100)
|
|
|
|
|
|
2026-01-31 15:38:02 +00:00
|
|
|
var window *gtk.ApplicationWindow
|
|
|
|
|
|
2026-01-30 10:40:38 +00:00
|
|
|
// stores members of mucs
|
|
|
|
|
var mucmembers sync.Map
|
|
|
|
|
|
2026-01-31 10:02:04 +00:00
|
|
|
// stores devices of users
|
2026-01-30 21:31:33 +00:00
|
|
|
var userdevices sync.Map
|
|
|
|
|
|
2026-02-02 13:23:08 +00:00
|
|
|
//go:embed debug.png
|
|
|
|
|
var defaultAvatarBytes []byte
|
|
|
|
|
var defaultAvatarB64 string = base64.StdEncoding.EncodeToString(defaultAvatarBytes)
|
|
|
|
|
|
|
|
|
|
//go:embed assets/owner.png
|
|
|
|
|
var ownerMedalBytes []byte
|
|
|
|
|
var ownerMedalB64 string = base64.StdEncoding.EncodeToString(ownerMedalBytes)
|
|
|
|
|
|
|
|
|
|
//go:embed assets/admin.png
|
|
|
|
|
var adminMedalBytes []byte
|
|
|
|
|
var adminMedalB64 string = base64.StdEncoding.EncodeToString(adminMedalBytes)
|
|
|
|
|
|
|
|
|
|
//go:embed assets/member.png
|
|
|
|
|
var memberMedalBytes []byte
|
|
|
|
|
var memberMedalB64 string = base64.StdEncoding.EncodeToString(memberMedalBytes)
|
|
|
|
|
|
|
|
|
|
//go:embed assets/noaffiliation.png
|
|
|
|
|
var noneMedalBytes []byte
|
|
|
|
|
var noneMedalB64 string = base64.StdEncoding.EncodeToString(noneMedalBytes)
|
|
|
|
|
|
|
|
|
|
//go:embed assets/outcast.png
|
|
|
|
|
var outcastMedalBytes []byte
|
|
|
|
|
var outcastMedalB64 string = base64.StdEncoding.EncodeToString(outcastMedalBytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
|
|
|
|
|
|
2026-01-29 21:35:36 +00:00
|
|
|
func init() {
|
|
|
|
|
go func() {
|
|
|
|
|
for fn := range uiQueue {
|
|
|
|
|
glib.IdleAdd(func() bool {
|
|
|
|
|
fn()
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
time.Sleep(10 * time.Millisecond) // Small delay between updates
|
|
|
|
|
}
|
|
|
|
|
}()
|
2026-02-02 13:23:08 +00:00
|
|
|
|
|
|
|
|
loader := gdkpixbuf.NewPixbufLoader()
|
|
|
|
|
|
|
|
|
|
defaultAvatarData, _ := base64.StdEncoding.DecodeString(defaultAvatarB64)
|
|
|
|
|
loader.Write(defaultAvatarData)
|
|
|
|
|
loader.Close()
|
|
|
|
|
clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loader = gdkpixbuf.NewPixbufLoader()
|
|
|
|
|
|
|
|
|
|
ownerMedalData, _ := base64.StdEncoding.DecodeString(ownerMedalB64)
|
|
|
|
|
loader.Write(ownerMedalData)
|
|
|
|
|
loader.Close()
|
|
|
|
|
|
|
|
|
|
clientAssets["owner"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
|
|
|
|
|
|
|
|
loader = gdkpixbuf.NewPixbufLoader()
|
|
|
|
|
|
|
|
|
|
adminMedalData, _ := base64.StdEncoding.DecodeString(adminMedalB64)
|
|
|
|
|
loader.Write(adminMedalData)
|
|
|
|
|
loader.Close()
|
|
|
|
|
|
|
|
|
|
clientAssets["admin"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
|
|
|
|
|
|
|
|
loader = gdkpixbuf.NewPixbufLoader()
|
|
|
|
|
|
|
|
|
|
memberMedalData, _ := base64.StdEncoding.DecodeString(memberMedalB64)
|
|
|
|
|
loader.Write(memberMedalData)
|
|
|
|
|
loader.Close()
|
|
|
|
|
|
|
|
|
|
clientAssets["member"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
|
|
|
|
|
|
|
|
loader = gdkpixbuf.NewPixbufLoader()
|
|
|
|
|
|
|
|
|
|
noneMedalData, _ := base64.StdEncoding.DecodeString(noneMedalB64)
|
|
|
|
|
loader.Write(noneMedalData)
|
|
|
|
|
loader.Close()
|
|
|
|
|
|
|
|
|
|
clientAssets["none"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
|
|
|
|
|
|
|
|
|
loader = gdkpixbuf.NewPixbufLoader()
|
|
|
|
|
|
|
|
|
|
outcastMedalData, _ := base64.StdEncoding.DecodeString(outcastMedalB64)
|
|
|
|
|
loader.Write(outcastMedalData)
|
|
|
|
|
loader.Close()
|
|
|
|
|
|
|
|
|
|
clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
2026-01-29 21:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-01 09:05:20 +00:00
|
|
|
func main() {
|
|
|
|
|
p, err := ensureConfig()
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
2026-01-31 21:35:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b, err := os.ReadFile(filepath.Join(p, "lambda.toml"))
|
2026-01-29 21:35:36 +00:00
|
|
|
if err != nil {
|
2026-01-31 21:35:26 +00:00
|
|
|
dropToSignInPage(err)
|
|
|
|
|
return
|
|
|
|
|
// panic(err)
|
2026-01-29 21:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = toml.Decode(string(b), &loadedConfig)
|
2026-01-31 21:35:26 +00:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
2026-01-29 21:35:36 +00:00
|
|
|
|
2026-02-02 13:23:08 +00:00
|
|
|
// Put 4 random characters at the end
|
2026-01-31 23:32:26 +00:00
|
|
|
chars := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZλ"
|
|
|
|
|
str := ""
|
|
|
|
|
for range 4 {
|
|
|
|
|
str = str + string(chars[rand.IntN(len(chars))])
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 21:35:36 +00:00
|
|
|
config := xmpp.Config{
|
|
|
|
|
TransportConfiguration: xmpp.TransportConfiguration{
|
|
|
|
|
Address: loadedConfig.Server,
|
|
|
|
|
},
|
2026-02-01 19:22:43 +00:00
|
|
|
Jid: loadedConfig.Username + "/lambda." + str,
|
|
|
|
|
Credential: xmpp.Password(loadedConfig.Password),
|
|
|
|
|
Insecure: loadedConfig.Insecure,
|
2026-02-01 13:49:30 +00:00
|
|
|
StreamLogger: os.Stdout,
|
2026-01-29 21:35:36 +00:00
|
|
|
}
|
|
|
|
|
router := xmpp.NewRouter()
|
|
|
|
|
|
|
|
|
|
router.NewRoute().IQNamespaces(stanza.NSDiscoInfo).HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
|
|
|
|
iq, ok := p.(*stanza.IQ)
|
|
|
|
|
if !ok || iq.Type != stanza.IQTypeGet {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
identity := stanza.Identity{
|
|
|
|
|
Name: "Lambda",
|
|
|
|
|
Category: "client", // TODO: Allow spoofing on user request
|
|
|
|
|
Type: "pc",
|
|
|
|
|
}
|
|
|
|
|
payload := stanza.DiscoInfo{
|
|
|
|
|
XMLName: xml.Name{
|
|
|
|
|
Space: stanza.NSDiscoInfo,
|
|
|
|
|
Local: "query",
|
|
|
|
|
},
|
|
|
|
|
Identity: []stanza.Identity{identity},
|
|
|
|
|
Features: []stanza.Feature{
|
|
|
|
|
{Var: stanza.NSDiscoInfo},
|
|
|
|
|
{Var: stanza.NSDiscoItems},
|
|
|
|
|
{Var: "jabber:iq:version"},
|
|
|
|
|
{Var: "urn:xmpp:delegation:1"},
|
|
|
|
|
{Var: "http://jabber.org/protocol/muc"},
|
|
|
|
|
{Var: "urn:xmpp:reply:0"},
|
2026-01-31 15:08:54 +00:00
|
|
|
{Var: "λ"},
|
2026-01-29 21:35:36 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
iqResp.Payload = &payload
|
|
|
|
|
s.Send(iqResp)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
router.NewRoute().IQNamespaces("jabber:iq:version").HandlerFunc(func(s xmpp.Sender, p stanza.Packet) {
|
|
|
|
|
iq, ok := p.(*stanza.IQ)
|
|
|
|
|
if !ok || iq.Type != stanza.IQTypeGet {
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iqResp, err := stanza.NewIQ(stanza.Attrs{Type: "result", From: iq.To, To: iq.From, Id: iq.Id})
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v := &stanza.Version{}
|
2026-01-30 15:56:58 +00:00
|
|
|
v = v.SetInfo("Lambda", lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
iqResp.Payload = v
|
|
|
|
|
s.Send(iqResp)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) {
|
|
|
|
|
m, ok := p.(stanza.Message)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 23:32:26 +00:00
|
|
|
/*
|
2026-02-01 13:49:30 +00:00
|
|
|
if m.Body == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-31 23:32:26 +00:00
|
|
|
*/
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
originator := jid.MustParse(m.From).Bare().String()
|
|
|
|
|
|
|
|
|
|
glib.IdleAdd(func() {
|
2026-02-01 18:16:55 +00:00
|
|
|
//uiQueue <- func() {
|
2026-02-01 19:22:43 +00:00
|
|
|
b := gtk.NewBox(gtk.OrientationVertical, 0)
|
|
|
|
|
ba, ok := generateMessageWidget(p).(*gtk.Box)
|
|
|
|
|
if ok {
|
|
|
|
|
b = ba
|
|
|
|
|
}
|
2026-01-29 21:35:36 +00:00
|
|
|
|
2026-02-01 19:22:43 +00:00
|
|
|
tab, ok := tabs.Load(originator)
|
|
|
|
|
typed_tab := tab.(*chatTab)
|
2026-01-29 21:35:36 +00:00
|
|
|
|
2026-02-01 19:22:43 +00:00
|
|
|
if ok {
|
|
|
|
|
typed_tab.msgs.Append(b)
|
|
|
|
|
scrollToBottomAfterUpdate(scroller)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println("Got message when the tab does not exist!")
|
|
|
|
|
}
|
2026-02-01 18:16:55 +00:00
|
|
|
//}
|
2026-01-29 21:35:36 +00:00
|
|
|
})
|
|
|
|
|
})
|
2026-01-30 10:40:38 +00:00
|
|
|
|
|
|
|
|
router.HandleFunc("presence", func(s xmpp.Sender, p stanza.Packet) {
|
|
|
|
|
presence, ok := p.(stanza.Presence)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 21:31:33 +00:00
|
|
|
if presence.Error != *new(stanza.Err) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 10:40:38 +00:00
|
|
|
var mu MucUser
|
|
|
|
|
var ocu OccupantID
|
|
|
|
|
|
|
|
|
|
ok = presence.Get(&mu)
|
|
|
|
|
|
|
|
|
|
if ok { // This is a presence stanza from a user in a MUC
|
|
|
|
|
presence.Get(&ocu)
|
2026-02-01 13:49:30 +00:00
|
|
|
from, _ := stanza.NewJid(presence.From)
|
|
|
|
|
muc := from.Bare()
|
2026-01-30 10:40:38 +00:00
|
|
|
_, ok = mucmembers.Load(muc)
|
|
|
|
|
if !ok {
|
|
|
|
|
mucmembers.Store(muc, mucUnit{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unit, ok := mucmembers.Load(muc)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typed_unit := unit.(mucUnit)
|
|
|
|
|
|
|
|
|
|
if presence.Type != "unavailable" {
|
|
|
|
|
typed_unit.Members.Store(ocu.ID, presence)
|
|
|
|
|
} else {
|
|
|
|
|
typed_unit.Members.Delete(ocu.ID)
|
2026-01-31 10:02:04 +00:00
|
|
|
glib.IdleAdd(func() {
|
2026-02-01 18:16:55 +00:00
|
|
|
//uiQueue <- func() {
|
2026-02-01 19:22:43 +00:00
|
|
|
b := gtk.NewLabel("")
|
|
|
|
|
ba, ok := generatePresenceWidget(p).(*gtk.Label)
|
|
|
|
|
if ok {
|
|
|
|
|
b = ba
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tab, ok := tabs.Load(muc)
|
|
|
|
|
typed_tab := tab.(*chatTab)
|
|
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
typed_tab.msgs.Append(b)
|
|
|
|
|
scrollToBottomAfterUpdate(scroller)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println("Got message when the tab does not exist!")
|
|
|
|
|
}
|
2026-02-01 18:16:55 +00:00
|
|
|
//}
|
2026-01-31 10:02:04 +00:00
|
|
|
})
|
2026-01-30 10:40:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mucmembers.Store(muc, typed_unit)
|
|
|
|
|
|
2026-01-30 21:31:33 +00:00
|
|
|
} else { // This is a presence stanza from a regular user
|
2026-01-31 10:02:04 +00:00
|
|
|
// The code is basically the exact same as above, we just don't check for mucuser
|
2026-01-30 21:31:33 +00:00
|
|
|
user := jid.MustParse(presence.From).Bare().String()
|
|
|
|
|
_, ok := userdevices.Load(user)
|
2026-01-31 10:02:04 +00:00
|
|
|
_, mok := mucmembers.Load(user)
|
|
|
|
|
if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check
|
2026-02-01 13:49:30 +00:00
|
|
|
ok := createTab(user, false)
|
|
|
|
|
if ok {
|
|
|
|
|
userdevices.Store(user, userUnit{})
|
|
|
|
|
|
|
|
|
|
b := gtk.NewButtonWithLabel(user)
|
|
|
|
|
b.ConnectClicked(func() {
|
|
|
|
|
b.AddCSSClass("accent")
|
|
|
|
|
switchToTab(user, &window.Window)
|
|
|
|
|
})
|
|
|
|
|
menu.Append(b)
|
|
|
|
|
}
|
2026-01-30 21:31:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unit, ok := userdevices.Load(user)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource := jid.MustParse(presence.From).Resourcepart()
|
|
|
|
|
|
|
|
|
|
typed_unit := unit.(userUnit)
|
|
|
|
|
|
|
|
|
|
if presence.Type != "unavailable" {
|
|
|
|
|
typed_unit.Devices.Store(resource, presence)
|
|
|
|
|
} else {
|
|
|
|
|
typed_unit.Devices.Delete(resource)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userdevices.Store(user, typed_unit)
|
2026-01-30 10:40:38 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-29 21:35:36 +00:00
|
|
|
c, err := xmpp.NewClient(&config, router, func(err error) {
|
|
|
|
|
showErrorDialog(err)
|
|
|
|
|
panic(err)
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
showErrorDialog(err)
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
client = c
|
|
|
|
|
clientroot = c
|
|
|
|
|
|
|
|
|
|
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
|
|
|
|
|
fmt.Println("XMPP client connected")
|
|
|
|
|
/*
|
|
|
|
|
*/
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
go func() {
|
2026-01-31 21:35:26 +00:00
|
|
|
time.Sleep(3 * time.Second)
|
2026-01-29 21:35:36 +00:00
|
|
|
err = cm.Run()
|
|
|
|
|
if err != nil {
|
|
|
|
|
showErrorDialog(err)
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
|
|
|
|
|
app.ConnectActivate(func() { activate(app) })
|
|
|
|
|
|
|
|
|
|
if code := app.Run(os.Args); code > 0 {
|
|
|
|
|
os.Exit(code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func activate(app *gtk.Application) {
|
|
|
|
|
// Load the CSS and apply it globally.
|
|
|
|
|
gtk.StyleContextAddProviderForDisplay(
|
|
|
|
|
gdk.DisplayGetDefault(), loadCSS(styleCSS),
|
|
|
|
|
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-31 15:38:02 +00:00
|
|
|
window = gtk.NewApplicationWindow(app)
|
2026-02-01 19:22:43 +00:00
|
|
|
the_menu := gio.NewMenu()
|
|
|
|
|
|
|
|
|
|
fileMenu := gio.NewMenu()
|
2026-02-02 13:23:08 +00:00
|
|
|
fileMenu.Append("Join MUC", "app.join")
|
2026-02-01 19:22:43 +00:00
|
|
|
fileMenu.Append("Start DM", "app.dm")
|
|
|
|
|
|
|
|
|
|
joinAction := gio.NewSimpleAction("join", nil)
|
|
|
|
|
joinAction.ConnectActivate(func(p *glib.Variant) {
|
|
|
|
|
box := gtk.NewBox(gtk.OrientationVertical, 0)
|
|
|
|
|
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
|
|
|
|
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
|
|
|
|
|
|
|
|
|
jid_entry := gtk.NewEntry()
|
|
|
|
|
nick_entry := gtk.NewEntry()
|
|
|
|
|
|
|
|
|
|
jid_entry.SetHAlign(gtk.AlignEnd)
|
|
|
|
|
jid_entry.SetHExpand(true)
|
|
|
|
|
|
|
|
|
|
nick_entry.SetHAlign(gtk.AlignEnd)
|
|
|
|
|
nick_entry.SetHExpand(true)
|
|
|
|
|
|
|
|
|
|
nick_entry.SetText(loadedConfig.Nick)
|
|
|
|
|
|
|
|
|
|
jid_box.Append(gtk.NewLabel("MUC JID:"))
|
|
|
|
|
jid_box.Append(jid_entry)
|
|
|
|
|
|
|
|
|
|
nick_box.Append(gtk.NewLabel("Nick:"))
|
|
|
|
|
nick_box.Append(nick_entry)
|
|
|
|
|
|
|
|
|
|
box.Append(jid_box)
|
|
|
|
|
box.Append(nick_box)
|
|
|
|
|
|
|
|
|
|
btn := gtk.NewButtonWithLabel("Submit")
|
|
|
|
|
btn.SetVAlign(gtk.AlignBaseline)
|
|
|
|
|
|
|
|
|
|
box.Append(btn)
|
|
|
|
|
|
|
|
|
|
win := gtk.NewWindow()
|
2026-02-02 13:23:08 +00:00
|
|
|
win.SetTitle("Join MUC")
|
2026-02-01 19:22:43 +00:00
|
|
|
win.SetDefaultSize(200, 200)
|
|
|
|
|
win.SetChild(box)
|
|
|
|
|
|
|
|
|
|
btn.ConnectClicked(func() {
|
|
|
|
|
t := jid_entry.Text()
|
|
|
|
|
_, ok := tabs.Load(t)
|
|
|
|
|
if !ok {
|
|
|
|
|
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text())
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createTab(t, true)
|
|
|
|
|
b := gtk.NewButtonWithLabel(t)
|
|
|
|
|
b.ConnectClicked(func() {
|
|
|
|
|
b.AddCSSClass("accent")
|
|
|
|
|
switchToTab(t, &window.Window)
|
|
|
|
|
})
|
|
|
|
|
menu.Append(b)
|
|
|
|
|
}
|
|
|
|
|
win.SetVisible(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
win.SetTransientFor(win)
|
|
|
|
|
win.Present()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.AddAction(joinAction)
|
|
|
|
|
|
|
|
|
|
the_menu.AppendSubmenu("File", fileMenu)
|
|
|
|
|
|
|
|
|
|
the_menuBar := gtk.NewPopoverMenuBarFromModel(the_menu)
|
2026-01-29 21:35:36 +00:00
|
|
|
app.SetMenubar(gio.NewMenu())
|
|
|
|
|
|
|
|
|
|
window.SetTitle("Lambda")
|
2026-01-30 19:08:18 +00:00
|
|
|
window.Window.AddCSSClass("ssd")
|
2026-02-01 09:17:57 +00:00
|
|
|
window.Window.SetDefaultSize(500, 500)
|
2026-01-31 10:02:04 +00:00
|
|
|
menu = gtk.NewBox(gtk.OrientationVertical, 0)
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
empty_dialog = gtk.NewLabel("You are not focused on any chats.")
|
|
|
|
|
empty_dialog.SetVExpand(true)
|
|
|
|
|
|
|
|
|
|
scroller = gtk.NewScrolledWindow()
|
2026-01-30 10:40:38 +00:00
|
|
|
memberList = gtk.NewScrolledWindow()
|
|
|
|
|
|
|
|
|
|
scroller.SetHExpand(true)
|
|
|
|
|
memberList.SetHExpand(true)
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
box := gtk.NewBox(gtk.OrientationVertical, 0)
|
2026-02-01 19:22:43 +00:00
|
|
|
box.Append(the_menuBar)
|
|
|
|
|
|
2026-01-29 21:35:36 +00:00
|
|
|
// scroller.SetChild(empty_dialog)
|
|
|
|
|
scroller.SetChild(empty_dialog)
|
|
|
|
|
menu_scroll := gtk.NewScrolledWindow()
|
2026-01-31 10:02:04 +00:00
|
|
|
menu_scroll.SetHExpand(true)
|
|
|
|
|
|
2026-01-29 21:35:36 +00:00
|
|
|
menu_scroll.SetChild(menu)
|
2026-01-31 10:02:04 +00:00
|
|
|
// box.Append(menu_scroll)
|
2026-01-30 10:40:38 +00:00
|
|
|
|
|
|
|
|
chatbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
2026-01-31 10:02:04 +00:00
|
|
|
// chatbox.Append(menu_scroll)
|
|
|
|
|
chat_pane := gtk.NewPaned(gtk.OrientationHorizontal)
|
|
|
|
|
chat_pane.SetStartChild(scroller)
|
|
|
|
|
chat_pane.SetEndChild(memberList)
|
2026-02-01 09:17:57 +00:00
|
|
|
chat_pane.SetPosition(225)
|
2026-01-31 10:02:04 +00:00
|
|
|
|
|
|
|
|
main_pane := gtk.NewPaned(gtk.OrientationHorizontal)
|
|
|
|
|
main_pane.SetStartChild(menu_scroll)
|
|
|
|
|
main_pane.SetEndChild(chat_pane)
|
2026-02-01 09:17:57 +00:00
|
|
|
main_pane.SetPosition(135)
|
2026-01-31 10:02:04 +00:00
|
|
|
|
|
|
|
|
chatbox.Append(main_pane)
|
2026-01-30 10:40:38 +00:00
|
|
|
box.Append(chatbox)
|
2026-01-29 21:35:36 +00:00
|
|
|
|
|
|
|
|
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
|
|
|
|
|
|
|
|
|
|
en := gtk.NewEntry()
|
|
|
|
|
en.SetPlaceholderText("Say something, what else are you gonna do here?")
|
|
|
|
|
b := gtk.NewButtonWithLabel("Send")
|
|
|
|
|
|
|
|
|
|
sendtxt := func() {
|
|
|
|
|
t := en.Text()
|
|
|
|
|
if t == "" {
|
|
|
|
|
dialog := >k.AlertDialog{}
|
|
|
|
|
dialog.SetDetail("detail")
|
|
|
|
|
dialog.SetMessage("message")
|
|
|
|
|
dialog.SetButtons([]string{"yes, no"})
|
|
|
|
|
dialog.Choose(context.TODO(), &window.Window, nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 21:31:33 +00:00
|
|
|
message_type := stanza.MessageTypeChat
|
|
|
|
|
tab, ok := tabs.Load(current)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typed_tab := tab.(*chatTab)
|
|
|
|
|
if typed_tab.isMuc {
|
2026-01-31 10:02:04 +00:00
|
|
|
message_type = stanza.MessageTypeGroupchat
|
|
|
|
|
}
|
2026-01-30 21:31:33 +00:00
|
|
|
|
|
|
|
|
err := sendMessage(client, current, message_type, t, "", "")
|
2026-01-29 21:35:36 +00:00
|
|
|
if err != nil {
|
|
|
|
|
panic(err) // TODO: Show error message via GTK
|
|
|
|
|
}
|
|
|
|
|
en.SetText("")
|
|
|
|
|
scrollToBottomAfterUpdate(scroller)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
en.Connect("activate", sendtxt)
|
|
|
|
|
|
|
|
|
|
b.ConnectClicked(sendtxt)
|
|
|
|
|
|
|
|
|
|
en.SetHExpand(true)
|
|
|
|
|
|
|
|
|
|
entry_box.Append(en)
|
|
|
|
|
entry_box.Append(b)
|
|
|
|
|
|
|
|
|
|
box.Append(entry_box)
|
|
|
|
|
|
|
|
|
|
window.SetChild(box)
|
|
|
|
|
|
|
|
|
|
window.SetVisible(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadCSS(content string) *gtk.CSSProvider {
|
|
|
|
|
prov := gtk.NewCSSProvider()
|
|
|
|
|
prov.LoadFromString(content)
|
|
|
|
|
return prov
|
|
|
|
|
}
|