32 Commits
3.1a ... 3.14a

Author SHA1 Message Date
6c3195b029 Merge branch 'master' of https://github.com/sunglocto/pi-im 2025-08-08 10:26:21 +01:00
5b5d4656aa Put all greek letters in alphabetical order and capitalize them, except for Pi for obvious reasons 2025-08-08 10:26:14 +01:00
sunglocto
3afa1e7e38 Update README.md 2025-08-08 07:59:19 +00:00
ece04e1c36 Fix artifacts which broke due to renamed application name 2025-08-07 23:27:11 +01:00
52e38e7e66 Format code 2025-08-07 23:12:40 +01:00
59d83cb185 Add identicons 2025-08-07 23:11:56 +01:00
4015107de0 Continue last commit 2025-08-07 22:29:40 +01:00
150f42bc58 Begin seperation of XMPP and UI 2025-08-07 21:53:28 +01:00
3d6f835d4f Add Delta menu 2025-08-07 21:13:56 +01:00
922bc1d7cf Some changes 2025-08-07 11:50:50 +01:00
a61d3090e1 Disable all mardown support in preparation for a better solution 2025-08-07 06:50:23 +01:00
215839d833 Change name to pi-im everywhere 2025-08-07 06:43:09 +01:00
d7264e91f7 Change name to pi-im everywhere 2025-08-07 06:42:59 +01:00
sunglocto
93d3bb20d2 Add more testimonials 2025-08-06 21:05:15 +00:00
181b91edb4 dont send a message if entry is empty - Fixes #7 2025-08-06 21:59:49 +01:00
ca6bda7950 Do not show unknown state - Fixes #5 2025-08-06 21:39:27 +01:00
14cda04e06 i am an idiot 2025-08-06 20:04:14 +01:00
47d93ffe0e Fix 1:1 dms, move pi.xml to ~/.config/fyne/pi-ism/Documents/pi.xml (or other location) to decrease platform dependency, add support for corrections, and re-enable classic history along with a bunch of other changes 2025-08-06 19:32:37 +01:00
3c84dd7702 format code 2025-08-06 11:00:22 +01:00
dcc3baaf02 Merge branch 'master' of https://github.com/sunglocto/pi 2025-08-06 10:55:14 +01:00
362930c5d5 Fix file uploads, and allow using Shift+Enter keybind to send a message 2025-08-06 10:55:06 +01:00
sunglocto
c6bd18ef9c Update README.md 2025-08-06 09:29:30 +00:00
9f57d6688b General code changes + new pane 2025-08-06 10:27:27 +01:00
13d6041711 Add extra information + fix grammar mistake in readme 2025-08-06 00:22:32 +01:00
d1521c9704 funny 2025-08-06 00:16:50 +01:00
20888a4e60 artifacts attempt no.1 2025-08-06 00:12:17 +01:00
e4300d9282 funky 2025-08-06 00:07:54 +01:00
12e4a064d6 idk 2025-08-05 23:54:14 +01:00
2190896442 Edit README.md and bump deps so hopefully CI will work 2025-08-05 23:52:35 +01:00
d1e67df750 Idfk read the commits 2025-08-05 21:00:16 +01:00
e15a6424bb Fully remove all JSON support 2025-08-05 17:49:29 +01:00
64d1c420a2 Format code 2025-08-05 16:32:55 +01:00
6 changed files with 561 additions and 275 deletions

View File

@@ -1,7 +1,7 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
name: build this now
on:
push:
@@ -24,7 +24,11 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev libglu1-mesa-dev libxxf86vm-dev
- name: Build
run: go build -v ./...
run: go build .
- name: Artifact
uses: actions/upload-artifact@v4
with:
name: pi-binary
path: pi-im
- name: Test
run: go test -v ./...

2
.gitignore vendored
View File

@@ -26,7 +26,7 @@ go.work.sum
# env file
.env
pi.json
pi.xml
pi
# Editor/IDE
# .idea/

View File

