ban, kick and change role&affil menu, status icons in muc, blocking avatars that are invalid so we dont fetch them over and over again
64
assets.go
@@ -39,6 +39,27 @@ var outcastMedalB64 string = base64.StdEncoding.EncodeToString(outcastMedalBytes
|
||||
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)
|
||||
@@ -104,7 +125,6 @@ func init() {
|
||||
loader.Close()
|
||||
clientAssets["DefaultAvatar"] = gdk.NewTextureForPixbuf(loader.Pixbuf())
|
||||
|
||||
|
||||
loader = gdkpixbuf.NewPixbufLoader()
|
||||
|
||||
failedData, _ := base64.StdEncoding.DecodeString(failedB64)
|
||||
@@ -272,4 +292,46 @@ func init() {
|
||||
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())
|
||||
}
|
||||
|
||||
BIN
assets/status_away.png
Normal file
|
After Width: | Height: | Size: 794 B |
BIN
assets/status_busy.png
Normal file
|
After Width: | Height: | Size: 751 B |
BIN
assets/status_chatty.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/status_online.png
Normal file
|
After Width: | Height: | Size: 722 B |
BIN
assets/status_xa.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
2
cache.go
@@ -19,7 +19,7 @@ import (
|
||||
var textureCache = make(map[string]gdk.Paintabler)
|
||||
|
||||
// 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)
|
||||
var invalidImages = make(map[string]bool)
|
||||
|
||||
func ensureCache() (string, error) {
|
||||
cachePath := configdir.LocalCache("lambda-im")
|
||||
|
||||
BIN
failed_load.png
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
162
gtk-helpers.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/diamondburned/gotk4/pkg/gdk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/glib/v2"
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"github.com/diamondburned/gotk4/pkg/pango"
|
||||
@@ -82,7 +83,7 @@ func switchToTab(jid string, w *gtk.Window) {
|
||||
nick_label.SetOpacity(0.5)
|
||||
}
|
||||
|
||||
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\nRight-click for more information", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation))
|
||||
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\nClick for more information", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation))
|
||||
userbox.Append(nick_label)
|
||||
|
||||
var hats Hats
|
||||
@@ -95,6 +96,13 @@ func switchToTab(jid string, w *gtk.Window) {
|
||||
}
|
||||
}
|
||||
|
||||
status := gtk.NewImageFromPaintable(clientAssets["status_"+string(u.Show)])
|
||||
status.SetTooltipText(string(u.Show))
|
||||
|
||||
status.SetHAlign(gtk.AlignEnd)
|
||||
// medal.SetHExpand(true)
|
||||
userbox.Prepend(status)
|
||||
|
||||
medal := gtk.NewImageFromPaintable(clientAssets[mu.MucUserItem.Affiliation])
|
||||
medal.SetTooltipText(mu.MucUserItem.Affiliation)
|
||||
|
||||
@@ -103,7 +111,145 @@ func switchToTab(jid string, w *gtk.Window) {
|
||||
userbox.Append(medal)
|
||||
|
||||
gesture := gtk.NewGestureClick()
|
||||
gesture.SetButton(3) // Right click
|
||||
gesture.SetButton(1)
|
||||
|
||||
mod_gesture := gtk.NewGestureClick()
|
||||
mod_gesture.SetButton(3)
|
||||
|
||||
popover := gtk.NewPopover()
|
||||
popover.SetHasArrow(false)
|
||||
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")
|
||||
|
||||
kb.ConnectClicked(func() {
|
||||
client.SendRaw(fmt.Sprintf(`
|
||||
<iq from='%s'
|
||||
id='kick1'
|
||||
to='%s'
|
||||
type='set'>
|
||||
<query xmlns='http://jabber.org/protocol/muc#admin'>
|
||||
<item nick='%s' role='none'>
|
||||
</item>
|
||||
</query>
|
||||
</iq>
|
||||
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource))
|
||||
})
|
||||
|
||||
bb.ConnectClicked(func() {
|
||||
var mu MucUser
|
||||
ok = u.Get(&mu)
|
||||
if ok {
|
||||
if mu.MucUserItem.JID != "" {
|
||||
client.SendRaw(fmt.Sprintf(`
|
||||
<iq from='%s'
|
||||
id='ban1'
|
||||
to='%s'
|
||||
type='set'>
|
||||
<query xmlns='http://jabber.org/protocol/muc#admin'>
|
||||
<item affiliation='outcast' jid='%s'/>
|
||||
</query>
|
||||
</iq>
|
||||
`, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare()))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ab.ConnectClicked(func() {
|
||||
var mu MucUser
|
||||
ok = u.Get(&mu)
|
||||
if ok {
|
||||
if mu.MucUserItem.JID != "" {
|
||||
win := gtk.NewWindow()
|
||||
win.SetDefaultSize(400, 1)
|
||||
win.SetResizable(false)
|
||||
|
||||
box := gtk.NewBox(gtk.OrientationVertical, 0)
|
||||
box.Append(gtk.NewLabel("Set "+JidMustParse(u.From).Resource+"'s affiliation"))
|
||||
|
||||
the_entry := gtk.NewEntry()
|
||||
the_entry.SetText(mu.MucUserItem.Affiliation)
|
||||
|
||||
submit := gtk.NewButtonWithLabel("Submit")
|
||||
submit.ConnectClicked(func() {
|
||||
client.SendRaw(fmt.Sprintf(`
|
||||
<iq from='%s'
|
||||
id='ba1'
|
||||
to='%s'
|
||||
type='set'>
|
||||
<query xmlns='http://jabber.org/protocol/muc#admin'>
|
||||
<item affiliation='%s' jid='%s'/>
|
||||
</query>
|
||||
</iq>
|
||||
`, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare()))
|
||||
win.SetVisible(false)
|
||||
})
|
||||
|
||||
box.Append(the_entry)
|
||||
box.Append(submit)
|
||||
|
||||
win.SetChild(box)
|
||||
win.SetVisible(true)
|
||||
}}
|
||||
})
|
||||
|
||||
|
||||
rb.ConnectClicked(func() {
|
||||
var mu MucUser
|
||||
ok = u.Get(&mu)
|
||||
if ok {
|
||||
if mu.MucUserItem.JID != "" {
|
||||
win := gtk.NewWindow()
|
||||
win.SetDefaultSize(400, 1)
|
||||
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"))
|
||||
|
||||
the_entry := gtk.NewEntry()
|
||||
the_entry.SetText(mu.MucUserItem.Role)
|
||||
|
||||
submit := gtk.NewButtonWithLabel("Submit")
|
||||
submit.ConnectClicked(func() {
|
||||
|
||||
client.SendRaw(fmt.Sprintf(`
|
||||
<iq from='%s'
|
||||
id='kick1'
|
||||
to='%s'
|
||||
type='set'>
|
||||
<query xmlns='http://jabber.org/protocol/muc#admin'>
|
||||
<item nick='%s' role='%s'>
|
||||
</item>
|
||||
</query>
|
||||
</iq>
|
||||
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text()))
|
||||
})
|
||||
|
||||
box.Append(the_entry)
|
||||
box.Append(submit)
|
||||
|
||||
win.SetChild(box)
|
||||
win.SetVisible(true)
|
||||
}}
|
||||
})
|
||||
|
||||
rc_box.Append(bb)
|
||||
rc_box.Append(kb)
|
||||
rc_box.Append(ab)
|
||||
rc_box.Append(rb)
|
||||
|
||||
popover.SetChild(rc_box)
|
||||
|
||||
mod_gesture.Connect("pressed", func(n_press, x, y int) {
|
||||
rect := gdk.NewRectangle(x, y, 1, 1)
|
||||
popover.SetPointingTo(&rect)
|
||||
popover.Popup()
|
||||
})
|
||||
|
||||
gesture.Connect("pressed", func(n_press, x, y int) {
|
||||
win := gtk.NewWindow()
|
||||
@@ -218,8 +364,13 @@ func switchToTab(jid string, w *gtk.Window) {
|
||||
version := ver.Version
|
||||
os := ver.OS
|
||||
|
||||
ver_text.SetText(fmt.Sprintf("%s %s %s", name, version, os))
|
||||
ver_text.RemoveCSSClass("visitor")
|
||||
vr := fmt.Sprintf("%s %s %s", name, version, os)
|
||||
if name == "" && version == "" && os == "" {
|
||||
ver_text.SetText("Client responded with empty version")
|
||||
} 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.SetTooltipText(result.Error.Reason + ": " + result.Error.Text)
|
||||
@@ -262,7 +413,10 @@ func switchToTab(jid string, w *gtk.Window) {
|
||||
win.SetTransientFor(win)
|
||||
win.Present()
|
||||
})
|
||||
|
||||
userbox.AddController(gesture)
|
||||
userbox.AddController(mod_gesture)
|
||||
|
||||
if mu.MucUserItem.Role == "moderator" {
|
||||
gen.Prepend(userbox)
|
||||
} else {
|
||||
|
||||
31
main.go
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"context"
|
||||
@@ -188,8 +188,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
pretty.Println(m)
|
||||
|
||||
e := stanza.PubSubEvent{}
|
||||
ok = m.Get(&e)
|
||||
if ok {
|
||||
@@ -211,6 +209,15 @@ func main() {
|
||||
beeep.Notify("Attention", fmt.Sprintf("%s: %s", JidMustParse(m.From).Resource, m.Body), commentBytes) // TODO: Use localpart if DM
|
||||
}
|
||||
|
||||
// Handle mentions
|
||||
for _, ext := range m.Extensions {
|
||||
mention, ok := ext.(*Mention)
|
||||
if ok {
|
||||
pretty.Println(mention)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sc := new(SentCarbon)
|
||||
ok = m.Get(sc)
|
||||
if ok {
|
||||
@@ -492,7 +499,7 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
conc := func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
connectionStatus.SetText("Connecting...")
|
||||
connectionIcon.SetFromPaintable(clientAssets["hourglass"])
|
||||
@@ -503,10 +510,13 @@ func main() {
|
||||
connectionStatus.SetText(fmt.Sprintf("Disconnected: %s", err.Error()))
|
||||
connectionIcon.SetFromPaintable(clientAssets["disconnect"])
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
app := gtk.NewApplication("net.sunglocto.lambda", gio.ApplicationFlagsNone)
|
||||
app.ConnectActivate(func() { activate(app) })
|
||||
app.ConnectActivate(func() {
|
||||
go conc()
|
||||
activate(app)
|
||||
})
|
||||
|
||||
if code := app.Run(os.Args); code > 0 {
|
||||
os.Exit(code)
|
||||
@@ -870,8 +880,15 @@ func activate(app *gtk.Application) {
|
||||
}
|
||||
|
||||
if strings.Contains(t, "@everyone") {
|
||||
start := strings.Index(t, "@everyone")
|
||||
end := start + len("@everyone")
|
||||
|
||||
new_mention := new(Mention)
|
||||
new_mention.Mentions = "urn:xmpp:mentions:0#channel"
|
||||
new_mention.Type = "urn:xmpp:mentions:0#channel"
|
||||
|
||||
new_mention.Begin = start
|
||||
new_mention.End = end
|
||||
|
||||
exts = append(exts, new_mention)
|
||||
} else if strings.Contains(t, "@here") {
|
||||
new_attention := new(Attention)
|
||||
|
||||
4
types.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/gotk4/pkg/gtk/v4"
|
||||
"mellium.im/xmpp/color"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -18,10 +19,11 @@ type lambdaConfig struct {
|
||||
Insecure bool
|
||||
Nick string
|
||||
JoinBookmarks bool
|
||||
CVD color.CVD
|
||||
}
|
||||
|
||||
type mucUnit struct {
|
||||
// key: OccupantID
|
||||
// key: Resource
|
||||
// value: last user presence
|
||||
Members sync.Map
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var lambda_version string = "0.1.0"
|
||||
var lambda_version string = "26w11a"
|
||||
|
||||
@@ -10,13 +10,12 @@ import (
|
||||
|
||||
type Mention struct {
|
||||
stanza.MsgExtension
|
||||
XMLName xml.Name `xml:"urn:xmpp:mentions:0 mention"`
|
||||
Mentions string `xml:"mentions,attr,omitempty"`
|
||||
URI string `xml:"uri,attr,omitempty"`
|
||||
Begin int `xml:"begin,attr,omitempty"`
|
||||
End int `xml:"end,attr,omitempty"`
|
||||
OccupantID string `xml:"occupantid,attr,omitempty"`
|
||||
JID string `xml:"ji,attr,omitempty"`
|
||||
XMLName xml.Name `xml:"urn:xmpp:mentions:0 mention"`
|
||||
URI string `xml:"uri,attr,omitempty"`
|
||||
Begin int `xml:"begin,attr,omitempty"`
|
||||
End int `xml:"end,attr,omitempty"`
|
||||
Type string `xml:"type,attr"`
|
||||
Target string `xml:"target,attr,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||