diff --git a/.gitignore b/.gitignore index aaadf73..e293e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ go.work.sum # env file .env - +pi.json # Editor/IDE # .idea/ # .vscode/ diff --git a/README.md b/README.md index 95489b5..a65866d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,57 @@ -# pi +# π Experimental and extremely weird XMPP client made with Go. No solicitors. pi is currently pre-pre-pre-pre alpha software which you should not use right now. -pi uses [Fyne](https://fyne.io) for the frontend. +pi uses [Fyne](https://fyne.io) for the frontend and uses the [Oasis SDK](https://github.com/jjj333-p/oasis-sdk) for XMPP functionality. + +pi is an extremely opinionated client. It aims to have as little extra windows as possible, instead using alt-menus to perform many of the actions you'd see in a typical client. + + +## διαμόρφωση +(configuration) + +In order to use pi, you currently have to create a `pi.json` file in the working directory of the executable. Here is how one looks like: + +```json +{ + "Host":"example.com:5222", + "User":"user@example.com", + "Password":"123456", + "DisplayName":"user", + "NoTLS":false, + "StartTLS":true, + "Mucs":["room@muc.example.com"]} +``` + +Edit this file as necessary. + +Currently joining and saving DM tabs is not supported, nor is getting avatars, reactions, encryption of media embed. + +As of writing, pi supports basic message sending and receiving, replies and file upload. + + +## να χτίσω +(building) + +To build pi, you will need the latest version of Go, at least 1.21. You can grab it [here](https://go.dev). + +The build instructions are very simple. Simply clone the repo, fetch the repositories and build the program: + +Here is a summary of the commands you would need to use to build and run the program: +```bash +git clone https://github.com/sunglocto/pi +cd pi +go mod tidy +go build . +vim pi.json +./pi +``` + +Static executable snapshots are also provided for GNU/Linux systems. + +## χρήση +(usage) + +TODO diff --git a/go.mod b/go.mod index d358a82..7b9838b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.24.5 require ( fyne.io/fyne/v2 v2.6.2 + fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb + github.com/mbaklor/fyne-catppuccin v0.0.2 mellium.im/xmpp v0.22.0 pain.agency/oasis-sdk v0.0.0-20250803100711-2ed1355344d4 ) @@ -11,6 +13,7 @@ require ( require ( fyne.io/systray v1.11.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/catppuccin/go v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -28,7 +31,6 @@ require ( github.com/hack-pad/safejs v0.1.0 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/kr/text v0.2.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 58b0a22..fc83902 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,12 @@ fyne.io/fyne/v2 v2.6.2 h1:RPgwmXWn+EuP/TKwO7w5p73ILVC26qHD9j3CZUZNwgM= fyne.io/fyne/v2 v2.6.2/go.mod h1:9IJ8uWgzfcMossFoUkLiOrUIEtaDvF4nML114WiCtXU= fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb h1:2BazNmb/kwgqRdvE9L+NgW8sfoWGn3iy1Ox8R4+CSmc= +fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb/go.mod h1:u3LF1EkElytjOT8OHxft16trctGndF9qpsoH6YIDOUU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -47,6 +50,8 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9 github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mbaklor/fyne-catppuccin v0.0.2 h1:yMNnYkmFwjKJkFQvCd1uNKZDs07ZC85wTkedTIGcViE= +github.com/mbaklor/fyne-catppuccin v0.0.2/go.mod h1:ZBIy4dV1yMj+7oEaZYkXm5OfYESmXuPWwNcuUmD1Njo= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= diff --git a/main.go b/main.go index 4a8ae29..f645128 100644 --- a/main.go +++ b/main.go @@ -1,58 +1,244 @@ package main import ( + "encoding/json" "fmt" "image/color" + "io" "log" + _ "net/url" + "os" + "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/canvas" + _ "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + _ "fyne.io/x/fyne/theme" + catppuccin "github.com/mbaklor/fyne-catppuccin" "mellium.im/xmpp/jid" "mellium.im/xmpp/muc" + "mellium.im/xmpp/stanza" oasisSdk "pain.agency/oasis-sdk" ) +// by sunglocto +// license AGPL -func main() { +type Message struct { + Author string + Content string + ID string + ReplyID string + Raw oasisSdk.XMPPChatMessage +} - login := oasisSdk.LoginInfo{ - Host: "sunglocto.net:5222", - User: "bot2@sunglocto.net", - Password: "iloverobots", - DisplayName: "bot2", - TLSoff: false, - StartTLS: true, - MucsToJoin: []string{"ringen@muc.isekai.rocks"}, +type MucTab struct { + Jid jid.JID + Nick string + Messages []Message + Scroller *widget.List + isMuc bool +} + +var chatTabs = make(map[string]*MucTab) +var tabs *container.AppTabs +var selectedId widget.ListItemID +var replying bool = false +var notifications bool = true + +type myTheme struct{} + +func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { + return catppuccin.New().Color(name, variant) +} + +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) +} + +func (m myTheme) Size(name fyne.ThemeSizeName) float32 { + if name == theme.SizeNameHeadingText { + return 18 + } + return theme.DefaultTheme().Size(name) +} + +var scrollDownOnNewMessage bool = true +var w fyne.Window +var a fyne.App + +func addChatTab(isMuc bool, chatJid jid.JID, nick string) { + mucJidStr := chatJid.String() + if _, ok := chatTabs[mucJidStr]; ok { + // Tab already exists + return } - maina := container.New(layout.NewHBoxLayout(), widget.NewLabel("pi")) - scroller := container.NewVScroll(maina) + tabData := &MucTab{ + Jid: chatJid, + Nick: nick, + Messages: []Message{}, + isMuc: isMuc, + } + + var scroller *widget.List + scroller = widget.NewList( + func() int { + return len(tabData.Messages) + }, + func() fyne.CanvasObject { + author := widget.NewLabel("author") + author.TextStyle.Bold = true + content := widget.NewRichTextWithText("content") + content.Wrapping = fyne.TextWrapWord + return container.NewVBox(author, content) + }, + func(i widget.ListItemID, co fyne.CanvasObject) { + vbox := co.(*fyne.Container) + author := vbox.Objects[0].(*widget.Label) + content := vbox.Objects[1].(*widget.RichText) + content.ParseMarkdown(tabData.Messages[i].Content) + if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" { + author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart())) + } else { + author.SetText(tabData.Messages[i].Author) + } + scroller.SetItemHeight(i, vbox.MinSize().Height) + }, + ) + scroller.OnSelected = func(id widget.ListItemID) { + selectedId = id + } + tabData.Scroller = scroller + + chatTabs[mucJidStr] = tabData + + tabItem := container.NewTabItem(chatJid.Localpart(), scroller) + tabs.Append(tabItem) +} + +func main() { + login := oasisSdk.LoginInfo{} + + DMs := []string{} + + bytes, err := os.ReadFile("./pi.json") + if err != nil { + a = app.New() + w = a.NewWindow("Error") + w.Resize(fyne.NewSize(500, 500)) + dialog.ShowInformation("Error", fmt.Sprintf("Please make sure there is a file named pi.json in the same directory you are running this executable...\n%s", err.Error()), w) + w.ShowAndRun() + return + } + err = json.Unmarshal(bytes, &login) + if err != nil { + a = app.New() + w = a.NewWindow("Error") + w.Resize(fyne.NewSize(500, 500)) + dialog.ShowError(err, w) + w.ShowAndRun() + return + } client, err := oasisSdk.CreateClient( &login, func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) { - fyne.Do(func(){ - card := widget.NewCard(msg.From.String(), *msg.CleanedBody, canvas.NewCircle(color.White)) - maina.Add(card) - maina.Refresh() - scroller.ScrollToBottom() - }) + fmt.Println(msg) + userJidStr := msg.From.Bare().String() + tab, ok := chatTabs[userJidStr] + fmt.Println(msg.From.String()) + if ok { + str := *msg.CleanedBody + if notifications { + a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str)) + } + /* + if strings.Contains(str, "https://") { + fmt.Println("Attempting to do URL thingy") + s := strings.Split(str, " ") + for i, v := range s { + _, err := url.Parse(v) + if err == nil { + s[i] = fmt.Sprintf("[%s](%s)", v, v) + } + } + str = strings.Join(s, " ") + }*/ + var replyID string + if msg.Reply == nil { + replyID = "PICLIENT:UNAVAILABLE" + } else { + replyID = msg.Reply.ID + } + myMessage := Message{ + Author: msg.From.Resourcepart(), + Content: str, + ID: msg.ID, + ReplyID: replyID, + Raw: *msg, + } + + tab.Messages = append(tab.Messages, myMessage) + fyne.Do(func() { + tab.Scroller.Refresh() + if scrollDownOnNewMessage { + tab.Scroller.ScrollToBottom() + } + }) + } }, func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) { - fyne.Do(func(){ - if msg.Reply != nil { + mucJidStr := msg.From.Bare().String() + if tab, ok := chatTabs[mucJidStr]; ok { + str := *msg.CleanedBody + if notifications { + if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.User)) { + a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str)) + } } - card := widget.NewCard(msg.From.String(), *msg.CleanedBody, canvas.NewCircle(color.White)) - maina.Add(card) - maina.Refresh() - scroller.ScrollToBottom() - }) + /* + if strings.Contains(str, "https://") { + s := strings.Split(str, " ") + for i, v := range s { + _, err := url.Parse(v) + if err == nil { + s[i] = fmt.Sprintf("[%s](%s)", v, v) + } + } + str = strings.Join(s, " ") + }*/ + fmt.Println(msg.ID) + var replyID string + if msg.Reply == nil { + replyID = "PICLIENT:UNAVAILABLE" + } else { + replyID = msg.Reply.To + } + myMessage := Message{ + Author: msg.From.Resourcepart(), + Content: str, + ID: msg.ID, + ReplyID: replyID, + Raw: *msg, + } + tab.Messages = append(tab.Messages, myMessage) + fyne.Do(func() { + tab.Scroller.Refresh() + if scrollDownOnNewMessage { + tab.Scroller.ScrollToBottom() + } + }) + } }, func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) { //fromStr := from.String() @@ -72,48 +258,214 @@ func main() { fmt.Printf("%s has seen %s", from.String(), id) }, ) + if err != nil { log.Fatalln("Could not create client - " + err.Error()) } go func() { - err = client.Connect() - if err != nil { - log.Fatalln("Could not connect - " + err.Error()) - } + err = client.Connect() + if err != nil { + log.Fatalln("Could not connect - " + err.Error()) + } }() - a := app.New() - w := a.NewWindow("pi") - w.Resize(fyne.NewSize(500,500)) + a = app.New() + a.Settings().SetTheme(myTheme{}) + w = a.NewWindow("pi") + w.Resize(fyne.NewSize(500, 500)) + + entry := widget.NewMultiLineEntry() + entry.SetPlaceHolder("Say something, you know you want to.") + + sendbtn := widget.NewButton("Send", func() { + text := entry.Text + if tabs.Selected() == nil || tabs.Selected().Content == nil { + return + } + + selectedScroller, ok := tabs.Selected().Content.(*widget.List) + if !ok { + return + } + + var activeMucJid string + var isMuc bool + for jid, tabData := range chatTabs { + if tabData.Scroller == selectedScroller { + activeMucJid = jid + isMuc = tabData.isMuc + break + } + } + + if activeMucJid == "" { + return + } + + go func() { + //TODO: Fix message hack until jjj adds message sending + if replying { + m := chatTabs[activeMucJid].Messages[selectedId].Raw + client.ReplyToEvent(&m, text) + return + } + var typ stanza.MessageType + if isMuc { + typ = stanza.GroupChatMessage + } else { + typ = stanza.ChatMessage + } + msg := oasisSdk.XMPPChatMessage{ + Message: stanza.Message{ + To: jid.MustParse(activeMucJid), + Type: typ, + }, + ChatMessageBody: oasisSdk.ChatMessageBody{ + Body: &text, + }, + } + err := client.Session.Encode(client.Ctx, msg) + if err != nil { + dialog.ShowError(err, w) + } + }() + + if !isMuc { + chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{ + Author: "You", + Content: text, + }) + fyne.Do(func() { + if scrollDownOnNewMessage { + chatTabs[activeMucJid].Scroller.ScrollToBottom() + } + }) + } + entry.SetText("") + }) + mit := fyne.NewMenuItem("About pi", func() { - dialog.ShowInformation("About pi", "the XMPP client from hell\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w) + dialog.ShowInformation("About pi", "the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", 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) }) mib := fyne.NewMenuItem("Join a room", func() { - nick := widget.NewEntry() - room := widget.NewEntry() + nickEntry := widget.NewEntry() + nickEntry.SetText(login.DisplayName) + roomEntry := widget.NewEntry() items := []*widget.FormItem{ - widget.NewFormItem("Nick", nick), - widget.NewFormItem("MUC address", room), + widget.NewFormItem("Nick", nickEntry), + widget.NewFormItem("MUC address", roomEntry), } dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) { if b { - fmt.Println("attempting to join MUC") - fmt.Println(nick) - fmt.Println(room) - go func(){ - client.MucClient.Join(client.Ctx, jid.MustParse(room.Text), client.Session, nil) + roomJid, err := jid.Parse(roomEntry.Text) + if err != nil { + dialog.ShowError(err, w) + return + } + nick := nickEntry.Text + go func() { + // We probably don't need to handle the error here, if it fails the user will know + _, err := client.MucClient.Join(client.Ctx, roomJid, client.Session, nil) + if err != nil { + panic(err) + } }() + addChatTab(true, roomJid, nick) } }, w) }) + + mic := fyne.NewMenuItem("Upload a file", func() { + dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { + if err != nil { + dialog.ShowError(err, w) + } + bytes, err := io.ReadAll(reader) + link, err := client.UploadFileFromBytes(reader.URI().String(), bytes) + if err != nil { + dialog.ShowError(err, w) + return + } + a.Clipboard().SetContent(link) + dialog.ShowInformation("File successfully uploaded", link, w) + }, w) + }) + menu_help := fyne.NewMenu("π", mit) - menu_changeroom := fyne.NewMenu("β", mib) - ma := fyne.NewMainMenu(menu_help, menu_changeroom) + menu_changeroom := fyne.NewMenu("β", mib, mic) + menu_configureview := fyne.NewMenu("γ", mia, mis) + bit := fyne.NewMenuItem("Mark message as read", func() { + selectedScroller, ok := tabs.Selected().Content.(*widget.List) + if !ok { + return + } + var activeMucJid string + for jid, tabData := range chatTabs { + 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 + }) + menu_messageoptions := fyne.NewMenu("Σ", bit, bia) + ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions) w.SetMainMenu(ma) - tabs := container.NewAppTabs(container.NewTabItem("pi", widget.NewLabel("pi\nthe XMPP client from hell")), container.NewTabItem("chat", scroller)) - w.SetContent(tabs) + tabs = container.NewAppTabs( + container.NewTabItem("τίποτα", widget.NewRichTextFromMarkdown("# No chat selected.")), + ) + + 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) + } + } + + w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn))) w.ShowAndRun() } diff --git a/main.go.copy b/main.go.copy new file mode 100644 index 0000000..78000f5 --- /dev/null +++ b/main.go.copy @@ -0,0 +1,210 @@ +package main + +import ( + "fmt" + "image/color" + "log" + + "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/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + _ "github.com/mbaklor/fyne-catppuccin" + _ "fyne.io/x/fyne/theme" + "mellium.im/xmpp/jid" + "mellium.im/xmpp/muc" + "mellium.im/xmpp/stanza" + oasisSdk "pain.agency/oasis-sdk" +) + +type Message struct { + Author string + Content string +} + +var Messages []Message + +type myTheme struct{} + +func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { + return theme.DefaultTheme().Color(name, variant) +} + +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) +} + +func (m myTheme) Size(name fyne.ThemeSizeName) float32 { + if name == theme.SizeNameHeadingText { + return 18 + } + return theme.DefaultTheme().Size(name) +} + +var scrollDownOnNewMessage bool = true + +func addMessageToView() fyne.CanvasObject { + author := widget.NewLabel("") + author.TextStyle.Bold = true + content := widget.NewLabel("") + content.Wrapping = fyne.TextWrapWord + return container.NewVBox(author, content) +} + +func main() { + + login := oasisSdk.LoginInfo{ + Host: "sunglocto.net:5222", + User: "bot2@sunglocto.net", + Password: "iloverobots", + DisplayName: "bot2", + TLSoff: false, + StartTLS: true, + MucsToJoin: []string{"ringen@muc.isekai.rocks"}, + } + + scroller := widget.NewList( + func() int { + return len(Messages) + }, func() fyne.CanvasObject { + return addMessageToView() + }, func(i widget.ListItemID, co fyne.CanvasObject) { + co.(*fyne.Container).Objects[0].(*widget.Label).SetText(Messages[i].Author) + co.(*fyne.Container).Objects[1].(*widget.Label).SetText(Messages[i].Content) + }) + + client, err := oasisSdk.CreateClient( + &login, + func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) { + myMessage := Message{} + myMessage.Author = msg.From.Resourcepart() + myMessage.Content = *msg.CleanedBody + Messages = append(Messages, myMessage) + if scrollDownOnNewMessage { + fyne.Do(func() { + scroller.ScrollToBottom() + }) + } + }, + func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) { + myMessage := Message{} + myMessage.Author = msg.From.Resourcepart() + myMessage.Content = *msg.CleanedBody + Messages = append(Messages, myMessage) + if scrollDownOnNewMessage { + fyne.Do(func() { + scroller.ScrollToBottom() + }) + } + }, + func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) { + //fromStr := from.String() + switch state { + case oasisSdk.ChatStateActive: + case oasisSdk.ChatStateComposing: + case oasisSdk.ChatStatePaused: + case oasisSdk.ChatStateInactive: + case oasisSdk.ChatStateGone: + default: + } + }, + func(_ *oasisSdk.XmppClient, from jid.JID, id string) { + fmt.Printf("Delivered %s to %s", id, from.String()) + }, + func(_ *oasisSdk.XmppClient, from jid.JID, id string) { + fmt.Printf("%s has seen %s", from.String(), id) + }, + ) + if err != nil { + log.Fatalln("Could not create client - " + err.Error()) + } + + go func() { + err = client.Connect() + if err != nil { + log.Fatalln("Could not connect - " + err.Error()) + } + }() + + + a := app.New() + a.Settings().SetTheme(myTheme{}) + w := a.NewWindow("pi") + w.Resize(fyne.NewSize(500, 500)) + + entry := widget.NewMultiLineEntry() + entry.SetPlaceHolder("Say something, you know you want to.") + + sendbtn := widget.NewButton("Send", func() { + text := entry.Text + go func() { + msg := oasisSdk.XMPPChatMessage{ // TODO: Remove hack when oasisSdk adds message sending (hopefully xd) + Message: stanza.Message{ + To: jid.MustParse("ringen@muc.isekai.rocks"), //FIXME + Type: stanza.GroupChatMessage, //FIXME + }, + ChatMessageBody: oasisSdk.ChatMessageBody{ + Body: &text, + }, + } + err := client.Session.Encode(client.Ctx, msg) + if err != nil { + dialog.ShowError(err, w) + } + }() + entry.SetText("") + }) + + mit := fyne.NewMenuItem("About pi", func() { + dialog.ShowInformation("About pi", "the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.", w) + }) + + mia := fyne.NewMenuItem("Configure message view", func() { + ch := widget.NewCheck("", func(b bool) {}) + ch.Checked = scrollDownOnNewMessage + scrollView := widget.NewFormItem("Scroll to bottom on new message", ch) + items := []*widget.FormItem{ + scrollView, + } + dialog.ShowForm("Configure message view", "Apply", "Cancel", items, func(b bool) { + if b { + scrollDownOnNewMessage = ch.Checked + } + }, w) + }) + mib := fyne.NewMenuItem("Join a room", func() { + nick := widget.NewEntry() + room := widget.NewEntry() + items := []*widget.FormItem{ + widget.NewFormItem("Nick", nick), + widget.NewFormItem("MUC address", room), + } + + dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) { + if b { + fmt.Println("attempting to join MUC") + fmt.Println(nick) + fmt.Println(room) + go func() { + client.MucClient.Join(client.Ctx, jid.MustParse(room.Text), client.Session, nil) + }() + } + }, w) + }) + menu_help := fyne.NewMenu("π", mit) + menu_changeroom := fyne.NewMenu("β", mib) + menu_configureview := fyne.NewMenu("γ", mia) + ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview) + w.SetMainMenu(ma) + tabs := container.NewAppTabs(container.NewTabItem("pi", widget.NewLabel("pi\n\nthe XMPP client from hell")), container.NewTabItem("chat", scroller)) + w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn))) // TODO: Add send message functionality + w.ShowAndRun() +} diff --git a/pi b/pi new file mode 100755 index 0000000..a1e4131 Binary files /dev/null and b/pi differ diff --git a/pi.png b/pi.png new file mode 100644 index 0000000..440368b Binary files /dev/null and b/pi.png differ diff --git a/pi.svg b/pi.svg new file mode 100644 index 0000000..e3b56b3 --- /dev/null +++ b/pi.svg @@ -0,0 +1,141 @@ + + + + + + + + + + π + π + + + + + + + + + + + + + + + +