1
0
forked from sunglocto/pi-im
This commit is contained in:
2025-08-04 16:45:56 +01:00
parent 47f45d396f
commit 479b877564
3 changed files with 110 additions and 43 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ go.work.sum
# env file # env file
.env .env
pi.json pi.json
pi
# Editor/IDE # Editor/IDE
# .idea/ # .idea/
# .vscode/ # .vscode/

Binary file not shown.

152
main.go
View File

@@ -6,15 +6,19 @@ import (
"image/color" "image/color"
"io" "io"
"log" "log"
"net/url"
_ "net/url" _ "net/url"
"os" "os"
"strings" "strings"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
_ "fyne.io/fyne/v2/canvas" _ "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
_ "fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
_ "fyne.io/x/fyne/theme" _ "fyne.io/x/fyne/theme"
@@ -25,6 +29,8 @@ import (
oasisSdk "pain.agency/oasis-sdk" oasisSdk "pain.agency/oasis-sdk"
) )
var version string = "3a"
// by sunglocto // by sunglocto
// license AGPL // license AGPL
@@ -33,6 +39,7 @@ type Message struct {
Content string Content string
ID string ID string
ReplyID string ReplyID string
ImageURL string
Raw oasisSdk.XMPPChatMessage Raw oasisSdk.XMPPChatMessage
} }
@@ -49,6 +56,7 @@ var tabs *container.AppTabs
var selectedId widget.ListItemID var selectedId widget.ListItemID
var replying bool = false var replying bool = false
var notifications bool = true var notifications bool = true
var connection bool = true
type myTheme struct{} type myTheme struct{}
@@ -99,13 +107,39 @@ func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
author.TextStyle.Bold = true author.TextStyle.Bold = true
content := widget.NewRichTextWithText("content") content := widget.NewRichTextWithText("content")
content.Wrapping = fyne.TextWrapWord content.Wrapping = fyne.TextWrapWord
return container.NewVBox(author, content) icon := theme.FileImageIcon()
btn := widget.NewButtonWithIcon("View image", icon, func() {
})
return container.NewVBox(author, content, btn)
}, },
func(i widget.ListItemID, co fyne.CanvasObject) { func(i widget.ListItemID, co fyne.CanvasObject) {
vbox := co.(*fyne.Container) vbox := co.(*fyne.Container)
author := vbox.Objects[0].(*widget.Label) author := vbox.Objects[0].(*widget.Label)
content := vbox.Objects[1].(*widget.RichText) content := vbox.Objects[1].(*widget.RichText)
content.ParseMarkdown(tabData.Messages[i].Content) //image := vbox.Objects[2].(*canvas.Image)
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() {
u, _ := storage.ParseURI(tabData.Messages[i].ImageURL)
image := canvas.NewImageFromURI(u)
image.FillMode = canvas.ImageFillOriginal
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] = "\n" + line + "\n"
}
}
msgContent = strings.Join(lines, "\n")
content.ParseMarkdown(msgContent)
if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" { if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart())) author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart()))
} else { } else {
@@ -117,6 +151,7 @@ func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
scroller.OnSelected = func(id widget.ListItemID) { scroller.OnSelected = func(id widget.ListItemID) {
selectedId = id selectedId = id
} }
tabData.Scroller = scroller tabData.Scroller = scroller
chatTabs[mucJidStr] = tabData chatTabs[mucJidStr] = tabData
@@ -141,12 +176,13 @@ func main() {
} }
err = json.Unmarshal(bytes, &login) err = json.Unmarshal(bytes, &login)
if err != nil { if err != nil {
fyne.Do(func() {
a = app.New() a = app.New()
w = a.NewWindow("Error") w = a.NewWindow("Error")
w.Resize(fyne.NewSize(500, 500)) w.Resize(fyne.NewSize(500, 500))
dialog.ShowError(err, w) dialog.ShowError(err, w)
w.ShowAndRun() w.ShowAndRun()
return })
} }
client, err := oasisSdk.CreateClient( client, err := oasisSdk.CreateClient(
@@ -161,18 +197,22 @@ func main() {
if notifications { if notifications {
a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str)) a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str))
} }
/* var img string = ""
if strings.Contains(str, "https://") { if strings.Contains(str, "https://") {
fmt.Println("Attempting to do URL thingy") lines := strings.Split(str, " ")
s := strings.Split(str, " ") for i, line := range lines {
for i, v := range s { s := strings.Split(line, " ")
_, err := url.Parse(v) for j, v := range s {
if err == nil { _, err := url.Parse(v)
s[i] = fmt.Sprintf("[%s](%s)", v, v) if err == nil && strings.HasPrefix(v, "https://") {
} img = v
s[j] = fmt.Sprintf("[%s](%s)", v, v)
} }
str = strings.Join(s, " ") }
}*/ lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
}
var replyID string var replyID string
if msg.Reply == nil { if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE" replyID = "PICLIENT:UNAVAILABLE"
@@ -185,6 +225,7 @@ func main() {
ID: msg.ID, ID: msg.ID,
ReplyID: replyID, ReplyID: replyID,
Raw: *msg, Raw: *msg,
ImageURL: img,
} }
tab.Messages = append(tab.Messages, myMessage) tab.Messages = append(tab.Messages, myMessage)
@@ -202,21 +243,25 @@ func main() {
str := *msg.CleanedBody str := *msg.CleanedBody
if notifications { if notifications {
if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.User)) { if 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)) a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
} }
} }
/* if strings.Contains(str, "https://") {
if strings.Contains(str, "https://") { lines := strings.Split(str, " ")
s := strings.Split(str, " ") for i, line := range lines {
for i, v := range s { s := strings.Split(line, " ")
_, err := url.Parse(v) for j, v := range s {
if err == nil { _, err := url.Parse(v)
s[i] = fmt.Sprintf("[%s](%s)", v, v) if err == nil && strings.HasPrefix(v, "https://") {
} s[j] = fmt.Sprintf("[%s](%s)", v, v)
} }
str = strings.Join(s, " ") }
}*/ lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
fmt.Println(str)
}
fmt.Println(msg.ID) fmt.Println(msg.ID)
var replyID string var replyID string
if msg.Reply == nil { if msg.Reply == nil {
@@ -264,9 +309,19 @@ func main() {
} }
go func() { go func() {
err = client.Connect() for connection {
if err != nil { err = client.Connect()
log.Fatalln("Could not connect - " + err.Error()) 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
}
}
} }
}() }()
@@ -277,6 +332,8 @@ func main() {
entry := widget.NewMultiLineEntry() entry := widget.NewMultiLineEntry()
entry.SetPlaceHolder("Say something, you know you want to.") entry.SetPlaceHolder("Say something, you know you want to.")
entry.OnChanged = func(s string) {
}
sendbtn := widget.NewButton("Send", func() { sendbtn := widget.NewButton("Send", func() {
text := entry.Text text := entry.Text
@@ -345,22 +402,23 @@ func main() {
entry.SetText("") 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) 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)
}) })
mia := fyne.NewMenuItem("Configure message view", func() { mia := fyne.NewMenuItem("configure message view", func() {
ch := widget.NewCheck("", func(b bool) {}) ch := widget.NewCheck("", func(b bool) {})
ch2 := widget.NewCheck("", func(b bool) {}) ch2 := widget.NewCheck("", func(b bool) {})
ch.Checked = scrollDownOnNewMessage ch.Checked = scrollDownOnNewMessage
ch2.Checked = notifications ch2.Checked = notifications
scrollView := widget.NewFormItem("Scroll to bottom on new message", ch) scrollView := widget.NewFormItem("scroll to bottom on new message", ch)
notiView := widget.NewFormItem("Send notifications when mentioned", ch2) notiView := widget.NewFormItem("send notifications when mentioned", ch2)
items := []*widget.FormItem{ items := []*widget.FormItem{
scrollView, scrollView,
notiView, notiView,
} }
dialog.ShowForm("Configure message view", "Apply", "Cancel", items, func(b bool) { dialog.ShowForm("configure message view", "apply", "cancel", items, func(b bool) {
if b { if b {
scrollDownOnNewMessage = ch.Checked scrollDownOnNewMessage = ch.Checked
notifications = ch2.Checked notifications = ch2.Checked
@@ -368,8 +426,8 @@ func main() {
}, w) }, w)
}) })
mis := fyne.NewMenuItem("Clear chat window", func() { 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) { dialog.ShowConfirm("clear chat window", "are you sure you want to clear the chat window?", func(b bool) {
if b { if b {
fmt.Println("clearing chat") fmt.Println("clearing chat")
} }
@@ -384,7 +442,7 @@ func main() {
widget.NewFormItem("MUC address", roomEntry), widget.NewFormItem("MUC address", roomEntry),
} }
dialog.ShowForm("Join a MUC", "Join", "Cancel", items, func(b bool) { dialog.ShowForm("join a MUC", "join", "cancel", items, func(b bool) {
if b { if b {
roomJid, err := jid.Parse(roomEntry.Text) roomJid, err := jid.Parse(roomEntry.Text)
if err != nil { if err != nil {
@@ -404,7 +462,7 @@ func main() {
}, w) }, w)
}) })
mic := fyne.NewMenuItem("Upload a file", func() { mic := fyne.NewMenuItem("upload a file", func() {
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil { if err != nil {
dialog.ShowError(err, w) dialog.ShowError(err, w)
@@ -416,14 +474,14 @@ func main() {
return return
} }
a.Clipboard().SetContent(link) a.Clipboard().SetContent(link)
dialog.ShowInformation("File successfully uploaded", link, w) dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
}, w) }, w)
}) })
menu_help := fyne.NewMenu("π", mit) menu_help := fyne.NewMenu("π", mit)
menu_changeroom := fyne.NewMenu("β", mib, mic) menu_changeroom := fyne.NewMenu("β", mib, mic)
menu_configureview := fyne.NewMenu("γ", mia, mis) menu_configureview := fyne.NewMenu("γ", mia, mis)
bit := fyne.NewMenuItem("Mark message as read", func() { bit := fyne.NewMenuItem("mark selected message as read", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List) selectedScroller, ok := tabs.Selected().Content.(*widget.List)
if !ok { if !ok {
return return
@@ -440,7 +498,7 @@ func main() {
client.MarkAsRead(&m) client.MarkAsRead(&m)
}) })
bia := fyne.NewMenuItem("Toggle replying to message", func() { bia := fyne.NewMenuItem("toggle replying to message", func() {
replying = !replying replying = !replying
}) })
menu_messageoptions := fyne.NewMenu("Σ", bit, bia) menu_messageoptions := fyne.NewMenu("Σ", bit, bia)
@@ -448,7 +506,15 @@ func main() {
w.SetMainMenu(ma) w.SetMainMenu(ma)
tabs = container.NewAppTabs( tabs = container.NewAppTabs(
container.NewTabItem("τίποτα", widget.NewRichTextFromMarkdown("# No chat selected.")), container.NewTabItem("τίποτα", widget.NewLabel(`
welcome to pi
you are currently not focused on any rooms.
you can add new rooms by editing your pi.json file.
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.
`)),
) )
for _, mucJidStr := range login.MucsToJoin { for _, mucJidStr := range login.MucsToJoin {
@@ -466,6 +532,6 @@ func main() {
} }
} }
w.SetContent(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn))) w.SetContent(container.NewVSplit(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn)), widget.NewLabel("pi")))
w.ShowAndRun() w.ShowAndRun()
} }