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"
2025-08-09 22:45:39 +01:00
_ "image/color"
2025-08-04 10:05:43 +01:00
"io"
2025-08-03 16:14:07 +01:00
"log"
2025-08-08 19:12:59 +01:00
"math/rand/v2"
2025-08-04 16:45:56 +01:00
"net/url"
2025-08-07 21:53:28 +01:00
"os"
2025-08-04 10:05:43 +01:00
"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-10 07:15:36 +01:00
extraWidgets "fyne.io/x/fyne/widget"
2025-08-12 10:55:00 +01:00
"github.com/rrivera/identicon"
2025-08-05 21:00:16 +01:00
2025-09-02 14:47:39 +01:00
"github.com/shreve/musicwand/pkg/mpris"
2025-08-05 21:00:16 +01:00
// xmpp - required
2025-08-27 12:02:38 +01:00
_ "mellium.im/xmpp/disco"
2025-08-03 16:14:07 +01:00
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
2025-08-27 12:02:38 +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"
// TODO: integrated theme switcher
2025-08-03 16:14:07 +01:00
)
2025-08-12 10:55:15 +01:00
var version string = "3i"
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-09 14:31:26 +01:00
var agreesToSendingHotFuckIntoChannel bool = false
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-07 11:50:50 +01:00
Author string
Content string
ID string
ReplyID string
ImageURL string
Raw oasisSdk . XMPPChatMessage
Important bool
2025-08-09 14:31:26 +01:00
Readers [ ] jid . JID
2025-08-04 10:05:43 +01:00
}
2025-08-07 21:53:28 +01:00
type ChatTab struct {
2025-08-07 23:12:40 +01:00
Jid jid . JID
Nick string
Messages [ ] Message
isMuc bool
2025-08-08 10:26:14 +01:00
Muc * muc . Channel
2025-08-07 23:12:40 +01:00
UpdateSidebar bool
2025-08-07 22:29:40 +01:00
}
type ChatTabUI struct {
Internal * ChatTab
2025-08-07 23:12:40 +01:00
Scroller * widget . List ` xml:"-" `
Sidebar * fyne . Container ` xml:"-" `
2025-08-04 10:05:43 +01:00
}
2025-08-09 18:10:32 +01:00
type CustomMultiLineEntry struct {
2025-08-09 22:45:39 +01:00
widget . Entry
2025-08-09 18:10:32 +01:00
}
func NewCustomMultiLineEntry ( ) * CustomMultiLineEntry {
2025-08-09 22:45:39 +01:00
entry := & CustomMultiLineEntry { }
entry . ExtendBaseWidget ( entry )
entry . MultiLine = true
return entry
2025-08-09 18:10:32 +01:00
}
func ( e * CustomMultiLineEntry ) TypedShortcut ( sc fyne . Shortcut ) {
2025-08-09 22:45:39 +01:00
if sc . ShortcutName ( ) == "CustomDesktop:Control+Return" {
e . Entry . TypedRune ( '\n' )
return
}
e . Entry . TypedShortcut ( sc )
2025-08-09 18:10:32 +01:00
}
func ( e * CustomMultiLineEntry ) TypedKey ( ev * fyne . KeyEvent ) {
2025-08-09 22:45:39 +01:00
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 )
}
2025-08-09 18:10:32 +01:00
}
2025-08-05 12:55:22 +01:00
type piConfig struct {
Login oasisSdk . LoginInfo
DMs [ ] string
Notifications bool
2025-08-31 17:58:05 +01:00
JoinBookmarks bool
2025-08-05 12:55:22 +01:00
}
var config piConfig
var login oasisSdk . LoginInfo
var DMs [ ] string
2025-08-07 21:53:28 +01:00
var chatTabs = make ( map [ string ] * ChatTab )
2025-08-07 22:29:40 +01:00
var UITabs = make ( map [ string ] * ChatTabUI )
var AppTabs * container . AppTabs
2025-08-04 10:05:43 +01:00
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
2025-08-09 22:45:39 +01:00
/ *
func ( m myTheme ) Font ( style fyne . TextStyle ) fyne . Resource {
return resourceAppleColorEmojiTtf
}
2025-08-04 10:05:43 +01:00
func ( m myTheme ) Color ( name fyne . ThemeColorName , variant fyne . ThemeVariant ) color . Color {
2025-08-09 18:10:32 +01:00
return adwaita . AdwaitaTheme ( ) . Color ( name , fyne . CurrentApp ( ) . Settings ( ) . ThemeVariant ( ) )
2025-08-04 10:05:43 +01:00
}
func ( m myTheme ) Icon ( name fyne . ThemeIconName ) fyne . Resource {
return theme . DefaultTheme ( ) . Icon ( name )
}
2025-08-09 22:45:39 +01:00
/ *
2025-08-04 10:05:43 +01:00
func ( m myTheme ) Font ( style fyne . TextStyle ) fyne . Resource {
return theme . DefaultTheme ( ) . Font ( style )
}
2025-08-09 22:45:39 +01:00
* /
2025-08-04 10:05:43 +01:00
var scrollDownOnNewMessage bool = true
var w fyne . Window
var a fyne . App
2025-08-07 22:29:40 +01:00
func CreateUITab ( chatJidStr string ) ChatTabUI {
2025-08-04 10:05:43 +01:00
var scroller * widget . List
scroller = widget . NewList (
func ( ) int {
2025-08-07 22:29:40 +01:00
return len ( chatTabs [ chatJidStr ] . Messages )
2025-08-04 10:05:43 +01:00
} ,
func ( ) fyne . CanvasObject {
2025-08-07 23:11:56 +01:00
gen , _ := identicon . New ( "github" , 5 , 3 )
ii , _ := gen . Draw ( "default" )
im := ii . Image ( 25 )
2025-08-07 23:12:40 +01:00
ico := canvas . NewImageFromImage ( im )
2025-08-07 23:11:56 +01:00
ico . FillMode = canvas . ImageFillOriginal
2025-08-04 10:05:43 +01:00
author := widget . NewLabel ( "author" )
author . TextStyle . Bold = true
2025-08-07 06:50:23 +01:00
content := widget . NewLabel ( "content" )
2025-08-04 10:05:43 +01:00
content . Wrapping = fyne . TextWrapWord
2025-08-07 11:50:50 +01:00
content . Selectable = true
2025-08-05 23:54:14 +01:00
icon := theme . FileVideoIcon ( )
2025-09-02 09:26:10 +01:00
replytext := widget . NewLabel ( "> fallback reply text" )
replytext . Hide ( )
replytext . Importance = widget . SuccessImportance
2025-08-05 23:54:14 +01:00
btn := widget . NewButtonWithIcon ( "View media" , icon , func ( ) {
2025-08-04 16:45:56 +01:00
} )
2025-09-02 09:26:10 +01:00
return container . NewVBox ( replytext , container . NewHBox ( ico , author ) , content , btn )
2025-08-04 10:05:43 +01:00
} ,
func ( i widget . ListItemID , co fyne . CanvasObject ) {
vbox := co . ( * fyne . Container )
2025-09-02 08:18:02 +01:00
authorBox := vbox . Objects [ 1 ] . ( * fyne . Container )
2025-09-02 09:26:10 +01:00
replytext := vbox . Objects [ 0 ] . ( * widget . Label )
2025-08-07 23:11:56 +01:00
// 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 )
2025-09-02 08:18:02 +01:00
content := vbox . Objects [ 2 ] . ( * widget . Label )
btn := vbox . Objects [ 3 ] . ( * widget . Button )
2025-08-07 22:29:40 +01:00
if chatTabs [ chatJidStr ] . Messages [ i ] . Important {
2025-08-07 11:50:50 +01:00
//content.Importance = widget.DangerImportance TODO: Fix highlighting messages with mentions, it's currently broken
}
2025-08-04 16:45:56 +01:00
btn . Hidden = true // Hide by default
2025-08-07 22:29:40 +01:00
msgContent := chatTabs [ chatJidStr ] . Messages [ i ] . Content
if chatTabs [ chatJidStr ] . Messages [ i ] . ImageURL != "" {
2025-08-05 12:55:22 +01:00
btn . Hidden = false
btn . OnTapped = func ( ) {
2025-08-09 10:07:05 +01:00
go func ( ) {
2025-08-07 22:29:40 +01:00
u , err := storage . ParseURI ( chatTabs [ chatJidStr ] . Messages [ i ] . ImageURL )
2025-08-05 23:54:14 +01:00
if err != nil {
2025-08-09 10:07:05 +01:00
fyne . Do ( func ( ) {
dialog . ShowError ( err , w )
} )
2025-08-05 23:54:14 +01:00
return
}
2025-08-12 10:55:00 +01:00
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?
2025-08-07 22:29:40 +01:00
url , err := url . Parse ( chatTabs [ chatJidStr ] . Messages [ i ] . ImageURL )
2025-08-05 23:54:14 +01:00
if err != nil {
2025-08-09 10:07:05 +01:00
fyne . Do ( func ( ) {
dialog . ShowError ( err , w )
} )
2025-08-05 23:54:14 +01:00
return
}
2025-08-09 10:07:05 +01:00
fyne . Do ( func ( ) {
a . OpenURL ( url )
} )
2025-08-05 23:54:14 +01:00
return
}
2025-08-05 12:55:22 +01:00
image := canvas . NewImageFromURI ( u )
image . FillMode = canvas . ImageFillOriginal
2025-08-09 10:07:05 +01:00
fyne . Do ( func ( ) {
dialog . ShowCustom ( "Image" , "Close" , image , w )
} )
} ( )
2025-08-05 12:55:22 +01:00
}
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" )
2025-08-07 06:50:23 +01:00
//content.ParseMarkdown(msgContent)
content . SetText ( msgContent )
2025-08-07 22:29:40 +01:00
if chatTabs [ chatJidStr ] . Messages [ i ] . ReplyID != "PICLIENT:UNAVAILABLE" {
2025-09-02 09:26:10 +01:00
reply := chatTabs [ chatJidStr ] . Messages [ i ] . Raw . Reply
2025-09-02 09:55:04 +01:00
guy := jid . MustParse ( reply . To ) . Resourcepart ( )
2025-09-02 09:26:10 +01:00
// 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 ) )
2025-09-02 09:55:04 +01:00
guy = chatTabs [ chatJidStr ] . Messages [ i ] . Author
2025-09-02 09:27:20 +01:00
break
2025-09-02 09:26:10 +01:00
}
}
2025-09-02 09:55:04 +01:00
author . SetText ( fmt . Sprintf ( "%s > %s" , chatTabs [ chatJidStr ] . Messages [ i ] . Author , guy ) )
2025-08-04 10:05:43 +01:00
} else {
2025-08-07 22:29:40 +01:00
author . SetText ( chatTabs [ chatJidStr ] . Messages [ i ] . Author )
2025-09-02 14:47:39 +01:00
replytext . Hide ( )
2025-08-04 10:05:43 +01:00
}
2025-08-08 10:26:14 +01:00
2025-08-08 15:26:12 -10:00
sl := strings . Split ( msgContent , " " )
if sl [ 0 ] == "/me" {
2025-08-09 22:45:39 +01:00
author . SetText ( author . Text + " " + strings . Join ( sl [ 1 : ] , " " ) )
2025-08-08 10:26:14 +01:00
content . SetText ( " " )
}
2025-08-04 10:05:43 +01:00
scroller . SetItemHeight ( i , vbox . MinSize ( ) . Height )
2025-09-02 14:47:39 +01:00
vbox . Refresh ( )
2025-08-04 10:05:43 +01:00
} ,
)
2025-08-07 22:29:40 +01:00
2025-08-04 10:05:43 +01:00
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-07 22:29:40 +01:00
myUITab := ChatTabUI { }
2025-08-06 10:27:27 +01:00
scroller . CreateItem ( )
2025-08-07 22:29:40 +01:00
myUITab . Scroller = scroller
2025-08-08 19:12:59 +01:00
gen , _ := identicon . New ( "github" , 50 , 20 )
ii , _ := gen . Draw ( chatJidStr )
im := ii . Image ( 250 )
imw := canvas . NewImageFromImage ( im )
imw . FillMode = canvas . ImageFillOriginal
2025-08-08 10:26:14 +01:00
myUITab . Sidebar = container . NewVBox ( imw )
2025-08-03 16:14:07 +01:00
2025-08-07 22:29:40 +01:00
return myUITab
}
2025-08-07 23:12:40 +01:00
func addChatTab ( isMuc bool , chatJid jid . JID , nick string ) {
2025-08-07 22:29:40 +01:00
chatJidStr := chatJid . String ( )
if _ , ok := chatTabs [ chatJidStr ] ; ok {
// Tab already exists
return
}
myChatTab := ChatTab {
2025-08-07 23:12:40 +01:00
Jid : chatJid ,
Nick : nick ,
Messages : [ ] Message { } ,
isMuc : isMuc ,
2025-08-07 22:29:40 +01:00
}
myUITab := CreateUITab ( chatJid . String ( ) )
myUITab . Internal = & myChatTab
chatTabs [ chatJidStr ] = & myChatTab
2025-08-07 23:12:40 +01:00
UITabs [ chatJidStr ] = & myUITab
2025-08-10 09:50:24 +01:00
var icon fyne . Resource
if isMuc {
icon = theme . HomeIcon ( )
2025-08-12 10:55:00 +01:00
} else {
2025-08-10 09:50:24 +01:00
icon = theme . AccountIcon ( )
}
2025-08-04 10:05:43 +01:00
2025-08-07 22:29:40 +01:00
fyne . Do ( func ( ) {
2025-08-10 09:50:24 +01:00
AppTabs . Append ( container . NewTabItemWithIcon ( chatJid . String ( ) , icon , myUITab . Scroller ) )
2025-08-07 22:29:40 +01:00
} )
2025-08-04 10:05:43 +01:00
}
2025-08-05 12:55:22 +01:00
func dropToSignInPage ( reason string ) {
2025-09-02 09:55:04 +01:00
w = a . NewWindow ( "Welcome to pi" )
2025-08-05 16:32:55 +01:00
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
2025-08-07 11:50:50 +01:00
config . Notifications = false
2025-08-05 16:32:55 +01:00
2025-08-06 19:32:37 +01:00
bytes , err := xml . MarshalIndent ( config , "" , "\t" )
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-06 19:32:37 +01:00
writer , err := a . Storage ( ) . Create ( "pi.xml" )
2025-08-05 17:49:29 +01:00
if err != nil {
dialog . ShowError ( err , w )
return
}
2025-08-06 19:32:37 +01:00
defer writer . Close ( )
_ , err = writer . Write ( bytes )
2025-08-05 17:49:29 +01:00
if err != nil {
dialog . ShowError ( err , w )
return
}
2025-08-05 16:32:55 +01:00
a . SendNotification ( fyne . NewNotification ( "Done" , "Relaunch the application" ) )
2025-08-06 19:32:37 +01:00
a . Quit ( )
//w.Close()
2025-08-05 16:32:55 +01:00
}
} , 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-07 23:11:56 +01:00
2025-08-05 12:55:22 +01:00
config = piConfig { }
2025-08-07 06:42:59 +01:00
a = app . NewWithID ( "pi-im" )
2025-09-02 09:26:10 +01:00
//a.Settings().SetTheme(&myTheme{})
2025-08-06 19:32:37 +01:00
reader , err := a . Storage ( ) . Open ( "pi.xml" )
if err != nil {
dropToSignInPage ( err . Error ( ) )
return
}
defer reader . Close ( )
2025-08-04 10:05:43 +01:00
2025-08-06 19:32:37 +01:00
bytes , err := io . ReadAll ( reader )
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
DMs = config . DMs
2025-08-06 19:32:37 +01:00
login = config . Login
2025-08-05 12:55:22 +01:00
notifications = config . Notifications
2025-08-03 16:14:07 +01:00
client , err := oasisSdk . CreateClient (
2025-08-31 15:53:00 +01:00
& 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 ) )
}
2025-08-12 10:55:00 +01:00
2025-08-31 15:53:00 +01:00
for _ , v := range msg . Unknown {
if v . XMLName . Local == "replace" {
correction = true
break // dont need to look at more fields
2025-08-12 10:55:00 +01:00
}
2025-08-31 15:53:00 +01:00
}
2025-08-12 10:55:00 +01:00
2025-08-31 15:53:00 +01:00
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
2025-08-04 10:05:43 +01:00
}
2025-08-04 16:45:56 +01:00
}
}
2025-08-31 15:53:00 +01:00
lines [ i ] = strings . Join ( s , " " )
2025-08-04 10:05:43 +01:00
}
2025-08-31 15:53:00 +01:00
str = strings . Join ( lines , " " )
}
var replyID string
if msg . Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg . Reply . ID
}
2025-08-12 10:55:00 +01:00
2025-08-31 15:53:00 +01:00
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
2025-08-12 10:55:00 +01:00
}
}
2025-08-31 15:53:00 +01:00
}
2025-08-12 10:55:00 +01:00
2025-08-31 15:53:00 +01:00
myMessage := Message {
Author : msg . From . Resourcepart ( ) ,
Content : str ,
ID : msg . ID ,
ReplyID : replyID ,
Raw : * msg ,
ImageURL : img ,
2025-08-04 10:05:43 +01:00
}
2025-08-31 15:53:00 +01:00
tab . Messages = append ( tab . Messages , myMessage )
fyne . Do ( func ( ) {
UITabs [ userJidStr ] . Scroller . Refresh ( )
if scrollDownOnNewMessage {
UITabs [ userJidStr ] . Scroller . ScrollToBottom ( )
2025-08-06 19:32:37 +01:00
}
2025-08-31 15:53:00 +01:00
} )
}
} )
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
2025-08-06 19:32:37 +01:00
}
2025-08-31 15:53:00 +01:00
}
2025-08-06 19:32:37 +01:00
2025-08-31 15:53:00 +01:00
for _ , v := range msg . Unknown {
if v . XMLName . Local == "replace" {
correction = true
break // dont need to look at more fields
2025-08-06 10:27:27 +01:00
}
2025-08-31 15:53:00 +01:00
}
2025-08-06 10:27:27 +01:00
2025-08-31 15:53:00 +01:00
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 ) )
2025-08-03 16:14:07 +01:00
}
2025-08-31 15:53:00 +01:00
}
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
2025-08-04 10:05:43 +01:00
}
2025-08-04 16:45:56 +01:00
}
}
2025-08-31 15:53:00 +01:00
lines [ i ] = strings . Join ( s , " " )
2025-08-04 10:05:43 +01:00
}
2025-08-31 15:53:00 +01:00
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
}
2025-08-06 19:32:37 +01:00
2025-08-31 15:53:00 +01:00
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
2025-08-06 19:32:37 +01:00
}
}
2025-08-04 10:05:43 +01:00
}
2025-08-05 23:54:14 +01:00
2025-08-31 15:53:00 +01:00
myMessage := Message {
Author : msg . From . Resourcepart ( ) ,
Content : str ,
ID : msg . ID ,
ReplyID : replyID ,
Raw : * msg ,
ImageURL : ImageID ,
Important : important ,
2025-08-03 16:14:07 +01:00
}
2025-08-31 15:53:00 +01:00
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 (
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 ( ) )
2025-08-31 15:53:00 +01:00
} )
client . SetReadReceiptHandler (
2025-08-03 16:14:07 +01:00
func ( _ * oasisSdk . XmppClient , from jid . JID , id string ) {
2025-08-09 14:31:26 +01:00
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 )
2025-08-31 15:53:00 +01:00
} )
2025-08-03 16:14:07 +01:00
if err != nil {
log . Fatalln ( "Could not create client - " + err . Error ( ) )
}
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 ( )
2025-09-02 09:26:10 +01:00
//a.Settings().SetTheme(&myTheme{})
2025-08-04 10:05:43 +01:00
w = a . NewWindow ( "pi" )
w . Resize ( fyne . NewSize ( 500 , 500 ) )
2025-08-09 18:10:32 +01:00
entry := NewCustomMultiLineEntry ( )
entry . SetPlaceHolder ( "Say something, you know you want to.\nCtrl+Enter for newline" )
2025-08-08 19:12:59 +01:00
entry . Wrapping = fyne . TextWrapBreak
2025-08-09 10:07:05 +01:00
//entry.TypedShortcut()
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
2025-08-07 22:29:40 +01:00
if AppTabs . Selected ( ) == nil || AppTabs . Selected ( ) . Content == nil || text == "" {
2025-08-04 10:05:43 +01:00
return
}
2025-08-07 22:29:40 +01:00
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
2025-08-04 10:05:43 +01:00
if ! ok {
return
}
var activeMucJid string
var isMuc bool
2025-08-07 22:29:40 +01:00
for jid , tabData := range UITabs {
2025-08-04 10:05:43 +01:00
if tabData . Scroller == selectedScroller {
activeMucJid = jid
2025-08-07 22:29:40 +01:00
isMuc = chatTabs [ activeMucJid ] . isMuc
2025-08-04 10:05:43 +01:00
break
}
}
if activeMucJid == "" {
return
}
go func ( ) {
if replying {
m := chatTabs [ activeMucJid ] . Messages [ selectedId ] . Raw
2025-08-09 18:10:32 +01:00
fmt . Println ( selectedId )
2025-08-07 11:50:50 +01:00
err = client . ReplyToEvent ( & m , text )
if err != nil {
dialog . ShowError ( err , w )
}
2025-08-04 10:05:43 +01:00
return
}
2025-08-05 13:08:47 +01:00
2025-08-07 11:50:50 +01:00
url , uerr := url . Parse ( strings . Split ( text , " " ) [ 0 ] )
if uerr == nil && strings . HasPrefix ( strings . Split ( text , " " ) [ 0 ] , "https://" ) {
2025-09-01 20:05:06 +01:00
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 )
2025-08-07 11:50:50 +01:00
}
2025-08-06 19:32:37 +01:00
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
2025-08-07 11:50:50 +01:00
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 ,
2025-08-06 19:32:37 +01:00
ReplyID : "PICLIENT:UNAVAILABLE" ,
2025-08-04 10:05:43 +01:00
} )
fyne . Do ( func ( ) {
if scrollDownOnNewMessage {
2025-08-07 22:29:40 +01:00
UITabs [ activeMucJid ] . Scroller . ScrollToBottom ( )
2025-08-04 10:05:43 +01:00
}
} )
}
entry . SetText ( "" )
2025-08-06 11:00:22 +01:00
}
2025-08-06 10:55:06 +01:00
sendbtn := widget . NewButton ( "Send" , SendCallback )
2025-08-09 18:10:32 +01:00
replybtn := widget . NewButton ( "Reply" , func ( ) {
replying = true
SendCallback ( )
replying = false
} )
2025-08-06 10:55:06 +01:00
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
} )
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 ( ) {
2025-08-07 22:29:40 +01:00
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
2025-08-05 12:55:22 +01:00
if ! ok {
return
}
selectedScroller . ScrollToBottom ( )
2025-08-12 10:55:00 +01:00
selectedScroller . Refresh ( )
2025-08-05 12:55:22 +01:00
} )
jtt := fyne . NewMenuItem ( "jump to top" , func ( ) {
2025-08-07 22:29:40 +01:00
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
2025-08-05 12:55:22 +01:00
if ! ok {
return
}
selectedScroller . ScrollToTop ( )
2025-08-12 10:55:00 +01:00
selectedScroller . Refresh ( )
2025-08-05 12:55:22 +01:00
} )
2025-08-03 16:14:07 +01:00
2025-08-06 19:32:37 +01:00
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
2025-08-03 16:14:07 +01:00
}
2025-08-06 19:32:37 +01:00
if update . GetURL != "" {
link = update . GetURL
}
}
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
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-05 12:55:22 +01:00
go func ( ) {
2025-08-06 10:55:06 +01:00
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-05 12:55:22 +01:00
2025-08-06 11:00:22 +01:00
if toperr != nil {
dialog . ShowError ( toperr , w )
2025-08-05 12:55:22 +01:00
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 )
2025-08-08 10:26:14 +01:00
fyne . Do ( func ( ) {
diag . Show ( )
} )
2025-08-06 11:00:22 +01:00
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-05 12:55:22 +01:00
}
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-05 12:55:22 +01:00
2025-08-04 10:05:43 +01:00
} , w )
} )
2025-08-10 09:50:24 +01:00
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)
} )
2025-08-09 22:45:39 +01:00
joinARoom := fyne . NewMenuItem ( "Join a room" , func ( ) {
dialog . ShowEntryDialog ( "Join a room" , "JID:" , func ( s string ) {
2025-08-10 07:15:36 +01:00
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 ( )
2025-08-09 22:45:39 +01:00
go func ( ) {
myjid , err := jid . Parse ( s )
if err != nil {
2025-08-10 07:15:36 +01:00
d . Hide ( )
2025-08-09 22:45:39 +01:00
dialog . ShowError ( err , w )
return
}
joinjid , err := myjid . WithResource ( login . DisplayName )
if err != nil {
2025-08-10 07:15:36 +01:00
d . Hide ( )
2025-08-09 22:45:39 +01:00
dialog . ShowError ( err , w )
return
}
ch , err := client . MucClient . Join ( client . Ctx , joinjid , client . Session )
if err != nil {
2025-08-10 07:15:36 +01:00
d . Hide ( )
2025-08-09 22:45:39 +01:00
dialog . ShowError ( err , w )
return
}
client . MucChannels [ s ] = ch
addChatTab ( true , myjid , login . DisplayName )
2025-08-10 07:15:36 +01:00
d . Hide ( )
2025-08-09 22:45:39 +01:00
} ( )
} , w )
} )
2025-08-10 09:50:24 +01:00
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 )
} )
2025-09-01 20:05:06 +01:00
/ *
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 )
2025-08-31 15:53:00 +01:00
if err != nil {
d . Hide ( )
dialog . ShowError ( err , w )
return
}
2025-09-01 20:05:06 +01:00
r , err := client . Session . EncodeIQ ( client . Ctx , stan )
2025-08-31 15:53:00 +01:00
if err != nil {
d . Hide ( )
dialog . ShowError ( err , w )
return
}
2025-09-01 20:05:06 +01:00
ra , _ := r . Token ( )
t , _ := xml . MarshalIndent ( ra , "" , "\t" )
fmt . Println ( string ( t ) )
d . Hide ( )
/ *
2025-08-06 19:32:37 +01:00
2025-09-01 20:05:06 +01:00
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 )
} )
* /
2025-08-06 19:32:37 +01:00
2025-08-07 21:53:28 +01:00
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 )
} )
2025-08-31 17:58:05 +01:00
menu_help := fyne . NewMenu ( "π" , mit , reconnect , savedata , fyne . NewMenuItem ( "Join rooms in bookmarks" , func ( ) {
2025-09-01 20:05:06 +01:00
// FIXME: Race condition
2025-08-31 17:58:05 +01:00
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 )
}
} ( )
}
} ) )
2025-08-08 19:12:59 +01:00
2025-09-01 20:05:06 +01:00
menu_changeroom := fyne . NewMenu ( "Α " , mic , beginADM , joinARoom , leaveRoom )
2025-08-08 10:26:14 +01:00
menu_configureview := fyne . NewMenu ( "Β " , mia , mis , jtt , jtb )
2025-08-07 21:13:56 +01:00
hafjag := fyne . NewMenuItem ( "Hafjag" , func ( ) {
entry . Text = "Hafjag"
SendCallback ( )
entry . Text = "Hafjag"
} )
hotfuck := fyne . NewMenuItem ( "Hot Fuck" , func ( ) {
2025-08-09 14:31:26 +01:00
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 ) {
2025-08-09 10:07:05 +01:00
if b {
2025-08-09 14:31:26 +01:00
agreesToSendingHotFuckIntoChannel = true
2025-08-09 10:07:05 +01:00
entry . Text = "Hot Fuck"
SendCallback ( )
entry . Text = "Oh Yeah."
}
} , w )
2025-08-09 14:31:26 +01:00
if agreesToSendingHotFuckIntoChannel {
d . Confirm ( )
} else {
d . Show ( )
}
2025-08-07 21:13:56 +01:00
} )
2025-08-08 19:12:59 +01:00
agree := fyne . NewMenuItem ( "Agree" , func ( ) {
old := entry . Text
entry . Text = strings . Repeat ( "^" , rand . IntN ( 30 ) )
SendCallback ( )
entry . Text = old
} )
2025-08-07 21:13:56 +01:00
mycurrenttime := fyne . NewMenuItem ( "Current time" , func ( ) {
entry . Text = fmt . Sprintf ( "It is currently %s" , time . Now ( ) . Format ( time . RFC850 ) )
SendCallback ( )
} )
2025-09-02 14:47:39 +01:00
mycurrentplayingsong := fyne . NewMenuItem ( "Get currently playing song" , func ( ) {
client , err := mpris . NewClient ( )
if err != nil {
dialog . ShowError ( err , w )
return
}
present := false
for _ , player := range client . Players ( ) {
old := entry . Text
entry . SetText ( fmt . Sprintf ( "I'm currently listening to \"%v\" by %v" , player . RawMetadata ( ) [ "xesam:title" ] . Value ( ) , player . RawMetadata ( ) [ "xesam:artist" ] ) )
SendCallback ( )
entry . SetText ( old )
present = true
break
}
if ! present {
dialog . ShowInformation ( "Failed" , "Could not find any players. You might need an MPRIS plugin for players such as mpv.\nSee the MPRIS ArchWiki article for more information:\nhttps://wiki.archlinux.org/title/MPRIS\nMPRIS is not supported on Windows or MacOS." , w )
}
} )
menu_jokes := fyne . NewMenu ( "Δ" , mycurrenttime , hafjag , hotfuck , agree , mycurrentplayingsong )
2025-08-04 16:45:56 +01:00
bit := fyne . NewMenuItem ( "mark selected message as read" , func ( ) {
2025-08-07 22:29:40 +01:00
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
2025-08-04 10:05:43 +01:00
if ! ok {
return
}
var activeMucJid string
2025-08-07 22:29:40 +01:00
for jid , tabData := range UITabs {
2025-08-04 10:05:43 +01:00
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 ( ) {
2025-08-16 10:57:36 +01:00
pre := widget . NewLabel ( "" )
2025-08-07 22:29:40 +01:00
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
2025-08-05 12:55:22 +01:00
if ! ok {
return
}
var activeChatJid string
2025-08-07 22:29:40 +01:00
for jid , tabData := range UITabs {
2025-08-05 12:55:22 +01:00
if tabData . Scroller == selectedScroller {
activeChatJid = jid
break
}
}
2025-08-05 16:12:47 +01:00
m := chatTabs [ activeChatJid ] . Messages [ selectedId ] . Raw
2025-08-06 19:32:37 +01:00
bytes , err := xml . MarshalIndent ( m , "" , "\t" )
2025-08-05 16:12:47 +01:00
if err != nil {
dialog . ShowError ( err , w )
return
}
pre . SetText ( string ( bytes ) )
2025-08-16 10:57:36 +01:00
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
} )
2025-08-09 14:31:26 +01:00
2025-09-02 08:52:23 +00:00
red := fyne . NewMenuItem ( "show read receipts on message" , func ( ) {
2025-08-09 14:31:26 +01:00
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 )
2025-08-07 21:13:56 +01:00
ma := fyne . NewMainMenu ( menu_help , menu_changeroom , menu_configureview , menu_messageoptions , menu_jokes )
2025-08-03 16:14:07 +01:00
w . SetMainMenu ( ma )
2025-08-07 22:29:40 +01:00
AppTabs = container . NewAppTabs (
2025-08-04 16:45:56 +01:00
container . NewTabItem ( "τίποτα" , widget . NewLabel ( `
2025-08-06 19:32:37 +01:00
pi
2025-08-04 16:45:56 +01:00
` ) ) ,
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-07 22:29:40 +01:00
AppTabs . OnSelected = func ( ti * container . TabItem ) {
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
2025-08-05 23:54:14 +01:00
if ! ok {
return
}
var activeChatJid string
2025-08-07 22:29:40 +01:00
for jid , tabData := range UITabs {
2025-08-05 23:54:14 +01:00
if tabData . Scroller == selectedScroller {
activeChatJid = jid
break
}
}
tab := chatTabs [ activeChatJid ]
2025-08-07 22:29:40 +01:00
UITab := UITabs [ activeChatJid ]
2025-08-05 23:54:14 +01:00
if tab . isMuc {
2025-08-08 11:24:36 +01:00
//chatInfo = *container.NewHBox(widget.NewLabel(tab.Muc.Addr().String()))
2025-08-05 23:54:14 +01:00
} else {
chatInfo = * container . NewHBox ( widget . NewLabel ( tab . Jid . String ( ) ) )
}
2025-08-06 10:27:27 +01:00
2025-08-07 22:29:40 +01:00
chatSidebar = * UITab . Sidebar
2025-08-08 10:26:14 +01:00
old := chatSidebar . Position ( )
2025-08-07 11:50:50 +01:00
chatSidebar . Refresh ( )
2025-08-08 10:26:14 +01:00
chatSidebar . Move ( old )
2025-08-05 23:54:14 +01:00
}
2025-08-08 10:26:14 +01:00
// HACK - disable chatsidebar because it's currently very buggy
chatSidebar . Hidden = true
2025-08-07 11:50:50 +01:00
statBar . SetText ( "" )
2025-08-09 18:10:32 +01:00
w . SetContent ( container . NewVSplit ( container . NewVSplit ( AppTabs , container . NewHSplit ( entry , container . NewGridWithRows ( 1 , sendbtn , replybtn ) ) ) , container . NewHSplit ( & statBar , & chatInfo ) ) )
2025-08-03 16:14:07 +01:00
w . ShowAndRun ( )
2025-08-03 11:17:46 +01:00
}