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{}) }