forked from sunglocto/pi-im
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
6c3195b029 | |||
5b5d4656aa | |||
![]() |
3afa1e7e38 | ||
ece04e1c36 | |||
52e38e7e66 | |||
59d83cb185 | |||
4015107de0 | |||
150f42bc58 | |||
3d6f835d4f | |||
922bc1d7cf | |||
a61d3090e1 | |||
215839d833 | |||
d7264e91f7 | |||
![]() |
93d3bb20d2 | ||
181b91edb4 | |||
ca6bda7950 | |||
14cda04e06 | |||
47d93ffe0e | |||
3c84dd7702 | |||
dcc3baaf02 | |||
362930c5d5 | |||
![]() |
c6bd18ef9c | ||
9f57d6688b | |||
13d6041711 | |||
d1521c9704 | |||
20888a4e60 | |||
e4300d9282 | |||
12e4a064d6 | |||
2190896442 | |||
d1e67df750 | |||
e15a6424bb | |||
64d1c420a2 |
12
.github/workflows/go.yml
vendored
12
.github/workflows/go.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
# This workflow will build a golang project
|
# 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
|
# 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:
|
on:
|
||||||
push:
|
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
|
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
|
- 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
2
.gitignore
vendored
@@ -26,7 +26,7 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
pi.json
|
pi.xml
|
||||||
pi
|
pi
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
|
90
README.md
90
README.md
@@ -1,14 +1,17 @@
|
|||||||
<center>
|
<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>
|
</center>
|
||||||
|
|
||||||
# π
|
[](https://github.com/sunglocto/pi/actions/workflows/go.yml)
|
||||||
[](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
|
## 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.
|
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.
|
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
|
```xml
|
||||||
{
|
<piConfig>
|
||||||
"Login": {
|
<Login>
|
||||||
"Host": "example.com:5222",
|
<Host>example.com:5222</Host>
|
||||||
"User": "user@example.com",
|
<User>user@example.com</User>
|
||||||
"Password": "123456",
|
<Password>123456789</Password>
|
||||||
"DisplayName": "user",
|
<DisplayName>sunglocto</DisplayName>
|
||||||
"NoTLS": false,
|
<TLSoff>false</TLSoff>
|
||||||
"StartTLS": true,
|
<StartTLS>true</StartTLS>
|
||||||
"Mucs": [
|
<MucsToJoin>room1@muc.example.com</MucsToJoin>
|
||||||
"room1@group.example.com",
|
<MucsToJoin>room2@muc.example.com</MucsToJoin>
|
||||||
"room2@group.example.com"
|
</Login>
|
||||||
]
|
<Notifications>true</Notifications>
|
||||||
},
|
</piConfig>
|
||||||
"DMs": [
|
|
||||||
"mike@example.com",
|
|
||||||
"louis@example.com"
|
|
||||||
],
|
|
||||||
"Notifications": false
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
cd pi
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go build .
|
go build .
|
||||||
vim pi.json
|
|
||||||
./pi
|
./pi
|
||||||
```
|
```
|
||||||
> Uh, Windows???
|
> Uh, Windows???
|
||||||
|
|
||||||
Eventually. Don't count on it.
|
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)
|
(extra)
|
||||||
|
|
||||||
Pi version numbers are the digits of Pi followed by a letter indicating the phase of development the program is in.
|
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.
|
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).
|
Named after [Psi](https://github.com/psi-im/psi).
|
||||||
|
5
go.mod
5
go.mod
@@ -1,11 +1,11 @@
|
|||||||
module pi
|
module pi-im
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.6.2
|
fyne.io/fyne/v2 v2.6.2
|
||||||
fyne.io/x/fyne v0.0.0-20250418202416-58a230ad1acb
|
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
|
mellium.im/xmpp v0.22.0
|
||||||
pain.agency/oasis-sdk v0.0.0-20250805052243-df6be3f9f629
|
pain.agency/oasis-sdk v0.0.0-20250805052243-df6be3f9f629
|
||||||
)
|
)
|
||||||
@@ -13,7 +13,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
fyne.io/systray v1.11.0 // indirect
|
fyne.io/systray v1.11.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.5.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/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fredbi/uri v1.1.0 // indirect
|
github.com/fredbi/uri v1.1.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
6
go.sum
6
go.sum
@@ -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=
|
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 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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=
|
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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
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=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||||
|
577
main.go
577
main.go
@@ -1,17 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
//core - required
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
_ "io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// gui - required
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
@@ -20,13 +21,24 @@ import (
|
|||||||
"fyne.io/fyne/v2/storage"
|
"fyne.io/fyne/v2/storage"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"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/jid"
|
||||||
"mellium.im/xmpp/muc"
|
"mellium.im/xmpp/muc"
|
||||||
oasisSdk "pain.agency/oasis-sdk"
|
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
|
// by sunglocto
|
||||||
// license AGPL
|
// license AGPL
|
||||||
@@ -38,14 +50,22 @@ type Message struct {
|
|||||||
ReplyID string
|
ReplyID string
|
||||||
ImageURL string
|
ImageURL string
|
||||||
Raw oasisSdk.XMPPChatMessage
|
Raw oasisSdk.XMPPChatMessage
|
||||||
|
Important bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type MucTab struct {
|
type ChatTab struct {
|
||||||
Jid jid.JID
|
Jid jid.JID
|
||||||
Nick string
|
Nick string
|
||||||
Messages []Message
|
Messages []Message
|
||||||
Scroller *widget.List
|
|
||||||
isMuc bool
|
isMuc bool
|
||||||
|
Muc *muc.Channel
|
||||||
|
UpdateSidebar bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatTabUI struct {
|
||||||
|
Internal *ChatTab
|
||||||
|
Scroller *widget.List `xml:"-"`
|
||||||
|
Sidebar *fyne.Container `xml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type piConfig struct {
|
type piConfig struct {
|
||||||
@@ -58,8 +78,10 @@ var config piConfig
|
|||||||
var login oasisSdk.LoginInfo
|
var login oasisSdk.LoginInfo
|
||||||
var DMs []string
|
var DMs []string
|
||||||
|
|
||||||
var chatTabs = make(map[string]*MucTab)
|
var chatTabs = make(map[string]*ChatTab)
|
||||||
var tabs *container.AppTabs
|
var UITabs = make(map[string]*ChatTabUI)
|
||||||
|
|
||||||
|
var AppTabs *container.AppTabs
|
||||||
var selectedId widget.ListItemID
|
var selectedId widget.ListItemID
|
||||||
var replying bool = false
|
var replying bool = false
|
||||||
var notifications bool
|
var notifications bool
|
||||||
@@ -68,7 +90,7 @@ var connection bool = true
|
|||||||
type myTheme struct{}
|
type myTheme struct{}
|
||||||
|
|
||||||
func (m myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
|
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 {
|
func (m myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
|
||||||
@@ -90,48 +112,69 @@ var scrollDownOnNewMessage bool = true
|
|||||||
var w fyne.Window
|
var w fyne.Window
|
||||||
var a fyne.App
|
var a fyne.App
|
||||||
|
|
||||||
func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
|
func CreateUITab(chatJidStr string) ChatTabUI {
|
||||||
mucJidStr := chatJid.String()
|
|
||||||
if _, ok := chatTabs[mucJidStr]; ok {
|
|
||||||
// Tab already exists
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tabData := &MucTab{
|
|
||||||
Jid: chatJid,
|
|
||||||
Nick: nick,
|
|
||||||
Messages: []Message{},
|
|
||||||
isMuc: isMuc,
|
|
||||||
}
|
|
||||||
|
|
||||||
var scroller *widget.List
|
var scroller *widget.List
|
||||||
scroller = widget.NewList(
|
scroller = widget.NewList(
|
||||||
func() int {
|
func() int {
|
||||||
return len(tabData.Messages)
|
return len(chatTabs[chatJidStr].Messages)
|
||||||
},
|
},
|
||||||
func() fyne.CanvasObject {
|
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 := widget.NewLabel("author")
|
||||||
author.TextStyle.Bold = true
|
author.TextStyle.Bold = true
|
||||||
content := widget.NewRichTextWithText("content")
|
content := widget.NewLabel("content")
|
||||||
content.Wrapping = fyne.TextWrapWord
|
content.Wrapping = fyne.TextWrapWord
|
||||||
icon := theme.FileImageIcon()
|
content.Selectable = true
|
||||||
btn := widget.NewButtonWithIcon("View image", icon, func() {
|
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) {
|
func(i widget.ListItemID, co fyne.CanvasObject) {
|
||||||
vbox := co.(*fyne.Container)
|
vbox := co.(*fyne.Container)
|
||||||
author := vbox.Objects[0].(*widget.Label)
|
authorBox := vbox.Objects[0].(*fyne.Container)
|
||||||
content := vbox.Objects[1].(*widget.RichText)
|
// 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)
|
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
|
btn.Hidden = true // Hide by default
|
||||||
msgContent := tabData.Messages[i].Content
|
msgContent := chatTabs[chatJidStr].Messages[i].Content
|
||||||
if tabData.Messages[i].ImageURL != "" {
|
if chatTabs[chatJidStr].Messages[i].ImageURL != "" {
|
||||||
btn.Hidden = false
|
btn.Hidden = false
|
||||||
btn.OnTapped = func() {
|
btn.OnTapped = func() {
|
||||||
fyne.Do(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 := canvas.NewImageFromURI(u)
|
||||||
image.FillMode = canvas.ImageFillOriginal
|
image.FillMode = canvas.ImageFillOriginal
|
||||||
dialog.ShowCustom("Image", "Close", image, w)
|
dialog.ShowCustom("Image", "Close", image, w)
|
||||||
@@ -142,34 +185,75 @@ func addChatTab(isMuc bool, chatJid jid.JID, nick string) {
|
|||||||
lines := strings.Split(msgContent, "\n")
|
lines := strings.Split(msgContent, "\n")
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
if strings.HasPrefix(line, ">") {
|
if strings.HasPrefix(line, ">") {
|
||||||
lines[i] = "\n" + line + "\n"
|
lines[i] = fmt.Sprintf("\n %s \n", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msgContent = strings.Join(lines, "\n")
|
msgContent = strings.Join(lines, "\n")
|
||||||
|
|
||||||
content.ParseMarkdown(msgContent)
|
//content.ParseMarkdown(msgContent)
|
||||||
if tabData.Messages[i].ReplyID != "PICLIENT:UNAVAILABLE" {
|
content.SetText(msgContent)
|
||||||
author.SetText(fmt.Sprintf("%s ↳ %s", tabData.Messages[i].Author, jid.MustParse(tabData.Messages[i].ReplyID).Resourcepart()))
|
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 {
|
} 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.SetItemHeight(i, vbox.MinSize().Height)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
scroller.OnSelected = func(id widget.ListItemID) {
|
scroller.OnSelected = func(id widget.ListItemID) {
|
||||||
selectedId = id
|
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)
|
return myUITab
|
||||||
tabs.Append(tabItem)
|
}
|
||||||
|
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) {
|
func dropToSignInPage(reason string) {
|
||||||
a = app.New()
|
|
||||||
w = a.NewWindow("Welcome to Pi")
|
w = a.NewWindow("Welcome to Pi")
|
||||||
w.Resize(fyne.NewSize(500, 500))
|
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!")
|
rt := widget.NewRichTextFromMarkdown("# Welcome to pi\nIt appears you do not have a valid account configured. Let's create one!")
|
||||||
@@ -202,48 +286,65 @@ func dropToSignInPage(reason string) {
|
|||||||
config.Login.User = userEntry.Text
|
config.Login.User = userEntry.Text
|
||||||
config.Login.Password = passwordEntry.Text
|
config.Login.Password = passwordEntry.Text
|
||||||
config.Login.DisplayName = nicknameEntry.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 {
|
if err != nil {
|
||||||
dialog.ShowError(err, w)
|
dialog.ShowError(err, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Create("pi.json")
|
writer, err := a.Storage().Create("pi.xml")
|
||||||
os.WriteFile("pi.json", bytes, os.FileMode(os.O_RDWR)) // TODO: See if this works on non-unix like systems
|
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"))
|
a.SendNotification(fyne.NewNotification("Done", "Relaunch the application"))
|
||||||
w.Close()
|
a.Quit()
|
||||||
|
//w.Close()
|
||||||
}
|
}
|
||||||
}, w)
|
}, w)
|
||||||
})
|
})
|
||||||
btn2 := widget.NewButton("Close pi", func() {
|
btn2 := widget.NewButton("Close pi", func() {
|
||||||
w.Close()
|
w.Close()
|
||||||
})
|
})
|
||||||
w.SetContent(container.NewVBox(rt, btn, btn2,footer))
|
w.SetContent(container.NewVBox(rt, btn, btn2, footer))
|
||||||
w.ShowAndRun()
|
w.ShowAndRun()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
muc.Since(time.Now())
|
||||||
|
|
||||||
config = piConfig{}
|
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 {
|
if err != nil {
|
||||||
dropToSignInPage(err.Error())
|
dropToSignInPage(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = xml.Unmarshal(bytes, &config)
|
||||||
err = json.Unmarshal(bytes, &config)
|
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
login = config.Login
|
|
||||||
DMs = config.DMs
|
DMs = config.DMs
|
||||||
|
login = config.Login
|
||||||
notifications = config.Notifications
|
notifications = config.Notifications
|
||||||
|
|
||||||
client, err := oasisSdk.CreateClient(
|
client, err := oasisSdk.CreateClient(
|
||||||
@@ -263,11 +364,12 @@ func main() {
|
|||||||
lines := strings.Split(str, "\n")
|
lines := strings.Split(str, "\n")
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
s := strings.Split(line, " ")
|
s := strings.Split(line, " ")
|
||||||
for j, v := range s {
|
for _, v := range s {
|
||||||
_, err := url.Parse(v)
|
_, err := url.Parse(v)
|
||||||
if err == nil && strings.HasPrefix(v, "https://") {
|
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
|
img = v
|
||||||
s[j] = fmt.Sprintf("[%s](%s)", v, v)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines[i] = strings.Join(s, " ")
|
lines[i] = strings.Join(s, " ")
|
||||||
@@ -291,21 +393,42 @@ func main() {
|
|||||||
|
|
||||||
tab.Messages = append(tab.Messages, myMessage)
|
tab.Messages = append(tab.Messages, myMessage)
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
tab.Scroller.Refresh()
|
UITabs[userJidStr].Scroller.Refresh()
|
||||||
if scrollDownOnNewMessage {
|
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 = ""
|
var ImageID string = ""
|
||||||
mucJidStr := msg.From.Bare().String()
|
mucJidStr := msg.From.Bare().String()
|
||||||
if tab, ok := chatTabs[mucJidStr]; ok {
|
if tab, ok := chatTabs[mucJidStr]; ok {
|
||||||
|
chatTabs[mucJidStr].Muc = muc
|
||||||
str := *msg.CleanedBody
|
str := *msg.CleanedBody
|
||||||
if notifications {
|
if strings.Contains(str, login.DisplayName) {
|
||||||
if strings.Contains(str, login.DisplayName) || (msg.Reply != nil && strings.Contains(msg.Reply.To, 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))
|
a.SendNotification(fyne.NewNotification(fmt.Sprintf("Mentioned in %s", mucJidStr), str))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,11 +436,10 @@ func main() {
|
|||||||
lines := strings.Split(str, "\n")
|
lines := strings.Split(str, "\n")
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
s := strings.Split(line, " ")
|
s := strings.Split(line, " ")
|
||||||
for j, v := range s {
|
for _, v := range s {
|
||||||
_, err := url.Parse(v)
|
_, err := url.Parse(v)
|
||||||
if err == nil && strings.HasPrefix(v, "https://") {
|
if err == nil && strings.HasPrefix(v, "https://") {
|
||||||
s[j] = fmt.Sprintf("[%s](%s)", v, v)
|
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") {
|
||||||
if strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jp") || strings.HasSuffix(v, ".webp") {
|
|
||||||
ImageID = v
|
ImageID = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,6 +456,19 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
replyID = msg.Reply.To
|
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{
|
myMessage := Message{
|
||||||
Author: msg.From.Resourcepart(),
|
Author: msg.From.Resourcepart(),
|
||||||
Content: str,
|
Content: str,
|
||||||
@@ -341,24 +476,42 @@ func main() {
|
|||||||
ReplyID: replyID,
|
ReplyID: replyID,
|
||||||
Raw: *msg,
|
Raw: *msg,
|
||||||
ImageURL: ImageID,
|
ImageURL: ImageID,
|
||||||
|
Important: important,
|
||||||
}
|
}
|
||||||
|
if !ignore {
|
||||||
tab.Messages = append(tab.Messages, myMessage)
|
tab.Messages = append(tab.Messages, myMessage)
|
||||||
|
}
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
tab.Scroller.Refresh()
|
UITabs[mucJidStr].Scroller.Refresh()
|
||||||
if scrollDownOnNewMessage {
|
if scrollDownOnNewMessage {
|
||||||
tab.Scroller.ScrollToBottom()
|
UITabs[mucJidStr].Scroller.ScrollToBottom()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
|
func(_ *oasisSdk.XmppClient, from jid.JID, state oasisSdk.ChatState) {
|
||||||
switch state {
|
switch state {
|
||||||
case oasisSdk.ChatStateActive:
|
|
||||||
case oasisSdk.ChatStateComposing:
|
case oasisSdk.ChatStateComposing:
|
||||||
|
fyne.Do(func() {
|
||||||
|
statBar.SetText(fmt.Sprintf("%s is typing...", from.Resourcepart()))
|
||||||
|
})
|
||||||
case oasisSdk.ChatStatePaused:
|
case oasisSdk.ChatStatePaused:
|
||||||
|
|
||||||
|
fyne.Do(func() {
|
||||||
|
statBar.SetText(fmt.Sprintf("%s has stoped typing.", from.Resourcepart()))
|
||||||
|
})
|
||||||
case oasisSdk.ChatStateInactive:
|
case oasisSdk.ChatStateInactive:
|
||||||
|
fyne.Do(func() {
|
||||||
|
statBar.SetText(fmt.Sprintf("%s is idle", from.Resourcepart()))
|
||||||
|
})
|
||||||
case oasisSdk.ChatStateGone:
|
case oasisSdk.ChatStateGone:
|
||||||
|
fyne.Do(func() {
|
||||||
|
statBar.SetText(fmt.Sprintf("%s is gone", from.Resourcepart()))
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
|
fyne.Do(func() {
|
||||||
|
statBar.SetText("")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
|
func(_ *oasisSdk.XmppClient, from jid.JID, id string) {
|
||||||
@@ -368,7 +521,6 @@ func main() {
|
|||||||
fmt.Printf("%s has seen %s", from.String(), id)
|
fmt.Printf("%s has seen %s", from.String(), id)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Could not create client - " + err.Error())
|
log.Fatalln("Could not create client - " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -400,23 +552,23 @@ func main() {
|
|||||||
entry.OnChanged = func(s string) {
|
entry.OnChanged = func(s string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendbtn := widget.NewButton("Send", func() {
|
SendCallback := func() {
|
||||||
text := entry.Text
|
text := entry.Text
|
||||||
if tabs.Selected() == nil || tabs.Selected().Content == nil {
|
if AppTabs.Selected() == nil || AppTabs.Selected().Content == nil || text == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
|
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeMucJid string
|
var activeMucJid string
|
||||||
var isMuc bool
|
var isMuc bool
|
||||||
for jid, tabData := range chatTabs {
|
for jid, tabData := range UITabs {
|
||||||
if tabData.Scroller == selectedScroller {
|
if tabData.Scroller == selectedScroller {
|
||||||
activeMucJid = jid
|
activeMucJid = jid
|
||||||
isMuc = tabData.isMuc
|
isMuc = chatTabs[activeMucJid].isMuc
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,14 +578,25 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
//TODO: Fix message hack until jjj adds message sending
|
|
||||||
if replying {
|
if replying {
|
||||||
m := chatTabs[activeMucJid].Messages[selectedId].Raw
|
m := chatTabs[activeMucJid].Messages[selectedId].Raw
|
||||||
client.ReplyToEvent(&m, text)
|
err = client.ReplyToEvent(&m, text)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, w)
|
||||||
|
}
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
dialog.ShowError(err, w)
|
dialog.ShowError(err, w)
|
||||||
}
|
}
|
||||||
@@ -443,25 +606,32 @@ func main() {
|
|||||||
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
|
chatTabs[activeMucJid].Messages = append(chatTabs[activeMucJid].Messages, Message{
|
||||||
Author: "You",
|
Author: "You",
|
||||||
Content: text,
|
Content: text,
|
||||||
|
ReplyID: "PICLIENT:UNAVAILABLE",
|
||||||
})
|
})
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
if scrollDownOnNewMessage {
|
if scrollDownOnNewMessage {
|
||||||
chatTabs[activeMucJid].Scroller.ScrollToBottom()
|
UITabs[activeMucJid].Scroller.ScrollToBottom()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
entry.SetText("")
|
entry.SetText("")
|
||||||
})
|
}
|
||||||
|
|
||||||
|
sendbtn := widget.NewButton("Send", SendCallback)
|
||||||
|
entry.OnSubmitted = func(s string) {
|
||||||
|
SendCallback()
|
||||||
|
// i fucking hate fyne
|
||||||
|
}
|
||||||
|
|
||||||
mit := fyne.NewMenuItem("about pi", func() {
|
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)
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
reconnect := fyne.NewMenuItem("reconnect", func() {
|
reconnect := fyne.NewMenuItem("reconnect", func() {
|
||||||
go func(){
|
go func() {
|
||||||
err := client.Connect()
|
err := client.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fyne.Do(func(){
|
fyne.Do(func() {
|
||||||
dialog.ShowError(err, w)
|
dialog.ShowError(err, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -496,7 +666,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
jtb := fyne.NewMenuItem("jump to bottom", func() {
|
jtb := fyne.NewMenuItem("jump to bottom", func() {
|
||||||
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
|
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -504,74 +674,32 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
jtt := fyne.NewMenuItem("jump to top", func() {
|
jtt := fyne.NewMenuItem("jump to top", func() {
|
||||||
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
|
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedScroller.ScrollToTop()
|
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) {
|
w.SetOnDropped(func(p fyne.Position, u []fyne.URI) {
|
||||||
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() {
|
|
||||||
var link string
|
var link string
|
||||||
var bytes []byte
|
myUri := u[0] // Only upload a single file
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
progress := make(chan oasisSdk.UploadProgress)
|
progress := make(chan oasisSdk.UploadProgress)
|
||||||
myprogressbar := widget.NewProgressBar()
|
myprogressbar := widget.NewProgressBar()
|
||||||
dialog.ShowCustom("Uploading file", "Hide", myprogressbar, w)
|
diag := dialog.NewCustom("Uploading file", "Hide", myprogressbar, w)
|
||||||
|
diag.Show()
|
||||||
go func() {
|
go func() {
|
||||||
|
client.UploadFile(client.Ctx, myUri.Path(), progress)
|
||||||
client.UploadFileFromBytes(client.Ctx, topreader.URI().Name(), bytes, progress)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for update := range progress {
|
for update := range progress {
|
||||||
myprogressbar.Value = float64(update.Percentage)
|
fyne.Do(func() {
|
||||||
|
myprogressbar.Value = float64(update.Percentage) / 100
|
||||||
myprogressbar.Refresh()
|
myprogressbar.Refresh()
|
||||||
|
})
|
||||||
|
|
||||||
if update.Error != nil {
|
if update.Error != nil {
|
||||||
|
diag.Dismiss()
|
||||||
dialog.ShowError(update.Error, w)
|
dialog.ShowError(update.Error, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -581,22 +709,138 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diag.Dismiss()
|
||||||
a.Clipboard().SetContent(link)
|
a.Clipboard().SetContent(link)
|
||||||
dialog.ShowInformation("file successfully uploaded\nURL copied to your clipboard", link, w)
|
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)
|
}, w)
|
||||||
})
|
})
|
||||||
|
|
||||||
menu_help := fyne.NewMenu("π", mit, reconnect)
|
servDisc := fyne.NewMenuItem("Disco features", func() {
|
||||||
menu_changeroom := fyne.NewMenu("β", mic)
|
var search jid.JID
|
||||||
menu_configureview := fyne.NewMenu("γ", mia, mis, jtt, jtb)
|
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() {
|
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 {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var activeMucJid string
|
var activeMucJid string
|
||||||
for jid, tabData := range chatTabs {
|
for jid, tabData := range UITabs {
|
||||||
if tabData.Scroller == selectedScroller {
|
if tabData.Scroller == selectedScroller {
|
||||||
activeMucJid = jid
|
activeMucJid = jid
|
||||||
break
|
break
|
||||||
@@ -614,13 +858,13 @@ func main() {
|
|||||||
bic := fyne.NewMenuItem("show message XML", func() {
|
bic := fyne.NewMenuItem("show message XML", func() {
|
||||||
pre := widget.NewLabel("")
|
pre := widget.NewLabel("")
|
||||||
|
|
||||||
selectedScroller, ok := tabs.Selected().Content.(*widget.List)
|
selectedScroller, ok := AppTabs.Selected().Content.(*widget.List)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeChatJid string
|
var activeChatJid string
|
||||||
for jid, tabData := range chatTabs {
|
for jid, tabData := range UITabs {
|
||||||
if tabData.Scroller == selectedScroller {
|
if tabData.Scroller == selectedScroller {
|
||||||
activeChatJid = jid
|
activeChatJid = jid
|
||||||
break
|
break
|
||||||
@@ -628,7 +872,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := chatTabs[activeChatJid].Messages[selectedId].Raw
|
m := chatTabs[activeChatJid].Messages[selectedId].Raw
|
||||||
bytes, err := xml.MarshalIndent(m, "", " ")
|
bytes, err := xml.MarshalIndent(m, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dialog.ShowError(err, w)
|
dialog.ShowError(err, w)
|
||||||
return
|
return
|
||||||
@@ -638,19 +882,13 @@ func main() {
|
|||||||
pre.Refresh()
|
pre.Refresh()
|
||||||
dialog.ShowCustom("Message", "Close", pre, w)
|
dialog.ShowCustom("Message", "Close", pre, w)
|
||||||
})
|
})
|
||||||
menu_messageoptions := fyne.NewMenu("Σ", bit, bia, bic)
|
menu_messageoptions := fyne.NewMenu("Γ", bit, bia, bic)
|
||||||
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions)
|
ma := fyne.NewMainMenu(menu_help, menu_changeroom, menu_configureview, menu_messageoptions, menu_jokes)
|
||||||
w.SetMainMenu(ma)
|
w.SetMainMenu(ma)
|
||||||
|
|
||||||
tabs = container.NewAppTabs(
|
AppTabs = container.NewAppTabs(
|
||||||
container.NewTabItem("τίποτα", widget.NewLabel(`
|
container.NewTabItem("τίποτα", widget.NewLabel(`
|
||||||
welcome to pi
|
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.
|
|
||||||
`)),
|
`)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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()
|
w.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user