Compare commits

..

9 Commits

Author SHA1 Message Date
sunglocto 82aa2abfbd t 2026-05-03 11:10:15 +01:00
sunglocto 58c7165ce5 fix avatars 2026-05-02 06:56:08 +01:00
sunglocto 5bcf0eda0b t 2026-05-01 21:05:14 +01:00
sunglocto 0bfb140dc7 Fix lag issues 2026-04-30 14:55:17 +01:00
sunglocto 7b63799f0b Add extra fields to vCard and format code 2026-04-28 13:08:16 +01:00
sunglocto a97c42323c Do not let ebassi see this code 2026-04-28 12:58:00 +01:00
sunglocto fc0ed5ac2c somehow sunglocto returned 2026-04-26 10:40:13 +01:00
sunglocto 69994d9856 remove base64 encoding/decoding which was not needed for embedding assets 2026-04-18 11:09:59 +01:00
sunglocto 843687ff2b add throughput, format code, and begin to add MUC preview window 2026-04-18 10:29:07 +01:00
33 changed files with 1060 additions and 566 deletions
+108 -243
View File
@@ -2,333 +2,198 @@ package main
import (
_ "embed"
"encoding/base64"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gdkpixbuf/v2"
)
//go:embed debug.png
var defaultAvatarBytes []byte
var defaultAvatarB64 string = base64.StdEncoding.EncodeToString(defaultAvatarBytes)
//go:embed failed_load.png
var failedBytes []byte
var failedB64 string = base64.StdEncoding.EncodeToString(failedBytes)
//go:embed assets/owner.png
var ownerMedalBytes []byte
var ownerMedalB64 string = base64.StdEncoding.EncodeToString(ownerMedalBytes)
//go:embed assets/admin.png
var adminMedalBytes []byte
var adminMedalB64 string = base64.StdEncoding.EncodeToString(adminMedalBytes)
//go:embed assets/member.png
var memberMedalBytes []byte
var memberMedalB64 string = base64.StdEncoding.EncodeToString(memberMedalBytes)
//go:embed assets/noaffiliation.png
var noneMedalBytes []byte
var noneMedalB64 string = base64.StdEncoding.EncodeToString(noneMedalBytes)
//go:embed assets/outcast.png
var outcastMedalBytes []byte
var outcastMedalB64 string = base64.StdEncoding.EncodeToString(outcastMedalBytes)
//go:embed assets/cancel.png
var cancelBytes []byte
var cancelB64 string = base64.StdEncoding.EncodeToString(cancelBytes)
//go:embed assets/status_away.png
var sABytes []byte
var sAB64 string = base64.StdEncoding.EncodeToString(sABytes)
//go:embed assets/status_busy.png
var sBBytes []byte
var sBB64 string = base64.StdEncoding.EncodeToString(sBBytes)
//go:embed assets/status_chatty.png
var sCBytes []byte
var sCB64 string = base64.StdEncoding.EncodeToString(sCBytes)
//go:embed assets/status_online.png
var sOBytes []byte
var sOB64 string = base64.StdEncoding.EncodeToString(sOBytes)
//go:embed assets/status_xa.png
var xaBytes []byte
var xaB64 string = base64.StdEncoding.EncodeToString(xaBytes)
//go:embed assets/tag.png
var tagBytes []byte
var tagB64 string = base64.StdEncoding.EncodeToString(tagBytes)
//go:embed assets/lambda-disabled.png
var logoDisabledBytes []byte
var logoDisabledB64 string = base64.StdEncoding.EncodeToString(logoDisabledBytes)
//go:embed assets/group.png
var groupBytes []byte
var groupB64 string = base64.StdEncoding.EncodeToString(groupBytes)
//go:embed assets/door_in.png
var doorInBytes []byte
var doorInB64 string = base64.StdEncoding.EncodeToString(doorInBytes)
//go:embed assets/door_out.png
var doorOutBytes []byte
var doorOutB64 string = base64.StdEncoding.EncodeToString(doorOutBytes)
//go:embed assets/large_group.png
var largeGroupBytes []byte
var largeGroupB64 string = base64.StdEncoding.EncodeToString(largeGroupBytes)
//go:embed assets/world.png
var worldBytes []byte
var worldB64 string = base64.StdEncoding.EncodeToString(worldBytes)
//go:embed assets/disconnect.png
var disconnectBytes []byte
var disconnectB64 string = base64.StdEncoding.EncodeToString(disconnectBytes)
//go:embed assets/chart_bar.png
var barBytes []byte
var barB64 string = base64.StdEncoding.EncodeToString(barBytes)
//go:embed assets/chart_bar_laggy.png
var barLaggyBytes []byte
//go:embed assets/ok.png
var okBytes []byte
var okB64 string = base64.StdEncoding.EncodeToString(okBytes)
//go:embed assets/hourglass.png
var hourglassBytes []byte
var hourglassB64 string = base64.StdEncoding.EncodeToString(hourglassBytes)
//go:embed assets/connect_tls.png
var connectBytes []byte
var connectB64 string = base64.StdEncoding.EncodeToString(connectBytes)
//go:embed assets/comment.png
var commentBytes []byte
var commentB64 string = base64.StdEncoding.EncodeToString(commentBytes)
//go:embed assets/information.png
var informationBytes []byte
var informationB64 string = base64.StdEncoding.EncodeToString(informationBytes)
//go:embed assets/car.png
var carBytes []byte
//go:embed assets/car_high.png
var carHighBytes []byte
// muc icons
//go:embed assets/muc_open.png
var mucOpenBytes []byte
//go:embed assets/muc_membersonly.png
var mucMembersOnlyBytes []byte
//go:embed assets/muc_passwordprotected.png
var mucPasswordProtectedBytes []byte
//go:embed assets/muc_unsecured.png
var mucUnsecuredBytes []byte
//go:embed assets/muc_hidden.png
var mucHiddenBytes []byte
//go:embed assets/muc_public.png
var mucPublicBytes []byte
//go:embed assets/muc_unmoderated.png
var mucUnmoderatedBytes []byte
//go:embed assets/muc_moderated.png
var mucModeratedBytes []byte
//go:embed assets/muc_nonanonymous.png
var mucNonAnonymousBytes []byte
//go:embed assets/muc_semianonymous.png
var mucSemiAnonymousBytes []byte
//go:embed assets/muc_persistent.png
var mucPersistentBytes []byte
//go:embed assets/muc_temporary.png
var mucTemporaryBytes []byte
//go:embed assets/moderate.png
var moderateBytes []byte
//go:embed assets/jabber.png
var jabberBytes []byte
func loadAsset(key string, data []byte) {
loader := gdkpixbuf.NewPixbufLoader()
loader.Write(data)
loader.Close()
clientAssets[key] = gdk.NewTextureForPixbuf(loader.Pixbuf())
}
func init() {
loader := gdkpixbuf.NewPixbufLoader()
defaultAvatarData, _ := base64.StdEncoding.DecodeString(defaultAvatarB64)
loader.Write(defaultAvatarData)
loader.Close()
clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
failedData, _ := base64.StdEncoding.DecodeString(failedB64)
loader.Write(failedData)
loader.Close()
clientAssets["FailedAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
ownerMedalData, _ := base64.StdEncoding.DecodeString(ownerMedalB64)
loader.Write(ownerMedalData)
loader.Close()
clientAssets["owner"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
cancelData, _ := base64.StdEncoding.DecodeString(cancelB64)
loader.Write(cancelData)
loader.Close()
clientAssets["cancel"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
tagData, _ := base64.StdEncoding.DecodeString(tagB64)
loader.Write(tagData)
loader.Close()
clientAssets["tag"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
adminMedalData, _ := base64.StdEncoding.DecodeString(adminMedalB64)
loader.Write(adminMedalData)
loader.Close()
clientAssets["admin"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
memberMedalData, _ := base64.StdEncoding.DecodeString(memberMedalB64)
loader.Write(memberMedalData)
loader.Close()
clientAssets["member"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
noneMedalData, _ := base64.StdEncoding.DecodeString(noneMedalB64)
loader.Write(noneMedalData)
loader.Close()
clientAssets["none"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
outcastMedalData, _ := base64.StdEncoding.DecodeString(outcastMedalB64)
loader.Write(outcastMedalData)
loader.Close()
clientAssets["outcast"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
disabledLogoData, _ := base64.StdEncoding.DecodeString(logoDisabledB64)
loader.Write(disabledLogoData)
loader.Close()
clientAssets["disabled_logo"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
groupData, _ := base64.StdEncoding.DecodeString(groupB64)
loader.Write(groupData)
loader.Close()
clientAssets["group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
doorInData, _ := base64.StdEncoding.DecodeString(doorInB64)
loader.Write(doorInData)
loader.Close()
clientAssets["door_in"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
doorOutData, _ := base64.StdEncoding.DecodeString(doorOutB64)
loader.Write(doorOutData)
loader.Close()
clientAssets["door_out"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
largeGroupData, _ := base64.StdEncoding.DecodeString(largeGroupB64)
loader.Write(largeGroupData)
loader.Close()
clientAssets["large_group"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
worldData, _ := base64.StdEncoding.DecodeString(worldB64)
loader.Write(worldData)
loader.Close()
clientAssets["world"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
disconnectData, _ := base64.StdEncoding.DecodeString(disconnectB64)
loader.Write(disconnectData)
loader.Close()
clientAssets["disconnect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
barData, _ := base64.StdEncoding.DecodeString(barB64)
loader.Write(barData)
loader.Close()
clientAssets["chart_bar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
okData, _ := base64.StdEncoding.DecodeString(okB64)
loader.Write(okData)
loader.Close()
clientAssets["ok"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
hourglassData, _ := base64.StdEncoding.DecodeString(hourglassB64)
loader.Write(hourglassData)
loader.Close()
clientAssets["hourglass"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
connectData, _ := base64.StdEncoding.DecodeString(connectB64)
loader.Write(connectData)
loader.Close()
clientAssets["connect"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
commentData, _ := base64.StdEncoding.DecodeString(commentB64)
loader.Write(commentData)
loader.Close()
clientAssets["comment"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
informationData, _ := base64.StdEncoding.DecodeString(informationB64)
loader.Write(informationData)
loader.Close()
clientAssets["information"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sAData, _ := base64.StdEncoding.DecodeString(sAB64)
loader.Write(sAData)
loader.Close()
clientAssets["status_away"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sBData, _ := base64.StdEncoding.DecodeString(sBB64)
loader.Write(sBData)
loader.Close()
clientAssets["status_dnd"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sCData, _ := base64.StdEncoding.DecodeString(sCB64)
loader.Write(sCData)
loader.Close()
clientAssets["status_chat"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
xaData, _ := base64.StdEncoding.DecodeString(xaB64)
loader.Write(xaData)
loader.Close()
clientAssets["status_xa"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
loader = gdkpixbuf.NewPixbufLoader()
sOData, _ := base64.StdEncoding.DecodeString(sOB64)
loader.Write(sOData)
loader.Close()
clientAssets["status_"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
for key, data := range map[string][]byte{
"DefaultAvatar": defaultAvatarBytes,
"FailedAvatar": failedBytes,
"owner": ownerMedalBytes,
"admin": adminMedalBytes,
"member": memberMedalBytes,
"none": noneMedalBytes,
"outcast": outcastMedalBytes,
"cancel": cancelBytes,
"tag": tagBytes,
"disabled_logo": logoDisabledBytes,
"group": groupBytes,
"door_in": doorInBytes,
"door_out": doorOutBytes,
"large_group": largeGroupBytes,
"world": worldBytes,
"disconnect": disconnectBytes,
"chart_bar": barBytes,
"chart_bar_laggy": barLaggyBytes,
"ok": okBytes,
"hourglass": hourglassBytes,
"connect": connectBytes,
"comment": commentBytes,
"information": informationBytes,
"status_away": sABytes,
"status_dnd": sBBytes,
"status_chat": sCBytes,
"status_xa": xaBytes,
"status_": sOBytes,
"car": carBytes,
"car_high": carHighBytes,
"muc_open": mucOpenBytes,
"muc_membersonly": mucMembersOnlyBytes,
"muc_passwordprotected": mucPasswordProtectedBytes,
"muc_unsecured": mucUnsecuredBytes,
"muc_hidden": mucHiddenBytes,
"muc_public": mucPublicBytes,
"muc_unmoderated": mucUnmoderatedBytes,
"muc_moderated": mucModeratedBytes,
"muc_nonanonymous": mucNonAnonymousBytes,
"muc_semianonymous": mucSemiAnonymousBytes,
"muc_persistent": mucPersistentBytes,
"muc_temporary": mucTemporaryBytes,
"moderate": moderateBytes,
"jabber": jabberBytes,
} {
loadAsset(key, data)
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

+35 -25
View File
@@ -13,10 +13,12 @@ import (
"net/http"
"os"
"path/filepath"
"sync"
)
// global or app-level map/cache
var textureCache = make(map[string]gdk.Paintabler)
// var textureCache = make(map[string]gdk.Paintabler)
var textureCache sync.Map
// Invalid images, if an image/avatar cannot be loaded on the system (e.g: incompatible format) it's put here
var invalidImages = make(map[string]bool)
@@ -31,49 +33,57 @@ func ensureCache() (string, error) {
return cachePath, nil
}
func getTexture(path string) gdk.Paintabler {
if tex, exists := textureCache[path]; exists {
return tex
func getTexture(path string) (gdk.Paintabler, error) {
tex, exists := textureCache.Load(path)
if exists {
return tex.(gdk.Paintabler), nil
}
tex, err := gdk.NewTextureFromFilename(path) // load once
if err != nil {
panic(err)
return nil, err
}
textureCache[path] = tex
return tex
textureCache.Store(path, tex)
return tex.(gdk.Paintabler), nil
}
func newPictureFromPath(path string) *gtk.Picture {
tex := getTexture(path)
func newPictureFromPath(path string) (*gtk.Picture, error) {
tex, err := getTexture(path)
if err != nil {
return nil, err
}
img := gtk.NewPictureForPaintable(tex)
return img
return img, nil
}
func newImageFromPath(path string) *gtk.Image {
tex := getTexture(path)
func newImageFromPath(path string) (*gtk.Image, error) {
tex, err := getTexture(path)
if err != nil {
return nil, err
}
img := gtk.NewImageFromPaintable(tex)
return img
return img, nil
}
func newPictureFromWeb(url string) *gtk.Picture {
func newPictureFromWeb(url string) (*gtk.Picture, error) {
pa, _ := ensureCache()
// step 1: get a sha256 sum of the URL
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
p, ok := textureCache[sum]
p, ok := textureCache.Load(sum)
if ok {
return gtk.NewPictureForPaintable(p)
return gtk.NewPictureForPaintable(p.(gdk.Paintabler)), nil
}
// step 2: download it
resp, err := http.Get(url)
if err != nil {
return nil
return nil, err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil
return nil, err
}
fullpath := filepath.Join(pa, sum)
@@ -81,31 +91,31 @@ func newPictureFromWeb(url string) *gtk.Picture {
// step 3: save it
err = os.WriteFile(fullpath, b, 0644)
if err != nil {
return nil
return nil, err
}
return newPictureFromPath(fullpath)
}
func newImageFromWeb(url string) *gtk.Image {
func newImageFromWeb(url string) (*gtk.Image, error) {
pa, _ := ensureCache()
// step 1: get a sha256 sum of the URL
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
p, ok := textureCache[sum]
p, ok := textureCache.Load(sum)
if ok {
return gtk.NewImageFromPaintable(p)
return gtk.NewImageFromPaintable(p.(gdk.Paintabler)), nil
}
// step 2: download it
resp, err := http.Get(url)
if err != nil {
return nil
return nil, err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil
return nil, err
}
fullpath := filepath.Join(pa, sum)
@@ -113,7 +123,7 @@ func newImageFromWeb(url string) *gtk.Image {
// step 3: save it
err = os.WriteFile(fullpath, b, 0644)
if err != nil {
return nil
return nil, err
}
return newImageFromPath(fullpath)
+136 -28
View File
@@ -27,8 +27,10 @@ func scrollToBottomAfterUpdate(scrolledWindow *gtk.ScrolledWindow) {
})
}
func createTab(jid string, isMuc bool) bool {
fmt.Println("Creating tab", jid, "isMuc:", isMuc)
func createTab(jid string, isMuc bool, name string) bool {
if name == "" {
name = jid
}
_, ok := tabs.Load(jid)
_, uok := userdevices.Load(jid)
_, mok := mucmembers.Load(jid)
@@ -38,8 +40,9 @@ func createTab(jid string, isMuc bool) bool {
newTab.msgs = gtk.NewListBox()
newTab.msgs.SetVExpand(true)
newTab.msgs.SetShowSeparators(true)
newTab.name = name
newTab.msgs.Append(gtk.NewButtonWithLabel("Get past messages..."))
newTab.msgs.Append(gtk.NewButtonWithLabel(loadedLocale["getPastMessages"]))
tabs.Store(jid, newTab)
return true
}
@@ -55,10 +58,9 @@ 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)
if !ok {
return
@@ -68,14 +70,18 @@ func switchToTab(jid string, w *gtk.Window) {
return
}
mm := ma.Members
gen := gtk.NewBox(gtk.OrientationVertical, 0)
gen := gtk.NewBox(gtk.OrientationVertical, 10)
i := 0
rangeOrdered(&mm, (func(k, v any) bool {
i++
userbox := gtk.NewBox(gtk.OrientationHorizontal, 0)
u, ok := v.(stanza.Presence)
if !ok {
return true
}
userbox := gtk.NewBox(gtk.OrientationHorizontal, 2)
u := v.(stanza.Presence)
var mu MucUser
var ocu OccupantID
u.Get(&mu)
@@ -99,11 +105,11 @@ func switchToTab(jid string, w *gtk.Window) {
nick_label.SetOpacity(0.5)
}
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\nClick for more information", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation))
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
ok := u.Get(&hats)
ok = u.Get(&hats)
if ok {
for _, hat := range hats.Hats {
var val float64
@@ -134,6 +140,7 @@ func switchToTab(jid string, w *gtk.Window) {
}
}
status := gtk.NewImageFromPaintable(clientAssets["status_"+string(u.Show)])
status.SetTooltipText(string(u.Show))
@@ -148,6 +155,22 @@ func switchToTab(jid string, w *gtk.Window) {
medal.SetHExpand(true)
userbox.Append(medal)
default_av := createIdenticon(u.From)
userbox.Prepend(default_av)
var vcu VCardUpdate
ok = u.Get(&vcu)
if ok {
photo := vcu.Photo
go func() {
new_im := getAvatar(u.From, photo)
glib.IdleAdd(func() {
userbox.Remove(default_av)
userbox.Prepend(new_im)
})
}()
}
gesture := gtk.NewGestureClick()
gesture.SetButton(1)
@@ -159,10 +182,10 @@ func switchToTab(jid string, w *gtk.Window) {
popover.SetParent(userbox)
rc_box := gtk.NewBox(gtk.OrientationVertical, 0)
bb := gtk.NewButtonWithLabel("Ban")
kb := gtk.NewButtonWithLabel("Kick")
ab := gtk.NewButtonWithLabel("Set affil")
rb := gtk.NewButtonWithLabel("Set role")
bb := gtk.NewButtonWithLabel(loadedLocale["ban"])
kb := gtk.NewButtonWithLabel(loadedLocale["kick"])
ab := gtk.NewButtonWithLabel(loadedLocale["setAffil"])
rb := gtk.NewButtonWithLabel(loadedLocale["setRole"])
kb.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
@@ -207,12 +230,12 @@ func switchToTab(jid string, w *gtk.Window) {
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Set " + JidMustParse(u.From).Resource + "'s affiliation"))
box.Append(gtk.NewLabel(loadedLocale["setAffilDescPartOne"] + JidMustParse(u.From).Resource + loadedLocale["setAffilDescPartTwo"]))
the_entry := gtk.NewEntry()
the_entry.SetText(mu.MucUserItem.Affiliation)
submit := gtk.NewButtonWithLabel("Submit")
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
<iq from='%s'
@@ -246,13 +269,13 @@ func switchToTab(jid string, w *gtk.Window) {
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Set " + JidMustParse(u.From).Resource + "'s role"))
box.Append(gtk.NewLabel("Important: if you want this to be permanent, set their affiliation instead"))
box.Append(gtk.NewLabel(loadedLocale["setRoleDescPartOne"] + JidMustParse(u.From).Resource + loadedLocale["setRoleDescPartTwo"]))
box.Append(gtk.NewLabel(loadedLocale["setRoleWarning"]))
the_entry := gtk.NewEntry()
the_entry.SetText(mu.MucUserItem.Role)
submit := gtk.NewButtonWithLabel("Submit")
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(`
@@ -295,7 +318,7 @@ func switchToTab(jid string, w *gtk.Window) {
win.SetDefaultSize(400, 400)
profile_box := gtk.NewBox(gtk.OrientationVertical, 0)
nick := gtk.NewLabel(JidMustParse(u.From).Resource)
ver_text := gtk.NewLabel("Getting version...")
ver_text := gtk.NewLabel(loadedLocale["gettingVersion"])
ver_text.AddCSSClass("visitor")
win.SetTitle(JidMustParse(u.From).Resource)
@@ -343,12 +366,11 @@ func switchToTab(jid string, w *gtk.Window) {
ji.SetSelectable(true)
profile_box.Append(ji)
}
profile_box.Append(gtk.NewLabel("Connected with role " + mu.MucUserItem.Role))
profile_box.Append(gtk.NewLabel("Affiliated as " + mu.MucUserItem.Affiliation))
profile_box.Append(gtk.NewLabel(loadedLocale["connectedWithRole"] + mu.MucUserItem.Role))
profile_box.Append(gtk.NewLabel(loadedLocale["affiliatedAs"] + mu.MucUserItem.Affiliation))
}
go func() {
fmt.Println("Attempting to get Disco info")
myIQ, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
@@ -408,13 +430,13 @@ func switchToTab(jid string, w *gtk.Window) {
vr := fmt.Sprintf("%s %s %s", name, version, os)
if name == "" && version == "" && os == "" {
ver_text.SetText("Client responded with empty version")
ver_text.SetText(loadedLocale["versionQueryEmpty"])
} else {
ver_text.SetText(vr)
ver_text.RemoveCSSClass("visitor")
}
} else if result.Error != nil && result.Error.Type != "" {
ver_text.SetText("Got error trying to get version")
ver_text.SetText(loadedLocale["versionQueryError"])
ver_text.SetTooltipText(result.Error.Reason + ": " + result.Error.Text)
ver_text.RemoveCSSClass("visitor")
ver_text.AddCSSClass("error")
@@ -470,12 +492,15 @@ func switchToTab(jid string, w *gtk.Window) {
} else {
headerBox.Append(gtk.NewImageFromPaintable(clientAssets["group"]))
}
headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d participant(s)", i)))
headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, loadedLocale["participants"])))
gen.Prepend(headerBox)
muci := getAvatar(jid, jid)
muci.SetPixelSize(80)
gen.Prepend(muci)
muc_name := gtk.NewLabel(typed_tab.name)
muc_name.AddCSSClass("author")
gen.Prepend(muc_name)
memberList.SetChild(gen)
} else {
memberList.SetChild(gtk.NewLabel(jid))
@@ -484,7 +509,23 @@ func switchToTab(jid string, w *gtk.Window) {
}
func showErrorDialog(err error) {
fmt.Println(err.Error())
err_win := gtk.NewWindow()
err_win.SetTitle(loadedLocale["error"])
err_win.SetDefaultSize(400, 200)
err_win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
err_label := gtk.NewLabel(err.Error())
err_label.SetSelectable(true)
box.Append(err_label)
close_btn := gtk.NewButtonWithLabel(loadedLocale["close"])
close_btn.ConnectClicked(func() {
err_win.SetVisible(false)
})
box.Append(close_btn)
err_win.SetChild(box)
err_win.Present()
}
func createIdenticon(word string) *gtk.Image { // This function generates an identicon
@@ -496,7 +537,7 @@ func createIdenticon(word string) *gtk.Image { // This function generates an ide
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw(word)
im := ii.Image(25)
im := ii.Image(250)
buf := new(bytes.Buffer)
err := png.Encode(buf, im)
@@ -512,3 +553,70 @@ func createIdenticon(word string) *gtk.Image { // This function generates an ide
return i
}
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.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 2)
header := gtk.NewLabel("Build-A-JID")
header.AddCSSClass("author")
box.Append(header)
box.Append(gtk.NewLabel("All fields except for domain are optional"))
jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2)
localPartEntry := gtk.NewEntry()
localPartEntry.SetPlaceholderText("localpart")
jid_builder.Append(localPartEntry)
at_sign := gtk.NewLabel("@")
at_sign.AddCSSClass("author")
jid_builder.Append(at_sign)
domainEntry := gtk.NewEntry()
domainEntry.SetPlaceholderText("domain")
jid_builder.Append(domainEntry)
resource_sign := gtk.NewLabel("/")
resource_sign.AddCSSClass("author")
jid_builder.Append(resource_sign)
resourceEntry := gtk.NewEntry()
resourceEntry.SetPlaceholderText("resource")
jid_builder.Append(resourceEntry)
box.Append(jid_builder)
submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
localPart := localPartEntry.Text()
domain := domainEntry.Text()
resource := resourceEntry.Text()
at := "@"
slash := "/"
if localPart == "" {
at = ""
}
if resource == "" {
slash = ""
}
jid := localPart + at + domain + slash + resource
en.SetText(jid)
win.SetVisible(false)
})
box.Append(submit)
win.SetChild(box)
win.SetVisible(true)
}
+61 -25
View File
@@ -7,9 +7,9 @@ import (
"encoding/base64"
"fmt"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/google/uuid"
"github.com/jacoblockett/sanitizefilename"
"github.com/jasonlovesdoggo/gopen"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
@@ -23,7 +23,7 @@ func generatePresenceWidget(p stanza.Packet) gtk.Widgetter {
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
presence, ok := p.(stanza.Presence)
if !ok {
return gtk.NewLabel("Unsupported message.")
return gtk.NewLabel(loadedLocale["unsupportedMessage"])
}
if presence.Type == stanza.PresenceTypeUnavailable {
@@ -32,7 +32,7 @@ func generatePresenceWidget(p stanza.Packet) gtk.Widgetter {
if ok {
if mu.MucUserItem.Affiliation == "outcast" {
b.Append(gtk.NewImageFromPaintable(clientAssets["outcast"]))
b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource + " has been banned by " + mu.MucUserItem.Actor.Nick + "!"))
b.Append(gtk.NewLabel(JidMustParse(presence.From).Resource + loadedLocale["bannedWidget"] + mu.MucUserItem.Actor.Nick + "!"))
return b
}
}
@@ -59,7 +59,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
ok = m.Get(&readmarker)
if ok {
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
b.Append(gtk.NewLabel(fmt.Sprintf("%s has read to this point", JidMustParse(m.From).Resource)))
b.Append(gtk.NewLabel(fmt.Sprintf("%s%s", JidMustParse(m.From).Resource, loadedLocale["readWidget"])))
return b
}
@@ -67,7 +67,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
ok = m.Get(&composing)
if ok {
b := gtk.NewBox(gtk.OrientationHorizontal, 0)
b.Append(gtk.NewLabel(fmt.Sprintf("%s is typing...", JidMustParse(m.From).Resource)))
b.Append(gtk.NewLabel(fmt.Sprintf("%s%s", JidMustParse(m.From).Resource, loadedLocale["isTyping"])))
return b
}
@@ -85,7 +85,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
sid := StanzaID{}
m.Get(&sid)
mainBox := gtk.NewBox(gtk.OrientationVertical, 0)
mainBox := gtk.NewBox(gtk.OrientationVertical, 10)
gesture := gtk.NewGestureClick()
gesture.SetButton(3) // Right click
@@ -155,35 +155,39 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
// authorBox.Append(im)
n := jid.MustParse(m.From).Resourcepart()
n := JidMustParse(m.From).Resource
if n == "" {
n = jid.MustParse(m.From).String()
n = JidMustParse(m.From).Resource
}
al := gtk.NewLabel(n)
al.AddCSSClass("author")
al.SetSelectable(true)
if m.Type == stanza.MessageTypeGroupchat {
mo, _ := mucmembers.Load(jid.MustParse(m.From).Bare().String())
mo, _ := mucmembers.Load(JidMustParse(m.From).Bare())
mm := mo.(mucUnit)
mmm := mm.Members
mmmm, ok := mmm.Load(id)
if ok {
pres := mmmm.(stanza.Presence)
var vu VCardUpdate
pres.Get(&vu)
if vu.Photo != "" {
im := getAvatar(m.From, vu.Photo)
im.SetPixelSize(40)
im.AddCSSClass("author_img")
authorBox.Append(im)
} else {
im := createIdenticon(m.From)
im.SetPixelSize(40)
im.AddCSSClass("author_img")
authorBox.Append(im)
if vu.Photo != "" {
go func() {
new_im := getAvatar(m.From, vu.Photo)
glib.IdleAdd(func() {
new_im.SetPixelSize(40)
new_im.AddCSSClass("author_img")
authorBox.Remove(im)
authorBox.Prepend(new_im)
})
}()
}
} else {
im := createIdenticon(m.From)
im.SetPixelSize(40)
@@ -191,7 +195,7 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
authorBox.Append(im)
}
} else if m.Type == stanza.MessageTypeChat {
al.SetText(al.Text() + " whispers")
al.SetText(al.Text() + loadedLocale["whispers"])
}
authorBox.Append(al)
@@ -204,18 +208,20 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
mlabel := gtk.NewLabel(m.Body)
if m.Body == "" {
mlabel.SetText("No body set")
mlabel.SetText(loadedLocale["noBodySet"])
mlabel.AddCSSClass("visitor")
}
mlabel.SetWrap(true)
mlabel.SetSelectable(true)
mlabel.SetHAlign(gtk.AlignFill)
/*
mum := MucUser{}
ok = m.Get(&mum)
if ok {
mlabel.SetText(fmt.Sprintf("%s's affiliation has been changed to %s", mum.MucUserItem.JID, mum.MucUserItem.Affiliation))
mlabel.SetText(fmt.Sprintf("%s%s%s", mum.MucUserItem.JID, loadedLocale["affilChange"], mum.MucUserItem.Affiliation))
}
*/
contentBox.Append(mlabel)
@@ -248,6 +254,30 @@ func generateMessageWidget(p stanza.Packet) gtk.Widgetter {
mainBox.Append(subjectlabel)
}
link_preview := LinkPreview{}
ok = m.Get(&link_preview)
if ok {
lp_box := gtk.NewBox(gtk.OrientationVertical, 10)
lp_box.AddCSSClass("link_preview")
lp_title := gtk.NewLabel(link_preview.Title)
lp_title.SetSelectable(true)
lp_title.SetWrap(true)
lp_title.SetHAlign(gtk.AlignFill)
lp_desc := gtk.NewLabel(link_preview.URL + "\n" + link_preview.Description)
lp_desc.SetSelectable(true)
lp_desc.SetWrap(true)
lp_desc.SetHAlign(gtk.AlignFill)
lp_box.Append(lp_title)
lp_box.Append(lp_desc)
warning := gtk.NewLabel("⚠️")
warning.SetTooltipText(loadedLocale["linkPreviewWarning"])
lp_box.Append(warning)
mainBox.Append(lp_box)
}
return mainBox
}
@@ -264,21 +294,23 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
}
if hash == "" {
fmt.Println("Hash is nil!")
return createIdenticon(j)
}
_, ok := invalidImages[hash]
if ok {
fmt.Println("Image is invalid")
return createIdenticon(j)
}
hash = filepath.Join(p, sanitizefilename.Sanitize(hash))
hash = filepath.Join(p, hash)
_, err = os.ReadFile(hash)
if err == nil {
i := newImageFromPath(hash)
i, err := newImageFromPath(hash)
if err != nil {
invalidImages[oghash] = true
return createIdenticon(j)
}
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
return i
}
@@ -287,7 +319,7 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
Type: "get",
From: clientroot.Session.BindJid,
To: j,
Id: "vc2",
Id: uuid.New().String(),
Lang: "en",
})
@@ -325,7 +357,11 @@ func getAvatar(j, hash string) *gtk.Image { // TODO: This function probably shou
panic(err)
}
i := newImageFromPath(hash)
i, err := newImageFromPath(hash)
if err != nil {
invalidImages[oghash] = true
return createIdenticon(j)
}
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
return i
}
+7 -7
View File
@@ -35,12 +35,12 @@ func dropToSignInPage(err error) {
nickname_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
insecure_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
server_label := gtk.NewLabel("Server: ")
username_label := gtk.NewLabel("JID: ")
password_label := gtk.NewLabel("Password: ")
nickname_label := gtk.NewLabel("Nickname: ")
insecure_label := gtk.NewLabel("Insecure: (?)")
insecure_label.SetTooltipText("Tick this if you need to connect without TLS, usually for connecting to Tor XMPP servers")
server_label := gtk.NewLabel(loadedLocale["SIServerLabel"])
username_label := gtk.NewLabel(loadedLocale["SIUsernameLabel"])
password_label := gtk.NewLabel(loadedLocale["SIPasswordLabel"])
nickname_label := gtk.NewLabel(loadedLocale["SINicknameLabel"])
insecure_label := gtk.NewLabel(loadedLocale["SIInsecureLabel"])
insecure_label.SetTooltipText(loadedLocale["SIInsecureLabelTooltip"])
server_entry := gtk.NewEntry()
server_entry.SetHAlign(gtk.AlignEnd)
@@ -83,7 +83,7 @@ func dropToSignInPage(err error) {
form_box.Append(nickname_box)
form_box.Append(insecure_box)
sumbit_btn := gtk.NewButtonWithLabel("Submit")
sumbit_btn := gtk.NewButtonWithLabel(loadedLocale["submit"])
sumbit_btn.ConnectClicked(func() {
conf := new(lambdaConfig)
conf.Server = server_entry.Text()
+1 -2
View File
@@ -2,8 +2,8 @@
package main
import (
"sync"
"sort"
"sync"
)
func rangeOrdered(m *sync.Map, fn func(k, v any) bool) {
@@ -23,4 +23,3 @@ func rangeOrdered(m *sync.Map, fn func(k, v any) bool) {
}
}
}
+139
View File
@@ -0,0 +1,139 @@
package main
// Default language is en_GB
var loadedLocale = make(map[string]string)
var enGB = map[string]string{ // British English
// main.go
"appName": "Lambda",
"cancel": "Cancel",
"submit": "Submit",
"join": "Join",
"send": "Send",
"error": "Error",
"close": "Close",
"userRequested": "User requested",
"configResourceEmptyWarning": "Config resource is empty! Generating a random one",
"attention": "Attention",
"disconnected": "Disconnected: ",
"connecting": "Connecting...",
"milliseconds": "ms",
"KBPerSecond": "KB/s",
"connectedAs": "Connected as ",
"bindedJid": "Binded JID: ",
"usingTLS": "Using TLS: ",
"joinMUCMenu": "Join MUC",
"joinMUCJIDEntry": "MUC JID:",
"joinMUCNickEntry": "Nick:",
"joinMUCDiscoCheck": "Check MUC features before joining",
"joinMUCDiscoCheckTooltip": "If you are creating a MUC through this window then turn this off",
"joinPreviewTitle": "Joining ",
"joinPasswordRequired": "Password required",
"muc_passwordprotected_description": "This MUC is password-protected",
"muc_unsecured_description": "This MUC does not require a password",
"muc_membersonly_description": "Only members can join this MUC",
"muc_open_description": "Anyone can join this MUC",
"muc_moderated_description": "Only members can speak in this MUC",
"muc_unmoderated_description": "Anyone can speak in this MUC",
"muc_nonanonymous_description": "This MUC is non-anonymous, your JID will be visible to other users",
"muc_semianonymous_description": "This MUC is semi-anonymous, only moderators will see your full JID",
"muc_persistent_description": "This MUC is persistent, it will not be deleted when the last user leaves",
"muc_temporary_description": "This MUC is temporary, it will be deleted when the last user leaves",
"muc_public_description": "This MUC can be found in directories and search engines",
"muc_hidden_description": "This MUC is hidden and cannot be found in directories or search engines",
"urn:xmpp:mam_description": "This MUC supports archiving via MAM",
"urn:xmpp:message-moderate_description": "This MUC supports message moderation",
"discoFail": "Failed to get Disco info",
"startDMMenu": "Start DM",
"destroyMUCMenu": "Destroy MUC",
"aboutMenu": "About",
"destroyMUCWarningOne": "Are you sure? This MUC will be gone forever! (a very long time)",
"destroyMUCWarningTwo": "If you wish to continue, type 'I understand'",
"destroyMUCPassword": "I understand",
"destroyMUCActionButton": "Destroy",
"destroyMUCNotOwnerWarning": "You are not an owner of this MUC and thus will most likely not be able to delete it",
"pingBarTooltip": "Ping between you and your XMPP server\nRight-click to see graph",
"pingGraphTitle": "Server latency",
"pingGraphYAxis": "Ping (ms)",
"throughputTooltip": "Throughput of your XMPP connection in KB/s",
"messageEntryPlaceholder": "Say something, what else are you going to do here?",
// gtk-message.go
"unsupportedMessage": "Unsupported message.",
"bannedWidget": " has been banned by ",
"readWidget": " has read to this point",
"isTyping": " is typing...",
"whispers": " whispers",
"noBodySet": "No body set",
"affilChange": "'s affiliation has been changed to ",
"linkPreviewWarning": "This link preview was generated by the client sending it and may not be accurate of the actual website content",
// 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 ",
"participants": "participant(s)",
"versionQueryEmpty": "Client responded with empty version",
"versionQueryError": "Got error trying to get version",
// gtk-signin.go
"SIServerLabel": "Server: ",
"SIUsernameLabel": "Username: ",
"SIPasswordLabel": "Password: ",
"SINicknameLabel": "Nickname: ",
"SIInsecureLabel": "Insecure: (?)",
"SIInsecureLabelTooltip": "Tick this if you need to connect without TLS, usually for connecting to Tor XMPP servers",
}
var kaGE = map[string]string{ // Georgian (Georgia)
}
var roRo = map[string]string{ // Romanian (Romania)
"appName": "Lambda",
"cancel": "Canselează",
"submit": "A preda",
"join": "Intră",
"send": "Trimite",
"error": "Eroare",
"close": "închide",
"userRequested": "Uzator cerut",
"configResourceEmptyWarning": "Resursa configurată este goala! Creiez unu aleatoriu",
"attention": "Atenție",
"disconnected": "Deconectat",
"connecting": "Conectat",
"bindedJid": "Lipit JID",
"joinMUCMenu": "Intră pe MUC",
"joinMUCJIDEntry": "MUC JID:",
"joinMUCNickEntry": "Poreclă:",
"joinMUCDiscoCheck": "Verifica detalile de MUC înainte sa intri",
"joinMUCDiscoCheckTooltip": "Dacă creiezi un MUC prin această oglindă închido",
}
var enUS = enGB // American English
var locales = map[string]map[string]string{
"en_GB": enGB,
"ka_GE": kaGE,
"en_US": enUS,
}
// TODO: Load locale according to user configuration
func init() {
loadedLocale = locales["en_GB"]
}
+369 -133
View File
@@ -20,7 +20,6 @@ import (
"github.com/BurntSushi/toml"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
"mellium.im/xmpp/jid"
"time"
_ "embed"
@@ -40,14 +39,17 @@ 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
// var msgs *gtk.ListBox
var content *gtk.Widgetter
// var tabs map[string]*chatTab = make(map[string]*chatTab)
var tabs sync.Map
var current string
@@ -75,8 +77,10 @@ var pingTimes = [][]float64{}
var clientAssets map[string]gdk.Paintabler = make(map[string]gdk.Paintabler)
var xmlLog *os.File
func init() {
beeep.AppName = "Lambda"
beeep.AppName = loadedLocale["appName"]
go func() {
for fn := range uiQueue {
@@ -91,6 +95,7 @@ func init() {
}
func main() {
pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig()
if err != nil {
@@ -101,7 +106,6 @@ func main() {
if err != nil {
dropToSignInPage(err)
return
// panic(err)
}
_, err = toml.Decode(string(b), &loadedConfig)
@@ -110,22 +114,35 @@ func main() {
}
if loadedConfig.Resource == "" {
fmt.Println("Config resource is empty! Generating a random one")
fmt.Println(loadedLocale["configResourceEmptyWarning"])
loadedConfig.Resource = randomClientResource()
}
if !loadedConfig.Debug {
xmlLog, err = os.CreateTemp("", "xmpp-log")
if err != nil {
panic(err)
}
defer os.Remove(xmlLog.Name())
} else {
xmlLog = os.Stdout
}
config := xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: loadedConfig.Server,
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,
StreamManagementEnable: true,
ConnectTimeout: 300,
StreamLogger: xmlLog,
}
router := xmpp.NewRouter()
@@ -178,7 +195,7 @@ func main() {
}
v := &stanza.Version{}
v = v.SetInfo("Lambda", lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request
v = v.SetInfo(loadedLocale["appName"], lambda_version, runtime.GOOS) // TODO: Allow spoofing on user request
iqResp.Payload = v
s.Send(iqResp)
@@ -203,12 +220,14 @@ func main() {
*/
originator := JidMustParse(m.From).Bare()
glib.IdleAdd(func() {
mStatus.SetText(originator)
})
at := new(Attention)
ok = m.Get(at)
if ok {
beeep.Notify("Attention", fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
beeep.Notify(loadedLocale["attention"], fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
}
// Handle mentions
@@ -252,7 +271,7 @@ func main() {
composing := stanza.StateComposing{}
ok = m.Get(&composing)
if ok && current == JidMustParse(m.From).Bare() {
typingStatus.SetText(fmt.Sprintf("%s is typing...", m.From))
typingStatus.SetText(fmt.Sprintf("%s%s", m.From, loadedLocale["isTyping"]))
return
}
@@ -264,7 +283,6 @@ func main() {
}
glib.IdleAdd(func() {
//uiQueue <- func() {
b := gtk.NewBox(gtk.OrientationVertical, 0)
tab, ok := tabs.Load(originator)
@@ -276,7 +294,9 @@ func main() {
if ok {
typed_tab.msgs.Append(b)
if current == JidMustParse(m.From).Bare() {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
@@ -285,7 +305,6 @@ func main() {
if ok {
b.Append(ba)
}
//}
})
})
@@ -340,7 +359,9 @@ func main() {
if ok {
typed_tab.msgs.Append(b)
if current == muc {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
@@ -361,7 +382,9 @@ func main() {
if ok {
typed_tab.msgs.Append(b)
if current == muc {
scrollToBottomAfterUpdate(scroller)
}
} else {
fmt.Println("Got message when the tab does not exist!")
}
@@ -372,48 +395,13 @@ func main() {
} else { // This is a presence stanza from a regular user
// The code is basically the exact same as above, we just don't check for mucuser
user := jid.MustParse(presence.From).Bare().String()
_, ok := userdevices.Load(user)
_, mok := mucmembers.Load(user)
if !ok && !mok { // FIXME: The initial muc presence gets picked up from this check
ok := createTab(user, false)
if ok {
userdevices.Store(user, userUnit{})
b := gtk.NewLabel(user)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(user, &window.Window)
})
b.AddController(gesture1)
menu.Append(b)
// TODO: Presence handling code goes here
}
}
unit, ok := userdevices.Load(user)
if !ok {
return
}
resource := jid.MustParse(presence.From).Resourcepart()
typed_unit := unit.(userUnit)
if presence.Type != "unavailable" {
typed_unit.Devices.Store(resource, presence)
} else {
typed_unit.Devices.Delete(resource)
}
userdevices.Store(user, typed_unit)
}
time.Sleep(1 * time.Second)
})
c, err := xmpp.NewClient(&config, router, func(err error) {
connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["disconnected"], err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
})
@@ -426,9 +414,11 @@ func main() {
cm := xmpp.NewStreamManager(c, func(c xmpp.Sender) {
fmt.Println("XMPP client connected")
// Ping
go func() {
for {
time.Sleep(5 * time.Second)
go func() {
pingStatus.AddCSSClass("pending")
before := time.Now()
iq := new(stanza.IQ)
@@ -439,20 +429,57 @@ func main() {
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
mychan, err := client.SendIQ(ctx, iq)
if err != nil {
continue
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))
glib.IdleAdd(func() {
pingStatus.RemoveCSSClass("pending")
pingStatus.SetText(fmt.Sprintf("%d %s", delay, loadedLocale["milliseconds"]))
})
}()
}
}()
connectionStatus.SetText(fmt.Sprintf("Connected as %s", JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("Binded JID: %s\nUsing TLS: %t", clientroot.Session.BindJid, clientroot.Session.TlsEnabled))
// Throughput
/*
var oldsize int64
var newsize int64
var diff float64
go func() {
for {
time.Sleep(5 * time.Second)
stat, err := xmlLog.Stat()
if err != nil {
panic(err)
}
newsize = stat.Size()
diff = float64(newsize-oldsize) / 1000
ic := clientAssets["car"]
if diff >= 25 {
ic = clientAssets["car_high"]
}
glib.IdleAdd(func() {
sStatus.SetText(fmt.Sprintf("%.2f%s", diff, loadedLocale["KBPerSecond"]))
sIcon.SetFromPaintable(ic)
})
oldsize = newsize
}
}()
*/
glib.IdleAdd(func() {
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["connectedAs"], JidMustParse(clientroot.Session.BindJid).Bare()))
connectionStatus.SetTooltipText(fmt.Sprintf("%s%s\n%s%t", loadedLocale["bindedJid"], clientroot.Session.BindJid, loadedLocale["usingTLS"], clientroot.Session.TlsEnabled))
connectionIcon.SetFromPaintable(clientAssets["connect"])
})
// Enable carbons
client.SendRaw(fmt.Sprintf(
`<iq xmlns='jabber:client'
@@ -463,6 +490,55 @@ func main() {
</iq>
`, clientroot.Session.BindJid))
// Fetch roster
i, err := stanza.NewIQ(stanza.Attrs{
Type: "get",
})
if err != nil {
panic(err)
}
roster := i.RosterItems()
i.Payload = roster
mychan, err := c.SendIQ(context.TODO(), i)
result := <-mychan
if err == nil {
items, ok := result.Payload.(*stanza.RosterItems)
if ok {
for _, v := range items.Items {
name := v.Name
jid := v.Jid
if name == "" {
name = jid
}
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)
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)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
})
}
}
}
// Join rooms in bookmarks
if loadedConfig.JoinBookmarks {
books, err := stanza.NewItemsRequest("", "urn:xmpp:bookmarks:1", 0)
@@ -473,66 +549,99 @@ 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
name := jid
password := ""
nick := loadedConfig.Nick
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
break
}
}
for _, attr := range node.Attrs {
if attr.Name.Local == "name" {
name = attr.Value
break
}
}
for _, attr := range node.Attrs {
if attr.Name.Local == "autojoin" {
autojoin = attr.Value == "true"
break
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "nick" {
nick = node.Content
break
}
}
for _, node := range node.Nodes {
if node.XMLName.Local == "password" {
password = node.Content
break
}
}
_, 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)
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)
})
b.AddController(gesture1)
menu.Append(b)
}
box.Append(b)
go func() {
new_im := getAvatar(jid, jid)
glib.IdleAdd(func() {
new_im.SetPixelSize(40)
box.Prepend(new_im)
})
}()
box.AddController(gesture1)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
})
}
}
}
}
}
}
})
conc := func() {
time.Sleep(3 * time.Second)
connectionStatus.SetText("Connecting...")
// time.Sleep(3 * time.Second)
connectionStatus.SetText(loadedLocale["connecting"])
connectionIcon.SetFromPaintable(clientAssets["hourglass"])
err = cm.Run()
if err != nil {
fmt.Println(err.Error())
connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
connectionStatus.SetText(fmt.Sprintf("%s%s", loadedLocale["disconnected"], err.Error()))
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
}
}
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
app.ConnectActivate(func() {
go conc()
activate(app)
go conc()
})
if code := app.Run(os.Args); code > 0 {
@@ -551,17 +660,35 @@ func activate(app *gtk.Application) {
the_menu := gio.NewMenu()
fileMenu := gio.NewMenu()
fileMenu.Append("Join MUC", "app.join")
fileMenu.Append("Start DM", "app.dm")
fileMenu.Append("Destroy MUC", "app.destroymuc")
fileMenu.Append(loadedLocale["joinMUCMenu"], "app.join")
fileMenu.Append(loadedLocale["startDMMenu"], "app.dm")
fileMenu.Append(loadedLocale["destroyMUCMenu"], "app.destroymuc")
helpMenu := gio.NewMenu()
helpMenu.Append("About", "app.about")
aboutAction := gio.NewSimpleAction("about", nil)
aboutAction.ConnectActivate(func(p *glib.Variant) {
a := gtk.AboutDialog{}
a.SetVisible(true)
a := gtk.NewAboutDialog()
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.SetComments("yet another XMPP client")
a.SetAuthors([]string{"Sunglocto"})
a.SetLicense("GPL3")
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)
})
destroymucAction := gio.NewSimpleAction("destroymuc", nil)
@@ -571,24 +698,24 @@ func activate(app *gtk.Application) {
cur := cur.(*chatTab)
if cur.isMuc {
win := gtk.NewWindow()
win.SetTitle("Destroy MUC")
win.SetTitle(loadedLocale["destroyMUCMenu"])
win.SetDefaultSize(400, 1)
win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(gtk.NewLabel("Are you sure? This MUC will be gone forever! (a very long time)"))
box.Append(gtk.NewLabel("If you wish to continue, type 'I understand'"))
cancel := gtk.NewButtonWithLabel("Cancel")
box := gtk.NewBox(gtk.OrientationVertical, 10)
box.Append(gtk.NewLabel(loadedLocale["destroyMUCWarningOne"]))
box.Append(gtk.NewLabel(loadedLocale["destroyMUCWarningTwo"]))
cancel := gtk.NewButtonWithLabel(loadedLocale["cancel"])
cancel.ConnectClicked(func() {
win.SetVisible(false)
})
en := gtk.NewEntry()
en.SetPlaceholderText("...")
submit := gtk.NewButtonWithLabel("Destroy")
submit := gtk.NewButtonWithLabel(loadedLocale["destroyMUCActionButton"])
submit.ConnectClicked(func() {
fmt.Println(en.Text())
if en.Text() == "I understand" {
if en.Text() == loadedLocale["destroyMUCPassword"] {
cur, ok := tabs.Load(current)
if ok {
cur := cur.(*chatTab)
@@ -600,11 +727,11 @@ func activate(app *gtk.Application) {
type='set'>
<query xmlns='http://jabber.org/protocol/muc#owner'>
<destroy jid='%s'>
<reason>User requested</reason>
<reason>%s</reason>
</destroy>
</query>
</iq>
`, clientroot.Session.BindJid, current, JidMustParse(clientroot.Session.BindJid).Bare()))
`, clientroot.Session.BindJid, current, JidMustParse(clientroot.Session.BindJid).Bare(), loadedLocale["userRequested"]))
}
}
win.SetVisible(false)
@@ -627,7 +754,7 @@ func activate(app *gtk.Application) {
if mu.MucUserItem.JID != "" {
if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() {
if mu.MucUserItem.Affiliation != "owner" {
box.Append(gtk.NewLabel("You are not an owner of this MUC and thus will most likely not be able to delete it"))
box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"]))
}
// return false
}
@@ -653,12 +780,14 @@ func activate(app *gtk.Application) {
joinAction := gio.NewSimpleAction("join", nil)
joinAction.ConnectActivate(func(p *glib.Variant) {
box := gtk.NewBox(gtk.OrientationVertical, 0)
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
box := gtk.NewBox(gtk.OrientationVertical, 10)
jid_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
nick_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
disco_box := gtk.NewBox(gtk.OrientationHorizontal, 10)
jid_entry := gtk.NewEntry()
nick_entry := gtk.NewEntry()
disco_check := gtk.NewCheckButton()
jid_entry.SetHAlign(gtk.AlignEnd)
jid_entry.SetHExpand(true)
@@ -668,22 +797,37 @@ func activate(app *gtk.Application) {
nick_entry.SetText(loadedConfig.Nick)
jid_box.Append(gtk.NewLabel("MUC JID:"))
create_jid := gtk.NewImageFromPaintable(clientAssets["jabber"])
gesture := gtk.NewGestureClick()
gesture.SetButton(1)
gesture.Connect("pressed", func() {
jidBuilder(jid_entry)
})
create_jid.AddController(gesture)
jid_box.Append(gtk.NewLabel(loadedLocale["joinMUCJIDEntry"]))
jid_box.Append(create_jid)
jid_box.Append(jid_entry)
nick_box.Append(gtk.NewLabel("Nick:"))
nick_box.Append(gtk.NewLabel(loadedLocale["joinMUCNickEntry"]))
nick_box.Append(nick_entry)
disco_check.SetActive(true)
disco_box.Append(gtk.NewLabel(loadedLocale["joinMUCDiscoCheck"]))
disco_box.Append(disco_check)
disco_box.SetTooltipText(loadedLocale["joinMUCDiscoCheckTooltip"])
box.Append(jid_box)
box.Append(nick_box)
box.Append(disco_box)
btn := gtk.NewButtonWithLabel("Submit")
btn := gtk.NewButtonWithLabel(loadedLocale["submit"])
btn.SetVAlign(gtk.AlignBaseline)
box.Append(btn)
win := gtk.NewWindow()
win.SetTitle("Join MUC")
win.SetTitle(loadedLocale["joinMUCMenu"])
win.SetDefaultSize(400, 1)
win.SetResizable(false)
win.SetChild(box)
@@ -691,15 +835,24 @@ func activate(app *gtk.Application) {
btn.ConnectClicked(func() {
t := jid_entry.Text()
_, ok := tabs.Load(t)
jm := func() {
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text())
jm := func(n string, pw string) {
err := joinMuc(client, clientroot.Session.BindJid, t, nick_entry.Text(), pw)
if err != nil {
panic(err)
showErrorDialog(err)
return
}
createTab(t, true)
b := gtk.NewLabel(t)
createTab(t, true, n)
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
go func() {
new_im := getAvatar(t, t)
glib.IdleAdd(func() {
new_im.SetPixelSize(40)
box.Prepend(new_im)
})
}()
b := gtk.NewLabel(n)
box.Append(b)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
@@ -707,10 +860,18 @@ func activate(app *gtk.Application) {
})
b.AddController(gesture1)
menu.Append(b)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}
if !ok {
// First check the MUC's disco and see if it's semianon
if !disco_check.Active() {
jm(t, "")
win.SetVisible(false)
return
}
var res *stanza.DiscoInfo
allowed := true
fmt.Println("Attempting to get Disco info")
@@ -732,55 +893,119 @@ func activate(app *gtk.Application) {
mychan, err := client.SendIQ(ctx, myIQ)
if err == nil {
result := <-mychan
res, ok := result.Payload.(*stanza.DiscoInfo)
res, ok = result.Payload.(*stanza.DiscoInfo)
if ok {
semianon := false
features := res.Features
for _, feature := range features {
if feature.Var == "muc_nonanonymous" {
semianon = false
break
} else if feature.Var == "muc_semianonymous" {
semianon = true
break
}
}
if !semianon {
allowed = false
password_protected := false
password := ""
warning_win := gtk.NewWindow()
warning_win.SetTitle("Warning")
warning_win.SetDefaultSize(400, 400)
warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name))
warning_win.SetDefaultSize(400, 1)
warning_win.SetResizable(false)
warning_box := gtk.NewBox(gtk.OrientationVertical, 0)
warning_label := gtk.NewLabel("This muc is not semi-anonymous. Your JID will be revealed to non-moderators if you join. Continue?")
buttons := gtk.NewBox(gtk.OrientationHorizontal, 10)
buttons := gtk.NewBox(gtk.OrientationHorizontal, 0)
join_button := gtk.NewButtonWithLabel("Join")
join_button.ConnectClicked(func() {
warning_win.SetVisible(false)
jm()
if password_protected {
allowed = 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)
}
jm(res.Identity[0].Name, password)
})
cancel_button := gtk.NewButtonWithLabel("Cancel")
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(clientAssets[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(warning_label)
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))
} else {
showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]))
}
}
}
if allowed {
jm()
jm(res.Identity[0].Name, "")
}
}
win.SetVisible(false)
@@ -794,7 +1019,7 @@ func activate(app *gtk.Application) {
app.AddAction(aboutAction)
app.AddAction(destroymucAction)
the_menu.AppendSubmenu("File", fileMenu)
the_menu.AppendSubmenu("MUC", fileMenu)
the_menu.AppendSubmenu("Help", helpMenu)
the_menuBar := gtk.NewPopoverMenuBarFromModel(the_menu)
@@ -823,7 +1048,7 @@ func activate(app *gtk.Application) {
cBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
connectionIcon = gtk.NewImageFromPaintable((clientAssets["disconnect"]))
connectionIcon.AddCSSClass("icon")
connectionStatus = gtk.NewLabel("Disconnected")
connectionStatus = gtk.NewLabel(loadedLocale["disconnected"])
cBox.Append(connectionIcon)
cBox.Append(connectionStatus)
@@ -850,13 +1075,13 @@ func activate(app *gtk.Application) {
statBar.Append(mBox)
pBox := gtk.NewBox(gtk.OrientationHorizontal, 0)
pBox.SetTooltipText("Ping between you and your XMPP server\nRight-click to see graph")
pBox.SetTooltipText(loadedLocale["pingBarTooltip"])
gesture := gtk.NewGestureClick()
gesture.SetButton(3)
gesture.Connect("pressed", func() {
opt := charts.NewLineChartOptionWithData(pingTimes)
opt.Title = charts.TitleOption{
Text: "Server latency",
Text: loadedLocale["pingGraphTitle"],
}
/*
opt.XAxis.Labels = []string{
@@ -865,7 +1090,7 @@ func activate(app *gtk.Application) {
}*/
opt.Legend = charts.LegendOption{
SeriesNames: []string{
"Ping (ms)",
loadedLocale["pingGraphYAxis"],
},
}
@@ -892,7 +1117,7 @@ func activate(app *gtk.Application) {
i := gtk.NewPictureForPaintable(gdk.NewTextureForPixbuf(loader.Pixbuf()))
win := gtk.NewWindow()
win.SetDefaultSize(600, 400)
win.SetTitle("Server latency")
win.SetTitle(loadedLocale["pingGraphTitle"])
box := gtk.NewBox(gtk.OrientationVertical, 0)
box.Append(i)
win.SetChild(box)
@@ -907,6 +1132,17 @@ 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)
@@ -937,11 +1173,11 @@ func activate(app *gtk.Application) {
entry_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
oob_en := gtk.NewEntry()
oob_en.SetPlaceholderText("Embed URL")
oob_en.SetPlaceholderText("URL")
message_en = gtk.NewEntry()
message_en.SetPlaceholderText("Say something, what else are you gonna do here?")
b := gtk.NewButtonWithLabel("Send")
message_en.SetPlaceholderText(loadedLocale["messageEntryPlaceholder"])
b := gtk.NewButtonWithLabel(loadedLocale["send"])
sendtxt := func() {
t := message_en.Text()
@@ -989,7 +1225,7 @@ func activate(app *gtk.Application) {
err := sendMessage(client, current, message_type, t, "", "", exts)
if err != nil {
panic(err) // TODO: Show error message via GTK
showErrorDialog(err)
}
message_en.SetText("")
scrollToBottomAfterUpdate(scroller)
+7
View File
@@ -73,3 +73,10 @@
.None_CVD {
}
.link_preview {
color: white;
background-color: grey;
border-radius: 5px;
padding: 5px;
}
+2 -2
View File
@@ -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) {
+2
View File
@@ -9,6 +9,7 @@ import (
type chatTab struct {
isMuc bool
msgs *gtk.ListBox
name string
}
type lambdaConfig struct {
@@ -21,6 +22,7 @@ type lambdaConfig struct {
JoinBookmarks bool
CVD color.CVD
Identicons bool
Debug bool
}
type mucUnit struct {
+1 -1
View File
@@ -1,3 +1,3 @@
package main
var lambda_version string = "26w15a"
var lambda_version string = "26w17a"
+62 -4
View File
@@ -1,7 +1,6 @@
package main
import (
"fmt"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
@@ -28,10 +27,11 @@ func sendMessage(c xmpp.Sender, sendTo string, msgType stanza.StanzaType, body s
}
// Joins a MUC
func joinMuc(c xmpp.Sender, jid string, muc string, nick string) error {
func joinMuc(c xmpp.Sender, jid string, muc string, nick string, password string) error {
var joinPresence stanza.Presence
addr := muc + "/" + nick
fmt.Println(addr)
joinPresence := stanza.Presence{
if password == "" {
joinPresence = stanza.Presence{
Attrs: stanza.Attrs{
From: jid,
To: addr,
@@ -40,6 +40,64 @@ func joinMuc(c xmpp.Sender, jid string, muc string, nick string) error {
&stanza.MucPresence{},
},
}
} else {
joinPresence = stanza.Presence{
Attrs: stanza.Attrs{
From: jid,
To: addr,
},
Extensions: []stanza.PresExtension{
&stanza.MucPresence{
Password: password,
},
},
}
}
err := client.Send(joinPresence)
if err != nil {
return err
}
return nil
}
func joinMucWithoutHistory(c xmpp.Sender, jid string, muc string, nick string, password string) error {
var joinPresence stanza.Presence
addr := muc + "/" + nick
if password == "" {
joinPresence = stanza.Presence{
Attrs: stanza.Attrs{
From: jid,
To: addr,
},
Extensions: []stanza.PresExtension{
&stanza.MucPresence{
History: stanza.History{
MaxChars: stanza.NewNullableInt(0),
MaxStanzas: stanza.NewNullableInt(0),
Seconds: stanza.NewNullableInt(0),
},
},
},
}
} else {
joinPresence = stanza.Presence{
Attrs: stanza.Attrs{
From: jid,
To: addr,
},
Extensions: []stanza.PresExtension{
&stanza.MucPresence{
Password: password,
History: stanza.History{
MaxChars: stanza.NewNullableInt(0),
MaxStanzas: stanza.NewNullableInt(0),
Seconds: stanza.NewNullableInt(0),
},
},
},
}
}
err := client.Send(joinPresence)
if err != nil {
+20
View File
@@ -0,0 +1,20 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
)
type LinkPreview struct {
stanza.MsgExtension
XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# Description"`
About string `xml:"https://ogp.me/ns#,attr"`
Title string `xml:"https://ogp.me/ns# title"`
Description string `xml:"https://ogp.me/ns# description"`
Image string `xml:"https://ogp.me/ns# image"`
URL string `xml:"https://ogp.me/ns# url"`
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", Local: "Description"}, LinkPreview{})
}
+14
View File
@@ -10,7 +10,21 @@ import (
type VCard struct {
XMLName xml.Name `xml:"vcard-temp vCard"`
FirstName string `xml:"FN"`
LastName string `xml:"N>FAMILY"`
GivenName string `xml:"N>GIVEN"`
MiddleName string `xml:"N>MIDDLE"`
Nickname string `xml:"NICKNAME"`
URI string `xml:"URL"`
Birthday string `xml:"BDAY"`
OrgName string `xml:"ORG>ORGNAME"`
OrgUnit string `xml:"ORG>ORGUNIT"`
Title string `xml:"TITLE"`
Role string `xml:"ROLE"`
Description string `xml:"DESC"`
Jid string `xml:"JABBERID"`
Photo Photo `xml:"PHOTO"`
Email string `xml:"EMAIL>USERID"`
ResultSet *stanza.ResultSet `xml:"set,omitempty"`
}