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\n\tWe have had a most delightful evening, a most excellent ball.\n\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("Hello from Pi!", 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("This user retracted a previous message, but it's unsupported by your client.", 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()
}