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
2025-08-05 16:12:47 +01:00
"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 10:27:27 +01:00
var chatSidebar fyne . Container
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 {
2025-08-05 12:55:22 +01:00
Author string
Content string
ID string
ReplyID string
2025-08-04 16:45:56 +01:00
ImageURL string
2025-08-05 12:55:22 +01:00
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-05 23:54:14 +01:00
Muc * muc . Channel
2025-08-04 10:05:43 +01:00
}
2025-08-05 12:55:22 +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
2025-08-05 12:55:22 +01:00
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-05 23:54:14 +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 != "" {
2025-08-05 12:55:22 +01:00
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
}
2025-08-05 12:55:22 +01:00
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 )
}
2025-08-05 12:55:22 +01:00
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 12:55:22 +01:00
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 12:55:22 +01:00
}
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-05 12:55:22 +01:00
}
2025-08-04 10:05:43 +01:00
func main ( ) {
2025-08-06 10:27:27 +01:00
muc . Since ( time . Now ( ) )
2025-08-05 12:55:22 +01:00
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 {
2025-08-05 12:55:22 +01:00
dropToSignInPage ( err . Error ( ) )
2025-08-04 10:05:43 +01:00
return
}
2025-08-05 12:55:22 +01:00
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 ( ) ) )
2025-08-05 12:55:22 +01:00
return
2025-08-04 10:05:43 +01:00
}
2025-08-03 16:14:07 +01:00
2025-08-05 12:55:22 +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://" ) {
2025-08-05 12:55:22 +01:00
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 )
2025-08-06 10:55:06 +01:00
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 {
2025-08-05 12:55:22 +01:00
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!" )
}
}
2025-08-05 16:12:47 +01:00
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://" ) {
2025-08-05 12:55:22 +01:00
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 )
2025-08-06 10:55:06 +01:00
if strings . HasSuffix ( v , ".png" ) || strings . HasSuffix ( v , ".jpg" ) || strings . HasSuffix ( v , ".jpeg" ) || strings . HasSuffix ( v , ".webp" ) || strings . HasSuffix ( v , ".mp4" ) {
2025-08-05 16:12:47 +01:00
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 ,
2025-08-05 16:12:47 +01:00
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 10:27:27 +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-06 10:27:27 +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
2025-08-06 10:55:06 +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 10:55:06 +01:00
}
sendbtn := widget . NewButton ( "Send" , SendCallback )
entry . OnSubmitted = func ( s string ) {
SendCallback ( )
// i fucking hate fyne
}
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
} )
2025-08-05 16:12:47 +01:00
reconnect := fyne . NewMenuItem ( "reconnect" , func ( ) {
2025-08-05 16:32:55 +01:00
go func ( ) {
2025-08-05 16:12:47 +01:00
err := client . Connect ( )
if err != nil {
2025-08-05 16:32:55 +01:00
fyne . Do ( func ( ) {
2025-08-05 16:12:47 +01:00
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
} )
2025-08-05 12:55:22 +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 ( ) {
//res, err := history.Fetch(client.Ctx, history.Query{}, jid.MustParse("ringen@muc.isekai.rocks"), client.Session)
} )
2025-08-04 16:45:56 +01:00
mic := fyne . NewMenuItem ( "upload a file" , func ( ) {
2025-08-05 12:55:22 +01:00
var link string
var toperr error
2025-08-06 10:55:06 +01:00
//var topreader fyne.URIReadCloser
2025-08-04 10:05:43 +01:00
dialog . ShowFileOpen ( func ( reader fyne . URIReadCloser , err error ) {
2025-08-06 10:55:06 +01:00
go func ( ) {
2025-08-04 10:05:43 +01:00
if err != nil {
dialog . ShowError ( err , w )
2025-08-05 12:55:22 +01:00
return
2025-08-04 10:05:43 +01:00
}
2025-08-05 12:55:22 +01:00
if reader == nil {
return
}
bytes , toperr = io . ReadAll ( reader )
2025-08-06 10:55:06 +01:00
//topreader = reader
2025-08-05 12:55:22 +01:00
if toperr != nil {
dialog . ShowError ( toperr , w )
2025-08-04 10:05:43 +01:00
return
}
2025-08-05 12:55:22 +01:00
progress := make ( chan oasisSdk . UploadProgress )
myprogressbar := widget . NewProgressBar ( )
2025-08-06 10:55:06 +01:00
diag := dialog . NewCustom ( "Uploading file" , "Hide" , myprogressbar , w )
diag . Show ( )
2025-08-05 12:55:22 +01:00
go func ( ) {
2025-08-06 10:55:06 +01:00
client . UploadFile ( client . Ctx , reader . URI ( ) . Path ( ) , progress )
2025-08-05 12:55:22 +01:00
} ( )
2025-08-06 10:55:06 +01:00
2025-08-05 12:55:22 +01:00
for update := range progress {
2025-08-06 10:55:06 +01:00
fyne . Do ( func ( ) {
myprogressbar . Value = float64 ( update . Percentage ) / 100
2025-08-05 12:55:22 +01:00
myprogressbar . Refresh ( )
2025-08-06 10:55:06 +01:00
} )
2025-08-05 12:55:22 +01:00
if update . Error != nil {
2025-08-06 10:55:06 +01:00
diag . Dismiss ( )
2025-08-05 12:55:22 +01:00
dialog . ShowError ( update . Error , w )
return
}
if update . GetURL != "" {
link = update . GetURL
}
}
2025-08-06 10:55:06 +01:00
diag . Dismiss ( )
2025-08-04 10:05:43 +01:00
a . Clipboard ( ) . SetContent ( link )
2025-08-04 16:45:56 +01:00
dialog . ShowInformation ( "file successfully uploaded\nURL copied to your clipboard" , link , w )
2025-08-06 10:55:06 +01:00
} ( )
2025-08-05 12:55:22 +01:00
2025-08-04 10:05:43 +01:00
} , w )
} )
2025-08-06 10:27:27 +01:00
menu_help := fyne . NewMenu ( "π" , mit , reconnect , deb )
2025-08-04 19:19:30 +01:00
menu_changeroom := fyne . NewMenu ( "β" , mic )
2025-08-05 12:55:22 +01:00
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
} )
2025-08-05 12:55:22 +01:00
2025-08-05 16:12:47 +01:00
bic := fyne . NewMenuItem ( "show message XML" , func ( ) {
pre := widget . NewLabel ( "" )
2025-08-05 12:55:22 +01:00
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
}
}
2025-08-05 16:12:47 +01:00
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
2025-08-05 12:55:22 +01:00
pre . Refresh ( )
2025-08-05 16:12:47 +01:00
dialog . ShowCustom ( "Message" , "Close" , pre , w )
2025-08-05 12:55:22 +01:00
} )
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 ) {
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
}
}
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 10:27:27 +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
}