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-09-27 09:45:53 +01:00
"context"
2025-08-05 16:12:47 +01:00
"encoding/xml"
2025-09-02 19:25:54 +01:00
"errors"
2025-08-03 16:14:07 +01:00
"fmt"
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-09-07 08:10:47 +01:00
"fyne.io/fyne/v2/driver/desktop"
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-09-02 19:25:54 +01:00
"github.com/makeworld-the-better-one/go-isemoji"
2025-08-12 10:55:00 +01:00
"github.com/rrivera/identicon"
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-09-27 09:45:53 +01:00
"mellium.im/xmpp/bookmarks"
2025-08-03 16:14:07 +01:00
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
oasisSdk "pain.agency/oasis-sdk"
2025-08-05 23:54:14 +01:00
// TODO: integrated theme switcher
2025-08-03 16:14:07 +01:00
)
2025-09-28 15:54:34 +01:00
var version string = "3.1i"
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-09-03 13:23:41 +01:00
var replyNameIcon string = ">"
var replyBodyIcon string = ">"
2025-09-07 08:10:47 +01:00
var newlineIcon string = " |-> "
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-10-09 05:25:10 +01:00
Reactions map [ string ] string
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-09-27 13:10:49 +01:00
Members map [ string ] oasisSdk . UserPresence
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
}
2025-09-03 13:23:41 +01:00
func isUTF8Locale ( ) bool {
2025-09-04 18:02:53 +01:00
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
2025-09-03 13:23:41 +01:00
}
2025-09-04 18:11:37 +01:00
func isWindows ( ) bool {
return os . PathSeparator == '\\' && os . PathListSeparator == ';'
}
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 )
2025-10-09 05:25:10 +01:00
var AppTabs * container . DocTabs
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
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-09-07 08:10:47 +01:00
author . Selectable = 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-09-07 08:10:47 +01:00
content . Importance = widget . MediumImportance
2025-09-14 13:51:21 +01:00
//content.SizeName = fyne.ThemeSizeName(theme.SizeNameText)
2025-08-05 23:54:14 +01:00
icon := theme . FileVideoIcon ( )
2025-09-03 08:59:19 +01:00
replytext := widget . NewLabel ( ">fallback reply text" )
2025-09-02 09:26:10 +01:00
replytext . Hide ( )
replytext . Importance = widget . SuccessImportance
2025-09-07 08:10:47 +01:00
replytext . Selectable = true
2025-08-05 23:54:14 +01:00
btn := widget . NewButtonWithIcon ( "View media" , icon , func ( ) {
2025-08-04 16:45:56 +01:00
} )
2025-10-09 05:25:10 +01:00
reactions := container . NewHBox ( )
return container . NewVBox ( replytext , container . NewHBox ( ico , author ) , content , btn , reactions )
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-10-09 05:25:10 +01:00
reactions := vbox . Objects [ 4 ] . ( * fyne . Container )
reactions = container . NewVBox ( )
for _ , reaction := range chatTabs [ chatJidStr ] . Messages [ i ] . Reactions {
reactions . Add ( widget . NewLabel ( reaction ) )
}
reactions . Refresh ( )
2025-08-07 22:29:40 +01:00
if chatTabs [ chatJidStr ] . Messages [ i ] . Important {
2025-09-27 09:45:53 +01:00
content . Importance = widget . DangerImportance
2025-08-07 11:50:50 +01:00
}
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
2025-10-09 05:25:10 +01:00
if chatTabs [ chatJidStr ] . Messages [ i ] . Raw . OutOfBandMedia != nil {
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-10-09 05:25:10 +01:00
u , err := storage . ParseURI ( chatTabs [ chatJidStr ] . Messages [ i ] . Raw . OutOfBandMedia . URL )
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-10-09 05:25:10 +01:00
url , err := url . Parse ( chatTabs [ chatJidStr ] . Messages [ i ] . Raw . OutOfBandMedia . URL )
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 ( )
2025-09-04 18:27:51 +01:00
replytext . SetText ( fmt . Sprintf ( "%s %s" , replyBodyIcon , strings . ReplaceAll ( chatTabs [ chatJidStr ] . Messages [ i ] . Content , "\n" , newlineIcon ) ) )
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-03 13:23:41 +01:00
author . SetText ( fmt . Sprintf ( "%s %s %s" , chatTabs [ chatJidStr ] . Messages [ i ] . Author , replyNameIcon , 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 , " " )
2025-09-02 19:25:54 +01:00
if len ( sl ) == 1 && isemoji . IsEmoji ( sl [ 0 ] ) {
content . SizeName = fyne . ThemeSizeName ( theme . SizeNameHeadingText )
content . Refresh ( )
}
2025-08-08 15:26:12 -10:00
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-07 08:10:47 +01:00
scroller . CreateItem ( ) . Refresh ( )
2025-09-02 14:47:39 +01:00
vbox . Refresh ( )
2025-09-07 08:10:47 +01:00
/ *
2025-09-27 09:45:53 +01:00
fyne . Do ( func ( ) {
scroller . RefreshItem ( i )
} )
2025-09-07 08:10:47 +01:00
* /
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-09-27 13:10:49 +01:00
Members : make ( map [ string ] oasisSdk . UserPresence ) ,
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-10-09 05:25:10 +01:00
myTab := container . NewTabItemWithIcon ( chatJid . String ( ) , icon , myUITab . Scroller )
AppTabs . Append ( myTab )
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-05 12:55:22 +01:00
config = piConfig { }
2025-08-07 06:42:59 +01:00
a = app . NewWithID ( "pi-im" )
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-09-03 13:23:41 +01:00
if isUTF8Locale ( ) {
2025-09-04 18:02:53 +01:00
replyBodyIcon = "↱"
2025-09-03 13:23:41 +01:00
replyNameIcon = "→ "
2025-09-04 18:27:51 +01:00
newlineIcon = " ⮡ "
2025-09-03 13:23:41 +01:00
}
2025-09-27 09:45:53 +01:00
client , err := oasisSdk . CreateClient ( & login )
2025-08-31 15:53:00 +01:00
client . SetDmHandler ( func ( client * oasisSdk . XmppClient , msg * oasisSdk . XMPPChatMessage ) {
correction := false
userJidStr := msg . From . Bare ( ) . String ( )
2025-09-30 19:29:31 +01:00
fmt . Println ( userJidStr )
2025-08-31 15:53:00 +01:00
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 {
2025-09-30 19:29:31 +01:00
fmt . Println ( "Received reply in DM" )
2025-08-31 15:53:00 +01:00
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 ( ) {
2025-09-28 09:45:56 +01:00
tab . Messages [ i ] . Content = * msg . CleanedBody + " (corrected)"
2025-08-31 15:53:00 +01:00
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 {
2025-10-09 05:25:10 +01:00
Author : msg . From . Resourcepart ( ) ,
Content : str ,
ID : msg . ID ,
ReplyID : replyID ,
Raw : * msg ,
ImageURL : img ,
Reactions : make ( map [ string ] string ) ,
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 ( ) {
2025-09-30 19:29:31 +01:00
//UITabs[userJidStr].Scroller.Refresh()
2025-08-31 15:53:00 +01:00
if scrollDownOnNewMessage {
2025-09-30 19:29:31 +01:00
//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 ) {
ignore := false
2025-10-09 05:25:10 +01:00
reaction := false
2025-08-31 15:53:00 +01:00
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-10-09 05:25:10 +01:00
if v . XMLName . Local == "reactions" {
reaction = true
break
}
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 {
2025-09-30 19:29:31 +01:00
chatInfo . Objects [ 0 ] = widget . NewLabel ( fmt . Sprintf ( "[!] %s" , mucJidStr ) )
2025-08-31 15:53:00 +01:00
chatTabs [ mucJidStr ] . Muc = muc
str := * msg . CleanedBody
if strings . Contains ( str , login . DisplayName ) {
2025-09-04 18:11:37 +01:00
fmt . Println ( str , login . DisplayName )
2025-08-31 15:53:00 +01:00
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 , " " )
}
var replyID string
if msg . Reply == nil {
replyID = "PICLIENT:UNAVAILABLE"
} else {
replyID = msg . Reply . To
}
2025-08-06 19:32:37 +01:00
2025-10-09 05:25:10 +01:00
if reaction {
for i := len ( tab . Messages ) - 1 ; i > 0 ; i -- {
if tab . Messages [ i ] . Raw . StanzaID . ID == msg . Reply . ID {
tab . Messages [ i ] . Reactions [ msg . From . String ( ) ] = * msg . CleanedBody
fyne . Do ( func ( ) {
UITabs [ mucJidStr ] . Scroller . Refresh ( )
} )
return
}
}
}
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 ( ) {
2025-09-28 09:45:56 +01:00
tab . Messages [ i ] . Content = * msg . CleanedBody + " (corrected)"
2025-08-31 15:53:00 +01:00
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-10-09 05:25:10 +01:00
Reactions : make ( map [ string ] string ) ,
2025-08-03 16:14:07 +01:00
}
2025-08-31 15:53:00 +01:00
if ! ignore {
tab . Messages = append ( tab . Messages , myMessage )
}
2025-09-04 18:27:51 +01:00
important = false
2025-08-31 15:53:00 +01:00
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 ( "" )
} )
}
} )
2025-09-27 09:45:53 +01:00
client . SetPresenceHandler ( func ( client * oasisSdk . XmppClient , from jid . JID , p oasisSdk . UserPresence ) {
2025-09-27 13:10:49 +01:00
bareAcc := from . Bare ( )
tab , ok := chatTabs [ bareAcc . String ( ) ]
if ! ok {
2025-09-30 19:29:31 +01:00
// User presence
addChatTab ( false , bareAcc , client . Login . DisplayName )
2025-09-27 13:10:49 +01:00
return
}
if tab . isMuc {
tab . Members [ from . String ( ) ] = p
}
2025-09-27 09:45:53 +01:00
} )
client . SetBookmarkHandler ( false , func ( client * oasisSdk . XmppClient , bookmark bookmarks . Channel ) {
2025-09-27 13:10:49 +01:00
// FIXME
if bookmark . JID . String ( ) == "conversations-offtopic-reloaded@conference.trashserver.net" {
return
}
2025-09-27 09:45:53 +01:00
if bookmark . Autojoin {
if bookmark . Nick == "" {
2025-09-28 15:54:34 +01:00
fmt . Println ( "WARNING: Bookmark has no name" )
2025-09-27 09:45:53 +01:00
bookmark . Nick = client . Login . DisplayName
}
2025-09-27 13:10:49 +01:00
addChatTab ( true , bookmark . JID , client . Login . DisplayName )
2025-09-27 09:45:53 +01:00
_ , err := client . ConnectMuc ( bookmark , oasisSdk . MucLegacyHistoryConfig { } , context . TODO ( ) )
if err != nil {
fmt . Println ( "ERROR: " + err . Error ( ) )
return
}
}
} )
2025-08-31 15:53:00 +01:00
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 -- {
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
}
}
}
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 )
2025-09-27 09:45:53 +01:00
//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 )
//})
2025-08-04 16:45:56 +01:00
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-07 08:10:47 +01:00
2025-08-04 10:05:43 +01:00
w = a . NewWindow ( "pi" )
2025-09-30 19:29:31 +01:00
w . SetCloseIntercept ( func ( ) {
2025-10-09 05:25:10 +01:00
w . RequestFocus ( )
2025-09-30 19:29:31 +01:00
dialog . ShowConfirm ( "Close pi" , "You hit the close button. Do you want Pi to close completely (confirm) or minimize to the tray? (cancel)" , func ( b bool ) {
if b {
w . Close ( )
a . Quit ( )
log . Fatalln ( "Goodbye!" )
} else {
w . Hide ( )
}
} , w )
} )
2025-08-04 10:05:43 +01:00
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-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
2025-09-03 09:32:24 +01:00
} else {
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
if err != nil {
dialog . ShowError ( err , w )
}
2025-09-01 20:05:06 +01:00
}
} , w )
2025-09-30 19:29:31 +01:00
} else if text == "@here" && chatTabs [ activeMucJid ] . isMuc {
tab := chatTabs [ activeMucJid ]
dialog . ShowConfirm ( "WARNING" , fmt . Sprintf ( "There are %d members in this room.\nYou are about to mention every single one of them.\nYou may be punished if you are not a moderator of this chat.\nWould you like to continue?" , len ( tab . Members ) ) , func ( b bool ) {
if b {
text = ""
for name := range tab . Members {
text = fmt . Sprintf ( "%s %s" , text , jid . MustParse ( name ) . Resourcepart ( ) )
}
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
if err != nil {
dialog . ShowError ( err , w )
}
}
} , w )
2025-09-03 09:32:24 +01:00
} else {
err = client . SendText ( jid . MustParse ( activeMucJid ) . Bare ( ) , text )
2025-08-07 11:50:50 +01:00
2025-09-03 09:32:24 +01:00
if err != nil {
dialog . ShowError ( err , w )
}
2025-08-04 10:05:43 +01:00
}
} ( )
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-09-03 07:59:23 +01:00
licensesbtn := fyne . NewMenuItem ( "credits" , func ( ) {
2025-09-02 19:40:35 +01:00
CreditsWindow ( fyne . CurrentApp ( ) , fyne . NewSize ( 800 , 400 ) ) . Show ( )
} )
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-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-09-28 09:45:56 +01:00
leaveRoom := fyne . NewMenuItem ( "disconnect from current room" , func ( ) {
2025-08-10 09:50:24 +01:00
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
}
}
2025-10-09 05:25:10 +01:00
if ! chatTabs [ activeMucJid ] . isMuc {
return
}
2025-08-10 09:50:24 +01:00
AppTabs . Selected ( ) . Text = fmt . Sprintf ( "%s (disconnected)" , AppTabs . Selected ( ) . Text )
2025-09-27 09:45:53 +01:00
AppTabs . Items = append ( AppTabs . Items [ : AppTabs . SelectedIndex ( ) ] , AppTabs . Items [ AppTabs . SelectedIndex ( ) + 1 : ] ... )
2025-08-10 09:50:24 +01:00
AppTabs . SelectIndex ( 0 )
2025-09-28 09:45:56 +01:00
err := client . DisconnectMuc ( activeMucJid , "user left on own accord" , context . TODO ( ) )
if err != nil {
dialog . ShowError ( err , w )
2025-09-27 09:45:53 +01:00
}
delete ( UITabs , activeMucJid )
2025-08-10 09:50:24 +01:00
} )
2025-09-28 09:45:56 +01:00
manageBookmarks := fyne . NewMenuItem ( "manage bookmarks" , func ( ) {
bookmarks := client . BookmarkCache ( )
box := container . NewVBox ( )
2025-09-28 15:54:34 +01:00
box . Add ( widget . NewRichTextFromMarkdown ( "# Manage Bookmarks" ) )
2025-09-28 09:45:56 +01:00
for address , bookmark := range bookmarks {
2025-09-28 15:54:34 +01:00
bookmarkWidget := container . NewGridWithColumns ( 5 )
nameLabel := widget . NewLabel ( bookmark . Name )
nameLabel . Wrapping = fyne . TextWrapBreak
bookmarkWidget . Add ( nameLabel )
bookmarkJidWidget := widget . NewLabel ( address )
bookmarkJidWidget . TextStyle . Monospace = true
bookmarkJidWidget . Selectable = true
bookmarkJidWidget . Wrapping = fyne . TextWrapWord
bookmarkWidget . Add ( bookmarkJidWidget )
2025-09-28 09:45:56 +01:00
var autojoinCheck * widget . Check
var deleteBookmarkButton * widget . Button
var joinRoomButton * widget . Button
joinRoomButton = widget . NewButtonWithIcon ( "Join" , theme . AccountIcon ( ) , func ( ) {
fyne . Do ( func ( ) { joinRoomButton . Disable ( ) } )
go func ( ) {
2025-09-28 15:54:34 +01:00
if bookmark . Nick == "" {
bookmark . Nick = client . Login . DisplayName
}
2025-09-28 09:45:56 +01:00
var zero uint64 = uint64 ( 0 )
addChatTab ( true , bookmark . JID , bookmark . Nick )
_ , err := client . ConnectMuc ( bookmark , oasisSdk . MucLegacyHistoryConfig { MaxCount : & zero } , context . TODO ( ) )
if err != nil {
fyne . Do ( func ( ) {
joinRoomButton . Enable ( )
dialog . ShowError ( err , w )
} )
return
}
client . RefreshBookmarks ( false )
fyne . Do ( func ( ) { joinRoomButton . Enable ( ) } )
} ( )
} )
autojoinCheck = widget . NewCheck ( "Autojoin" , func ( b bool ) {
go func ( ) {
fyne . Do ( func ( ) { autojoinCheck . Disable ( ) } )
bookmark . Autojoin = b
err := client . PublishBookmark ( bookmark , context . TODO ( ) )
if err != nil {
fyne . Do ( func ( ) {
autojoinCheck . Enable ( )
dialog . ShowError ( err , w )
} )
return
}
client . RefreshBookmarks ( false )
fyne . Do ( func ( ) { autojoinCheck . Enable ( ) } )
} ( )
} )
autojoinCheck . Checked = bookmark . Autojoin
deleteBookmarkButton = widget . NewButtonWithIcon ( "Delete" , theme . CancelIcon ( ) , func ( ) {
go func ( ) {
err := client . DeleteBookmark ( bookmark . JID , context . TODO ( ) )
if err != nil {
fyne . Do ( func ( ) {
dialog . ShowError ( err , w )
} )
return
}
client . RefreshBookmarks ( false )
bookmarkWidget . RemoveAll ( )
} ( )
} )
bookmarkWidget . Add ( autojoinCheck )
bookmarkWidget . Add ( deleteBookmarkButton )
bookmarkWidget . Add ( joinRoomButton )
box . Add ( bookmarkWidget )
}
2025-09-28 15:54:34 +01:00
myScroller := container . NewScroll ( box )
AppTabs . Items [ 0 ] . Content = myScroller
AppTabs . Items [ 0 ] . Text = "Bookmarks"
AppTabs . SelectIndex ( 0 )
2025-09-30 19:29:31 +01:00
chatSidebar . Hidden = true
2025-09-28 15:54:34 +01:00
//d := dialog.NewCustom("manage bookmarks", "cancel", myScroller, w)
//d.Show()
2025-09-28 09:45:56 +01:00
} )
joinARoom := fyne . NewMenuItem ( "connect to a room" , func ( ) {
dialog . ShowEntryDialog ( "connect to 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
}
2025-09-27 13:10:49 +01:00
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 ( ) )
2025-08-09 22:45:39 +01:00
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
}
2025-09-27 13:10:49 +01:00
//client.MucChannels[s] = ch
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-28 15:56:08 +01:00
menu_help := fyne . NewMenu ( "π" , mit , reconnect , licensesbtn )
2025-09-02 19:25:54 +01:00
2025-09-28 09:45:56 +01:00
menu_changeroom := fyne . NewMenu ( "Α " , mic , beginADM , joinARoom , leaveRoom , manageBookmarks )
2025-09-04 18:11:37 +01:00
menu_configureview := fyne . NewMenu ( "Β " , mia , 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-09-30 19:29:31 +01:00
kai := fyne . NewMenuItem ( "kai cenat beg" , func ( ) {
old := entry . Text
entry . Text = "chat will you subscribe to save the kai cenat mafiathon 3"
SendCallback ( )
entry . Text = old
} )
mipipiemi := fyne . NewMenuItem ( "mi pipi e mi" , func ( ) {
old := entry . Text
entry . Text = "mi pipi e mi"
SendCallback ( )
entry . Text = old
} )
exposed_suffix := fyne . NewMenuItem ( ".exposed" , func ( ) {
selectedScroller , ok := AppTabs . Selected ( ) . Content . ( * widget . List )
if ! ok {
return
}
var activeChatJid string
for jid , tabData := range UITabs {
if tabData . Scroller == selectedScroller {
activeChatJid = jid
}
}
LatestMessage := chatTabs [ activeChatJid ] . Messages [ len ( chatTabs [ activeChatJid ] . Messages ) - 1 ]
old := entry . Text
entry . Text = fmt . Sprintf ( "%s.exposed" , LatestMessage . Content )
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 ( ) {
2025-09-03 08:59:19 +01:00
// BEGIN PLATFORM SPECIFIC CODE
2025-09-27 09:45:53 +01:00
if isWindows ( ) {
dialog . ShowError ( errors . New ( "this feature is not supported on your operating system" ) , w )
2025-09-03 08:59:19 +01:00
return
}
// END PLATFORM SPECIFIC CODE
2025-09-02 14:47:39 +01:00
client , err := mpris . NewClient ( )
if err != nil {
dialog . ShowError ( err , w )
return
}
present := false
for _ , player := range client . Players ( ) {
2025-09-02 19:25:54 +01:00
fmt . Println ( player . RawMetadata ( ) )
2025-09-02 14:47:39 +01:00
old := entry . Text
2025-09-02 19:25:54 +01:00
newtext := ""
title , t_ok := player . RawMetadata ( ) [ "xesam:title" ]
artist , a_ok := player . RawMetadata ( ) [ "xesam:artist" ]
2025-09-03 08:59:19 +01:00
album , al_ok := player . RawMetadata ( ) [ "xesam:album" ]
artists := [ ] string { }
2025-09-03 09:32:24 +01:00
if a_ok {
2025-09-03 08:59:19 +01:00
artist . Store ( & artists )
}
2025-09-03 09:32:24 +01:00
if t_ok && a_ok && al_ok && album . String ( ) != "\"\"" {
2025-09-03 08:59:19 +01:00
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 {
2025-09-03 09:32:24 +01:00
newtext = fmt . Sprintf ( "I'm currently listening to %s by %s" , strings . Trim ( title . String ( ) , "\"" ) , strings . Join ( artists , "," ) )
2025-09-02 19:25:54 +01:00
} else if t_ok {
2025-09-03 08:59:19 +01:00
newtext = fmt . Sprintf ( "I'm currently listening to %s" , strings . Trim ( title . String ( ) , "\"" ) )
2025-09-02 19:25:54 +01:00
} else if a_ok {
2025-09-03 08:59:19 +01:00
newtext = fmt . Sprintf ( "I'm currently listening to a song by %s" , artists [ 0 ] )
2025-09-02 19:25:54 +01:00
} else {
2025-09-27 09:45:53 +01:00
dialog . ShowError ( errors . New ( "error: There's a playing song, but we could not get any information" ) , w )
2025-09-02 19:25:54 +01:00
return
}
2025-09-03 08:59:19 +01:00
2025-09-02 19:25:54 +01:00
entry . SetText ( newtext )
2025-09-02 14:47:39 +01:00
SendCallback ( )
entry . SetText ( old )
present = true
}
if ! present {
2025-09-02 19:25:54 +01:00
dialog . ShowInformation ( "Failed" , "Could not find any open 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" , w )
2025-09-02 14:47:39 +01:00
}
} )
2025-09-30 19:29:31 +01:00
menu_jokes := fyne . NewMenu ( "Δ" , mycurrenttime , mycurrentplayingsong , hafjag , hotfuck , agree , kai , mipipiemi , exposed_suffix )
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-09-07 08:10:47 +01:00
desk , ok := a . ( desktop . App )
if ok {
2025-09-30 19:29:31 +01:00
desk . SetSystemTrayMenu ( fyne . NewMenu ( "" , fyne . NewMenuItem ( "show" , w . Show ) ) )
2025-09-07 08:10:47 +01:00
}
2025-10-09 05:25:10 +01:00
AppTabs = container . NewDocTabs (
2025-09-28 15:54:34 +01:00
container . NewTabItem ( "..." , widget . NewLabel ( `
2025-08-06 19:32:37 +01:00
pi
2025-09-28 15:54:34 +01:00
This tab will be used for displaying certain actions , such as
2025-09-30 19:29:31 +01:00
managing your bookmarks and configuring rooms .
2025-08-04 16:45:56 +01:00
` ) ) ,
2025-08-04 10:05:43 +01:00
)
2025-10-09 05:25:10 +01:00
AppTabs . CloseIntercept = func ( ti * container . TabItem ) {
go func ( ) {
var activeChatJid string
scroller , ok := ti . Content . ( * widget . List )
if ! ok {
return
}
for jid , tabData := range UITabs {
if tabData . Scroller == scroller {
activeChatJid = jid
break
}
}
if ! chatTabs [ activeChatJid ] . isMuc {
return
}
err := client . DisconnectMuc ( activeChatJid , "user left on own accord" , context . TODO ( ) )
if err != nil {
dialog . ShowError ( err , w )
}
AppTabs . Selected ( ) . Text = fmt . Sprintf ( "%s (disconnected)" , AppTabs . Selected ( ) . Text )
AppTabs . Remove ( ti )
delete ( UITabs , activeChatJid )
} ( )
}
2025-08-04 10:05:43 +01:00
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 ) {
2025-09-30 19:29:31 +01:00
if AppTabs . Selected ( ) == AppTabs . Items [ 0 ] {
chatSidebar . Hidden = true
return
}
2025-08-07 22:29:40 +01:00
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-06 10:27:27 +01:00
2025-08-07 22:29:40 +01:00
chatSidebar = * UITab . Sidebar
2025-09-30 19:29:31 +01:00
if tab . isMuc {
2025-10-09 05:25:10 +01:00
nameLabel := widget . NewRichTextFromMarkdown ( "# " + activeChatJid )
nameLabel . Truncation = fyne . TextTruncateEllipsis
box := container . NewVBox ( nameLabel , widget . NewLabel ( fmt . Sprintf ( "%d members " , len ( tab . Members ) ) ) )
2025-09-30 19:29:31 +01:00
chatSidebar . Objects = [ ] fyne . CanvasObject { }
2025-09-27 13:10:49 +01:00
2025-09-30 19:29:31 +01:00
for name , p := 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 ( )
if nickname == "" {
continue // we got the MUC presence, do not include it in the member list
}
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 {
2025-10-09 05:25:10 +01:00
im := ii . Image ( 15 )
2025-09-30 19:29:31 +01:00
imageWidget := canvas . NewImageFromImage ( im )
imageWidget . FillMode = canvas . ImageFillOriginal
imageWidget . Refresh ( )
nickLabel := widget . NewLabel ( nickname )
nickLabel . Selectable = true
box . Add ( container . NewHBox ( imageWidget , nickLabel ) )
if p . Status != "" {
s := widget . NewLabel ( fmt . Sprintf ( "\"%s\"" , p . Status ) )
s . Importance = widget . WarningImportance
s . TextStyle = fyne . TextStyle {
Bold : false ,
Italic : true ,
Monospace : false ,
}
s . Truncation = fyne . TextTruncateEllipsis
box . Add ( s )
}
}
2025-09-27 13:10:49 +01:00
2025-09-30 19:29:31 +01:00
}
chatSidebar = * container . NewGridWithColumns ( 1 , container . NewVScroll ( box ) )
} else {
chatSidebar = * container . NewVBox ( widget . NewRichTextFromMarkdown ( "# " + activeChatJid ) )
2025-09-27 13:10:49 +01:00
}
2025-09-30 19:29:31 +01:00
chatSidebar . Hidden = false
2025-08-07 11:50:50 +01:00
chatSidebar . Refresh ( )
2025-09-27 13:10:49 +01:00
2025-08-05 23:54:14 +01:00
}
2025-09-27 13:10:49 +01:00
chatSidebar . Hidden = false
2025-08-07 11:50:50 +01:00
statBar . SetText ( "" )
2025-09-30 19:29:31 +01:00
chatInfo = * container . NewHBox ( widget . NewLabel ( "" ) )
2025-10-09 05:25:10 +01:00
MainSplit := container . NewHSplit ( AppTabs , & chatSidebar )
DownSplit := container . NewHSplit ( entry , container . NewGridWithRows ( 1 , sendbtn , replybtn ) )
BigSplit := container . NewVSplit ( MainSplit , DownSplit )
BigSplit . SetOffset ( 1 )
SmallSplit := container . NewHSplit ( & statBar , & chatInfo )
MasterSplit := container . NewVSplit ( BigSplit , SmallSplit )
MasterSplit . SetOffset ( 1 )
w . SetContent ( MasterSplit )
2025-08-03 16:14:07 +01:00
w . ShowAndRun ( )
2025-08-03 11:17:46 +01:00
}