@@ -2,10 +2,10 @@ package main
import (
//core - required
"context"
"encoding/xml"
"errors"
"fmt"
_ "image/color"
"io"
"log"
"math/rand/v2"
@@ -20,6 +20,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
@@ -29,13 +30,10 @@ import (
"github.com/shreve/musicwand/pkg/mpris"
// xmpp - required
_ "mellium.im/xmpp/disco "
"mellium.im/xmpp/bookmarks "
"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
)
@@ -43,7 +41,9 @@ var version string = "3i"
var statBar widget . Label
var chatInfo fyne . Container
var chatSidebar fyne . Container
var replyNameIcon string = ">"
var replyBodyIcon string = ">"
var newlineIcon string = " |-> "
var agreesToSendingHotFuckIntoChannel bool = false
// by sunglocto
@@ -67,6 +67,7 @@ type ChatTab struct {
isMuc bool
Muc * muc . Channel
UpdateSidebar bool
Members map [ string ] oasisSdk . UserPresence
}
type ChatTabUI struct {
@@ -79,6 +80,21 @@ type CustomMultiLineEntry struct {
widget . Entry
}
func isUTF8Locale ( ) bool {
localeVars := [ ] string { "LC_ALL" , "LC_CTYPE" , "LANG" }
for _ , envVar := range localeVars {
value := os . Getenv ( envVar )
if strings . Contains ( strings . ToLower ( value ) , "utf-8" ) {
return true
}
}
return false
}
func isWindows ( ) bool {
return os . PathSeparator == '\\' && os . PathListSeparator == ';'
}
func NewCustomMultiLineEntry ( ) * CustomMultiLineEntry {
entry := & CustomMultiLineEntry { }
entry . ExtendBaseWidget ( entry )
@@ -162,13 +178,17 @@ func CreateUITab(chatJidStr string) ChatTabUI {
ico . FillMode = canvas . ImageFillOriginal
author := widget . NewLabel ( "author" )
author . TextStyle . Bold = true
author . Selectable = true
content := widget . NewLabel ( "content" )
content . Wrapping = fyne . TextWrapWord
content . Selectable = true
content . Importance = widget . MediumImportance
//content.SizeName = fyne.ThemeSizeName(theme.SizeNameText)
icon := theme . FileVideoIcon ( )
replytext := widget . NewLabel ( ">fallback reply text" )
replytext . Hide ( )
replytext . Importance = widget . SuccessImportance
replytext . Selectable = true
btn := widget . NewButtonWithIcon ( "View media" , icon , func ( ) {
} )
@@ -193,7 +213,7 @@ func CreateUITab(chatJidStr string) ChatTabUI {
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
content . Importance = widget . DangerImportance
}
btn . Hidden = true // Hide by default
msgContent := chatTabs [ chatJidStr ] . Messages [ i ] . Content
@@ -249,13 +269,13 @@ func CreateUITab(chatJidStr string) ChatTabUI {
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 ) )
replytext . SetText ( fmt . Sprintf ( "%s %s" , replyBodyIcon , strings . ReplaceAll ( chatTabs [ chatJidStr ] . Messages [ i ] . Content , "\n" , newlineIcon ) ) )
guy = chatTabs [ chatJidStr ] . Messages [ i ] . Author
break
}
}
author . SetText ( fmt . Sprintf ( "%s > %s" , chatTabs [ chatJidStr ] . Messages [ i ] . Author , guy ) )
author . SetText ( fmt . Sprintf ( "%s %s %s" , chatTabs [ chatJidStr ] . Messages [ i ] . Author , replyNameIcon , guy ) )
} else {
author . SetText ( chatTabs [ chatJidStr ] . Messages [ i ] . Author )
replytext . Hide ( )
@@ -274,7 +294,13 @@ func CreateUITab(chatJidStr string) ChatTabUI {
}
scroller . SetItemHeight ( i , vbox . MinSize ( ) . Height )
scroller . CreateItem ( ) . Refresh ( )
vbox . Refresh ( )
/*
fyne.Do(func() {
scroller.RefreshItem(i)
})
*/
} ,
)
@@ -308,6 +334,7 @@ func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
Nick : nick ,
Messages : [ ] Message { } ,
isMuc : isMuc ,
Members : make ( map [ string ] oasisSdk . UserPresence ) ,
}
myUITab := CreateUITab ( chatJid . String ( ) )
@@ -422,8 +449,13 @@ func main() {
login = config . Login
notifications = config . Notifications
client , err := oasisSdk . CreateClient (
& login )
if isUTF8Locale ( ) {
replyBodyIcon = "↱"
replyNameIcon = "→ "
newlineIcon = " ⮡ "
}
client , err := oasisSdk . CreateClient ( & login )
client . SetDmHandler ( func ( client * oasisSdk . XmppClient , msg * oasisSdk . XMPPChatMessage ) {
correction := false
@@ -525,7 +557,7 @@ func main() {
chatTabs [ mucJidStr ] . Muc = muc
str := * msg . CleanedBody
if strings . Contains ( str , login . DisplayName ) {
fmt . Println ( str )
fmt . Println ( str , login . DisplayName )
important = true
}
if ! donotnotify && ! ignore && notifications {
@@ -548,9 +580,7 @@ func main() {
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"
@@ -582,6 +612,7 @@ func main() {
if ! ignore {
tab . Messages = append ( tab . Messages , myMessage )
}
important = false
fyne . Do ( func ( ) {
UITabs [ mucJidStr ] . Scroller . Refresh ( )
if scrollDownOnNewMessage {
@@ -616,6 +647,36 @@ func main() {
} )
}
} )
client . SetPresenceHandler ( func ( client * oasisSdk . XmppClient , from jid . JID , p oasisSdk . UserPresence ) {
bareAcc := from . Bare ( )
tab , ok := chatTabs [ bareAcc . String ( ) ]
if ! ok {
return
}
if tab . isMuc {
tab . Members [ from . String ( ) ] = p
}
} )
client . SetBookmarkHandler ( false , func ( client * oasisSdk . XmppClient , bookmark bookmarks . Channel ) {
// FIXME
if bookmark . JID . String ( ) == "conversations-offtopic-reloaded@conference.trashserver.net" {
return
}
if bookmark . Autojoin {
if bookmark . Nick == "" {
bookmark . Nick = client . Login . DisplayName
}
addChatTab ( true , bookmark . JID , client . Login . DisplayName )
_ , err := client . ConnectMuc ( bookmark , oasisSdk . MucLegacyHistoryConfig { } , context . TODO ( ) )
if err != nil {
fmt . Println ( "ERROR: " + err . Error ( ) )
return
}
}
} )
client . SetDeliveryReceiptHandler (
func ( _ * oasisSdk . XmppClient , from jid . JID , id string ) {
fmt . Printf ( "Delivered %s to %s" , id , from . String ( ) )
@@ -625,7 +686,6 @@ func main() {
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
}
@@ -635,7 +695,6 @@ func main() {
}
}
}
fmt . Printf ( "%s has seen %s\n" , from . String ( ) , id )
} )
if err != nil {
@@ -647,11 +706,13 @@ func main() {
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 )
} )
//fyne.Do(func() {
fmt . Println ( err )
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
}
@@ -660,7 +721,7 @@ func main() {
} ( )
a = app . New ( )
//a.Settings().SetTheme(&myTheme{})
w = a . NewWindow ( "pi" )
w . Resize ( fyne . NewSize ( 500 , 500 ) )
@@ -697,7 +758,6 @@ func main() {
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 )
@@ -714,13 +774,21 @@ func main() {
dialog . ShowError ( err , w )
}
return
} else {
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
if err != nil {
dialog . ShowError ( err , w )
}
}
} , w )
}
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
} else {
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
if err != nil {
dialog . ShowError ( err , w )
if err != nil {
dialog . ShowError ( err , w )
}
}
} ( )
@@ -788,14 +856,6 @@ func main() {
} , 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 {
@@ -905,7 +965,7 @@ func main() {
} , w )
} )
leaveRoom := fyne . NewMenuItem ( "Leave current room (experimental) " , func ( ) {
leaveRoom := fyne . NewMenuItem ( "Leave current room" , func ( ) {
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
if ! ok {
return
@@ -918,9 +978,13 @@ func main() {
}
}
AppTabs . Selected ( ) . Text = fmt . Sprintf ( "%s (disconnected)" , AppTabs . Selected ( ) . Text )
AppTabs . Items = append ( AppTabs . Items [ : AppTabs . SelectedIndex ( ) ] , AppTabs . Items [ AppTabs . SelectedIndex ( ) + 1 : ] ... )
AppTabs . SelectIndex ( 0 )
delete ( client . MucChannels , activeMucJid )
//delete(chatTabs, activeMucJid)
err1 , err2 := client . LeaveMuc ( activeMucJid , "cya suckers!" , context . TODO ( ) )
if err1 != nil || err2 != nil {
dialog . ShowError ( errors . Join ( err1 , err2 ) , w ) // beautiful...
}
delete ( UITabs , activeMucJid )
} )
joinARoom := fyne . NewMenuItem ( "Join a room" , func ( ) {
@@ -941,20 +1005,20 @@ func main() {
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
mychannel : = new ( bookmarks . Channel )
mychannel . JID = myjid
mychannel . Nick = login . DisplayName
//ch, err := client.MucClient.Join(client.Ctx, joinjid, client.Session)
addChatTab ( true , myjid , login . DisplayName )
num := uint64 ( 0 )
_ , err = client . ConnectMuc ( * mychannel , oasisSdk . MucLegacyHistoryConfig { MaxCount : & num } , context . TODO ( ) )
if err != nil {
d . Hide ( )
dialog . ShowError ( err , w )
return
}
//client.MucChannels[s] = ch
d . Hide ( )
} ( )
} , w )
@@ -1060,7 +1124,7 @@ func main() {
os . WriteFile ( "text.xml" , b , os . ModeAppend )
} )
jbookmarks := fyne . NewMenuItem ( "Join rooms in bookmarks" , func ( ) {
/*jbookmarks := fyne.NewMenuItem( "Join rooms in bookmarks", func() {
// FIXME: Race condition
client.FetchBookmarks()
rooms := client.BookmarkCache()
@@ -1083,11 +1147,12 @@ func main() {
}()
}
})
*/
menu_help := fyne . NewMenu ( "π" , mit , reconnect , licensesbtn , savedata )
menu_changeroom := fyne . NewMenu ( "Α " , mic , beginADM , joinARoom , leaveRoom , jbookmarks )
menu_configureview := fyne . NewMenu ( "Β " , mia , mis , jtt , jtb )
menu_changeroom := fyne . NewMenu ( "Α " , mic , beginADM , joinARoom , leaveRoom )
menu_configureview := fyne . NewMenu ( "Β " , mia , jtt , jtb )
hafjag := fyne . NewMenuItem ( "Hafjag" , func ( ) {
entry . Text = "Hafjag"
SendCallback ( )
@@ -1126,8 +1191,8 @@ func main() {
mycurrentplayingsong := fyne . NewMenuItem ( "Get currently playing song" , func ( ) {
// BEGIN PLATFORM SPECIFIC CODE
if os . PathSeparator == '\\' && os . PathListSeparator == ';' { // Windows
dialog . ShowError ( errors . New ( "T his feature is not supported on your operating system" ) , w )
if isWindows ( ) {
dialog . ShowError ( errors . New ( "t his feature is not supported on your operating system" ) , w )
return
}
// END PLATFORM SPECIFIC CODE
@@ -1147,20 +1212,20 @@ func main() {
album , al_ok := player . RawMetadata ( ) [ "xesam:album" ]
artists := [ ] string { }
if a_ok {
if a_ok {
artist . Store ( & artists )
}
if t_ok && a_ok && al_ok && album . String ( ) != "\"\"" {
if t_ok && a_ok && al_ok && album . String ( ) != "\"\"" {
newtext = fmt . Sprintf ( "I'm currently listening to %s by %s, in the %s album" , strings . Trim ( title . String ( ) , "\"" ) , strings . Join ( artists , "," ) , album )
} else if t_ok && a_ok {
newtext = fmt . Sprintf ( "I'm currently listening to %s by %s" , strings . Trim ( title . String ( ) , "\"" ) , strings . Join ( artists , "," ) )
newtext = fmt . Sprintf ( "I'm currently listening to %s by %s" , strings . Trim ( title . String ( ) , "\"" ) , strings . Join ( artists , "," ) )
} else if t_ok {
newtext = fmt . Sprintf ( "I'm currently listening to %s" , strings . Trim ( title . String ( ) , "\"" ) )
} else if a_ok {
newtext = fmt . Sprintf ( "I'm currently listening to a song by %s" , artists [ 0 ] )
} else {
dialog . ShowError ( errors . New ( "error: There's a playing song, but we could not get any information. " ) , w )
dialog . ShowError ( errors . New ( "error: There's a playing song, but we could not get any information" ) , w )
return
}
@@ -1264,18 +1329,24 @@ func main() {
ma := fyne . NewMainMenu ( menu_help , menu_changeroom , menu_configureview , menu_messageoptions , menu_jokes )
w . SetMainMenu ( ma )
desk , ok := a . ( desktop . App )
if ok {
desk . SetSystemTrayMenu ( menu_help )
}
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 _, mucJidStr := range login.MucsToJoin {
mucJid, err := jid.Parse(mucJidStr)
if err == nil {
addChatTab(true, mucJid, login.DisplayName)
}
}*/
for _ , userJidStr := range DMs {
fmt . Println ( userJidStr )
@@ -1308,14 +1379,41 @@ func main() {
}
chatSidebar = * UITab . Sidebar
old := chatSidebar . Position ( )
box := container . NewVBox ( widget . NewRichTextFromMarkdown ( "# " + activeChatJid ) , widget . NewLabel ( fmt . Sprintf ( "%d members " , len ( tab . Members ) ) ) )
chatSidebar . Objects = [ ] fyne . CanvasObject { }
for name := range tab . Members {
gen , _ := identicon . New ( "github" , 5 , 3 )
userjid , err := jid . Parse ( name )
if err != nil {
fmt . Println ( "ERROR: " + err . Error ( ) )
continue // unrecoverable
}
nickname := userjid . Resourcepart ( )
ii , err := gen . Draw ( nickname )
mention := func ( ) {
entry . SetText ( fmt . Sprintf ( "%s %s" , entry . Text , nickname ) )
}
if err != nil {
fmt . Println ( "ERROR: " + err . Error ( ) )
box . Add ( container . NewHBox ( widget . NewLabel ( nickname ) , widget . NewButton ( "Mention" , mention ) ) )
} else {
im := ii . Image ( 25 )
imageWidget := canvas . NewImageFromImage ( im )
imageWidget . FillMode = canvas . ImageFillOriginal
imageWidget . Refresh ( )
box . Add ( container . NewHBox ( imageWidget , widget . NewLabel ( nickname ) , widget . NewButton ( "Mention" , mention ) ) )
}
}
chatSidebar = * container . NewGridWithColumns ( 1 , container . NewVScroll ( box ) )
chatSidebar . Refresh ( )
chatSidebar . Move ( old )
}
// HACK - disable chatsidebar because it's currently very buggy
chatSidebar . Hidden = true
chatSidebar . Hidden = false
statBar . SetText ( "" )
w . SetContent ( container . NewVSplit ( container . NewVSplit ( AppTabs , container . NewHSplit ( entry , container . NewGridWithRows ( 1 , sendbtn , replybtn ) ) ) , container . NewHSplit ( & statBar , & chatInfo ) ) )
w . SetContent ( container . NewVSplit ( container . NewVSplit ( container . NewHSplit ( AppTabs , & chatSidebar ) , container . NewHSplit ( entry , container . NewGridWithRows ( 1 , sendbtn , replybtn ) ) ) , container . NewHSplit ( & statBar , & chatInfo ) ) )
w . ShowAndRun ( )
}