diff --git a/README.md b/README.md index 3dd08c0..ad41ac2 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ an XMPP client icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im)) -additional icons are by Mark James's Silk Icon Set [https://peacocksoftware.com/sites/peacocksoftware/silk_icons/comment.png](https://peacocksoftware.com/sites/peacocksoftware/silk_icons/comment.png) +additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) diff --git a/assets/ban.png b/assets/ban.png new file mode 100644 index 0000000..b0c7006 Binary files /dev/null and b/assets/ban.png differ diff --git a/assets/door_in.png b/assets/door_in.png new file mode 100644 index 0000000..41676a0 Binary files /dev/null and b/assets/door_in.png differ diff --git a/assets/door_out.png b/assets/door_out.png new file mode 100644 index 0000000..de93a96 Binary files /dev/null and b/assets/door_out.png differ diff --git a/assets/large_group.png b/assets/large_group.png new file mode 100644 index 0000000..4839312 Binary files /dev/null and b/assets/large_group.png differ diff --git a/assets/world.png b/assets/world.png new file mode 100644 index 0000000..68f21d3 Binary files /dev/null and b/assets/world.png differ diff --git a/gtk-helpers.go b/gtk-helpers.go index 8f50834..db6e8be 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -253,7 +253,13 @@ func switchToTab(jid string, w *gtk.Window) { }) headerBox := gtk.NewBox(gtk.OrientationHorizontal, 0) - headerBox.Append(gtk.NewImageFromPaintable(clientAssets["group"])) + if i >= 500 { + headerBox.Append(gtk.NewImageFromPaintable(clientAssets["world"])) + } else if i >= 50 { + headerBox.Append(gtk.NewImageFromPaintable(clientAssets["large_group"])) + } else { + headerBox.Append(gtk.NewImageFromPaintable(clientAssets["group"])) + } headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d participant(s)", i))) gen.Prepend(headerBox) diff --git a/gtk-message.go b/gtk-message.go index fc263a4..d52a5a2 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -19,6 +19,7 @@ import ( ) func generatePresenceWidget(p stanza.Packet) gtk.Widgetter { + b := gtk.NewBox(gtk.OrientationHorizontal, 0) presence, ok := p.(stanza.Presence) if !ok { return gtk.NewLabel("Unsupported message.") @@ -29,14 +30,21 @@ func generatePresenceWidget(p stanza.Packet) gtk.Widgetter { ok := presence.Get(&mu) if ok { if mu.MucUserItem.Affiliation == "outcast" { - return gtk.NewLabel(jid.MustParse(presence.From).Resourcepart() + " has been banned!") + b.Append(gtk.NewImageFromPaintable(clientAssets["outcast"])) + b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource + " has been banned by " + mu.MucUserItem.Actor.Nick + "!")) + return b } } - return gtk.NewLabel(JidMustParse(presence.From).Resource + " left the MUC") + b.Append(gtk.NewImageFromPaintable(clientAssets["door_out"])) + b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource)) } else { - return gtk.NewLabel(JidMustParse(presence.From).Resource + " joined the MUC") + b.Append(gtk.NewImageFromPaintable(clientAssets["door_in"])) + b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource)) } + + b.SetTooltipText(presence.Status) + return b } func generateMessageWidget(p stanza.Packet) gtk.Widgetter { @@ -65,7 +73,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { if m.Error.Type != "" { error_box := gtk.NewBox(gtk.OrientationHorizontal, 0) cancel_img := gtk.NewImageFromPaintable(clientAssets["cancel"]) - error_label := gtk.NewLabel(m.Error.Text+ ": ") + error_label := gtk.NewLabel(m.Error.Text + ": ") error_box.Append(cancel_img) error_box.Append(error_label) @@ -147,7 +155,11 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { // authorBox.Append(im) - al := gtk.NewLabel(jid.MustParse(m.From).Resourcepart()) + n := jid.MustParse(m.From).Resourcepart() + if n == "" { + n = jid.MustParse(m.From).String() + } + al := gtk.NewLabel(n) al.AddCSSClass("author") al.SetSelectable(true) @@ -195,6 +207,12 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { mlabel.SetSelectable(true) mlabel.SetHAlign(gtk.AlignFill) + mum := MucUser{} + ok = m.Get(&mum) + if ok { + mlabel.SetText(fmt.Sprintf("%s's affiliation has been changed to %s", mum.MucUserItem.JID, mum.MucUserItem.Affiliation)) + } + contentBox.Append(mlabel) mainBox.Append(authorBox) diff --git a/main.go b/main.go index 35a2d34..b844b3b 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,6 @@ var cancelB64 string = base64.StdEncoding.EncodeToString(cancelBytes) var tagBytes []byte var tagB64 string = base64.StdEncoding.EncodeToString(tagBytes) - //go:embed assets/lambda-disabled.png var logoDisabledBytes []byte var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes) @@ -99,10 +98,28 @@ var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes var groupBytes []byte var groupB64 string = base64.StdEncoding.EncodeToString(groupBytes) +//go:embed assets/door_in.png +var doorInBytes []byte +var doorInB64 string = base64.StdEncoding.EncodeToString(doorInBytes) + +//go:embed assets/door_out.png +var doorOutBytes []byte +var doorOutB64 string = base64.StdEncoding.EncodeToString(doorOutBytes) + +//go:embed assets/large_group.png +var largeGroupBytes []byte +var largeGroupB64 string = base64.StdEncoding.EncodeToString(largeGroupBytes) + +//go:embed assets/world.png +var worldBytes []byte +var worldB64 string = base64.StdEncoding.EncodeToString(worldBytes) + var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler) var lockedJIDs map[string]bool = make(map[string]bool) func init() { + beeep.AppName = "Lambda" + go func() { for fn := range uiQueue { glib.IdleAdd(func() bool { @@ -176,7 +193,6 @@ func init() { clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) - loader = gdkpixbuf.NewPixbufLoader() disabledLogoData, _ := base64.StdEncoding.DecodeString(logoDisabledB64) @@ -192,6 +208,38 @@ func init() { loader.Close() clientAssets["group"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + doorInData, _ := base64.StdEncoding.DecodeString(doorInB64) + loader.Write(doorInData) + loader.Close() + + clientAssets["door_in"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + doorOutData, _ := base64.StdEncoding.DecodeString(doorOutB64) + loader.Write(doorOutData) + loader.Close() + + clientAssets["door_out"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + largeGroupData, _ := base64.StdEncoding.DecodeString(largeGroupB64) + loader.Write(largeGroupData) + loader.Close() + + clientAssets["large_group"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + worldData, _ := base64.StdEncoding.DecodeString(worldB64) + loader.Write(worldData) + loader.Close() + + clientAssets["world"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) } func main() { @@ -368,8 +416,8 @@ func main() { _, ok := typed_unit.Members.Load(id) if !ok { glib.IdleAdd(func() { - b := gtk.NewLabel("") - ba, ok := generatePresenceWidget(p).(*gtk.Label) + b := gtk.NewBox(gtk.OrientationVertical, 0) + ba, ok := generatePresenceWidget(p).(*gtk.Box) if ok { b = ba } @@ -389,8 +437,8 @@ func main() { } else { typed_unit.Members.Delete(id) glib.IdleAdd(func() { - b := gtk.NewLabel("") - ba, ok := generatePresenceWidget(p).(*gtk.Label) + b := gtk.NewBox(gtk.OrientationVertical, 0) + ba, ok := generatePresenceWidget(p).(*gtk.Box) if ok { b = ba } @@ -462,8 +510,49 @@ func main() { cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) { fmt.Println("XMPP client connected") - /* - */ + books, err := stanza.NewItemsRequest("", "urn:xmpp:bookmarks:1", 0) + if err == nil { + mychan, err := c.SendIQ(context.TODO(), books) + result := <-mychan + if err == nil { + res, ok := result.Payload.(*stanza.PubSubGeneric) + if ok { + for _, item := range res.Items.List { + jid := item.Id + node := item.Any + autojoin := false + nick := loadedConfig.Nick + for _, attr := range node.Attrs { + if attr.Name.Local == "autojoin" { + autojoin = attr.Value == "true" + } + } + + for _, node := range node.Nodes { + if node.XMLName.Local == "nick" { + nick = node.Content + } + } + + _, ok := tabs.Load(jid) + if !ok && autojoin { + err := joinMuc(client, clientroot.Session.BindJid, jid, nick) + if err != nil { + panic(err) + } + + createTab(jid, true) + b := gtk.NewButtonWithLabel(jid) + b.ConnectClicked(func() { + b.AddCSSClass("accent") + switchToTab(jid, &window.Window) + }) + menu.Append(b) + } + } + } + } + } }) go func() { @@ -509,7 +598,7 @@ func activate(app *gtk.Application) { destroymucAction := gio.NewSimpleAction("destroymuc", nil) destroymucAction.ConnectActivate(func(p *glib.Variant) { - cur, ok := tabs.Load(current) + cur, ok := tabs.Load(current) if ok { cur := cur.(*chatTab) if cur.isMuc { diff --git a/xmpp-bookmarks.go b/xmpp-bookmarks.go new file mode 100644 index 0000000..1f86565 --- /dev/null +++ b/xmpp-bookmarks.go @@ -0,0 +1,22 @@ +package main + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" +) + +// Implementation of XEP-0402: PEP Native Bookmarks +// https://xmpp.org/extensions/xep-0402.html + +type Bookmark struct { + XMLName xml.Name `xml:"urn:xmpp:bookmarks:1 conference"` + Name string `xml:"name,attr,omitempty"` + Autojoin bool `xml:"autojoin,attr,omitempty"` + Nick string `xml:"nick,omitempty"` + Password string `xml:"password,omitempty"` + Extensions []any `xml:"extensions,omitempty"` +} + +func init() { + stanza.TypeRegistry.MapExtension(stanza.PKTIQ, xml.Name{Space: "urn:xmpp:bookmarks:1", Local: "conference"}, Bookmark{}) +} diff --git a/xmpp-mucuser.go b/xmpp-mucuser.go index f2b7c89..5a5a704 100644 --- a/xmpp-mucuser.go +++ b/xmpp-mucuser.go @@ -10,6 +10,7 @@ import ( type MucUser struct { stanza.PresExtension + stanza.MsgExtension XMLName xml.Name `xml:"http://jabber.org/protocol/muc#user x"` MucUserItem MucUserItem `xml:"item,omitempty"` } @@ -19,9 +20,16 @@ type MucUserItem struct { Affiliation string `xml:"affiliation,attr,omitempty"` // TODO: Use enum Role string `xml:"role,attr,omitempty"` // TODO: Use enum JID string `xml:"jid,attr,omitempty"` - Reason string `xml:"reason,omitempty"` + Reason string `xml:"reason,omitempty"` + Actor Actor `xml:"actor,omitempty"` +} + +type Actor struct { + JID string `xml:"jid,attr"` + Nick string `xml:"nick,attr"` } func init() { stanza.TypeRegistry.MapExtension(stanza.PKTPresence, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{}) + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "http://jabber.org/protocol/muc#user", Local: "x"}, MucUser{}) }