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.