diff --git a/README.md b/README.md index ad41ac2..0391da0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ an XMPP client +Experimental XMPP client written in Go, with the GTK4 UI toolkit and the Fluux IO XMPP libraries. + 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://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) +additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) and the [Haiku project](https://www.haiku-os.org/) + +pepper & carrot default avatar is from [Pepper&Carrot](https://peppercarrot.com) - created by David Revoy, licensed under Creative Commons Attribution 4.0. Change made: cropped and made black & white diff --git a/assets.go b/assets.go index ea162f1..7cecf77 100644 --- a/assets.go +++ b/assets.go @@ -7,7 +7,9 @@ import ( "sync" ) -//go:embed debug.png +// If you want to change the default profile picture used when Identicons are disabled, you can change the below line to an image in the repository and recompile Lambda. + +//go:embed debug_psi.png var defaultAvatarBytes []byte //go:embed failed_load.png @@ -159,6 +161,53 @@ var solarBytes []byte //go:embed assets/timeline_marker.png var timelineMarkerBytes []byte +//go:embed assets/cap.png +var capIconBytes []byte + +// client icons + +//go:embed assets/client_icons/monocles.png +var monoclesCapBytes []byte + +//go:embed assets/client_icons/gajim.png +var gajimCapBytes []byte + +//go:embed assets/client_icons/conversations.png +var conversationsCapBytes []byte + +//go:embed assets/client_icons/dino.png +var dinoCapBytes []byte + +//go:embed assets/client_icons/blabber.png +var blabberCapBytes []byte + +//go:embed assets/client_icons/profanity.png +var profanityCapBytes []byte + +//go:embed assets/client_icons/poezio.png +var poezioCapBytes []byte + +//go:embed assets/client_icons/mcabber.png +var mcabberCapBytes []byte + +//go:embed assets/client_icons/converse.png +var converseJSCapBytes []byte + +//go:embed assets/client_icons/pidgin.png +var pidginCapBytes []byte + +//go:embed assets/client_icons/anotherim.png +var anotherImCapBytes []byte + +//go:embed assets/client_icons/movim.png +var movimCapBytes []byte + +//go:embed assets/client_icons/yaxim.png +var yaximCapBytes []byte + +//go:embed assets/client_icons/psi.png +var psiCapBytes []byte + var clientAssets sync.Map func loadAsset(key string, data []byte) { @@ -234,6 +283,22 @@ func init() { "update": updateBytes, "solar": solarBytes, "timeline_marker": timelineMarkerBytes, + "cap": capIconBytes, + + "https://monocles.eu": monoclesCapBytes, + "https://gajim.org": gajimCapBytes, + "http://conversations.im": conversationsCapBytes, + "https://dino.im": dinoCapBytes, + "http://blabber.im": blabberCapBytes, + "http://profanity-im.github.io": profanityCapBytes, + "https://poez.io": poezioCapBytes, + "http://mcabber.com/caps": mcabberCapBytes, + "https://conversejs.org": converseJSCapBytes, + "http://pidgin.im/": pidginCapBytes, + "https://dev.narayana.im/narayana/Conversations-Classic": anotherImCapBytes, + "https://movim.eu/": movimCapBytes, + "https://yaxim.org/": yaximCapBytes, + "https://psi-im.org": psiCapBytes, } { loadAsset(key, data) } diff --git a/assets/cap.png b/assets/cap.png new file mode 100644 index 0000000..0cd8031 Binary files /dev/null and b/assets/cap.png differ diff --git a/assets/client_icons/adium.png b/assets/client_icons/adium.png new file mode 100644 index 0000000..8a7a9d5 Binary files /dev/null and b/assets/client_icons/adium.png differ diff --git a/assets/client_icons/anotherim.png b/assets/client_icons/anotherim.png new file mode 100644 index 0000000..4bdc43b Binary files /dev/null and b/assets/client_icons/anotherim.png differ diff --git a/assets/client_icons/bitlbee.png b/assets/client_icons/bitlbee.png new file mode 100644 index 0000000..217c11f Binary files /dev/null and b/assets/client_icons/bitlbee.png differ diff --git a/assets/client_icons/blabber.png b/assets/client_icons/blabber.png new file mode 100644 index 0000000..d83cbd0 Binary files /dev/null and b/assets/client_icons/blabber.png differ diff --git a/assets/client_icons/bot.png b/assets/client_icons/bot.png new file mode 100644 index 0000000..3d371a8 Binary files /dev/null and b/assets/client_icons/bot.png differ diff --git a/assets/client_icons/cheogram.png b/assets/client_icons/cheogram.png new file mode 100644 index 0000000..f90c4f0 Binary files /dev/null and b/assets/client_icons/cheogram.png differ diff --git a/assets/client_icons/conv6ations.png b/assets/client_icons/conv6ations.png new file mode 100644 index 0000000..9d4ad0f Binary files /dev/null and b/assets/client_icons/conv6ations.png differ diff --git a/assets/client_icons/conversations.png b/assets/client_icons/conversations.png new file mode 100644 index 0000000..1ea9b86 Binary files /dev/null and b/assets/client_icons/conversations.png differ diff --git a/assets/client_icons/converse.png b/assets/client_icons/converse.png new file mode 100644 index 0000000..2dfda08 Binary files /dev/null and b/assets/client_icons/converse.png differ diff --git a/assets/client_icons/dino.png b/assets/client_icons/dino.png new file mode 100644 index 0000000..951feb5 Binary files /dev/null and b/assets/client_icons/dino.png differ diff --git a/assets/client_icons/gajim.png b/assets/client_icons/gajim.png new file mode 100644 index 0000000..bdfa888 Binary files /dev/null and b/assets/client_icons/gajim.png differ diff --git a/assets/client_icons/isida-bot.png b/assets/client_icons/isida-bot.png new file mode 100644 index 0000000..b99883d Binary files /dev/null and b/assets/client_icons/isida-bot.png differ diff --git a/assets/client_icons/jtalk.png b/assets/client_icons/jtalk.png new file mode 100644 index 0000000..71f90b9 Binary files /dev/null and b/assets/client_icons/jtalk.png differ diff --git a/assets/client_icons/kopete.png b/assets/client_icons/kopete.png new file mode 100644 index 0000000..bad87a0 Binary files /dev/null and b/assets/client_icons/kopete.png differ diff --git a/assets/client_icons/leechcraft-azoth.png b/assets/client_icons/leechcraft-azoth.png new file mode 100644 index 0000000..e360eb6 Binary files /dev/null and b/assets/client_icons/leechcraft-azoth.png differ diff --git a/assets/client_icons/mcabber.png b/assets/client_icons/mcabber.png new file mode 100644 index 0000000..b8a7fe5 Binary files /dev/null and b/assets/client_icons/mcabber.png differ diff --git a/assets/client_icons/miranda-ng.png b/assets/client_icons/miranda-ng.png new file mode 100644 index 0000000..00d172a Binary files /dev/null and b/assets/client_icons/miranda-ng.png differ diff --git a/assets/client_icons/miranda.png b/assets/client_icons/miranda.png new file mode 100644 index 0000000..7a8b6cb Binary files /dev/null and b/assets/client_icons/miranda.png differ diff --git a/assets/client_icons/monocles.png b/assets/client_icons/monocles.png new file mode 100644 index 0000000..6197498 Binary files /dev/null and b/assets/client_icons/monocles.png differ diff --git a/assets/client_icons/movim.png b/assets/client_icons/movim.png new file mode 100644 index 0000000..295d98f Binary files /dev/null and b/assets/client_icons/movim.png differ diff --git a/assets/client_icons/pidgin.png b/assets/client_icons/pidgin.png new file mode 100644 index 0000000..36dfca0 Binary files /dev/null and b/assets/client_icons/pidgin.png differ diff --git a/assets/client_icons/pix-art.png b/assets/client_icons/pix-art.png new file mode 100644 index 0000000..10d8b1d Binary files /dev/null and b/assets/client_icons/pix-art.png differ diff --git a/assets/client_icons/poezio.png b/assets/client_icons/poezio.png new file mode 100644 index 0000000..0c14311 Binary files /dev/null and b/assets/client_icons/poezio.png differ diff --git a/assets/client_icons/profanity.png b/assets/client_icons/profanity.png new file mode 100644 index 0000000..2022adb Binary files /dev/null and b/assets/client_icons/profanity.png differ diff --git a/assets/client_icons/psi.png b/assets/client_icons/psi.png new file mode 100644 index 0000000..a342d9c Binary files /dev/null and b/assets/client_icons/psi.png differ diff --git a/assets/client_icons/qip.png b/assets/client_icons/qip.png new file mode 100644 index 0000000..3dfca69 Binary files /dev/null and b/assets/client_icons/qip.png differ diff --git a/assets/client_icons/qutim.png b/assets/client_icons/qutim.png new file mode 100644 index 0000000..3f67b0c Binary files /dev/null and b/assets/client_icons/qutim.png differ diff --git a/assets/client_icons/spark.png b/assets/client_icons/spark.png new file mode 100644 index 0000000..300dc09 Binary files /dev/null and b/assets/client_icons/spark.png differ diff --git a/assets/client_icons/swift.png b/assets/client_icons/swift.png new file mode 100644 index 0000000..2438fd4 Binary files /dev/null and b/assets/client_icons/swift.png differ diff --git a/assets/client_icons/tkabber.png b/assets/client_icons/tkabber.png new file mode 100644 index 0000000..536781a Binary files /dev/null and b/assets/client_icons/tkabber.png differ diff --git a/assets/client_icons/vacuum.png b/assets/client_icons/vacuum.png new file mode 100644 index 0000000..b41efcc Binary files /dev/null and b/assets/client_icons/vacuum.png differ diff --git a/assets/client_icons/vk4xmpp.png b/assets/client_icons/vk4xmpp.png new file mode 100644 index 0000000..c32d1a0 Binary files /dev/null and b/assets/client_icons/vk4xmpp.png differ diff --git a/assets/client_icons/xabber.png b/assets/client_icons/xabber.png new file mode 100644 index 0000000..e479714 Binary files /dev/null and b/assets/client_icons/xabber.png differ diff --git a/assets/client_icons/yaxim.png b/assets/client_icons/yaxim.png new file mode 100644 index 0000000..4a48b86 Binary files /dev/null and b/assets/client_icons/yaxim.png differ diff --git a/debug_pac.png b/debug_pac.png new file mode 100644 index 0000000..51bcf0b Binary files /dev/null and b/debug_pac.png differ diff --git a/debug_psi.png b/debug_psi.png new file mode 100644 index 0000000..c101887 Binary files /dev/null and b/debug_psi.png differ diff --git a/debug.png b/debug_snit.png similarity index 100% rename from debug.png rename to debug_snit.png diff --git a/go-xmpp/stanza/caps.go b/go-xmpp/stanza/caps.go new file mode 100644 index 0000000..6f5ae8c --- /dev/null +++ b/go-xmpp/stanza/caps.go @@ -0,0 +1,26 @@ +package stanza + +import ( + "encoding/xml" +) + +// Capabilities +// Reference: https://xmpp.org/extensions/xep-0115.html#stream +// "A server MAY include its entity capabilities in a stream feature element so that connecting clients +// and peer servers do not need to send service discovery requests each time they connect." +// This is not a stream feature but a way to let client cache server disco info. + +// Moved from stream_features.go and made it a presence extension as per spec + +type Caps struct { + PresExtension + XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"` + Hash string `xml:"hash,attr"` + Node string `xml:"node,attr"` + Ver string `xml:"ver,attr"` + Ext string `xml:"ext,attr,omitempty"` +} + +func init() { + TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/caps", Local: "c"}, Caps{}) +} diff --git a/go-xmpp/stanza/stream_features.go b/go-xmpp/stanza/stream_features.go index d1b6274..6fb1195 100644 --- a/go-xmpp/stanza/stream_features.go +++ b/go-xmpp/stanza/stream_features.go @@ -42,19 +42,6 @@ func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamF return packet, err } -// Capabilities -// Reference: https://xmpp.org/extensions/xep-0115.html#stream -// "A server MAY include its entity capabilities in a stream feature element so that connecting clients -// and peer servers do not need to send service discovery requests each time they connect." -// This is not a stream feature but a way to let client cache server disco info. -type Caps struct { - XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"` - Hash string `xml:"hash,attr"` - Node string `xml:"node,attr"` - Ver string `xml:"ver,attr"` - Ext string `xml:"ext,attr,omitempty"` -} - // ============================================================================ // Supported Stream Features diff --git a/gtk-helpers.go b/gtk-helpers.go index 70b4a16..cd54f93 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -4,6 +4,11 @@ import ( "bytes" "context" "fmt" + "image" + "image/color" + "image/png" + "strconv" + "github.com/boxes-ltd/imaging" "github.com/crazy3lf/colorconv" "github.com/diamondburned/gotk4/pkg/gdk/v4" @@ -14,20 +19,45 @@ import ( "github.com/google/uuid" "github.com/rrivera/identicon" "gosrc.io/xmpp/stanza" - "image" - "image/color" - "image/png" xmpp_color "mellium.im/xmpp/color" - "strconv" ) -func init() { +func createTabBox(name, jid, av_hash string, removable bool) *gtk.Box { + box := gtk.NewBox(gtk.OrientationHorizontal, 10) + name_label := gtk.NewLabel(name) + left_click_gesture := gtk.NewGestureClick() + left_click_gesture.SetButton(1) + left_click_gesture.Connect("pressed", func() { + switchToTab(jid, &window.Window) + }) + box.AddController(left_click_gesture) + + if removable { + right_click_gesture := gtk.NewGestureClick() + right_click_gesture.SetButton(3) + right_click_gesture.Connect("pressed", func() { + removeTab(jid, &window.Window) + box.SetVisible(false) + }) + box.AddController(right_click_gesture) + } + + box.Append(name_label) + image := gtk.NewImage() + image.SetPixelSize(40) + go getAvatar(jid, av_hash, image) + box.Prepend(image) + + return box } func showUserWindow(u stanza.Presence, custom_nick string) { var ocu OccupantID u.Get(&ocu) + var caps stanza.Caps + u.Get(&caps) + var id string id = JidMustParse(u.From).Resource @@ -67,7 +97,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) { } if loadedConfig.BlockingOI[ocu.ID] { - blocked_note := gtk.NewLabel("You have blocked this user!") + blocked_note := gtk.NewLabel(loadedLocale["blockedUserNotice"]) blocked_note.AddCSSClass("blocked") profile_box.Prepend(blocked_note) } @@ -82,7 +112,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) { Type: "get", From: clientroot.Session.BindJid, To: u.From, - Id: uuid.New().String(), + Id: uuid.NewString(), Lang: "en", }) @@ -135,7 +165,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) { Type: "get", From: clientroot.Session.BindJid, To: u.From, - Id: uuid.New().String(), + Id: uuid.NewString(), Lang: "en", }) @@ -210,30 +240,43 @@ func showUserWindow(u stanza.Presence, custom_nick string) { mmmm, ok := mmm.Load(id) im := gtk.NewImage() + im.SetPixelSize(160) + im.AddCSSClass("author_img") + profile_box.Prepend(im) + createIdenticon(u.From, false, im) + if ok { pres := mmmm.(stanza.Presence) - var vu VCardUpdate pres.Get(&vu) if vu.Photo != "" { getAvatar(u.From, vu.Photo, im) - im.SetPixelSize(80) - im.AddCSSClass("author_img") - profile_box.Prepend(im) - } else { - createIdenticon(u.From, false, im) - im.SetPixelSize(80) - im.AddCSSClass("author_img") - profile_box.Prepend(im) } - } else { - createIdenticon(u.From, false, im) - im.SetPixelSize(80) - im.AddCSSClass("author_img") - profile_box.Prepend(im) } }() + if caps.Hash != "" { + cap_box := gtk.NewBox(gtk.OrientationHorizontal, 2) + cap_box.SetHAlign(gtk.AlignCenter) + icon := gtk.NewImageFromPaintable(clientAssetsLoad("cap")) + icon.SetCursor(gdk.NewCursorFromName("pointer", nil)) + + cap_box.Append(icon) + cap_box.Append(gtk.NewLabel(fmt.Sprintf("Entity capabilities: Node %s", caps.Node))) + hasher := gtk.NewLabel(fmt.Sprintf("Using %s hash: %s", caps.Hash, caps.Ver)) + hasher.SetVisible(false) + cap_box.Append(hasher) + + left_click_gesture := gtk.NewGestureClick() + left_click_gesture.SetButton(1) + left_click_gesture.Connect("pressed", func() { + hasher.SetVisible(!(hasher.Visible())) + }) + icon.AddController(left_click_gesture) + profile_box.Append(cap_box) + + } + win.SetChild(profile_box) win.SetTransientFor(win) win.Present() @@ -311,37 +354,30 @@ func switchToTab(jid string, w *gtk.Window) { typed_tab := tab.(*chatTab) scroller.SetChild(typed_tab.msgs) typingStatus.SetText("") + + createFailBox := func() { + box := gtk.NewBox(gtk.OrientationVertical, 10) + failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail")) + failed_icon.SetPixelSize(100) + box.Append(failed_icon) + jid_label := gtk.NewLabel(jid) + jid_label.AddCSSClass("author") + jid_label.AddCSSClass("jid") + box.Append(jid_label) + label := gtk.NewLabel(loadedLocale["mucLoadError"]) + label.SetWrap(true) + box.Append(label) + memberList.SetChild(box) + } 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) - jid_label := gtk.NewLabel(jid) - jid_label.AddCSSClass("author") - jid_label.AddCSSClass("jid") - box.Append(jid_label) - 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) + createFailBox() 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) - jid_label := gtk.NewLabel(jid) - jid_label.AddCSSClass("author") - jid_label.AddCSSClass("jid") - box.Append(jid_label) - 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) + createFailBox() return } mm := ma.Members @@ -370,11 +406,6 @@ func switchToTab(jid string, w *gtk.Window) { gen.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) } - //id := ocu.ID - //if id == "" { - // id := JidMustParse(u.From).Resource - //} - nick_label := gtk.NewLabel(JidMustParse(u.From).Resource) var custom_nick string @@ -403,7 +434,7 @@ func switchToTab(jid string, w *gtk.Window) { 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.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 @@ -418,7 +449,6 @@ func switchToTab(jid string, w *gtk.Window) { 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)) @@ -452,12 +482,26 @@ func switchToTab(jid string, w *gtk.Window) { userbox.Append(status_message) } + end_box := gtk.NewBox(gtk.OrientationHorizontal, 0) + + var caps stanza.Caps + ok = u.Get(&caps) + if ok { + client_icon := gtk.NewImageFromPaintable(clientAssetsLoad(caps.Node)) + //client_icon.SetHAlign(gtk.AlignEnd) + //client_icon.SetHExpand(true) + end_box.Append(client_icon) + } medal := gtk.NewImageFromPaintable(clientAssetsLoad(mu.MucUserItem.Affiliation)) medal.SetTooltipText(mu.MucUserItem.Affiliation) - medal.SetHAlign(gtk.AlignEnd) - medal.SetHExpand(true) - userbox.Append(medal) + //medal.SetHAlign(gtk.AlignEnd) + //medal.SetHExpand(true) + end_box.Append(medal) + end_box.SetHAlign(gtk.AlignEnd) + end_box.SetHExpand(true) + + userbox.Append(end_box) if loadedConfig.ShowAvatarsInMemberList { default_av := gtk.NewImage() @@ -479,6 +523,52 @@ func switchToTab(jid string, w *gtk.Window) { } } + userbox.SetHasTooltip(true) + userbox.Connect("query-tooltip", func( + x int, + y int, + keyboardMode bool, + tooltip *gtk.Tooltip, + ) bool { + box := gtk.NewBox(gtk.OrientationHorizontal, 4) + av_box := gtk.NewBox(gtk.OrientationHorizontal, 0) + if loadedConfig.ShowAvatarsInMemberList { + default_av := gtk.NewImage() + default_av.SetPixelSize(100) + createIdenticon(u.From, false, default_av) + av_box.Prepend(default_av) + var vcu VCardUpdate + ok = u.Get(&vcu) + if ok { + photo := vcu.Photo + go func() { + glib.IdleAdd(func() { + new_im := gtk.NewImage() + new_im.SetPixelSize(100) + av_box.Remove(default_av) + av_box.Append(new_im) + go getAvatar(u.From, photo, new_im) + }) + }() + + } + } + + box.Append(av_box) + + content_box := gtk.NewBox(gtk.OrientationVertical, 4) + content_box.Append(gtk.NewLabel(u.From)) + content_box.Append(gtk.NewLabel(mu.MucUserItem.Role)) + content_box.Append(gtk.NewLabel(mu.MucUserItem.Affiliation)) + content_box.Append(gtk.NewLabel(mu.MucUserItem.JID)) + + box.Append(content_box) + + tooltip.SetCustom(box) + + return true + }) + gesture := gtk.NewGestureClick() gesture.SetButton(1) @@ -498,7 +588,7 @@ func switchToTab(jid string, w *gtk.Window) { kb.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` @@ -506,7 +596,7 @@ func switchToTab(jid string, w *gtk.Window) { - `, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource)) + `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(u.From).Resource)) }) bb.ConnectClicked(func() { @@ -516,14 +606,14 @@ func switchToTab(jid string, w *gtk.Window) { if mu.MucUserItem.JID != "" { client.SendRaw(fmt.Sprintf(` - `, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare())) + `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(mu.MucUserItem.JID).Bare())) } } }) @@ -547,14 +637,14 @@ func switchToTab(jid string, w *gtk.Window) { submit.ConnectClicked(func() { client.SendRaw(fmt.Sprintf(` - `, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare())) + `, clientroot.Session.BindJid, uuid.NewString(), jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare())) win.Destroy() }) @@ -588,7 +678,7 @@ func switchToTab(jid string, w *gtk.Window) { client.SendRaw(fmt.Sprintf(` @@ -596,7 +686,7 @@ func switchToTab(jid string, w *gtk.Window) { - `, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text())) + `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(u.From).Resource, the_entry.Text())) }) box.Append(the_entry) @@ -642,7 +732,13 @@ func switchToTab(jid string, w *gtk.Window) { } else { headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("group"))) } - headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, loadedLocale["participants"]))) + + pword := loadedLocale["participants"] + if i == 1 { + pword = loadedLocale["participant"] + } + + headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, pword))) gen.Prepend(headerBox) muc_presence := typed_tab.muc_presence @@ -717,37 +813,29 @@ func createIdenticon(word string, always_create bool, i *gtk.Image) { // This fu loader.Close() p := loader.Pixbuf() gt := gdk.NewTextureForPixbuf(p) - /* - i := gtk.NewImageFromPaintable(gt) - i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") - return i - */ glib.IdleAdd(func() { i.SetFromPaintable(gt) - i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") }) } 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.SetTitle(loadedLocale["buildAJID"]) win.SetDefaultSize(400, 1) win.SetResizable(false) box := gtk.NewBox(gtk.OrientationVertical, 2) - header := gtk.NewLabel("Build-A-JID") + header := gtk.NewLabel(loadedLocale["buildAJID"]) header.AddCSSClass("author") box.Append(header) - box.Append(gtk.NewLabel("All fields except for domain are optional")) + box.Append(gtk.NewLabel(loadedLocale["buildAJIDOptionalFieldsNote"])) jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2) localPartEntry := gtk.NewEntry() - localPartEntry.SetPlaceholderText("localpart") + localPartEntry.SetPlaceholderText(loadedLocale["BAJlocalpart"]) jid_builder.Append(localPartEntry) at_sign := gtk.NewLabel("@") @@ -755,7 +843,7 @@ func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows th jid_builder.Append(at_sign) domainEntry := gtk.NewEntry() - domainEntry.SetPlaceholderText("domain") + domainEntry.SetPlaceholderText(loadedLocale["BAJdomain"]) jid_builder.Append(domainEntry) resource_sign := gtk.NewLabel("/") @@ -763,7 +851,7 @@ func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows th jid_builder.Append(resource_sign) resourceEntry := gtk.NewEntry() - resourceEntry.SetPlaceholderText("resource") + resourceEntry.SetPlaceholderText(loadedLocale["BAJresource"]) jid_builder.Append(resourceEntry) box.Append(jid_builder) diff --git a/gtk-message.go b/gtk-message.go index f3616f5..aa26fcd 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -81,12 +81,29 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi return gtk.NewLabel("Unsupported message."), true } - readmarker := Marker{} - ok = m.Get(&readmarker) - if ok { - b := gtk.NewBox(gtk.OrientationHorizontal, 0) - b.Append(gtk.NewLabel(fmt.Sprintf("%s%s", JidMustParse(m.From).Resource, loadedLocale["readWidget"]))) - return b, true + sid := StanzaID{} + m.Get(&sid) + + ocu := OccupantID{} + m.Get(&ocu) + + store_label := false + var msg *abMessage + tab, ok := tabs.Load(target) + typed_tab, ok2 := tab.(*chatTab) + if ok && ok2 { + store_label = true + msg, ok = typed_tab.msg_refsID[m.Id] + if !ok { + msg = new(abMessage) + typed_tab.msg_refsID[m.Id] = msg + } + + msg.ocu_id = ocu.ID + + if sid.ID != "" { + typed_tab.msg_refsOSID[sid.ID] = msg + } } composing := stanza.StateComposing{} @@ -108,19 +125,6 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi return error_box, true } - store_label := false - tab, ok := tabs.Load(target) - if ok { - store_label = true - } - typed_tab := tab.(*chatTab) - - sid := StanzaID{} - m.Get(&sid) - - ocu := OccupantID{} - - m.Get(&ocu) id := JidMustParse(m.From).Resource name := id @@ -179,7 +183,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi %s - `, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, v)) + `, m.To, jid.MustParse(m.From).Bare().String(), uuid.NewString(), m.Type, sid.ID, v)) }) reactions.Append(like) } @@ -198,7 +202,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi %s - `, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, custom.Text())) + `, m.To, jid.MustParse(m.From).Bare().String(), uuid.NewString(), m.Type, sid.ID, custom.Text())) }) rc_box.Append(custom) rc_box.Append(enter_custom) @@ -234,6 +238,26 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi rc_box.Append(vp) + live_edit := gtk.NewButtonWithLabel("Live edit") + live_edit.ConnectClicked(func() { + en := gtk.NewEntry() + en.ConnectChanged(func() { + client.SendRaw(fmt.Sprintf("%s", + uuid.NewString(), + target, + m.Type, + en.Text(), + m.Id, + )) + }) + + win := gtk.NewWindow() + win.SetChild(en) + win.SetVisible(true) + }) + + rc_box.Append(live_edit) + popover.SetChild(rc_box) rect := gdk.NewRectangle(x, y, 1, 1) popover.SetPointingTo(&rect) @@ -273,18 +297,8 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi // TODO: Decouple this from widget generation if store_label { - msg, ok := typed_tab.msg_refsID[m.Id] - if !ok { - msg = new(abMessage) - typed_tab.msg_refsID[m.Id] = msg - } - msg.label_ref = mlabel msg.ocu_id = ocu.ID - - if sid.ID != "" { - typed_tab.msg_refsOSID[sid.ID] = msg - } } } else { authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10) @@ -312,10 +326,11 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi } im := gtk.NewImage() - im.SetFromPaintable(clientAssetsLoad("DefaultAvatar")) + // im := gtk.NewImageFromPaintable(clientAssetsLoad("DefaultAvatar")) + go createIdenticon(m.From, false, im) + // im.SetFromPaintable(clientAssetsLoad("DefaultAvatar")) im.SetPixelSize(40) im.AddCSSClass("author_img") - // createIdenticon(m.From, false, im) authorBox.Append(im) if can_generate { pres := mmmm.(stanza.Presence) @@ -325,14 +340,38 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi go getAvatar(m.From, vu.Photo, im) } } else { - createIdenticon(m.From, false, im) + go createIdenticon(m.From, false, im) } } else if m.Type == stanza.MessageTypeChat { al.SetText(al.Text() + loadedLocale["whispers"]) } + color_ind := gtk.NewLabel("") + + ycbcr := xmpp_color.String(id, 255, loadedConfig.CVD) + r, g, b, _ := ycbcr.RGBA() + hexcolor := fmt.Sprintf("#%02x%02x%02x", r, g, b) + + color_ind.SetMarkup(fmt.Sprintf("|", hexcolor)) + + authorBox.Append(color_ind) authorBox.Append(al) + oob := stanza.OOB{} + ok = m.Get(&oob) + if ok { + // media := newPictureFromWeb(oob.URL) + // media.SetCanShrink(false) + // mainBox.Append(media) + // media.AddCSSClass("chat_image") + mbtn := gtk.NewButtonWithLabel("🖼️") + // mbtn.SetChild(newImageFromWeb(oob.URL)) + mbtn.ConnectClicked(func() { + gopen.Open(oob.URL) + }) + authorBox.Append(mbtn) + } + if m.Thread != "" { im := gtk.NewImage() createIdenticon(m.Thread, true, im) @@ -346,6 +385,33 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi authorBox.Append(gtk.NewLabel("🎮")) } + stamp_label := gtk.NewLabel("") + stamp_label.SetHExpand(true) + stamp_label.SetHAlign(gtk.AlignEnd) + stamp_label.AddCSSClass("visitor") + + stamp_label.SetText(time.Now().Format("03:04 PM")) + stamp_label.SetTooltipText(time.Now().Format("2006-01-02 15:04:05")) + + stamp := new(Delayed_Stamp) + ok = m.Get(stamp) + + if ok { + stamped_time, err := stamp.GetStamp() + if err == nil { + stamped_time := *stamped_time + loc := time.Now().Location() + + stamped_time = stamped_time.In(loc) + str := stamped_time.Format("03:04 PM") + str_full := stamped_time.Format("2006-01-02 15:04:05") + stamp_label.SetText(str) + stamp_label.SetTooltipText(str_full) + } + } + + authorBox.Append(stamp_label) + body := m.Body body = XEPToPango(body, false) @@ -361,40 +427,15 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi // TODO: Decouple this from widget generation if store_label { - msg, ok := typed_tab.msg_refsID[m.Id] - if !ok { - msg = new(abMessage) - typed_tab.msg_refsID[m.Id] = msg - } - msg.label_ref = mlabel - msg.ocu_id = ocu.ID - - if sid.ID != "" { - typed_tab.msg_refsOSID[sid.ID] = msg - } + msg.read_indicator = gtk.NewBox(gtk.OrientationHorizontal, padding) + authorBox.Append(msg.read_indicator) } contentBox.Append(mlabel) - mainBox.Append(authorBox) mainBox.Append(contentBox) - oob := stanza.OOB{} - ok = m.Get(&oob) - if ok { - // media := newPictureFromWeb(oob.URL) - // media.SetCanShrink(false) - // mainBox.Append(media) - // media.AddCSSClass("chat_image") - mbtn := gtk.NewButtonWithLabel("🖼️") - // mbtn.SetChild(newImageFromWeb(oob.URL)) - mbtn.ConnectClicked(func() { - gopen.Open(oob.URL) - }) - authorBox.Append(mbtn) - } - if m.Subject != "" { subjectlabel := gtk.NewLabel(m.Subject) subjectlabel.SetWrap(true) @@ -460,6 +501,8 @@ func getVAdjustment(scrolledWindow *gtk.ScrolledWindow) *gtk.Adjustment { } func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably shouldn't be here, and should probably be in xmpp-helpers or somewhere similar. + time.Sleep(200 * time.Millisecond) + hash = strings.ReplaceAll(hash, "/", "-") oghash := hash p, err := ensureCache() if err != nil { @@ -492,7 +535,7 @@ func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably s Type: "get", From: clientroot.Session.BindJid, To: j, - Id: uuid.New().String(), + Id: uuid.NewString(), Lang: "en", }) diff --git a/gtk-xmpp-markup.go b/gtk-xmpp-markup.go index 29087c3..c176714 100644 --- a/gtk-xmpp-markup.go +++ b/gtk-xmpp-markup.go @@ -2,10 +2,10 @@ package main import ( "fmt" + "github.com/diamondburned/gotk4/pkg/gtk/v4" "html" "regexp" "strings" - "github.com/diamondburned/gotk4/pkg/gtk/v4" ) var ( @@ -65,6 +65,9 @@ func XEPToPango(text string, correction bool) string { } func GenerateRetraction(lab *gtk.Label, retraction Retraction) { + if lab == nil { + return + } if retraction.Moderated.OccupantID.ID != "" { lab.AddCSSClass("moderated") lab.SetTooltipMarkup(fmt.Sprintf("This message was retracted by a moderator.\nReason: '%s'", html.EscapeString(retraction.Reason))) diff --git a/helpers.go b/helpers.go index ea5016f..1a05ff3 100644 --- a/helpers.go +++ b/helpers.go @@ -2,6 +2,8 @@ package main import ( + "errors" + "gosrc.io/xmpp/stanza" "sort" "sync" ) @@ -23,3 +25,28 @@ func rangeOrdered(m *sync.Map, fn func(k, v any) bool) { } } } + +func getPresenceFromJIDMuc(jid *stanza.Jid) (*stanza.Presence, error) { + mo, ok := mucmembers.Load(jid.Bare()) + if !ok { + return nil, errors.New("tab is not present in mucmembers") + } + + mm, ok := mo.(mucUnit) + if !ok { + return nil, errors.New("mucUnit is nil") + } + + mmm := mm.Members + mmmm, ok := mmm.Load(jid.Resource) + if !ok { + return nil, errors.New("user not in room") + } + + pres, ok := mmmm.(stanza.Presence) + if !ok { + return nil, errors.New("presence is nil") + } + + return &pres, nil +} diff --git a/l18n.go b/l10n.go similarity index 81% rename from l18n.go rename to l10n.go index a004ca4..05685d2 100644 --- a/l18n.go +++ b/l10n.go @@ -76,24 +76,32 @@ var enGB = map[string]string{ // British English // gtk-helpers.go - "getPastMessages": "Get past messages...", - "clickForMoreInfo": "Click for more information", - "ban": "Ban", - "kick": "Kick", - "setAffil": "Set affiliation", - "setAffilDescPartOne": "Set ", - "setAffilDescPartTwo": "'s affiliation", - "setRole": "Set role", - "setRoleDescPartOne": "Set ", - "setRoleDescPartTwo": "'s role", - "setRoleWarning": "Important: if you want this to be permanent, set their affiliation instead", - "gettingVersion": "Getting version...", - "connectedWithRole": "Connected with role ", - "affiliatedAs": "Affiliated as ", - "unaffiliated": "Unaffiliated", - "participants": "participant(s)", - "versionQueryEmpty": "You do not have permission to view this client's version", - "versionQueryError": "Got error trying to get version", + "getPastMessages": "Get past messages...", + "mucLoadError": "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.", + "clickForMoreInfo": "Click for more information", + "ban": "Ban", + "kick": "Kick", + "setAffil": "Set affiliation", + "setAffilDescPartOne": "Set ", + "setAffilDescPartTwo": "'s affiliation", + "setRole": "Set role", + "setRoleDescPartOne": "Set ", + "setRoleDescPartTwo": "'s role", + "setRoleWarning": "Important: if you want this to be permanent, set their affiliation instead", + "gettingVersion": "Getting version...", + "connectedWithRole": "Connected with role ", + "affiliatedAs": "Affiliated as ", + "unaffiliated": "Unaffiliated", + "participants": "participants", + "participant": "participant", + "versionQueryEmpty": "You do not have permission to view this client's version", + "versionQueryError": "Got error trying to get version", + "blockedUserNotice": "You have blocked this user!", + "buildAJID": "Build-A-JID", + "buildAJIDOptionalFieldsNote": "All fields except for domain are optional", + "BAJlocalpart": "localpart", + "BAJdomain": "domain", + "BAJresource": "resource", "away": "Away", "chat": "Free for chat", diff --git a/main.go b/main.go index 812f833..704e776 100644 --- a/main.go +++ b/main.go @@ -49,11 +49,6 @@ var mIcon *gtk.Image var gStatus *gtk.Label var gIcon *gtk.Image -/* -var sStatus *gtk.Label -var sIcon *gtk.Image -*/ - var typingStatus *gtk.Label var pingStatus *gtk.Label @@ -93,9 +88,6 @@ func init() { } func main() { - go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() pingTimes = append(pingTimes, []float64{}) p, err := ensureConfig() @@ -113,6 +105,12 @@ func main() { if err != nil { panic(err) } + if loadedConfig.Debug { + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + log.Println("pprof debug server running at port 6060") + } if loadedConfig.Resource == "" { fmt.Println(loadedLocale["configResourceEmptyWarning"]) @@ -257,6 +255,31 @@ func main() { return } + originator := JidMustParse(m.From).Bare() + var typed_tab *chatTab + tab, ok := tabs.Load(originator) + if ok { + typed_tab = tab.(*chatTab) + } else { + + if m.Type != stanza.MessageTypeGroupchat { + createTab(originator, false, originator) + tab, ok = tabs.Load(originator) + if ok { + typed_tab = tab.(*chatTab) + glib.IdleAdd(func() { + box := createTabBox(originator, originator, originator, true) + menu.Append(box) + menu.Append(gtk.NewSeparator(gtk.OrientationVertical)) + }) + } else { + return + } + } else { + return + } + } + oi := new(OccupantID) ok = m.Get(oi) if ok { @@ -276,12 +299,6 @@ func main() { sid := new(StanzaID) m.Get(sid) - originator := JidMustParse(m.From).Bare() - - glib.IdleAdd(func() { - mStatus.SetText(originator) - }) - at := new(Attention) ok = m.Get(at) if ok { @@ -340,6 +357,41 @@ func main() { return } + // Handle read indicators + marker := new(Marker) + ok = m.Get(marker) + if ok { + _, ok := typed_tab.msg_refsOSID[marker.ID] + if ok { + im := gtk.NewImage() + im.SetPixelSize(20) + im.SetTooltipText(JidMustParse(m.From).Resource) + glib.IdleAdd(func() { + createIdenticon(m.From, false, im) + }) + + pres, err := getPresenceFromJIDMuc(JidMustParse(m.From)) + if err != nil { + return + } + + var vu VCardUpdate + ok = pres.Get(&vu) + if ok { + go getAvatar(m.From, vu.Photo, im) + } + + ind, ok := typed_tab.msg_refsOSID[marker.ID] + if ok { + if ind.read_indicator != nil { + ind.read_indicator.Append(im) + } + } + return + } + return + } + // Handle corrections BEFORE attempting to generate a message correction := new(Correction) ok = m.Get(correction) @@ -352,9 +404,11 @@ func main() { if ok { if msg.ocu_id == oi.ID && msg.ocu_id != "" { lab := msg.label_ref - glib.IdleAdd(func() { - lab.SetMarkup(XEPToPango(m.Body, true)) - }) + if lab != nil { + glib.IdleAdd(func() { + lab.SetMarkup(XEPToPango(m.Body, true)) + }) + } } return } @@ -370,15 +424,15 @@ func main() { if ok { typed_tab := tab.(*chatTab) /* - to_retract := "" - refs := typed_tab.msg_refsID + to_retract := "" + refs := typed_tab.msg_refsID - // If it is a DM, then we use regular message ID. If it is a MUC, we use stanza id - to_retract = sid.ID - if typed_tab.isMuc && sid != nil { + // If it is a DM, then we use regular message ID. If it is a MUC, we use stanza id to_retract = sid.ID - refs = typed_tab.msg_refsOSID - } + if typed_tab.isMuc && sid != nil { + to_retract = sid.ID + refs = typed_tab.msg_refsOSID + } */ msg, ok := typed_tab.msg_refsOSID[retraction.ID] @@ -387,7 +441,8 @@ func main() { glib.IdleAdd(func() { GenerateRetraction(lab, *retraction) }) - } + return + } } } @@ -399,7 +454,10 @@ func main() { return } - typed_tab := tab.(*chatTab) + typed_tab, ok := tab.(*chatTab) + if !ok { + return + } if ok { if current == JidMustParse(m.From).Bare() { @@ -413,6 +471,7 @@ func main() { ba, converted_successfully := untyped_box.(*gtk.Box) if valid && converted_successfully { + mStatus.SetText(originator) typed_tab.msgs.Append(b) b.Append(ba) b.Append(gtk.NewSeparator(gtk.OrientationVertical)) @@ -439,10 +498,7 @@ func main() { if ok { // This is a presence stanza from a user in a MUC presence.Get(&ocu) - // id := ocu.ID - // if id == "" { id := JidMustParse(presence.From).Resource - // } from, _ := stanza.NewJid(presence.From) muc := from.Bare() _, ok = mucmembers.Load(muc) @@ -567,7 +623,7 @@ func main() { pingStatus.AddCSSClass("pending") before := time.Now() iq := new(stanza.IQ) - iq.Id = uuid.New().String() + iq.Id = uuid.NewString() iq.From = clientroot.Session.BindJid iq.Type = "get" iq.Payload = &Ping{} @@ -621,22 +677,8 @@ func main() { name := "Note To Self" createTab(jid, false, name) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(name) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - box.Append(b) - im := gtk.NewImage() - im.SetPixelSize(40) - go getAvatar(jid, jid, im) - box.Prepend(im) - - box.AddController(gesture1) - menu.Append(box) + b := createTabBox(name, jid, jid, false) + menu.Append(b) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -666,30 +708,7 @@ func main() { createTab(jid, false, name) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(name) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(jid, &window.Window) - box.SetVisible(false) - }) - - im := gtk.NewImage() - im.SetPixelSize(40) - - box.Append(im) - box.Append(b) - - // TODO: Use PEP avatar and do not use JID as hash - go getAvatar(jid, jid, im) - - box.AddController(gesture1) + box := createTabBox(name, jid, jid, false) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -754,31 +773,7 @@ func main() { joinMuc(client, clientroot.Session.BindJid, jid, nick, password) createTab(jid, true, name) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(name) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(jid, &window.Window) - box.SetVisible(false) - }) - - im := gtk.NewImage() - im.SetPixelSize(40) - - box.Append(b) - box.Prepend(im) - - go getAvatar(jid, jid, im) - - box.AddController(gesture1) - box.AddController(gesture2) + box := createTabBox(name, jid, jid, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -795,31 +790,7 @@ func main() { joinMuc(client, clientroot.Session.BindJid, jid, nick, "") createTab(jid, true, jid) glib.IdleAdd(func() { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - b := gtk.NewLabel(jid) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(jid, &window.Window) - box.SetVisible(false) - }) - - im := gtk.NewImage() - im.SetPixelSize(40) - - box.Append(b) - box.Prepend(im) - - go getAvatar(jid, jid, im) - - box.AddController(gesture1) - box.AddController(gesture2) + box := createTabBox(jid, jid, jid, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) }) @@ -891,26 +862,8 @@ func activate(app *gtk.Application) { submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) submit.ConnectClicked(func() { t := jid_entry.Text() - n := t - createTab(t, false, n) - box := gtk.NewBox(gtk.OrientationHorizontal, 0) - b := gtk.NewLabel(n) - box.Append(b) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(t, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(t, &window.Window) - box.SetVisible(false) - }) - - box.AddController(gesture1) - box.AddController(gesture2) + createTab(t, false, t) + box := createTabBox(t, t, t, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) dm_window.SetVisible(false) @@ -1007,19 +960,14 @@ func activate(app *gtk.Application) { if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() { if mu.MucUserItem.Affiliation != "owner" { box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"])) + return false } } } - } else { - panic("not ok") } - } else { - panic("not ok") } return true }) - } else { - panic("not ok") } win.SetChild(box) @@ -1101,31 +1049,7 @@ func activate(app *gtk.Application) { } createTab(t, true, n) - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - im := gtk.NewImage() - im.SetPixelSize(40) - - go getAvatar(t, t, im) - - b := gtk.NewLabel(n) - - box.Append(im) - box.Append(b) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(t, &window.Window) - }) - - gesture2 := gtk.NewGestureClick() - gesture2.SetButton(3) - gesture2.Connect("pressed", func() { - removeTab(t, &window.Window) - box.SetVisible(false) - }) - - box.AddController(gesture1) - box.AddController(gesture2) + box := createTabBox(n, t, t, true) menu.Append(box) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) @@ -1137,7 +1061,6 @@ func activate(app *gtk.Application) { } } if !ok { - if !disco_check.Active() { jm(t, "") win.SetVisible(false) @@ -1152,7 +1075,7 @@ func activate(app *gtk.Application) { Type: "get", From: clientroot.Session.BindJid, To: t, - Id: "dicks", + Id: uuid.NewString(), Lang: "en", }) @@ -1163,122 +1086,128 @@ func activate(app *gtk.Application) { myIQ.Payload = &stanza.DiscoInfo{} ctx := context.Background() - mychan, err := client.SendIQ(ctx, myIQ) - if err == nil { - result := <-mychan - res, ok = result.Payload.(*stanza.DiscoInfo) - if ok { - if len(res.Identity) >= 1 { - features := res.Features - allowed = false - password_protected := false - password := "" - warning_win := gtk.NewWindow() - warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name)) - warning_win.SetDefaultSize(400, 1) - warning_win.SetResizable(false) - buttons := gtk.NewBox(gtk.OrientationHorizontal, 10) + var mychan chan stanza.IQ + //go func() { + mychan, err = client.SendIQ(ctx, myIQ) + if err != nil { + showErrorDialog(err, &window.Window) + } + //}() - join_button := gtk.NewButtonWithLabel("Join") - join_button.ConnectClicked(func() { - warning_win.SetVisible(false) - if password_protected { - allowed = false + result := <-mychan + res, ok = result.Payload.(*stanza.DiscoInfo) + if ok { + if len(res.Identity) >= 1 { + features := res.Features + allowed = false + password_protected := false + password := "" + warning_win := gtk.NewWindow() + warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name)) + warning_win.SetDefaultSize(400, 1) + warning_win.SetResizable(false) - password_win := gtk.NewWindow() - password_win.SetTitle(loadedLocale["joinPasswordRequired"]) - password_win.SetDefaultSize(400, 1) - password_win.SetResizable(false) - box := gtk.NewBox(gtk.OrientationVertical, 10) - en := gtk.NewPasswordEntry() - submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) - submit.ConnectClicked(func() { - password = en.Text() - jm(res.Identity[0].Name, password) - password_win.SetVisible(false) - }) - box.Append(en) - box.Append(submit) - password_win.SetChild(box) - password_win.SetVisible(true) - } + buttons := gtk.NewBox(gtk.OrientationHorizontal, 10) - jm(res.Identity[0].Name, password) - }) + join_button := gtk.NewButtonWithLabel("Join") + join_button.ConnectClicked(func() { + warning_win.SetVisible(false) + if password_protected { + allowed = false - cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"]) - cancel_button.ConnectClicked(func() { - warning_win.SetVisible(false) - }) - - join_button.SetHExpand(true) - cancel_button.SetHExpand(true) - - buttons.Append(join_button) - buttons.Append(cancel_button) - warning_box := gtk.NewBox(gtk.OrientationVertical, 10) - header := gtk.NewLabel(res.Identity[0].Name) - warning_box.Append(header) - addFeature := func(icon string, description string) { - box := gtk.NewBox(gtk.OrientationHorizontal, 10) - box.Append(gtk.NewImageFromPaintable(clientAssetsLoad(icon))) - box.Append(gtk.NewLabel(description)) - warning_box.Append(box) + password_win := gtk.NewWindow() + password_win.SetTitle(loadedLocale["joinPasswordRequired"]) + password_win.SetDefaultSize(400, 1) + password_win.SetResizable(false) + box := gtk.NewBox(gtk.OrientationVertical, 10) + en := gtk.NewPasswordEntry() + submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) + submit.ConnectClicked(func() { + password = en.Text() + jm(res.Identity[0].Name, password) + password_win.SetVisible(false) + }) + box.Append(en) + box.Append(submit) + password_win.SetChild(box) + password_win.SetVisible(true) } - for _, feature := range features { - switch feature.Var { - case "muc_passwordprotected": - password_protected = true - addFeature("muc_passwordprotected", loadedLocale["muc_passwordprotected_description"]) - case "muc_unsecured": - addFeature("muc_unsecured", loadedLocale["muc_unsecured_description"]) - case "muc_membersonly": - addFeature("muc_membersonly", loadedLocale["muc_membersonly_description"]) - case "muc_open": - addFeature("muc_open", loadedLocale["muc_open_description"]) - case "muc_moderated": - addFeature("muc_moderated", loadedLocale["muc_moderated_description"]) - case "muc_unmoderated": - addFeature("muc_unmoderated", loadedLocale["muc_unmoderated_description"]) - case "muc_nonanonymous": - addFeature("muc_nonanonymous", loadedLocale["muc_nonanonymous_description"]) - case "muc_semianonymous": - addFeature("muc_semianonymous", loadedLocale["muc_semianonymous_description"]) - case "muc_persistent": - addFeature("muc_persistent", loadedLocale["muc_persistent_description"]) - case "muc_temporary": - addFeature("muc_temporary", loadedLocale["muc_temporary_description"]) - case "muc_public": - addFeature("muc_public", loadedLocale["muc_public_description"]) - case "muc_hidden": - addFeature("muc_hidden", loadedLocale["muc_hidden_description"]) - case "urn:xmpp:mam:0": - addFeature("ok", loadedLocale["urn:xmpp:mam_description"]) - case "urn:xmpp:message-moderate:0": - addFeature("moderate", loadedLocale["urn:xmpp:message-moderate_description"]) - /* - default: - addFeature("comment", feature.Var) - */ - } - } + jm(res.Identity[0].Name, password) + }) - warning_box.Append(buttons) - warning_win.SetChild(warning_box) - warning_win.Present() - } else { - allowed = false - showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) + cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"]) + cancel_button.ConnectClicked(func() { + warning_win.SetVisible(false) + }) + + join_button.SetHExpand(true) + cancel_button.SetHExpand(true) + + buttons.Append(join_button) + buttons.Append(cancel_button) + warning_box := gtk.NewBox(gtk.OrientationVertical, 10) + header := gtk.NewLabel(res.Identity[0].Name) + warning_box.Append(header) + addFeature := func(icon string, description string) { + box := gtk.NewBox(gtk.OrientationHorizontal, 10) + box.Append(gtk.NewImageFromPaintable(clientAssetsLoad(icon))) + box.Append(gtk.NewLabel(description)) + warning_box.Append(box) } + + for _, feature := range features { + switch feature.Var { + case "muc_passwordprotected": + password_protected = true + addFeature("muc_passwordprotected", loadedLocale["muc_passwordprotected_description"]) + case "muc_unsecured": + addFeature("muc_unsecured", loadedLocale["muc_unsecured_description"]) + case "muc_membersonly": + addFeature("muc_membersonly", loadedLocale["muc_membersonly_description"]) + case "muc_open": + addFeature("muc_open", loadedLocale["muc_open_description"]) + case "muc_moderated": + addFeature("muc_moderated", loadedLocale["muc_moderated_description"]) + case "muc_unmoderated": + addFeature("muc_unmoderated", loadedLocale["muc_unmoderated_description"]) + case "muc_nonanonymous": + addFeature("muc_nonanonymous", loadedLocale["muc_nonanonymous_description"]) + case "muc_semianonymous": + addFeature("muc_semianonymous", loadedLocale["muc_semianonymous_description"]) + case "muc_persistent": + addFeature("muc_persistent", loadedLocale["muc_persistent_description"]) + case "muc_temporary": + addFeature("muc_temporary", loadedLocale["muc_temporary_description"]) + case "muc_public": + addFeature("muc_public", loadedLocale["muc_public_description"]) + case "muc_hidden": + addFeature("muc_hidden", loadedLocale["muc_hidden_description"]) + case "urn:xmpp:mam:0": + addFeature("ok", loadedLocale["urn:xmpp:mam_description"]) + case "urn:xmpp:message-moderate:0": + addFeature("moderate", loadedLocale["urn:xmpp:message-moderate_description"]) + /* + default: + addFeature("comment", feature.Var) + */ + } + } + + warning_box.Append(buttons) + warning_win.SetChild(warning_box) + warning_win.Present() } else { allowed = false - if result.Error != nil { - showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text), win) - } else { - showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) - } + showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) + } + } else { + allowed = false + if result.Error != nil { + showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text), win) + } else { + showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) } } diff --git a/style.css b/style.css index 60adecc..fd09de1 100644 --- a/style.css +++ b/style.css @@ -72,6 +72,9 @@ background-color: red; } + +// a11y + .RedGreen_CVD { filter: hue-rotate(30deg) saturate(120%) contrast(110%); } diff --git a/types.go b/types.go index 501929b..16609a1 100644 --- a/types.go +++ b/types.go @@ -8,8 +8,9 @@ import ( ) type abMessage struct { - label_ref *gtk.Label - ocu_id string + label_ref *gtk.Label + ocu_id string + read_indicator *gtk.Box } type chatTab struct { diff --git a/xmpp-delay.go b/xmpp-delay.go new file mode 100644 index 0000000..56e3e7c --- /dev/null +++ b/xmpp-delay.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/xml" + "gosrc.io/xmpp/stanza" + "time" +) + +// Implementation of XEP-0203: Delayed Delivery +// https://xmpp.org/extensions/xep-0203.html + +type Delayed_Stamp struct { + XMLName xml.Name `xml:"urn:xmpp:delay delay"` + From string `xml:"from,attr"` + Stamp string `xml:"stamp,attr"` +} + +func (d *Delayed_Stamp) GetStamp() (*time.Time, error) { + stamp := d.Stamp + time, err := time.Parse(time.RFC3339, stamp) + if err != nil { + return nil, err + } + + return &time, nil +} + +func init() { + stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:delay", Local: "delay"}, Delayed_Stamp{}) +} diff --git a/xmpp-retractions.go b/xmpp-retractions.go index 97ee754..430d4f7 100644 --- a/xmpp-retractions.go +++ b/xmpp-retractions.go @@ -5,21 +5,20 @@ import ( "gosrc.io/xmpp/stanza" ) -// Implementation of XEP-0424: Message Retraction as well as XEP-0425: Moderated Message Retraction +// Implementation of XEP-0424: Message Retraction as well as XEP-0425: Moderated Message Retraction type Retraction struct { stanza.MsgExtension - XMLName xml.Name `xml:"urn:xmpp:message-retract:1 retract"` - ID string `xml:"id,attr"` // Stanza ID is used in group chats. - Reason string `xml:"reason"` + XMLName xml.Name `xml:"urn:xmpp:message-retract:1 retract"` + ID string `xml:"id,attr"` // Stanza ID is used in group chats. + Reason string `xml:"reason"` Moderated Moderated `xml:"urn:xmpp:message-moderate:1 moderated"` } type Moderated struct { - XMLName xml.Name `xml:"urn:xmpp:message-moderate:1 moderated"` - By string `xml:"by,attr"` + XMLName xml.Name `xml:"urn:xmpp:message-moderate:1 moderated"` + By string `xml:"by,attr"` OccupantID OccupantID - } // To whoever thought putting the same element twice in the same message, but in different parents, and not specifying this behaviour anywhere in the XEP was a good idea, I want to kill you. Seriously, I do. If I see you walking home at night I will kill you. Brutally. Slowly. I will relish in the act. I will do it.