diff --git a/assets.go b/assets.go
index d48009d..ea162f1 100644
--- a/assets.go
+++ b/assets.go
@@ -52,6 +52,9 @@ var tagBytes []byte
//go:embed assets/lambda-disabled.png
var logoDisabledBytes []byte
+//go:embed assets/icon.png
+var logoBytes []byte
+
//go:embed assets/group.png
var groupBytes []byte
@@ -181,6 +184,7 @@ func clientAssetsLoad(key string) gdk.Paintabler {
func init() {
for key, data := range map[string][]byte{
+ "logo": logoBytes,
"DefaultAvatar": defaultAvatarBytes,
"FailedAvatar": failedBytes,
"owner": ownerMedalBytes,
diff --git a/gtk-helpers.go b/gtk-helpers.go
index c51d438..70b4a16 100644
--- a/gtk-helpers.go
+++ b/gtk-helpers.go
@@ -265,6 +265,7 @@ func createTab(jid string, isMuc bool, name string) bool {
})
newTab.name = name
newTab.msg_refsID = make(map[string]*abMessage)
+ newTab.msg_refsOSID = make(map[string]*abMessage)
tabs.Store(jid, newTab)
return true
}
diff --git a/gtk-message.go b/gtk-message.go
index 7191201..f3616f5 100644
--- a/gtk-message.go
+++ b/gtk-message.go
@@ -18,6 +18,7 @@ import (
"path/filepath"
"runtime"
"strings"
+ "time"
)
func generatePresenceWidget(p stanza.Packet) gtk.Widgetter {
@@ -247,10 +248,6 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
if loadedConfig.CompactMode {
if m.Body == "" {
- /*
- mlabel.SetText(loadedLocale["noBodySet"])
- mlabel.AddCSSClass("visitor")
- */
return nil, false
}
al := gtk.NewLabel(name)
@@ -274,9 +271,20 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
// mlabel.SetHAlign(gtk.AlignFill)
mainBox.Append(mlabel)
+ // TODO: Decouple this from widget generation
if store_label {
- typed_tab.msg_refsID[m.Id].label_ref = mlabel
- tabs.Store(target, tab)
+ 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)
@@ -318,11 +326,6 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
}
} else {
createIdenticon(m.From, false, im)
- /*
- im.SetPixelSize(40)
- im.AddCSSClass("author_img")
- authorBox.Append(im)
- */
}
} else if m.Type == stanza.MessageTypeChat {
al.SetText(al.Text() + loadedLocale["whispers"])
@@ -348,10 +351,6 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
mlabel := gtk.NewLabel("")
if m.Body == "" {
- /*
- mlabel.SetText(loadedLocale["noBodySet"])
- mlabel.AddCSSClass("visitor")
- */
return nil, false
}
@@ -360,6 +359,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
mlabel.SetSelectable(true)
mlabel.SetHAlign(gtk.AlignFill)
+ // TODO: Decouple this from widget generation
if store_label {
msg, ok := typed_tab.msg_refsID[m.Id]
if !ok {
@@ -369,8 +369,12 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
msg.label_ref = mlabel
msg.ocu_id = ocu.ID
- tabs.Store(target, typed_tab)
+
+ if sid.ID != "" {
+ typed_tab.msg_refsOSID[sid.ID] = msg
+ }
}
+
contentBox.Append(mlabel)
mainBox.Append(authorBox)
@@ -478,14 +482,6 @@ func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably s
_, err = os.ReadFile(hash)
if err == nil {
- /*
- i, err = newImageFromPath(hash)
- if err != nil {
- invalidImages.Store(oghash, true)
- createIdenticon(j, false, i)
- }
- */
-
glib.IdleAdd(func() {
setImageFromPath(hash, i)
})
@@ -506,12 +502,30 @@ func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably s
iqResp.Payload = &VCard{}
- ctx := context.TODO()
- mychan, err := client.SendIQ(ctx, iqResp)
+ ctx := context.Background()
+ timeoutctx, cancel := context.WithTimeout(ctx, time.Second*30)
+ defer cancel()
+
+ mychan, err := client.SendIQ(timeoutctx, iqResp)
if err != nil {
- panic(err)
+ createIdenticon(j, false, i)
+ return
+ // panic(err)
}
- result := <-mychan
+
+ var result stanza.IQ
+
+ select {
+ case <-timeoutctx.Done():
+ createIdenticon(j, false, i)
+ return
+ case result = <-mychan:
+ if result.Error != nil {
+ createIdenticon(j, false, i)
+ return
+ }
+ }
+
card, ok := result.Payload.(*VCard)
if !ok {
createIdenticon(j, false, i)
diff --git a/gtk-xmpp-markup.go b/gtk-xmpp-markup.go
index 9708a14..29087c3 100644
--- a/gtk-xmpp-markup.go
+++ b/gtk-xmpp-markup.go
@@ -5,6 +5,7 @@ import (
"html"
"regexp"
"strings"
+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
)
var (
@@ -62,3 +63,13 @@ func XEPToPango(text string, correction bool) string {
}
return str
}
+
+func GenerateRetraction(lab *gtk.Label, retraction Retraction) {
+ 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)))
+ } else {
+ lab.AddCSSClass("retracted")
+ lab.SetTooltipMarkup(fmt.Sprintf("This message was retracted.\nReason: '%s'", html.EscapeString(retraction.Reason)))
+ }
+}
diff --git a/main.go b/main.go
index 4483577..812f833 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,10 @@
package main
import (
+ "log"
+ "net/http"
+ _ "net/http/pprof"
+
"os"
"strings"
"sync"
@@ -69,8 +73,6 @@ var styleCSS string
var client xmpp.Sender
var clientroot *xmpp.Client
-var uiQueue = make(chan func(), 100)
-
var window *gtk.ApplicationWindow
// stores members of mucs
@@ -88,20 +90,13 @@ var Roster = make(map[string]bool)
func init() {
beeep.AppName = loadedLocale["appName"]
-
- go func() {
- for fn := range uiQueue {
- glib.IdleAdd(func() bool {
- fn()
- return false
- })
- time.Sleep(10 * time.Millisecond) // Small delay between updates
- }
- }()
-
}
func main() {
+ go func() {
+ log.Println(http.ListenAndServe("localhost:6060", nil))
+ }()
+
pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig()
if err != nil {
@@ -278,6 +273,9 @@ func main() {
}
}
+ sid := new(StanzaID)
+ m.Get(sid)
+
originator := JidMustParse(m.From).Bare()
glib.IdleAdd(func() {
@@ -364,6 +362,35 @@ func main() {
}
}
+ // The same, however with retractions
+ retraction := new(Retraction)
+ ok = m.Get(retraction)
+ if ok {
+ tab, ok := tabs.Load(originator)
+ if ok {
+ typed_tab := tab.(*chatTab)
+ /*
+ 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 {
+ to_retract = sid.ID
+ refs = typed_tab.msg_refsOSID
+ }
+ */
+
+ msg, ok := typed_tab.msg_refsOSID[retraction.ID]
+ if ok {
+ lab := msg.label_ref
+ glib.IdleAdd(func() {
+ GenerateRetraction(lab, *retraction)
+ })
+ }
+ }
+ }
+
glib.IdleAdd(func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
@@ -534,6 +561,7 @@ func main() {
gStatus.SetText(fmt.Sprintf("%d goroutines", goroutines))
})
}()
+
go func() {
// Ping
pingStatus.AddCSSClass("pending")
@@ -658,17 +686,8 @@ func main() {
box.Append(im)
box.Append(b)
+ // TODO: Use PEP avatar and do not use JID as hash
go getAvatar(jid, jid, im)
- /*
-
- go func() {
- new_im := getAvatar(jid, jid) // TODO: Use PEP avatar and do not use JID as hash
- glib.IdleAdd(func() {
- new_im.SetPixelSize(40)
- box.Prepend(new_im)
- })
- }()
- */
box.AddController(gesture1)
menu.Append(box)
@@ -757,15 +776,6 @@ func main() {
box.Prepend(im)
go getAvatar(jid, jid, im)
- /*
- go func() {
- new_im := getAvatar(jid, jid)
- glib.IdleAdd(func() {
- new_im.SetPixelSize(40)
- box.Prepend(new_im)
- })
- }()
- */
box.AddController(gesture1)
box.AddController(gesture2)
@@ -779,6 +789,41 @@ func main() {
}
}
+
+ // Local bookmarks
+ for jid, nick := range loadedConfig.LocalBookmarks {
+ 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)
+ menu.Append(box)
+ menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
+ })
+ }
}
})
@@ -794,7 +839,7 @@ func main() {
}
}
- app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
+ app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationDefaultFlags)
app.ConnectActivate(func() {
activate(app)
go conc()
@@ -886,22 +931,16 @@ func activate(app *gtk.Application) {
about_window := gtk.NewWindow()
about_window.SetTransientFor(&window.Window)
about_window.SetTitle(fmt.Sprintf("%s %s", "About", loadedLocale["appName"]))
- a.SetProgramName("Lambda")
- a.SetVersion(lambda_version)
+ a.SetProgramName(loadedLocale["appName"])
+ a.SetVersion(fmt.Sprintf("Version %s", lambda_version))
a.SetComments("yet another XMPP client")
- a.SetAuthors([]string{"Sunglocto"})
- a.SetLicense("GPL3")
+ a.SetAuthors([]string{"Sunglocto", "Hydrogen", "Kuyuhi"})
+ a.SetLicenseType(gtk.LicenseGPL30)
+ a.SetLogo(clientAssetsLoad("logo"))
a.SetWebsite("https://forge.sunglocto.net/sunglocto/lambda")
a.SetWebsiteLabel("Website")
- /*
- a.ConnectResponse(func() {
- about_window.SetVisible(false)
- })
- */
- about_window.SetChild(a)
- about_window.SetDefaultSize(400, 300)
- about_window.SetVisible(true)
+ a.SetVisible(true)
})
destroymucAction := gio.NewSimpleAction("destroymuc", nil)
@@ -969,7 +1008,6 @@ func activate(app *gtk.Application) {
if mu.MucUserItem.Affiliation != "owner" {
box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"]))
}
- // return false
}
}
} else {
@@ -1042,7 +1080,7 @@ func activate(app *gtk.Application) {
box.Append(bookmark_box)
btn := gtk.NewButtonWithLabel(loadedLocale["submit"])
- btn.SetVAlign(gtk.AlignBaseline)
+ btn.SetVAlign(gtk.AlignBaselineFill)
box.Append(btn)
@@ -1332,11 +1370,7 @@ func activate(app *gtk.Application) {
opt.Title = charts.TitleOption{
Text: loadedLocale["pingGraphTitle"],
}
- /*
- opt.XAxis.Labels = []string{
- // The 7 labels here match to the 7 values above
- "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
- }*/
+
opt.Legend = charts.LegendOption{
SeriesNames: []string{
loadedLocale["pingGraphYAxis"],
@@ -1383,17 +1417,6 @@ func activate(app *gtk.Application) {
pBox.Append(pingStatus)
statBar.Append(pBox)
- /*
- sBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
- sIcon = gtk.NewImageFromPaintable(clientAssets["car"])
- sIcon.AddCSSClass("icon")
- sStatus = gtk.NewLabel("-")
- sBox.Append(sIcon)
- sBox.Append(sStatus)
- sStatus.SetTooltipText(loadedLocale["throughputTooltip"])
- statBar.Append(sBox)
- */
-
scrollerStatBar := gtk.NewScrolledWindow()
scrollerStatBar.SetChild(statBar)
box.Append(scrollerStatBar)
diff --git a/style.css b/style.css
index 4886abf..60adecc 100644
--- a/style.css
+++ b/style.css
@@ -30,6 +30,16 @@
color: magenta;
}
+.moderated {
+ color: magenta;
+ opacity: 50%;
+}
+
+.retracted {
+ color: grey;
+ opacity: 50%;
+}
+
.visitor {
color: grey;
}
diff --git a/types.go b/types.go
index 21d4b1f..501929b 100644
--- a/types.go
+++ b/types.go
@@ -16,6 +16,7 @@ type chatTab struct {
isMuc bool
msgs *gtk.ListBox
msg_refsID map[string]*abMessage
+ msg_refsOSID map[string]*abMessage
name string
current_nick string
muc_presence *stanza.Presence
@@ -65,6 +66,9 @@ type lambdaConfig struct {
RandomizeResource bool // If this is set to true, then the resource will be completely randomized by the server on every single connect
+ // Local Bookmarks
+ LocalBookmarks map[string]string // key: JID, value: Nickname
+
// Privacy settings
DiscoPrivacy DiscoveryMode // Whether or not Lambda will report its service discovery features and Identity. Reccomended mode is Everyone, Default is Everyone
VersionPrivacy DiscoveryMode // Whether or not Lambda will report its version information. Reccomended mode is Roster, Default is Everyone
diff --git a/version.go b/version.go
index cf1c369..13b78f5 100644
--- a/version.go
+++ b/version.go
@@ -1,3 +1,3 @@
package main
-var lambda_version string = "8"
+var lambda_version string = "9"
diff --git a/xmpp-retractions.go b/xmpp-retractions.go
new file mode 100644
index 0000000..97ee754
--- /dev/null
+++ b/xmpp-retractions.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "encoding/xml"
+ "gosrc.io/xmpp/stanza"
+)
+
+// 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"`
+ 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"`
+ 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.
+
+func init() {
+ stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:message-retract:1", Local: "retract"}, Retraction{})
+}
diff --git a/xmpp-sid.go b/xmpp-sid.go
index a0a191f..0ce20b9 100644
--- a/xmpp-sid.go
+++ b/xmpp-sid.go
@@ -14,6 +14,13 @@ type StanzaID struct {
ID string `xml:"id,attr"`
}
+type OriginID struct {
+ stanza.MsgExtension
+ XMLName xml.Name `xml:"urn:xmpp:sid:0 origin-id"`
+ ID string `xml:"id,attr"`
+}
+
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:sid:0", Local: "stanza-id"}, StanzaID{})
+ stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:sid:0", Local: "origin-id"}, OriginID{})
}