@@ -1,14 +1,17 @@
<center>
<img src="https://github.com/sunglocto/pi/blob/255bc3749c089e3945871ddf19dd17d14a83f9ff/pi.png">
<img width="100" height="100" src="https://github.com/sunglocto/pi/blob/255bc3749c089e3945871ddf19dd17d14a83f9ff/pi.png">
</center>
# π
[![Go](https://github.com/sunglocto/pi/actions/workflows/go.yml/badge.svg)](https://github.com/sunglocto/pi/actions/workflows/go.yml)
[![build this now](https://github.com/sunglocto/pi/actions/workflows/go.yml/badge.svg)](https://github.com/sunglocto/pi/actions/workflows/go.yml)
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/9e2d9209-6ad5-4f22-94d0-4cc18c835372" />
## the XMPP client from hell
> it's 10% code. 20% ai
Experimental and extremely weird XMPP client made with Go. No solicitors.
Experimental and extremely weird XMPP client written in Go. No solicitors.
pi is currently pre-pre-pre-pre alpha software which you should not use right now.
pi is currently pre-pre-pre-pre alpha software which you should not use as your primary XMPP client.
pi uses [Fyne](https://fyne.io) for the frontend and uses the [Oasis SDK](https://github.com/jjj333-p/oasis-sdk) for XMPP functionality.
@@ -20,35 +23,27 @@ pi is an extremely opinionated client. It aims to have as little extra windows a
When you launch pi, you will be greeted with a create account screen. You will then be able to enter your XMPP account details and then relaunch the application to log in.
If you want to add MUCs or DMs, you must configure the program. Here is the general idea:
If you want to add MUCs or DMs, you must configure the program by editing the pi.xml file. Here is an example configuration:
```json
{
"Login": {
"Host": "example.com:5222",
"User": "user@example.com",
"Password": "123456",
"DisplayName": "user",
"NoTLS": false,
"StartTLS": true,
"Mucs": [
"room1@group.example.com",
"room2@group.example.com"
]
},
"DMs": [
"mike@example.com",
"louis@example.com"
],
"Notifications": false
}
```xml
<piConfig>
<Login>
<Host>example.com:5222</Host>
<User>user@example.com</User>
<Password>123456789</Password>
<DisplayName>sunglocto</DisplayName>
<TLSoff>false</TLSoff>
<StartTLS>true</StartTLS>
<MucsToJoin>room1@muc.example.com</MucsToJoin>
<MucsToJoin>room2@muc.example.com</MucsToJoin>
</Login>
<Notifications>true</Notifications>
</piConfig>
```
Edit this file as necessary.
Currently joining and saving DM tabs is not supported, nor is getting avatars, reactions or encryption.
Currently joining and saving DM tabs is not supported, nor is getting avatars, reactions, encryption of media embed.
As of writing, pi supports basic message sending and receiving, replies and file upload.
As of writing, pi supports basic message sending and receiving, replies and ~~file upload~~.
## να χτίσω
@@ -64,21 +59,42 @@ git clone https://github.com/sunglocto/pi
cd pi
go mod tidy
go build .
vim pi.json
./pi
```
> Uh, Windows???
Eventually. Don't count on it.
Fyne has first-class support for Windows and none of my dependencies are platform dependent. I've built this app for Android before. If you compile it, it will most likely work with no issues.
Static executable snapshots are also provided for GNU/Linux systems.
Static executable snapshots are also provided for GNU/Linux systems, and CI runs on every commit, producing a binary on every successful build. You're welcome.
## χρήση
(usage)
TODO
## υποστήριξη
(support)
# επιπλέον
You can file an issue and explain the problem you are having.
If you would like a more instant method of communication, join the [pi XMPP room.](xmpp:pi@room.sunglocto.net?join)
## μαρτυρίες
(testimonials)
From fellow insane and schizophrenic XMPP users:
> anyways this is your "just IM" client things ig.
> this looks like shit
> fyne is the best UI toolkit (sarcastic)
> i am going to explode you
> pi devstream when
<img width="361" height="66" alt="image" src="https://github.com/user-attachments/assets/5a926f6b-1005-4795-a6ef-4e0538bb4d5a" />
<img width="316" height="73" alt="image" src="https://github.com/user-attachments/assets/52309c60-8110-43eb-9c45-56c9cfd82cc4" />
## επιπλέον
(extra)
Pi version numbers are the digits of Pi followed by a letter indicating the phase of development the program is in.
@@ -91,6 +107,6 @@ Is the third version produced in the alpha phase.
The digits of Pi will reset back to `3` when moving to a new phase.
If the number gets too long, it will reset to one digit of 2π. Once that gets to long, it will be digits of 3π and etc.
If the number gets too long, it will reset to one digit of 2π. Once that gets too long, it will be digits of 3π and etc.
Named after [Psi](https://github.com/psi-im/psi).

5
go.mod
View File

@@ -1,11 +1,11 @@
module pi
module pi-im
go 1.24.5
require (
fyne.io/fyne/v2 v2.6.2
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb
github.com/mbaklor/fyne-catppuccin v0.0.2
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d
mellium.im/xmpp v0.22.0
pain.agency/oasis-sdk v0.0.0-20250805052243-df6be3f9f629
)
@@ -13,7 +13,6 @@ require (
require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect

6
go.sum
View File

@@ -6,8 +6,6 @@ fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb h1:2BazNmb/kwgqRdvE9L+NgW8sfoW
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb/go.mod h1:u3LF1EkElytjOT8OHxft16trctGndF9qpsoH6YIDOUU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
@@ -50,8 +48,6 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mbaklor/fyne-catppuccin v0.0.2 h1:yMNnYkmFwjKJkFQvCd1uNKZDs07ZC85wTkedTIGcViE=
github.com/mbaklor/fyne-catppuccin v0.0.2/go.mod h1:ZBIy4dV1yMj+7oEaZYkXm5OfYESmXuPWwNcuUmD1Njo=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
@@ -62,6 +58,8 @@ github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs=
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d/go.mod h1:TbpErkob6SY7cyozRVSGoB3OlO2qOAgVN8O3KAJ4fMI=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=

571
main.go
View File

@@ -1,17 +1,18 @@
package main
import (
"encoding/json"
//core - required
"encoding/xml"
"fmt"
"image/color"
"io"
_ "io/fs"
"log"
"net/url"
"os"
"strings"
"time"
// gui - required
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
@@ -20,13 +21,24 @@ import (
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
catppuccin "github.com/mbaklor/fyne-catppuccin"
"github.com/rrivera/identicon"
// xmpp - required
"mellium.im/xmpp/disco"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/muc"
oasisSdk "pain.agency/oasis-sdk"
// gui - optional
// catppuccin "github.com/mbaklor/fyne-catppuccin"
adwaita "fyne.io/x/fyne/theme"
// TODO: integrated theme switcher
)
var version string = "3.1a"
var version string = "3.14a"
var statBar widget.Label
var chatInfo fyne.Container
var chatSidebar fyne.Container
// by sunglocto
// license AGPL
@@ -38,14 +50,22 @@ type Message struct {
ReplyID string
ImageURL string
Raw oasisSdk.XMPPChatMessage
Important bool
}
type MucTab struct {
type ChatTab struct {
Jid jid.JID
Nick string
Messages []Message
Scroller *widget.List
isMuc bool
Muc *muc.Channel
UpdateSidebar bool
}
type ChatTabUI struct {
Internal *ChatTab
Scroller *widget.List `xml:"-"`
Sidebar *fyne.Container `xml:"-"`
}
type piConfig struct {
@@ -58,8 +78,10 @@ var config piConfig
var login oasisSdk.LoginInfo
var DMs []string
var chatTabs = make(map[string]*MucTab)
var tabs *container.AppTabs
var chatTabs = make(map[string]*ChatTab)
var UITabs = make(map[string]*ChatTabUI)
var AppTabs *container.AppTabs
var selectedId widget.ListItemID
var replying bool = false
var notifications bool
@@ -68,7 +90,7 @@ var connection bool = true
type myTheme struct{}
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return catppuccin.New().Color(name, variant)
return adwaita.AdwaitaTheme().Color(name, variant)
}
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
@@ -90,48 +112,69 @@ var scrollDownOnNewMessage bool = true
var w fyne.Window
var a fyne.App
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
mucJidStr := chatJid.String()
if _, ok := chatTabs[mucJidStr]; ok {
// Tab already exists
return
}
tabData := &MucTab{
Jid: chatJid,
Nick: nick,
Messages: []Message{},
isMuc: isMuc,
}
func CreateUITab(chatJidStr string) ChatTabUI {
var scroller *widget.List
scroller = widget.NewList(
func() int {
return len(tabData.Messages)
return len(chatTabs[chatJidStr].Messages)
},
func() fyne.CanvasObject {
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw("default")
im := ii.Image(25)
ico := canvas.NewImageFromImage(im)
ico.FillMode = canvas.ImageFillOriginal
author := widget.NewLabel("author")
author.TextStyle.Bold = true
content := widget.NewRichTextWithText("content")
content := widget.NewLabel("content")
content.Wrapping = fyne.TextWrapWord
icon := theme.FileImageIcon()
btn := widget.NewButtonWithIcon("View image", icon, func() {
content.Selectable = true
icon := theme.FileVideoIcon()
btn := widget.NewButtonWithIcon("View media", icon, func() {
})
return container.NewVBox(author, content, btn)
return container.NewVBox(container.NewHBox(ico, author), content, btn)
},
func(i widget.ListItemID, co fyne.CanvasObject) {
vbox := co.(*fyne.Container)
author := vbox.Objects[0].(*widget.Label)
content := vbox.Objects[1].(*widget.RichText)
authorBox := vbox.Objects[0].(*fyne.Container)
// generate a Icon
gen, _ := identicon.New("github", 5, 3)
ii, _ := gen.Draw(chatTabs[chatJidStr].Messages[i].Author)
im := ii.Image(25)
authorBox.Objects[0] = canvas.NewImageFromImage(im)
authorBox.Objects[0].(*canvas.Image).FillMode = canvas.ImageFillOriginal
authorBox.Objects[0].Refresh()
// Icon generate end
author := authorBox.Objects[1].(*widget.Label)
content := vbox.Objects[1].(*widget.Label)
btn := vbox.Objects[2].(*widget.Button)
if chatTabs[chatJidStr].Messages[i].Important {
//content.Importance = widget.DangerImportance TODO: Fix highlighting messages with mentions, it's currently broken
}
btn.Hidden = true // Hide by default
msgContent := tabData.Messages[i].Content
if tabData.Messages[i].ImageURL != "" {
msgContent := chatTabs[chatJidStr].Messages[i].Content
if chatTabs[chatJidStr].Messages[i].ImageURL != "" {
btn.Hidden = false
btn.OnTapped = func() {
fyne.Do(func() {
u, _ := storage.ParseURI(tabData.Messages[i].ImageURL)
u, err := storage.ParseURI(chatTabs[chatJidStr].Messages[i].ImageURL)
if err != nil {
dialog.ShowError(err, w)
return
}
if strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "mp4") || strings.HasSuffix(chatTabs[chatJidStr].Messages[i].ImageURL, "mp3") {
url, err := url.Parse(chatTabs[chatJidStr].Messages[i].ImageURL)
if err != nil {
dialog.ShowError(err, w)
return
}
a.OpenURL(url)
return
}
image := canvas.NewImageFromURI(u)
image.FillMode = canvas.ImageFillOriginal
dialog.ShowCustom("Image", "Close", image, w)
@@ -142,34 +185,75 @@ func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
lines := strings.Split(msgContent, "\n")
for i, line := range lines {
if strings.HasPrefix(line, ">") {
lines[i] = "\n" + line + "\n"
lines[i] = fmt.Sprintf("\n %s \n", line)
}
}
msgContent = strings.Join(lines, "\n")
content.ParseMarkdown(msgContent)
if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart()))
//content.ParseMarkdown(msgContent)
content.SetText(msgContent)
if chatTabs[chatJidStr].Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
author.SetText(fmt.Sprintf("%s > %s", chatTabs[chatJidStr].Messages[i].Author, jid.MustParse(chatTabs[chatJidStr].Messages[i].Raw.Reply.To).Resourcepart()))
} else {
author.SetText(tabData.Messages[i].Author)
author.SetText(chatTabs[chatJidStr].Messages[i].Author)
}
if strings.Split(msgContent," ")[0] == "/me" {
sl := strings.Split(msgContent, " ")
sl[0] = ""
author.SetText(author.Text + strings.Join(sl, " "))
content.SetText(" ")
}
scroller.SetItemHeight(i, vbox.MinSize().Height)
},
)
scroller.OnSelected = func(id widget.ListItemID) {
selectedId = id
}
tabData.Scroller = scroller
myUITab := ChatTabUI{}
chatTabs[mucJidStr] = tabData
scroller.CreateItem()
myUITab.Scroller = scroller
gen, _ := identicon.New("github", 50, 20)
ii, _ := gen.Draw(chatJidStr)
im := ii.Image(250)
imw := canvas.NewImageFromImage(im)
imw.FillMode = canvas.ImageFillOriginal
myUITab.Sidebar = container.NewVBox(imw)
tabItem := container.NewTabItem(chatJid.Localpart(), scroller)
tabs.Append(tabItem)
return myUITab
}
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
chatJidStr := chatJid.String()
if _, ok := chatTabs[chatJidStr]; ok {
// Tab already exists
return
}
myChatTab := ChatTab{
Jid: chatJid,
Nick: nick,
Messages: []Message{},
isMuc: isMuc,
}
myUITab := CreateUITab(chatJid.String())
myUITab.Internal = &myChatTab
chatTabs[chatJidStr] = &myChatTab
UITabs[chatJidStr] = &myUITab
fyne.Do(func() {
AppTabs.Append(container.NewTabItem(chatJid.String(), myUITab.Scroller))
})
}
func dropToSignInPage(reason string) {
a = app.New()
w = a.NewWindow("Welcome to Pi")
w.Resize(fyne.NewSize(500, 500))
rt := widget.NewRichTextFromMarkdown("# Welcome to pi\nIt appears you do not have a valid account configured. Let's create one!")
@@ -202,18 +286,28 @@ func dropToSignInPage(reason string) {
config.Login.User = userEntry.Text
config.Login.Password = passwordEntry.Text
config.Login.DisplayName = nicknameEntry.Text
config.Notifications = true
config.Notifications = false
bytes, err := json.MarshalIndent(config, "", " ")
bytes, err := xml.MarshalIndent(config, "", "\t")
if err != nil {
dialog.ShowError(err, w)
return
}
os.Create("pi.json")
os.WriteFile("pi.json", bytes, os.FileMode(os.O_RDWR)) // TODO: See if this works on non-unix like systems
writer, err := a.Storage().Create("pi.xml")
if err != nil {
dialog.ShowError(err, w)
return
}
defer writer.Close()
_, err = writer.Write(bytes)
if err != nil {
dialog.ShowError(err, w)
return
}
a.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
w.Close()
a.Quit()
//w.Close()
}
}, w)
})
@@ -226,24 +320,31 @@ func dropToSignInPage(reason string) {
}
func main() {
muc.Since(time.Now())
config = piConfig{}
a = app.NewWithID("pi-im")
reader, err := a.Storage().Open("pi.xml")
if err != nil {
dropToSignInPage(err.Error())
return
}
defer reader.Close()
bytes, err := os.ReadFile("./pi.json")
bytes, err := io.ReadAll(reader)
if err != nil {
dropToSignInPage(err.Error())
return
}
err = json.Unmarshal(bytes, &config)
err = xml.Unmarshal(bytes, &config)
if err != nil {
dropToSignInPage(fmt.Sprintf("Your pi.json file is invalid:\n%s", err.Error()))
dropToSignInPage(fmt.Sprintf("Your pi.xml file is invalid:\n%s", err.Error()))
return
}
login = config.Login
DMs = config.DMs
login = config.Login
notifications = config.Notifications
client, err := oasisSdk.CreateClient(
@@ -263,11 +364,12 @@ func main() {
lines := strings.Split(str, "\n")
for i, line := range lines {
s := strings.Split(line, " ")
for j, v := range s {
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") {
img = v
s[j] = fmt.Sprintf("[%s](%s)", v, v)
}
}
}
lines[i] = strings.Join(s, " ")
@@ -291,21 +393,42 @@ func main() {
tab.Messages = append(tab.Messages, myMessage)
fyne.Do(func() {
tab.Scroller.Refresh()
UITabs[userJidStr].Scroller.Refresh()
if scrollDownOnNewMessage {
tab.Scroller.ScrollToBottom()
UITabs[userJidStr].Scroller.ScrollToBottom()
}
})
}
},
func(client *oasisSdk.XmppClient, _ *muc.Channel, msg *oasisSdk.XMPPChatMessage) {
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
for _, v := range msg.Unknown {
if v.XMLName.Local == "delay" { // CLasic history message
//ignore = true
//fmt.Println("ignoring!")
}
}
for _, v := range msg.Unknown {
if v.XMLName.Local == "replace" {
correction = true
}
}
var ImageID string = ""
mucJidStr := msg.From.Bare().String()
if tab, ok := chatTabs[mucJidStr]; ok {
chatTabs[mucJidStr].Muc = muc
str := *msg.CleanedBody
if notifications {
if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, login.DisplayName)) {
if strings.Contains(str, login.DisplayName) {
fmt.Println(str)
important = true
}
if !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))
}
}
@@ -313,11 +436,10 @@ func main() {
lines := strings.Split(str, "\n")
for i, line := range lines {
s := strings.Split(line, " ")
for j, v := range s {
for _, v := range s {
_, err := url.Parse(v)
if err == nil && strings.HasPrefix(v, "https://") {
s[j] = fmt.Sprintf("[%s](%s)", v, v)
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jp") || strings.HasSuffix(v, ".webp") {
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") {
ImageID = v
}
}
@@ -334,6 +456,19 @@ func main() {
} else {
replyID = msg.Reply.To
}
if correction {
for i := len(tab.Messages) - 1; i > 0; i-- {
if tab.Messages[i].Raw.From.String() == msg.From.String() {
tab.Messages[i].Content = *msg.CleanedBody + " (edited)"
fyne.Do(func() {
UITabs[mucJidStr].Scroller.Refresh()
})
return
}
}
}
myMessage := Message{
Author: msg.From.Resourcepart(),
Content: str,
@@ -341,24 +476,42 @@ func main() {
ReplyID: replyID,
Raw: *msg,
ImageURL: ImageID,
Important: important,
}
if !ignore {
tab.Messages = append(tab.Messages, myMessage)
}
fyne.Do(func() {
tab.Scroller.Refresh()
UITabs[mucJidStr].Scroller.Refresh()
if scrollDownOnNewMessage {
tab.Scroller.ScrollToBottom()
UITabs[mucJidStr].Scroller.ScrollToBottom()
}
})
}
},
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
switch state {
case oasisSdk.ChatStateActive:
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 stoped 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("")
})
}
},
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
@@ -368,7 +521,6 @@ func main() {
fmt.Printf("%s has seen %s", from.String(), id)
},
)
if err != nil {
log.Fatalln("Could not create client - " + err.Error())
}
@@ -400,23 +552,23 @@ func main() {
entry.OnChanged = func(s string) {
}
sendbtn := widget.NewButton("Send", func() {
SendCallback := func() {
text := entry.Text
if tabs.Selected() == nil || tabs.Selected().Content == nil {
if AppTabs.Selected() == nil || AppTabs.Selected().Content == nil || text == "" {
return
}
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
var isMuc bool
for jid, tabData := range chatTabs {
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
isMuc = tabData.isMuc
isMuc = chatTabs[activeMucJid].isMuc
break
}
}
@@ -426,14 +578,25 @@ func main() {
}
go func() {
//TODO: Fix message hack until jjj adds message sending
if replying {
m := chatTabs[activeMucJid].Messages[selectedId].Raw
client.ReplyToEvent(&m, text)
err = client.ReplyToEvent(&m, text)
if err != nil {
dialog.ShowError(err, w)
}
return
}
err = client.SendText(jid.MustParse(activeMucJid), text)
url, uerr := url.Parse(strings.Split(text, " ")[0])
if uerr == nil && strings.HasPrefix(strings.Split(text, " ")[0], "https://") {
err = client.SendImage(jid.MustParse(activeMucJid).Bare(), text, url.String(), &text)
if err != nil {
dialog.ShowError(err, w)
}
return
}
err = client.SendText(jid.MustParse(activeMucJid).Bare(), text)
if err != nil {
dialog.ShowError(err, w)
}
@@ -443,15 +606,22 @@ func main() {
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
Author: "You",
Content: text,
ReplyID: "PICLIENT:UNAVAILABLE",
})
fyne.Do(func() {
if scrollDownOnNewMessage {
chatTabs[activeMucJid].Scroller.ScrollToBottom()
UITabs[activeMucJid].Scroller.ScrollToBottom()
}
})
}
entry.SetText("")
})
}
sendbtn := widget.NewButton("Send", SendCallback)
entry.OnSubmitted = func(s string) {
SendCallback()
// i fucking hate fyne
}
mit := fyne.NewMenuItem("about pi", func() {
dialog.ShowInformation("about pi", fmt.Sprintf("the XMPP client from hell\n\npi is an experimental XMPP client\nwritten by Sunglocto in Go.\n\nVersion %s", version), w)
@@ -496,7 +666,7 @@ func main() {
})
jtb := fyne.NewMenuItem("jump to bottom", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
@@ -504,74 +674,32 @@ func main() {
})
jtt := fyne.NewMenuItem("jump to top", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
selectedScroller.ScrollToTop()
})
/*mib := fyne.NewMenuItem("Join a room", func() {
nickEntry := widget.NewEntry()
nickEntry.SetText(login.DisplayName)
roomEntry := widget.NewEntry()
items := []*widget.FormItem{
widget.NewFormItem("Nick", nickEntry),
widget.NewFormItem("MUC address", roomEntry),
}
dialog.ShowForm("join a MUC", "join", "cancel", items, func(b bool) {
if b {
roomJid, err := jid.Parse(roomEntry.Text)
if err != nil {
dialog.ShowError(err, w)
return
}
nick := nickEntry.Text
go func() {
// We probably don't need to handle the error here, if it fails the user will know
_, err := client.MucClient.Join(client.Ctx, roomJid, client.Session, nil)
if err != nil {
panic(err)
}
}()
addChatTab(true, roomJid, nick)
}
}, w)
})*/
mic := fyne.NewMenuItem("upload a file", func() {
w.SetOnDropped(func(p fyne.Position, u []fyne.URI) {
var link string
var bytes []byte
var toperr error
var topreader fyne.URIReadCloser
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, w)
return
}
if reader == nil {
return
}
bytes, toperr = io.ReadAll(reader)
topreader = reader
if toperr != nil {
dialog.ShowError(toperr, w)
return
}
myUri := u[0] // Only upload a single file
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
dialog.ShowCustom("Uploading file", "Hide", myprogressbar, w)
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
diag.Show()
go func() {
client.UploadFileFromBytes(client.Ctx, topreader.URI().Name(), bytes, progress)
client.UploadFile(client.Ctx, myUri.Path(), progress)
}()
for update := range progress {
myprogressbar.Value = float64(update.Percentage)
fyne.Do(func() {
myprogressbar.Value = float64(update.Percentage) / 100
myprogressbar.Refresh()
})
if update.Error != nil {
diag.Dismiss()
dialog.ShowError(update.Error, w)
return
}
@@ -581,22 +709,138 @@ func main() {
}
}
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
})
//deb := fyne.NewMenuItem("DEBUG: Attempt to get MAM history from a user", func() {
//res, err := history.Fetch(client.Ctx, history.Query{}, jid.MustParse("ringen@muc.isekai.rocks"), client.Session)
//})
mic := fyne.NewMenuItem("upload a file", func() {
var link string
var toperr error
//var topreader fyne.URIReadCloser
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
go func() {
if err != nil {
dialog.ShowError(err, w)
return
}
if reader == nil {
return
}
bytes, toperr = io.ReadAll(reader)
//topreader = reader
if toperr != nil {
dialog.ShowError(toperr, w)
return
}
progress := make(chan oasisSdk.UploadProgress)
myprogressbar := widget.NewProgressBar()
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
fyne.Do(func() {
diag.Show()
})
go func() {
client.UploadFile(client.Ctx, reader.URI().Path(), progress)
}()
for update := range progress {
fyne.Do(func() {
myprogressbar.Value = float64(update.Percentage) / 100
myprogressbar.Refresh()
})
if update.Error != nil {
diag.Dismiss()
dialog.ShowError(update.Error, w)
return
}
if update.GetURL != "" {
link = update.GetURL
}
}
diag.Dismiss()
a.Clipboard().SetContent(link)
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
}()
}, w)
})
menu_help := fyne.NewMenu("π", mit, reconnect)
menu_changeroom := fyne.NewMenu("β", mic)
menu_configureview := fyne.NewMenu("γ", mia, mis, jtt, jtb)
servDisc := fyne.NewMenuItem("Disco features", func() {
var search jid.JID
dialog.ShowEntryDialog("Disco features", "JID: ", func(s string) { // TODO: replace with undeprecated widget
search, err = jid.Parse(s)
if err != nil {
dialog.ShowError(err, w)
return
}
myBox := container.NewGridWithColumns(1, widget.NewLabel("Items\na\na\na\na\na"))
info, err := disco.GetInfo(client.Ctx, "", search, client.Session)
if err != nil {
dialog.ShowError(err, w)
return
}
m := info.Features
for _, v := range m {
myBox.Add(widget.NewLabel(v.Var))
myBox.Refresh()
}
dialog.ShowCustom("Features", "cancel", myBox, w)
}, w)
})
savedata := fyne.NewMenuItem("DEBUG: Save tab data to disk", func() {
d := []ChatTab{}
for _, v := range chatTabs {
d = append(d, *v)
}
b, err := xml.Marshal(d)
if err != nil {
dialog.ShowError(err, w)
return
}
os.Create("test.xml")
os.WriteFile("text.xml", b, os.ModeAppend)
})
menu_help := fyne.NewMenu("π", mit, reconnect, savedata)
menu_changeroom := fyne.NewMenu("Α", mic, servDisc)
menu_configureview := fyne.NewMenu("Β", mia, mis, jtt, jtb)
hafjag := fyne.NewMenuItem("Hafjag", func() {
entry.Text = "Hafjag"
SendCallback()
entry.Text = "Hafjag"
})
hotfuck := fyne.NewMenuItem("Hot Fuck", func() {
entry.Text = "Hot Fuck"
SendCallback()
entry.Text = "Oh Yeah."
})
mycurrenttime := fyne.NewMenuItem("Current time", func() {
entry.Text = fmt.Sprintf("It is currently %s", time.Now().Format(time.RFC850))
SendCallback()
})
menu_jokes := fyne.NewMenu("δ", mycurrenttime, hafjag, hotfuck)
bit := fyne.NewMenuItem("mark selected message as read", func() {
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeMucJid string
for jid, tabData := range chatTabs {
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeMucJid = jid
break
@@ -614,13 +858,13 @@ func main() {
bic := fyne.NewMenuItem("show message XML", func() {
pre := widget.NewLabel("")
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range chatTabs {
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
@@ -628,7 +872,7 @@ func main() {
}
m := chatTabs[activeChatJid].Messages[selectedId].Raw
bytes, err := xml.MarshalIndent(m, "", " ")
bytes, err := xml.MarshalIndent(m, "", "\t")
if err != nil {
dialog.ShowError(err, w)
return
@@ -638,19 +882,13 @@ func main() {
pre.Refresh()
dialog.ShowCustom("Message", "Close", pre, w)
})
menu_messageoptions := fyne.NewMenu("Σ", bit, bia, bic)
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions)
menu_messageoptions := fyne.NewMenu("Γ", bit, bia, bic)
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions, menu_jokes)
w.SetMainMenu(ma)
tabs = container.NewAppTabs(
AppTabs = container.NewAppTabs(
container.NewTabItem("τίποτα", widget.NewLabel(`
welcome to pi
you are currently not focused on any rooms.
you can add new rooms by editing your pi.json file.
in order to change application settings, refer to the tab-menu with the Greek letters.
these buttons allow you to configure the application as well as other functions.
for more information about the pi project itself, hit the π button.
pi
`)),
)
@@ -669,6 +907,37 @@ func main() {
}
}
w.SetContent(container.NewVSplit(container.NewVSplit(tabs, container.NewHSplit(entry, sendbtn)), widget.NewLabel("pi")))
AppTabs.OnSelected = func(ti *container.TabItem) {
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
if !ok {
return
}
var activeChatJid string
for jid, tabData := range UITabs {
if tabData.Scroller == selectedScroller {
activeChatJid = jid
break
}
}
tab := chatTabs[activeChatJid]
UITab := UITabs[activeChatJid]
if tab.isMuc {
chatInfo = *container.NewHBox(widget.NewLabel(tab.Muc.Addr().String()))
} else {
chatInfo = *container.NewHBox(widget.NewLabel(tab.Jid.String()))
}
chatSidebar = *UITab.Sidebar
old := chatSidebar.Position()
chatSidebar.Refresh()
chatSidebar.Move(old)
}
// HACK - disable chatsidebar because it's currently very buggy
chatSidebar.Hidden = true
statBar.SetText("")
w.SetContent(container.NewVSplit(container.NewVSplit(AppTabs, container.NewHSplit(entry, sendbtn)), container.NewHSplit(&statBar, &chatInfo)))
w.ShowAndRun()
}