package main import ( //core - required "context" "encoding/xml" "errors" "fmt" "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" 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/bookmarks" "mellium.im/xmpp/jid" "mellium.im/xmpp/muc" oasisSdk "pain.agency/oasis-sdk" // TODO: integrated theme switcher ) var version string = "3.1i" 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 // 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 := widget.NewLabel("author") 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 btn := widget.NewButtonWithIcon("View media", icon, func() { }) 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].(*widget.Label) content := vbox.Objects[2].(*widget.Label) 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 } 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 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 %s", replyBodyIcon, strings.ReplaceAll(chatTabs[chatJidStr].Messages[i].Content, "\n", newlineIcon))) guy = chatTabs[chatJidStr].Messages[i].Author break } } author.SetText(fmt.Sprintf("%s %s %s", chatTabs[chatJidStr].Messages[i].Author, replyNameIcon, 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) 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")) 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") 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)) } 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)) } } 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() { 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.SetPresenceHandler(func(client *oasisSdk.XmppClient, from jid.JID, p oasisSdk.UserPresence) { bareAcc := from.Bare() tab, ok := chatTabs[bareAcc.String()] if !ok { // User presence addChatTab(false, bareAcc, client.Login.DisplayName) return } if tab.isMuc { tab.Members[from.String()] = p } }) client.SetBookmarkHandler(false, func(client *oasisSdk.XmppClient, bookmark bookmarks.Channel) { // 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 (confirm) or minimize to the tray? (cancel)", func(b bool) { if b { w.Close() a.Quit() log.Fatalln("Goodbye!") } else { w.Hide() } }, w) }) 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 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()) } 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) }) 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")) for address, bookmark := range bookmarks { bookmarkWidget := container.NewGridWithColumns(5) nameLabel := widget.NewLabel(bookmark.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 joinRoomButton = widget.NewButtonWithIcon("Join", theme.AccountIcon(), func() { fyne.Do(func() { joinRoomButton.Disable() }) go func() { if bookmark.Nick == "" { bookmark.Nick = client.Login.DisplayName } var zero uint64 = uint64(0) addChatTab(true, bookmark.JID, bookmark.Nick) _, err := client.ConnectMuc(bookmark, oasisSdk.MucLegacyHistoryConfig{MaxCount: &zero}, 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() }() }) bookmarkWidget.Add(autojoinCheck) bookmarkWidget.Add(deleteBookmarkButton) bookmarkWidget.Add(joinRoomButton) 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) { 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 } 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) num := uint64(0) _, err = client.ConnectMuc(*mychannel, oasisSdk.MucLegacyHistoryConfig{MaxCount: &num}, 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) { 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) }) menu_help := fyne.NewMenu("π", mit, reconnect, licensesbtn) menu_changeroom := fyne.NewMenu("Α", mic, beginADM, joinARoom, leaveRoom, manageBookmarks) 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 }) 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) 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) desk, ok := a.(desktop.App) if ok { desk.SetSystemTrayMenu(fyne.NewMenu("", fyne.NewMenuItem("show", 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) } 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.OnSelected = func(ti *container.TabItem) { if AppTabs.Selected() == AppTabs.Items[0] { chatSidebar.Hidden = true return } 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] chatSidebar = *UITab.Sidebar if tab.isMuc { nameLabel := widget.NewRichTextFromMarkdown("# " + activeChatJid) nameLabel.Truncation = fyne.TextTruncateEllipsis box := container.NewVBox(nameLabel, widget.NewLabel(fmt.Sprintf("%d members ", len(tab.Members)))) chatSidebar.Objects = []fyne.CanvasObject{} for name, p := range tab.Members { 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 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) } } } chatSidebar = *container.NewGridWithColumns(1, container.NewVScroll(box)) } else { chatSidebar = *container.NewVBox(widget.NewRichTextFromMarkdown("# " + activeChatJid)) } chatSidebar.Hidden = false chatSidebar.Refresh() } 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() }