1
0
forked from sunglocto/pi-im
Files
pi-im/main.go

1253 lines
32 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
//core - required
"encoding/xml"
"fmt"
_ "image/color"
"io"
"log"
"math/rand/v2"
"net/url"
"os"
"strings"
"time"
// gui - required
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
extraWidgets "fyne.io/x/fyne/widget"
"github.com/rrivera/identicon"
// xmpp - required
_ "mellium.im/xmpp/disco"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
_ "mellium.im/xmpp/stanza"
oasisSdk "pain.agency/oasis-sdk"
// gui - optional
// catppuccin "github.com/mbaklor/fyne-catppuccin"
// TODO: integrated theme switcher
)
var version string = "3i"
var statBar widget.Label
var chatInfo fyne.Container
var chatSidebar fyne.Container
var agreesToSendingHotFuckIntoChannel bool = false
// by sunglocto
// license AGPL
type Message struct {
Author string
Content string
ID string
ReplyID string
ImageURL string
Raw oasisSdk.XMPPChatMessage
Important bool
Readers []jid.JID
}
type ChatTab struct {
Jid jid.JID
Nick string
Messages []Message
isMuc bool
Muc *muc.Channel
UpdateSidebar bool
}
type ChatTabUI struct {
Internal *ChatTab
Scroller *widget.List `xml:"-"`
Sidebar *fyne.Container `xml:"-"`
}
type CustomMultiLineEntry struct {
widget.Entry
}
func NewCustomMultiLineEntry() *CustomMultiLineEntry {
entry := &CustomMultiLineEntry{}
entry.ExtendBaseWidget(entry)
entry.MultiLine = true
return entry
}
func (e *CustomMultiLineEntry) TypedShortcut(sc fyne.Shortcut) {
if sc.ShortcutName() == "CustomDesktop:Control+Return" {
e.Entry.TypedRune('\n')
return
}
e.Entry.TypedShortcut(sc)
}
func (e *CustomMultiLineEntry) TypedKey(ev *fyne.KeyEvent) {
if ev.Name == fyne.KeyReturn || ev.Name == fyne.KeyEnter {
// Normal Enter (no modifier) = submit
if e.OnSubmitted != nil {
e.OnSubmitted(e.Text)
}
} else {
e.Entry.TypedKey(ev)
}
}
type piConfig struct {
Login oasisSdk.LoginInfo
DMs []string
Notifications bool
JoinBookmarks bool
}
var config piConfig
var login oasisSdk.LoginInfo
var DMs []string
var chatTabs = make(map[string]*ChatTab)
var UITabs = make(map[string]*ChatTabUI)
var AppTabs *container.AppTabs
var selectedId widget.ListItemID
var replying bool = false
var notifications bool
var connection bool = true
/*
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
return resourceAppleColorEmojiTtf
}
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return adwaita.AdwaitaTheme().Color(name, fyne.CurrentApp().Settings().ThemeVariant())
}
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
/*
func (m myTheme) Font(style fyne.TextStyle) fyne.Resource {
return theme.DefaultTheme().Font(style)
}
*/
var scrollDownOnNewMessage bool = true
var w fyne.Window
var a fyne.App
func CreateUITab(chatJidStr string) ChatTabUI {
var scroller *widget.List
scroller = widget.NewList(
func() int {
return len(chatTabs[chatJidStr].Messages)
},
func() fyne.CanvasObject {
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw("default")
im := ii.Image(25)
ico := canvas.NewImageFromImage(im)
ico.FillMode = canvas.ImageFillOriginal
author := widget.NewLabel("author")
author.TextStyle.Bold = true
content := widget.NewLabel("content")
content.Wrapping = fyne.TextWrapWord
content.Selectable = true
icon := theme.FileVideoIcon()
replytext := widget.NewLabel("> fallback reply text")
replytext.Hide()
replytext.Importance = widget.SuccessImportance
btn := widget.NewButtonWithIcon("View media", icon, func() {
})
return container.NewVBox(replytext, container.NewHBox(ico, author), content, btn)
},
func(i widget.ListItemID, co fyne.CanvasObject) {
vbox := co.(*fyne.Container)
authorBox := vbox.Objects[1].(*fyne.Container)
replytext := vbox.Objects[0].(*widget.Label)
// generate a Icon
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw(chatTabs[chatJidStr].Messages[i].Author)
im := ii.Image(25)
authorBox.Objects[0] = canvas.NewImageFromImage(im)
authorBox.Objects[0].(*canvas.Image).FillMode = canvas.ImageFillOriginal
authorBox.Objects[0].Refresh()
// Icon generate end
author := authorBox.Objects[1].(*widget.Label)
content := vbox.Objects[2].(*widget.Label)
btn := vbox.Objects[3].(*widget.Button)
if chatTabs[chatJidStr].Messages[i].Important {
//content.Importance = widget.DangerImportance TODO: Fix highlighting messages with mentions, it's currently broken
}
btn.Hidden = true // Hide by default
msgContent := chatTabs[chatJidStr].Messages[i].Content
if chatTabs[chatJidStr].Messages[i].ImageURL != "" {
btn.Hidden = false
btn.OnTapped = func() {
go func() {
u, err := storage.ParseURI(chatTabs[chatJidStr].Messages[i].ImageURL)
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
return
}
if strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "mp4") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "mp3") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "gif") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "webp") { // FIXME: This code is fucking terrible // TODO: Could check mime?
url, err := url.Parse(chatTabs[chatJidStr].Messages[i].ImageURL)
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
return
}
fyne.Do(func() {
a.OpenURL(url)
})
return
}
image := canvas.NewImageFromURI(u)
image.FillMode = canvas.ImageFillOriginal
fyne.Do(func() {
dialog.ShowCustom("Image", "Close", image, w)
})
}()
}
}
// Check if the message is a quote
lines := strings.Split(msgContent, "\n")
for i, line := range lines {
if strings.HasPrefix(line, ">") {
lines[i] = fmt.Sprintf("\n %s \n", line)
}
}
msgContent = strings.Join(lines, "\n")
//content.ParseMarkdown(msgContent)
content.SetText(msgContent)
if chatTabs[chatJidStr].Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
reply := chatTabs[chatJidStr].Messages[i].Raw.Reply
guy := jid.MustParse(reply.To).Resourcepart()
// TODO: EXPERIMENTALLY GET REPLIED TO TEXT
for i := len(chatTabs[chatJidStr].Messages) - 1; i >= 0; i-- {
if reply.ID == chatTabs[chatJidStr].Messages[i].Raw.StanzaID.ID {
replytext.Show()
replytext.SetText(fmt.Sprintf("> %s", chatTabs[chatJidStr].Messages[i].Content))
guy = chatTabs[chatJidStr].Messages[i].Author
break
}
}
author.SetText(fmt.Sprintf("%s > %s", chatTabs[chatJidStr].Messages[i].Author, guy))
} else {
author.SetText(chatTabs[chatJidStr].Messages[i].Author)
}
sl := strings.Split(msgContent, " ")
if sl[0] == "/me" {
author.SetText(author.Text + " " + strings.Join(sl[1:], " "))
content.SetText(" ")
}
scroller.SetItemHeight(i, vbox.MinSize().Height)
},
)
scroller.OnSelected = func(id widget.ListItemID) {
selectedId = id
}
myUITab := ChatTabUI{}
scroller.CreateItem()
myUITab.Scroller = scroller
gen, _ := identicon.New("github", 50, 20)
ii, _ := gen.Draw(chatJidStr)
im := ii.Image(250)
imw := canvas.NewImageFromImage(im)
imw.FillMode = canvas.ImageFillOriginal
myUITab.Sidebar = container.NewVBox(imw)
return myUITab
}
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
chatJidStr := chatJid.String()
if _, ok := chatTabs[chatJidStr]; ok {
// Tab already exists
return
}
myChatTab := ChatTab{
Jid: chatJid,
Nick: nick,
Messages: []Message{},
isMuc: isMuc,
}
myUITab := CreateUITab(chatJid.String())
myUITab.Internal = &myChatTab
chatTabs[chatJidStr] = &myChatTab
UITabs[chatJidStr] = &myUITab
var icon fyne.Resource
if isMuc {
icon = theme.HomeIcon()
} else {
icon = theme.AccountIcon()
}
fyne.Do(func() {
AppTabs.Append(container.NewTabItemWithIcon(chatJid.String(), icon, myUITab.Scroller))
})
}
func dropToSignInPage(reason string) {
w = a.NewWindow("Welcome to pi")
w.Resize(fyne.NewSize(500, 500))
rt := widget.NewRichTextFromMarkdown("# Welcome to pi\nIt appears you do not have a valid account configured. Let's create one!")
footer := widget.NewRichTextFromMarkdown(fmt.Sprintf("Reason for being dropped to the sign-in page:\n\n```%s```", reason))
userEntry := widget.NewEntry()
userEntry.SetPlaceHolder("Your JID")
serverEntry := widget.NewEntry()
serverEntry.SetPlaceHolder("Server and port")
passwordEntry := widget.NewPasswordEntry()
passwordEntry.SetPlaceHolder("Your Password")
nicknameEntry := widget.NewEntry()
nicknameEntry.SetPlaceHolder("Your Nickname")
userView := widget.NewFormItem("", userEntry)
serverView := widget.NewFormItem("", serverEntry)
passwordView := widget.NewFormItem("", passwordEntry)
nicknameView := widget.NewFormItem("", nicknameEntry)
items := []*widget.FormItem{
serverView,
userView,
passwordView,
nicknameView,
}
btn := widget.NewButton("Create an account", func() {
dialog.ShowForm("Create an account", "Create", "Dismiss", items, func(b bool) {
if b {
config := piConfig{}
config.Login.Host = serverEntry.Text
config.Login.User = userEntry.Text
config.Login.Password = passwordEntry.Text
config.Login.DisplayName = nicknameEntry.Text
config.Notifications = false
bytes, err := xml.MarshalIndent(config, "", "\t")
if err != nil {
dialog.ShowError(err, w)
return
}
writer, err := a.Storage().Create("pi.xml")
if err != nil {
dialog.ShowError(err, w)
return
}
defer writer.Close()
_, err = writer.Write(bytes)
if err != nil {
dialog.ShowError(err, w)
return
}
a.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
a.Quit()
//w.Close()
}
}, w)
})
btn2 := widget.NewButton("Close pi", func() {
w.Close()
})
w.SetContent(container.NewVBox(rt, btn, btn2, footer))
w.ShowAndRun()
}
func main() {
muc.Since(time.Now())
config = piConfig{}
a = app.NewWithID("pi-im")
//a.Settings().SetTheme(&myTheme{})
reader, err := a.Storage().Open("pi.xml")
if err != nil {
dropToSignInPage(err.Error())
return
}
defer reader.Close()
bytes, err := io.ReadAll(reader)
if err != nil {
dropToSignInPage(err.Error())
return
}
err = xml.Unmarshal(bytes, &config)
if err != nil {
dropToSignInPage(fmt.Sprintf("Your pi.xml file is invalid:\n%s", err.Error()))
return
}
DMs = config.DMs
login = config.Login
notifications = config.Notifications
client, err := oasisSdk.CreateClient(
&login)
client.SetDmHandler(func(client *oasisSdk.XmppClient, msg *oasisSdk.XMPPChatMessage) {
correction := false
userJidStr := msg.From.Bare().String()
tab, ok := chatTabs[userJidStr]
if ok {
str := *msg.CleanedBody
if notifications {
a.SendNotification(fyne.NewNotification(fmt.Sprintf("%s says", userJidStr), str))
}
for _, v := range msg.Unknown {
if v.XMLName.Local == "replace" {
correction = true
break // dont need to look at more fields
}
}
var img string = ""
if strings.Contains(str, "https://") {
lines := strings.Split(str, "\n")
for i, line := range lines {
s := strings.Split(line, " ")
for _, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".webp") || strings.HasSuffix(v, ".mp4") || strings.HasSuffix(v, ".gif") {
img = v
}
}
}
lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
}
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg.Reply.ID
}
if correction {
for i := len(tab.Messages) - 1; i > 0; i-- {
if tab.Messages[i].Raw.From.String() == msg.From.String() {
tab.Messages[i].Content = *msg.CleanedBody + " (edited)"
fyne.Do(func() {
UITabs[userJidStr].Scroller.Refresh()
})
return
}
}
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
ImageURL: img,
}
tab.Messages = append(tab.Messages, myMessage)
fyne.Do(func() {
UITabs[userJidStr].Scroller.Refresh()
if scrollDownOnNewMessage {
UITabs[userJidStr].Scroller.ScrollToBottom()
}
})
}
})
client.SetGroupChatHandler(func(client *oasisSdk.XmppClient, muc *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
// HACK: IGNORING ALL MESSAGES FROM CLASSIC MUC HISTORY IN PREPARATION OF MAM SUPPORT
ignore := false
correction := false
important := false
donotnotify := false
for _, v := range msg.Unknown {
if v.XMLName.Local == "delay" { // Classic history message
donotnotify = true
//ignore = true
//fmt.Println("ignoring!")
//return //what is blud doing
}
}
for _, v := range msg.Unknown {
if v.XMLName.Local == "replace" {
correction = true
break // dont need to look at more fields
}
}
var ImageID string = ""
mucJidStr := msg.From.Bare().String()
if tab, ok := chatTabs[mucJidStr]; ok {
chatTabs[mucJidStr].Muc = muc
str := *msg.CleanedBody
if strings.Contains(str, login.DisplayName) {
fmt.Println(str)
important = true
}
if !donotnotify && !ignore && notifications {
if !correction && msg.From.String() != client.JID.String() && strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.DisplayName)) {
a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
}
}
if strings.Contains(str, "https://") {
lines := strings.Split(str, "\n")
for i, line := range lines {
s := strings.Split(line, " ")
for _, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".webp") || strings.HasSuffix(v, ".mp4") || strings.HasSuffix(v, ".mp3") || strings.HasSuffix(v, ".gif") {
ImageID = v
}
}
}
lines[i] = strings.Join(s, " ")
}
str = strings.Join(lines, " ")
fmt.Println(str)
}
fmt.Println(msg.ID)
var replyID string
if msg.Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg.Reply.To
}
if correction {
for i := len(tab.Messages) - 1; i > 0; i-- {
if tab.Messages[i].Raw.From.String() == msg.From.String() {
tab.Messages[i].Content = *msg.CleanedBody + " (edited)"
fyne.Do(func() {
UITabs[mucJidStr].Scroller.Refresh()
})
return
}
}
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
ID: msg.ID,
ReplyID: replyID,
Raw: *msg,
ImageURL: ImageID,
Important: important,
}
if !ignore {
tab.Messages = append(tab.Messages, myMessage)
}
fyne.Do(func() {
UITabs[mucJidStr].Scroller.Refresh()
if scrollDownOnNewMessage {
UITabs[mucJidStr].Scroller.ScrollToBottom()
}
})
}
})
client.SetChatstateHandler(func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
switch state {
case oasisSdk.ChatStateComposing:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is typing...", from.Resourcepart()))
})
case oasisSdk.ChatStatePaused:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s has stopped typing.", from.Resourcepart()))
})
case oasisSdk.ChatStateInactive:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is idle", from.Resourcepart()))
})
case oasisSdk.ChatStateGone:
fyne.Do(func() {
statBar.SetText(fmt.Sprintf("%s is gone", from.Resourcepart()))
})
default:
fyne.Do(func() {
statBar.SetText("")
})
}
})
client.SetDeliveryReceiptHandler(
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
fmt.Printf("Delivered %s to %s", id, from.String())
})
client.SetReadReceiptHandler(
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
for _, tab := range chatTabs {
for i := len(tab.Messages) - 1; i >= 0; i-- {
fmt.Println(tab.Messages[i])
if tab.Messages[i].Raw.StanzaID == nil {
continue
}
if tab.Messages[i].Raw.StanzaID.ID == id {
tab.Messages[i].Readers = append(tab.Messages[i].Readers, from)
break
}
}
}
fmt.Printf("%s has seen %s\n", from.String(), id)
})
if err != nil {
log.Fatalln("Could not create client - " + err.Error())
}
go func() {
for connection {
err = client.Connect()
if err != nil {
responseChan := make(chan bool)
fyne.Do(func() {
dialog.ShowConfirm("disconnected", fmt.Sprintf("the client disconnected. would you like to try and reconnect?\nreason:\n%s", err.Error()), func(b bool) {
responseChan <- b
}, w)
})
if !<-responseChan {
connection = false
}
}
}
}()
a = app.New()
//a.Settings().SetTheme(&myTheme{})
w = a.NewWindow("pi")
w.Resize(fyne.NewSize(500, 500))
entry := NewCustomMultiLineEntry()
entry.SetPlaceHolder("Say something, you know you want to.\nCtrl+Enter for newline")
entry.Wrapping = fyne.TextWrapBreak
//entry.TypedShortcut()
SendCallback := func() {
text := entry.Text
if AppTabs.Selected() == nil || AppTabs.Selected().Content == nil || text == "" {
return
}
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
var isMuc bool
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
isMuc = chatTabs[activeMucJid].isMuc
break
}
}
if activeMucJid == "" {
return
}
go func() {
if replying {
m := chatTabs[activeMucJid].Messages[selectedId].Raw
fmt.Println(selectedId)
err = client.ReplyToEvent(&m, text)
if err != nil {
dialog.ShowError(err, w)
}
return
}
url, uerr := url.Parse(strings.Split(text, " ")[0])
if uerr == nil && strings.HasPrefix(strings.Split(text, " ")[0], "https://") {
dialog.ShowConfirm("Confirm", "Do you want to embed this link into your message?", func(b bool) {
if b {
err = client.SendSingleFileMessage(jid.MustParse(activeMucJid).Bare(), url.String(), nil)
if err != nil {
dialog.ShowError(err, w)
}
return
}
}, w)
}
err = client.SendText(jid.MustParse(activeMucJid).Bare(), text)
if err != nil {
dialog.ShowError(err, w)
}
}()
if !isMuc {
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
Author: "You",
Content: text,
ReplyID: "PICLIENT:UNAVAILABLE",
})
fyne.Do(func() {
if scrollDownOnNewMessage {
UITabs[activeMucJid].Scroller.ScrollToBottom()
}
})
}
entry.SetText("")
}
sendbtn := widget.NewButton("Send", SendCallback)
replybtn := widget.NewButton("Reply", func() {
replying = true
SendCallback()
replying = false
})
entry.OnSubmitted = func(s string) {
SendCallback()
// i fucking hate fyne
}
mit := fyne.NewMenuItem("about pi", func() {
dialog.ShowInformation("about pi", fmt.Sprintf("the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.\n\nVersion %s", version), w)
})
reconnect := fyne.NewMenuItem("reconnect", func() {
go func() {
err := client.Connect()
if err != nil {
fyne.Do(func() {
dialog.ShowError(err, w)
})
}
}()
})
mia := fyne.NewMenuItem("configure message view", func() {
ch := widget.NewCheck("", func(b bool) {})
ch2 := widget.NewCheck("", func(b bool) {})
ch.Checked = scrollDownOnNewMessage
ch2.Checked = notifications
scrollView := widget.NewFormItem("scroll to bottom on new message", ch)
notiView := widget.NewFormItem("send notifications when mentioned", ch2)
items := []*widget.FormItem{
scrollView,
notiView,
}
dialog.ShowForm("configure message view", "apply", "cancel", items, func(b bool) {
if b {
scrollDownOnNewMessage = ch.Checked
notifications = ch2.Checked
}
}, w)
})
mis := fyne.NewMenuItem("clear chat window", func() {
dialog.ShowConfirm("clear chat window", "are you sure you want to clear the chat window?", func(b bool) {
if b {
fmt.Println("clearing chat")
}
}, w)
})
jtb := fyne.NewMenuItem("jump to bottom", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToBottom()
selectedScroller.Refresh()
})
jtt := fyne.NewMenuItem("jump to top", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToTop()
selectedScroller.Refresh()
})
w.SetOnDropped(func(p fyne.Position, u []fyne.URI) {
var link string
myUri := u[0] // Only upload a single file
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
diag.Show()
go func() {
client.UploadFile(client.Ctx, myUri.Path(), progress)
}()
for update := range progress {
fyne.Do(func() {
myprogressbar.Value = float64(update.Percentage) / 100
myprogressbar.Refresh()
})
if update.Error != nil {
diag.Dismiss()
dialog.ShowError(update.Error, w)
return
}
if update.GetURL != "" {
link = update.GetURL
}
}
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
})
mic := fyne.NewMenuItem("upload a file", func() {
var link string
var toperr error
//var topreader fyne.URIReadCloser
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
go func() {
if err != nil {
dialog.ShowError(err, w)
return
}
if reader == nil {
return
}
bytes, toperr = io.ReadAll(reader)
//topreader = reader
if toperr != nil {
dialog.ShowError(toperr, w)
return
}
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
fyne.Do(func() {
diag.Show()
})
go func() {
client.UploadFile(client.Ctx, reader.URI().Path(), progress)
}()
for update := range progress {
fyne.Do(func() {
myprogressbar.Value = float64(update.Percentage) / 100
myprogressbar.Refresh()
})
if update.Error != nil {
diag.Dismiss()
dialog.ShowError(update.Error, w)
return
}
if update.GetURL != "" {
link = update.GetURL
}
}
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
}()
}, w)
})
leaveRoom := fyne.NewMenuItem("Leave current room (experimental)", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
}
}
AppTabs.Selected().Text = fmt.Sprintf("%s (disconnected)", AppTabs.Selected().Text)
AppTabs.SelectIndex(0)
delete(client.MucChannels, activeMucJid)
//delete(chatTabs, activeMucJid)
})
joinARoom := fyne.NewMenuItem("Join a room", func() {
dialog.ShowEntryDialog("Join a room", "JID:", func(s string) {
i := resourcePiloadingGif
gif, err := extraWidgets.NewAnimatedGifFromResource(i)
if err != nil {
panic(err)
}
gif.Start()
gif.Show()
d := dialog.NewCustom("Please wait", "Close", gif, w)
d.Show()
go func() {
myjid, err := jid.Parse(s)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
joinjid, err := myjid.WithResource(login.DisplayName)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
ch, err := client.MucClient.Join(client.Ctx, joinjid, client.Session)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
client.MucChannels[s] = ch
addChatTab(true, myjid, login.DisplayName)
d.Hide()
}()
}, w)
})
beginADM := fyne.NewMenuItem("Start a DM", func() {
dialog.ShowEntryDialog("Start a DM", "JID:", func(s string) {
i := resourcePiloadingGif
gif, err := extraWidgets.NewAnimatedGifFromResource(i)
if err != nil {
panic(err)
}
gif.Start()
gif.Show()
d := dialog.NewCustom("Please wait", "Close", gif, w)
d.Show()
go func() {
myjid, err := jid.Parse(s)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
addChatTab(false, myjid, login.DisplayName)
d.Hide()
}()
}, w)
})
/*
servDisc := fyne.NewMenuItem("Disco features", func() {
//var search jid.JID
dialog.ShowEntryDialog("Disco features", "JID: ", func(s string) { // TODO: replace with undeprecated widgetd
d := dialog.NewCustom("Please wait", "Close", widget.NewLabel("..."), w)
d.Show()
go func() {
//search, err = jid.Parse(s)
//if err != nil {
// d.Hide()
// dialog.ShowError(err, w)
// return
//}
txt := `<iq from='ringen@muc.isekai.rocks/sunglocto'
to='ringen@muc.iskai.rocks/snit'
type='get'
id='vc2'>
<vCard xmlns='vcard-temp'/>
</iq>`
var stan stanza.IQ
xml.Unmarshal([]byte(txt), &stan)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
r, err := client.Session.EncodeIQ(client.Ctx, stan)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
ra, _ := r.Token()
t, _ := xml.MarshalIndent(ra, "", "\t")
fmt.Println(string(t))
d.Hide()
/*
myBox := container.NewGridWithColumns(1, widget.NewLabel("Items"))
info, err := disco.GetInfo(client.Ctx, "", search, client.Session)
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
m := info.Identity
bytes, err := xml.MarshalIndent(m, "", "\t")
if err != nil {
d.Hide()
dialog.ShowError(err, w)
return
}
fyne.Do(func() {d.Hide()})
myBox.Objects = append(myBox.Objects, widget.NewLabel(string(bytes)))
dialog.ShowCustom("Service discovery", "cancel", myBox, w)
}()
}, w)
})
*/
savedata := fyne.NewMenuItem("DEBUG: Save tab data to disk", func() {
d := []ChatTab{}
for _, v := range chatTabs {
d = append(d, *v)
}
b, err := xml.Marshal(d)
if err != nil {
dialog.ShowError(err, w)
return
}
os.Create("test.xml")
os.WriteFile("text.xml", b, os.ModeAppend)
})
menu_help := fyne.NewMenu("π", mit, reconnect, savedata, fyne.NewMenuItem("Join rooms in bookmarks", func() {
// FIXME: Race condition
client.FetchBookmarks()
rooms := client.BookmarkCache()
for _, v := range rooms {
go func() {
if v.Autojoin == true {
joinjid, err := v.JID.WithResource(login.DisplayName)
if err != nil {
dialog.ShowError(err, w)
return
}
room, err := client.MucClient.Join(client.Ctx, joinjid, client.Session)
if err != nil {
dialog.ShowError(err, w)
return
}
client.MucChannels[v.JID.String()] = room
addChatTab(true, v.JID, login.DisplayName)
}
}()
}
}))
menu_changeroom := fyne.NewMenu("Α", mic, beginADM, joinARoom, leaveRoom)
menu_configureview := fyne.NewMenu("Β", mia, mis, jtt, jtb)
hafjag := fyne.NewMenuItem("Hafjag", func() {
entry.Text = "Hafjag"
SendCallback()
entry.Text = "Hafjag"
})
hotfuck := fyne.NewMenuItem("Hot Fuck", func() {
d := dialog.NewConfirm("WARNING", "This button will send the message \"Hot Fuck\" into your currently focused chat. Do you want to continue?", func(b bool) {
if b {
agreesToSendingHotFuckIntoChannel = true
entry.Text = "Hot Fuck"
SendCallback()
entry.Text = "Oh Yeah."
}
}, w)
if agreesToSendingHotFuckIntoChannel {
d.Confirm()
} else {
d.Show()
}
})
agree := fyne.NewMenuItem("Agree", func() {
old := entry.Text
entry.Text = strings.Repeat("^", rand.IntN(30))
SendCallback()
entry.Text = old
})
mycurrenttime := fyne.NewMenuItem("Current time", func() {
entry.Text = fmt.Sprintf("It is currently %s", time.Now().Format(time.RFC850))
SendCallback()
})
menu_jokes := fyne.NewMenu("Δ", mycurrenttime, hafjag, hotfuck, agree)
bit := fyne.NewMenuItem("mark selected message as read", func() {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
}
}
m := chatTabs[activeMucJid].Messages[selectedId].Raw
client.MarkAsRead(&m)
})
bia := fyne.NewMenuItem("toggle replying to message", func() {
replying = !replying
})
bic := fyne.NewMenuItem("show message XML", func() {
pre := widget.NewLabel("")
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
m := chatTabs[activeChatJid].Messages[selectedId].Raw
bytes, err := xml.MarshalIndent(m, "", "\t")
if err != nil {
dialog.ShowError(err, w)
return
}
pre.SetText(string(bytes))
pre.Selectable = true
pre.Refresh()
dialog.ShowCustom("Message", "Close", pre, w)
})
red := fyne.NewMenuItem("show read receipts on message", func() {
pre := container.NewVBox()
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
gen, _ := identicon.New("github", 5, 3)
m := chatTabs[activeChatJid].Messages[selectedId].Readers
for _, v := range m {
if chatTabs[activeChatJid].isMuc {
ii, _ := gen.Draw(v.Resourcepart())
im := ii.Image(25)
iw := canvas.NewImageFromImage(im)
iw.FillMode = canvas.ImageFillOriginal
pre.Add(container.NewHBox(iw, widget.NewLabel(v.Resourcepart())))
} else {
ii, _ := gen.Draw(v.Localpart())
im := ii.Image(25)
iw := canvas.NewImageFromImage(im)
iw.FillMode = canvas.ImageFillOriginal
pre.Add(container.NewHBox(iw, widget.NewLabel(v.Localpart())))
}
}
pre.Refresh()
dialog.ShowCustom("Message", "Close", pre, w)
})
menu_messageoptions := fyne.NewMenu("Γ", bit, bia, bic, red)
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions, menu_jokes)
w.SetMainMenu(ma)
AppTabs = container.NewAppTabs(
container.NewTabItem("τίποτα", widget.NewLabel(`
pi
`)),
)
for _, mucJidStr := range login.MucsToJoin {
mucJid, err := jid.Parse(mucJidStr)
if err == nil {
addChatTab(true, mucJid, login.DisplayName)
}
}
for _, userJidStr := range DMs {
fmt.Println(userJidStr)
DMjid, err := jid.Parse(userJidStr)
if err == nil {
addChatTab(false, DMjid, login.DisplayName)
}
}
AppTabs.OnSelected = func(ti *container.TabItem) {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
tab := chatTabs[activeChatJid]
UITab := UITabs[activeChatJid]
if tab.isMuc {
//chatInfo = *container.NewHBox(widget.NewLabel(tab.Muc.Addr().String()))
} else {
chatInfo = *container.NewHBox(widget.NewLabel(tab.Jid.String()))
}
chatSidebar = *UITab.Sidebar
old := chatSidebar.Position()
chatSidebar.Refresh()
chatSidebar.Move(old)
}
// HACK - disable chatsidebar because it's currently very buggy
chatSidebar.Hidden = true
statBar.SetText("")
w.SetContent(container.NewVSplit(container.NewVSplit(AppTabs, container.NewHSplit(entry, container.NewGridWithRows(1, sendbtn, replybtn))), container.NewHSplit(&statBar, &chatInfo)))
w.ShowAndRun()
}