package main import ( //core - required "encoding/xml" "errors" "fmt" _ "image/color" "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/storage" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" extraWidgets "fyne.io/x/fyne/widget" "github.com/makeworld-the-better-one/go-isemoji" "github.com/rrivera/identicon" "github.com/shreve/musicwand/pkg/mpris" // xmpp - required _ "mellium.im/xmpp/disco" "mellium.im/xmpp/jid" "mellium.im/xmpp/muc" _ "mellium.im/xmpp/stanza" oasisSdk "pain.agency/oasis-sdk" // gui - optional // catppuccin "github.com/mbaklor/fyne-catppuccin" // TODO: integrated theme switcher ) var version string = "3i" var statBar widget.Label var chatInfo fyne.Container var chatSidebar fyne.Container var agreesToSendingHotFuckIntoChannel bool = false // 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 } type ChatTab struct { Jid jid.JID Nick string Messages []Message isMuc bool Muc *muc.Channel UpdateSidebar bool } type ChatTabUI struct { Internal *ChatTab Scroller *widget.List `xml:"-"` Sidebar *fyne.Container `xml:"-"` } type CustomMultiLineEntry struct { widget.Entry } 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.AppTabs var selectedId widget.ListItemID var replying bool = false var notifications bool var connection bool = true /* func (m myTheme) Font(style fyne.TextStyle) fyne.Resource { return resourceAppleColorEmojiTtf } func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { return adwaita.AdwaitaTheme().Color(name, fyne.CurrentApp().Settings().ThemeVariant()) } func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(name) } /* func (m myTheme) Font(style fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(style) } */ 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 := widget.NewLabel("author") author.TextStyle.Bold = true content := widget.NewLabel("content") content.Wrapping = fyne.TextWrapWord content.Selectable = true icon := theme.FileVideoIcon() replytext := widget.NewLabel("> fallback reply text") replytext.Hide() replytext.Importance = widget.SuccessImportance btn := widget.NewButtonWithIcon("View media", icon, func() { }) return container.NewVBox(replytext, container.NewHBox(ico, author), content, btn) }, 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].(*widget.Label) content := vbox.Objects[2].(*widget.Label) btn := vbox.Objects[3].(*widget.Button) if chatTabs[chatJidStr].Messages[i].Important { //content.Importance = widget.DangerImportance TODO: Fix highlighting messages with mentions, it's currently broken } btn.Hidden = true // Hide by default msgContent := chatTabs[chatJidStr].Messages[i].Content if chatTabs[chatJidStr].Messages[i].ImageURL != "" { btn.Hidden = false btn.OnTapped = func() { go func() { u, err := storage.ParseURI(chatTabs[chatJidStr].Messages[i].ImageURL) 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].ImageURL) 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 guy := jid.MustParse(reply.To).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.SetText(fmt.Sprintf("> %s", chatTabs[chatJidStr].Messages[i].Content)) guy = chatTabs[chatJidStr].Messages[i].Author break } } author.SetText(fmt.Sprintf("%s > %s", chatTabs[chatJidStr].Messages[i].Author, guy)) } else { author.SetText(chatTabs[chatJidStr].Messages[i].Author) replytext.Hide() } sl := strings.Split(msgContent, " ") if len(sl) == 1 && isemoji.IsEmoji(sl[0]) { content.SizeName = fyne.ThemeSizeName(theme.SizeNameHeadingText) content.Refresh() } if sl[0] == "/me" { author.SetText(author.Text + " " + strings.Join(sl[1:], " ")) content.SetText(" ") } scroller.SetItemHeight(i, vbox.MinSize().Height) vbox.Refresh() }, ) 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, } 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() { AppTabs.Append(container.NewTabItemWithIcon(chatJid.String(), icon, myUITab.Scroller)) }) } 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")) 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{} a = app.NewWithID("pi-im") //a.Settings().SetTheme(&myTheme{}) 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 client, err := oasisSdk.CreateClient( &login) client.SetDmHandler(func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) { correction := false userJidStr := msg.From.Bare().String() tab, ok := chatTabs[userJidStr] if ok { str := *msg.CleanedBody if notifications { a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str)) } 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 { 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 + " (edited)" 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, } 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) { // HACK: IGNORING ALL MESSAGES FROM CLASSIC MUC HISTORY IN PREPARATION OF MAM SUPPORT ignore := 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 } } var ImageID string = "" mucJidStr := msg.From.Bare().String() if tab, ok := chatTabs[mucJidStr]; ok { chatTabs[mucJidStr].Muc = muc str := *msg.CleanedBody if strings.Contains(str, login.DisplayName) { fmt.Println(str) 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)) } } 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, " ") fmt.Println(str) } fmt.Println(msg.ID) var replyID string if msg.Reply == nil { replyID = "PICLIENT:UNAVAILABLE" } else { replyID = msg.Reply.To } 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 + " (edited)" 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, } if !ignore { tab.Messages = append(tab.Messages, myMessage) } fyne.Do(func() { UITabs[mucJidStr].Scroller.Refresh() if scrollDownOnNewMessage { UITabs[mucJidStr].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.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-- { fmt.Println(tab.Messages[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 } } } fmt.Printf("%s has seen %s\n", from.String(), id) }) 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() { 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() //a.Settings().SetTheme(&myTheme{}) w = a.NewWindow("pi") w.Resize(fyne.NewSize(500, 500)) 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 AppTabs.Selected() == nil || AppTabs.Selected().Content == nil || text == "" { 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 fmt.Println(selectedId) 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 } }, w) } 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("license", 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) }) mis := fyne.NewMenuItem("clear chat window", func() { dialog.ShowConfirm("clear chat window", "are you sure you want to clear the chat window?", func(b bool) { if b { fmt.Println("clearing chat") } }, 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) }) leaveRoom := fyne.NewMenuItem("Leave current room (experimental)", 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 } } AppTabs.Selected().Text = fmt.Sprintf("%s (disconnected)", AppTabs.Selected().Text) AppTabs.SelectIndex(0) delete(client.MucChannels, activeMucJid) //delete(chatTabs, activeMucJid) }) joinARoom := fyne.NewMenuItem("Join a room", func() { dialog.ShowEntryDialog("Join a room", "JID:", func(s string) { i := resourcePiloadingGif gif, err := extraWidgets.NewAnimatedGifFromResource(i) if err != nil { panic(err) } gif.Start() gif.Show() d := dialog.NewCustom("Please wait", "Close", gif, w) d.Show() go func() { myjid, err := jid.Parse(s) if err != nil { d.Hide() dialog.ShowError(err, w) return } joinjid, err := myjid.WithResource(login.DisplayName) if err != nil { d.Hide() dialog.ShowError(err, w) return } ch, err := client.MucClient.Join(client.Ctx, joinjid, client.Session) if err != nil { d.Hide() dialog.ShowError(err, w) return } client.MucChannels[s] = ch addChatTab(true, myjid, login.DisplayName) d.Hide() }() }, w) }) beginADM := fyne.NewMenuItem("Start a DM", func() { dialog.ShowEntryDialog("Start a DM", "JID:", func(s string) { i := resourcePiloadingGif gif, err := extraWidgets.NewAnimatedGifFromResource(i) if err != nil { panic(err) } gif.Start() gif.Show() d := dialog.NewCustom("Please wait", "Close", gif, 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) }) /* servDisc := fyne.NewMenuItem("Disco features", func() { //var search jid.JID dialog.ShowEntryDialog("Disco features", "JID: ", func(s string) { // TODO: replace with undeprecated widgetd d := dialog.NewCustom("Please wait", "Close", widget.NewLabel("..."), w) d.Show() go func() { //search, err = jid.Parse(s) //if err != nil { // d.Hide() // dialog.ShowError(err, w) // return //} txt := ` ` var stan stanza.IQ xml.Unmarshal([]byte(txt), &stan) if err != nil { d.Hide() dialog.ShowError(err, w) return } r, err := client.Session.EncodeIQ(client.Ctx, stan) if err != nil { d.Hide() dialog.ShowError(err, w) return } ra, _ := r.Token() t, _ := xml.MarshalIndent(ra, "", "\t") fmt.Println(string(t)) d.Hide() /* myBox := container.NewGridWithColumns(1, widget.NewLabel("Items")) info, err := disco.GetInfo(client.Ctx, "", search, client.Session) if err != nil { d.Hide() dialog.ShowError(err, w) return } m := info.Identity bytes, err := xml.MarshalIndent(m, "", "\t") if err != nil { d.Hide() dialog.ShowError(err, w) return } fyne.Do(func() {d.Hide()}) myBox.Objects = append(myBox.Objects, widget.NewLabel(string(bytes))) dialog.ShowCustom("Service discovery", "cancel", myBox, w) }() }, w) }) */ savedata := fyne.NewMenuItem("DEBUG: Save tab data to disk", func() { d := []ChatTab{} for _, v := range chatTabs { d = append(d, *v) } b, err := xml.Marshal(d) if err != nil { dialog.ShowError(err, w) return } os.Create("test.xml") os.WriteFile("text.xml", b, os.ModeAppend) }) jbookmarks := fyne.NewMenuItem("Join rooms in bookmarks", func() { // FIXME: Race condition client.FetchBookmarks() rooms := client.BookmarkCache() for _, v := range rooms { go func() { if v.Autojoin == true { joinjid, err := v.JID.WithResource(login.DisplayName) if err != nil { dialog.ShowError(err, w) return } room, err := client.MucClient.Join(client.Ctx, joinjid, client.Session) if err != nil { dialog.ShowError(err, w) return } client.MucChannels[v.JID.String()] = room addChatTab(true, v.JID, login.DisplayName) } }() } }) menu_help := fyne.NewMenu("π", mit, reconnect, licensesbtn, savedata) menu_changeroom := fyne.NewMenu("Α", mic, beginADM, joinARoom, leaveRoom, jbookmarks) menu_configureview := fyne.NewMenu("Β", mia, mis, 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 }) 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() { 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"] if t_ok && a_ok { newtext = fmt.Sprintf("I'm currently listening to %s by %s", title.String(), artist.String()) } else if t_ok { newtext = fmt.Sprintf("I'm currently listening to %s", title.String()) } else if a_ok { newtext = fmt.Sprintf("I'm currently listening to a song by %s", artist.String()) } else { dialog.ShowError(errors.New("error: There's a playing song, but we could not get the artist or title 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, hafjag, hotfuck, agree, mycurrentplayingsong) 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) }) 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) }) 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, red) ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions, menu_jokes) w.SetMainMenu(ma) AppTabs = container.NewAppTabs( container.NewTabItem("τίποτα", widget.NewLabel(` pi `)), ) for _, mucJidStr := range login.MucsToJoin { mucJid, err := jid.Parse(mucJidStr) if err == nil { addChatTab(true, mucJid, login.DisplayName) } } for _, userJidStr := range DMs { fmt.Println(userJidStr) DMjid, err := jid.Parse(userJidStr) if err == nil { addChatTab(false, DMjid, login.DisplayName) } } AppTabs.OnSelected = func(ti *container.TabItem) { 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 } } tab := chatTabs[activeChatJid] UITab := UITabs[activeChatJid] if tab.isMuc { //chatInfo = *container.NewHBox(widget.NewLabel(tab.Muc.Addr().String())) } else { chatInfo = *container.NewHBox(widget.NewLabel(tab.Jid.String())) } chatSidebar = *UITab.Sidebar old := chatSidebar.Position() chatSidebar.Refresh() chatSidebar.Move(old) } // HACK - disable chatsidebar because it's currently very buggy chatSidebar.Hidden = true statBar.SetText("") w.SetContent(container.NewVSplit(container.NewVSplit(AppTabs, container.NewHSplit(entry, container.NewGridWithRows(1, sendbtn, replybtn))), container.NewHSplit(&statBar, &chatInfo))) w.ShowAndRun() }