diff --git a/assets.go b/assets.go index 33a0cf2..c3ef4a7 100644 --- a/assets.go +++ b/assets.go @@ -115,6 +115,10 @@ var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes) var informationBytes []byte var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes) +//go:embed assets/car.png +var carBytes []byte +var carB64 string = base64.StdEncoding.EncodeToString(carBytes) + func init() { loader := gdkpixbuf.NewPixbufLoader() @@ -331,4 +335,12 @@ func init() { loader.Close() clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) + + loader = gdkpixbuf.NewPixbufLoader() + + carData, _ := base64.StdEncoding.DecodeString(carB64) + loader.Write(carData) + loader.Close() + + clientAssets["car"] = gdk.NewTextureForPixbuf(loader.Pixbuf()) } diff --git a/assets/car.png b/assets/car.png new file mode 100644 index 0000000..f2f6e15 Binary files /dev/null and b/assets/car.png differ diff --git a/assets/muc_hidden.png b/assets/muc_hidden.png new file mode 100644 index 0000000..b0f4dd7 Binary files /dev/null and b/assets/muc_hidden.png differ diff --git a/assets/muc_membersonly.png b/assets/muc_membersonly.png new file mode 100644 index 0000000..ff9ca62 Binary files /dev/null and b/assets/muc_membersonly.png differ diff --git a/assets/muc_moderated.png b/assets/muc_moderated.png new file mode 100644 index 0000000..397d58d Binary files /dev/null and b/assets/muc_moderated.png differ diff --git a/assets/muc_nonanonymous.png b/assets/muc_nonanonymous.png new file mode 100644 index 0000000..86d93be Binary files /dev/null and b/assets/muc_nonanonymous.png differ diff --git a/assets/muc_open.png b/assets/muc_open.png new file mode 100644 index 0000000..64bab57 Binary files /dev/null and b/assets/muc_open.png differ diff --git a/assets/muc_passwordprotected.png b/assets/muc_passwordprotected.png new file mode 100644 index 0000000..4ec1a92 Binary files /dev/null and b/assets/muc_passwordprotected.png differ diff --git a/assets/muc_persistent.png b/assets/muc_persistent.png new file mode 100644 index 0000000..fed6221 Binary files /dev/null and b/assets/muc_persistent.png differ diff --git a/assets/muc_public.png b/assets/muc_public.png new file mode 100644 index 0000000..7d863f9 Binary files /dev/null and b/assets/muc_public.png differ diff --git a/assets/muc_semianonymous.png b/assets/muc_semianonymous.png new file mode 100644 index 0000000..f4525b6 Binary files /dev/null and b/assets/muc_semianonymous.png differ diff --git a/assets/muc_temporary.png b/assets/muc_temporary.png new file mode 100644 index 0000000..e2672c2 Binary files /dev/null and b/assets/muc_temporary.png differ diff --git a/assets/muc_unmoderated.png b/assets/muc_unmoderated.png new file mode 100644 index 0000000..d9912ed Binary files /dev/null and b/assets/muc_unmoderated.png differ diff --git a/assets/muc_unsecured.png b/assets/muc_unsecured.png new file mode 100644 index 0000000..f582489 Binary files /dev/null and b/assets/muc_unsecured.png differ diff --git a/gtk-helpers.go b/gtk-helpers.go index 5b292f1..c64295c 100644 --- a/gtk-helpers.go +++ b/gtk-helpers.go @@ -55,8 +55,8 @@ func switchToTab(jid string, w *gtk.Window) { } typed_tab := tab.(*chatTab) - scroller.SetChild(typed_tab.msgs) + typingStatus.SetText("") if typed_tab.isMuc { m, ok := mucmembers.Load(jid) @@ -490,7 +490,7 @@ func showErrorDialog(err error) { func createIdenticon(word string) *gtk.Image { // This function generates an identicon if !loadedConfig.Identicons { i := gtk.NewImageFromPaintable(clientAssets["DefaultAvatar"]) - i.AddCSSClass(loadedConfig.CVD.String()+"_CVD") + i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") return i } diff --git a/gtk-message.go b/gtk-message.go index 0b809e9..8eb344c 100644 --- a/gtk-message.go +++ b/gtk-message.go @@ -68,7 +68,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { if ok { b := gtk.NewBox(gtk.OrientationHorizontal, 0) b.Append(gtk.NewLabel(fmt.Sprintf("%s is typing...", JidMustParse(m.From).Resource))) - return b + return b } if m.Error.Type != "" { @@ -120,7 +120,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { quote.ConnectClicked(func() { lines := strings.Split(m.Body, "\n") for i, line := range lines { - quoteline:= "> " + line + quoteline := "> " + line lines[i] = quoteline } @@ -179,7 +179,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter { im.AddCSSClass("author_img") authorBox.Append(im) } else { - im := createIdenticon(m.From) + im := createIdenticon(m.From) im.SetPixelSize(40) im.AddCSSClass("author_img") authorBox.Append(im) @@ -260,18 +260,18 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou oghash := hash p, err := ensureCache() if err != nil { - return createIdenticon(j) + return createIdenticon(j) } if hash == "" { fmt.Println("Hash is nil!") - return createIdenticon(j) + return createIdenticon(j) } _, ok := invalidImages[hash] if ok { fmt.Println("Image is invalid") - return createIdenticon(j) + return createIdenticon(j) } hash = filepath.Join(p, sanitizefilename.Sanitize(hash)) @@ -279,7 +279,7 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou _, err = os.ReadFile(hash) if err == nil { i := newImageFromPath(hash) - i.AddCSSClass(loadedConfig.CVD.String()+"_CVD") + i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") return i } @@ -305,14 +305,14 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou result := <-mychan card, ok := result.Payload.(*VCard) if !ok { - return createIdenticon(j) + return createIdenticon(j) } base64_data := card.Photo.Binval if card.Photo.Binval == "" || ((card.Photo.Type == "image/svg+xml" || card.Photo.Type == "image/webp") && (runtime.GOOS == "windows" || runtime.GOOS == "netbsd")) { fmt.Println("Blocking image") invalidImages[oghash] = true - return createIdenticon(j) + return createIdenticon(j) } data, err := base64.StdEncoding.DecodeString(base64_data) @@ -326,6 +326,6 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou } i := newImageFromPath(hash) - i.AddCSSClass(loadedConfig.CVD.String()+"_CVD") + i.AddCSSClass(loadedConfig.CVD.String() + "_CVD") return i } diff --git a/helpers.go b/helpers.go index 043c338..ea5016f 100644 --- a/helpers.go +++ b/helpers.go @@ -2,25 +2,24 @@ package main import ( - "sync" "sort" + "sync" ) func rangeOrdered(m *sync.Map, fn func(k, v any) bool) { - var keys []string + var keys []string - m.Range(func(k, v any) bool { - keys = append(keys, k.(string)) - return true - }) + m.Range(func(k, v any) bool { + keys = append(keys, k.(string)) + return true + }) - sort.Strings(keys) + sort.Strings(keys) - for _, k := range keys { - v, _ := m.Load(k) - if !fn(k, v) { - break - } - } + for _, k := range keys { + v, _ := m.Load(k) + if !fn(k, v) { + break + } + } } - diff --git a/main.go b/main.go index 79d074f..1c5a87f 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,9 @@ var connectionIcon *gtk.Image var mStatus *gtk.Label var mIcon *gtk.Image +var sStatus *gtk.Label +var sIcon *gtk.Image + var typingStatus *gtk.Label var pingStatus *gtk.Label @@ -91,6 +94,14 @@ func init() { } func main() { + // Setup log + xmlLog, err := os.CreateTemp("", "xmpp-log") + if err != nil { + panic(err) + } + + defer os.Remove(xmlLog.Name()) + pingTimes = append(pingTimes, []float64{}) p, err := ensureConfig() if err != nil { @@ -120,12 +131,14 @@ func main() { CharsetReader: func(c string, input io.Reader) (io.Reader, error) { return charset.NewReaderLabel(c, input) }, + ConnectTimeout: 300, }, - Jid: loadedConfig.Username + "/" + loadedConfig.Resource, - Credential: xmpp.Password(loadedConfig.Password), - Insecure: loadedConfig.Insecure, - // StreamLogger: os.Stdout, + Jid: loadedConfig.Username + "/" + loadedConfig.Resource, + Credential: xmpp.Password(loadedConfig.Password), + Insecure: loadedConfig.Insecure, StreamManagementEnable: true, + ConnectTimeout: 300, + StreamLogger: xmlLog, } router := xmpp.NewRouter() @@ -276,7 +289,9 @@ func main() { if ok { typed_tab.msgs.Append(b) - scrollToBottomAfterUpdate(scroller) + if current == JidMustParse(m.From).Bare() { + scrollToBottomAfterUpdate(scroller) + } } else { fmt.Println("Got message when the tab does not exist!") } @@ -340,7 +355,9 @@ func main() { if ok { typed_tab.msgs.Append(b) - scrollToBottomAfterUpdate(scroller) + if current == muc { + scrollToBottomAfterUpdate(scroller) + } } else { fmt.Println("Got message when the tab does not exist!") } @@ -426,28 +443,49 @@ func main() { cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) { fmt.Println("XMPP client connected") + // Ping go func() { for { time.Sleep(5 * time.Second) - pingStatus.AddCSSClass("pending") - before := time.Now() - iq := new(stanza.IQ) - iq.From = clientroot.Session.BindJid - iq.To = iq.From - iq.Type = "get" + go func() { + pingStatus.AddCSSClass("pending") + before := time.Now() + iq := new(stanza.IQ) + iq.From = clientroot.Session.BindJid + iq.To = iq.From + iq.Type = "get" - ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) - mychan, err := client.SendIQ(ctx, iq) + ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) + mychan, err := client.SendIQ(ctx, iq) + if err != nil { + return + } + _ = <-mychan + + pingStatus.RemoveCSSClass("pending") + delay := time.Since(before) / time.Millisecond + pingStatus.SetText(fmt.Sprintf("%d ms", delay)) + pingTimes[0] = append(pingTimes[0], float64(delay)) + }() + + } + }() + + // Throughput + var oldsize int64 + var newsize int64 + go func() { + for { + time.Sleep(1 * time.Second) + stat, err := xmlLog.Stat() if err != nil { - continue + panic(err) } - _ = <-mychan - - pingStatus.RemoveCSSClass("pending") - delay := time.Since(before) / time.Millisecond - pingStatus.SetText(fmt.Sprintf("%d ms", delay)) - pingTimes[0] = append(pingTimes[0], float64(delay)) + newsize = stat.Size() + diff := float64(newsize-oldsize) / 1000 + sStatus.SetText(fmt.Sprintf("%.2fKB/s", diff)) + oldsize = stat.Size() } }() connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare())) @@ -473,42 +511,55 @@ func main() { res, ok := result.Payload.(*stanza.PubSubGeneric) if ok { for _, item := range res.Items.List { - go func() { - jid := item.Id - node := item.Any - autojoin := false - nick := loadedConfig.Nick - for _, attr := range node.Attrs { - if attr.Name.Local == "autojoin" { - autojoin = attr.Value == "true" - } + jid := item.Id + node := item.Any + autojoin := false + for _, attr := range node.Attrs { + if attr.Name.Local == "autojoin" { + autojoin = attr.Value == "true" } + } - for _, node := range node.Nodes { - if node.XMLName.Local == "nick" { - nick = node.Content - } + _, ok := tabs.Load(jid) + if !ok && autojoin { + createTab(jid, true) + b := gtk.NewLabel(jid) + gesture1 := gtk.NewGestureClick() + gesture1.SetButton(1) + gesture1.Connect("pressed", func() { + switchToTab(jid, &window.Window) + }) + + b.AddController(gesture1) + menu.Append(b) + } + } + + for _, item := range res.Items.List { + + jid := item.Id + node := item.Any + autojoin := false + nick := loadedConfig.Nick + for _, attr := range node.Attrs { + if attr.Name.Local == "autojoin" { + autojoin = attr.Value == "true" } + } - _, ok := tabs.Load(jid) - if !ok && autojoin { - err := joinMuc(client, clientroot.Session.BindJid, jid, nick) - if err != nil { - panic(err) - } - - createTab(jid, true) - b := gtk.NewLabel(jid) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(jid, &window.Window) - }) - - b.AddController(gesture1) - menu.Append(b) + for _, node := range node.Nodes { + if node.XMLName.Local == "nick" { + nick = node.Content } - }() + } + + if autojoin { + err := joinMuc(client, clientroot.Session.BindJid, jid, nick) + if err != nil { + panic(err) + } + } + } } } @@ -693,21 +744,21 @@ func activate(app *gtk.Application) { _, ok := tabs.Load(t) jm := func() { - err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text()) - if err != nil { - panic(err) - } + err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text()) + if err != nil { + panic(err) + } - createTab(t, true) - b := gtk.NewLabel(t) - gesture1 := gtk.NewGestureClick() - gesture1.SetButton(1) - gesture1.Connect("pressed", func() { - switchToTab(t, &window.Window) - }) + createTab(t, true) + b := gtk.NewLabel(t) + gesture1 := gtk.NewGestureClick() + gesture1.SetButton(1) + gesture1.Connect("pressed", func() { + switchToTab(t, &window.Window) + }) - b.AddController(gesture1) - menu.Append(b) + b.AddController(gesture1) + menu.Append(b) } if !ok { // First check the MUC's disco and see if it's semianon @@ -907,6 +958,15 @@ 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("Throughput of your XMPP connection in KB/s") + statBar.Append(sBox) + scrollerStatBar := gtk.NewScrolledWindow() scrollerStatBar.SetChild(statBar) box.Append(scrollerStatBar) diff --git a/svg-conv.go b/svg-conv.go index 22db2e4..b939c08 100644 --- a/svg-conv.go +++ b/svg-conv.go @@ -3,10 +3,10 @@ package main import ( "bytes" "fmt" - "image" - "image/png" "github.com/srwiley/oksvg" "github.com/srwiley/rasterx" + "image" + "image/png" ) func SVGToPNG(svgData []byte) ([]byte, error) { diff --git a/types.go b/types.go index cf8bea5..ad2bbcf 100644 --- a/types.go +++ b/types.go @@ -20,7 +20,7 @@ type lambdaConfig struct { Nick string JoinBookmarks bool CVD color.CVD - Identicons bool + Identicons bool } type mucUnit struct {