1
0
forked from sunglocto/pi-im
Files
pi-im/main.go

1773 lines
48 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
//core - required
"context"
_ "embed"
"encoding/xml"
"errors"
"fmt"
"image/color"
_ "image/jpeg"
_ "image/png"
"io"
"log"
"math/rand/v2"
"net/url"
"os"
"strings"
"time"
// gui - required
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/gen2brain/beeep"
"github.com/makeworld-the-better-one/go-isemoji"
"github.com/rrivera/identicon"
"github.com/shreve/musicwand/pkg/mpris"
// xmpp - required
oasisSdk "github.com/sunglocto/oasis-sdk"
"mellium.im/xmpp/bookmarks"
xmppColor "mellium.im/xmpp/color"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
"mellium.im/xmpp/pubsub"
"mellium.im/xmpp/stanza"
// TODO: integrated theme switcher
)
//go:embed pi.png
var iconBytes []byte
var version string = "3.14i"
var statBar widget.Label
var chatInfo fyne.Container
var chatSidebar fyne.Container
var replyNameIcon string = ">"
var replyBodyIcon string = ">"
var newlineIcon string = " |-> "
var agreesToSendingHotFuckIntoChannel bool = false
var sendEmpty bool = false
var OccupantIdsToBlock = make(map[string]string)
// by sunglocto
// license AGPL
type Message struct {
Author string
Content string
ID string
ReplyID string
ImageURL string
Raw oasisSdk.XMPPChatMessage
Important bool
Readers []jid.JID
Reactions map[string]string
}
type ChatTab struct {
Jid jid.JID
Nick string
Messages []Message
isMuc bool
Muc *muc.Channel
UpdateSidebar bool
Members map[string]oasisSdk.UserPresence
}
type ChatTabUI struct {
Internal *ChatTab
Scroller *widget.List `xml:"-"`
Sidebar *fyne.Container `xml:"-"`
}
type CustomMultiLineEntry struct {
widget.Entry
}
func isUTF8Locale() bool {
localeVars := []string{"LC_ALL", "LC_CTYPE", "LANG"}
for _, envVar := range localeVars {
value := os.Getenv(envVar)
if strings.Contains(strings.ToLower(value), "utf-8") {
return true
}
}
return false
}
func isWindows() bool {
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
}
func NewCustomMultiLineEntry() *CustomMultiLineEntry {
entry := &CustomMultiLineEntry{}
entry.ExtendBaseWidget(entry)
entry.MultiLine = true
return entry
}
func (e *CustomMultiLineEntry) TypedShortcut(sc fyne.Shortcut) {
if sc.ShortcutName() == "CustomDesktop:Control+Return" {
e.Entry.TypedRune('\n')
return
}
e.Entry.TypedShortcut(sc)
}
func (e *CustomMultiLineEntry) TypedKey(ev *fyne.KeyEvent) {
if ev.Name == fyne.KeyReturn || ev.Name == fyne.KeyEnter {
// Normal Enter (no modifier) = submit
if e.OnSubmitted != nil {
e.OnSubmitted(e.Text)
}
} else {
e.Entry.TypedKey(ev)
}
}
type piConfig struct {
Login oasisSdk.LoginInfo
DMs []string
Notifications bool
JoinBookmarks bool
}
var config piConfig
var login oasisSdk.LoginInfo
var DMs []string
var chatTabs = make(map[string]*ChatTab)
var UITabs = make(map[string]*ChatTabUI)
var AppTabs *container.DocTabs
var selectedId widget.ListItemID
var replying bool = false
var notifications bool
var connection bool = true
var scrollDownOnNewMessage bool = true
var w fyne.Window
var a fyne.App
func CreateUITab(chatJidStr string) ChatTabUI {
var scroller *widget.List
scroller = widget.NewList(
func() int {
return len(chatTabs[chatJidStr].Messages)
},
func() fyne.CanvasObject {
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw("default")
im := ii.Image(25)
ico := canvas.NewImageFromImage(im)
ico.FillMode = canvas.ImageFillOriginal
author := canvas.NewText("author", color.RGBA{255, 255, 255, 255})
author.TextStyle.Bold = true
// author.Selectable = true
content := widget.NewLabel("content")
content.Wrapping = fyne.TextWrapWord
content.Selectable = true
content.Importance = widget.MediumImportance
content.SizeName = fyne.ThemeSizeName(theme.SizeNameText)
icon := theme.FileVideoIcon()
replytext := widget.NewLabel(">fallback reply text")
replytext.Hide()
replytext.Importance = widget.SuccessImportance
replytext.Selectable = true
// replytext.Wrapping = fyne.TextWrapWord
replytext.Truncation = fyne.TextTruncateEllipsis
btn := widget.NewButtonWithIcon("View media", icon, func() {
})
btn.Hide()
reactions := container.NewHBox()
return container.NewVBox(replytext, container.NewHBox(ico, author), content, btn, reactions)
},
func(i widget.ListItemID, co fyne.CanvasObject) {
vbox := co.(*fyne.Container)
authorBox := vbox.Objects[1].(*fyne.Container)
replytext := vbox.Objects[0].(*widget.Label)
// generate a Icon
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw(chatTabs[chatJidStr].Messages[i].Author)
im := ii.Image(25)
authorBox.Objects[0] = canvas.NewImageFromImage(im)
authorBox.Objects[0].(*canvas.Image).FillMode = canvas.ImageFillOriginal
authorBox.Objects[0].Refresh()
// Icon generate end
author := authorBox.Objects[1].(*canvas.Text)
col := xmppColor.String(chatTabs[chatJidStr].Messages[i].Author, 0, xmppColor.None)
author.Color = col
content := vbox.Objects[2].(*widget.Label)
unknown_tags := chatTabs[chatJidStr].Messages[i].Raw.Unknown
for _, v := range unknown_tags {
if v.XMLName.Local == "occupant-id" {
for _, attr := range v.Attrs {
if attr.Name.Local == "id" {
reason, ok := OccupantIdsToBlock[attr.Value]
if ok {
author.Text = ("Ignored user")
content.SetText("This user is ignored due to: " + reason)
content.Importance = widget.LowImportance
return // message is from blocked user
}
}
}
}
}
btn := vbox.Objects[3].(*widget.Button)
reactions := vbox.Objects[4].(*fyne.Container)
reactions = container.NewVBox()
for _, reaction := range chatTabs[chatJidStr].Messages[i].Reactions {
reactions.Add(widget.NewLabel(reaction))
}
reactions.Refresh()
if chatTabs[chatJidStr].Messages[i].Important {
content.Importance = widget.DangerImportance
} else {
content.Importance = widget.MediumImportance
}
btn.Hidden = true // Hide by default
msgContent := chatTabs[chatJidStr].Messages[i].Content
if chatTabs[chatJidStr].Messages[i].Raw.OutOfBandMedia != nil {
btn.Hidden = false
btn.OnTapped = func() {
go func() {
u, err := storage.ParseURI(chatTabs[chatJidStr].Messages[i].Raw.OutOfBandMedia.URL)
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
return
}
if strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "mp4") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "mp3") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "gif") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "webp") { // FIXME: This code is fucking terrible // TODO: Could check mime?
url, err := url.Parse(chatTabs[chatJidStr].Messages[i].Raw.OutOfBandMedia.URL)
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
return
}
fyne.Do(func() {
a.OpenURL(url)
})
return
}
image := canvas.NewImageFromURI(u)
image.FillMode = canvas.ImageFillOriginal
fyne.Do(func() {
dialog.ShowCustom("Image", "Close", image, w)
})
}()
}
}
// Check if the message is a quote
lines := strings.Split(msgContent, "\n")
for i, line := range lines {
if strings.HasPrefix(line, ">") {
lines[i] = fmt.Sprintf("\n %s \n", line)
}
}
msgContent = strings.Join(lines, "\n")
//content.ParseMarkdown(msgContent)
content.SetText(msgContent)
if chatTabs[chatJidStr].Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
reply := chatTabs[chatJidStr].Messages[i].Raw.Reply
j, err := jid.Parse(reply.To)
if err != nil {
log.Println("ERR: " + err.Error()) // FIXME
return
}
guy := j.Resourcepart()
// TODO: EXPERIMENTALLY GET REPLIED TO TEXT
for i := len(chatTabs[chatJidStr].Messages) - 1; i >= 0; i-- {
if reply.ID == chatTabs[chatJidStr].Messages[i].Raw.StanzaID.ID {
replytext.Show()
replytext.Text = fmt.Sprintf("%s %s", replyBodyIcon, strings.ReplaceAll(chatTabs[chatJidStr].Messages[i].Content, "\n", newlineIcon))
guy = chatTabs[chatJidStr].Messages[i].Author
break
}
}
author.Text = (fmt.Sprintf("%s %s %s", chatTabs[chatJidStr].Messages[i].Author, replyNameIcon, guy))
} else {
author.Text = (chatTabs[chatJidStr].Messages[i].Author)
replytext.Hide()
}
sl := strings.Split(msgContent, " ")
content.SizeName = fyne.ThemeSizeName(theme.SizeNameText)
if len(sl) == 1 && isemoji.IsEmoji(sl[0]) {
content.SizeName = fyne.ThemeSizeName(theme.SizeNameHeadingText)
content.Refresh()
} else {
content.SizeName = fyne.ThemeSizeName(theme.SizeNameText)
content.Refresh()
}
if sl[0] == "/me" {
author.Text = (author.Text + " " + strings.Join(sl[1:], " "))
content.SetText(" ")
}
scroller.SetItemHeight(i, vbox.MinSize().Height)
scroller.CreateItem().Refresh()
vbox.Refresh()
/*
fyne.Do(func() {
scroller.RefreshItem(i)
})
*/
},
)
scroller.OnSelected = func(id widget.ListItemID) {
selectedId = id
}
myUITab := ChatTabUI{}
scroller.CreateItem()
myUITab.Scroller = scroller
gen, _ := identicon.New("github", 50, 20)
ii, _ := gen.Draw(chatJidStr)
im := ii.Image(250)
imw := canvas.NewImageFromImage(im)
imw.FillMode = canvas.ImageFillOriginal
myUITab.Sidebar = container.NewVBox(imw)
return myUITab
}
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
chatJidStr := chatJid.String()
if _, ok := chatTabs[chatJidStr]; ok {
// Tab already exists
return
}
myChatTab := ChatTab{
Jid: chatJid,
Nick: nick,
Messages: []Message{},
isMuc: isMuc,
Members: make(map[string]oasisSdk.UserPresence),
}
myUITab := CreateUITab(chatJid.String())
myUITab.Internal = &myChatTab
chatTabs[chatJidStr] = &myChatTab
UITabs[chatJidStr] = &myUITab
var icon fyne.Resource
if isMuc {
icon = theme.HomeIcon()
} else {
icon = theme.AccountIcon()
}
fyne.Do(func() {
myTab := container.NewTabItemWithIcon(chatJid.String(), icon, myUITab.Scroller)
AppTabs.Append(myTab)
})
}
func dropToSignInPage(reason string) {
w = a.NewWindow("Welcome to pi")
w.Resize(fyne.NewSize(500, 500))
rt := widget.NewRichTextFromMarkdown("# Welcome to pi\nIt appears you do not have a valid account configured. Let's create one!")
footer := widget.NewRichTextFromMarkdown(fmt.Sprintf("Reason for being dropped to the sign-in page:\n\n```%s```", reason))
userEntry := widget.NewEntry()
userEntry.SetPlaceHolder("Your JID")
serverEntry := widget.NewEntry()
serverEntry.SetPlaceHolder("Server and port")
passwordEntry := widget.NewPasswordEntry()
passwordEntry.SetPlaceHolder("Your Password")
nicknameEntry := widget.NewEntry()
nicknameEntry.SetPlaceHolder("Your Nickname")
userView := widget.NewFormItem("", userEntry)
serverView := widget.NewFormItem("", serverEntry)
passwordView := widget.NewFormItem("", passwordEntry)
nicknameView := widget.NewFormItem("", nicknameEntry)
items := []*widget.FormItem{
serverView,
userView,
passwordView,
nicknameView,
}
btn := widget.NewButton("Create an account", func() {
dialog.ShowForm("Create an account", "Create", "Dismiss", items, func(b bool) {
if b {
config := piConfig{}
config.Login.Host = serverEntry.Text
config.Login.User = userEntry.Text
config.Login.Password = passwordEntry.Text
config.Login.DisplayName = nicknameEntry.Text
config.Notifications = false
bytes, err := xml.MarshalIndent(config, "", "\t")
if err != nil {
dialog.ShowError(err, w)
return
}
writer, err := a.Storage().Create("pi.xml")
if err != nil {
dialog.ShowError(err, w)
return
}
defer writer.Close()
_, err = writer.Write(bytes)
if err != nil {
dialog.ShowError(err, w)
return
}
// a.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
beeep.Notify("Done", "Relaunch the application", iconBytes)
a.Quit()
//w.Close()
}
}, w)
})
btn2 := widget.NewButton("Close pi", func() {
w.Close()
})
w.SetContent(container.NewVBox(rt, btn, btn2, footer))
w.ShowAndRun()
}
func main() {
muc.Since(time.Now())
config = piConfig{}
res := fyne.NewStaticResource("image", iconBytes)
a = app.NewWithID("pi-im")
a.SetIcon(res)
reader, err := a.Storage().Open("pi.xml")
if err != nil {
dropToSignInPage(err.Error())
return
}
defer reader.Close()
bytes, err := io.ReadAll(reader)
if err != nil {
dropToSignInPage(err.Error())
return
}
err = xml.Unmarshal(bytes, &config)
if err != nil {
dropToSignInPage(fmt.Sprintf("Your pi.xml file is invalid:\n%s", err.Error()))
return
}
DMs = config.DMs
login = config.Login
notifications = config.Notifications
if isUTF8Locale() {
replyBodyIcon = "↱"
replyNameIcon = "→ "
newlineIcon = " ⮡ "
}
client, err := oasisSdk.CreateClient(&login)
client.SetDmHandler(func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
correction := false
userJidStr := msg.From.Bare().String()
fmt.Println(userJidStr)
tab, ok := chatTabs[userJidStr]
if ok {
str := *msg.CleanedBody
if notifications {
// a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str))
beeep.Notify("Direct message", fmt.Sprintf("%s says:\n%s", userJidStr, str), iconBytes)
}
for _, v := range msg.Unknown {
if v.XMLName.Local == "replace" {
correction = true
break // dont need to look at more fields
}
}
var img string = ""
if strings.Contains(str, "https://") {
lines := strings.Split(str, "\n")
for i, line := range lines {
s := strings.Split(line, " ")
for _, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".webp") || strings.HasSuffix(v, ".mp4") || strings.HasSuffix(v, ".gif") {
img = v
}
}
}
lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
}
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
fmt.Println("Received reply in DM")
replyID = msg.Reply.ID
}
if correction {
for i := len(tab.Messages) - 1; i > 0; i-- {
if tab.Messages[i].Raw.From.String() == msg.From.String() {
tab.Messages[i].Content = *msg.CleanedBody + " (corrected)"
fyne.Do(func() {
UITabs[userJidStr].Scroller.Refresh()
})
return
}
}
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
ImageURL: img,
Reactions: make(map[string]string),
}
tab.Messages = append(tab.Messages, myMessage)
fyne.Do(func() {
//UITabs[userJidStr].Scroller.Refresh()
if scrollDownOnNewMessage {
//UITabs[userJidStr].Scroller.ScrollToBottom()
}
})
}
})
client.SetGroupChatHandler(func(client *oasisSdk.XmppClient, muc *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
ignore := false
reaction := false
correction := false
important := false
donotnotify := false
for _, v := range msg.Unknown {
if v.XMLName.Local == "delay" { // Classic history message
donotnotify = true
//ignore = true
//fmt.Println("ignoring!")
//return //what is blud doing
}
}
for _, v := range msg.Unknown {
if v.XMLName.Local == "replace" {
correction = true
break // dont need to look at more fields
}
if v.XMLName.Local == "reactions" {
reaction = true
break
}
}
var ImageID string = ""
mucJidStr := msg.From.Bare().String()
if tab, ok := chatTabs[mucJidStr]; ok {
//chatInfo.Objects[0] = widget.NewLabel(fmt.Sprintf("[!] %s", mucJidStr))
chatTabs[mucJidStr].Muc = muc
str := *msg.CleanedBody
if strings.Contains(str, login.DisplayName) {
fmt.Println(str, login.DisplayName)
important = true
}
if !donotnotify && !ignore && notifications {
if !correction && msg.From.String() != client.JID.String() && strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.DisplayName)) {
// a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
beeep.Notify(fmt.Sprintf("Mentioned in %s by %s", mucJidStr, msg.From.Resourcepart()), str, iconBytes)
}
}
if strings.Contains(str, "https://") {
lines := strings.Split(str, "\n")
for i, line := range lines {
s := strings.Split(line, " ")
for _, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".webp") || strings.HasSuffix(v, ".mp4") || strings.HasSuffix(v, ".mp3") || strings.HasSuffix(v, ".gif") {
ImageID = v
}
}
}
lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
}
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg.Reply.To
}
if reaction {
for i := len(tab.Messages) - 1; i > 0; i-- {
if tab.Messages[i].Raw.StanzaID.ID == msg.Reply.ID {
tab.Messages[i].Reactions[msg.From.String()] = *msg.CleanedBody
fyne.Do(func() {
UITabs[mucJidStr].Scroller.Refresh()
})
return
}
}
}
if correction {
for i := len(tab.Messages) - 1; i > 0; i-- {
if tab.Messages[i].Raw.From.String() == msg.From.String() {
tab.Messages[i].Content = *msg.CleanedBody + " (corrected)"
fyne.Do(func() {
UITabs[mucJidStr].Scroller.Refresh()
})
return
}
}
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
ImageURL: ImageID,
Important: important,
Reactions: make(map[string]string),
}
if !ignore {
tab.Messages = append(tab.Messages, myMessage)
}
important = false
fyne.Do(func() {
_, ok := UITabs[mucJidStr]
if !ok {
return
}
UITabs[mucJidStr].Scroller.Refresh()
if scrollDownOnNewMessage {
tab, ok := UITabs[mucJidStr]
if ok {
tab.Scroller.ScrollToBottom()
}
}
})
}
})
client.SetChatstateHandler(func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
switch state {
case oasisSdk.ChatStateComposing:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is typing...", from.Resourcepart()))
})
case oasisSdk.ChatStatePaused:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s has stopped typing.", from.Resourcepart()))
})
case oasisSdk.ChatStateInactive:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is idle", from.Resourcepart()))
})
case oasisSdk.ChatStateGone:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is gone", from.Resourcepart()))
})
default:
fyne.Do(func() {
statBar.SetText("")
})
}
})
client.SetPresenceHandler(func(client *oasisSdk.XmppClient, from jid.JID, p oasisSdk.UserPresence) {
fmt.Println(p.Header.Type == stanza.UnavailablePresence)
bareAcc := from.Bare()
tab, ok := chatTabs[bareAcc.String()]
if !ok {
// User presence
addChatTab(false, bareAcc, client.Login.DisplayName)
return
}
if tab.isMuc {
fmt.Println(p.Type)
tab.Members[from.String()] = p
}
})
client.SetBookmarkHandler(false, func(client *oasisSdk.XmppClient, bookmark bookmarks.Channel) {
if !config.JoinBookmarks {
return
}
// FIXME
if bookmark.JID.String() == "conversations-offtopic-reloaded@conference.trashserver.net" {
return
}
if bookmark.Autojoin {
if bookmark.Nick == "" {
fmt.Println("WARNING: Bookmark has no name")
bookmark.Nick = client.Login.DisplayName
}
addChatTab(true, bookmark.JID, client.Login.DisplayName)
_, err := client.ConnectMuc(bookmark, oasisSdk.MucLegacyHistoryConfig{}, context.TODO())
if err != nil {
fmt.Println("ERROR: " + err.Error())
return
}
}
})
client.SetDeliveryReceiptHandler(
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
fmt.Printf("Delivered %s to %s", id, from.String())
})
client.SetReadReceiptHandler(
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
for _, tab := range chatTabs {
for i := len(tab.Messages) - 1; i >= 0; i-- {
if tab.Messages[i].Raw.StanzaID == nil {
continue
}
if tab.Messages[i].Raw.StanzaID.ID == id {
tab.Messages[i].Readers = append(tab.Messages[i].Readers, from)
break
}
}
}
})
if err != nil {
log.Fatalln("Could not create client - " + err.Error())
}
go func() {
for connection {
err = client.Connect()
if err != nil {
responseChan := make(chan bool)
//fyne.Do(func() {
fmt.Println(err)
dialog.ShowConfirm("disconnected", fmt.Sprintf("the client disconnected. would you like to try and reconnect?\nreason:\n%s", err.Error()), func(b bool) {
responseChan <- b
}, w)
//})
if !<-responseChan {
connection = false
}
}
}
}()
a = app.New()
w = a.NewWindow("pi")
w.SetCloseIntercept(func() {
w.RequestFocus()
dialog.ShowConfirm("Close pi", "You hit the close button. Do you want Pi to close completely?", func(b bool) {
if b {
w.Close()
a.Quit()
log.Fatalln("Goodbye!")
} else {
w.Hide()
}
}, w)
})
w.Resize(fyne.NewSize(500, 500))
w.SetIcon(res)
entry := NewCustomMultiLineEntry()
entry.SetPlaceHolder("Say something, you know you want to.\nCtrl+Enter for newline")
entry.Wrapping = fyne.TextWrapBreak
//entry.TypedShortcut()
SendCallback := func() {
text := entry.Text
if text == "" {
dialog.ShowConfirm("Empty message", "Send an empty message?", func(b bool) {
sendEmpty = b
}, w)
}
if AppTabs.Selected() == nil || AppTabs.Selected().Content == nil || (text == "" && !sendEmpty) {
return
}
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
var isMuc bool
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
isMuc = chatTabs[activeMucJid].isMuc
break
}
}
if activeMucJid == "" {
return
}
go func() {
if replying {
m := chatTabs[activeMucJid].Messages[selectedId].Raw
err = client.ReplyToEvent(&m, text)
if err != nil {
dialog.ShowError(err, w)
}
return
}
url, uerr := url.Parse(strings.Split(text, " ")[0])
if uerr == nil && strings.HasPrefix(strings.Split(text, " ")[0], "https://") {
dialog.ShowConfirm("Confirm", "Do you want to embed this link into your message?", func(b bool) {
if b {
err = client.SendSingleFileMessage(jid.MustParse(activeMucJid).Bare(), url.String(), nil)
if err != nil {
dialog.ShowError(err, w)
}
return
} else {
err = client.SendText(jid.MustParse(activeMucJid).Bare(), text)
if err != nil {
dialog.ShowError(err, w)
}
}
}, w)
} else if text == "@here" && chatTabs[activeMucJid].isMuc {
tab := chatTabs[activeMucJid]
dialog.ShowConfirm("WARNING", fmt.Sprintf("There are %d members in this room.\nYou are about to mention every single one of them.\nYou may be punished if you are not a moderator of this chat.\nWould you like to continue?", len(tab.Members)), func(b bool) {
if b {
text = ""
for name := range tab.Members {
text = fmt.Sprintf("%s %s", text, jid.MustParse(name).Resourcepart())
}
a := pubsub.Fetch(context.TODO(), client.Session, pubsub.Query{})
log.Println(a.Item())
err = client.SendText(jid.MustParse(activeMucJid).Bare(), text)
if err != nil {
dialog.ShowError(err, w)
}
}
}, w)
} else {
err = client.SendText(jid.MustParse(activeMucJid).Bare(), text)
if err != nil {
dialog.ShowError(err, w)
}
}
}()
if !isMuc {
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
Author: "You",
Content: text,
ReplyID: "PICLIENT:UNAVAILABLE",
})
fyne.Do(func() {
if scrollDownOnNewMessage {
UITabs[activeMucJid].Scroller.ScrollToBottom()
}
})
}
entry.SetText("")
}
sendbtn := widget.NewButton("Send", SendCallback)
replybtn := widget.NewButton("Reply", func() {
replying = true
SendCallback()
replying = false
})
entry.OnSubmitted = func(s string) {
SendCallback()
// i fucking hate fyne
}
mit := fyne.NewMenuItem("about pi", func() {
dialog.ShowInformation("about pi", fmt.Sprintf("the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.\n\nVersion %s", version), w)
})
licensesbtn := fyne.NewMenuItem("credits", func() {
CreditsWindow(fyne.CurrentApp(), fyne.NewSize(800, 400)).Show()
})
reconnect := fyne.NewMenuItem("reconnect", func() {
go func() {
err := client.Connect()
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
}
}()
})
mia := fyne.NewMenuItem("configure message view", func() {
ch := widget.NewCheck("", func(b bool) {})
ch2 := widget.NewCheck("", func(b bool) {})
ch.Checked = scrollDownOnNewMessage
ch2.Checked = notifications
scrollView := widget.NewFormItem("scroll to bottom on new message", ch)
notiView := widget.NewFormItem("send notifications when mentioned", ch2)
items := []*widget.FormItem{
scrollView,
notiView,
}
dialog.ShowForm("configure message view", "apply", "cancel", items, func(b bool) {
if b {
scrollDownOnNewMessage = ch.Checked
notifications = ch2.Checked
}
}, w)
})
jtb := fyne.NewMenuItem("jump to bottom", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToBottom()
selectedScroller.Refresh()
})
jtt := fyne.NewMenuItem("jump to top", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToTop()
selectedScroller.Refresh()
})
w.SetOnDropped(func(p fyne.Position, u []fyne.URI) {
var link string
myUri := u[0] // Only upload a single file
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
diag.Show()
go func() {
client.UploadFile(client.Ctx, myUri.Path(), progress)
}()
for update := range progress {
fyne.Do(func() {
myprogressbar.Value = float64(update.Percentage) / 100
myprogressbar.Refresh()
})
if update.Error != nil {
diag.Dismiss()
dialog.ShowError(update.Error, w)
return
}
if update.GetURL != "" {
link = update.GetURL
}
}
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
})
mic := fyne.NewMenuItem("upload a file", func() {
var link string
var toperr error
//var topreader fyne.URIReadCloser
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
go func() {
if err != nil {
dialog.ShowError(err, w)
return
}
if reader == nil {
return
}
bytes, toperr = io.ReadAll(reader)
//topreader = reader
if toperr != nil {
dialog.ShowError(toperr, w)
return
}
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
fyne.Do(func() {
diag.Show()
})
go func() {
client.UploadFile(client.Ctx, reader.URI().Path(), progress)
}()
for update := range progress {
fyne.Do(func() {
myprogressbar.Value = float64(update.Percentage) / 100
myprogressbar.Refresh()
})
if update.Error != nil {
diag.Dismiss()
dialog.ShowError(update.Error, w)
return
}
if update.GetURL != "" {
link = update.GetURL
}
}
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
}()
}, w)
})
getMAM := fyne.NewMenuItem("send stanza", func() {
sendiq := false
b := container.NewVBox()
note := widget.NewRichTextFromMarkdown(fmt.Sprintf("# How to send a stanza\nThe stanza must be formed properly or the client will disconnect! Some actions such as joining rooms should be done via the client.\n Here is an example of a typical stanza:\n\n```\n<message from=abc@example.com to=xyz@example.com type=chat>\n\t<body>We have had a most delightful evening, a most excellent ball.</body>\n</message>\n```\n\nThe `from` attribute should be your JID `(%s)` and the `to` attribute should be the intended recipient. The `type` attribute can be `chat`, `groupchat`, `headline` and more.\n\nFor more information, read [RFC 6120](https://www.rfc-editor.org/rfc/rfc6120.html).", client.JID.String()))
note.Hidden = true
// note.Wrapping = fyne.TextWrapWord
b.Add(note)
isiq := widget.NewCheck("IQ stanza (get a reponse back)", func(b bool) {sendiq=b})
b.Add(isiq)
hlpbtn := widget.NewButton("Help!", func() {note.Hidden = !note.Hidden})
b.Add(hlpbtn)
entry := widget.NewMultiLineEntry()
entry.SetPlaceHolder(fmt.Sprintf("<message type='chat' from='%s' id='ilovepi' to='friend@home.org'><body>Hello from Pi!</body></message>", client.JID.String()))
b.Add(entry)
btn := widget.NewButton("Send", func() {
fmt.Println(sendiq)
msg := entry.Text
err := client.Session.Send(context.TODO(), xml.NewDecoder(strings.NewReader(msg)))
if err != nil {
dialog.ShowError(err, w)
return
}
})
b.Add(btn)
dialog.ShowCustom("Send a custom XML stanza", "Close", b, w)
})
leaveRoom := fyne.NewMenuItem("disconnect from current room", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
}
}
if !chatTabs[activeMucJid].isMuc {
return
}
AppTabs.Selected().Text = fmt.Sprintf("%s (disconnected)", AppTabs.Selected().Text)
AppTabs.Items = append(AppTabs.Items[:AppTabs.SelectedIndex()], AppTabs.Items[AppTabs.SelectedIndex()+1:]...)
AppTabs.SelectIndex(0)
err := client.DisconnectMuc(activeMucJid, "user left on own accord", context.TODO())
if err != nil {
dialog.ShowError(err, w)
}
delete(UITabs, activeMucJid)
})
manageBookmarks := fyne.NewMenuItem("manage bookmarks", func() {
bookmarks := client.BookmarkCache()
box := container.NewVBox()
box.Add(widget.NewRichTextFromMarkdown("# Manage Bookmarks"))
box.Add(widget.NewLabel(fmt.Sprintf("%d bookmarks", len(bookmarks))))
for address, bookmark := range bookmarks {
bookmarkWidget := container.NewGridWithColumns(7)
var nameLabel *widget.RichText
if bookmark.Name != "" {
nameLabel = widget.NewRichTextFromMarkdown(bookmark.Name)
} else {
nameLabel = widget.NewRichTextFromMarkdown("_no name_")
}
nameLabel.Wrapping = fyne.TextWrapBreak
bookmarkWidget.Add(nameLabel)
bookmarkJidWidget := widget.NewLabel(address)
bookmarkJidWidget.TextStyle.Monospace = true
bookmarkJidWidget.Selectable = true
bookmarkJidWidget.Wrapping = fyne.TextWrapWord
bookmarkWidget.Add(bookmarkJidWidget)
var autojoinCheck *widget.Check
var deleteBookmarkButton *widget.Button
var joinRoomButton *widget.Button
var setNick *widget.Button
joinRoomButton = widget.NewButtonWithIcon("Join", theme.AccountIcon(), func() {
fyne.Do(func() { joinRoomButton.Disable() })
go func() {
if bookmark.Nick == "" {
bookmark.Nick = client.Login.DisplayName
}
addChatTab(true, bookmark.JID, bookmark.Nick)
_, err := client.ConnectMuc(bookmark, oasisSdk.MucLegacyHistoryConfig{}, context.TODO())
if err != nil {
fyne.Do(func() {
joinRoomButton.Enable()
dialog.ShowError(err, w)
})
return
}
client.RefreshBookmarks(false)
fyne.Do(func() { joinRoomButton.Enable() })
}()
})
autojoinCheck = widget.NewCheck("Autojoin", func(b bool) {
go func() {
fyne.Do(func() { autojoinCheck.Disable() })
bookmark.Autojoin = b
err := client.PublishBookmark(bookmark, context.TODO())
if err != nil {
fyne.Do(func() {
autojoinCheck.Enable()
dialog.ShowError(err, w)
})
return
}
client.RefreshBookmarks(false)
fyne.Do(func() { autojoinCheck.Enable() })
}()
})
autojoinCheck.Checked = bookmark.Autojoin
deleteBookmarkButton = widget.NewButtonWithIcon("Delete", theme.CancelIcon(), func() {
go func() {
err := client.DeleteBookmark(bookmark.JID, context.TODO())
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
return
}
client.RefreshBookmarks(false)
bookmarkWidget.RemoveAll()
}()
})
nickEntry := widget.NewEntry()
nickEntry.SetText(bookmark.Nick)
setNick = widget.NewButton("Set nick", func() {
go func() {
fyne.Do(func() { setNick.Disable() })
newNick := nickEntry.Text
bookmark.Nick = newNick
err := client.PublishBookmark(bookmark, context.TODO())
if err != nil {
fyne.Do(func() {
setNick.Enable()
dialog.ShowError(err, w)
})
return
}
client.RefreshBookmarks(false)
fyne.Do(func() { setNick.Enable() })
}()
})
bookmarkWidget.Add(autojoinCheck)
bookmarkWidget.Add(deleteBookmarkButton)
bookmarkWidget.Add(joinRoomButton)
bookmarkWidget.Add(nickEntry)
bookmarkWidget.Add(setNick)
box.Add(bookmarkWidget)
}
myScroller := container.NewScroll(box)
AppTabs.Items[0].Content = myScroller
AppTabs.Items[0].Text = "Bookmarks"
AppTabs.SelectIndex(0)
chatSidebar.Hidden = true
//d := dialog.NewCustom("manage bookmarks", "cancel", myScroller, w)
//d.Show()
})
joinARoom := fyne.NewMenuItem("connect to a room", func() {
dialog.ShowEntryDialog("connect to a room", "JID:", func(s string) {
d := dialog.NewCustom("Please wait", "Close", widget.NewProgressBarInfinite(), w)
d.Show()
go func() {
myjid, err := jid.Parse(s)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
mychannel := new(bookmarks.Channel)
mychannel.JID = myjid
mychannel.Nick = login.DisplayName
//ch, err := client.MucClient.Join(client.Ctx, joinjid, client.Session)
addChatTab(true, myjid, login.DisplayName)
_, err = client.ConnectMuc(*mychannel, oasisSdk.MucLegacyHistoryConfig{}, context.TODO())
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
//client.MucChannels[s] = ch
d.Hide()
}()
}, w)
})
beginADM := fyne.NewMenuItem("start a DM", func() {
dialog.ShowEntryDialog("Start a DM", "JID:", func(s string) {
d := dialog.NewCustom("Please wait", "Close", widget.NewLabel("Opening a DM with "+s), w)
d.Show()
go func() {
myjid, err := jid.Parse(s)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
addChatTab(false, myjid, login.DisplayName)
d.Hide()
}()
}, w)
})
menu_help := fyne.NewMenu("π", mit, reconnect, licensesbtn)
menu_changeroom := fyne.NewMenu("Α", mic, beginADM, joinARoom, leaveRoom, manageBookmarks, getMAM)
menu_configureview := fyne.NewMenu("Β", mia, jtt, jtb)
hafjag := fyne.NewMenuItem("Hafjag", func() {
entry.Text = "Hafjag"
SendCallback()
entry.Text = "Hafjag"
})
hotfuck := fyne.NewMenuItem("Hot Fuck", func() {
d := dialog.NewConfirm("WARNING", "This button will send the message \"Hot Fuck\" into your currently focused chat. Do you want to continue?", func(b bool) {
if b {
agreesToSendingHotFuckIntoChannel = true
entry.Text = "Hot Fuck"
SendCallback()
entry.Text = "Oh Yeah."
}
}, w)
if agreesToSendingHotFuckIntoChannel {
d.Confirm()
} else {
d.Show()
}
})
agree := fyne.NewMenuItem("Agree", func() {
old := entry.Text
entry.Text = strings.Repeat("^", rand.IntN(30))
SendCallback()
entry.Text = old
})
kai := fyne.NewMenuItem("kai cenat beg", func() {
old := entry.Text
entry.Text = "chat will you subscribe to save the kai cenat mafiathon 3"
SendCallback()
entry.Text = old
})
mipipiemi := fyne.NewMenuItem("mi pipi e mi", func() {
old := entry.Text
entry.Text = "mi pipi e mi"
SendCallback()
entry.Text = old
})
wspeed := fyne.NewMenuItem("W Speed ❤️‍🩹❤️‍🩹", func() {
old := entry.Text
entry.Text = "W Speed ❤️‍🩹❤️‍🩹"
SendCallback()
entry.Text = old
})
exposed_suffix := fyne.NewMenuItem(".exposed", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
}
}
LatestMessage := chatTabs[activeChatJid].Messages[len(chatTabs[activeChatJid].Messages)-1]
old := entry.Text
entry.Text = fmt.Sprintf("%s.exposed", LatestMessage.Content)
SendCallback()
entry.Text = old
})
mycurrenttime := fyne.NewMenuItem("Current time", func() {
entry.Text = fmt.Sprintf("It is currently %s", time.Now().Format(time.RFC850))
SendCallback()
})
mycurrentplayingsong := fyne.NewMenuItem("Get currently playing song", func() {
// BEGIN PLATFORM SPECIFIC CODE
if isWindows() {
dialog.ShowError(errors.New("this feature is not supported on your operating system"), w)
return
}
// END PLATFORM SPECIFIC CODE
client, err := mpris.NewClient()
if err != nil {
dialog.ShowError(err, w)
return
}
present := false
for _, player := range client.Players() {
fmt.Println(player.RawMetadata())
old := entry.Text
newtext := ""
title, t_ok := player.RawMetadata()["xesam:title"]
artist, a_ok := player.RawMetadata()["xesam:artist"]
album, al_ok := player.RawMetadata()["xesam:album"]
artists := []string{}
if a_ok {
artist.Store(&artists)
}
if t_ok && a_ok && al_ok && album.String() != "\"\"" {
newtext = fmt.Sprintf("I'm currently listening to %s by %s, in the %s album", strings.Trim(title.String(), "\""), strings.Join(artists, ","), album)
} else if t_ok && a_ok {
newtext = fmt.Sprintf("I'm currently listening to %s by %s", strings.Trim(title.String(), "\""), strings.Join(artists, ","))
} else if t_ok {
newtext = fmt.Sprintf("I'm currently listening to %s", strings.Trim(title.String(), "\""))
} else if a_ok {
newtext = fmt.Sprintf("I'm currently listening to a song by %s", artists[0])
} else {
dialog.ShowError(errors.New("error: There's a playing song, but we could not get any information"), w)
return
}
entry.SetText(newtext)
SendCallback()
entry.SetText(old)
present = true
}
if !present {
dialog.ShowInformation("Failed", "Could not find any open players. You might need an MPRIS plugin for players such as mpv.\nSee the MPRIS ArchWiki article for more information:\nhttps://wiki.archlinux.org/title/MPRIS", w)
}
})
menu_jokes := fyne.NewMenu("Δ", mycurrenttime, mycurrentplayingsong, hafjag, hotfuck, agree, kai, mipipiemi, exposed_suffix, wspeed)
bit := fyne.NewMenuItem("mark selected message as read", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
}
}
m := chatTabs[activeMucJid].Messages[selectedId].Raw
client.MarkAsRead(&m)
})
rec := fyne.NewMenuItem("retract selected message", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
}
}
if !chatTabs[activeMucJid].isMuc {
return // TODO: For 1:1 DMs we use OccupantID for the ID and send message type 'chat'
}
id := chatTabs[activeMucJid].Messages[selectedId].Raw.StanzaID
if id == nil {
return
}
msg := fmt.Sprintf("<message type='groupchat' to='%s' id='retract-message-1'><retract id='%s' xmlns='urn:xmpp:message-retract:1'/><fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:message-retract:1' /><body>This user retracted a previous message, but it's unsupported by your client.</body><store xmlns='urn:xmpp:hints'/></message>", activeMucJid, id.ID)
fmt.Println(msg)
err := client.Session.Send(context.TODO(), xml.NewDecoder(strings.NewReader(msg)))
if err != nil {
dialog.ShowError(err, w)
}
})
bia := fyne.NewMenuItem("toggle replying to message", func() {
replying = !replying
})
bic := fyne.NewMenuItem("show message XML", func() {
pre := widget.NewLabel("")
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
m := chatTabs[activeChatJid].Messages[selectedId].Raw
bytes, err := xml.MarshalIndent(m, "", "\t")
if err != nil {
dialog.ShowError(err, w)
return
}
pre.SetText(string(bytes))
pre.Selectable = true
pre.Refresh()
dialog.ShowCustom("Message", "Close", pre, w)
})
blck := fyne.NewMenuItem("ignore messages from this user", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
m := chatTabs[activeChatJid].Messages[selectedId]
unknown_tags := m.Raw.Unknown
for _, v := range unknown_tags {
if v.XMLName.Local == "occupant-id" {
for _, attr := range v.Attrs {
if attr.Name.Local == "id" {
occupant_id := attr.Value
reason, ok := OccupantIdsToBlock[occupant_id]
if !ok {
dialog.ShowConfirm("ignore user", "All messages sent by users with an occupant ID of\n"+occupant_id+"\nwill be ignored. Continue?", func(b bool) {
if b {
OccupantIdsToBlock[occupant_id] = "User requested"
}
}, w)
} else {
dialog.ShowConfirm("unignore user", "This user is currently ignored due to:\n"+reason+"\nWould you like to unignore them?", func(b bool) {
if b {
delete(OccupantIdsToBlock, occupant_id)
}
}, w)
}
}
}
}
}
})
red := fyne.NewMenuItem("show read receipts on message", func() {
pre := container.NewVBox()
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
gen, _ := identicon.New("github", 5, 3)
m := chatTabs[activeChatJid].Messages[selectedId].Readers
for _, v := range m {
if chatTabs[activeChatJid].isMuc {
ii, _ := gen.Draw(v.Resourcepart())
im := ii.Image(25)
iw := canvas.NewImageFromImage(im)
iw.FillMode = canvas.ImageFillOriginal
pre.Add(container.NewHBox(iw, widget.NewLabel(v.Resourcepart())))
} else {
ii, _ := gen.Draw(v.Localpart())
im := ii.Image(25)
iw := canvas.NewImageFromImage(im)
iw.FillMode = canvas.ImageFillOriginal
pre.Add(container.NewHBox(iw, widget.NewLabel(v.Localpart())))
}
}
pre.Refresh()
dialog.ShowCustom("Message", "Close", pre, w)
})
menu_messageoptions := fyne.NewMenu("Γ", bit, bia, bic, rec, red, blck)
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions, menu_jokes)
w.SetMainMenu(ma)
desk, ok := a.(desktop.App)
if ok {
desk.SetSystemTrayMenu(fyne.NewMenu("", fyne.NewMenuItem("Show window", w.Show)))
}
AppTabs = container.NewDocTabs(
container.NewTabItem("...", widget.NewLabel(`
pi
This tab will be used for displaying certain actions, such as
managing your bookmarks and configuring rooms.
`)),
)
AppTabs.CloseIntercept = func(ti *container.TabItem) {
go func() {
var activeChatJid string
scroller, ok := ti.Content.(*widget.List)
if !ok {
return
}
for jid, tabData := range UITabs {
if tabData.Scroller == scroller {
activeChatJid = jid
break
}
}
if !chatTabs[activeChatJid].isMuc {
return
}
err := client.DisconnectMuc(activeChatJid, "user left on own accord", context.TODO())
if err != nil {
dialog.ShowError(err, w)
}
fyne.Do(func() {
AppTabs.Selected().Text = fmt.Sprintf("%s (disconnected)", AppTabs.Selected().Text)
AppTabs.Remove(ti)
delete(UITabs, activeChatJid)
})
}()
}
for _, userJidStr := range DMs {
fmt.Println(userJidStr)
DMjid, err := jid.Parse(userJidStr)
if err == nil {
addChatTab(false, DMjid, login.DisplayName)
}
}
AppTabs.OnUnselected = func(ti *container.TabItem) {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.Hidden = true
}
var MainSplit *container.Split
AppTabs.OnSelected = func(ti *container.TabItem) {
chatSidebar.Resize(chatSidebar.Size())
if AppTabs.Selected() == AppTabs.Items[0] {
chatSidebar.Hidden = true
return
}
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.Hidden = false
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
tab := chatTabs[activeChatJid]
UITab := UITabs[activeChatJid]
chatSidebar = *UITab.Sidebar
if tab.isMuc {
nameLabel := widget.NewRichTextFromMarkdown("# " + activeChatJid)
nameLabel.Truncation = fyne.TextTruncateEllipsis
memberLabel := widget.NewLabel(fmt.Sprintf("%d members ", len(tab.Members)))
memberLabel.Truncation = fyne.TextTruncateEllipsis
box := container.NewVBox(nameLabel)
// chatSidebar.Objects = []fyne.CanvasObject{}
for name, p := range tab.Members {
log.Println(string(p.Type))
gen, _ := identicon.New("github", 5, 3)
userjid, err := jid.Parse(name)
if err != nil {
fmt.Println("ERROR: " + err.Error())
continue // unrecoverable
}
nickname := userjid.Resourcepart()
if nickname == "" {
continue // we got the MUC presence, do not include it in the member list
}
ii, err := gen.Draw(nickname)
mention := func() {
entry.SetText(fmt.Sprintf("%s %s", entry.Text, nickname))
}
if err != nil {
fmt.Println("ERROR: " + err.Error())
box.Add(container.NewHBox(widget.NewLabel(nickname), widget.NewButton("Mention", mention)))
} else {
im := ii.Image(15)
imageWidget := canvas.NewImageFromImage(im)
imageWidget.FillMode = canvas.ImageFillOriginal
imageWidget.Refresh()
nickLabel := widget.NewLabel(nickname)
nickLabel.Selectable = true
nickLabel.Truncation = fyne.TextTruncateClip
box.Add(container.NewHBox(imageWidget, nickLabel))
if p.Status != "" {
s := widget.NewLabel(fmt.Sprintf("\"%s\"", p.Status))
s.Importance = widget.WarningImportance
s.TextStyle = fyne.TextStyle{
Bold: false,
Italic: true,
Monospace: false,
}
s.Truncation = fyne.TextTruncateEllipsis
box.Add(s)
box.Refresh()
}
}
}
chatSidebar = *container.NewGridWithColumns(1, container.NewVScroll(box))
} else {
if activeChatJid == config.Login.User {
chatSidebar = *container.NewVBox(widget.NewRichTextFromMarkdown("# "+activeChatJid), widget.NewLabel("Hey, that's you!"))
} else {
chatSidebar = *container.NewVBox(widget.NewRichTextFromMarkdown("# " + activeChatJid))
}
}
chatSidebar.Hidden = false
chatSidebar.Show()
MainSplit.Resize(MainSplit.Size())
}
chatSidebar.Hidden = false
statBar.SetText("")
chatInfo = *container.NewHBox(widget.NewLabel(""))
MainSplit = container.NewHSplit(AppTabs, &chatSidebar)
DownSplit := container.NewHSplit(entry, container.NewGridWithRows(1, sendbtn, replybtn))
BigSplit := container.NewVSplit(MainSplit, DownSplit)
BigSplit.SetOffset(1)
SmallSplit := container.NewHSplit(&statBar, &chatInfo)
MasterSplit := container.NewVSplit(BigSplit, SmallSplit)
MasterSplit.SetOffset(1)
w.SetContent(MasterSplit)
w.ShowAndRun()
}