Files
pi-im/main.go

808 lines
21 KiB
Go
Raw Normal View History

2025-08-03 11:17:46 +01:00
package main
2025-08-03 16:14:07 +01:00
import (
2025-08-05 21:00:16 +01:00
//core - required
"encoding/xml"
2025-08-03 16:14:07 +01:00
"fmt"
"image/color"
2025-08-04 10:05:43 +01:00
"io"
2025-08-03 16:14:07 +01:00
"log"
2025-08-04 16:45:56 +01:00
"net/url"
2025-08-04 10:05:43 +01:00
"os"
"strings"
2025-08-06 10:27:27 +01:00
"time"
2025-08-05 21:00:16 +01:00
// gui - required
2025-08-03 16:14:07 +01:00
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
2025-08-04 16:45:56 +01:00
"fyne.io/fyne/v2/canvas"
2025-08-03 16:14:07 +01:00
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
2025-08-04 16:45:56 +01:00
"fyne.io/fyne/v2/storage"
2025-08-04 10:05:43 +01:00
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
2025-08-05 21:00:16 +01:00
// xmpp - required
2025-08-06 10:27:27 +01:00
_ "mellium.im/xmlstream"
_ "mellium.im/xmpp"
2025-08-03 16:14:07 +01:00
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
2025-08-06 10:27:27 +01:00
_ "mellium.im/xmpp/stanza"
2025-08-03 16:14:07 +01:00
oasisSdk "pain.agency/oasis-sdk"
2025-08-05 21:00:16 +01:00
// gui - optional
2025-08-05 23:54:14 +01:00
// catppuccin "github.com/mbaklor/fyne-catppuccin"
adwaita "fyne.io/x/fyne/theme"
// TODO: integrated theme switcher
2025-08-03 16:14:07 +01:00
)
2025-08-05 13:08:47 +01:00
var version string = "3.1a"
2025-08-05 23:54:14 +01:00
var statBar widget.Label
var chatInfo fyne.Container
2025-08-06 11:00:22 +01:00
var chatSidebar fyne.Container
2025-08-06 10:27:27 +01:00
2025-08-04 10:05:43 +01:00
// by sunglocto
// license AGPL
2025-08-03 11:17:46 +01:00
2025-08-04 10:05:43 +01:00
type Message struct {
Author string
Content string
ID string
ReplyID string
2025-08-04 16:45:56 +01:00
ImageURL string
Raw oasisSdk.XMPPChatMessage
2025-08-04 10:05:43 +01:00
}
type MucTab struct {
Jid jid.JID
Nick string
Messages []Message
Scroller *widget.List
isMuc bool
2025-08-06 11:00:22 +01:00
Muc *muc.Channel
2025-08-04 10:05:43 +01:00
}
type piConfig struct {
Login oasisSdk.LoginInfo
DMs []string
Notifications bool
}
var config piConfig
var login oasisSdk.LoginInfo
var DMs []string
2025-08-04 10:05:43 +01:00
var chatTabs = make(map[string]*MucTab)
var tabs *container.AppTabs
var selectedId widget.ListItemID
var replying bool = false
var notifications bool
2025-08-04 16:45:56 +01:00
var connection bool = true
2025-08-04 10:05:43 +01:00
type myTheme struct{}
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
2025-08-06 11:00:22 +01:00
return adwaita.AdwaitaTheme().Color(name, variant)
2025-08-04 10:05:43 +01:00
}
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)
}
2025-08-03 16:14:07 +01:00
2025-08-04 10:05:43 +01:00
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
}
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
2025-08-05 23:54:14 +01:00
icon := theme.FileVideoIcon()
btn := widget.NewButtonWithIcon("View media", icon, func() {
2025-08-04 16:45:56 +01:00
})
return container.NewVBox(author, content, btn)
2025-08-04 10:05:43 +01:00
},
func(i widget.ListItemID, co fyne.CanvasObject) {
vbox := co.(*fyne.Container)
author := vbox.Objects[0].(*widget.Label)
content := vbox.Objects[1].(*widget.RichText)
2025-08-04 16:45:56 +01:00
btn := vbox.Objects[2].(*widget.Button)
btn.Hidden = true // Hide by default
msgContent := tabData.Messages[i].Content
if tabData.Messages[i].ImageURL != "" {
btn.Hidden = false
btn.OnTapped = func() {
fyne.Do(func() {
2025-08-05 23:54:14 +01:00
u, err := storage.ParseURI(tabData.Messages[i].ImageURL)
if err != nil {
dialog.ShowError(err, w)
return
}
if strings.HasSuffix(tabData.Messages[i].ImageURL, "mp4") {
url, err := url.Parse(tabData.Messages[i].ImageURL)
if err != nil {
dialog.ShowError(err, w)
return
}
a.OpenURL(url)
return
}
image := canvas.NewImageFromURI(u)
image.FillMode = canvas.ImageFillOriginal
dialog.ShowCustom("Image", "Close", image, w)
})
}
2025-08-04 16:45:56 +01:00
}
// Check if the message is a quote
lines := strings.Split(msgContent, "\n")
for i, line := range lines {
if strings.HasPrefix(line, ">") {
2025-08-05 17:49:29 +01:00
lines[i] = fmt.Sprintf("\n %s \n", line)
2025-08-04 16:45:56 +01:00
}
}
msgContent = strings.Join(lines, "\n")
content.ParseMarkdown(msgContent)
2025-08-04 10:05:43 +01:00
if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
2025-08-05 17:49:29 +01:00
author.SetText(fmt.Sprintf("%s > %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart()))
2025-08-04 10:05:43 +01:00
} else {
author.SetText(tabData.Messages[i].Author)
}
scroller.SetItemHeight(i, vbox.MinSize().Height)
},
)
scroller.OnSelected = func(id widget.ListItemID) {
selectedId = id
2025-08-03 16:14:07 +01:00
}
2025-08-04 16:45:56 +01:00
2025-08-06 10:27:27 +01:00
scroller.CreateItem()
2025-08-04 10:05:43 +01:00
tabData.Scroller = scroller
2025-08-03 16:14:07 +01:00
2025-08-04 10:05:43 +01:00
chatTabs[mucJidStr] = tabData
tabItem := container.NewTabItem(chatJid.Localpart(), scroller)
tabs.Append(tabItem)
}
func dropToSignInPage(reason string) {
2025-08-05 16:32:55 +01:00
a = app.New()
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!")
2025-08-05 23:54:14 +01:00
footer := widget.NewRichTextFromMarkdown(fmt.Sprintf("Reason for being dropped to the sign-in page:\n\n```%s```", reason))
2025-08-05 16:32:55 +01:00
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,
}
2025-08-05 16:32:55 +01:00
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 = true
2025-08-05 17:49:29 +01:00
bytes, err := xml.MarshalIndent(config, "", " ")
2025-08-05 16:32:55 +01:00
if err != nil {
dialog.ShowError(err, w)
return
}
2025-08-05 16:32:55 +01:00
2025-08-05 17:49:29 +01:00
_, err = os.Create("pi.xml")
if err != nil {
dialog.ShowError(err, w)
return
}
err = os.WriteFile("pi.xml", bytes, os.FileMode(os.O_RDWR)) // TODO: See if this works on non-unix like systems
if err != nil {
dialog.ShowError(err, w)
return
}
2025-08-05 16:32:55 +01:00
a.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
w.Close()
}
}, w)
})
btn2 := widget.NewButton("Close pi", func() {
w.Close()
})
w.SetContent(container.NewVBox(rt, btn, btn2, footer))
w.ShowAndRun()
}
2025-08-04 10:05:43 +01:00
func main() {
2025-08-06 10:27:27 +01:00
muc.Since(time.Now())
config = piConfig{}
2025-08-04 10:05:43 +01:00
2025-08-05 17:49:29 +01:00
bytes, err := os.ReadFile("./pi.xml")
2025-08-04 10:05:43 +01:00
if err != nil {
dropToSignInPage(err.Error())
2025-08-04 10:05:43 +01:00
return
}
2025-08-05 17:49:29 +01:00
err = xml.Unmarshal(bytes, &config)
2025-08-05 16:32:55 +01:00
if err != nil {
2025-08-05 17:49:29 +01:00
dropToSignInPage(fmt.Sprintf("Your pi.xml file is invalid:\n%s", err.Error()))
return
2025-08-04 10:05:43 +01:00
}
2025-08-03 16:14:07 +01:00
login = config.Login
DMs = config.DMs
notifications = config.Notifications
2025-08-03 16:14:07 +01:00
client, err := oasisSdk.CreateClient(
&login,
func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
2025-08-04 10:05:43 +01:00
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))
}
2025-08-04 16:45:56 +01:00
var img string = ""
if strings.Contains(str, "https://") {
lines := strings.Split(str, "\n")
2025-08-04 16:45:56 +01:00
for i, line := range lines {
s := strings.Split(line, " ")
for j, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
s[j] = fmt.Sprintf("[%s](%s)", v, v)
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".webp") || strings.HasSuffix(v, ".mp4") {
2025-08-05 23:54:14 +01:00
img = v
}
2025-08-04 10:05:43 +01:00
}
2025-08-04 16:45:56 +01:00
}
lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
}
2025-08-04 10:05:43 +01:00
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,
2025-08-04 16:45:56 +01:00
ImageURL: img,
2025-08-04 10:05:43 +01:00
}
tab.Messages = append(tab.Messages, myMessage)
fyne.Do(func() {
tab.Scroller.Refresh()
if scrollDownOnNewMessage {
tab.Scroller.ScrollToBottom()
}
})
}
2025-08-03 16:14:07 +01:00
},
2025-08-05 23:54:14 +01:00
func(client *oasisSdk.XmppClient, muc *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
2025-08-06 10:27:27 +01:00
// HACK: IGNORING ALL MESSAGES FROM CLASSIC MUC HISTORY IN PREPARATION OF MAM SUPPORT
ignore := false
for _, v := range msg.Unknown {
if v.XMLName.Local == "delay" { // CLasic history message
ignore = true
fmt.Println("ignoring!")
}
}
var ImageID string = ""
2025-08-04 10:05:43 +01:00
mucJidStr := msg.From.Bare().String()
if tab, ok := chatTabs[mucJidStr]; ok {
2025-08-05 23:54:14 +01:00
chatTabs[mucJidStr].Muc = muc
2025-08-04 10:05:43 +01:00
str := *msg.CleanedBody
2025-08-06 10:27:27 +01:00
if !ignore && notifications {
2025-08-04 16:45:56 +01:00
if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.DisplayName)) {
2025-08-04 10:05:43 +01:00
a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
}
2025-08-03 16:14:07 +01:00
}
2025-08-04 16:45:56 +01:00
if strings.Contains(str, "https://") {
lines := strings.Split(str, "\n")
2025-08-04 16:45:56 +01:00
for i, line := range lines {
s := strings.Split(line, " ")
for j, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
s[j] = fmt.Sprintf("[%s](%s)", v, v)
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".webp") || strings.HasSuffix(v, ".mp4") {
ImageID = v
}
2025-08-04 10:05:43 +01:00
}
2025-08-04 16:45:56 +01:00
}
lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
fmt.Println(str)
}
2025-08-04 10:05:43 +01:00
fmt.Println(msg.ID)
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg.Reply.To
}
myMessage := Message{
2025-08-05 16:32:55 +01:00
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
ImageURL: ImageID,
2025-08-04 10:05:43 +01:00
}
2025-08-06 10:27:27 +01:00
if !ignore {
tab.Messages = append(tab.Messages, myMessage)
}
2025-08-04 10:05:43 +01:00
fyne.Do(func() {
tab.Scroller.Refresh()
if scrollDownOnNewMessage {
tab.Scroller.ScrollToBottom()
}
})
}
2025-08-03 16:14:07 +01:00
},
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
switch state {
case oasisSdk.ChatStateComposing:
2025-08-05 23:54:14 +01:00
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is typing...", from.Resourcepart()))
})
2025-08-03 16:14:07 +01:00
case oasisSdk.ChatStatePaused:
2025-08-05 23:54:14 +01:00
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s has stoped typing.", from.Resourcepart()))
})
2025-08-03 16:14:07 +01:00
case oasisSdk.ChatStateInactive:
2025-08-05 23:54:14 +01:00
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is idle", from.Resourcepart()))
})
2025-08-03 16:14:07 +01:00
case oasisSdk.ChatStateGone:
2025-08-05 23:54:14 +01:00
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is gone", from.Resourcepart()))
})
2025-08-03 16:14:07 +01:00
default:
2025-08-05 23:54:14 +01:00
fyne.Do(func() {
statBar.SetText(fmt.Sprint("Unknown state: ", state))
})
2025-08-03 16:14:07 +01:00
}
},
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())
}
2025-08-06 11:00:22 +01:00
/*
client.Session.Serve(xmpp.HandlerFunc(func(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
d := xml.NewTokenDecoder(t)
// Ignore anything that's not a message.
if start.Name.Local != "message" {
return nil
}
msg := struct {
stanza.Message
Body string `xml:"body"`
}{}
err := d.DecodeElement(&msg, start)
if err != nil {
return err
}
if msg.Body != "" {
log.Println("Got message: %q", msg.Body)
}
return nil
}))
*/
2025-08-03 16:14:07 +01:00
go func() {
2025-08-04 16:45:56 +01:00
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
}
}
2025-08-04 10:05:43 +01:00
}
2025-08-03 16:14:07 +01:00
}()
2025-08-04 10:05:43 +01:00
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.")
2025-08-04 16:45:56 +01:00
entry.OnChanged = func(s string) {
}
2025-08-04 10:05:43 +01:00
SendCallback := func() {
2025-08-04 10:05:43 +01:00
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() {
if replying {
m := chatTabs[activeMucJid].Messages[selectedId].Raw
client.ReplyToEvent(&m, text)
return
}
2025-08-05 13:08:47 +01:00
err = client.SendText(jid.MustParse(activeMucJid), text)
2025-08-04 10:05:43 +01:00
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("")
2025-08-06 11:00:22 +01:00
}
sendbtn := widget.NewButton("Send", SendCallback)
entry.OnSubmitted = func(s string) {
SendCallback()
// i fucking hate fyne
2025-08-06 11:00:22 +01:00
}
2025-08-04 10:05:43 +01:00
2025-08-04 16:45:56 +01:00
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)
2025-08-04 10:05:43 +01:00
})
reconnect := fyne.NewMenuItem("reconnect", func() {
2025-08-05 16:32:55 +01:00
go func() {
err := client.Connect()
if err != nil {
2025-08-05 16:32:55 +01:00
fyne.Do(func() {
dialog.ShowError(err, w)
})
}
}()
})
2025-08-04 16:45:56 +01:00
mia := fyne.NewMenuItem("configure message view", func() {
2025-08-04 10:05:43 +01:00
ch := widget.NewCheck("", func(b bool) {})
ch2 := widget.NewCheck("", func(b bool) {})
ch.Checked = scrollDownOnNewMessage
ch2.Checked = notifications
2025-08-04 16:45:56 +01:00
scrollView := widget.NewFormItem("scroll to bottom on new message", ch)
notiView := widget.NewFormItem("send notifications when mentioned", ch2)
2025-08-04 10:05:43 +01:00
items := []*widget.FormItem{
scrollView,
notiView,
}
2025-08-04 16:45:56 +01:00
dialog.ShowForm("configure message view", "apply", "cancel", items, func(b bool) {
2025-08-04 10:05:43 +01:00
if b {
scrollDownOnNewMessage = ch.Checked
notifications = ch2.Checked
}
}, w)
})
2025-08-04 16:45:56 +01:00
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) {
2025-08-04 10:05:43 +01:00
if b {
fmt.Println("clearing chat")
}
}, w)
2025-08-03 16:14:07 +01:00
})
jtb := fyne.NewMenuItem("jump to bottom", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToBottom()
})
jtt := fyne.NewMenuItem("jump to top", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToTop()
})
2025-08-04 19:19:30 +01:00
/*mib := fyne.NewMenuItem("Join a room", func() {
2025-08-04 10:05:43 +01:00
nickEntry := widget.NewEntry()
nickEntry.SetText(login.DisplayName)
roomEntry := widget.NewEntry()
2025-08-03 16:14:07 +01:00
items := []*widget.FormItem{
2025-08-04 10:05:43 +01:00
widget.NewFormItem("Nick", nickEntry),
widget.NewFormItem("MUC address", roomEntry),
2025-08-03 16:14:07 +01:00
}
2025-08-04 16:45:56 +01:00
dialog.ShowForm("join a MUC", "join", "cancel", items, func(b bool) {
2025-08-03 16:14:07 +01:00
if b {
2025-08-04 10:05:43 +01:00
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)
}
2025-08-03 16:14:07 +01:00
}()
2025-08-04 10:05:43 +01:00
addChatTab(true, roomJid, nick)
2025-08-03 16:14:07 +01:00
}
}, w)
2025-08-04 19:19:30 +01:00
})*/
2025-08-04 10:05:43 +01:00
2025-08-06 10:27:27 +01:00
deb := fyne.NewMenuItem("DEBUG: Attempt to get MAM history from a user", func() {
2025-08-06 11:00:22 +01:00
//res, err := history.Fetch(client.Ctx, history.Query{}, jid.MustParse("ringen@muc.isekai.rocks"), client.Session)
2025-08-06 10:27:27 +01:00
})
2025-08-04 16:45:56 +01:00
mic := fyne.NewMenuItem("upload a file", func() {
var link string
var toperr error
//var topreader fyne.URIReadCloser
2025-08-04 10:05:43 +01:00
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
go func() {
2025-08-06 11:00:22 +01:00
if err != nil {
dialog.ShowError(err, w)
return
}
if reader == nil {
return
}
bytes, toperr = io.ReadAll(reader)
//topreader = reader
2025-08-06 11:00:22 +01:00
if toperr != nil {
dialog.ShowError(toperr, w)
return
}
2025-08-06 11:00:22 +01:00
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
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
}
}
2025-08-06 11:00:22 +01:00
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
}()
2025-08-04 10:05:43 +01:00
}, w)
})
2025-08-06 11:00:22 +01:00
menu_help := fyne.NewMenu("π", mit, reconnect, deb)
2025-08-04 19:19:30 +01:00
menu_changeroom := fyne.NewMenu("β", mic)
menu_configureview := fyne.NewMenu("γ", mia, mis, jtt, jtb)
2025-08-04 16:45:56 +01:00
bit := fyne.NewMenuItem("mark selected message as read", func() {
2025-08-04 10:05:43 +01:00
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)
})
2025-08-04 16:45:56 +01:00
bia := fyne.NewMenuItem("toggle replying to message", func() {
2025-08-04 10:05:43 +01:00
replying = !replying
})
bic := fyne.NewMenuItem("show message XML", func() {
pre := widget.NewLabel("")
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range chatTabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
m := chatTabs[activeChatJid].Messages[selectedId].Raw
bytes, err := xml.MarshalIndent(m, "", " ")
if err != nil {
dialog.ShowError(err, w)
return
}
pre.SetText(string(bytes))
pre.Selectable = true
pre.Refresh()
dialog.ShowCustom("Message", "Close", pre, w)
})
menu_messageoptions := fyne.NewMenu("Σ", bit, bia, bic)
2025-08-04 10:05:43 +01:00
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions)
2025-08-03 16:14:07 +01:00
w.SetMainMenu(ma)
2025-08-04 10:05:43 +01:00
tabs = container.NewAppTabs(
2025-08-04 16:45:56 +01:00
container.NewTabItem("τίποτα", widget.NewLabel(`
welcome to pi
you are currently not focused on any rooms.
2025-08-05 21:00:16 +01:00
you can add new rooms by editing your pi.xml file.
2025-08-04 16:45:56 +01:00
in order to change application settings, refer to the tab-menu with the Greek letters.
these buttons allow you to configure the application as well as other functions.
for more information about the pi project itself, hit the π button.
`)),
2025-08-04 10:05:43 +01:00
)
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)
}
}
2025-08-05 23:54:14 +01:00
tabs.OnSelected = func(ti *container.TabItem) {
2025-08-06 11:00:22 +01:00
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
2025-08-05 23:54:14 +01:00
if !ok {
return
}
var activeChatJid string
for jid, tabData := range chatTabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
tab := chatTabs[activeChatJid]
if tab.isMuc {
chatInfo = *container.NewHBox(widget.NewLabel(tab.Muc.Addr().String()))
} else {
chatInfo = *container.NewHBox(widget.NewLabel(tab.Jid.String()))
}
2025-08-06 10:27:27 +01:00
if tab.isMuc {
fyne.Do(func() {
desc := widget.NewLabel("A MUC is a chatroom that can have multiple members. Eventually this pane will display information about this room, such as the members in it, the name of the MUC and its topic.")
desc.Wrapping = fyne.TextWrapBreak
chatSidebar = *container.NewStack(container.NewVScroll(container.NewVBox(widget.NewRichTextFromMarkdown(fmt.Sprintf("# %s", tab.Muc.Addr().Localpart())), widget.NewRichTextFromMarkdown(tab.Muc.Addr().String()), desc)))
//chatSidebar.Refresh()
})
}
2025-08-05 23:54:14 +01:00
}
statBar.SetText("nothing seems to be happening right now...")
2025-08-06 11:00:22 +01:00
w.SetContent(container.NewVSplit(container.NewVSplit(container.NewHSplit(tabs, &chatSidebar), container.NewHSplit(entry, sendbtn)), container.NewHSplit(&statBar, &chatInfo)))
2025-08-03 16:14:07 +01:00
w.ShowAndRun()
2025-08-03 11:17:46 +01:00
}