Rest in peace Dolly Patron

This commit is contained in:
2026-08-25 20:17:23 +01:00
parent 2717137270
commit a6e74ec808
52 changed files with 675 additions and 462 deletions
+5 -1
View File
@@ -2,6 +2,10 @@
an XMPP client an XMPP client
Experimental XMPP client written in Go, with the GTK4 UI toolkit and the Fluux IO XMPP libraries.
icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im)) icons are from Psi+ ([https://github.com/psi-im](https://github.com/psi-im))
additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) additional icons are by Mark James's Silk Icon Set [https://github.com/markjames/famfamfam-silk-icons](https://github.com/markjames/famfamfam-silk-icons) and the [Haiku project](https://www.haiku-os.org/)
pepper & carrot default avatar is from [Pepper&Carrot](https://peppercarrot.com) - created by David Revoy, licensed under Creative Commons Attribution 4.0. Change made: cropped and made black & white
+66 -1
View File
@@ -7,7 +7,9 @@ import (
"sync" "sync"
) )
//go:embed debug.png // If you want to change the default profile picture used when Identicons are disabled, you can change the below line to an image in the repository and recompile Lambda.
//go:embed debug_psi.png
var defaultAvatarBytes []byte var defaultAvatarBytes []byte
//go:embed failed_load.png //go:embed failed_load.png
@@ -159,6 +161,53 @@ var solarBytes []byte
//go:embed assets/timeline_marker.png //go:embed assets/timeline_marker.png
var timelineMarkerBytes []byte var timelineMarkerBytes []byte
//go:embed assets/cap.png
var capIconBytes []byte
// client icons
//go:embed assets/client_icons/monocles.png
var monoclesCapBytes []byte
//go:embed assets/client_icons/gajim.png
var gajimCapBytes []byte
//go:embed assets/client_icons/conversations.png
var conversationsCapBytes []byte
//go:embed assets/client_icons/dino.png
var dinoCapBytes []byte
//go:embed assets/client_icons/blabber.png
var blabberCapBytes []byte
//go:embed assets/client_icons/profanity.png
var profanityCapBytes []byte
//go:embed assets/client_icons/poezio.png
var poezioCapBytes []byte
//go:embed assets/client_icons/mcabber.png
var mcabberCapBytes []byte
//go:embed assets/client_icons/converse.png
var converseJSCapBytes []byte
//go:embed assets/client_icons/pidgin.png
var pidginCapBytes []byte
//go:embed assets/client_icons/anotherim.png
var anotherImCapBytes []byte
//go:embed assets/client_icons/movim.png
var movimCapBytes []byte
//go:embed assets/client_icons/yaxim.png
var yaximCapBytes []byte
//go:embed assets/client_icons/psi.png
var psiCapBytes []byte
var clientAssets sync.Map var clientAssets sync.Map
func loadAsset(key string, data []byte) { func loadAsset(key string, data []byte) {
@@ -234,6 +283,22 @@ func init() {
"update": updateBytes, "update": updateBytes,
"solar": solarBytes, "solar": solarBytes,
"timeline_marker": timelineMarkerBytes, "timeline_marker": timelineMarkerBytes,
"cap": capIconBytes,
"https://monocles.eu": monoclesCapBytes,
"https://gajim.org": gajimCapBytes,
"http://conversations.im": conversationsCapBytes,
"https://dino.im": dinoCapBytes,
"http://blabber.im": blabberCapBytes,
"http://profanity-im.github.io": profanityCapBytes,
"https://poez.io": poezioCapBytes,
"http://mcabber.com/caps": mcabberCapBytes,
"https://conversejs.org": converseJSCapBytes,
"http://pidgin.im/": pidginCapBytes,
"https://dev.narayana.im/narayana/Conversations-Classic": anotherImCapBytes,
"https://movim.eu/": movimCapBytes,
"https://yaxim.org/": yaximCapBytes,
"https://psi-im.org": psiCapBytes,
} { } {
loadAsset(key, data) loadAsset(key, data)
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

+26
View File
@@ -0,0 +1,26 @@
package stanza
import (
"encoding/xml"
)
// Capabilities
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
// and peer servers do not need to send service discovery requests each time they connect."
// This is not a stream feature but a way to let client cache server disco info.
// Moved from stream_features.go and made it a presence extension as per spec
type Caps struct {
PresExtension
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
Hash string `xml:"hash,attr"`
Node string `xml:"node,attr"`
Ver string `xml:"ver,attr"`
Ext string `xml:"ext,attr,omitempty"`
}
func init() {
TypeRegistry.MapExtension(PKTPresence, xml.Name{Space: "http://jabber.org/protocol/caps", Local: "c"}, Caps{})
}
-13
View File
@@ -42,19 +42,6 @@ func (streamFeatureDecoder) decode(p *xml.Decoder, se xml.StartElement) (StreamF
return packet, err return packet, err
} }
// Capabilities
// Reference: https://xmpp.org/extensions/xep-0115.html#stream
// "A server MAY include its entity capabilities in a stream feature element so that connecting clients
// and peer servers do not need to send service discovery requests each time they connect."
// This is not a stream feature but a way to let client cache server disco info.
type Caps struct {
XMLName xml.Name `xml:"http://jabber.org/protocol/caps c"`
Hash string `xml:"hash,attr"`
Node string `xml:"node,attr"`
Ver string `xml:"ver,attr"`
Ext string `xml:"ext,attr,omitempty"`
}
// ============================================================================ // ============================================================================
// Supported Stream Features // Supported Stream Features
+167 -79
View File
@@ -4,6 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"image"
"image/color"
"image/png"
"strconv"
"github.com/boxes-ltd/imaging" "github.com/boxes-ltd/imaging"
"github.com/crazy3lf/colorconv" "github.com/crazy3lf/colorconv"
"github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gdk/v4"
@@ -14,20 +19,45 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rrivera/identicon" "github.com/rrivera/identicon"
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
"image"
"image/color"
"image/png"
xmpp_color "mellium.im/xmpp/color" xmpp_color "mellium.im/xmpp/color"
"strconv"
) )
func init() { func createTabBox(name, jid, av_hash string, removable bool) *gtk.Box {
box := gtk.NewBox(gtk.OrientationHorizontal, 10)
name_label := gtk.NewLabel(name)
left_click_gesture := gtk.NewGestureClick()
left_click_gesture.SetButton(1)
left_click_gesture.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
box.AddController(left_click_gesture)
if removable {
right_click_gesture := gtk.NewGestureClick()
right_click_gesture.SetButton(3)
right_click_gesture.Connect("pressed", func() {
removeTab(jid, &window.Window)
box.SetVisible(false)
})
box.AddController(right_click_gesture)
}
box.Append(name_label)
image := gtk.NewImage()
image.SetPixelSize(40)
go getAvatar(jid, av_hash, image)
box.Prepend(image)
return box
} }
func showUserWindow(u stanza.Presence, custom_nick string) { func showUserWindow(u stanza.Presence, custom_nick string) {
var ocu OccupantID var ocu OccupantID
u.Get(&ocu) u.Get(&ocu)
var caps stanza.Caps
u.Get(&caps)
var id string var id string
id = JidMustParse(u.From).Resource id = JidMustParse(u.From).Resource
@@ -67,7 +97,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) {
} }
if loadedConfig.BlockingOI[ocu.ID] { if loadedConfig.BlockingOI[ocu.ID] {
blocked_note := gtk.NewLabel("You have blocked this user!") blocked_note := gtk.NewLabel(loadedLocale["blockedUserNotice"])
blocked_note.AddCSSClass("blocked") blocked_note.AddCSSClass("blocked")
profile_box.Prepend(blocked_note) profile_box.Prepend(blocked_note)
} }
@@ -82,7 +112,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) {
Type: "get", Type: "get",
From: clientroot.Session.BindJid, From: clientroot.Session.BindJid,
To: u.From, To: u.From,
Id: uuid.New().String(), Id: uuid.NewString(),
Lang: "en", Lang: "en",
}) })
@@ -135,7 +165,7 @@ func showUserWindow(u stanza.Presence, custom_nick string) {
Type: "get", Type: "get",
From: clientroot.Session.BindJid, From: clientroot.Session.BindJid,
To: u.From, To: u.From,
Id: uuid.New().String(), Id: uuid.NewString(),
Lang: "en", Lang: "en",
}) })
@@ -210,30 +240,43 @@ func showUserWindow(u stanza.Presence, custom_nick string) {
mmmm, ok := mmm.Load(id) mmmm, ok := mmm.Load(id)
im := gtk.NewImage() im := gtk.NewImage()
im.SetPixelSize(160)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
createIdenticon(u.From, false, im)
if ok { if ok {
pres := mmmm.(stanza.Presence) pres := mmmm.(stanza.Presence)
var vu VCardUpdate var vu VCardUpdate
pres.Get(&vu) pres.Get(&vu)
if vu.Photo != "" { if vu.Photo != "" {
getAvatar(u.From, vu.Photo, im) getAvatar(u.From, vu.Photo, im)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
} else {
createIdenticon(u.From, false, im)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
} }
} else {
createIdenticon(u.From, false, im)
im.SetPixelSize(80)
im.AddCSSClass("author_img")
profile_box.Prepend(im)
} }
}() }()
if caps.Hash != "" {
cap_box := gtk.NewBox(gtk.OrientationHorizontal, 2)
cap_box.SetHAlign(gtk.AlignCenter)
icon := gtk.NewImageFromPaintable(clientAssetsLoad("cap"))
icon.SetCursor(gdk.NewCursorFromName("pointer", nil))
cap_box.Append(icon)
cap_box.Append(gtk.NewLabel(fmt.Sprintf("Entity capabilities: Node %s", caps.Node)))
hasher := gtk.NewLabel(fmt.Sprintf("Using %s hash: %s", caps.Hash, caps.Ver))
hasher.SetVisible(false)
cap_box.Append(hasher)
left_click_gesture := gtk.NewGestureClick()
left_click_gesture.SetButton(1)
left_click_gesture.Connect("pressed", func() {
hasher.SetVisible(!(hasher.Visible()))
})
icon.AddController(left_click_gesture)
profile_box.Append(cap_box)
}
win.SetChild(profile_box) win.SetChild(profile_box)
win.SetTransientFor(win) win.SetTransientFor(win)
win.Present() win.Present()
@@ -311,37 +354,30 @@ func switchToTab(jid string, w *gtk.Window) {
typed_tab := tab.(*chatTab) typed_tab := tab.(*chatTab)
scroller.SetChild(typed_tab.msgs) scroller.SetChild(typed_tab.msgs)
typingStatus.SetText("") typingStatus.SetText("")
createFailBox := func() {
box := gtk.NewBox(gtk.OrientationVertical, 10)
failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail"))
failed_icon.SetPixelSize(100)
box.Append(failed_icon)
jid_label := gtk.NewLabel(jid)
jid_label.AddCSSClass("author")
jid_label.AddCSSClass("jid")
box.Append(jid_label)
label := gtk.NewLabel(loadedLocale["mucLoadError"])
label.SetWrap(true)
box.Append(label)
memberList.SetChild(box)
}
if typed_tab.isMuc { if typed_tab.isMuc {
m, ok := mucmembers.Load(jid) m, ok := mucmembers.Load(jid)
if !ok { if !ok {
box := gtk.NewBox(gtk.OrientationVertical, 10) createFailBox()
failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail"))
failed_icon.SetPixelSize(100)
box.Append(failed_icon)
jid_label := gtk.NewLabel(jid)
jid_label.AddCSSClass("author")
jid_label.AddCSSClass("jid")
box.Append(jid_label)
label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.")
label.SetWrap(true)
box.Append(label)
memberList.SetChild(box)
return return
} }
ma, ok := m.(mucUnit) ma, ok := m.(mucUnit)
if !ok { if !ok {
box := gtk.NewBox(gtk.OrientationVertical, 10) createFailBox()
failed_icon := gtk.NewImageFromPaintable(clientAssetsLoad("fail"))
failed_icon.SetPixelSize(100)
box.Append(failed_icon)
jid_label := gtk.NewLabel(jid)
jid_label.AddCSSClass("author")
jid_label.AddCSSClass("jid")
box.Append(jid_label)
label := gtk.NewLabel("There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.")
label.SetWrap(true)
box.Append(label)
memberList.SetChild(box)
return return
} }
mm := ma.Members mm := ma.Members
@@ -370,11 +406,6 @@ func switchToTab(jid string, w *gtk.Window) {
gen.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) gen.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
} }
//id := ocu.ID
//if id == "" {
// id := JidMustParse(u.From).Resource
//}
nick_label := gtk.NewLabel(JidMustParse(u.From).Resource) nick_label := gtk.NewLabel(JidMustParse(u.From).Resource)
var custom_nick string var custom_nick string
@@ -403,7 +434,7 @@ func switchToTab(jid string, w *gtk.Window) {
nick_label.SetOpacity(0.5) nick_label.SetOpacity(0.5)
} }
userbox.SetTooltipText(fmt.Sprintf("%s\n%s\n%s\n%s", u.From, mu.MucUserItem.Role, mu.MucUserItem.Affiliation, loadedLocale["clickForMoreInfo"])) // 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) userbox.Append(nick_label)
var hats Hats var hats Hats
@@ -418,7 +449,6 @@ func switchToTab(jid string, w *gtk.Window) {
xc := xmpp_color.String(hat.URI, 255, loadedConfig.CVD) xc := xmpp_color.String(hat.URI, 255, loadedConfig.CVD)
r, g, b, _ := xc.RGBA() r, g, b, _ := xc.RGBA()
val, _, _ = colorconv.RGBToHSV(uint8(r), uint8(g), uint8(b)) val, _, _ = colorconv.RGBToHSV(uint8(r), uint8(g), uint8(b))
} }
tB := tagBytes tB := tagBytes
img, _, _ := image.Decode(bytes.NewReader(tB)) img, _, _ := image.Decode(bytes.NewReader(tB))
@@ -452,12 +482,26 @@ func switchToTab(jid string, w *gtk.Window) {
userbox.Append(status_message) userbox.Append(status_message)
} }
end_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
var caps stanza.Caps
ok = u.Get(&caps)
if ok {
client_icon := gtk.NewImageFromPaintable(clientAssetsLoad(caps.Node))
//client_icon.SetHAlign(gtk.AlignEnd)
//client_icon.SetHExpand(true)
end_box.Append(client_icon)
}
medal := gtk.NewImageFromPaintable(clientAssetsLoad(mu.MucUserItem.Affiliation)) medal := gtk.NewImageFromPaintable(clientAssetsLoad(mu.MucUserItem.Affiliation))
medal.SetTooltipText(mu.MucUserItem.Affiliation) medal.SetTooltipText(mu.MucUserItem.Affiliation)
medal.SetHAlign(gtk.AlignEnd) //medal.SetHAlign(gtk.AlignEnd)
medal.SetHExpand(true) //medal.SetHExpand(true)
userbox.Append(medal) end_box.Append(medal)
end_box.SetHAlign(gtk.AlignEnd)
end_box.SetHExpand(true)
userbox.Append(end_box)
if loadedConfig.ShowAvatarsInMemberList { if loadedConfig.ShowAvatarsInMemberList {
default_av := gtk.NewImage() default_av := gtk.NewImage()
@@ -479,6 +523,52 @@ func switchToTab(jid string, w *gtk.Window) {
} }
} }
userbox.SetHasTooltip(true)
userbox.Connect("query-tooltip", func(
x int,
y int,
keyboardMode bool,
tooltip *gtk.Tooltip,
) bool {
box := gtk.NewBox(gtk.OrientationHorizontal, 4)
av_box := gtk.NewBox(gtk.OrientationHorizontal, 0)
if loadedConfig.ShowAvatarsInMemberList {
default_av := gtk.NewImage()
default_av.SetPixelSize(100)
createIdenticon(u.From, false, default_av)
av_box.Prepend(default_av)
var vcu VCardUpdate
ok = u.Get(&vcu)
if ok {
photo := vcu.Photo
go func() {
glib.IdleAdd(func() {
new_im := gtk.NewImage()
new_im.SetPixelSize(100)
av_box.Remove(default_av)
av_box.Append(new_im)
go getAvatar(u.From, photo, new_im)
})
}()
}
}
box.Append(av_box)
content_box := gtk.NewBox(gtk.OrientationVertical, 4)
content_box.Append(gtk.NewLabel(u.From))
content_box.Append(gtk.NewLabel(mu.MucUserItem.Role))
content_box.Append(gtk.NewLabel(mu.MucUserItem.Affiliation))
content_box.Append(gtk.NewLabel(mu.MucUserItem.JID))
box.Append(content_box)
tooltip.SetCustom(box)
return true
})
gesture := gtk.NewGestureClick() gesture := gtk.NewGestureClick()
gesture.SetButton(1) gesture.SetButton(1)
@@ -498,7 +588,7 @@ func switchToTab(jid string, w *gtk.Window) {
kb.ConnectClicked(func() { kb.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(` client.SendRaw(fmt.Sprintf(`
<iq from='%s' <iq from='%s'
id='kick1' id='%s'
to='%s' to='%s'
type='set'> type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'> <query xmlns='http://jabber.org/protocol/muc#admin'>
@@ -506,7 +596,7 @@ func switchToTab(jid string, w *gtk.Window) {
</item> </item>
</query> </query>
</iq> </iq>
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource)) `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(u.From).Resource))
}) })
bb.ConnectClicked(func() { bb.ConnectClicked(func() {
@@ -516,14 +606,14 @@ func switchToTab(jid string, w *gtk.Window) {
if mu.MucUserItem.JID != "" { if mu.MucUserItem.JID != "" {
client.SendRaw(fmt.Sprintf(` client.SendRaw(fmt.Sprintf(`
<iq from='%s' <iq from='%s'
id='ban1' id='%s'
to='%s' to='%s'
type='set'> type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'> <query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='outcast' jid='%s'/> <item affiliation='outcast' jid='%s'/>
</query> </query>
</iq> </iq>
`, clientroot.Session.BindJid, jid, JidMustParse(mu.MucUserItem.JID).Bare())) `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(mu.MucUserItem.JID).Bare()))
} }
} }
}) })
@@ -547,14 +637,14 @@ func switchToTab(jid string, w *gtk.Window) {
submit.ConnectClicked(func() { submit.ConnectClicked(func() {
client.SendRaw(fmt.Sprintf(` client.SendRaw(fmt.Sprintf(`
<iq from='%s' <iq from='%s'
id='ba1' id='%s'
to='%s' to='%s'
type='set'> type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'> <query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='%s' jid='%s'/> <item affiliation='%s' jid='%s'/>
</query> </query>
</iq> </iq>
`, clientroot.Session.BindJid, jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare())) `, clientroot.Session.BindJid, uuid.NewString(), jid, the_entry.Text(), JidMustParse(mu.MucUserItem.JID).Bare()))
win.Destroy() win.Destroy()
}) })
@@ -588,7 +678,7 @@ func switchToTab(jid string, w *gtk.Window) {
client.SendRaw(fmt.Sprintf(` client.SendRaw(fmt.Sprintf(`
<iq from='%s' <iq from='%s'
id='kick1' id='%s'
to='%s' to='%s'
type='set'> type='set'>
<query xmlns='http://jabber.org/protocol/muc#admin'> <query xmlns='http://jabber.org/protocol/muc#admin'>
@@ -596,7 +686,7 @@ func switchToTab(jid string, w *gtk.Window) {
</item> </item>
</query> </query>
</iq> </iq>
`, clientroot.Session.BindJid, jid, JidMustParse(u.From).Resource, the_entry.Text())) `, clientroot.Session.BindJid, uuid.NewString(), jid, JidMustParse(u.From).Resource, the_entry.Text()))
}) })
box.Append(the_entry) box.Append(the_entry)
@@ -642,7 +732,13 @@ func switchToTab(jid string, w *gtk.Window) {
} else { } else {
headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("group"))) headerBox.Append(gtk.NewImageFromPaintable(clientAssetsLoad("group")))
} }
headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, loadedLocale["participants"])))
pword := loadedLocale["participants"]
if i == 1 {
pword = loadedLocale["participant"]
}
headerBox.Append(gtk.NewLabel(fmt.Sprintf("%d %s", i, pword)))
gen.Prepend(headerBox) gen.Prepend(headerBox)
muc_presence := typed_tab.muc_presence muc_presence := typed_tab.muc_presence
@@ -717,37 +813,29 @@ func createIdenticon(word string, always_create bool, i *gtk.Image) { // This fu
loader.Close() loader.Close()
p := loader.Pixbuf() p := loader.Pixbuf()
gt := gdk.NewTextureForPixbuf(p) gt := gdk.NewTextureForPixbuf(p)
/*
i := gtk.NewImageFromPaintable(gt)
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
return i
*/
glib.IdleAdd(func() { glib.IdleAdd(func() {
i.SetFromPaintable(gt) i.SetFromPaintable(gt)
i.AddCSSClass(loadedConfig.CVD.String() + "_CVD")
}) })
} }
func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows the user to interactively build a JID 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 := gtk.NewWindow()
win.SetTitle("Build-A-JID") win.SetTitle(loadedLocale["buildAJID"])
win.SetDefaultSize(400, 1) win.SetDefaultSize(400, 1)
win.SetResizable(false) win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 2) box := gtk.NewBox(gtk.OrientationVertical, 2)
header := gtk.NewLabel("Build-A-JID") header := gtk.NewLabel(loadedLocale["buildAJID"])
header.AddCSSClass("author") header.AddCSSClass("author")
box.Append(header) box.Append(header)
box.Append(gtk.NewLabel("All fields except for domain are optional")) box.Append(gtk.NewLabel(loadedLocale["buildAJIDOptionalFieldsNote"]))
jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2) jid_builder := gtk.NewBox(gtk.OrientationHorizontal, 2)
localPartEntry := gtk.NewEntry() localPartEntry := gtk.NewEntry()
localPartEntry.SetPlaceholderText("localpart") localPartEntry.SetPlaceholderText(loadedLocale["BAJlocalpart"])
jid_builder.Append(localPartEntry) jid_builder.Append(localPartEntry)
at_sign := gtk.NewLabel("@") at_sign := gtk.NewLabel("@")
@@ -755,7 +843,7 @@ func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows th
jid_builder.Append(at_sign) jid_builder.Append(at_sign)
domainEntry := gtk.NewEntry() domainEntry := gtk.NewEntry()
domainEntry.SetPlaceholderText("domain") domainEntry.SetPlaceholderText(loadedLocale["BAJdomain"])
jid_builder.Append(domainEntry) jid_builder.Append(domainEntry)
resource_sign := gtk.NewLabel("/") resource_sign := gtk.NewLabel("/")
@@ -763,7 +851,7 @@ func jidBuilder(en *gtk.Entry) { // This function spawns a window that allows th
jid_builder.Append(resource_sign) jid_builder.Append(resource_sign)
resourceEntry := gtk.NewEntry() resourceEntry := gtk.NewEntry()
resourceEntry.SetPlaceholderText("resource") resourceEntry.SetPlaceholderText(loadedLocale["BAJresource"])
jid_builder.Append(resourceEntry) jid_builder.Append(resourceEntry)
box.Append(jid_builder) box.Append(jid_builder)
+105 -62
View File
@@ -81,12 +81,29 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
return gtk.NewLabel("Unsupported message."), true return gtk.NewLabel("Unsupported message."), true
} }
readmarker := Marker{} sid := StanzaID{}
ok = m.Get(&readmarker) m.Get(&sid)
if ok {
b := gtk.NewBox(gtk.OrientationHorizontal, 0) ocu := OccupantID{}
b.Append(gtk.NewLabel(fmt.Sprintf("%s%s", JidMustParse(m.From).Resource, loadedLocale["readWidget"]))) m.Get(&ocu)
return b, true
store_label := false
var msg *abMessage
tab, ok := tabs.Load(target)
typed_tab, ok2 := tab.(*chatTab)
if ok && ok2 {
store_label = true
msg, ok = typed_tab.msg_refsID[m.Id]
if !ok {
msg = new(abMessage)
typed_tab.msg_refsID[m.Id] = msg
}
msg.ocu_id = ocu.ID
if sid.ID != "" {
typed_tab.msg_refsOSID[sid.ID] = msg
}
} }
composing := stanza.StateComposing{} composing := stanza.StateComposing{}
@@ -108,19 +125,6 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
return error_box, true return error_box, true
} }
store_label := false
tab, ok := tabs.Load(target)
if ok {
store_label = true
}
typed_tab := tab.(*chatTab)
sid := StanzaID{}
m.Get(&sid)
ocu := OccupantID{}
m.Get(&ocu)
id := JidMustParse(m.From).Resource id := JidMustParse(m.From).Resource
name := id name := id
@@ -179,7 +183,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
<reaction>%s</reaction> <reaction>%s</reaction>
</reactions> </reactions>
</message> </message>
`, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, v)) `, m.To, jid.MustParse(m.From).Bare().String(), uuid.NewString(), m.Type, sid.ID, v))
}) })
reactions.Append(like) reactions.Append(like)
} }
@@ -198,7 +202,7 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
<reaction>%s</reaction> <reaction>%s</reaction>
</reactions> </reactions>
</message> </message>
`, m.To, jid.MustParse(m.From).Bare().String(), uuid.New().String(), m.Type, sid.ID, custom.Text())) `, m.To, jid.MustParse(m.From).Bare().String(), uuid.NewString(), m.Type, sid.ID, custom.Text()))
}) })
rc_box.Append(custom) rc_box.Append(custom)
rc_box.Append(enter_custom) rc_box.Append(enter_custom)
@@ -234,6 +238,26 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
rc_box.Append(vp) rc_box.Append(vp)
live_edit := gtk.NewButtonWithLabel("Live edit")
live_edit.ConnectClicked(func() {
en := gtk.NewEntry()
en.ConnectChanged(func() {
client.SendRaw(fmt.Sprintf("<message id='%s' to='%s' type='%s'><body>%s</body><replace id='%s' xmlns='urn:xmpp:message-correct:0'/></message>",
uuid.NewString(),
target,
m.Type,
en.Text(),
m.Id,
))
})
win := gtk.NewWindow()
win.SetChild(en)
win.SetVisible(true)
})
rc_box.Append(live_edit)
popover.SetChild(rc_box) popover.SetChild(rc_box)
rect := gdk.NewRectangle(x, y, 1, 1) rect := gdk.NewRectangle(x, y, 1, 1)
popover.SetPointingTo(&rect) popover.SetPointingTo(&rect)
@@ -273,18 +297,8 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
// TODO: Decouple this from widget generation // TODO: Decouple this from widget generation
if store_label { if store_label {
msg, ok := typed_tab.msg_refsID[m.Id]
if !ok {
msg = new(abMessage)
typed_tab.msg_refsID[m.Id] = msg
}
msg.label_ref = mlabel msg.label_ref = mlabel
msg.ocu_id = ocu.ID msg.ocu_id = ocu.ID
if sid.ID != "" {
typed_tab.msg_refsOSID[sid.ID] = msg
}
} }
} else { } else {
authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10) authorBox := gtk.NewBox(gtk.OrientationHorizontal, 10)
@@ -312,10 +326,11 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
} }
im := gtk.NewImage() im := gtk.NewImage()
im.SetFromPaintable(clientAssetsLoad("DefaultAvatar")) // im := gtk.NewImageFromPaintable(clientAssetsLoad("DefaultAvatar"))
go createIdenticon(m.From, false, im)
// im.SetFromPaintable(clientAssetsLoad("DefaultAvatar"))
im.SetPixelSize(40) im.SetPixelSize(40)
im.AddCSSClass("author_img") im.AddCSSClass("author_img")
// createIdenticon(m.From, false, im)
authorBox.Append(im) authorBox.Append(im)
if can_generate { if can_generate {
pres := mmmm.(stanza.Presence) pres := mmmm.(stanza.Presence)
@@ -325,14 +340,38 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
go getAvatar(m.From, vu.Photo, im) go getAvatar(m.From, vu.Photo, im)
} }
} else { } else {
createIdenticon(m.From, false, im) go createIdenticon(m.From, false, im)
} }
} else if m.Type == stanza.MessageTypeChat { } else if m.Type == stanza.MessageTypeChat {
al.SetText(al.Text() + loadedLocale["whispers"]) al.SetText(al.Text() + loadedLocale["whispers"])
} }
color_ind := gtk.NewLabel("")
ycbcr := xmpp_color.String(id, 255, loadedConfig.CVD)
r, g, b, _ := ycbcr.RGBA()
hexcolor := fmt.Sprintf("#%02x%02x%02x", r, g, b)
color_ind.SetMarkup(fmt.Sprintf("<span foreground='%s'>|</span>", hexcolor))
authorBox.Append(color_ind)
authorBox.Append(al) authorBox.Append(al)
oob := stanza.OOB{}
ok = m.Get(&oob)
if ok {
// media := newPictureFromWeb(oob.URL)
// media.SetCanShrink(false)
// mainBox.Append(media)
// media.AddCSSClass("chat_image")
mbtn := gtk.NewButtonWithLabel("🖼️")
// mbtn.SetChild(newImageFromWeb(oob.URL))
mbtn.ConnectClicked(func() {
gopen.Open(oob.URL)
})
authorBox.Append(mbtn)
}
if m.Thread != "" { if m.Thread != "" {
im := gtk.NewImage() im := gtk.NewImage()
createIdenticon(m.Thread, true, im) createIdenticon(m.Thread, true, im)
@@ -346,6 +385,33 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
authorBox.Append(gtk.NewLabel("🎮")) authorBox.Append(gtk.NewLabel("🎮"))
} }
stamp_label := gtk.NewLabel("")
stamp_label.SetHExpand(true)
stamp_label.SetHAlign(gtk.AlignEnd)
stamp_label.AddCSSClass("visitor")
stamp_label.SetText(time.Now().Format("03:04 PM"))
stamp_label.SetTooltipText(time.Now().Format("2006-01-02 15:04:05"))
stamp := new(Delayed_Stamp)
ok = m.Get(stamp)
if ok {
stamped_time, err := stamp.GetStamp()
if err == nil {
stamped_time := *stamped_time
loc := time.Now().Location()
stamped_time = stamped_time.In(loc)
str := stamped_time.Format("03:04 PM")
str_full := stamped_time.Format("2006-01-02 15:04:05")
stamp_label.SetText(str)
stamp_label.SetTooltipText(str_full)
}
}
authorBox.Append(stamp_label)
body := m.Body body := m.Body
body = XEPToPango(body, false) body = XEPToPango(body, false)
@@ -361,40 +427,15 @@ func generateMessageWidget(p stanza.Packet, blocked bool, target string) (gtk.Wi
// TODO: Decouple this from widget generation // TODO: Decouple this from widget generation
if store_label { if store_label {
msg, ok := typed_tab.msg_refsID[m.Id]
if !ok {
msg = new(abMessage)
typed_tab.msg_refsID[m.Id] = msg
}
msg.label_ref = mlabel msg.label_ref = mlabel
msg.ocu_id = ocu.ID msg.read_indicator = gtk.NewBox(gtk.OrientationHorizontal, padding)
authorBox.Append(msg.read_indicator)
if sid.ID != "" {
typed_tab.msg_refsOSID[sid.ID] = msg
}
} }
contentBox.Append(mlabel) contentBox.Append(mlabel)
mainBox.Append(authorBox) mainBox.Append(authorBox)
mainBox.Append(contentBox) mainBox.Append(contentBox)
oob := stanza.OOB{}
ok = m.Get(&oob)
if ok {
// media := newPictureFromWeb(oob.URL)
// media.SetCanShrink(false)
// mainBox.Append(media)
// media.AddCSSClass("chat_image")
mbtn := gtk.NewButtonWithLabel("🖼️")
// mbtn.SetChild(newImageFromWeb(oob.URL))
mbtn.ConnectClicked(func() {
gopen.Open(oob.URL)
})
authorBox.Append(mbtn)
}
if m.Subject != "" { if m.Subject != "" {
subjectlabel := gtk.NewLabel(m.Subject) subjectlabel := gtk.NewLabel(m.Subject)
subjectlabel.SetWrap(true) subjectlabel.SetWrap(true)
@@ -460,6 +501,8 @@ func getVAdjustment(scrolledWindow *gtk.ScrolledWindow) *gtk.Adjustment {
} }
func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably shouldn't be here, and should probably be in xmpp-helpers or somewhere similar. func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably shouldn't be here, and should probably be in xmpp-helpers or somewhere similar.
time.Sleep(200 * time.Millisecond)
hash = strings.ReplaceAll(hash, "/", "-")
oghash := hash oghash := hash
p, err := ensureCache() p, err := ensureCache()
if err != nil { if err != nil {
@@ -492,7 +535,7 @@ func getAvatar(j, hash string, i *gtk.Image) { // TODO: This function probably s
Type: "get", Type: "get",
From: clientroot.Session.BindJid, From: clientroot.Session.BindJid,
To: j, To: j,
Id: uuid.New().String(), Id: uuid.NewString(),
Lang: "en", Lang: "en",
}) })
+4 -1
View File
@@ -2,10 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"html" "html"
"regexp" "regexp"
"strings" "strings"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
) )
var ( var (
@@ -65,6 +65,9 @@ func XEPToPango(text string, correction bool) string {
} }
func GenerateRetraction(lab *gtk.Label, retraction Retraction) { func GenerateRetraction(lab *gtk.Label, retraction Retraction) {
if lab == nil {
return
}
if retraction.Moderated.OccupantID.ID != "" { if retraction.Moderated.OccupantID.ID != "" {
lab.AddCSSClass("moderated") lab.AddCSSClass("moderated")
lab.SetTooltipMarkup(fmt.Sprintf("<span weight='bold'>This message was <span foreground='magenta'>retracted</span> by a moderator.</span>\nReason: '%s'", html.EscapeString(retraction.Reason))) lab.SetTooltipMarkup(fmt.Sprintf("<span weight='bold'>This message was <span foreground='magenta'>retracted</span> by a moderator.</span>\nReason: '%s'", html.EscapeString(retraction.Reason)))
+27
View File
@@ -2,6 +2,8 @@
package main package main
import ( import (
"errors"
"gosrc.io/xmpp/stanza"
"sort" "sort"
"sync" "sync"
) )
@@ -23,3 +25,28 @@ func rangeOrdered(m *sync.Map, fn func(k, v any) bool) {
} }
} }
} }
func getPresenceFromJIDMuc(jid *stanza.Jid) (*stanza.Presence, error) {
mo, ok := mucmembers.Load(jid.Bare())
if !ok {
return nil, errors.New("tab is not present in mucmembers")
}
mm, ok := mo.(mucUnit)
if !ok {
return nil, errors.New("mucUnit is nil")
}
mmm := mm.Members
mmmm, ok := mmm.Load(jid.Resource)
if !ok {
return nil, errors.New("user not in room")
}
pres, ok := mmmm.(stanza.Presence)
if !ok {
return nil, errors.New("presence is nil")
}
return &pres, nil
}
+26 -18
View File
@@ -76,24 +76,32 @@ var enGB = map[string]string{ // British English
// gtk-helpers.go // gtk-helpers.go
"getPastMessages": "Get past messages...", "getPastMessages": "Get past messages...",
"clickForMoreInfo": "Click for more information", "mucLoadError": "There was an error loading the members of this room. Maybe the MUC does not give user presence, it does not exist, you have been banned from it, or you have to fill a CAPTCHA to access it. Try joining the room again or check if the room exists.",
"ban": "Ban", "clickForMoreInfo": "Click for more information",
"kick": "Kick", "ban": "Ban",
"setAffil": "Set affiliation", "kick": "Kick",
"setAffilDescPartOne": "Set ", "setAffil": "Set affiliation",
"setAffilDescPartTwo": "'s affiliation", "setAffilDescPartOne": "Set ",
"setRole": "Set role", "setAffilDescPartTwo": "'s affiliation",
"setRoleDescPartOne": "Set ", "setRole": "Set role",
"setRoleDescPartTwo": "'s role", "setRoleDescPartOne": "Set ",
"setRoleWarning": "Important: if you want this to be permanent, set their affiliation instead", "setRoleDescPartTwo": "'s role",
"gettingVersion": "Getting version...", "setRoleWarning": "Important: if you want this to be permanent, set their affiliation instead",
"connectedWithRole": "Connected with role ", "gettingVersion": "Getting version...",
"affiliatedAs": "Affiliated as ", "connectedWithRole": "Connected with role ",
"unaffiliated": "Unaffiliated", "affiliatedAs": "Affiliated as ",
"participants": "participant(s)", "unaffiliated": "Unaffiliated",
"versionQueryEmpty": "You do not have permission to view this client's version", "participants": "participants",
"versionQueryError": "Got error trying to get version", "participant": "participant",
"versionQueryEmpty": "You do not have permission to view this client's version",
"versionQueryError": "Got error trying to get version",
"blockedUserNotice": "You have blocked this user!",
"buildAJID": "Build-A-JID",
"buildAJIDOptionalFieldsNote": "All fields except for domain are optional",
"BAJlocalpart": "localpart",
"BAJdomain": "domain",
"BAJresource": "resource",
"away": "Away", "away": "Away",
"chat": "Free for chat", "chat": "Free for chat",
+207 -278
View File
@@ -49,11 +49,6 @@ var mIcon *gtk.Image
var gStatus *gtk.Label var gStatus *gtk.Label
var gIcon *gtk.Image var gIcon *gtk.Image
/*
var sStatus *gtk.Label
var sIcon *gtk.Image
*/
var typingStatus *gtk.Label var typingStatus *gtk.Label
var pingStatus *gtk.Label var pingStatus *gtk.Label
@@ -93,9 +88,6 @@ func init() {
} }
func main() { func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
pingTimes = append(pingTimes, []float64{}) pingTimes = append(pingTimes, []float64{})
p, err := ensureConfig() p, err := ensureConfig()
@@ -113,6 +105,12 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
if loadedConfig.Debug {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
log.Println("pprof debug server running at port 6060")
}
if loadedConfig.Resource == "" { if loadedConfig.Resource == "" {
fmt.Println(loadedLocale["configResourceEmptyWarning"]) fmt.Println(loadedLocale["configResourceEmptyWarning"])
@@ -257,6 +255,31 @@ func main() {
return return
} }
originator := JidMustParse(m.From).Bare()
var typed_tab *chatTab
tab, ok := tabs.Load(originator)
if ok {
typed_tab = tab.(*chatTab)
} else {
if m.Type != stanza.MessageTypeGroupchat {
createTab(originator, false, originator)
tab, ok = tabs.Load(originator)
if ok {
typed_tab = tab.(*chatTab)
glib.IdleAdd(func() {
box := createTabBox(originator, originator, originator, true)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationVertical))
})
} else {
return
}
} else {
return
}
}
oi := new(OccupantID) oi := new(OccupantID)
ok = m.Get(oi) ok = m.Get(oi)
if ok { if ok {
@@ -276,12 +299,6 @@ func main() {
sid := new(StanzaID) sid := new(StanzaID)
m.Get(sid) m.Get(sid)
originator := JidMustParse(m.From).Bare()
glib.IdleAdd(func() {
mStatus.SetText(originator)
})
at := new(Attention) at := new(Attention)
ok = m.Get(at) ok = m.Get(at)
if ok { if ok {
@@ -340,6 +357,41 @@ func main() {
return return
} }
// Handle read indicators
marker := new(Marker)
ok = m.Get(marker)
if ok {
_, ok := typed_tab.msg_refsOSID[marker.ID]
if ok {
im := gtk.NewImage()
im.SetPixelSize(20)
im.SetTooltipText(JidMustParse(m.From).Resource)
glib.IdleAdd(func() {
createIdenticon(m.From, false, im)
})
pres, err := getPresenceFromJIDMuc(JidMustParse(m.From))
if err != nil {
return
}
var vu VCardUpdate
ok = pres.Get(&vu)
if ok {
go getAvatar(m.From, vu.Photo, im)
}
ind, ok := typed_tab.msg_refsOSID[marker.ID]
if ok {
if ind.read_indicator != nil {
ind.read_indicator.Append(im)
}
}
return
}
return
}
// Handle corrections BEFORE attempting to generate a message // Handle corrections BEFORE attempting to generate a message
correction := new(Correction) correction := new(Correction)
ok = m.Get(correction) ok = m.Get(correction)
@@ -352,9 +404,11 @@ func main() {
if ok { if ok {
if msg.ocu_id == oi.ID && msg.ocu_id != "" { if msg.ocu_id == oi.ID && msg.ocu_id != "" {
lab := msg.label_ref lab := msg.label_ref
glib.IdleAdd(func() { if lab != nil {
lab.SetMarkup(XEPToPango(m.Body, true)) glib.IdleAdd(func() {
}) lab.SetMarkup(XEPToPango(m.Body, true))
})
}
} }
return return
} }
@@ -370,15 +424,15 @@ func main() {
if ok { if ok {
typed_tab := tab.(*chatTab) typed_tab := tab.(*chatTab)
/* /*
to_retract := "" to_retract := ""
refs := typed_tab.msg_refsID refs := typed_tab.msg_refsID
// If it is a DM, then we use regular message ID. If it is a MUC, we use stanza id // If it is a DM, then we use regular message ID. If it is a MUC, we use stanza id
to_retract = sid.ID
if typed_tab.isMuc && sid != nil {
to_retract = sid.ID to_retract = sid.ID
refs = typed_tab.msg_refsOSID if typed_tab.isMuc && sid != nil {
} to_retract = sid.ID
refs = typed_tab.msg_refsOSID
}
*/ */
msg, ok := typed_tab.msg_refsOSID[retraction.ID] msg, ok := typed_tab.msg_refsOSID[retraction.ID]
@@ -387,7 +441,8 @@ func main() {
glib.IdleAdd(func() { glib.IdleAdd(func() {
GenerateRetraction(lab, *retraction) GenerateRetraction(lab, *retraction)
}) })
} return
}
} }
} }
@@ -399,7 +454,10 @@ func main() {
return return
} }
typed_tab := tab.(*chatTab) typed_tab, ok := tab.(*chatTab)
if !ok {
return
}
if ok { if ok {
if current == JidMustParse(m.From).Bare() { if current == JidMustParse(m.From).Bare() {
@@ -413,6 +471,7 @@ func main() {
ba, converted_successfully := untyped_box.(*gtk.Box) ba, converted_successfully := untyped_box.(*gtk.Box)
if valid && converted_successfully { if valid && converted_successfully {
mStatus.SetText(originator)
typed_tab.msgs.Append(b) typed_tab.msgs.Append(b)
b.Append(ba) b.Append(ba)
b.Append(gtk.NewSeparator(gtk.OrientationVertical)) b.Append(gtk.NewSeparator(gtk.OrientationVertical))
@@ -439,10 +498,7 @@ func main() {
if ok { // This is a presence stanza from a user in a MUC if ok { // This is a presence stanza from a user in a MUC
presence.Get(&ocu) presence.Get(&ocu)
// id := ocu.ID
// if id == "" {
id := JidMustParse(presence.From).Resource id := JidMustParse(presence.From).Resource
// }
from, _ := stanza.NewJid(presence.From) from, _ := stanza.NewJid(presence.From)
muc := from.Bare() muc := from.Bare()
_, ok = mucmembers.Load(muc) _, ok = mucmembers.Load(muc)
@@ -567,7 +623,7 @@ func main() {
pingStatus.AddCSSClass("pending") pingStatus.AddCSSClass("pending")
before := time.Now() before := time.Now()
iq := new(stanza.IQ) iq := new(stanza.IQ)
iq.Id = uuid.New().String() iq.Id = uuid.NewString()
iq.From = clientroot.Session.BindJid iq.From = clientroot.Session.BindJid
iq.Type = "get" iq.Type = "get"
iq.Payload = &Ping{} iq.Payload = &Ping{}
@@ -621,22 +677,8 @@ func main() {
name := "Note To Self" name := "Note To Self"
createTab(jid, false, name) createTab(jid, false, name)
glib.IdleAdd(func() { glib.IdleAdd(func() {
box := gtk.NewBox(gtk.OrientationHorizontal, 10) b := createTabBox(name, jid, jid, false)
b := gtk.NewLabel(name) menu.Append(b)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
box.Append(b)
im := gtk.NewImage()
im.SetPixelSize(40)
go getAvatar(jid, jid, im)
box.Prepend(im)
box.AddController(gesture1)
menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}) })
@@ -666,30 +708,7 @@ func main() {
createTab(jid, false, name) createTab(jid, false, name)
glib.IdleAdd(func() { glib.IdleAdd(func() {
box := gtk.NewBox(gtk.OrientationHorizontal, 10) box := createTabBox(name, jid, jid, false)
b := gtk.NewLabel(name)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
gesture2 := gtk.NewGestureClick()
gesture2.SetButton(3)
gesture2.Connect("pressed", func() {
removeTab(jid, &window.Window)
box.SetVisible(false)
})
im := gtk.NewImage()
im.SetPixelSize(40)
box.Append(im)
box.Append(b)
// TODO: Use PEP avatar and do not use JID as hash
go getAvatar(jid, jid, im)
box.AddController(gesture1)
menu.Append(box) menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}) })
@@ -754,31 +773,7 @@ func main() {
joinMuc(client, clientroot.Session.BindJid, jid, nick, password) joinMuc(client, clientroot.Session.BindJid, jid, nick, password)
createTab(jid, true, name) createTab(jid, true, name)
glib.IdleAdd(func() { glib.IdleAdd(func() {
box := gtk.NewBox(gtk.OrientationHorizontal, 10) box := createTabBox(name, jid, jid, true)
b := gtk.NewLabel(name)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
gesture2 := gtk.NewGestureClick()
gesture2.SetButton(3)
gesture2.Connect("pressed", func() {
removeTab(jid, &window.Window)
box.SetVisible(false)
})
im := gtk.NewImage()
im.SetPixelSize(40)
box.Append(b)
box.Prepend(im)
go getAvatar(jid, jid, im)
box.AddController(gesture1)
box.AddController(gesture2)
menu.Append(box) menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}) })
@@ -795,31 +790,7 @@ func main() {
joinMuc(client, clientroot.Session.BindJid, jid, nick, "") joinMuc(client, clientroot.Session.BindJid, jid, nick, "")
createTab(jid, true, jid) createTab(jid, true, jid)
glib.IdleAdd(func() { glib.IdleAdd(func() {
box := gtk.NewBox(gtk.OrientationHorizontal, 10) box := createTabBox(jid, jid, jid, true)
b := gtk.NewLabel(jid)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(jid, &window.Window)
})
gesture2 := gtk.NewGestureClick()
gesture2.SetButton(3)
gesture2.Connect("pressed", func() {
removeTab(jid, &window.Window)
box.SetVisible(false)
})
im := gtk.NewImage()
im.SetPixelSize(40)
box.Append(b)
box.Prepend(im)
go getAvatar(jid, jid, im)
box.AddController(gesture1)
box.AddController(gesture2)
menu.Append(box) menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
}) })
@@ -891,26 +862,8 @@ func activate(app *gtk.Application) {
submit := gtk.NewButtonWithLabel(loadedLocale["submit"]) submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() { submit.ConnectClicked(func() {
t := jid_entry.Text() t := jid_entry.Text()
n := t createTab(t, false, t)
createTab(t, false, n) box := createTabBox(t, t, t, true)
box := gtk.NewBox(gtk.OrientationHorizontal, 0)
b := gtk.NewLabel(n)
box.Append(b)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(t, &window.Window)
})
gesture2 := gtk.NewGestureClick()
gesture2.SetButton(3)
gesture2.Connect("pressed", func() {
removeTab(t, &window.Window)
box.SetVisible(false)
})
box.AddController(gesture1)
box.AddController(gesture2)
menu.Append(box) menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
dm_window.SetVisible(false) dm_window.SetVisible(false)
@@ -1007,19 +960,14 @@ func activate(app *gtk.Application) {
if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() { if JidMustParse(mu.MucUserItem.JID).Bare() == JidMustParse(clientroot.Session.BindJid).Bare() {
if mu.MucUserItem.Affiliation != "owner" { if mu.MucUserItem.Affiliation != "owner" {
box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"])) box.Append(gtk.NewLabel(loadedLocale["destroyMUCNotOwnerWarning"]))
return false
} }
} }
} }
} else {
panic("not ok")
} }
} else {
panic("not ok")
} }
return true return true
}) })
} else {
panic("not ok")
} }
win.SetChild(box) win.SetChild(box)
@@ -1101,31 +1049,7 @@ func activate(app *gtk.Application) {
} }
createTab(t, true, n) createTab(t, true, n)
box := gtk.NewBox(gtk.OrientationHorizontal, 10) box := createTabBox(n, t, t, true)
im := gtk.NewImage()
im.SetPixelSize(40)
go getAvatar(t, t, im)
b := gtk.NewLabel(n)
box.Append(im)
box.Append(b)
gesture1 := gtk.NewGestureClick()
gesture1.SetButton(1)
gesture1.Connect("pressed", func() {
switchToTab(t, &window.Window)
})
gesture2 := gtk.NewGestureClick()
gesture2.SetButton(3)
gesture2.Connect("pressed", func() {
removeTab(t, &window.Window)
box.SetVisible(false)
})
box.AddController(gesture1)
box.AddController(gesture2)
menu.Append(box) menu.Append(box)
menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal)) menu.Append(gtk.NewSeparator(gtk.OrientationHorizontal))
@@ -1137,7 +1061,6 @@ func activate(app *gtk.Application) {
} }
} }
if !ok { if !ok {
if !disco_check.Active() { if !disco_check.Active() {
jm(t, "") jm(t, "")
win.SetVisible(false) win.SetVisible(false)
@@ -1152,7 +1075,7 @@ func activate(app *gtk.Application) {
Type: "get", Type: "get",
From: clientroot.Session.BindJid, From: clientroot.Session.BindJid,
To: t, To: t,
Id: "dicks", Id: uuid.NewString(),
Lang: "en", Lang: "en",
}) })
@@ -1163,122 +1086,128 @@ func activate(app *gtk.Application) {
myIQ.Payload = &stanza.DiscoInfo{} myIQ.Payload = &stanza.DiscoInfo{}
ctx := context.Background() ctx := context.Background()
mychan, err := client.SendIQ(ctx, myIQ)
if err == nil {
result := <-mychan
res, ok = result.Payload.(*stanza.DiscoInfo)
if ok {
if len(res.Identity) >= 1 {
features := res.Features
allowed = false
password_protected := false
password := ""
warning_win := gtk.NewWindow()
warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name))
warning_win.SetDefaultSize(400, 1)
warning_win.SetResizable(false)
buttons := gtk.NewBox(gtk.OrientationHorizontal, 10) var mychan chan stanza.IQ
//go func() {
mychan, err = client.SendIQ(ctx, myIQ)
if err != nil {
showErrorDialog(err, &window.Window)
}
//}()
join_button := gtk.NewButtonWithLabel("Join") result := <-mychan
join_button.ConnectClicked(func() { res, ok = result.Payload.(*stanza.DiscoInfo)
warning_win.SetVisible(false) if ok {
if password_protected { if len(res.Identity) >= 1 {
allowed = false features := res.Features
allowed = false
password_protected := false
password := ""
warning_win := gtk.NewWindow()
warning_win.SetTitle(fmt.Sprintf("%s%s", loadedLocale["joinPreviewTitle"], res.Identity[0].Name))
warning_win.SetDefaultSize(400, 1)
warning_win.SetResizable(false)
password_win := gtk.NewWindow() buttons := gtk.NewBox(gtk.OrientationHorizontal, 10)
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) join_button := gtk.NewButtonWithLabel("Join")
}) join_button.ConnectClicked(func() {
warning_win.SetVisible(false)
if password_protected {
allowed = false
cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"]) password_win := gtk.NewWindow()
cancel_button.ConnectClicked(func() { password_win.SetTitle(loadedLocale["joinPasswordRequired"])
warning_win.SetVisible(false) password_win.SetDefaultSize(400, 1)
}) password_win.SetResizable(false)
box := gtk.NewBox(gtk.OrientationVertical, 10)
join_button.SetHExpand(true) en := gtk.NewPasswordEntry()
cancel_button.SetHExpand(true) submit := gtk.NewButtonWithLabel(loadedLocale["submit"])
submit.ConnectClicked(func() {
buttons.Append(join_button) password = en.Text()
buttons.Append(cancel_button) jm(res.Identity[0].Name, password)
warning_box := gtk.NewBox(gtk.OrientationVertical, 10) password_win.SetVisible(false)
header := gtk.NewLabel(res.Identity[0].Name) })
warning_box.Append(header) box.Append(en)
addFeature := func(icon string, description string) { box.Append(submit)
box := gtk.NewBox(gtk.OrientationHorizontal, 10) password_win.SetChild(box)
box.Append(gtk.NewImageFromPaintable(clientAssetsLoad(icon))) password_win.SetVisible(true)
box.Append(gtk.NewLabel(description))
warning_box.Append(box)
} }
for _, feature := range features { jm(res.Identity[0].Name, password)
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(buttons) cancel_button := gtk.NewButtonWithLabel(loadedLocale["cancel"])
warning_win.SetChild(warning_box) cancel_button.ConnectClicked(func() {
warning_win.Present() warning_win.SetVisible(false)
} else { })
allowed = false
showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) 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(clientAssetsLoad(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(buttons)
warning_win.SetChild(warning_box)
warning_win.Present()
} else { } else {
allowed = false allowed = false
if result.Error != nil { showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win)
showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text), win) }
} else { } else {
showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win) allowed = false
} if result.Error != nil {
showErrorDialog(fmt.Errorf("%s: %s - %s", loadedLocale["discoFail"], result.Error.Reason, result.Error.Text), win)
} else {
showErrorDialog(fmt.Errorf(loadedLocale["discoFail"]), win)
} }
} }
+3
View File
@@ -72,6 +72,9 @@
background-color: red; background-color: red;
} }
// a11y
.RedGreen_CVD { .RedGreen_CVD {
filter: hue-rotate(30deg) saturate(120%) contrast(110%); filter: hue-rotate(30deg) saturate(120%) contrast(110%);
} }
+3 -2
View File
@@ -8,8 +8,9 @@ import (
) )
type abMessage struct { type abMessage struct {
label_ref *gtk.Label label_ref *gtk.Label
ocu_id string ocu_id string
read_indicator *gtk.Box
} }
type chatTab struct { type chatTab struct {
+30
View File
@@ -0,0 +1,30 @@
package main
import (
"encoding/xml"
"gosrc.io/xmpp/stanza"
"time"
)
// Implementation of XEP-0203: Delayed Delivery
// https://xmpp.org/extensions/xep-0203.html
type Delayed_Stamp struct {
XMLName xml.Name `xml:"urn:xmpp:delay delay"`
From string `xml:"from,attr"`
Stamp string `xml:"stamp,attr"`
}
func (d *Delayed_Stamp) GetStamp() (*time.Time, error) {
stamp := d.Stamp
time, err := time.Parse(time.RFC3339, stamp)
if err != nil {
return nil, err
}
return &time, nil
}
func init() {
stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "urn:xmpp:delay", Local: "delay"}, Delayed_Stamp{})
}
+6 -7
View File
@@ -5,21 +5,20 @@ import (
"gosrc.io/xmpp/stanza" "gosrc.io/xmpp/stanza"
) )
// Implementation of XEP-0424: Message Retraction as well as XEP-0425: Moderated Message Retraction // Implementation of XEP-0424: Message Retraction as well as XEP-0425: Moderated Message Retraction
type Retraction struct { type Retraction struct {
stanza.MsgExtension stanza.MsgExtension
XMLName xml.Name `xml:"urn:xmpp:message-retract:1 retract"` XMLName xml.Name `xml:"urn:xmpp:message-retract:1 retract"`
ID string `xml:"id,attr"` // Stanza ID is used in group chats. ID string `xml:"id,attr"` // Stanza ID is used in group chats.
Reason string `xml:"reason"` Reason string `xml:"reason"`
Moderated Moderated `xml:"urn:xmpp:message-moderate:1 moderated"` Moderated Moderated `xml:"urn:xmpp:message-moderate:1 moderated"`
} }
type Moderated struct { type Moderated struct {
XMLName xml.Name `xml:"urn:xmpp:message-moderate:1 moderated"` XMLName xml.Name `xml:"urn:xmpp:message-moderate:1 moderated"`
By string `xml:"by,attr"` By string `xml:"by,attr"`
OccupantID OccupantID OccupantID OccupantID
} }
// To whoever thought putting the same element twice in the same message, but in different parents, and not specifying this behaviour anywhere in the XEP was a good idea, I want to kill you. Seriously, I do. If I see you walking home at night I will kill you. Brutally. Slowly. I will relish in the act. I will do it. // To whoever thought putting the same element twice in the same message, but in different parents, and not specifying this behaviour anywhere in the XEP was a good idea, I want to kill you. Seriously, I do. If I see you walking home at night I will kill you. Brutally. Slowly. I will relish in the act. I will do it.