mirror of
https://github.com/youwen5/iamb.git
synced 2025-08-04 19:48:28 -07:00
Compare commits
56 commits
Author | SHA1 | Date | |
---|---|---|---|
1475d9ce50 | |||
5029c6ee3b | |||
![]() |
e9cdb3371a | ||
![]() |
0ff8828a1c | ||
![]() |
331a6bca89 | ||
![]() |
963ce3c7c2 | ||
![]() |
ec88f4441e | ||
![]() |
34d3b844af | ||
![]() |
52010d44d7 | ||
![]() |
0ef5c39f7f | ||
![]() |
fed19d7a4b | ||
![]() |
ed9ee26854 | ||
![]() |
2e6c711644 | ||
![]() |
d1b03880f3 | ||
![]() |
d961fe3f7b | ||
![]() |
9e40b49e5e | ||
![]() |
33d3407694 | ||
![]() |
f880358a83 | ||
![]() |
f0de97a049 | ||
![]() |
a9cb5608f0 | ||
![]() |
c420c9dd65 | ||
![]() |
ba7d0392d8 | ||
![]() |
9ed9400b67 | ||
![]() |
84eaadc09a | ||
![]() |
998e50f4a5 | ||
![]() |
f39261ff84 | ||
![]() |
98aa2f871d | ||
![]() |
952374aab0 | ||
![]() |
e99674b245 | ||
![]() |
82ed796a91 | ||
![]() |
3296f58859 | ||
![]() |
26802bab55 | ||
![]() |
fd3fef5c9e | ||
![]() |
af96bfbb41 | ||
![]() |
5f927ce9c3 | ||
![]() |
6e923f3878 | ||
![]() |
ebd89423e9 | ||
![]() |
9fce71f896 | ||
![]() |
93502f9993 | ||
![]() |
6529e61963 | ||
![]() |
a9c1e69a89 | ||
![]() |
3e45ca3d2c | ||
![]() |
7dd09e32a8 | ||
![]() |
1dcd658928 | ||
![]() |
382a72a468 | ||
![]() |
591fc0af83 | ||
![]() |
2b6363f529 | ||
![]() |
6470e845e0 | ||
![]() |
b023e38f77 | ||
![]() |
e66a8c6716 | ||
![]() |
9a9bdb4862 | ||
![]() |
e40a8a8d2e | ||
![]() |
f4492c9f77 | ||
![]() |
a32915b7e9 | ||
![]() |
3355eb2d26 | ||
![]() |
7b6c5df268 |
28 changed files with 5115 additions and 1788 deletions
12
.github/workflows/binaries.yml
vendored
12
.github/workflows/binaries.yml
vendored
|
@ -60,9 +60,9 @@ jobs:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Run sccache-cache
|
- name: Run sccache-cache
|
||||||
uses: mozilla-actions/sccache-action@v0.0.3
|
uses: mozilla-actions/sccache-action@v0.0.9
|
||||||
- name: 'Build: binary'
|
- name: 'Build: binary'
|
||||||
run: cargo build --release --locked --target ${{ env.TARGET }}
|
run: cargo +stable build --release --locked --target ${{ env.TARGET }}
|
||||||
- name: 'Upload: binary'
|
- name: 'Upload: binary'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
@ -73,8 +73,8 @@ jobs:
|
||||||
- name: 'Package: deb'
|
- name: 'Package: deb'
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
cargo install --locked cargo-deb
|
cargo +stable install --locked cargo-deb
|
||||||
cargo deb --no-strip --target ${{ env.TARGET }}
|
cargo +stable deb --no-strip --target ${{ env.TARGET }}
|
||||||
- name: 'Upload: deb'
|
- name: 'Upload: deb'
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
@ -84,8 +84,8 @@ jobs:
|
||||||
- name: 'Package: rpm'
|
- name: 'Package: rpm'
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
cargo install --locked cargo-generate-rpm
|
cargo +stable install --locked cargo-generate-rpm
|
||||||
cargo generate-rpm --target ${{ env.TARGET }}
|
cargo +stable generate-rpm --target ${{ env.TARGET }}
|
||||||
- name: 'Upload: rpm'
|
- name: 'Upload: rpm'
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -22,8 +22,8 @@ jobs:
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Install Rust (1.70 w/ clippy)
|
- name: Install Rust (1.83 w/ clippy)
|
||||||
uses: dtolnay/rust-toolchain@1.70
|
uses: dtolnay/rust-toolchain@1.83
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
- name: Install Rust (nightly w/ rustfmt)
|
- name: Install Rust (nightly w/ rustfmt)
|
||||||
|
@ -34,7 +34,7 @@ jobs:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Run sccache-cache
|
- name: Run sccache-cache
|
||||||
uses: mozilla-actions/sccache-action@v0.0.3
|
uses: mozilla-actions/sccache-action@v0.0.9
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: cargo +nightly fmt --all -- --check
|
run: cargo +nightly fmt --all -- --check
|
||||||
- name: Check Clippy
|
- name: Check Clippy
|
||||||
|
|
3600
Cargo.lock
generated
3600
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
24
Cargo.toml
24
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "iamb"
|
name = "iamb"
|
||||||
version = "0.0.10"
|
version = "0.0.11-alpha.1"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Ulyssa <git@ulyssa.dev>"]
|
authors = ["Ulyssa <git@ulyssa.dev>"]
|
||||||
repository = "https://github.com/ulyssa/iamb"
|
repository = "https://github.com/ulyssa/iamb"
|
||||||
|
@ -11,7 +11,7 @@ license = "Apache-2.0"
|
||||||
exclude = [".github", "CONTRIBUTING.md"]
|
exclude = [".github", "CONTRIBUTING.md"]
|
||||||
keywords = ["matrix", "chat", "tui", "vim"]
|
keywords = ["matrix", "chat", "tui", "vim"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
rust-version = "1.70"
|
rust-version = "1.83"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -34,10 +34,11 @@ clap = {version = "~4.3", features = ["derive"]}
|
||||||
css-color-parser = "0.1.2"
|
css-color-parser = "0.1.2"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
emojis = "0.5"
|
emojis = "0.5"
|
||||||
|
feruca = "0.10.1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
gethostname = "0.4.1"
|
gethostname = "0.4.1"
|
||||||
html5ever = "0.26.0"
|
html5ever = "0.26.0"
|
||||||
image = "0.24.5"
|
image = "^0.25.6"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
markup5ever_rcdom = "0.2.0"
|
markup5ever_rcdom = "0.2.0"
|
||||||
mime = "^0.3.16"
|
mime = "^0.3.16"
|
||||||
|
@ -45,8 +46,8 @@ mime_guess = "^2.0.4"
|
||||||
nom = "7.0.0"
|
nom = "7.0.0"
|
||||||
open = "3.2.0"
|
open = "3.2.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
ratatui = "0.26"
|
ratatui = "0.29.0"
|
||||||
ratatui-image = { version = "1.0.0", features = ["serde"] }
|
ratatui-image = { version = "~8.0.1", features = ["serde"] }
|
||||||
regex = "^1.5"
|
regex = "^1.5"
|
||||||
rpassword = "^7.2"
|
rpassword = "^7.2"
|
||||||
serde = "^1.0"
|
serde = "^1.0"
|
||||||
|
@ -63,6 +64,7 @@ unicode-width = "0.1.10"
|
||||||
url = {version = "^2.2.2", features = ["serde"]}
|
url = {version = "^2.2.2", features = ["serde"]}
|
||||||
edit = "0.1.4"
|
edit = "0.1.4"
|
||||||
humansize = "2.0.0"
|
humansize = "2.0.0"
|
||||||
|
linkify = "0.10.0"
|
||||||
|
|
||||||
[dependencies.comrak]
|
[dependencies.comrak]
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
|
@ -70,24 +72,24 @@ default-features = false
|
||||||
features = ["shortcodes"]
|
features = ["shortcodes"]
|
||||||
|
|
||||||
[dependencies.notify-rust]
|
[dependencies.notify-rust]
|
||||||
version = "4.10.0"
|
version = "~4.10.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["zbus", "serde"]
|
features = ["zbus", "serde"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.modalkit]
|
[dependencies.modalkit]
|
||||||
version = "0.0.20"
|
version = "0.0.23"
|
||||||
default-features = false
|
default-features = false
|
||||||
#git = "https://github.com/ulyssa/modalkit"
|
#git = "https://github.com/ulyssa/modalkit"
|
||||||
#rev = "24f3ec11c7f634005a27b26878d0fbbdcc08f272"
|
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
|
||||||
|
|
||||||
[dependencies.modalkit-ratatui]
|
[dependencies.modalkit-ratatui]
|
||||||
version = "0.0.20"
|
version = "0.0.23"
|
||||||
#git = "https://github.com/ulyssa/modalkit"
|
#git = "https://github.com/ulyssa/modalkit"
|
||||||
#rev = "24f3ec11c7f634005a27b26878d0fbbdcc08f272"
|
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
|
||||||
|
|
||||||
[dependencies.matrix-sdk]
|
[dependencies.matrix-sdk]
|
||||||
version = "0.7.1"
|
version = "0.10.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["e2e-encryption", "sqlite", "sso-login"]
|
features = ["e2e-encryption", "sqlite", "sso-login"]
|
||||||
|
|
||||||
|
|
22
README.md
22
README.md
|
@ -53,7 +53,7 @@ user_id = "@user:example.com"
|
||||||
|
|
||||||
## Installation (via `crates.io`)
|
## Installation (via `crates.io`)
|
||||||
|
|
||||||
Install Rust (1.70.0 or above) and Cargo, and then run:
|
Install Rust (1.83.0 or above) and Cargo, and then run:
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install --locked iamb
|
cargo install --locked iamb
|
||||||
|
@ -80,9 +80,27 @@ On FreeBSD a package is available from the official repositories. To install it
|
||||||
pkg install iamb
|
pkg install iamb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Gentoo
|
||||||
|
|
||||||
|
On Gentoo, an ebuild is available from the community-managed
|
||||||
|
[GURU overlay](https://wiki.gentoo.org/wiki/Project:GURU).
|
||||||
|
|
||||||
|
You can enable the GURU overlay with:
|
||||||
|
|
||||||
|
```
|
||||||
|
eselect repository enable guru
|
||||||
|
emerge --sync guru
|
||||||
|
```
|
||||||
|
|
||||||
|
And then install `iamb` with:
|
||||||
|
|
||||||
|
```
|
||||||
|
emerge --ask iamb
|
||||||
|
```
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
On macOS a [package](https://formulae.brew.sh/formula/iamb#default) is availabe in Homebrew's
|
On macOS a [package](https://formulae.brew.sh/formula/iamb#default) is available in Homebrew's
|
||||||
repository. To install it simply run:
|
repository. To install it simply run:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
67
docs/iamb.1
67
docs/iamb.1
|
@ -54,7 +54,7 @@ version and quit.
|
||||||
View a list of joined rooms and direct messages.
|
View a list of joined rooms and direct messages.
|
||||||
.It Sy ":dms"
|
.It Sy ":dms"
|
||||||
View a list of direct messages.
|
View a list of direct messages.
|
||||||
.It Sy ":logout"
|
.It Sy ":logout [user id]"
|
||||||
Log out of
|
Log out of
|
||||||
.Nm .
|
.Nm .
|
||||||
.It Sy ":rooms"
|
.It Sy ":rooms"
|
||||||
|
@ -63,6 +63,8 @@ View a list of joined rooms.
|
||||||
View a list of joined spaces.
|
View a list of joined spaces.
|
||||||
.It Sy ":unreads"
|
.It Sy ":unreads"
|
||||||
View a list of unread rooms.
|
View a list of unread rooms.
|
||||||
|
.It Sy ":unreads clear"
|
||||||
|
Mark all rooms as read.
|
||||||
.It Sy ":welcome"
|
.It Sy ":welcome"
|
||||||
View the startup Welcome window.
|
View the startup Welcome window.
|
||||||
.El
|
.El
|
||||||
|
@ -77,39 +79,54 @@ Import and decrypt keys from
|
||||||
.Pa path .
|
.Pa path .
|
||||||
.It Sy ":verify"
|
.It Sy ":verify"
|
||||||
View a list of ongoing E2EE verifications.
|
View a list of ongoing E2EE verifications.
|
||||||
|
.It Sy ":verify accept [key]"
|
||||||
|
Accept a verification request.
|
||||||
|
.It Sy ":verify cancel [key]"
|
||||||
|
Cancel an in-progress verification.
|
||||||
|
.It Sy ":verify confirm [key]"
|
||||||
|
Confirm an in-progress verification.
|
||||||
|
.It Sy ":verify mismatch [key]"
|
||||||
|
Reject an in-progress verification due to mismatched Emoji.
|
||||||
|
.It Sy ":verify request [user id]"
|
||||||
|
Request a new verification with the specified user.
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Sh "MESSAGE COMMANDS"
|
.Sh "MESSAGE COMMANDS"
|
||||||
.Bl -tag -width Ds
|
.Bl -tag -width Ds
|
||||||
.It Sy ":download"
|
.It Sy ":download [path]"
|
||||||
Download an attachment from the selected message.
|
Download an attachment from the selected message and save it to the optional path.
|
||||||
|
.It Sy ":open [path]"
|
||||||
|
Download and then open an attachment, or open a link in a message.
|
||||||
.It Sy ":edit"
|
.It Sy ":edit"
|
||||||
Edit the selected message.
|
Edit the selected message.
|
||||||
.It Sy ":editor"
|
.It Sy ":editor"
|
||||||
Open an external
|
Open an external
|
||||||
.Ev $EDITOR
|
.Ev $EDITOR
|
||||||
to compose a message.
|
to compose a message.
|
||||||
.It Sy ":open"
|
|
||||||
Download and then open an attachment, or open a link in a message.
|
|
||||||
.It Sy ":react [shortcode]"
|
.It Sy ":react [shortcode]"
|
||||||
React to the selected message with an Emoji.
|
React to the selected message with an Emoji.
|
||||||
.It Sy ":redact [reason]"
|
|
||||||
Redact the selected message.
|
|
||||||
.It Sy ":reply"
|
|
||||||
Reply to the selected message.
|
|
||||||
.It Sy ":unreads clear"
|
|
||||||
Mark all unread rooms as read.
|
|
||||||
.It Sy ":unreact [shortcode]"
|
.It Sy ":unreact [shortcode]"
|
||||||
Remove your reaction from the selected message.
|
Remove your reaction from the selected message.
|
||||||
When no arguments are given, remove all of your reactions from the message.
|
When no arguments are given, remove all of your reactions from the message.
|
||||||
.It Sy ":upload"
|
.It Sy ":redact [reason]"
|
||||||
|
Redact the selected message with the optional reason.
|
||||||
|
.It Sy ":reply"
|
||||||
|
Reply to the selected message.
|
||||||
|
.It Sy ":cancel"
|
||||||
|
Cancel the currently drafted message including replies.
|
||||||
|
.It Sy ":upload [path]"
|
||||||
Upload an attachment and send it to the currently selected room.
|
Upload an attachment and send it to the currently selected room.
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Sh "ROOM COMMANDS"
|
.Sh "ROOM COMMANDS"
|
||||||
.Bl -tag -width Ds
|
.Bl -tag -width Ds
|
||||||
.It Sy ":create"
|
.It Sy ":create [arguments]"
|
||||||
Create a new room.
|
Create a new room. Arguments can be
|
||||||
|
.Dq ++alias=[alias] ,
|
||||||
|
.Dq ++public ,
|
||||||
|
.Dq ++space ,
|
||||||
|
and
|
||||||
|
.Dq ++encrypted .
|
||||||
.It Sy ":invite accept"
|
.It Sy ":invite accept"
|
||||||
Accept an invitation to the currently focused room.
|
Accept an invitation to the currently focused room.
|
||||||
.It Sy ":invite reject"
|
.It Sy ":invite reject"
|
||||||
|
@ -117,7 +134,7 @@ Reject an invitation to the currently focused room.
|
||||||
.It Sy ":invite send [user]"
|
.It Sy ":invite send [user]"
|
||||||
Send an invitation to a user to join the currently focused room.
|
Send an invitation to a user to join the currently focused room.
|
||||||
.It Sy ":join [room]"
|
.It Sy ":join [room]"
|
||||||
Join a room.
|
Join a room or open it if you are already joined.
|
||||||
.It Sy ":leave"
|
.It Sy ":leave"
|
||||||
Leave the currently focused room.
|
Leave the currently focused room.
|
||||||
.It Sy ":members"
|
.It Sy ":members"
|
||||||
|
@ -126,6 +143,10 @@ View a list of members of the currently focused room.
|
||||||
Set the name of the currently focused room.
|
Set the name of the currently focused room.
|
||||||
.It Sy ":room name unset"
|
.It Sy ":room name unset"
|
||||||
Unset the name of the currently focused room.
|
Unset the name of the currently focused room.
|
||||||
|
.It Sy ":room dm set"
|
||||||
|
Mark the currently focused room as a direct message.
|
||||||
|
.It Sy ":room dm unset"
|
||||||
|
Mark the currently focused room as a normal room.
|
||||||
.It Sy ":room notify set [level]"
|
.It Sy ":room notify set [level]"
|
||||||
Set a notification level for the currently focused room.
|
Set a notification level for the currently focused room.
|
||||||
Valid levels are
|
Valid levels are
|
||||||
|
@ -153,12 +174,16 @@ Remove a tag from the currently focused room.
|
||||||
Set the topic of the currently focused room.
|
Set the topic of the currently focused room.
|
||||||
.It Sy ":room topic unset"
|
.It Sy ":room topic unset"
|
||||||
Unset the topic of the currently focused room.
|
Unset the topic of the currently focused room.
|
||||||
|
.It Sy ":room topic show"
|
||||||
|
Show the topic of the currently focused room.
|
||||||
.It Sy ":room alias set [alias]"
|
.It Sy ":room alias set [alias]"
|
||||||
Create and point the given alias to the room.
|
Create and point the given alias to the room.
|
||||||
.It Sy ":room alias unset [alias]"
|
.It Sy ":room alias unset [alias]"
|
||||||
Delete the provided alias from the room's alternative alias list.
|
Delete the provided alias from the room's alternative alias list.
|
||||||
.It Sy ":room alias show"
|
.It Sy ":room alias show"
|
||||||
Show alternative aliases to the room, if any are set.
|
Show alternative aliases to the room, if any are set.
|
||||||
|
.It Sy ":room id show"
|
||||||
|
Show the Matrix identifier for the room.
|
||||||
.It Sy ":room canon set [alias]"
|
.It Sy ":room canon set [alias]"
|
||||||
Set the room's canonical alias to the one provided, and make the previous one an alternative alias.
|
Set the room's canonical alias to the one provided, and make the previous one an alternative alias.
|
||||||
.It Sy ":room canon unset [alias]"
|
.It Sy ":room canon unset [alias]"
|
||||||
|
@ -173,6 +198,18 @@ Unban a user from this room with an optional reason.
|
||||||
Kick a user from this room with an optional reason.
|
Kick a user from this room with an optional reason.
|
||||||
.El
|
.El
|
||||||
|
|
||||||
|
.Sh "SPACE COMMANDS"
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Sy ":space child set [room_id] [arguments]"
|
||||||
|
Add a room to the currently focused space.
|
||||||
|
.Dq ++suggested
|
||||||
|
marks the room as a suggested child.
|
||||||
|
.Dq ++order=[string]
|
||||||
|
specifies a string by which children are lexicographically ordered.
|
||||||
|
.It Sy ":space child remove"
|
||||||
|
Remove the selected room from the currently focused space.
|
||||||
|
.El
|
||||||
|
|
||||||
.Sh "WINDOW COMMANDS"
|
.Sh "WINDOW COMMANDS"
|
||||||
.Bl -tag -width Ds
|
.Bl -tag -width Ds
|
||||||
.It Sy ":horizontal [cmd]"
|
.It Sy ":horizontal [cmd]"
|
||||||
|
|
34
docs/iamb.5
34
docs/iamb.5
|
@ -173,6 +173,9 @@ respective shortcodes.
|
||||||
.It Sy message_user_color
|
.It Sy message_user_color
|
||||||
Defines whether or not the message body is colored like the username.
|
Defines whether or not the message body is colored like the username.
|
||||||
|
|
||||||
|
.It Sy normal_after_send
|
||||||
|
Defines whether to reset input to Normal mode after sending a message.
|
||||||
|
|
||||||
.It Sy notifications
|
.It Sy notifications
|
||||||
When this subsection is present, you can enable and configure push notifications.
|
When this subsection is present, you can enable and configure push notifications.
|
||||||
See
|
See
|
||||||
|
@ -208,6 +211,9 @@ See
|
||||||
.Sx "SORTING LISTS"
|
.Sx "SORTING LISTS"
|
||||||
for more details.
|
for more details.
|
||||||
|
|
||||||
|
.It Sy state_event_display
|
||||||
|
Defines whether the state events like joined or left are shown.
|
||||||
|
|
||||||
.It Sy typing_notice_send
|
.It Sy typing_notice_send
|
||||||
Defines whether or not the typing state is sent.
|
Defines whether or not the typing state is sent.
|
||||||
|
|
||||||
|
@ -231,6 +237,10 @@ Possible values are
|
||||||
Specify the width of the column where usernames are displayed in a room.
|
Specify the width of the column where usernames are displayed in a room.
|
||||||
Usernames that are too long are truncated.
|
Usernames that are too long are truncated.
|
||||||
Defaults to 30.
|
Defaults to 30.
|
||||||
|
|
||||||
|
.It Sy tabstop
|
||||||
|
Number of spaces that a <Tab> counts for.
|
||||||
|
Defaults to 4.
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Ss Example 1: Avoid showing Emojis (useful for terminals w/o support)
|
.Ss Example 1: Avoid showing Emojis (useful for terminals w/o support)
|
||||||
|
@ -269,6 +279,8 @@ to use the desktop mechanism (default).
|
||||||
Setting this field to
|
Setting this field to
|
||||||
.Dq Sy bell
|
.Dq Sy bell
|
||||||
will use the terminal bell instead.
|
will use the terminal bell instead.
|
||||||
|
Both can be used via
|
||||||
|
.Dq Sy desktop|bell .
|
||||||
|
|
||||||
.It Sy show_message
|
.It Sy show_message
|
||||||
controls whether to show the message in the desktop notification, and defaults to
|
controls whether to show the message in the desktop notification, and defaults to
|
||||||
|
@ -332,9 +344,29 @@ window.
|
||||||
Defaults to
|
Defaults to
|
||||||
.Sy ["power",\ "id"] .
|
.Sy ["power",\ "id"] .
|
||||||
.El
|
.El
|
||||||
|
|
||||||
|
The available values are:
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Sy favorite
|
||||||
|
Put favorite rooms before other rooms.
|
||||||
|
.It Sy lowpriority
|
||||||
|
Put lowpriority rooms after other rooms.
|
||||||
|
.It Sy name
|
||||||
|
Sort rooms by alphabetically ascending room name.
|
||||||
|
.It Sy alias
|
||||||
|
Sort rooms by alphabetically ascending canonical room alias.
|
||||||
|
.It Sy id
|
||||||
|
Sort rooms by alphabetically ascending Matrix room identifier.
|
||||||
|
.It Sy unread
|
||||||
|
Put unread rooms before other rooms.
|
||||||
|
.It Sy recent
|
||||||
|
Sort rooms by most recent message timestamp.
|
||||||
|
.It Sy invite
|
||||||
|
Put invites before other rooms.
|
||||||
|
.El
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Ss Example 1: Group room members by ther server first
|
.Ss Example 1: Group room members by their server first
|
||||||
.Bd -literal -offset indent
|
.Bd -literal -offset indent
|
||||||
[settings.sort]
|
[settings.sort]
|
||||||
members = ["server", "localpart"]
|
members = ["server", "localpart"]
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<component type="console-application">
|
<component type="console-application">
|
||||||
<id>iamb</id>
|
<id>chat.iamb.iamb</id>
|
||||||
|
|
||||||
<name>iamb</name>
|
<name>iamb</name>
|
||||||
<summary>A terminal Matrix client for Vim addicts</summary>
|
<summary>A terminal Matrix client for Vim addicts</summary>
|
||||||
<url type="homepage">https://iamb.chat</url>
|
<url type="homepage">https://iamb.chat</url>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.0.10" date="2024-08-20"/>
|
||||||
<release version="0.0.9" date="2024-03-28"/>
|
<release version="0.0.9" date="2024-03-28"/>
|
||||||
</releases>
|
</releases>
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
<name>Ulyssa</name>
|
<name>Ulyssa</name>
|
||||||
</developer>
|
</developer>
|
||||||
|
|
||||||
|
<developer_name>Ulyssa</developer_name>
|
||||||
<metadata_license>CC-BY-SA-4.0</metadata_license>
|
<metadata_license>CC-BY-SA-4.0</metadata_license>
|
||||||
<project_license>Apache-2.0</project_license>
|
<project_license>Apache-2.0</project_license>
|
||||||
|
|
||||||
|
@ -23,8 +25,8 @@
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://iamb.chat/static/images/iamb-demo.gif</image>
|
<image>https://iamb.chat/static/images/metainfo-screenshot.png</image>
|
||||||
<caption>Example conversation within iamb</caption>
|
<caption>Example screenshot of room and lists of rooms, spaces and members within iamb</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
|
@ -37,7 +39,6 @@
|
||||||
</p>
|
</p>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<icon type="remote">https://iamb.chat/images/iamb.svg</icon>
|
|
||||||
<launchable type="desktop-id">iamb.desktop</launchable>
|
<launchable type="desktop-id">iamb.desktop</launchable>
|
||||||
|
|
||||||
<categories>
|
<categories>
|
||||||
|
|
58
flake.lock
generated
58
flake.lock
generated
|
@ -5,29 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709126324,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1705309234,
|
|
||||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -38,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709703039,
|
"lastModified": 1736883708,
|
||||||
"narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=",
|
"narHash": "sha256-uQ+NQ0/xYU0N1CnXsa2zghgNaOPxWpMJXSUJJ9W7140=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d",
|
"rev": "eb62e6aa39ea67e0b8018ba8ea077efe65807dc8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -54,11 +36,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706487304,
|
"lastModified": 1736320768,
|
||||||
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
|
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
|
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -77,15 +59,14 @@
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709863839,
|
"lastModified": 1736994333,
|
||||||
"narHash": "sha256-QpEL5FmZNi2By3sKZY55wGniFXc4wEn9PQczlE8TG0o=",
|
"narHash": "sha256-v4Jrok5yXsZ6dwj2+2uo5cSyUi9fBTurHqHvNHLT1XA=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "e5ab9ee98f479081ad971473d2bc13c59e9fbc0a",
|
"rev": "848db855cb9e88785996e961951659570fc58814",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -108,21 +89,6 @@
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# We only need the nightly overlay in the devShell because .rs files are formatted with nightly.
|
# We only need the nightly overlay in the devShell because .rs files are formatted with nightly.
|
||||||
overlays = [ (import rust-overlay) ];
|
overlays = [ (import rust-overlay) ];
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
rustNightly = pkgs.rust-bin.nightly."2024-03-08".default;
|
rustNightly = pkgs.rust-bin.nightly."2024-12-12".default;
|
||||||
in
|
in
|
||||||
with pkgs;
|
with pkgs;
|
||||||
{
|
{
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
};
|
};
|
||||||
nativeBuildInputs = [ pkg-config ];
|
nativeBuildInputs = [ pkg-config ];
|
||||||
buildInputs = [ openssl ] ++ lib.optionals stdenv.isDarwin
|
buildInputs = [ openssl ] ++ lib.optionals stdenv.isDarwin
|
||||||
(with darwin.apple_sdk.frameworks; [ AppKit Security Cocoa]);
|
(with darwin.apple_sdk.frameworks; [ AppKit Security Cocoa ]);
|
||||||
};
|
};
|
||||||
|
|
||||||
devShell = mkShell {
|
devShell = mkShell {
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
pkg-config
|
pkg-config
|
||||||
cargo-tarpaulin
|
cargo-tarpaulin
|
||||||
cargo-watch
|
cargo-watch
|
||||||
|
sqlite
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "1.83"
|
||||||
|
components = [ "clippy" ]
|
282
src/base.rs
282
src/base.rs
|
@ -12,6 +12,7 @@ use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use emojis::Emoji;
|
use emojis::Emoji;
|
||||||
|
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
|
@ -47,6 +48,7 @@ use matrix_sdk::{
|
||||||
},
|
},
|
||||||
room::redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent},
|
room::redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent},
|
||||||
tag::{TagName, Tags},
|
tag::{TagName, Tags},
|
||||||
|
AnySyncStateEvent,
|
||||||
MessageLikeEvent,
|
MessageLikeEvent,
|
||||||
},
|
},
|
||||||
presence::PresenceState,
|
presence::PresenceState,
|
||||||
|
@ -72,7 +74,7 @@ use modalkit::{
|
||||||
ApplicationStore,
|
ApplicationStore,
|
||||||
ApplicationWindowId,
|
ApplicationWindowId,
|
||||||
},
|
},
|
||||||
completion::{complete_path, CompletionMap},
|
completion::{complete_path, Completer, CompletionMap},
|
||||||
context::EditContext,
|
context::EditContext,
|
||||||
cursor::Cursor,
|
cursor::Cursor,
|
||||||
rope::EditRope,
|
rope::EditRope,
|
||||||
|
@ -90,6 +92,7 @@ use modalkit::{
|
||||||
|
|
||||||
use crate::config::ImagePreviewProtocolValues;
|
use crate::config::ImagePreviewProtocolValues;
|
||||||
use crate::message::ImageStatus;
|
use crate::message::ImageStatus;
|
||||||
|
use crate::notifications::NotificationHandle;
|
||||||
use crate::preview::{source_from_event, spawn_insert_preview};
|
use crate::preview::{source_from_event, spawn_insert_preview};
|
||||||
use crate::{
|
use crate::{
|
||||||
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
|
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
|
||||||
|
@ -177,6 +180,19 @@ pub enum MessageAction {
|
||||||
Unreact(Option<String>, bool),
|
Unreact(Option<String>, bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An action taken in the currently selected space.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum SpaceAction {
|
||||||
|
/// Add a room or update metadata.
|
||||||
|
///
|
||||||
|
/// The [`Option<String>`] argument is the order parameter.
|
||||||
|
/// The [`bool`] argument indicates whether the room is suggested.
|
||||||
|
SetChild(OwnedRoomId, Option<String>, bool),
|
||||||
|
|
||||||
|
/// Remove the selected room.
|
||||||
|
RemoveChild,
|
||||||
|
}
|
||||||
|
|
||||||
/// The type of room being created.
|
/// The type of room being created.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum CreateRoomType {
|
pub enum CreateRoomType {
|
||||||
|
@ -243,6 +259,9 @@ pub enum SortFieldRoom {
|
||||||
|
|
||||||
/// Sort rooms by the timestamps of their most recent messages.
|
/// Sort rooms by the timestamps of their most recent messages.
|
||||||
Recent,
|
Recent,
|
||||||
|
|
||||||
|
/// Sort rooms by whether they are invites.
|
||||||
|
Invite,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fields that users can be sorted by.
|
/// Fields that users can be sorted by.
|
||||||
|
@ -277,7 +296,7 @@ impl<'de> Deserialize<'de> for SortColumn<SortFieldRoom> {
|
||||||
/// [serde] visitor for deserializing [SortColumn] for rooms and spaces.
|
/// [serde] visitor for deserializing [SortColumn] for rooms and spaces.
|
||||||
struct SortRoomVisitor;
|
struct SortRoomVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for SortRoomVisitor {
|
impl Visitor<'_> for SortRoomVisitor {
|
||||||
type Value = SortColumn<SortFieldRoom>;
|
type Value = SortColumn<SortFieldRoom>;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -307,6 +326,7 @@ impl<'de> Visitor<'de> for SortRoomVisitor {
|
||||||
"name" => SortFieldRoom::Name,
|
"name" => SortFieldRoom::Name,
|
||||||
"alias" => SortFieldRoom::Alias,
|
"alias" => SortFieldRoom::Alias,
|
||||||
"id" => SortFieldRoom::RoomId,
|
"id" => SortFieldRoom::RoomId,
|
||||||
|
"invite" => SortFieldRoom::Invite,
|
||||||
_ => {
|
_ => {
|
||||||
let msg = format!("Unknown sort field: {value:?}");
|
let msg = format!("Unknown sort field: {value:?}");
|
||||||
return Err(E::custom(msg));
|
return Err(E::custom(msg));
|
||||||
|
@ -329,7 +349,7 @@ impl<'de> Deserialize<'de> for SortColumn<SortFieldUser> {
|
||||||
/// [serde] visitor for deserializing [SortColumn] for users.
|
/// [serde] visitor for deserializing [SortColumn] for users.
|
||||||
struct SortUserVisitor;
|
struct SortUserVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for SortUserVisitor {
|
impl Visitor<'_> for SortUserVisitor {
|
||||||
type Value = SortColumn<SortFieldUser>;
|
type Value = SortColumn<SortFieldUser>;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -375,6 +395,9 @@ pub enum RoomField {
|
||||||
/// The room name.
|
/// The room name.
|
||||||
Name,
|
Name,
|
||||||
|
|
||||||
|
/// The room id.
|
||||||
|
Id,
|
||||||
|
|
||||||
/// A room tag.
|
/// A room tag.
|
||||||
Tag(TagName),
|
Tag(TagName),
|
||||||
|
|
||||||
|
@ -493,6 +516,9 @@ pub enum IambAction {
|
||||||
/// Perform an action on the currently selected message.
|
/// Perform an action on the currently selected message.
|
||||||
Message(MessageAction),
|
Message(MessageAction),
|
||||||
|
|
||||||
|
/// Perform an action on the current space.
|
||||||
|
Space(SpaceAction),
|
||||||
|
|
||||||
/// Open a URL.
|
/// Open a URL.
|
||||||
OpenLink(String),
|
OpenLink(String),
|
||||||
|
|
||||||
|
@ -534,6 +560,12 @@ impl From<MessageAction> for IambAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SpaceAction> for IambAction {
|
||||||
|
fn from(act: SpaceAction) -> Self {
|
||||||
|
IambAction::Space(act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<RoomAction> for IambAction {
|
impl From<RoomAction> for IambAction {
|
||||||
fn from(act: RoomAction) -> Self {
|
fn from(act: RoomAction) -> Self {
|
||||||
IambAction::Room(act)
|
IambAction::Room(act)
|
||||||
|
@ -553,6 +585,7 @@ impl ApplicationAction for IambAction {
|
||||||
IambAction::Homeserver(..) => SequenceStatus::Break,
|
IambAction::Homeserver(..) => SequenceStatus::Break,
|
||||||
IambAction::Keys(..) => SequenceStatus::Break,
|
IambAction::Keys(..) => SequenceStatus::Break,
|
||||||
IambAction::Message(..) => SequenceStatus::Break,
|
IambAction::Message(..) => SequenceStatus::Break,
|
||||||
|
IambAction::Space(..) => SequenceStatus::Break,
|
||||||
IambAction::Room(..) => SequenceStatus::Break,
|
IambAction::Room(..) => SequenceStatus::Break,
|
||||||
IambAction::OpenLink(..) => SequenceStatus::Break,
|
IambAction::OpenLink(..) => SequenceStatus::Break,
|
||||||
IambAction::Send(..) => SequenceStatus::Break,
|
IambAction::Send(..) => SequenceStatus::Break,
|
||||||
|
@ -568,6 +601,7 @@ impl ApplicationAction for IambAction {
|
||||||
IambAction::Homeserver(..) => SequenceStatus::Atom,
|
IambAction::Homeserver(..) => SequenceStatus::Atom,
|
||||||
IambAction::Keys(..) => SequenceStatus::Atom,
|
IambAction::Keys(..) => SequenceStatus::Atom,
|
||||||
IambAction::Message(..) => SequenceStatus::Atom,
|
IambAction::Message(..) => SequenceStatus::Atom,
|
||||||
|
IambAction::Space(..) => SequenceStatus::Atom,
|
||||||
IambAction::OpenLink(..) => SequenceStatus::Atom,
|
IambAction::OpenLink(..) => SequenceStatus::Atom,
|
||||||
IambAction::Room(..) => SequenceStatus::Atom,
|
IambAction::Room(..) => SequenceStatus::Atom,
|
||||||
IambAction::Send(..) => SequenceStatus::Atom,
|
IambAction::Send(..) => SequenceStatus::Atom,
|
||||||
|
@ -583,6 +617,7 @@ impl ApplicationAction for IambAction {
|
||||||
IambAction::Homeserver(..) => SequenceStatus::Ignore,
|
IambAction::Homeserver(..) => SequenceStatus::Ignore,
|
||||||
IambAction::Keys(..) => SequenceStatus::Ignore,
|
IambAction::Keys(..) => SequenceStatus::Ignore,
|
||||||
IambAction::Message(..) => SequenceStatus::Ignore,
|
IambAction::Message(..) => SequenceStatus::Ignore,
|
||||||
|
IambAction::Space(..) => SequenceStatus::Ignore,
|
||||||
IambAction::Room(..) => SequenceStatus::Ignore,
|
IambAction::Room(..) => SequenceStatus::Ignore,
|
||||||
IambAction::OpenLink(..) => SequenceStatus::Ignore,
|
IambAction::OpenLink(..) => SequenceStatus::Ignore,
|
||||||
IambAction::Send(..) => SequenceStatus::Ignore,
|
IambAction::Send(..) => SequenceStatus::Ignore,
|
||||||
|
@ -597,6 +632,7 @@ impl ApplicationAction for IambAction {
|
||||||
IambAction::ClearUnreads => false,
|
IambAction::ClearUnreads => false,
|
||||||
IambAction::Homeserver(..) => false,
|
IambAction::Homeserver(..) => false,
|
||||||
IambAction::Message(..) => false,
|
IambAction::Message(..) => false,
|
||||||
|
IambAction::Space(..) => false,
|
||||||
IambAction::Room(..) => false,
|
IambAction::Room(..) => false,
|
||||||
IambAction::Keys(..) => false,
|
IambAction::Keys(..) => false,
|
||||||
IambAction::Send(..) => false,
|
IambAction::Send(..) => false,
|
||||||
|
@ -614,6 +650,12 @@ impl From<RoomAction> for ProgramAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SpaceAction> for ProgramAction {
|
||||||
|
fn from(act: SpaceAction) -> Self {
|
||||||
|
IambAction::from(act).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<IambAction> for ProgramAction {
|
impl From<IambAction> for ProgramAction {
|
||||||
fn from(act: IambAction) -> Self {
|
fn from(act: IambAction) -> Self {
|
||||||
Action::Application(act)
|
Action::Application(act)
|
||||||
|
@ -709,10 +751,22 @@ pub enum IambError {
|
||||||
#[error("Current window is not a room or space")]
|
#[error("Current window is not a room or space")]
|
||||||
NoSelectedRoomOrSpace,
|
NoSelectedRoomOrSpace,
|
||||||
|
|
||||||
|
/// A failure due to not having a room or space item selected in a list.
|
||||||
|
#[error("No room or space currently selected in list")]
|
||||||
|
NoSelectedRoomOrSpaceItem,
|
||||||
|
|
||||||
/// A failure due to not having a room selected.
|
/// A failure due to not having a room selected.
|
||||||
#[error("Current window is not a room")]
|
#[error("Current window is not a room")]
|
||||||
NoSelectedRoom,
|
NoSelectedRoom,
|
||||||
|
|
||||||
|
/// A failure due to not having a space selected.
|
||||||
|
#[error("Current window is not a space")]
|
||||||
|
NoSelectedSpace,
|
||||||
|
|
||||||
|
/// A failure due to not having sufficient permission to perform an action in a room.
|
||||||
|
#[error("You do not have the permission to do that")]
|
||||||
|
InsufficientPermission,
|
||||||
|
|
||||||
/// A failure due to not having an outstanding room invitation.
|
/// A failure due to not having an outstanding room invitation.
|
||||||
#[error("You do not have a current invitation to this room")]
|
#[error("You do not have a current invitation to this room")]
|
||||||
NotInvited,
|
NotInvited,
|
||||||
|
@ -785,6 +839,9 @@ pub enum EventLocation {
|
||||||
|
|
||||||
/// The [EventId] belongs to a reaction to the given event.
|
/// The [EventId] belongs to a reaction to the given event.
|
||||||
Reaction(OwnedEventId),
|
Reaction(OwnedEventId),
|
||||||
|
|
||||||
|
/// The [EventId] belongs to a state event in the main timeline of the room.
|
||||||
|
State(MessageKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventLocation {
|
impl EventLocation {
|
||||||
|
@ -814,7 +871,6 @@ impl UnreadInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about room's the user's joined.
|
/// Information about room's the user's joined.
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RoomInfo {
|
pub struct RoomInfo {
|
||||||
/// The display name for this room.
|
/// The display name for this room.
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
@ -829,15 +885,13 @@ pub struct RoomInfo {
|
||||||
messages: Messages,
|
messages: Messages,
|
||||||
|
|
||||||
/// A map of read markers to display on different events.
|
/// A map of read markers to display on different events.
|
||||||
pub event_receipts: HashMap<OwnedEventId, HashSet<OwnedUserId>>,
|
pub event_receipts: HashMap<ReceiptThread, HashMap<OwnedEventId, HashSet<OwnedUserId>>>,
|
||||||
|
|
||||||
/// A map of the most recent read marker for each user.
|
/// A map of the most recent read marker for each user.
|
||||||
///
|
///
|
||||||
/// Every receipt in this map should also have an entry in [`event_receipts`],
|
/// Every receipt in this map should also have an entry in [`event_receipts`](`Self::event_receipts`),
|
||||||
/// however not every user has an entry. If a user's most recent receipt is
|
/// however not every user has an entry. If a user's most recent receipt is
|
||||||
/// older than the oldest loaded event, that user will not be included.
|
/// older than the oldest loaded event, that user will not be included.
|
||||||
pub user_receipts: HashMap<OwnedUserId, OwnedEventId>,
|
pub user_receipts: HashMap<ReceiptThread, HashMap<OwnedUserId, OwnedEventId>>,
|
||||||
|
|
||||||
/// A map of message identifiers to a map of reaction events.
|
/// A map of message identifiers to a map of reaction events.
|
||||||
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
||||||
|
|
||||||
|
@ -863,6 +917,28 @@ pub struct RoomInfo {
|
||||||
pub draw_last: Option<Instant>,
|
pub draw_last: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for RoomInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
messages: Messages::new(ReceiptThread::Main),
|
||||||
|
|
||||||
|
name: Default::default(),
|
||||||
|
tags: Default::default(),
|
||||||
|
keys: Default::default(),
|
||||||
|
event_receipts: Default::default(),
|
||||||
|
user_receipts: Default::default(),
|
||||||
|
reactions: Default::default(),
|
||||||
|
threads: Default::default(),
|
||||||
|
fetching: Default::default(),
|
||||||
|
fetch_id: Default::default(),
|
||||||
|
fetch_last: Default::default(),
|
||||||
|
users_typing: Default::default(),
|
||||||
|
display_names: Default::default(),
|
||||||
|
draw_last: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RoomInfo {
|
impl RoomInfo {
|
||||||
pub fn get_thread(&self, root: Option<&EventId>) -> Option<&Messages> {
|
pub fn get_thread(&self, root: Option<&EventId>) -> Option<&Messages> {
|
||||||
if let Some(thread_root) = root {
|
if let Some(thread_root) = root {
|
||||||
|
@ -874,7 +950,9 @@ impl RoomInfo {
|
||||||
|
|
||||||
pub fn get_thread_mut(&mut self, root: Option<OwnedEventId>) -> &mut Messages {
|
pub fn get_thread_mut(&mut self, root: Option<OwnedEventId>) -> &mut Messages {
|
||||||
if let Some(thread_root) = root {
|
if let Some(thread_root) = root {
|
||||||
self.threads.entry(thread_root).or_default()
|
self.threads
|
||||||
|
.entry(thread_root.clone())
|
||||||
|
.or_insert_with(|| Messages::thread(thread_root))
|
||||||
} else {
|
} else {
|
||||||
&mut self.messages
|
&mut self.messages
|
||||||
}
|
}
|
||||||
|
@ -952,6 +1030,12 @@ impl RoomInfo {
|
||||||
|
|
||||||
match self.keys.get(redacts) {
|
match self.keys.get(redacts) {
|
||||||
None => return,
|
None => return,
|
||||||
|
Some(EventLocation::State(key)) => {
|
||||||
|
if let Some(msg) = self.messages.get_mut(key) {
|
||||||
|
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||||
|
msg.redact(ev, room_version);
|
||||||
|
}
|
||||||
|
},
|
||||||
Some(EventLocation::Message(None, key)) => {
|
Some(EventLocation::Message(None, key)) => {
|
||||||
if let Some(msg) = self.messages.get_mut(key) {
|
if let Some(msg) = self.messages.get_mut(key) {
|
||||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||||
|
@ -1008,7 +1092,9 @@ impl RoomInfo {
|
||||||
};
|
};
|
||||||
|
|
||||||
let source = if let Some(thread) = thread {
|
let source = if let Some(thread) = thread {
|
||||||
self.threads.entry(thread.clone()).or_default()
|
self.threads
|
||||||
|
.entry(thread.clone())
|
||||||
|
.or_insert_with(|| Messages::thread(thread.clone()))
|
||||||
} else {
|
} else {
|
||||||
&mut self.messages
|
&mut self.messages
|
||||||
};
|
};
|
||||||
|
@ -1025,6 +1111,7 @@ impl RoomInfo {
|
||||||
content.apply_replacement(new_msgtype);
|
content.apply_replacement(new_msgtype);
|
||||||
},
|
},
|
||||||
MessageEvent::Redacted(_) |
|
MessageEvent::Redacted(_) |
|
||||||
|
MessageEvent::State(_) |
|
||||||
MessageEvent::EncryptedOriginal(_) |
|
MessageEvent::EncryptedOriginal(_) |
|
||||||
MessageEvent::EncryptedRedacted(_) => {
|
MessageEvent::EncryptedRedacted(_) => {
|
||||||
return;
|
return;
|
||||||
|
@ -1034,16 +1121,32 @@ impl RoomInfo {
|
||||||
msg.html = msg.event.html();
|
msg.html = msg.event.html();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_any_state(&mut self, msg: AnySyncStateEvent) {
|
||||||
|
let event_id = msg.event_id().to_owned();
|
||||||
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
||||||
|
|
||||||
|
let loc = EventLocation::State(key.clone());
|
||||||
|
self.keys.insert(event_id, loc);
|
||||||
|
self.messages.insert_message(key, msg);
|
||||||
|
}
|
||||||
|
|
||||||
/// Indicates whether this room has unread messages.
|
/// Indicates whether this room has unread messages.
|
||||||
pub fn unreads(&self, settings: &ApplicationSettings) -> UnreadInfo {
|
pub fn unreads(&self, settings: &ApplicationSettings) -> UnreadInfo {
|
||||||
let last_message = self.messages.last_key_value();
|
let last_message = self.messages.last_key_value();
|
||||||
let last_receipt = self.get_receipt(&settings.profile.user_id);
|
let last_receipt = self
|
||||||
|
.user_receipts
|
||||||
|
.get(&ReceiptThread::Main)
|
||||||
|
.and_then(|receipts| receipts.get(&settings.profile.user_id));
|
||||||
|
|
||||||
match (last_message, last_receipt) {
|
match (last_message, last_receipt) {
|
||||||
(Some(((ts, recent), _)), Some(last_read)) => {
|
(Some(((ts, recent), _)), Some(last_read)) => {
|
||||||
UnreadInfo { unread: last_read != recent, latest: Some(*ts) }
|
UnreadInfo { unread: last_read != recent, latest: Some(*ts) }
|
||||||
},
|
},
|
||||||
(Some(((ts, _), _)), None) => UnreadInfo { unread: false, latest: Some(*ts) },
|
(Some(((ts, _), _)), None) => {
|
||||||
|
// If we've never loaded/generated a room's receipt (example,
|
||||||
|
// a newly joined but never viewed room), show it as unread.
|
||||||
|
UnreadInfo { unread: true, latest: Some(*ts) }
|
||||||
|
},
|
||||||
(None, _) => UnreadInfo::default(),
|
(None, _) => UnreadInfo::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1071,7 +1174,10 @@ impl RoomInfo {
|
||||||
let event_id = msg.event_id().to_owned();
|
let event_id = msg.event_id().to_owned();
|
||||||
let key = (msg.origin_server_ts().into(), event_id.clone());
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
||||||
|
|
||||||
let replies = self.threads.entry(thread_root.clone()).or_default();
|
let replies = self
|
||||||
|
.threads
|
||||||
|
.entry(thread_root.clone())
|
||||||
|
.or_insert_with(|| Messages::thread(thread_root.clone()));
|
||||||
let loc = EventLocation::Message(Some(thread_root), key.clone());
|
let loc = EventLocation::Message(Some(thread_root), key.clone());
|
||||||
self.keys.insert(event_id, loc);
|
self.keys.insert(event_id, loc);
|
||||||
replies.insert_message(key, msg);
|
replies.insert_message(key, msg);
|
||||||
|
@ -1131,40 +1237,73 @@ impl RoomInfo {
|
||||||
|
|
||||||
/// Indicates whether we've recently fetched scrollback for this room.
|
/// Indicates whether we've recently fetched scrollback for this room.
|
||||||
pub fn recently_fetched(&self) -> bool {
|
pub fn recently_fetched(&self) -> bool {
|
||||||
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
self.fetch_last.is_some_and(|i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_receipt(&mut self, user_id: &OwnedUserId) -> Option<()> {
|
fn clear_receipt(&mut self, thread: &ReceiptThread, user_id: &OwnedUserId) -> Option<()> {
|
||||||
let old_event_id = self.user_receipts.get(user_id)?;
|
let old_event_id =
|
||||||
let old_receipts = self.event_receipts.get_mut(old_event_id)?;
|
self.user_receipts.get(thread).and_then(|receipts| receipts.get(user_id))?;
|
||||||
|
let old_thread = self.event_receipts.get_mut(thread)?;
|
||||||
|
let old_receipts = old_thread.get_mut(old_event_id)?;
|
||||||
old_receipts.remove(user_id);
|
old_receipts.remove(user_id);
|
||||||
|
|
||||||
if old_receipts.is_empty() {
|
if old_receipts.is_empty() {
|
||||||
self.event_receipts.remove(old_event_id);
|
old_thread.remove(old_event_id);
|
||||||
|
}
|
||||||
|
if old_thread.is_empty() {
|
||||||
|
self.event_receipts.remove(thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_receipt(&mut self, user_id: OwnedUserId, event_id: OwnedEventId) {
|
pub fn set_receipt(
|
||||||
self.clear_receipt(&user_id);
|
&mut self,
|
||||||
|
thread: ReceiptThread,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
event_id: OwnedEventId,
|
||||||
|
) {
|
||||||
|
self.clear_receipt(&thread, &user_id);
|
||||||
self.event_receipts
|
self.event_receipts
|
||||||
|
.entry(thread.clone())
|
||||||
|
.or_default()
|
||||||
.entry(event_id.clone())
|
.entry(event_id.clone())
|
||||||
.or_default()
|
.or_default()
|
||||||
.insert(user_id.clone());
|
.insert(user_id.clone());
|
||||||
self.user_receipts.insert(user_id, event_id);
|
self.user_receipts.entry(thread).or_default().insert(user_id, event_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fully_read(&mut self, user_id: OwnedUserId) {
|
pub fn fully_read(&mut self, user_id: &UserId) {
|
||||||
let Some(((_, event_id), _)) = self.messages.last_key_value() else {
|
let Some(((_, event_id), _)) = self.messages.last_key_value() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.set_receipt(user_id, event_id.clone());
|
self.set_receipt(ReceiptThread::Main, user_id.to_owned(), event_id.clone());
|
||||||
|
|
||||||
|
let newest = self
|
||||||
|
.threads
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(thread_id, messages)| {
|
||||||
|
let thread = ReceiptThread::Thread(thread_id.to_owned());
|
||||||
|
|
||||||
|
messages
|
||||||
|
.last_key_value()
|
||||||
|
.map(|((_, event_id), _)| (thread, event_id.to_owned()))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (thread, event_id) in newest.into_iter() {
|
||||||
|
self.set_receipt(thread, user_id.to_owned(), event_id.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_receipt(&self, user_id: &UserId) -> Option<&OwnedEventId> {
|
pub fn receipts<'a>(
|
||||||
self.user_receipts.get(user_id)
|
&'a self,
|
||||||
|
user_id: &'a UserId,
|
||||||
|
) -> impl Iterator<Item = (&'a ReceiptThread, &'a OwnedEventId)> + 'a {
|
||||||
|
self.user_receipts
|
||||||
|
.iter()
|
||||||
|
.filter_map(move |(t, rs)| rs.get(user_id).map(|r| (t, r)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_typers(&self) -> &[OwnedUserId] {
|
fn get_typers(&self) -> &[OwnedUserId] {
|
||||||
|
@ -1223,7 +1362,9 @@ impl RoomInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.tunables.typing_notice_display {
|
if !settings.tunables.typing_notice_display {
|
||||||
return area;
|
// still keep one line blank, so `render_jump_to_recent` doesn't immediately hide the
|
||||||
|
// last line in scrollback
|
||||||
|
return Rect::new(area.x, area.y, area.width, area.height - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
||||||
|
@ -1268,7 +1409,7 @@ fn emoji_map() -> CompletionMap<String, &'static Emoji> {
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
|
fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
|
||||||
let mut picker = match Picker::from_termios() {
|
let mut picker = match Picker::from_query_stdio() {
|
||||||
Ok(picker) => picker,
|
Ok(picker) => picker,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to setup image previews: {e}");
|
tracing::error!("Failed to setup image previews: {e}");
|
||||||
|
@ -1277,9 +1418,7 @@ fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(protocol_type) = protocol_type {
|
if let Some(protocol_type) = protocol_type {
|
||||||
picker.protocol_type = protocol_type;
|
picker.set_protocol_type(protocol_type);
|
||||||
} else {
|
|
||||||
picker.guess_protocol();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(picker)
|
Some(picker)
|
||||||
|
@ -1302,8 +1441,8 @@ fn picker_from_settings(settings: &ApplicationSettings) -> Option<Picker> {
|
||||||
}) = image_preview_protocol
|
}) = image_preview_protocol
|
||||||
{
|
{
|
||||||
// User forced type and font_size: use that.
|
// User forced type and font_size: use that.
|
||||||
let mut picker = Picker::new(font_size);
|
let mut picker = Picker::from_fontsize(font_size);
|
||||||
picker.protocol_type = protocol_type;
|
picker.set_protocol_type(protocol_type);
|
||||||
Some(picker)
|
Some(picker)
|
||||||
} else {
|
} else {
|
||||||
// Guess, but use type if forced.
|
// Guess, but use type if forced.
|
||||||
|
@ -1417,6 +1556,12 @@ pub struct ChatStore {
|
||||||
|
|
||||||
/// Whether the application is currently focused
|
/// Whether the application is currently focused
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
|
|
||||||
|
/// Collator for locale-aware text sorting.
|
||||||
|
pub collator: feruca::Collator,
|
||||||
|
|
||||||
|
/// Notifications that should be dismissed when the user opens the room.
|
||||||
|
pub open_notifications: HashMap<OwnedRoomId, Vec<NotificationHandle>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatStore {
|
impl ChatStore {
|
||||||
|
@ -1431,6 +1576,7 @@ impl ChatStore {
|
||||||
cmds: crate::commands::setup_commands(),
|
cmds: crate::commands::setup_commands(),
|
||||||
emojis: emoji_map(),
|
emojis: emoji_map(),
|
||||||
|
|
||||||
|
collator: Default::default(),
|
||||||
names: Default::default(),
|
names: Default::default(),
|
||||||
rooms: Default::default(),
|
rooms: Default::default(),
|
||||||
presences: Default::default(),
|
presences: Default::default(),
|
||||||
|
@ -1440,6 +1586,7 @@ impl ChatStore {
|
||||||
draw_curr: None,
|
draw_curr: None,
|
||||||
ring_bell: false,
|
ring_bell: false,
|
||||||
focused: true,
|
focused: true,
|
||||||
|
open_notifications: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1560,7 +1707,7 @@ impl<'de> Deserialize<'de> for IambId {
|
||||||
/// [serde] visitor for deserializing [IambId].
|
/// [serde] visitor for deserializing [IambId].
|
||||||
struct IambIdVisitor;
|
struct IambIdVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for IambIdVisitor {
|
impl Visitor<'_> for IambIdVisitor {
|
||||||
type Value = IambId;
|
type Value = IambId;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -1697,6 +1844,13 @@ impl RoomFocus {
|
||||||
pub fn is_msgbar(&self) -> bool {
|
pub fn is_msgbar(&self) -> bool {
|
||||||
matches!(self, RoomFocus::MessageBar)
|
matches!(self, RoomFocus::MessageBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle(&mut self) {
|
||||||
|
*self = match self {
|
||||||
|
RoomFocus::MessageBar => RoomFocus::Scrollback,
|
||||||
|
RoomFocus::Scrollback => RoomFocus::MessageBar,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifiers used to track where a mark was placed.
|
/// Identifiers used to track where a mark was placed.
|
||||||
|
@ -1765,11 +1919,20 @@ impl ApplicationInfo for IambInfo {
|
||||||
type WindowId = IambId;
|
type WindowId = IambId;
|
||||||
type ContentId = IambBufferId;
|
type ContentId = IambBufferId;
|
||||||
|
|
||||||
|
fn content_of_command(ct: CommandType) -> IambBufferId {
|
||||||
|
IambBufferId::Command(ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IambCompleter;
|
||||||
|
|
||||||
|
impl Completer<IambInfo> for IambCompleter {
|
||||||
fn complete(
|
fn complete(
|
||||||
|
&mut self,
|
||||||
text: &EditRope,
|
text: &EditRope,
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
content: &IambBufferId,
|
content: &IambBufferId,
|
||||||
store: &mut ProgramStore,
|
store: &mut ChatStore,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
match content {
|
match content {
|
||||||
IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store),
|
IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store),
|
||||||
|
@ -1787,21 +1950,16 @@ impl ApplicationInfo for IambInfo {
|
||||||
IambBufferId::UnreadList => vec![],
|
IambBufferId::UnreadList => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content_of_command(ct: CommandType) -> IambBufferId {
|
|
||||||
IambBufferId::Command(ct)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for user IDs.
|
/// Tab completion for user IDs.
|
||||||
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||||
let id = text
|
let id = text
|
||||||
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
||||||
.unwrap_or_else(EditRope::empty);
|
.unwrap_or_else(EditRope::empty);
|
||||||
let id = Cow::from(&id);
|
let id = Cow::from(&id);
|
||||||
|
|
||||||
store
|
store
|
||||||
.application
|
|
||||||
.presences
|
.presences
|
||||||
.complete(id.as_ref())
|
.complete(id.as_ref())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1810,7 +1968,7 @@ fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion within the message bar.
|
/// Tab completion within the message bar.
|
||||||
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||||
let id = text
|
let id = text
|
||||||
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
||||||
.unwrap_or_else(EditRope::empty);
|
.unwrap_or_else(EditRope::empty);
|
||||||
|
@ -1819,13 +1977,12 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
|
||||||
match id.chars().next() {
|
match id.chars().next() {
|
||||||
// Complete room aliases.
|
// Complete room aliases.
|
||||||
Some('#') => {
|
Some('#') => {
|
||||||
return store.application.names.complete(id.as_ref());
|
return store.names.complete(id.as_ref());
|
||||||
},
|
},
|
||||||
|
|
||||||
// Complete room identifiers.
|
// Complete room identifiers.
|
||||||
Some('!') => {
|
Some('!') => {
|
||||||
return store
|
return store
|
||||||
.application
|
|
||||||
.rooms
|
.rooms
|
||||||
.complete(id.as_ref())
|
.complete(id.as_ref())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1835,7 +1992,7 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
|
||||||
|
|
||||||
// Complete Emoji shortcodes.
|
// Complete Emoji shortcodes.
|
||||||
Some(':') => {
|
Some(':') => {
|
||||||
let list = store.application.emojis.complete(&id[1..]);
|
let list = store.emojis.complete(&id[1..]);
|
||||||
let iter = list.into_iter().take(200).map(|s| format!(":{}:", s));
|
let iter = list.into_iter().take(200).map(|s| format!(":{}:", s));
|
||||||
|
|
||||||
return iter.collect();
|
return iter.collect();
|
||||||
|
@ -1844,7 +2001,6 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
|
||||||
// Complete usernames for @ and empty strings.
|
// Complete usernames for @ and empty strings.
|
||||||
Some('@') | None => {
|
Some('@') | None => {
|
||||||
return store
|
return store
|
||||||
.application
|
|
||||||
.presences
|
.presences
|
||||||
.complete(id.as_ref())
|
.complete(id.as_ref())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1858,28 +2014,23 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for Matrix identifiers (usernames, room aliases, etc.)
|
/// Tab completion for Matrix identifiers (usernames, room aliases, etc.)
|
||||||
fn complete_matrix_names(
|
fn complete_matrix_names(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||||
text: &EditRope,
|
|
||||||
cursor: &mut Cursor,
|
|
||||||
store: &ProgramStore,
|
|
||||||
) -> Vec<String> {
|
|
||||||
let id = text
|
let id = text
|
||||||
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
||||||
.unwrap_or_else(EditRope::empty);
|
.unwrap_or_else(EditRope::empty);
|
||||||
let id = Cow::from(&id);
|
let id = Cow::from(&id);
|
||||||
|
|
||||||
let list = store.application.names.complete(id.as_ref());
|
let list = store.names.complete(id.as_ref());
|
||||||
if !list.is_empty() {
|
if !list.is_empty() {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
let list = store.application.presences.complete(id.as_ref());
|
let list = store.presences.complete(id.as_ref());
|
||||||
if !list.is_empty() {
|
if !list.is_empty() {
|
||||||
return list.into_iter().map(|i| i.to_string()).collect();
|
return list.into_iter().map(|i| i.to_string()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
store
|
store
|
||||||
.application
|
|
||||||
.rooms
|
.rooms
|
||||||
.complete(id.as_ref())
|
.complete(id.as_ref())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1888,12 +2039,12 @@ fn complete_matrix_names(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for Emoji shortcode names.
|
/// Tab completion for Emoji shortcode names.
|
||||||
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||||
let sc = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
let sc = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
||||||
let sc = sc.unwrap_or_else(EditRope::empty);
|
let sc = sc.unwrap_or_else(EditRope::empty);
|
||||||
let sc = Cow::from(&sc);
|
let sc = Cow::from(&sc);
|
||||||
|
|
||||||
store.application.emojis.complete(sc.as_ref())
|
store.emojis.complete(sc.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for command names.
|
/// Tab completion for command names.
|
||||||
|
@ -1901,11 +2052,11 @@ fn complete_cmdname(
|
||||||
desc: CommandDescription,
|
desc: CommandDescription,
|
||||||
text: &EditRope,
|
text: &EditRope,
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
store: &ProgramStore,
|
store: &ChatStore,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
// Complete command name and set cursor position.
|
// Complete command name and set cursor position.
|
||||||
let _ = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
let _ = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
||||||
store.application.cmds.complete_name(desc.command.as_str())
|
store.cmds.complete_name(desc.command.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for command arguments.
|
/// Tab completion for command arguments.
|
||||||
|
@ -1913,9 +2064,9 @@ fn complete_cmdarg(
|
||||||
desc: CommandDescription,
|
desc: CommandDescription,
|
||||||
text: &EditRope,
|
text: &EditRope,
|
||||||
cursor: &mut Cursor,
|
cursor: &mut Cursor,
|
||||||
store: &ProgramStore,
|
store: &ChatStore,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let cmd = match store.application.cmds.get(desc.command.as_str()) {
|
let cmd = match store.cmds.get(desc.command.as_str()) {
|
||||||
Ok(cmd) => cmd,
|
Ok(cmd) => cmd,
|
||||||
Err(_) => return vec![],
|
Err(_) => return vec![],
|
||||||
};
|
};
|
||||||
|
@ -1938,12 +2089,7 @@ fn complete_cmdarg(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for commands.
|
/// Tab completion for commands.
|
||||||
fn complete_cmd(
|
fn complete_cmd(cmd: &str, text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||||
cmd: &str,
|
|
||||||
text: &EditRope,
|
|
||||||
cursor: &mut Cursor,
|
|
||||||
store: &ProgramStore,
|
|
||||||
) -> Vec<String> {
|
|
||||||
match CommandDescription::from_str(cmd) {
|
match CommandDescription::from_str(cmd) {
|
||||||
Ok(desc) => {
|
Ok(desc) => {
|
||||||
if desc.arg.untrimmed.is_empty() {
|
if desc.arg.untrimmed.is_empty() {
|
||||||
|
@ -1960,7 +2106,7 @@ fn complete_cmd(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab completion for the command bar.
|
/// Tab completion for the command bar.
|
||||||
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||||
let eo = text.cursor_to_offset(cursor);
|
let eo = text.cursor_to_offset(cursor);
|
||||||
let slice = text.slice(..eo);
|
let slice = text.slice(..eo);
|
||||||
let cow = Cow::from(&slice);
|
let cow = Cow::from(&slice);
|
||||||
|
@ -2140,6 +2286,7 @@ pub mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_complete_msgbar() {
|
async fn test_complete_msgbar() {
|
||||||
let store = mock_store().await;
|
let store = mock_store().await;
|
||||||
|
let store = store.application;
|
||||||
|
|
||||||
let text = EditRope::from("going for a walk :walk ");
|
let text = EditRope::from("going for a walk :walk ");
|
||||||
let mut cursor = Cursor::new(0, 22);
|
let mut cursor = Cursor::new(0, 22);
|
||||||
|
@ -2163,6 +2310,7 @@ pub mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_complete_cmdbar() {
|
async fn test_complete_cmdbar() {
|
||||||
let store = mock_store().await;
|
let store = mock_store().await;
|
||||||
|
let store = store.application;
|
||||||
let users = vec![
|
let users = vec![
|
||||||
"@user1:example.com",
|
"@user1:example.com",
|
||||||
"@user2:example.com",
|
"@user2:example.com",
|
||||||
|
|
218
src/commands.rs
218
src/commands.rs
|
@ -2,9 +2,9 @@
|
||||||
//!
|
//!
|
||||||
//! The command-bar commands are set up here, and iamb-specific commands are defined here. See
|
//! The command-bar commands are set up here, and iamb-specific commands are defined here. See
|
||||||
//! [modalkit::env::vim::command] for additional Vim commands we pull in.
|
//! [modalkit::env::vim::command] for additional Vim commands we pull in.
|
||||||
use std::convert::TryFrom;
|
use std::{convert::TryFrom, str::FromStr as _};
|
||||||
|
|
||||||
use matrix_sdk::ruma::{events::tag::TagName, OwnedUserId};
|
use matrix_sdk::ruma::{events::tag::TagName, OwnedRoomId, OwnedUserId};
|
||||||
|
|
||||||
use modalkit::{
|
use modalkit::{
|
||||||
commands::{CommandError, CommandResult, CommandStep},
|
commands::{CommandError, CommandResult, CommandStep},
|
||||||
|
@ -27,6 +27,7 @@ use crate::base::{
|
||||||
RoomAction,
|
RoomAction,
|
||||||
RoomField,
|
RoomField,
|
||||||
SendAction,
|
SendAction,
|
||||||
|
SpaceAction,
|
||||||
VerifyAction,
|
VerifyAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -475,10 +476,18 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
("topic", "unset", None) => RoomAction::Unset(RoomField::Topic).into(),
|
("topic", "unset", None) => RoomAction::Unset(RoomField::Topic).into(),
|
||||||
("topic", "unset", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
("topic", "unset", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
|
||||||
|
// :room topic show
|
||||||
|
("topic", "show", None) => RoomAction::Show(RoomField::Topic).into(),
|
||||||
|
("topic", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
|
||||||
// :room tag set <tag-name>
|
// :room tag set <tag-name>
|
||||||
("tag", "set", Some(s)) => RoomAction::Set(RoomField::Tag(tag_name(s)?), "".into()).into(),
|
("tag", "set", Some(s)) => RoomAction::Set(RoomField::Tag(tag_name(s)?), "".into()).into(),
|
||||||
("tag", "set", None) => return Result::Err(CommandError::InvalidArgument),
|
("tag", "set", None) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
|
||||||
|
// :room tag unset <tag-name>
|
||||||
|
("tag", "unset", Some(s)) => RoomAction::Unset(RoomField::Tag(tag_name(s)?)).into(),
|
||||||
|
("tag", "unset", None) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
|
||||||
// :room notify set <notification-level>
|
// :room notify set <notification-level>
|
||||||
("notify", "set", Some(s)) => RoomAction::Set(RoomField::NotificationMode, s).into(),
|
("notify", "set", Some(s)) => RoomAction::Set(RoomField::NotificationMode, s).into(),
|
||||||
("notify", "set", None) => return Result::Err(CommandError::InvalidArgument),
|
("notify", "set", None) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
@ -491,10 +500,6 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
("notify", "show", None) => RoomAction::Show(RoomField::NotificationMode).into(),
|
("notify", "show", None) => RoomAction::Show(RoomField::NotificationMode).into(),
|
||||||
("notify", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
("notify", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
|
||||||
// :room tag unset <tag-name>
|
|
||||||
("tag", "unset", Some(s)) => RoomAction::Unset(RoomField::Tag(tag_name(s)?)).into(),
|
|
||||||
("tag", "unset", None) => return Result::Err(CommandError::InvalidArgument),
|
|
||||||
|
|
||||||
// :room aliases show
|
// :room aliases show
|
||||||
("alias", "show", None) => RoomAction::Show(RoomField::Aliases).into(),
|
("alias", "show", None) => RoomAction::Show(RoomField::Aliases).into(),
|
||||||
("alias", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
("alias", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
@ -531,6 +536,91 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
return Result::Err(CommandError::InvalidArgument)
|
return Result::Err(CommandError::InvalidArgument)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// :room id show
|
||||||
|
("id", "show", None) => RoomAction::Show(RoomField::Id).into(),
|
||||||
|
("id", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
|
||||||
|
_ => return Result::Err(CommandError::InvalidArgument),
|
||||||
|
};
|
||||||
|
|
||||||
|
let step = CommandStep::Continue(act.into(), ctx.context.clone());
|
||||||
|
|
||||||
|
return Ok(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iamb_space(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
|
let mut args = desc.arg.options()?;
|
||||||
|
|
||||||
|
if args.len() < 2 {
|
||||||
|
return Err(CommandError::InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
let OptionType::Positional(field) = args.remove(0) else {
|
||||||
|
return Err(CommandError::InvalidArgument);
|
||||||
|
};
|
||||||
|
let OptionType::Positional(action) = args.remove(0) else {
|
||||||
|
return Err(CommandError::InvalidArgument);
|
||||||
|
};
|
||||||
|
|
||||||
|
let act: IambAction = match (field.as_str(), action.as_str()) {
|
||||||
|
// :space child remove
|
||||||
|
("child", "remove") => {
|
||||||
|
if !(args.is_empty()) {
|
||||||
|
return Err(CommandError::InvalidArgument);
|
||||||
|
}
|
||||||
|
SpaceAction::RemoveChild.into()
|
||||||
|
},
|
||||||
|
// :space child set <child>
|
||||||
|
("child", "set") => {
|
||||||
|
let mut order = None;
|
||||||
|
let mut suggested = false;
|
||||||
|
let mut raw_child = None;
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match arg {
|
||||||
|
OptionType::Flag(name, Some(arg)) => {
|
||||||
|
match name.as_str() {
|
||||||
|
"order" => {
|
||||||
|
if order.is_some() {
|
||||||
|
let msg = "Multiple ++order arguments are not allowed";
|
||||||
|
let err = CommandError::Error(msg.into());
|
||||||
|
|
||||||
|
return Err(err);
|
||||||
|
} else {
|
||||||
|
order = Some(arg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => return Err(CommandError::InvalidArgument),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OptionType::Flag(name, None) => {
|
||||||
|
match name.as_str() {
|
||||||
|
"suggested" => suggested = true,
|
||||||
|
_ => return Err(CommandError::InvalidArgument),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OptionType::Positional(arg) => {
|
||||||
|
if raw_child.is_some() {
|
||||||
|
let msg = "Multiple room arguments are not allowed";
|
||||||
|
let err = CommandError::Error(msg.into());
|
||||||
|
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
raw_child = Some(arg);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = if let Some(child) = raw_child {
|
||||||
|
OwnedRoomId::from_str(&child)
|
||||||
|
.map_err(|_| CommandError::Error("Invalid room id specified".into()))?
|
||||||
|
} else {
|
||||||
|
let msg = "Must specify a room to add";
|
||||||
|
return Err(CommandError::Error(msg.into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
SpaceAction::SetChild(child, order, suggested).into()
|
||||||
|
},
|
||||||
_ => return Result::Err(CommandError::InvalidArgument),
|
_ => return Result::Err(CommandError::InvalidArgument),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -667,6 +757,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
||||||
f: iamb_rooms,
|
f: iamb_rooms,
|
||||||
});
|
});
|
||||||
cmds.add_command(ProgramCommand { name: "room".into(), aliases: vec![], f: iamb_room });
|
cmds.add_command(ProgramCommand { name: "room".into(), aliases: vec![], f: iamb_room });
|
||||||
|
cmds.add_command(ProgramCommand {
|
||||||
|
name: "space".into(),
|
||||||
|
aliases: vec![],
|
||||||
|
f: iamb_space,
|
||||||
|
});
|
||||||
cmds.add_command(ProgramCommand {
|
cmds.add_command(ProgramCommand {
|
||||||
name: "spaces".into(),
|
name: "spaces".into(),
|
||||||
aliases: vec![],
|
aliases: vec![],
|
||||||
|
@ -721,7 +816,7 @@ pub fn setup_commands() -> ProgramCommands {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use matrix_sdk::ruma::user_id;
|
use matrix_sdk::ruma::{room_id, user_id};
|
||||||
use modalkit::actions::WindowAction;
|
use modalkit::actions::WindowAction;
|
||||||
use modalkit::editing::context::EditContext;
|
use modalkit::editing::context::EditContext;
|
||||||
|
|
||||||
|
@ -1047,22 +1142,119 @@ mod tests {
|
||||||
let mut cmds = setup_commands();
|
let mut cmds = setup_commands();
|
||||||
let ctx = EditContext::default();
|
let ctx = EditContext::default();
|
||||||
|
|
||||||
let cmd = format!("room notify set mute");
|
let cmd = "room notify set mute";
|
||||||
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
|
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||||
let act = RoomAction::Set(RoomField::NotificationMode, "mute".into());
|
let act = RoomAction::Set(RoomField::NotificationMode, "mute".into());
|
||||||
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
|
|
||||||
let cmd = format!("room notify unset");
|
let cmd = "room notify unset";
|
||||||
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
|
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||||
let act = RoomAction::Unset(RoomField::NotificationMode);
|
let act = RoomAction::Unset(RoomField::NotificationMode);
|
||||||
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
|
|
||||||
let cmd = format!("room notify show");
|
let cmd = "room notify show";
|
||||||
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
|
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||||
let act = RoomAction::Show(RoomField::NotificationMode);
|
let act = RoomAction::Show(RoomField::NotificationMode);
|
||||||
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cmd_room_id_show() {
|
||||||
|
let mut cmds = setup_commands();
|
||||||
|
let ctx = EditContext::default();
|
||||||
|
|
||||||
|
let res = cmds.input_cmd("room id show", ctx.clone()).unwrap();
|
||||||
|
let act = RoomAction::Show(RoomField::Id);
|
||||||
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
|
|
||||||
|
let res = cmds.input_cmd("room id show foo", ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cmd_space_child() {
|
||||||
|
let mut cmds = setup_commands();
|
||||||
|
let ctx = EditContext::default();
|
||||||
|
|
||||||
|
let cmd = "space";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
|
||||||
|
let cmd = "space ++foo bar baz";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
|
||||||
|
let cmd = "space child foo";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cmd_space_child_set() {
|
||||||
|
let mut cmds = setup_commands();
|
||||||
|
let ctx = EditContext::default();
|
||||||
|
|
||||||
|
let cmd = "space child set !roomid:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||||
|
let act = SpaceAction::SetChild(room_id!("!roomid:example.org").to_owned(), None, false);
|
||||||
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
|
|
||||||
|
let cmd = "space child set ++order=abcd ++suggested !roomid:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||||
|
let act = SpaceAction::SetChild(
|
||||||
|
room_id!("!roomid:example.org").to_owned(),
|
||||||
|
Some("abcd".into()),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
|
|
||||||
|
let cmd = "space child set ++order=abcd ++order=1234 !roomid:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
Err(CommandError::Error("Multiple ++order arguments are not allowed".into()))
|
||||||
|
);
|
||||||
|
|
||||||
|
let cmd = "space child set !roomid:example.org !otherroom:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::Error("Multiple room arguments are not allowed".into())));
|
||||||
|
|
||||||
|
let cmd = "space child set ++foo=abcd !roomid:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
|
||||||
|
let cmd = "space child set ++foo !roomid:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
|
||||||
|
let cmd = "space child ++order=abcd ++suggested set !roomid:example.org";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
|
||||||
|
let cmd = "space child set foo";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::Error("Invalid room id specified".into())));
|
||||||
|
|
||||||
|
let cmd = "space child set";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::Error("Must specify a room to add".into())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cmd_space_child_remove() {
|
||||||
|
let mut cmds = setup_commands();
|
||||||
|
let ctx = EditContext::default();
|
||||||
|
|
||||||
|
let cmd = "space child remove";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||||
|
let act = SpaceAction::RemoveChild;
|
||||||
|
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||||
|
|
||||||
|
let cmd = "space child remove foo";
|
||||||
|
let res = cmds.input_cmd(cmd, ctx.clone());
|
||||||
|
assert_eq!(res, Err(CommandError::InvalidArgument));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cmd_invite() {
|
fn test_cmd_invite() {
|
||||||
let mut cmds = setup_commands();
|
let mut cmds = setup_commands();
|
||||||
|
|
258
src/config.rs
258
src/config.rs
|
@ -1,16 +1,17 @@
|
||||||
//! # Logic for loading and validating application configuration
|
//! # Logic for loading and validating application configuration
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::env;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::{BufReader, BufWriter};
|
use std::io::{BufReader, BufWriter, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use matrix_sdk::matrix_auth::MatrixSession;
|
use matrix_sdk::authentication::matrix::MatrixSession;
|
||||||
use matrix_sdk::ruma::{OwnedDeviceId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId};
|
use matrix_sdk::ruma::{OwnedDeviceId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId};
|
||||||
use ratatui::style::{Color, Modifier as StyleModifier, Style};
|
use ratatui::style::{Color, Modifier as StyleModifier, Style};
|
||||||
use ratatui::text::Span;
|
use ratatui::text::Span;
|
||||||
|
@ -45,8 +46,9 @@ const DEFAULT_MEMBERS_SORT: [SortColumn<SortFieldUser>; 2] = [
|
||||||
SortColumn(SortFieldUser::UserId, SortOrder::Ascending),
|
SortColumn(SortFieldUser::UserId, SortOrder::Ascending),
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_ROOM_SORT: [SortColumn<SortFieldRoom>; 4] = [
|
const DEFAULT_ROOM_SORT: [SortColumn<SortFieldRoom>; 5] = [
|
||||||
SortColumn(SortFieldRoom::Favorite, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Favorite, SortOrder::Ascending),
|
||||||
|
SortColumn(SortFieldRoom::Invite, SortOrder::Ascending),
|
||||||
SortColumn(SortFieldRoom::LowPriority, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::LowPriority, SortOrder::Ascending),
|
||||||
SortColumn(SortFieldRoom::Unread, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Unread, SortOrder::Ascending),
|
||||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||||
|
@ -97,14 +99,14 @@ fn validate_profile_name(name: &str) -> bool {
|
||||||
|
|
||||||
let mut chars = name.chars();
|
let mut chars = name.chars();
|
||||||
|
|
||||||
if !chars.next().map_or(false, |c| c.is_ascii_alphanumeric()) {
|
if !chars.next().is_some_and(|c| c.is_ascii_alphanumeric()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
name.chars().all(is_profile_char)
|
name.chars().all(is_profile_char)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_profile_names(names: &HashMap<String, ProfileConfig>) {
|
fn validate_profile_names(names: &BTreeMap<String, ProfileConfig>) {
|
||||||
for name in names.keys() {
|
for name in names.keys() {
|
||||||
if validate_profile_name(name.as_str()) {
|
if validate_profile_name(name.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -151,7 +153,7 @@ pub enum ConfigError {
|
||||||
pub struct Keys(pub Vec<TerminalKey>, pub String);
|
pub struct Keys(pub Vec<TerminalKey>, pub String);
|
||||||
pub struct KeysVisitor;
|
pub struct KeysVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for KeysVisitor {
|
impl Visitor<'_> for KeysVisitor {
|
||||||
type Value = Keys;
|
type Value = Keys;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -182,7 +184,7 @@ impl<'de> Deserialize<'de> for Keys {
|
||||||
pub struct VimModes(pub Vec<VimMode>);
|
pub struct VimModes(pub Vec<VimMode>);
|
||||||
pub struct VimModesVisitor;
|
pub struct VimModesVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for VimModesVisitor {
|
impl Visitor<'_> for VimModesVisitor {
|
||||||
type Value = VimModes;
|
type Value = VimModes;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -232,7 +234,7 @@ impl From<LogLevel> for Level {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for LogLevelVisitor {
|
impl Visitor<'_> for LogLevelVisitor {
|
||||||
type Value = LogLevel;
|
type Value = LogLevel;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -267,7 +269,7 @@ impl<'de> Deserialize<'de> for LogLevel {
|
||||||
pub struct UserColor(pub Color);
|
pub struct UserColor(pub Color);
|
||||||
pub struct UserColorVisitor;
|
pub struct UserColorVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for UserColorVisitor {
|
impl Visitor<'_> for UserColorVisitor {
|
||||||
type Value = UserColor;
|
type Value = UserColor;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
@ -321,7 +323,7 @@ pub struct Session {
|
||||||
impl From<Session> for MatrixSession {
|
impl From<Session> for MatrixSession {
|
||||||
fn from(session: Session) -> Self {
|
fn from(session: Session) -> Self {
|
||||||
MatrixSession {
|
MatrixSession {
|
||||||
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
|
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||||
access_token: session.access_token,
|
access_token: session.access_token,
|
||||||
refresh_token: session.refresh_token,
|
refresh_token: session.refresh_token,
|
||||||
},
|
},
|
||||||
|
@ -352,29 +354,31 @@ pub struct UserDisplayTunables {
|
||||||
|
|
||||||
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
|
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
|
||||||
|
|
||||||
fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides {
|
fn merge_sorts(profile: SortOverrides, global: SortOverrides) -> SortOverrides {
|
||||||
SortOverrides {
|
SortOverrides {
|
||||||
chats: b.chats.or(a.chats),
|
chats: profile.chats.or(global.chats),
|
||||||
dms: b.dms.or(a.dms),
|
dms: profile.dms.or(global.dms),
|
||||||
rooms: b.rooms.or(a.rooms),
|
rooms: profile.rooms.or(global.rooms),
|
||||||
spaces: b.spaces.or(a.spaces),
|
spaces: profile.spaces.or(global.spaces),
|
||||||
members: b.members.or(a.members),
|
members: profile.members.or(global.members),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_maps<K, V>(a: Option<HashMap<K, V>>, b: Option<HashMap<K, V>>) -> Option<HashMap<K, V>>
|
fn merge_maps<K, V>(
|
||||||
|
profile: Option<HashMap<K, V>>,
|
||||||
|
global: Option<HashMap<K, V>>,
|
||||||
|
) -> Option<HashMap<K, V>>
|
||||||
where
|
where
|
||||||
K: Eq + Hash,
|
K: Eq + Hash,
|
||||||
{
|
{
|
||||||
match (a, b) {
|
match (global, profile) {
|
||||||
(Some(a), None) => Some(a),
|
(Some(m), None) | (None, Some(m)) => Some(m),
|
||||||
(None, Some(b)) => Some(b),
|
(Some(mut global), Some(profile)) => {
|
||||||
(Some(mut a), Some(b)) => {
|
for (k, v) in profile {
|
||||||
for (k, v) in b {
|
global.insert(k, v);
|
||||||
a.insert(k, v);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(a)
|
Some(global)
|
||||||
},
|
},
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
}
|
}
|
||||||
|
@ -396,28 +400,84 @@ pub enum UserDisplayStyle {
|
||||||
// it can wind up being the Matrix username if there are display name collisions in the room,
|
// it can wind up being the Matrix username if there are display name collisions in the room,
|
||||||
// in order to avoid any confusion.
|
// in order to avoid any confusion.
|
||||||
DisplayName,
|
DisplayName,
|
||||||
|
|
||||||
|
// Acts like Username, except when the username matches given regex, then acts like DisplayName
|
||||||
|
Regex,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
pub struct NotifyVia {
|
||||||
pub enum NotifyVia {
|
|
||||||
/// Deliver notifications via terminal bell.
|
/// Deliver notifications via terminal bell.
|
||||||
Bell,
|
pub bell: bool,
|
||||||
/// Deliver notifications via desktop mechanism.
|
/// Deliver notifications via desktop mechanism.
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
Desktop,
|
pub desktop: bool,
|
||||||
}
|
}
|
||||||
|
pub struct NotifyViaVisitor;
|
||||||
|
|
||||||
impl Default for NotifyVia {
|
impl Default for NotifyVia {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
#[cfg(not(feature = "desktop"))]
|
Self {
|
||||||
return NotifyVia::Bell;
|
bell: cfg!(not(feature = "desktop")),
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
#[cfg(feature = "desktop")]
|
desktop: true,
|
||||||
return NotifyVia::Desktop;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Visitor<'_> for NotifyViaVisitor {
|
||||||
|
type Value = NotifyVia;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a valid notify destination (e.g. \"bell\" or \"desktop\")")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: SerdeError,
|
||||||
|
{
|
||||||
|
let mut via = NotifyVia {
|
||||||
|
bell: false,
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
desktop: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for value in value.split('|') {
|
||||||
|
match value.to_ascii_lowercase().as_str() {
|
||||||
|
"bell" => {
|
||||||
|
via.bell = true;
|
||||||
|
},
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
"desktop" => {
|
||||||
|
via.desktop = true;
|
||||||
|
},
|
||||||
|
#[cfg(not(feature = "desktop"))]
|
||||||
|
"desktop" => {
|
||||||
|
return Err(E::custom("desktop notification support was compiled out"))
|
||||||
|
},
|
||||||
|
_ => return Err(E::custom("could not parse into a notify destination")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(via)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for NotifyVia {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(NotifyViaVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct Mouse {
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
pub struct Notifications {
|
pub struct Notifications {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -501,29 +561,35 @@ impl SortOverrides {
|
||||||
pub struct TunableValues {
|
pub struct TunableValues {
|
||||||
pub log_level: Level,
|
pub log_level: Level,
|
||||||
pub message_shortcode_display: bool,
|
pub message_shortcode_display: bool,
|
||||||
|
pub normal_after_send: bool,
|
||||||
pub reaction_display: bool,
|
pub reaction_display: bool,
|
||||||
pub reaction_shortcode_display: bool,
|
pub reaction_shortcode_display: bool,
|
||||||
pub read_receipt_send: bool,
|
pub read_receipt_send: bool,
|
||||||
pub read_receipt_display: bool,
|
pub read_receipt_display: bool,
|
||||||
pub request_timeout: u64,
|
pub request_timeout: u64,
|
||||||
pub sort: SortValues,
|
pub sort: SortValues,
|
||||||
|
pub state_event_display: bool,
|
||||||
pub typing_notice_send: bool,
|
pub typing_notice_send: bool,
|
||||||
pub typing_notice_display: bool,
|
pub typing_notice_display: bool,
|
||||||
pub users: UserOverrides,
|
pub users: UserOverrides,
|
||||||
pub username_display: UserDisplayStyle,
|
pub username_display: UserDisplayStyle,
|
||||||
|
pub username_display_regex: Option<String>,
|
||||||
pub message_user_color: bool,
|
pub message_user_color: bool,
|
||||||
pub default_room: Option<String>,
|
pub default_room: Option<String>,
|
||||||
pub open_command: Option<Vec<String>>,
|
pub open_command: Option<Vec<String>>,
|
||||||
|
pub mouse: Mouse,
|
||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
pub image_preview: Option<ImagePreviewValues>,
|
pub image_preview: Option<ImagePreviewValues>,
|
||||||
pub user_gutter_width: usize,
|
pub user_gutter_width: usize,
|
||||||
pub external_edit_file_suffix: String,
|
pub external_edit_file_suffix: String,
|
||||||
|
pub tabstop: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub struct Tunables {
|
pub struct Tunables {
|
||||||
pub log_level: Option<LogLevel>,
|
pub log_level: Option<LogLevel>,
|
||||||
pub message_shortcode_display: Option<bool>,
|
pub message_shortcode_display: Option<bool>,
|
||||||
|
pub normal_after_send: Option<bool>,
|
||||||
pub reaction_display: Option<bool>,
|
pub reaction_display: Option<bool>,
|
||||||
pub reaction_shortcode_display: Option<bool>,
|
pub reaction_shortcode_display: Option<bool>,
|
||||||
pub read_receipt_send: Option<bool>,
|
pub read_receipt_send: Option<bool>,
|
||||||
|
@ -531,17 +597,21 @@ pub struct Tunables {
|
||||||
pub request_timeout: Option<u64>,
|
pub request_timeout: Option<u64>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sort: SortOverrides,
|
pub sort: SortOverrides,
|
||||||
|
pub state_event_display: Option<bool>,
|
||||||
pub typing_notice_send: Option<bool>,
|
pub typing_notice_send: Option<bool>,
|
||||||
pub typing_notice_display: Option<bool>,
|
pub typing_notice_display: Option<bool>,
|
||||||
pub users: Option<UserOverrides>,
|
pub users: Option<UserOverrides>,
|
||||||
pub username_display: Option<UserDisplayStyle>,
|
pub username_display: Option<UserDisplayStyle>,
|
||||||
|
pub username_display_regex: Option<String>,
|
||||||
pub message_user_color: Option<bool>,
|
pub message_user_color: Option<bool>,
|
||||||
pub default_room: Option<String>,
|
pub default_room: Option<String>,
|
||||||
pub open_command: Option<Vec<String>>,
|
pub open_command: Option<Vec<String>>,
|
||||||
|
pub mouse: Option<Mouse>,
|
||||||
pub notifications: Option<Notifications>,
|
pub notifications: Option<Notifications>,
|
||||||
pub image_preview: Option<ImagePreview>,
|
pub image_preview: Option<ImagePreview>,
|
||||||
pub user_gutter_width: Option<usize>,
|
pub user_gutter_width: Option<usize>,
|
||||||
pub external_edit_file_suffix: Option<String>,
|
pub external_edit_file_suffix: Option<String>,
|
||||||
|
pub tabstop: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tunables {
|
impl Tunables {
|
||||||
|
@ -551,6 +621,7 @@ impl Tunables {
|
||||||
message_shortcode_display: self
|
message_shortcode_display: self
|
||||||
.message_shortcode_display
|
.message_shortcode_display
|
||||||
.or(other.message_shortcode_display),
|
.or(other.message_shortcode_display),
|
||||||
|
normal_after_send: self.normal_after_send.or(other.normal_after_send),
|
||||||
reaction_display: self.reaction_display.or(other.reaction_display),
|
reaction_display: self.reaction_display.or(other.reaction_display),
|
||||||
reaction_shortcode_display: self
|
reaction_shortcode_display: self
|
||||||
.reaction_shortcode_display
|
.reaction_shortcode_display
|
||||||
|
@ -559,19 +630,23 @@ impl Tunables {
|
||||||
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
||||||
request_timeout: self.request_timeout.or(other.request_timeout),
|
request_timeout: self.request_timeout.or(other.request_timeout),
|
||||||
sort: merge_sorts(self.sort, other.sort),
|
sort: merge_sorts(self.sort, other.sort),
|
||||||
|
state_event_display: self.state_event_display.or(other.state_event_display),
|
||||||
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
|
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
|
||||||
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||||
users: merge_maps(self.users, other.users),
|
users: merge_maps(self.users, other.users),
|
||||||
username_display: self.username_display.or(other.username_display),
|
username_display: self.username_display.or(other.username_display),
|
||||||
|
username_display_regex: self.username_display_regex.or(other.username_display_regex),
|
||||||
message_user_color: self.message_user_color.or(other.message_user_color),
|
message_user_color: self.message_user_color.or(other.message_user_color),
|
||||||
default_room: self.default_room.or(other.default_room),
|
default_room: self.default_room.or(other.default_room),
|
||||||
open_command: self.open_command.or(other.open_command),
|
open_command: self.open_command.or(other.open_command),
|
||||||
|
mouse: self.mouse.or(other.mouse),
|
||||||
notifications: self.notifications.or(other.notifications),
|
notifications: self.notifications.or(other.notifications),
|
||||||
image_preview: self.image_preview.or(other.image_preview),
|
image_preview: self.image_preview.or(other.image_preview),
|
||||||
user_gutter_width: self.user_gutter_width.or(other.user_gutter_width),
|
user_gutter_width: self.user_gutter_width.or(other.user_gutter_width),
|
||||||
external_edit_file_suffix: self
|
external_edit_file_suffix: self
|
||||||
.external_edit_file_suffix
|
.external_edit_file_suffix
|
||||||
.or(other.external_edit_file_suffix),
|
.or(other.external_edit_file_suffix),
|
||||||
|
tabstop: self.tabstop.or(other.tabstop),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,25 +654,30 @@ impl Tunables {
|
||||||
TunableValues {
|
TunableValues {
|
||||||
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
||||||
message_shortcode_display: self.message_shortcode_display.unwrap_or(false),
|
message_shortcode_display: self.message_shortcode_display.unwrap_or(false),
|
||||||
|
normal_after_send: self.normal_after_send.unwrap_or(false),
|
||||||
reaction_display: self.reaction_display.unwrap_or(true),
|
reaction_display: self.reaction_display.unwrap_or(true),
|
||||||
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
||||||
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
||||||
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
||||||
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT),
|
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT),
|
||||||
sort: self.sort.values(),
|
sort: self.sort.values(),
|
||||||
|
state_event_display: self.state_event_display.unwrap_or(true),
|
||||||
typing_notice_send: self.typing_notice_send.unwrap_or(true),
|
typing_notice_send: self.typing_notice_send.unwrap_or(true),
|
||||||
typing_notice_display: self.typing_notice_display.unwrap_or(true),
|
typing_notice_display: self.typing_notice_display.unwrap_or(true),
|
||||||
users: self.users.unwrap_or_default(),
|
users: self.users.unwrap_or_default(),
|
||||||
username_display: self.username_display.unwrap_or_default(),
|
username_display: self.username_display.unwrap_or_default(),
|
||||||
|
username_display_regex: self.username_display_regex,
|
||||||
message_user_color: self.message_user_color.unwrap_or(false),
|
message_user_color: self.message_user_color.unwrap_or(false),
|
||||||
default_room: self.default_room,
|
default_room: self.default_room,
|
||||||
open_command: self.open_command,
|
open_command: self.open_command,
|
||||||
|
mouse: self.mouse.unwrap_or_default(),
|
||||||
notifications: self.notifications.unwrap_or_default(),
|
notifications: self.notifications.unwrap_or_default(),
|
||||||
image_preview: self.image_preview.map(ImagePreview::values),
|
image_preview: self.image_preview.map(ImagePreview::values),
|
||||||
user_gutter_width: self.user_gutter_width.unwrap_or(30),
|
user_gutter_width: self.user_gutter_width.unwrap_or(30),
|
||||||
external_edit_file_suffix: self
|
external_edit_file_suffix: self
|
||||||
.external_edit_file_suffix
|
.external_edit_file_suffix
|
||||||
.unwrap_or_else(|| ".md".to_string()),
|
.unwrap_or_else(|| ".md".to_string()),
|
||||||
|
tabstop: self.tabstop.unwrap_or(4),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -729,7 +809,7 @@ pub struct ProfileConfig {
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct IambConfig {
|
pub struct IambConfig {
|
||||||
pub profiles: HashMap<String, ProfileConfig>,
|
pub profiles: BTreeMap<String, ProfileConfig>,
|
||||||
pub default_profile: Option<String>,
|
pub default_profile: Option<String>,
|
||||||
pub settings: Option<Tunables>,
|
pub settings: Option<Tunables>,
|
||||||
pub dirs: Option<Directories>,
|
pub dirs: Option<Directories>,
|
||||||
|
@ -769,14 +849,22 @@ pub struct ApplicationSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationSettings {
|
impl ApplicationSettings {
|
||||||
|
fn get_xdg_config_home() -> Option<PathBuf> {
|
||||||
|
env::var("XDG_CONFIG_HOME").ok().map(PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(cli: Iamb) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(cli: Iamb) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let mut config_dir = cli.config_directory.or_else(dirs::config_dir).unwrap_or_else(|| {
|
let mut config_dir = cli
|
||||||
usage!(
|
.config_directory
|
||||||
"No user configuration directory found;\
|
.or_else(Self::get_xdg_config_home)
|
||||||
please specify one via -C.\n\n
|
.or_else(dirs::config_dir)
|
||||||
For more information try '--help'"
|
.unwrap_or_else(|| {
|
||||||
);
|
usage!(
|
||||||
});
|
"No user configuration directory found;\
|
||||||
|
please specify one via -C.\n\n
|
||||||
|
For more information try '--help'"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
config_dir.push("iamb");
|
config_dir.push("iamb");
|
||||||
let config_json = config_dir.join("config.json");
|
let config_json = config_dir.join("config.json");
|
||||||
|
@ -816,14 +904,36 @@ impl ApplicationSettings {
|
||||||
} else if profiles.len() == 1 {
|
} else if profiles.len() == 1 {
|
||||||
profiles.into_iter().next().unwrap()
|
profiles.into_iter().next().unwrap()
|
||||||
} else {
|
} else {
|
||||||
usage!(
|
loop {
|
||||||
"No profile specified. \
|
println!("\nNo profile specified. Available profiles:");
|
||||||
Please use -P or add \"default_profile\" to your configuration.\n\n\
|
profiles
|
||||||
For more information try '--help'",
|
.keys()
|
||||||
);
|
.enumerate()
|
||||||
|
.for_each(|(i, name)| println!("{}: {}", i, name));
|
||||||
|
|
||||||
|
print!("Select a number or 'q' to quit: ");
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
let _ = std::io::stdin().read_line(&mut input);
|
||||||
|
|
||||||
|
if input.trim() == "q" {
|
||||||
|
usage!(
|
||||||
|
"No profile specified. \
|
||||||
|
Please use -P or add \"default_profile\" to your configuration.\n\n\
|
||||||
|
For more information try '--help'",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Ok(i) = input.trim().parse::<usize>() {
|
||||||
|
if i < profiles.len() {
|
||||||
|
break profiles.into_iter().nth(i).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\nInvalid index.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let macros = merge_maps(macros, profile.macros.take()).unwrap_or_default();
|
let macros = merge_maps(profile.macros.take(), macros).unwrap_or_default();
|
||||||
let layout = profile.layout.take().or(layout).unwrap_or_default();
|
let layout = profile.layout.take().or(layout).unwrap_or_default();
|
||||||
|
|
||||||
let tunables = global.unwrap_or_default();
|
let tunables = global.unwrap_or_default();
|
||||||
|
@ -898,7 +1008,7 @@ impl ApplicationSettings {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_user_char_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
|
pub fn get_user_char_span(&self, user_id: &UserId) -> Span {
|
||||||
let (color, c) = self
|
let (color, c) = self
|
||||||
.tunables
|
.tunables
|
||||||
.users
|
.users
|
||||||
|
@ -958,6 +1068,20 @@ impl ApplicationSettings {
|
||||||
Cow::Borrowed(user_id.as_str())
|
Cow::Borrowed(user_id.as_str())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
(None, UserDisplayStyle::Regex) => {
|
||||||
|
let re = regex::Regex::new(
|
||||||
|
&self.tunables.username_display_regex.clone().unwrap_or("*".into()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !re.is_match(user_id.as_str()) {
|
||||||
|
Cow::Borrowed(user_id.as_str())
|
||||||
|
} else if let Some(display) = info.display_names.get(user_id) {
|
||||||
|
Cow::Borrowed(display.as_str())
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(user_id.as_str())
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Span::styled(name, style)
|
Span::styled(name, style)
|
||||||
|
@ -1022,10 +1146,10 @@ mod tests {
|
||||||
assert_eq!(res, Some(b.clone()));
|
assert_eq!(res, Some(b.clone()));
|
||||||
|
|
||||||
let res = merge_maps(Some(b.clone()), Some(c.clone()));
|
let res = merge_maps(Some(b.clone()), Some(c.clone()));
|
||||||
assert_eq!(res, Some(c.clone()));
|
assert_eq!(res, Some(b.clone()));
|
||||||
|
|
||||||
let res = merge_maps(Some(c.clone()), Some(b.clone()));
|
let res = merge_maps(Some(c.clone()), Some(b.clone()));
|
||||||
assert_eq!(res, Some(b.clone()));
|
assert_eq!(res, Some(c.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1074,6 +1198,13 @@ mod tests {
|
||||||
let res: Tunables =
|
let res: Tunables =
|
||||||
serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap();
|
serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap();
|
||||||
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
|
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
|
||||||
|
|
||||||
|
let res: Tunables = serde_json::from_str(
|
||||||
|
"{\"username_display\": \"regex\",\n\"username_display_regex\": \"foo\"}",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res.username_display, Some(UserDisplayStyle::Regex));
|
||||||
|
assert_eq!(res.username_display_regex.unwrap_or("FAILED".into()), "foo".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1189,6 +1320,29 @@ mod tests {
|
||||||
assert_eq!(run, &exp);
|
assert_eq!(run, &exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_notify_via() {
|
||||||
|
assert_eq!(NotifyVia { bell: false, desktop: true }, NotifyVia::default());
|
||||||
|
assert_eq!(
|
||||||
|
NotifyVia { bell: false, desktop: true },
|
||||||
|
serde_json::from_str(r#""desktop""#).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
NotifyVia { bell: true, desktop: false },
|
||||||
|
serde_json::from_str(r#""bell""#).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
NotifyVia { bell: true, desktop: true },
|
||||||
|
serde_json::from_str(r#""bell|desktop""#).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
NotifyVia { bell: true, desktop: true },
|
||||||
|
serde_json::from_str(r#""desktop|bell""#).unwrap()
|
||||||
|
);
|
||||||
|
assert!(serde_json::from_str::<NotifyVia>(r#""other""#).is_err());
|
||||||
|
assert!(serde_json::from_str::<NotifyVia>(r#""""#).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_example_config_toml() {
|
fn test_load_example_config_toml() {
|
||||||
let path = PathBuf::from("config.example.toml");
|
let path = PathBuf::from("config.example.toml");
|
||||||
|
|
79
src/main.rs
79
src/main.rs
|
@ -44,11 +44,14 @@ use modalkit::crossterm::{
|
||||||
read,
|
read,
|
||||||
DisableBracketedPaste,
|
DisableBracketedPaste,
|
||||||
DisableFocusChange,
|
DisableFocusChange,
|
||||||
|
DisableMouseCapture,
|
||||||
EnableBracketedPaste,
|
EnableBracketedPaste,
|
||||||
EnableFocusChange,
|
EnableFocusChange,
|
||||||
|
EnableMouseCapture,
|
||||||
Event,
|
Event,
|
||||||
KeyEventKind,
|
KeyEventKind,
|
||||||
KeyboardEnhancementFlags,
|
KeyboardEnhancementFlags,
|
||||||
|
MouseEventKind,
|
||||||
PopKeyboardEnhancementFlags,
|
PopKeyboardEnhancementFlags,
|
||||||
PushKeyboardEnhancementFlags,
|
PushKeyboardEnhancementFlags,
|
||||||
},
|
},
|
||||||
|
@ -59,7 +62,7 @@ use modalkit::crossterm::{
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::Span,
|
text::Span,
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
@ -86,6 +89,7 @@ use crate::{
|
||||||
ChatStore,
|
ChatStore,
|
||||||
HomeserverAction,
|
HomeserverAction,
|
||||||
IambAction,
|
IambAction,
|
||||||
|
IambCompleter,
|
||||||
IambError,
|
IambError,
|
||||||
IambId,
|
IambId,
|
||||||
IambInfo,
|
IambInfo,
|
||||||
|
@ -310,7 +314,7 @@ impl Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
term.draw(|f| {
|
term.draw(|f| {
|
||||||
let area = f.size();
|
let area = f.area();
|
||||||
|
|
||||||
let modestr = bindings.show_mode();
|
let modestr = bindings.show_mode();
|
||||||
let cursor = bindings.get_cursor_indicator();
|
let cursor = bindings.get_cursor_indicator();
|
||||||
|
@ -324,6 +328,9 @@ impl Application {
|
||||||
.show_dialog(dialogstr)
|
.show_dialog(dialogstr)
|
||||||
.show_mode(modestr)
|
.show_mode(modestr)
|
||||||
.borders(true)
|
.borders(true)
|
||||||
|
.border_style(Style::default().add_modifier(Modifier::DIM))
|
||||||
|
.tab_style(Style::default().add_modifier(Modifier::DIM))
|
||||||
|
.tab_style_focused(Style::default().remove_modifier(Modifier::DIM))
|
||||||
.focus(focused);
|
.focus(focused);
|
||||||
f.render_stateful_widget(screen, area, sstate);
|
f.render_stateful_widget(screen, area, sstate);
|
||||||
|
|
||||||
|
@ -339,7 +346,7 @@ impl Application {
|
||||||
let inner = Rect::new(cx, cy, 1, 1);
|
let inner = Rect::new(cx, cy, 1, 1);
|
||||||
f.render_widget(para, inner)
|
f.render_widget(para, inner)
|
||||||
}
|
}
|
||||||
f.set_cursor(cx, cy);
|
f.set_cursor_position((cx, cy));
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -364,8 +371,30 @@ impl Application {
|
||||||
|
|
||||||
return Ok(ke.into());
|
return Ok(ke.into());
|
||||||
},
|
},
|
||||||
Event::Mouse(_) => {
|
Event::Mouse(me) => {
|
||||||
// Do nothing for now.
|
let dir = match me.kind {
|
||||||
|
MouseEventKind::ScrollUp => MoveDir2D::Up,
|
||||||
|
MouseEventKind::ScrollDown => MoveDir2D::Down,
|
||||||
|
MouseEventKind::ScrollLeft => MoveDir2D::Left,
|
||||||
|
MouseEventKind::ScrollRight => MoveDir2D::Right,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = ScrollSize::Cell;
|
||||||
|
let style = ScrollStyle::Direction2D(dir, size, 1.into());
|
||||||
|
let ctx = ProgramContext::default();
|
||||||
|
let mut store = self.store.lock().await;
|
||||||
|
|
||||||
|
match self.screen.scroll(&style, &ctx, store.deref_mut()) {
|
||||||
|
Ok(None) => {},
|
||||||
|
Ok(Some(info)) => {
|
||||||
|
drop(store);
|
||||||
|
self.handle_info(info);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
self.screen.push_error(e);
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Event::FocusGained => {
|
Event::FocusGained => {
|
||||||
let mut store = self.store.lock().await;
|
let mut store = self.store.lock().await;
|
||||||
|
@ -504,7 +533,7 @@ impl Application {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Unimplemented.
|
// Unimplemented.
|
||||||
Action::KeywordLookup => {
|
Action::KeywordLookup(_) => {
|
||||||
// XXX: implement
|
// XXX: implement
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
@ -532,9 +561,12 @@ impl Application {
|
||||||
IambAction::ClearUnreads => {
|
IambAction::ClearUnreads => {
|
||||||
let user_id = &store.application.settings.profile.user_id;
|
let user_id = &store.application.settings.profile.user_id;
|
||||||
|
|
||||||
|
// Clear any notifications we displayed:
|
||||||
|
store.application.open_notifications.clear();
|
||||||
|
|
||||||
for room_id in store.application.sync_info.chats() {
|
for room_id in store.application.sync_info.chats() {
|
||||||
if let Some(room) = store.application.rooms.get_mut(room_id) {
|
if let Some(room) = store.application.rooms.get_mut(room_id) {
|
||||||
room.fully_read(user_id.clone());
|
room.fully_read(user_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,6 +589,9 @@ impl Application {
|
||||||
IambAction::Message(act) => {
|
IambAction::Message(act) => {
|
||||||
self.screen.current_window_mut()?.message_command(act, ctx, store).await?
|
self.screen.current_window_mut()?.message_command(act, ctx, store).await?
|
||||||
},
|
},
|
||||||
|
IambAction::Space(act) => {
|
||||||
|
self.screen.current_window_mut()?.space_command(act, ctx, store).await?
|
||||||
|
},
|
||||||
IambAction::Room(act) => {
|
IambAction::Room(act) => {
|
||||||
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store).await?;
|
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store).await?;
|
||||||
self.action_prepend(acts);
|
self.action_prepend(acts);
|
||||||
|
@ -564,6 +599,9 @@ impl Application {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
IambAction::Send(act) => {
|
IambAction::Send(act) => {
|
||||||
|
if store.application.settings.tunables.normal_after_send {
|
||||||
|
self.bindings.reset_mode();
|
||||||
|
}
|
||||||
self.screen.current_window_mut()?.send_command(act, ctx, store).await?
|
self.screen.current_window_mut()?.send_command(act, ctx, store).await?
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -847,7 +885,7 @@ async fn check_import_keys(
|
||||||
let encrypted = match encrypt_room_key_export(&keys, &passphrase, 500000) {
|
let encrypted = match encrypt_room_key_export(&keys, &passphrase, 500000) {
|
||||||
Ok(encrypted) => encrypted,
|
Ok(encrypted) => encrypted,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
format!("* Failed to encrypt room keys during export: {e}");
|
println!("* Failed to encrypt room keys during export: {e}");
|
||||||
process::exit(2);
|
process::exit(2);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -929,8 +967,8 @@ async fn login_normal(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set up the terminal for drawing the TUI, and getting additional info.
|
/// Set up the terminal for drawing the TUI, and getting additional info.
|
||||||
fn setup_tty(title: &str, enable_enhanced_keys: bool) -> std::io::Result<()> {
|
fn setup_tty(settings: &ApplicationSettings, enable_enhanced_keys: bool) -> std::io::Result<()> {
|
||||||
let title = format!("iamb ({})", title);
|
let title = format!("iamb ({})", settings.profile.user_id.as_str());
|
||||||
|
|
||||||
// Enable raw mode and enter the alternate screen.
|
// Enable raw mode and enter the alternate screen.
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
@ -944,15 +982,23 @@ fn setup_tty(title: &str, enable_enhanced_keys: bool) -> std::io::Result<()> {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.tunables.mouse.enabled {
|
||||||
|
crossterm::execute!(stdout(), EnableMouseCapture)?;
|
||||||
|
}
|
||||||
|
|
||||||
crossterm::execute!(stdout(), EnableBracketedPaste, EnableFocusChange, SetTitle(title))
|
crossterm::execute!(stdout(), EnableBracketedPaste, EnableFocusChange, SetTitle(title))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do our best to reverse what we did in setup_tty() when we exit or crash.
|
// Do our best to reverse what we did in setup_tty() when we exit or crash.
|
||||||
fn restore_tty(enable_enhanced_keys: bool) {
|
fn restore_tty(enable_enhanced_keys: bool, enable_mouse: bool) {
|
||||||
if enable_enhanced_keys {
|
if enable_enhanced_keys {
|
||||||
let _ = crossterm::queue!(stdout(), PopKeyboardEnhancementFlags);
|
let _ = crossterm::queue!(stdout(), PopKeyboardEnhancementFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if enable_mouse {
|
||||||
|
let _ = crossterm::queue!(stdout(), DisableMouseCapture);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = crossterm::execute!(
|
let _ = crossterm::execute!(
|
||||||
stdout(),
|
stdout(),
|
||||||
DisableBracketedPaste,
|
DisableBracketedPaste,
|
||||||
|
@ -975,7 +1021,9 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
||||||
// Set up the async worker thread and global store.
|
// Set up the async worker thread and global store.
|
||||||
let worker = ClientWorker::spawn(client.clone(), settings.clone()).await;
|
let worker = ClientWorker::spawn(client.clone(), settings.clone()).await;
|
||||||
let store = ChatStore::new(worker.clone(), settings.clone());
|
let store = ChatStore::new(worker.clone(), settings.clone());
|
||||||
let store = Store::new(store);
|
let mut store = Store::new(store);
|
||||||
|
store.completer = Box::new(IambCompleter);
|
||||||
|
|
||||||
let store = Arc::new(AsyncMutex::new(store));
|
let store = Arc::new(AsyncMutex::new(store));
|
||||||
worker.init(store.clone());
|
worker.init(store.clone());
|
||||||
|
|
||||||
|
@ -1006,11 +1054,12 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
||||||
false
|
false
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
setup_tty(settings.profile.user_id.as_str(), enable_enhanced_keys)?;
|
setup_tty(&settings, enable_enhanced_keys)?;
|
||||||
|
|
||||||
let orig_hook = std::panic::take_hook();
|
let orig_hook = std::panic::take_hook();
|
||||||
|
let enable_mouse = settings.tunables.mouse.enabled;
|
||||||
std::panic::set_hook(Box::new(move |panic_info| {
|
std::panic::set_hook(Box::new(move |panic_info| {
|
||||||
restore_tty(enable_enhanced_keys);
|
restore_tty(enable_enhanced_keys, enable_mouse);
|
||||||
orig_hook(panic_info);
|
orig_hook(panic_info);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}));
|
}));
|
||||||
|
@ -1020,7 +1069,7 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
||||||
application.run().await?;
|
application.run().await?;
|
||||||
|
|
||||||
// Clean up the terminal on exit.
|
// Clean up the terminal on exit.
|
||||||
restore_tty(enable_enhanced_keys);
|
restore_tty(enable_enhanced_keys, enable_mouse);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,12 @@
|
||||||
//!
|
//!
|
||||||
//! This isn't as important for iamb, since it isn't a browser environment, but we do still map
|
//! This isn't as important for iamb, since it isn't a browser environment, but we do still map
|
||||||
//! input onto an enum of the safe list of tags to keep it easy to understand and process.
|
//! input onto an enum of the safe list of tags to keep it easy to understand and process.
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use css_color_parser::Color as CssColor;
|
use css_color_parser::Color as CssColor;
|
||||||
use markup5ever_rcdom::{Handle, NodeData, RcDom};
|
use markup5ever_rcdom::{Handle, NodeData, RcDom};
|
||||||
|
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId, OwnedUserId};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -34,10 +36,13 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::ApplicationSettings,
|
||||||
message::printer::TextPrinter,
|
message::printer::TextPrinter,
|
||||||
util::{join_cell_text, space_text},
|
util::{join_cell_text, space_text},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const QUOTE_COLOR: Color = Color::Indexed(236);
|
||||||
|
|
||||||
/// Generate bullet points from a [ListStyle].
|
/// Generate bullet points from a [ListStyle].
|
||||||
pub struct BulletIterator {
|
pub struct BulletIterator {
|
||||||
style: ListStyle,
|
style: ListStyle,
|
||||||
|
@ -148,7 +153,12 @@ impl Table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_text(&self, width: usize, style: Style, emoji_shortcodes: bool) -> Text {
|
fn to_text<'a>(
|
||||||
|
&'a self,
|
||||||
|
width: usize,
|
||||||
|
style: Style,
|
||||||
|
settings: &'a ApplicationSettings,
|
||||||
|
) -> Text<'a> {
|
||||||
let mut text = Text::default();
|
let mut text = Text::default();
|
||||||
let columns = self.columns();
|
let columns = self.columns();
|
||||||
let cell_total = width.saturating_sub(columns).saturating_sub(1);
|
let cell_total = width.saturating_sub(columns).saturating_sub(1);
|
||||||
|
@ -167,7 +177,7 @@ impl Table {
|
||||||
if let Some(caption) = &self.caption {
|
if let Some(caption) = &self.caption {
|
||||||
let subw = width.saturating_sub(6);
|
let subw = width.saturating_sub(6);
|
||||||
let mut printer =
|
let mut printer =
|
||||||
TextPrinter::new(subw, style, true, emoji_shortcodes).align(Alignment::Center);
|
TextPrinter::new(subw, style, true, settings).align(Alignment::Center);
|
||||||
caption.print(&mut printer, style);
|
caption.print(&mut printer, style);
|
||||||
|
|
||||||
for mut line in printer.finish().lines {
|
for mut line in printer.finish().lines {
|
||||||
|
@ -214,7 +224,7 @@ impl Table {
|
||||||
CellType::Data => style,
|
CellType::Data => style,
|
||||||
};
|
};
|
||||||
|
|
||||||
cell.to_text(*w, style, emoji_shortcodes)
|
cell.to_text(*w, style, settings)
|
||||||
} else {
|
} else {
|
||||||
space_text(*w, style)
|
space_text(*w, style)
|
||||||
};
|
};
|
||||||
|
@ -271,13 +281,22 @@ pub enum StyleTreeNode {
|
||||||
Ruler,
|
Ruler,
|
||||||
Style(Box<StyleTreeNode>, Style),
|
Style(Box<StyleTreeNode>, Style),
|
||||||
Table(Table),
|
Table(Table),
|
||||||
Text(String),
|
Text(Cow<'static, str>),
|
||||||
Sequence(StyleTreeChildren),
|
Sequence(StyleTreeChildren),
|
||||||
|
RoomAlias(OwnedRoomAliasId),
|
||||||
|
RoomId(OwnedRoomId),
|
||||||
|
UserId(OwnedUserId),
|
||||||
|
DisplayName(String, OwnedUserId),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyleTreeNode {
|
impl StyleTreeNode {
|
||||||
pub fn to_text(&self, width: usize, style: Style, emoji_shortcodes: bool) -> Text {
|
pub fn to_text<'a>(
|
||||||
let mut printer = TextPrinter::new(width, style, true, emoji_shortcodes);
|
&'a self,
|
||||||
|
width: usize,
|
||||||
|
style: Style,
|
||||||
|
settings: &'a ApplicationSettings,
|
||||||
|
) -> Text<'a> {
|
||||||
|
let mut printer = TextPrinter::new(width, style, true, settings);
|
||||||
self.print(&mut printer, style);
|
self.print(&mut printer, style);
|
||||||
printer.finish()
|
printer.finish()
|
||||||
}
|
}
|
||||||
|
@ -312,6 +331,12 @@ impl StyleTreeNode {
|
||||||
StyleTreeNode::Ruler => {},
|
StyleTreeNode::Ruler => {},
|
||||||
StyleTreeNode::Text(_) => {},
|
StyleTreeNode::Text(_) => {},
|
||||||
StyleTreeNode::Break => {},
|
StyleTreeNode::Break => {},
|
||||||
|
|
||||||
|
// TODO: eventually these should turn into internal links:
|
||||||
|
StyleTreeNode::UserId(_) => {},
|
||||||
|
StyleTreeNode::RoomId(_) => {},
|
||||||
|
StyleTreeNode::RoomAlias(_) => {},
|
||||||
|
StyleTreeNode::DisplayName(_, _) => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,11 +353,14 @@ impl StyleTreeNode {
|
||||||
printer.push_span_nobreak(span);
|
printer.push_span_nobreak(span);
|
||||||
},
|
},
|
||||||
StyleTreeNode::Blockquote(child) => {
|
StyleTreeNode::Blockquote(child) => {
|
||||||
let mut subp = printer.sub(4);
|
let mut subp = printer.sub(3);
|
||||||
child.print(&mut subp, style);
|
child.print(&mut subp, style);
|
||||||
|
|
||||||
for mut line in subp.finish() {
|
for mut line in subp.finish() {
|
||||||
line.spans.insert(0, Span::styled(" ", style));
|
line.spans.insert(0, Span::styled(" ", style));
|
||||||
|
line.spans
|
||||||
|
.insert(0, Span::styled(line::THICK_VERTICAL, style.fg(QUOTE_COLOR)));
|
||||||
|
line.spans.insert(0, Span::styled(" ", style));
|
||||||
printer.push_line(line);
|
printer.push_line(line);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -430,14 +458,14 @@ impl StyleTreeNode {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
StyleTreeNode::Table(table) => {
|
StyleTreeNode::Table(table) => {
|
||||||
let text = table.to_text(width, style, printer.emoji_shortcodes());
|
let text = table.to_text(width, style, printer.settings);
|
||||||
printer.push_text(text);
|
printer.push_text(text);
|
||||||
},
|
},
|
||||||
StyleTreeNode::Break => {
|
StyleTreeNode::Break => {
|
||||||
printer.push_break();
|
printer.push_break();
|
||||||
},
|
},
|
||||||
StyleTreeNode::Text(s) => {
|
StyleTreeNode::Text(s) => {
|
||||||
printer.push_str(s.as_str(), style);
|
printer.push_str(s.as_ref(), style);
|
||||||
},
|
},
|
||||||
|
|
||||||
StyleTreeNode::Style(child, patch) => child.print(printer, style.patch(*patch)),
|
StyleTreeNode::Style(child, patch) => child.print(printer, style.patch(*patch)),
|
||||||
|
@ -446,13 +474,30 @@ impl StyleTreeNode {
|
||||||
child.print(printer, style);
|
child.print(printer, style);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
StyleTreeNode::UserId(user_id) => {
|
||||||
|
let style = printer.settings().get_user_style(user_id);
|
||||||
|
printer.push_str(user_id.as_str(), style);
|
||||||
|
},
|
||||||
|
StyleTreeNode::DisplayName(display_name, user_id) => {
|
||||||
|
let style = printer.settings().get_user_style(user_id);
|
||||||
|
printer.push_str(display_name.as_str(), style);
|
||||||
|
},
|
||||||
|
StyleTreeNode::RoomId(room_id) => {
|
||||||
|
let bold = style.add_modifier(StyleModifier::BOLD);
|
||||||
|
printer.push_str(room_id.as_str(), bold);
|
||||||
|
},
|
||||||
|
StyleTreeNode::RoomAlias(alias) => {
|
||||||
|
let bold = style.add_modifier(StyleModifier::BOLD);
|
||||||
|
printer.push_str(alias.as_str(), bold);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A processed HTML document.
|
/// A processed HTML document.
|
||||||
pub struct StyleTree {
|
pub struct StyleTree {
|
||||||
children: StyleTreeChildren,
|
pub(super) children: StyleTreeChildren,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyleTree {
|
impl StyleTree {
|
||||||
|
@ -466,14 +511,14 @@ impl StyleTree {
|
||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_text(
|
pub fn to_text<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
width: usize,
|
width: usize,
|
||||||
style: Style,
|
style: Style,
|
||||||
hide_reply: bool,
|
hide_reply: bool,
|
||||||
emoji_shortcodes: bool,
|
settings: &'a ApplicationSettings,
|
||||||
) -> Text<'_> {
|
) -> Text<'a> {
|
||||||
let mut printer = TextPrinter::new(width, style, hide_reply, emoji_shortcodes);
|
let mut printer = TextPrinter::new(width, style, hide_reply, settings);
|
||||||
|
|
||||||
for child in self.children.iter() {
|
for child in self.children.iter() {
|
||||||
child.print(&mut printer, style);
|
child.print(&mut printer, style);
|
||||||
|
@ -484,11 +529,11 @@ impl StyleTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TreeGenState {
|
pub struct TreeGenState {
|
||||||
link_num: u8,
|
pub link_num: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TreeGenState {
|
impl TreeGenState {
|
||||||
fn next_link_char(&mut self) -> Option<char> {
|
pub fn next_link_char(&mut self) -> Option<char> {
|
||||||
let num = self.link_num;
|
let num = self.link_num;
|
||||||
|
|
||||||
if num < 62 {
|
if num < 62 {
|
||||||
|
@ -661,7 +706,7 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
|
||||||
|
|
||||||
let tree = match &node.data {
|
let tree = match &node.data {
|
||||||
NodeData::Document => *c2t(node.children.borrow().as_slice(), state),
|
NodeData::Document => *c2t(node.children.borrow().as_slice(), state),
|
||||||
NodeData::Text { contents } => StyleTreeNode::Text(contents.borrow().to_string()),
|
NodeData::Text { contents } => StyleTreeNode::Text(contents.borrow().to_string().into()),
|
||||||
NodeData::Element { name, attrs, .. } => {
|
NodeData::Element { name, attrs, .. } => {
|
||||||
match name.local.as_ref() {
|
match name.local.as_ref() {
|
||||||
// Message that this one replies to.
|
// Message that this one replies to.
|
||||||
|
@ -708,7 +753,7 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
|
||||||
|
|
||||||
StyleTreeNode::Style(c, s)
|
StyleTreeNode::Style(c, s)
|
||||||
},
|
},
|
||||||
"del" | "strike" => {
|
"del" | "s" | "strike" => {
|
||||||
let c = c2t(&node.children.borrow(), state);
|
let c = c2t(&node.children.borrow(), state);
|
||||||
let s = Style::default().add_modifier(StyleModifier::CROSSED_OUT);
|
let s = Style::default().add_modifier(StyleModifier::CROSSED_OUT);
|
||||||
|
|
||||||
|
@ -811,17 +856,19 @@ pub fn parse_matrix_html(s: &str) -> StyleTree {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::tests::mock_settings;
|
||||||
use crate::util::space_span;
|
use crate::util::space_span;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_header() {
|
fn test_header() {
|
||||||
|
let settings = mock_settings();
|
||||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||||
|
|
||||||
let s = "<h1>Header 1</h1>";
|
let s = "<h1>Header 1</h1>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
Span::styled(" ", bold),
|
Span::styled(" ", bold),
|
||||||
|
@ -833,7 +880,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<h2>Header 2</h2>";
|
let s = "<h2>Header 2</h2>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
|
@ -846,7 +893,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<h3>Header 3</h3>";
|
let s = "<h3>Header 3</h3>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
|
@ -860,7 +907,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<h4>Header 4</h4>";
|
let s = "<h4>Header 4</h4>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
|
@ -875,7 +922,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<h5>Header 5</h5>";
|
let s = "<h5>Header 5</h5>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
|
@ -891,7 +938,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<h6>Header 6</h6>";
|
let s = "<h6>Header 6</h6>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
Span::styled("#", bold),
|
Span::styled("#", bold),
|
||||||
|
@ -909,6 +956,7 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_style() {
|
fn test_style() {
|
||||||
|
let settings = mock_settings();
|
||||||
let def = Style::default();
|
let def = Style::default();
|
||||||
let bold = def.add_modifier(StyleModifier::BOLD);
|
let bold = def.add_modifier(StyleModifier::BOLD);
|
||||||
let italic = def.add_modifier(StyleModifier::ITALIC);
|
let italic = def.add_modifier(StyleModifier::ITALIC);
|
||||||
|
@ -918,7 +966,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<b>Bold!</b>";
|
let s = "<b>Bold!</b>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Bold", bold),
|
Span::styled("Bold", bold),
|
||||||
Span::styled("!", bold),
|
Span::styled("!", bold),
|
||||||
|
@ -927,7 +975,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<strong>Bold!</strong>";
|
let s = "<strong>Bold!</strong>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Bold", bold),
|
Span::styled("Bold", bold),
|
||||||
Span::styled("!", bold),
|
Span::styled("!", bold),
|
||||||
|
@ -936,7 +984,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<i>Italic!</i>";
|
let s = "<i>Italic!</i>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Italic", italic),
|
Span::styled("Italic", italic),
|
||||||
Span::styled("!", italic),
|
Span::styled("!", italic),
|
||||||
|
@ -945,7 +993,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<em>Italic!</em>";
|
let s = "<em>Italic!</em>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Italic", italic),
|
Span::styled("Italic", italic),
|
||||||
Span::styled("!", italic),
|
Span::styled("!", italic),
|
||||||
|
@ -954,7 +1002,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<del>Strikethrough!</del>";
|
let s = "<del>Strikethrough!</del>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Strikethrough", strike),
|
Span::styled("Strikethrough", strike),
|
||||||
Span::styled("!", strike),
|
Span::styled("!", strike),
|
||||||
|
@ -963,7 +1011,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<strike>Strikethrough!</strike>";
|
let s = "<strike>Strikethrough!</strike>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Strikethrough", strike),
|
Span::styled("Strikethrough", strike),
|
||||||
Span::styled("!", strike),
|
Span::styled("!", strike),
|
||||||
|
@ -972,7 +1020,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<u>Underline!</u>";
|
let s = "<u>Underline!</u>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Underline", underl),
|
Span::styled("Underline", underl),
|
||||||
Span::styled("!", underl),
|
Span::styled("!", underl),
|
||||||
|
@ -981,7 +1029,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<font color=\"#ff0000\">Red!</u>";
|
let s = "<font color=\"#ff0000\">Red!</u>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Red", red),
|
Span::styled("Red", red),
|
||||||
Span::styled("!", red),
|
Span::styled("!", red),
|
||||||
|
@ -990,7 +1038,7 @@ pub mod tests {
|
||||||
|
|
||||||
let s = "<font color=\"red\">Red!</u>";
|
let s = "<font color=\"red\">Red!</u>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::styled("Red", red),
|
Span::styled("Red", red),
|
||||||
Span::styled("!", red),
|
Span::styled("!", red),
|
||||||
|
@ -1000,9 +1048,10 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_paragraph() {
|
fn test_paragraph() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<p>Hello world!</p><p>Content</p><p>Goodbye world!</p>";
|
let s = "<p>Hello world!</p><p>Content</p><p>Goodbye world!</p>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(10, Style::default(), false, false);
|
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines.len(), 7);
|
assert_eq!(text.lines.len(), 7);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
|
@ -1027,25 +1076,42 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_blockquote() {
|
fn test_blockquote() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<blockquote>Hello world!</blockquote>";
|
let s = "<blockquote>Hello world!</blockquote>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(10, Style::default(), false, false);
|
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||||
|
let style = Style::new().fg(QUOTE_COLOR);
|
||||||
assert_eq!(text.lines.len(), 2);
|
assert_eq!(text.lines.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
Line::from(vec![Span::raw(" "), Span::raw("Hello"), Span::raw(" ")])
|
Line::from(vec![
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(line::THICK_VERTICAL, style),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw("Hello"),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw(" "),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[1],
|
text.lines[1],
|
||||||
Line::from(vec![Span::raw(" "), Span::raw("world"), Span::raw("!")])
|
Line::from(vec![
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(line::THICK_VERTICAL, style),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw("world"),
|
||||||
|
Span::raw("!"),
|
||||||
|
Span::raw(" "),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_unordered() {
|
fn test_list_unordered() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<ul><li>List Item 1</li><li>List Item 2</li><li>List Item 3</li></ul>";
|
let s = "<ul><li>List Item 1</li><li>List Item 2</li><li>List Item 3</li></ul>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(8, Style::default(), false, false);
|
let text = tree.to_text(8, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines.len(), 6);
|
assert_eq!(text.lines.len(), 6);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
|
@ -1105,9 +1171,10 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_list_ordered() {
|
fn test_list_ordered() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<ol><li>List Item 1</li><li>List Item 2</li><li>List Item 3</li></ol>";
|
let s = "<ol><li>List Item 1</li><li>List Item 2</li><li>List Item 3</li></ol>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(9, Style::default(), false, false);
|
let text = tree.to_text(9, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines.len(), 6);
|
assert_eq!(text.lines.len(), 6);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
|
@ -1167,6 +1234,7 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_table() {
|
fn test_table() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<table>\
|
let s = "<table>\
|
||||||
<thead>\
|
<thead>\
|
||||||
<tr><th>Column 1</th><th>Column 2</th><th>Column 3</th></tr>
|
<tr><th>Column 1</th><th>Column 2</th><th>Column 3</th></tr>
|
||||||
|
@ -1177,7 +1245,7 @@ pub mod tests {
|
||||||
<tr><td>a</td><td>b</td><td>c</td></tr>\
|
<tr><td>a</td><td>b</td><td>c</td></tr>\
|
||||||
</tbody></table>";
|
</tbody></table>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(15, Style::default(), false, false);
|
let text = tree.to_text(15, Style::default(), false, &settings);
|
||||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||||
assert_eq!(text.lines.len(), 11);
|
assert_eq!(text.lines.len(), 11);
|
||||||
|
|
||||||
|
@ -1267,10 +1335,11 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_matrix_reply() {
|
fn test_matrix_reply() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<mx-reply>This was replied to</mx-reply>This is the reply";
|
let s = "<mx-reply>This was replied to</mx-reply>This is the reply";
|
||||||
|
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(10, Style::default(), false, false);
|
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||||
assert_eq!(text.lines.len(), 4);
|
assert_eq!(text.lines.len(), 4);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
|
@ -1307,7 +1376,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(10, Style::default(), true, false);
|
let text = tree.to_text(10, Style::default(), true, &settings);
|
||||||
assert_eq!(text.lines.len(), 2);
|
assert_eq!(text.lines.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
|
@ -1332,9 +1401,10 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_self_closing() {
|
fn test_self_closing() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "Hello<br>World<br>Goodbye";
|
let s = "Hello<br>World<br>Goodbye";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(7, Style::default(), true, false);
|
let text = tree.to_text(7, Style::default(), true, &settings);
|
||||||
assert_eq!(text.lines.len(), 3);
|
assert_eq!(text.lines.len(), 3);
|
||||||
assert_eq!(text.lines[0], Line::from(vec![Span::raw("Hello"), Span::raw(" "),]));
|
assert_eq!(text.lines[0], Line::from(vec![Span::raw("Hello"), Span::raw(" "),]));
|
||||||
assert_eq!(text.lines[1], Line::from(vec![Span::raw("World"), Span::raw(" "),]));
|
assert_eq!(text.lines[1], Line::from(vec![Span::raw("World"), Span::raw(" "),]));
|
||||||
|
@ -1343,9 +1413,10 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_embedded_newline() {
|
fn test_embedded_newline() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = "<p>Hello\nWorld</p>";
|
let s = "<p>Hello\nWorld</p>";
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(15, Style::default(), true, false);
|
let text = tree.to_text(15, Style::default(), true, &settings);
|
||||||
assert_eq!(text.lines.len(), 1);
|
assert_eq!(text.lines.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
|
@ -1360,16 +1431,18 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pre_tag() {
|
fn test_pre_tag() {
|
||||||
|
let settings = mock_settings();
|
||||||
let s = concat!(
|
let s = concat!(
|
||||||
"<pre><code class=\"language-rust\">",
|
"<pre><code class=\"language-rust\">",
|
||||||
"fn hello() -> usize {\n",
|
"fn hello() -> usize {\n",
|
||||||
|
" \t// weired\n",
|
||||||
" return 5;\n",
|
" return 5;\n",
|
||||||
"}\n",
|
"}\n",
|
||||||
"</code></pre>\n"
|
"</code></pre>\n"
|
||||||
);
|
);
|
||||||
let tree = parse_matrix_html(s);
|
let tree = parse_matrix_html(s);
|
||||||
let text = tree.to_text(25, Style::default(), true, false);
|
let text = tree.to_text(25, Style::default(), true, &settings);
|
||||||
assert_eq!(text.lines.len(), 5);
|
assert_eq!(text.lines.len(), 6);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[0],
|
text.lines[0],
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
|
@ -1400,6 +1473,20 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[2],
|
text.lines[2],
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw(line::VERTICAL),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw("/"),
|
||||||
|
Span::raw("/"),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw("weired"),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::raw(line::VERTICAL)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
text.lines[3],
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(line::VERTICAL),
|
Span::raw(line::VERTICAL),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
|
@ -1412,7 +1499,7 @@ pub mod tests {
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[3],
|
text.lines[4],
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(line::VERTICAL),
|
Span::raw(line::VERTICAL),
|
||||||
Span::raw("}"),
|
Span::raw("}"),
|
||||||
|
@ -1421,7 +1508,7 @@ pub mod tests {
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.lines[4],
|
text.lines[5],
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(line::BOTTOM_LEFT),
|
Span::raw(line::BOTTOM_LEFT),
|
||||||
Span::raw(line::HORIZONTAL.repeat(23)),
|
Span::raw(line::HORIZONTAL.repeat(23)),
|
||||||
|
@ -1432,6 +1519,11 @@ pub mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_emoji_shortcodes() {
|
fn test_emoji_shortcodes() {
|
||||||
|
let mut enabled = mock_settings();
|
||||||
|
enabled.tunables.message_shortcode_display = true;
|
||||||
|
let mut disabled = mock_settings();
|
||||||
|
disabled.tunables.message_shortcode_display = false;
|
||||||
|
|
||||||
for shortcode in ["exploding_head", "polar_bear", "canada"] {
|
for shortcode in ["exploding_head", "polar_bear", "canada"] {
|
||||||
let emoji = emojis::get_by_shortcode(shortcode).unwrap().as_str();
|
let emoji = emojis::get_by_shortcode(shortcode).unwrap().as_str();
|
||||||
let emoji_width = UnicodeWidthStr::width(emoji);
|
let emoji_width = UnicodeWidthStr::width(emoji);
|
||||||
|
@ -1440,13 +1532,13 @@ pub mod tests {
|
||||||
let s = format!("<p>{emoji}</p>");
|
let s = format!("<p>{emoji}</p>");
|
||||||
let tree = parse_matrix_html(s.as_str());
|
let tree = parse_matrix_html(s.as_str());
|
||||||
// Test with emojis_shortcodes set to false
|
// Test with emojis_shortcodes set to false
|
||||||
let text = tree.to_text(20, Style::default(), false, false);
|
let text = tree.to_text(20, Style::default(), false, &disabled);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::raw(emoji),
|
Span::raw(emoji),
|
||||||
space_span(20 - emoji_width, Style::default()),
|
space_span(20 - emoji_width, Style::default()),
|
||||||
]),]);
|
]),]);
|
||||||
// Test with emojis_shortcodes set to true
|
// Test with emojis_shortcodes set to true
|
||||||
let text = tree.to_text(20, Style::default(), false, true);
|
let text = tree.to_text(20, Style::default(), false, &enabled);
|
||||||
assert_eq!(text.lines, vec![Line::from(vec![
|
assert_eq!(text.lines, vec![Line::from(vec![
|
||||||
Span::raw(replacement.as_str()),
|
Span::raw(replacement.as_str()),
|
||||||
space_span(20 - replacement_width, Style::default()),
|
space_span(20 - replacement_width, Style::default()),
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::collections::hash_set;
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use chrono::{DateTime, Local as LocalTz, NaiveDateTime, TimeZone};
|
use chrono::{DateTime, Local as LocalTz};
|
||||||
use humansize::{format_size, DECIMAL};
|
use humansize::{format_size, DECIMAL};
|
||||||
|
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ use matrix_sdk::ruma::{
|
||||||
},
|
},
|
||||||
redaction::SyncRoomRedactionEvent,
|
redaction::SyncRoomRedactionEvent,
|
||||||
},
|
},
|
||||||
|
AnySyncStateEvent,
|
||||||
RedactContent,
|
RedactContent,
|
||||||
RedactedUnsigned,
|
RedactedUnsigned,
|
||||||
},
|
},
|
||||||
|
@ -67,13 +68,17 @@ use crate::{
|
||||||
mod compose;
|
mod compose;
|
||||||
mod html;
|
mod html;
|
||||||
mod printer;
|
mod printer;
|
||||||
|
mod state;
|
||||||
|
|
||||||
pub use self::compose::text_to_message;
|
pub use self::compose::text_to_message;
|
||||||
|
use self::state::{body_cow_state, html_state};
|
||||||
|
pub use html::TreeGenState;
|
||||||
|
|
||||||
|
type ProtocolPreview<'a> = (&'a Protocol, u16, u16);
|
||||||
|
|
||||||
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
pub type MessageKey = (MessageTimeStamp, OwnedEventId);
|
||||||
|
|
||||||
#[derive(Default)]
|
pub struct Messages(BTreeMap<MessageKey, Message>, pub ReceiptThread);
|
||||||
pub struct Messages(BTreeMap<MessageKey, Message>);
|
|
||||||
|
|
||||||
impl Deref for Messages {
|
impl Deref for Messages {
|
||||||
type Target = BTreeMap<MessageKey, Message>;
|
type Target = BTreeMap<MessageKey, Message>;
|
||||||
|
@ -90,6 +95,18 @@ impl DerefMut for Messages {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Messages {
|
impl Messages {
|
||||||
|
pub fn new(thread: ReceiptThread) -> Self {
|
||||||
|
Self(Default::default(), thread)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> Self {
|
||||||
|
Self::new(ReceiptThread::Main)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thread(root: OwnedEventId) -> Self {
|
||||||
|
Self::new(ReceiptThread::Thread(root))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_message(&mut self, key: MessageKey, msg: impl Into<Message>) {
|
pub fn insert_message(&mut self, key: MessageKey, msg: impl Into<Message>) {
|
||||||
let event_id = key.1.clone();
|
let event_id = key.1.clone();
|
||||||
let msg = msg.into();
|
let msg = msg.into();
|
||||||
|
@ -160,7 +177,9 @@ fn placeholder_frame(
|
||||||
}
|
}
|
||||||
let mut placeholder = "\u{230c}".to_string();
|
let mut placeholder = "\u{230c}".to_string();
|
||||||
placeholder.push_str(&" ".repeat(width - 2));
|
placeholder.push_str(&" ".repeat(width - 2));
|
||||||
placeholder.push_str("\u{230d}\n");
|
placeholder.push('\u{230d}');
|
||||||
|
placeholder.push_str(&"\n".repeat((height - 1) / 2));
|
||||||
|
|
||||||
if *height > 2 {
|
if *height > 2 {
|
||||||
if let Some(text) = text {
|
if let Some(text) = text {
|
||||||
if text.width() <= width - 2 {
|
if text.width() <= width - 2 {
|
||||||
|
@ -170,7 +189,7 @@ fn placeholder_frame(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
placeholder.push_str(&"\n".repeat(height - 2));
|
placeholder.push_str(&"\n".repeat(height / 2));
|
||||||
placeholder.push('\u{230e}');
|
placeholder.push('\u{230e}');
|
||||||
placeholder.push_str(&" ".repeat(width - 2));
|
placeholder.push_str(&" ".repeat(width - 2));
|
||||||
placeholder.push_str("\u{230f}\n");
|
placeholder.push_str("\u{230f}\n");
|
||||||
|
@ -180,9 +199,8 @@ fn placeholder_frame(
|
||||||
#[inline]
|
#[inline]
|
||||||
fn millis_to_datetime(ms: UInt) -> DateTime<LocalTz> {
|
fn millis_to_datetime(ms: UInt) -> DateTime<LocalTz> {
|
||||||
let time = i64::from(ms) / 1000;
|
let time = i64::from(ms) / 1000;
|
||||||
let time = NaiveDateTime::from_timestamp_opt(time, 0).unwrap_or_default();
|
let time = DateTime::from_timestamp(time, 0).unwrap_or_default();
|
||||||
|
time.into()
|
||||||
LocalTz.from_utc_datetime(&time)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -426,6 +444,7 @@ pub enum MessageEvent {
|
||||||
EncryptedRedacted(Box<RedactedRoomEncryptedEvent>),
|
EncryptedRedacted(Box<RedactedRoomEncryptedEvent>),
|
||||||
Original(Box<OriginalRoomMessageEvent>),
|
Original(Box<OriginalRoomMessageEvent>),
|
||||||
Redacted(Box<RedactedRoomMessageEvent>),
|
Redacted(Box<RedactedRoomMessageEvent>),
|
||||||
|
State(Box<AnySyncStateEvent>),
|
||||||
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,6 +455,7 @@ impl MessageEvent {
|
||||||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.as_ref(),
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Redacted(ev) => ev.event_id.as_ref(),
|
MessageEvent::Redacted(ev) => ev.event_id.as_ref(),
|
||||||
|
MessageEvent::State(ev) => ev.event_id(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -446,6 +466,7 @@ impl MessageEvent {
|
||||||
MessageEvent::Original(ev) => Some(&ev.content),
|
MessageEvent::Original(ev) => Some(&ev.content),
|
||||||
MessageEvent::EncryptedRedacted(_) => None,
|
MessageEvent::EncryptedRedacted(_) => None,
|
||||||
MessageEvent::Redacted(_) => None,
|
MessageEvent::Redacted(_) => None,
|
||||||
|
MessageEvent::State(_) => None,
|
||||||
MessageEvent::Local(_, content) => Some(content),
|
MessageEvent::Local(_, content) => Some(content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,6 +484,7 @@ impl MessageEvent {
|
||||||
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
||||||
MessageEvent::EncryptedRedacted(ev) => body_cow_reason(&ev.unsigned),
|
MessageEvent::EncryptedRedacted(ev) => body_cow_reason(&ev.unsigned),
|
||||||
MessageEvent::Redacted(ev) => body_cow_reason(&ev.unsigned),
|
MessageEvent::Redacted(ev) => body_cow_reason(&ev.unsigned),
|
||||||
|
MessageEvent::State(ev) => body_cow_state(ev),
|
||||||
MessageEvent::Local(_, content) => body_cow_content(content),
|
MessageEvent::Local(_, content) => body_cow_content(content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,6 +495,7 @@ impl MessageEvent {
|
||||||
MessageEvent::EncryptedRedacted(_) => return None,
|
MessageEvent::EncryptedRedacted(_) => return None,
|
||||||
MessageEvent::Original(ev) => &ev.content,
|
MessageEvent::Original(ev) => &ev.content,
|
||||||
MessageEvent::Redacted(_) => return None,
|
MessageEvent::Redacted(_) => return None,
|
||||||
|
MessageEvent::State(ev) => return Some(html_state(ev)),
|
||||||
MessageEvent::Local(_, content) => content,
|
MessageEvent::Local(_, content) => content,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -492,6 +515,7 @@ impl MessageEvent {
|
||||||
MessageEvent::EncryptedOriginal(_) => return,
|
MessageEvent::EncryptedOriginal(_) => return,
|
||||||
MessageEvent::EncryptedRedacted(_) => return,
|
MessageEvent::EncryptedRedacted(_) => return,
|
||||||
MessageEvent::Redacted(_) => return,
|
MessageEvent::Redacted(_) => return,
|
||||||
|
MessageEvent::State(_) => return,
|
||||||
MessageEvent::Local(_, _) => return,
|
MessageEvent::Local(_, _) => return,
|
||||||
MessageEvent::Original(ev) => {
|
MessageEvent::Original(ev) => {
|
||||||
let redacted = RedactedRoomMessageEvent {
|
let redacted = RedactedRoomMessageEvent {
|
||||||
|
@ -623,8 +647,8 @@ struct MessageFormatter<'a> {
|
||||||
/// The date the message was sent.
|
/// The date the message was sent.
|
||||||
date: Option<Span<'a>>,
|
date: Option<Span<'a>>,
|
||||||
|
|
||||||
/// Iterator over the users who have read up to this message.
|
/// The users who have read up to this message.
|
||||||
read: Option<hash_set::Iter<'a, OwnedUserId>>,
|
read: Vec<OwnedUserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MessageFormatter<'a> {
|
impl<'a> MessageFormatter<'a> {
|
||||||
|
@ -657,13 +681,11 @@ impl<'a> MessageFormatter<'a> {
|
||||||
line.push(time);
|
line.push(time);
|
||||||
|
|
||||||
// Show read receipts.
|
// Show read receipts.
|
||||||
let user_char =
|
let user_char = |user: OwnedUserId| -> Span { settings.get_user_char_span(&user) };
|
||||||
|user: &'a OwnedUserId| -> Span<'a> { settings.get_user_char_span(user) };
|
|
||||||
let mut read = self.read.iter_mut().flatten();
|
|
||||||
|
|
||||||
let a = read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
let a = self.read.pop().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||||
let b = read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
let b = self.read.pop().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||||
let c = read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
let c = self.read.pop().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||||
|
|
||||||
line.push(Span::raw(" "));
|
line.push(Span::raw(" "));
|
||||||
line.push(c);
|
line.push(c);
|
||||||
|
@ -716,11 +738,11 @@ impl<'a> MessageFormatter<'a> {
|
||||||
style: Style,
|
style: Style,
|
||||||
text: &mut Text<'a>,
|
text: &mut Text<'a>,
|
||||||
info: &'a RoomInfo,
|
info: &'a RoomInfo,
|
||||||
) {
|
settings: &'a ApplicationSettings,
|
||||||
|
) -> Option<ProtocolPreview<'a>> {
|
||||||
let width = self.width();
|
let width = self.width();
|
||||||
let w = width.saturating_sub(2);
|
let w = width.saturating_sub(2);
|
||||||
let shortcodes = self.settings.tunables.message_shortcode_display;
|
let (mut replied, proto) = msg.show_msg(w, style, true, settings);
|
||||||
let (mut replied, _) = msg.show_msg(w, style, true, shortcodes);
|
|
||||||
let mut sender = msg.sender_span(info, self.settings);
|
let mut sender = msg.sender_span(info, self.settings);
|
||||||
let sender_width = UnicodeWidthStr::width(sender.content.as_ref());
|
let sender_width = UnicodeWidthStr::width(sender.content.as_ref());
|
||||||
let trailing = w.saturating_sub(sender_width + 1);
|
let trailing = w.saturating_sub(sender_width + 1);
|
||||||
|
@ -739,16 +761,26 @@ impl<'a> MessageFormatter<'a> {
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Determine the image offset of the reply header, taking into account the formatting
|
||||||
|
let proto = proto.map(|p| {
|
||||||
|
let y_off = text.lines.len() as u16;
|
||||||
|
// Adjust x_off by 2 to account for the vertical line and indent
|
||||||
|
let x_off = self.cols.user_gutter_width(settings) + 2;
|
||||||
|
(p, x_off, y_off)
|
||||||
|
});
|
||||||
|
|
||||||
for line in replied.lines.iter_mut() {
|
for line in replied.lines.iter_mut() {
|
||||||
line.spans.insert(0, Span::styled(THICK_VERTICAL, style));
|
line.spans.insert(0, Span::styled(THICK_VERTICAL, style));
|
||||||
line.spans.insert(0, Span::styled(" ", style));
|
line.spans.insert(0, Span::styled(" ", style));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push_text(replied, style, text);
|
self.push_text(replied, style, text);
|
||||||
|
|
||||||
|
proto
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_reactions(&mut self, counts: Vec<(&'a str, usize)>, style: Style, text: &mut Text<'a>) {
|
fn push_reactions(&mut self, counts: Vec<(&'a str, usize)>, style: Style, text: &mut Text<'a>) {
|
||||||
let mut emojis = printer::TextPrinter::new(self.width(), style, false, false);
|
let mut emojis = printer::TextPrinter::new(self.width(), style, false, self.settings);
|
||||||
let mut reactions = 0;
|
let mut reactions = 0;
|
||||||
|
|
||||||
for (key, count) in counts {
|
for (key, count) in counts {
|
||||||
|
@ -797,7 +829,7 @@ impl<'a> MessageFormatter<'a> {
|
||||||
let plural = len != 1;
|
let plural = len != 1;
|
||||||
let style = Style::default();
|
let style = Style::default();
|
||||||
let mut threaded =
|
let mut threaded =
|
||||||
printer::TextPrinter::new(self.width(), style, false, false).literal(true);
|
printer::TextPrinter::new(self.width(), style, false, self.settings).literal(true);
|
||||||
let len = Span::styled(len.to_string(), style.add_modifier(StyleModifier::BOLD));
|
let len = Span::styled(len.to_string(), style.add_modifier(StyleModifier::BOLD));
|
||||||
threaded.push_str(" \u{2937} ", style);
|
threaded.push_str(" \u{2937} ", style);
|
||||||
threaded.push_span_nobreak(len);
|
threaded.push_span_nobreak(len);
|
||||||
|
@ -814,7 +846,7 @@ impl<'a> MessageFormatter<'a> {
|
||||||
pub enum ImageStatus {
|
pub enum ImageStatus {
|
||||||
None,
|
None,
|
||||||
Downloading(ImagePreviewSize),
|
Downloading(ImagePreviewSize),
|
||||||
Loaded(Box<dyn Protocol>),
|
Loaded(Protocol),
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -849,6 +881,7 @@ impl Message {
|
||||||
MessageEvent::Local(_, content) => content,
|
MessageEvent::Local(_, content) => content,
|
||||||
MessageEvent::Original(ev) => &ev.content,
|
MessageEvent::Original(ev) => &ev.content,
|
||||||
MessageEvent::Redacted(_) => return None,
|
MessageEvent::Redacted(_) => return None,
|
||||||
|
MessageEvent::State(_) => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match &content.relates_to {
|
match &content.relates_to {
|
||||||
|
@ -869,6 +902,7 @@ impl Message {
|
||||||
MessageEvent::Local(_, content) => content,
|
MessageEvent::Local(_, content) => content,
|
||||||
MessageEvent::Original(ev) => &ev.content,
|
MessageEvent::Original(ev) => &ev.content,
|
||||||
MessageEvent::Redacted(_) => return None,
|
MessageEvent::Redacted(_) => return None,
|
||||||
|
MessageEvent::State(_) => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match &content.relates_to {
|
match &content.relates_to {
|
||||||
|
@ -922,7 +956,13 @@ impl Message {
|
||||||
let fill = width - user_gutter - TIME_GUTTER - READ_GUTTER;
|
let fill = width - user_gutter - TIME_GUTTER - READ_GUTTER;
|
||||||
let user = self.show_sender(prev, true, info, settings);
|
let user = self.show_sender(prev, true, info, settings);
|
||||||
let time = self.timestamp.show_time();
|
let time = self.timestamp.show_time();
|
||||||
let read = info.event_receipts.get(self.event.event_id()).map(|read| read.iter());
|
let read = info
|
||||||
|
.event_receipts
|
||||||
|
.values()
|
||||||
|
.filter_map(|receipts| receipts.get(self.event.event_id()))
|
||||||
|
.flat_map(|read| read.iter())
|
||||||
|
.map(|user_id| user_id.to_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||||
} else if user_gutter + TIME_GUTTER + MIN_MSG_LEN <= width {
|
} else if user_gutter + TIME_GUTTER + MIN_MSG_LEN <= width {
|
||||||
|
@ -930,7 +970,7 @@ impl Message {
|
||||||
let fill = width - user_gutter - TIME_GUTTER;
|
let fill = width - user_gutter - TIME_GUTTER;
|
||||||
let user = self.show_sender(prev, true, info, settings);
|
let user = self.show_sender(prev, true, info, settings);
|
||||||
let time = self.timestamp.show_time();
|
let time = self.timestamp.show_time();
|
||||||
let read = None;
|
let read = Vec::new();
|
||||||
|
|
||||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||||
} else if user_gutter + MIN_MSG_LEN <= width {
|
} else if user_gutter + MIN_MSG_LEN <= width {
|
||||||
|
@ -938,7 +978,7 @@ impl Message {
|
||||||
let fill = width - user_gutter;
|
let fill = width - user_gutter;
|
||||||
let user = self.show_sender(prev, true, info, settings);
|
let user = self.show_sender(prev, true, info, settings);
|
||||||
let time = None;
|
let time = None;
|
||||||
let read = None;
|
let read = Vec::new();
|
||||||
|
|
||||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||||
} else {
|
} else {
|
||||||
|
@ -946,7 +986,7 @@ impl Message {
|
||||||
let fill = width.saturating_sub(2);
|
let fill = width.saturating_sub(2);
|
||||||
let user = self.show_sender(prev, false, info, settings);
|
let user = self.show_sender(prev, false, info, settings);
|
||||||
let time = None;
|
let time = None;
|
||||||
let read = None;
|
let read = Vec::new();
|
||||||
|
|
||||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||||
}
|
}
|
||||||
|
@ -962,7 +1002,7 @@ impl Message {
|
||||||
vwctx: &ViewportContext<MessageCursor>,
|
vwctx: &ViewportContext<MessageCursor>,
|
||||||
info: &'a RoomInfo,
|
info: &'a RoomInfo,
|
||||||
settings: &'a ApplicationSettings,
|
settings: &'a ApplicationSettings,
|
||||||
) -> (Text<'a>, Option<(&dyn Protocol, u16, u16)>) {
|
) -> (Text<'a>, [Option<ProtocolPreview<'a>>; 2]) {
|
||||||
let width = vwctx.get_width();
|
let width = vwctx.get_width();
|
||||||
|
|
||||||
let style = self.get_render_style(selected, settings);
|
let style = self.get_render_style(selected, settings);
|
||||||
|
@ -975,24 +1015,20 @@ impl Message {
|
||||||
.reply_to()
|
.reply_to()
|
||||||
.or_else(|| self.thread_root())
|
.or_else(|| self.thread_root())
|
||||||
.and_then(|e| info.get_event(&e));
|
.and_then(|e| info.get_event(&e));
|
||||||
|
let proto_reply = reply.as_ref().and_then(|r| {
|
||||||
if let Some(r) = &reply {
|
// Format the reply header, push it into the `Text` buffer, and get any image.
|
||||||
fmt.push_in_reply(r, style, &mut text, info);
|
fmt.push_in_reply(r, style, &mut text, info, settings)
|
||||||
}
|
});
|
||||||
|
|
||||||
// Now show the message contents, and the inlined reply if we couldn't find it above.
|
// Now show the message contents, and the inlined reply if we couldn't find it above.
|
||||||
let (msg, proto) = self.show_msg(
|
let (msg, proto) = self.show_msg(width, style, reply.is_some(), settings);
|
||||||
width,
|
|
||||||
style,
|
|
||||||
reply.is_some(),
|
|
||||||
settings.tunables.message_shortcode_display,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Given our text so far, determine the image offset.
|
// Given our text so far, determine the image offset.
|
||||||
let proto = proto.map(|p| {
|
let proto_main = proto.map(|p| {
|
||||||
let y_off = text.lines.len() as u16;
|
let y_off = text.lines.len() as u16;
|
||||||
let x_off = fmt.cols.user_gutter_width(settings);
|
let x_off = fmt.cols.user_gutter_width(settings);
|
||||||
// Adjust y_off by 1 if a date was printed before the message to account for the extra line.
|
// Adjust y_off by 1 if a date was printed before the message to account for
|
||||||
|
// the extra line we're going to print.
|
||||||
let y_off = if fmt.date.is_some() { y_off + 1 } else { y_off };
|
let y_off = if fmt.date.is_some() { y_off + 1 } else { y_off };
|
||||||
(p, x_off, y_off)
|
(p, x_off, y_off)
|
||||||
});
|
});
|
||||||
|
@ -1013,7 +1049,7 @@ impl Message {
|
||||||
fmt.push_thread_reply_count(thread.len(), &mut text);
|
fmt.push_thread_reply_count(thread.len(), &mut text);
|
||||||
}
|
}
|
||||||
|
|
||||||
(text, proto)
|
(text, [proto_main, proto_reply])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show<'a>(
|
pub fn show<'a>(
|
||||||
|
@ -1027,18 +1063,18 @@ impl Message {
|
||||||
self.show_with_preview(prev, selected, vwctx, info, settings).0
|
self.show_with_preview(prev, selected, vwctx, info, settings).0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_msg(
|
fn show_msg<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
width: usize,
|
width: usize,
|
||||||
style: Style,
|
style: Style,
|
||||||
hide_reply: bool,
|
hide_reply: bool,
|
||||||
emoji_shortcodes: bool,
|
settings: &'a ApplicationSettings,
|
||||||
) -> (Text, Option<&dyn Protocol>) {
|
) -> (Text<'a>, Option<&'a Protocol>) {
|
||||||
if let Some(html) = &self.html {
|
if let Some(html) = &self.html {
|
||||||
(html.to_text(width, style, hide_reply, emoji_shortcodes), None)
|
(html.to_text(width, style, hide_reply, settings), None)
|
||||||
} else {
|
} else {
|
||||||
let mut msg = self.event.body();
|
let mut msg = self.event.body();
|
||||||
if emoji_shortcodes {
|
if settings.tunables.message_shortcode_display {
|
||||||
msg = Cow::Owned(replace_emojis_in_str(msg.as_ref()));
|
msg = Cow::Owned(replace_emojis_in_str(msg.as_ref()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1053,8 +1089,8 @@ impl Message {
|
||||||
placeholder_frame(Some("Downloading..."), width, image_preview_size)
|
placeholder_frame(Some("Downloading..."), width, image_preview_size)
|
||||||
},
|
},
|
||||||
ImageStatus::Loaded(backend) => {
|
ImageStatus::Loaded(backend) => {
|
||||||
proto = Some(backend.as_ref());
|
proto = Some(backend);
|
||||||
placeholder_frame(None, width, &backend.rect().into())
|
placeholder_frame(Some("Cut off..."), width, &backend.area().into())
|
||||||
},
|
},
|
||||||
ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")),
|
ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")),
|
||||||
};
|
};
|
||||||
|
@ -1097,9 +1133,9 @@ impl Message {
|
||||||
let padding = user_gutter - 2 - width;
|
let padding = user_gutter - 2 - width;
|
||||||
|
|
||||||
let sender = if align_right {
|
let sender = if align_right {
|
||||||
space(padding) + &truncated + " "
|
format!("{}{} ", space(padding), truncated)
|
||||||
} else {
|
} else {
|
||||||
truncated.into_owned() + &space(padding) + " "
|
format!("{}{} ", truncated, space(padding))
|
||||||
};
|
};
|
||||||
|
|
||||||
Span::styled(sender, style).into()
|
Span::styled(sender, style).into()
|
||||||
|
@ -1108,6 +1144,8 @@ impl Message {
|
||||||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
||||||
self.event.redact(redaction, version);
|
self.event.redact(redaction, version);
|
||||||
self.html = None;
|
self.html = None;
|
||||||
|
self.downloaded = false;
|
||||||
|
self.image_preview = ImageStatus::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1153,6 +1191,16 @@ impl From<RoomMessageEvent> for Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AnySyncStateEvent> for Message {
|
||||||
|
fn from(event: AnySyncStateEvent) -> Self {
|
||||||
|
let timestamp = event.origin_server_ts().into();
|
||||||
|
let user_id = event.sender().to_owned();
|
||||||
|
let event = MessageEvent::State(event.into());
|
||||||
|
|
||||||
|
Message::new(event, user_id, timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for Message {
|
impl Display for Message {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", self.event.body())
|
write!(f, "{}", self.event.body())
|
||||||
|
@ -1251,7 +1299,7 @@ pub mod tests {
|
||||||
assert_eq!(k6, &MSG1_KEY.clone());
|
assert_eq!(k6, &MSG1_KEY.clone());
|
||||||
|
|
||||||
// MessageCursor::latest() fails to convert for a room w/o messages.
|
// MessageCursor::latest() fails to convert for a room w/o messages.
|
||||||
let messages_empty = Messages::default();
|
let messages_empty = Messages::new(ReceiptThread::Main);
|
||||||
assert_eq!(mc6.to_key(&messages_empty), None);
|
assert_eq!(mc6.to_key(&messages_empty), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1313,6 +1361,33 @@ pub mod tests {
|
||||||
OK
|
OK
|
||||||
|
|
||||||
⌎ ⌏
|
⌎ ⌏
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
placeholder_frame(Some("OK"), 6, &ImagePreviewSize { width: 6, height: 6 }),
|
||||||
|
pretty_frame_test(
|
||||||
|
r#"
|
||||||
|
⌌ ⌍
|
||||||
|
|
||||||
|
OK
|
||||||
|
|
||||||
|
|
||||||
|
⌎ ⌏
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
placeholder_frame(Some("OK"), 6, &ImagePreviewSize { width: 6, height: 7 }),
|
||||||
|
pretty_frame_test(
|
||||||
|
r#"
|
||||||
|
⌌ ⌍
|
||||||
|
|
||||||
|
|
||||||
|
OK
|
||||||
|
|
||||||
|
|
||||||
|
⌎ ⌏
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,7 @@ use ratatui::text::{Line, Span, Text};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::config::{ApplicationSettings, TunableValues};
|
||||||
use crate::util::{
|
use crate::util::{
|
||||||
replace_emojis_in_line,
|
replace_emojis_in_line,
|
||||||
replace_emojis_in_span,
|
replace_emojis_in_span,
|
||||||
|
@ -25,28 +26,34 @@ pub struct TextPrinter<'a> {
|
||||||
width: usize,
|
width: usize,
|
||||||
base_style: Style,
|
base_style: Style,
|
||||||
hide_reply: bool,
|
hide_reply: bool,
|
||||||
emoji_shortcodes: bool,
|
|
||||||
|
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
curr_spans: Vec<Span<'a>>,
|
curr_spans: Vec<Span<'a>>,
|
||||||
curr_width: usize,
|
curr_width: usize,
|
||||||
literal: bool,
|
literal: bool,
|
||||||
|
|
||||||
|
pub(super) settings: &'a ApplicationSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextPrinter<'a> {
|
impl<'a> TextPrinter<'a> {
|
||||||
/// Create a new printer.
|
/// Create a new printer.
|
||||||
pub fn new(width: usize, base_style: Style, hide_reply: bool, emoji_shortcodes: bool) -> Self {
|
pub fn new(
|
||||||
|
width: usize,
|
||||||
|
base_style: Style,
|
||||||
|
hide_reply: bool,
|
||||||
|
settings: &'a ApplicationSettings,
|
||||||
|
) -> Self {
|
||||||
TextPrinter {
|
TextPrinter {
|
||||||
text: Text::default(),
|
text: Text::default(),
|
||||||
width,
|
width,
|
||||||
base_style,
|
base_style,
|
||||||
hide_reply,
|
hide_reply,
|
||||||
emoji_shortcodes,
|
|
||||||
|
|
||||||
alignment: Alignment::Left,
|
alignment: Alignment::Left,
|
||||||
curr_spans: vec![],
|
curr_spans: vec![],
|
||||||
curr_width: 0,
|
curr_width: 0,
|
||||||
literal: false,
|
literal: false,
|
||||||
|
settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +76,15 @@ impl<'a> TextPrinter<'a> {
|
||||||
|
|
||||||
/// Indicates whether emojis should be replaced by shortcodes
|
/// Indicates whether emojis should be replaced by shortcodes
|
||||||
pub fn emoji_shortcodes(&self) -> bool {
|
pub fn emoji_shortcodes(&self) -> bool {
|
||||||
self.emoji_shortcodes
|
self.tunables().message_shortcode_display
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings(&self) -> &ApplicationSettings {
|
||||||
|
self.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tunables(&self) -> &TunableValues {
|
||||||
|
&self.settings.tunables
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicates the current printer's width.
|
/// Indicates the current printer's width.
|
||||||
|
@ -84,12 +99,12 @@ impl<'a> TextPrinter<'a> {
|
||||||
width: self.width.saturating_sub(indent),
|
width: self.width.saturating_sub(indent),
|
||||||
base_style: self.base_style,
|
base_style: self.base_style,
|
||||||
hide_reply: self.hide_reply,
|
hide_reply: self.hide_reply,
|
||||||
emoji_shortcodes: self.emoji_shortcodes,
|
|
||||||
|
|
||||||
alignment: self.alignment,
|
alignment: self.alignment,
|
||||||
curr_spans: vec![],
|
curr_spans: vec![],
|
||||||
curr_width: 0,
|
curr_width: 0,
|
||||||
literal: self.literal,
|
literal: self.literal,
|
||||||
|
settings: self.settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +194,7 @@ impl<'a> TextPrinter<'a> {
|
||||||
|
|
||||||
/// Push a [Span] that isn't allowed to break across lines.
|
/// Push a [Span] that isn't allowed to break across lines.
|
||||||
pub fn push_span_nobreak(&mut self, mut span: Span<'a>) {
|
pub fn push_span_nobreak(&mut self, mut span: Span<'a>) {
|
||||||
if self.emoji_shortcodes {
|
if self.emoji_shortcodes() {
|
||||||
replace_emojis_in_span(&mut span);
|
replace_emojis_in_span(&mut span);
|
||||||
}
|
}
|
||||||
let sw = UnicodeWidthStr::width(span.content.as_ref());
|
let sw = UnicodeWidthStr::width(span.content.as_ref());
|
||||||
|
@ -201,6 +216,8 @@ impl<'a> TextPrinter<'a> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tabstop = self.settings().tunables.tabstop;
|
||||||
|
|
||||||
for mut word in UnicodeSegmentation::split_word_bounds(s) {
|
for mut word in UnicodeSegmentation::split_word_bounds(s) {
|
||||||
if let "\n" | "\r\n" = word {
|
if let "\n" | "\r\n" = word {
|
||||||
if self.literal {
|
if self.literal {
|
||||||
|
@ -217,11 +234,17 @@ impl<'a> TextPrinter<'a> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cow = if self.emoji_shortcodes {
|
let mut cow = if self.emoji_shortcodes() {
|
||||||
Cow::Owned(replace_emojis_in_str(word))
|
Cow::Owned(replace_emojis_in_str(word))
|
||||||
} else {
|
} else {
|
||||||
Cow::Borrowed(word)
|
Cow::Borrowed(word)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if cow == "\t" {
|
||||||
|
let tablen = tabstop - (self.curr_width % tabstop);
|
||||||
|
cow = Cow::Owned(" ".repeat(tablen));
|
||||||
|
}
|
||||||
|
|
||||||
let sw = UnicodeWidthStr::width(cow.as_ref());
|
let sw = UnicodeWidthStr::width(cow.as_ref());
|
||||||
|
|
||||||
if sw > self.width {
|
if sw > self.width {
|
||||||
|
@ -253,7 +276,7 @@ impl<'a> TextPrinter<'a> {
|
||||||
/// Push a [Line] into the printer.
|
/// Push a [Line] into the printer.
|
||||||
pub fn push_line(&mut self, mut line: Line<'a>) {
|
pub fn push_line(&mut self, mut line: Line<'a>) {
|
||||||
self.commit();
|
self.commit();
|
||||||
if self.emoji_shortcodes {
|
if self.emoji_shortcodes() {
|
||||||
replace_emojis_in_line(&mut line);
|
replace_emojis_in_line(&mut line);
|
||||||
}
|
}
|
||||||
self.text.lines.push(line);
|
self.text.lines.push(line);
|
||||||
|
@ -262,7 +285,7 @@ impl<'a> TextPrinter<'a> {
|
||||||
/// Push multiline [Text] into the printer.
|
/// Push multiline [Text] into the printer.
|
||||||
pub fn push_text(&mut self, mut text: Text<'a>) {
|
pub fn push_text(&mut self, mut text: Text<'a>) {
|
||||||
self.commit();
|
self.commit();
|
||||||
if self.emoji_shortcodes {
|
if self.emoji_shortcodes() {
|
||||||
for line in &mut text.lines {
|
for line in &mut text.lines {
|
||||||
replace_emojis_in_line(line);
|
replace_emojis_in_line(line);
|
||||||
}
|
}
|
||||||
|
@ -280,10 +303,12 @@ impl<'a> TextPrinter<'a> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::tests::mock_settings;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_push_nobreak() {
|
fn test_push_nobreak() {
|
||||||
let mut printer = TextPrinter::new(5, Style::default(), false, false);
|
let settings = mock_settings();
|
||||||
|
let mut printer = TextPrinter::new(5, Style::default(), false, &settings);
|
||||||
printer.push_span_nobreak("hello world".into());
|
printer.push_span_nobreak("hello world".into());
|
||||||
let text = printer.finish();
|
let text = printer.finish();
|
||||||
assert_eq!(text.lines.len(), 1);
|
assert_eq!(text.lines.len(), 1);
|
||||||
|
|
956
src/message/state.rs
Normal file
956
src/message/state.rs
Normal file
|
@ -0,0 +1,956 @@
|
||||||
|
//! Code for displaying state events.
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use matrix_sdk::ruma::{
|
||||||
|
events::{
|
||||||
|
room::member::MembershipChange,
|
||||||
|
AnyFullStateEventContent,
|
||||||
|
AnySyncStateEvent,
|
||||||
|
FullStateEventContent,
|
||||||
|
},
|
||||||
|
OwnedRoomId,
|
||||||
|
UserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::html::{StyleTree, StyleTreeNode};
|
||||||
|
use ratatui::style::{Modifier as StyleModifier, Style};
|
||||||
|
|
||||||
|
fn bold(s: impl Into<Cow<'static, str>>) -> StyleTreeNode {
|
||||||
|
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||||
|
let text = StyleTreeNode::Text(s.into());
|
||||||
|
StyleTreeNode::Style(Box::new(text), bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body_cow_state(ev: &AnySyncStateEvent) -> Cow<'static, str> {
|
||||||
|
let event = match ev.content() {
|
||||||
|
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let mut m = format!(
|
||||||
|
"* updated the room policy rule for {:?} to {:?}",
|
||||||
|
content.0.entity,
|
||||||
|
content.0.recommendation.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
if !content.0.reason.is_empty() {
|
||||||
|
m.push_str(" (reason: ");
|
||||||
|
m.push_str(&content.0.reason);
|
||||||
|
m.push(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let mut m = format!(
|
||||||
|
"* updated the server policy rule for {:?} to {:?}",
|
||||||
|
content.0.entity,
|
||||||
|
content.0.recommendation.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
if !content.0.reason.is_empty() {
|
||||||
|
m.push_str(" (reason: ");
|
||||||
|
m.push_str(&content.0.reason);
|
||||||
|
m.push(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let mut m = format!(
|
||||||
|
"* updated the user policy rule for {:?} to {:?}",
|
||||||
|
content.0.entity,
|
||||||
|
content.0.recommendation.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
if !content.0.reason.is_empty() {
|
||||||
|
m.push_str(" (reason: ");
|
||||||
|
m.push_str(&content.0.reason);
|
||||||
|
m.push(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
let mut m = String::from("* set the room aliases to: ");
|
||||||
|
|
||||||
|
for (i, alias) in content.aliases.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
m.push_str(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
m.push_str(alias.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
prev_content,
|
||||||
|
}) => {
|
||||||
|
let prev_url = prev_content.as_ref().and_then(|p| p.url.as_ref());
|
||||||
|
|
||||||
|
match (prev_url, content.url) {
|
||||||
|
(None, Some(_)) => return Cow::Borrowed("* added a room avatar"),
|
||||||
|
(Some(old), Some(new)) => {
|
||||||
|
if old != &new {
|
||||||
|
return Cow::Borrowed("* replaced the room avatar");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cow::Borrowed("* updated the room avatar state");
|
||||||
|
},
|
||||||
|
(Some(_), None) => return Cow::Borrowed("* removed the room avatar"),
|
||||||
|
(None, None) => return Cow::Borrowed("* updated the room avatar state"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
prev_content,
|
||||||
|
}) => {
|
||||||
|
let old_canon = prev_content.as_ref().and_then(|p| p.alias.as_ref());
|
||||||
|
let new_canon = content.alias.as_ref();
|
||||||
|
|
||||||
|
match (old_canon, new_canon) {
|
||||||
|
(None, Some(canon)) => {
|
||||||
|
format!("* updated the canonical alias for the room to: {}", canon)
|
||||||
|
},
|
||||||
|
(Some(old), Some(new)) => {
|
||||||
|
if old != new {
|
||||||
|
format!("* updated the canonical alias for the room to: {}", new)
|
||||||
|
} else {
|
||||||
|
return Cow::Borrowed("* removed the canonical alias for the room");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(Some(_), None) => {
|
||||||
|
return Cow::Borrowed("* removed the canonical alias for the room");
|
||||||
|
},
|
||||||
|
(None, None) => {
|
||||||
|
return Cow::Borrowed("* did not change the canonical alias");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
if content.federate {
|
||||||
|
return Cow::Borrowed("* created a federated room");
|
||||||
|
} else {
|
||||||
|
return Cow::Borrowed("* created a non-federated room");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Original { .. }) => {
|
||||||
|
return Cow::Borrowed("* updated the encryption settings for the room");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
format!("* set guest access for the room to {:?}", content.guest_access.as_str())
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
format!(
|
||||||
|
"* updated history visibility for the room to {:?}",
|
||||||
|
content.history_visibility.as_str()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
format!("* update the join rules for the room to {:?}", content.join_rule.as_str())
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomMember(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
prev_content,
|
||||||
|
}) => {
|
||||||
|
let Ok(state_key) = UserId::parse(ev.state_key()) else {
|
||||||
|
return Cow::Owned(format!(
|
||||||
|
"* failed to calculate membership change for {:?}",
|
||||||
|
ev.state_key()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_details = prev_content.as_ref().map(|p| p.details());
|
||||||
|
let change = content.membership_change(prev_details, ev.sender(), &state_key);
|
||||||
|
|
||||||
|
match change {
|
||||||
|
MembershipChange::None => {
|
||||||
|
format!("* did nothing to {}", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::Error => {
|
||||||
|
format!("* failed to calculate membership change to {}", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::Joined => {
|
||||||
|
return Cow::Borrowed("* joined the room");
|
||||||
|
},
|
||||||
|
MembershipChange::Left => {
|
||||||
|
return Cow::Borrowed("* left the room");
|
||||||
|
},
|
||||||
|
MembershipChange::Banned => {
|
||||||
|
format!("* banned {} from the room", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::Unbanned => {
|
||||||
|
format!("* unbanned {} from the room", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::Kicked => {
|
||||||
|
format!("* kicked {} from the room", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::Invited => {
|
||||||
|
format!("* invited {} to the room", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::KickedAndBanned => {
|
||||||
|
format!("* kicked and banned {} from the room", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::InvitationAccepted => {
|
||||||
|
return Cow::Borrowed("* accepted an invitation to join the room");
|
||||||
|
},
|
||||||
|
MembershipChange::InvitationRejected => {
|
||||||
|
return Cow::Borrowed("* rejected an invitation to join the room");
|
||||||
|
},
|
||||||
|
MembershipChange::InvitationRevoked => {
|
||||||
|
format!("* revoked an invitation for {} to join the room", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::Knocked => {
|
||||||
|
return Cow::Borrowed("* would like to join the room");
|
||||||
|
},
|
||||||
|
MembershipChange::KnockAccepted => {
|
||||||
|
format!("* accepted the room knock from {}", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::KnockRetracted => {
|
||||||
|
return Cow::Borrowed("* retracted their room knock");
|
||||||
|
},
|
||||||
|
MembershipChange::KnockDenied => {
|
||||||
|
format!("* rejected the room knock from {}", state_key)
|
||||||
|
},
|
||||||
|
MembershipChange::ProfileChanged { displayname_change, avatar_url_change } => {
|
||||||
|
match (displayname_change, avatar_url_change) {
|
||||||
|
(Some(change), avatar_change) => {
|
||||||
|
let mut m = match (change.old, change.new) {
|
||||||
|
(None, Some(new)) => {
|
||||||
|
format!("* set their display name to {:?}", new)
|
||||||
|
},
|
||||||
|
(Some(old), Some(new)) => {
|
||||||
|
format!("* changed their display name from {old} to {new}")
|
||||||
|
},
|
||||||
|
(Some(_), None) => "* unset their display name".to_string(),
|
||||||
|
(None, None) => {
|
||||||
|
"* made an unknown change to their display name".to_string()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if avatar_change.is_some() {
|
||||||
|
m.push_str(" and changed their user avatar");
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
(None, Some(change)) => {
|
||||||
|
match (change.old, change.new) {
|
||||||
|
(None, Some(_)) => {
|
||||||
|
return Cow::Borrowed("* added a user avatar");
|
||||||
|
},
|
||||||
|
(Some(_), Some(_)) => {
|
||||||
|
return Cow::Borrowed("* changed their user avatar");
|
||||||
|
},
|
||||||
|
(Some(_), None) => {
|
||||||
|
return Cow::Borrowed("* removed their user avatar");
|
||||||
|
},
|
||||||
|
(None, None) => {
|
||||||
|
return Cow::Borrowed(
|
||||||
|
"* made an unknown change to their user avatar",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(None, None) => {
|
||||||
|
return Cow::Borrowed("* changed their user profile");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ev => {
|
||||||
|
format!("* made an unknown membership change to {}: {:?}", state_key, ev)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => {
|
||||||
|
format!("* updated the room name to {:?}", content.name)
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { .. }) => {
|
||||||
|
return Cow::Borrowed("* updated the pinned events for the room");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Original { .. }) => {
|
||||||
|
return Cow::Borrowed("* updated the power levels for the room");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Original { .. }) => {
|
||||||
|
return Cow::Borrowed("* updated the room's server ACLs");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
format!("* sent a third-party invite to {:?}", content.display_name)
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
format!(
|
||||||
|
"* upgraded the room; replacement room is {}",
|
||||||
|
content.replacement_room.as_str()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
format!("* set the room topic to {:?}", content.topic)
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Original { .. }) => {
|
||||||
|
format!("* added a space child: {}", ev.state_key())
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
if content.canonical {
|
||||||
|
format!("* added a canonical parent space: {}", ev.state_key())
|
||||||
|
} else {
|
||||||
|
format!("* added a parent space: {}", ev.state_key())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Original { .. }) => {
|
||||||
|
return Cow::Borrowed("* shared beacon information");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::CallMember(FullStateEventContent::Original { .. }) => {
|
||||||
|
return Cow::Borrowed("* updated membership for room call");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::MemberHints(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
let mut m = String::from("* updated the list of service members in the room hints: ");
|
||||||
|
|
||||||
|
for (i, member) in content.service_members.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
m.push_str(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
m.push_str(member.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
|
||||||
|
// Redacted variants of state events:
|
||||||
|
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated a room policy rule (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated a server policy rule (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated a user policy rule (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the room aliases for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the room avatar (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the canonical alias for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* created the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the encryption settings for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed(
|
||||||
|
"* updated the guest access configuration for the room (redacted)",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated history visilibity for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the join rules for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomMember(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the room membership (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomName(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the room name (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the pinned events for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the power levels for the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the room's server ACLs (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* sent a third-party invite (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* upgraded the room (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* updated the room topic (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* added a space child (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* added a parent space (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("* shared beacon information (redacted)");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::CallMember(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("Call membership changed");
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::MemberHints(FullStateEventContent::Redacted(_)) => {
|
||||||
|
return Cow::Borrowed("Member hints changed");
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle unknown events:
|
||||||
|
e => {
|
||||||
|
format!("* sent an unknown state event: {:?}", e.event_type())
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Cow::Owned(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn html_state(ev: &AnySyncStateEvent) -> StyleTree {
|
||||||
|
let children = match ev.content() {
|
||||||
|
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* updated the room policy rule for ".into());
|
||||||
|
let entity = bold(format!("{:?}", content.0.entity));
|
||||||
|
let middle = StyleTreeNode::Text(" to ".into());
|
||||||
|
let rec =
|
||||||
|
StyleTreeNode::Text(format!("{:?}", content.0.recommendation.as_str()).into());
|
||||||
|
let mut cs = vec![prefix, entity, middle, rec];
|
||||||
|
|
||||||
|
if !content.0.reason.is_empty() {
|
||||||
|
let reason = format!(" (reason: {})", content.0.reason);
|
||||||
|
cs.push(StyleTreeNode::Text(reason.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* updated the server policy rule for ".into());
|
||||||
|
let entity = bold(format!("{:?}", content.0.entity));
|
||||||
|
let middle = StyleTreeNode::Text(" to ".into());
|
||||||
|
let rec =
|
||||||
|
StyleTreeNode::Text(format!("{:?}", content.0.recommendation.as_str()).into());
|
||||||
|
let mut cs = vec![prefix, entity, middle, rec];
|
||||||
|
|
||||||
|
if !content.0.reason.is_empty() {
|
||||||
|
let reason = format!(" (reason: {})", content.0.reason);
|
||||||
|
cs.push(StyleTreeNode::Text(reason.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* updated the user policy rule for ".into());
|
||||||
|
let entity = bold(format!("{:?}", content.0.entity));
|
||||||
|
let middle = StyleTreeNode::Text(" to ".into());
|
||||||
|
let rec =
|
||||||
|
StyleTreeNode::Text(format!("{:?}", content.0.recommendation.as_str()).into());
|
||||||
|
let mut cs = vec![prefix, entity, middle, rec];
|
||||||
|
|
||||||
|
if !content.0.reason.is_empty() {
|
||||||
|
let reason = format!(" (reason: {})", content.0.reason);
|
||||||
|
cs.push(StyleTreeNode::Text(reason.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* set the room aliases to: ".into());
|
||||||
|
let mut cs = vec![prefix];
|
||||||
|
|
||||||
|
for (i, alias) in content.aliases.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
cs.push(StyleTreeNode::Text(", ".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.push(StyleTreeNode::RoomAlias(alias.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
prev_content,
|
||||||
|
}) => {
|
||||||
|
let prev_url = prev_content.as_ref().and_then(|p| p.url.as_ref());
|
||||||
|
|
||||||
|
let node = match (prev_url, content.url) {
|
||||||
|
(None, Some(_)) => StyleTreeNode::Text("* added a room avatar".into()),
|
||||||
|
(Some(old), Some(new)) => {
|
||||||
|
if old != &new {
|
||||||
|
StyleTreeNode::Text("* replaced the room avatar".into())
|
||||||
|
} else {
|
||||||
|
StyleTreeNode::Text("* updated the room avatar state".into())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(Some(_), None) => StyleTreeNode::Text("* removed the room avatar".into()),
|
||||||
|
(None, None) => StyleTreeNode::Text("* updated the room avatar state".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![node]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if let Some(canon) = content.alias.as_ref() {
|
||||||
|
let canon = bold(canon.to_string());
|
||||||
|
let prefix =
|
||||||
|
StyleTreeNode::Text("* updated the canonical alias for the room to: ".into());
|
||||||
|
vec![prefix, canon]
|
||||||
|
} else {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* removed the canonical alias for the room".into(),
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
if content.federate {
|
||||||
|
vec![StyleTreeNode::Text("* created a federated room".into())]
|
||||||
|
} else {
|
||||||
|
vec![StyleTreeNode::Text("* created a non-federated room".into())]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Original { .. }) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the encryption settings for the room".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let access = bold(format!("{:?}", content.guest_access.as_str()));
|
||||||
|
let prefix = StyleTreeNode::Text("* set guest access for the room to ".into());
|
||||||
|
vec![prefix, access]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix =
|
||||||
|
StyleTreeNode::Text("* updated history visibility for the room to ".into());
|
||||||
|
let vis = bold(format!("{:?}", content.history_visibility.as_str()));
|
||||||
|
vec![prefix, vis]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* update the join rules for the room to ".into());
|
||||||
|
let rule = bold(format!("{:?}", content.join_rule.as_str()));
|
||||||
|
vec![prefix, rule]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomMember(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
prev_content,
|
||||||
|
}) => {
|
||||||
|
let Ok(state_key) = UserId::parse(ev.state_key()) else {
|
||||||
|
let prefix =
|
||||||
|
StyleTreeNode::Text("* failed to calculate membership change for ".into());
|
||||||
|
let user_id = bold(format!("{:?}", ev.state_key()));
|
||||||
|
let children = vec![prefix, user_id];
|
||||||
|
|
||||||
|
return StyleTree { children };
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_details = prev_content.as_ref().map(|p| p.details());
|
||||||
|
let change = content.membership_change(prev_details, ev.sender(), &state_key);
|
||||||
|
let user_id = StyleTreeNode::UserId(state_key.clone());
|
||||||
|
|
||||||
|
match change {
|
||||||
|
MembershipChange::None => {
|
||||||
|
let prefix = StyleTreeNode::Text("* did nothing to ".into());
|
||||||
|
vec![prefix, user_id]
|
||||||
|
},
|
||||||
|
MembershipChange::Error => {
|
||||||
|
let prefix =
|
||||||
|
StyleTreeNode::Text("* failed to calculate membership change to ".into());
|
||||||
|
vec![prefix, user_id]
|
||||||
|
},
|
||||||
|
MembershipChange::Joined => {
|
||||||
|
vec![StyleTreeNode::Text("* joined the room".into())]
|
||||||
|
},
|
||||||
|
MembershipChange::Left => {
|
||||||
|
vec![StyleTreeNode::Text("* left the room".into())]
|
||||||
|
},
|
||||||
|
MembershipChange::Banned => {
|
||||||
|
let prefix = StyleTreeNode::Text("* banned ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
MembershipChange::Unbanned => {
|
||||||
|
let prefix = StyleTreeNode::Text("* unbanned ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
MembershipChange::Kicked => {
|
||||||
|
let prefix = StyleTreeNode::Text("* kicked ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
MembershipChange::Invited => {
|
||||||
|
let prefix = StyleTreeNode::Text("* invited ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(" to the room".into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
MembershipChange::KickedAndBanned => {
|
||||||
|
let prefix = StyleTreeNode::Text("* kicked and banned ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
MembershipChange::InvitationAccepted => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* accepted an invitation to join the room".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
MembershipChange::InvitationRejected => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* rejected an invitation to join the room".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
MembershipChange::InvitationRevoked => {
|
||||||
|
let prefix = StyleTreeNode::Text("* revoked an invitation for ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(" to join the room".into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
MembershipChange::Knocked => {
|
||||||
|
vec![StyleTreeNode::Text("* would like to join the room".into())]
|
||||||
|
},
|
||||||
|
MembershipChange::KnockAccepted => {
|
||||||
|
let prefix = StyleTreeNode::Text("* accepted the room knock from ".into());
|
||||||
|
vec![prefix, user_id]
|
||||||
|
},
|
||||||
|
MembershipChange::KnockRetracted => {
|
||||||
|
vec![StyleTreeNode::Text("* retracted their room knock".into())]
|
||||||
|
},
|
||||||
|
MembershipChange::KnockDenied => {
|
||||||
|
let prefix = StyleTreeNode::Text("* rejected the room knock from ".into());
|
||||||
|
vec![prefix, user_id]
|
||||||
|
},
|
||||||
|
MembershipChange::ProfileChanged { displayname_change, avatar_url_change } => {
|
||||||
|
match (displayname_change, avatar_url_change) {
|
||||||
|
(Some(change), avatar_change) => {
|
||||||
|
let mut m = match (change.old, change.new) {
|
||||||
|
(None, Some(new)) => {
|
||||||
|
vec![
|
||||||
|
StyleTreeNode::Text("* set their display name to ".into()),
|
||||||
|
StyleTreeNode::DisplayName(new.into(), state_key),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
(Some(old), Some(new)) => {
|
||||||
|
vec![
|
||||||
|
StyleTreeNode::Text(
|
||||||
|
"* changed their display name from ".into(),
|
||||||
|
),
|
||||||
|
StyleTreeNode::DisplayName(old.into(), state_key.clone()),
|
||||||
|
StyleTreeNode::Text(" to ".into()),
|
||||||
|
StyleTreeNode::DisplayName(new.into(), state_key),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
(Some(_), None) => {
|
||||||
|
vec![StyleTreeNode::Text("* unset their display name".into())]
|
||||||
|
},
|
||||||
|
(None, None) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* made an unknown change to their display name".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if avatar_change.is_some() {
|
||||||
|
m.push(StyleTreeNode::Text(
|
||||||
|
" and changed their user avatar".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
},
|
||||||
|
(None, Some(change)) => {
|
||||||
|
let m = match (change.old, change.new) {
|
||||||
|
(None, Some(_)) => Cow::Borrowed("* added a user avatar"),
|
||||||
|
(Some(_), Some(_)) => Cow::Borrowed("* changed their user avatar"),
|
||||||
|
(Some(_), None) => Cow::Borrowed("* removed their user avatar"),
|
||||||
|
(None, None) => {
|
||||||
|
Cow::Borrowed("* made an unknown change to their user avatar")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![StyleTreeNode::Text(m)]
|
||||||
|
},
|
||||||
|
(None, None) => {
|
||||||
|
vec![StyleTreeNode::Text("* changed their user profile".into())]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ev => {
|
||||||
|
let prefix =
|
||||||
|
StyleTreeNode::Text("* made an unknown membership change to ".into());
|
||||||
|
let suffix = StyleTreeNode::Text(format!(": {:?}", ev).into());
|
||||||
|
vec![prefix, user_id, suffix]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* updated the room name to ".into());
|
||||||
|
let name = bold(format!("{:?}", content.name));
|
||||||
|
vec![prefix, name]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { .. }) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the pinned events for the room".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Original { .. }) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the power levels for the room".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Original { .. }) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room's server ACLs".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* sent a third-party invite to ".into());
|
||||||
|
let name = bold(format!("{:?}", content.display_name));
|
||||||
|
vec![prefix, name]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Original {
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* upgraded the room; replacement room is ".into());
|
||||||
|
let room = StyleTreeNode::RoomId(content.replacement_room.clone());
|
||||||
|
vec![prefix, room]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* set the room topic to ".into());
|
||||||
|
let topic = bold(format!("{:?}", content.topic));
|
||||||
|
vec![prefix, topic]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Original { .. }) => {
|
||||||
|
let prefix = StyleTreeNode::Text("* added a space child: ".into());
|
||||||
|
|
||||||
|
let room_id = if let Ok(room_id) = OwnedRoomId::from_str(ev.state_key()) {
|
||||||
|
StyleTreeNode::RoomId(room_id)
|
||||||
|
} else {
|
||||||
|
bold(ev.state_key().to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![prefix, room_id]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
let prefix = if content.canonical {
|
||||||
|
StyleTreeNode::Text("* added a canonical parent space: ".into())
|
||||||
|
} else {
|
||||||
|
StyleTreeNode::Text("* added a parent space: ".into())
|
||||||
|
};
|
||||||
|
|
||||||
|
let room_id = if let Ok(room_id) = OwnedRoomId::from_str(ev.state_key()) {
|
||||||
|
StyleTreeNode::RoomId(room_id)
|
||||||
|
} else {
|
||||||
|
bold(ev.state_key().to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![prefix, room_id]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Original { .. }) => {
|
||||||
|
vec![StyleTreeNode::Text("* shared beacon information".into())]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::CallMember(FullStateEventContent::Original { .. }) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated membership for room call".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::MemberHints(FullStateEventContent::Original {
|
||||||
|
content, ..
|
||||||
|
}) => {
|
||||||
|
let prefix = StyleTreeNode::Text(
|
||||||
|
"* updated the list of service members in the room hints: ".into(),
|
||||||
|
);
|
||||||
|
let mut cs = vec![prefix];
|
||||||
|
|
||||||
|
for (i, member) in content.service_members.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
cs.push(StyleTreeNode::Text(", ".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.push(StyleTreeNode::UserId(member.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
cs
|
||||||
|
},
|
||||||
|
|
||||||
|
// Redacted variants of state events:
|
||||||
|
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated a room policy rule (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated a server policy rule (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated a user policy rule (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room aliases for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room avatar (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the canonical alias for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text("* created the room (redacted)".into())]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the encryption settings for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the guest access configuration for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated history visilibity for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the join rules for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomMember(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room membership (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomName(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room name (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the pinned events for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the power levels for the room (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room's server ACLs (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* sent a third-party invite (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text("* upgraded the room (redacted)".into())]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* updated the room topic (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* added a space child (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* added a parent space (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text(
|
||||||
|
"* shared beacon information (redacted)".into(),
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::CallMember(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text("Call membership changed".into())]
|
||||||
|
},
|
||||||
|
AnyFullStateEventContent::MemberHints(FullStateEventContent::Redacted(_)) => {
|
||||||
|
vec![StyleTreeNode::Text("Member hints changed".into())]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle unknown events:
|
||||||
|
e => {
|
||||||
|
let prefix = StyleTreeNode::Text("* sent an unknown state event: ".into());
|
||||||
|
let event = bold(format!("{:?}", e.event_type()));
|
||||||
|
vec![prefix, event]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
StyleTree { children }
|
||||||
|
}
|
|
@ -1,12 +1,14 @@
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
|
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||||
notification_settings::{IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode},
|
notification_settings::{IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode},
|
||||||
room::Room as MatrixRoom,
|
room::Room as MatrixRoom,
|
||||||
ruma::{
|
ruma::{
|
||||||
api::client::push::get_notifications::v3::Notification,
|
|
||||||
events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent},
|
events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent},
|
||||||
|
serde::Raw,
|
||||||
MilliSecondsSinceUnixEpoch,
|
MilliSecondsSinceUnixEpoch,
|
||||||
|
OwnedRoomId,
|
||||||
RoomId,
|
RoomId,
|
||||||
},
|
},
|
||||||
Client,
|
Client,
|
||||||
|
@ -23,6 +25,21 @@ const IAMB_XDG_NAME: &str = match option_env!("IAMB_XDG_NAME") {
|
||||||
Some(iamb) => iamb,
|
Some(iamb) => iamb,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Handle for an open notification that should be closed when the user views it.
|
||||||
|
pub struct NotificationHandle(
|
||||||
|
#[cfg(all(feature = "desktop", unix, not(target_os = "macos")))]
|
||||||
|
Option<notify_rust::NotificationHandle>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Drop for NotificationHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
#[cfg(all(feature = "desktop", unix, not(target_os = "macos")))]
|
||||||
|
if let Some(handle) = self.0.take() {
|
||||||
|
handle.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn register_notifications(
|
pub async fn register_notifications(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
settings: &ApplicationSettings,
|
settings: &ApplicationSettings,
|
||||||
|
@ -53,51 +70,103 @@ pub async fn register_notifications(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match parse_notification(notification, room, show_message).await {
|
let room_id = room.room_id().to_owned();
|
||||||
Ok((summary, body, server_ts)) => {
|
match notification.event {
|
||||||
if server_ts < startup_ts {
|
RawAnySyncOrStrippedTimelineEvent::Sync(e) => {
|
||||||
return;
|
match parse_full_notification(e, room, show_message).await {
|
||||||
}
|
Ok((summary, body, server_ts)) => {
|
||||||
|
if server_ts < startup_ts {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if is_missing_mention(&body, mode, &client) {
|
if is_missing_mention(&body, mode, &client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match notify_via {
|
send_notification(
|
||||||
#[cfg(feature = "desktop")]
|
¬ify_via,
|
||||||
NotifyVia::Desktop => send_notification_desktop(summary, body),
|
&summary,
|
||||||
NotifyVia::Bell => send_notification_bell(&store).await,
|
body.as_deref(),
|
||||||
|
room_id,
|
||||||
|
&store,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to extract notification data: {err}")
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
// Stripped events may be dropped silently because they're
|
||||||
tracing::error!("Failed to extract notification data: {err}")
|
// only relevant if we're not in a room, and we presumably
|
||||||
},
|
// don't want notifications for rooms we're not in.
|
||||||
|
RawAnySyncOrStrippedTimelineEvent::Stripped(_) => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_notification(
|
||||||
|
via: &NotifyVia,
|
||||||
|
summary: &str,
|
||||||
|
body: Option<&str>,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
store: &AsyncProgramStore,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
if via.desktop {
|
||||||
|
send_notification_desktop(summary, body, room_id, store).await;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "desktop"))]
|
||||||
|
{
|
||||||
|
let _ = (summary, body, IAMB_XDG_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
if via.bell {
|
||||||
|
send_notification_bell(store).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_notification_bell(store: &AsyncProgramStore) {
|
async fn send_notification_bell(store: &AsyncProgramStore) {
|
||||||
let mut locked = store.lock().await;
|
let mut locked = store.lock().await;
|
||||||
locked.application.ring_bell = true;
|
locked.application.ring_bell = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
fn send_notification_desktop(summary: String, body: Option<String>) {
|
async fn send_notification_desktop(
|
||||||
|
summary: &str,
|
||||||
|
body: Option<&str>,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
_store: &AsyncProgramStore,
|
||||||
|
) {
|
||||||
let mut desktop_notification = notify_rust::Notification::new();
|
let mut desktop_notification = notify_rust::Notification::new();
|
||||||
desktop_notification
|
desktop_notification
|
||||||
.summary(&summary)
|
.summary(summary)
|
||||||
.appname(IAMB_XDG_NAME)
|
.appname(IAMB_XDG_NAME)
|
||||||
.icon(IAMB_XDG_NAME)
|
.icon(IAMB_XDG_NAME)
|
||||||
.action("default", "default");
|
.action("default", "default");
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
desktop_notification.urgency(notify_rust::Urgency::Normal);
|
||||||
|
|
||||||
if let Some(body) = body {
|
if let Some(body) = body {
|
||||||
desktop_notification.body(&body);
|
desktop_notification.body(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = desktop_notification.show() {
|
match desktop_notification.show() {
|
||||||
tracing::error!("Failed to send notification: {err}")
|
Err(err) => tracing::error!("Failed to send notification: {err}"),
|
||||||
|
Ok(handle) => {
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
_store
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.application
|
||||||
|
.open_notifications
|
||||||
|
.entry(room_id)
|
||||||
|
.or_default()
|
||||||
|
.push(NotificationHandle(Some(handle)));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,12 +224,12 @@ async fn is_visible_room(store: &AsyncProgramStore, room_id: &RoomId) -> bool {
|
||||||
is_focused(&locked) && is_open(&mut locked, room_id)
|
is_focused(&locked) && is_open(&mut locked, room_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_notification(
|
pub async fn parse_full_notification(
|
||||||
notification: Notification,
|
event: Raw<AnySyncTimelineEvent>,
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
show_body: bool,
|
show_body: bool,
|
||||||
) -> IambResult<(String, Option<String>, MilliSecondsSinceUnixEpoch)> {
|
) -> IambResult<(String, Option<String>, MilliSecondsSinceUnixEpoch)> {
|
||||||
let event = notification.event.deserialize().map_err(IambError::from)?;
|
let event = event.deserialize().map_err(IambError::from)?;
|
||||||
|
|
||||||
let server_ts = event.origin_server_ts();
|
let server_ts = event.origin_server_ts();
|
||||||
|
|
||||||
|
@ -172,19 +241,19 @@ pub async fn parse_notification(
|
||||||
.and_then(|m| m.display_name())
|
.and_then(|m| m.display_name())
|
||||||
.unwrap_or_else(|| sender_id.localpart());
|
.unwrap_or_else(|| sender_id.localpart());
|
||||||
|
|
||||||
let summary = if let Ok(room_name) = room.display_name().await {
|
let summary = if let Some(room_name) = room.cached_display_name() {
|
||||||
format!("{sender_name} in {room_name}")
|
if room.is_direct().await.map_err(IambError::from)? && sender_name == room_name.to_string()
|
||||||
|
{
|
||||||
|
sender_name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{sender_name} in {room_name}")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sender_name.to_string()
|
sender_name.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = if show_body {
|
let body = if show_body {
|
||||||
event_notification_body(
|
event_notification_body(&event, sender_name).map(truncate)
|
||||||
&event,
|
|
||||||
sender_name,
|
|
||||||
room.is_direct().await.map_err(IambError::from)?,
|
|
||||||
)
|
|
||||||
.map(truncate)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -192,11 +261,7 @@ pub async fn parse_notification(
|
||||||
return Ok((summary, body, server_ts));
|
return Ok((summary, body, server_ts));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn event_notification_body(
|
pub fn event_notification_body(event: &AnySyncTimelineEvent, sender_name: &str) -> Option<String> {
|
||||||
event: &AnySyncTimelineEvent,
|
|
||||||
sender_name: &str,
|
|
||||||
is_direct: bool,
|
|
||||||
) -> Option<String> {
|
|
||||||
let AnySyncTimelineEvent::MessageLike(event) = event else {
|
let AnySyncTimelineEvent::MessageLike(event) = event else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
@ -207,10 +272,7 @@ pub fn event_notification_body(
|
||||||
MessageType::Audio(_) => {
|
MessageType::Audio(_) => {
|
||||||
format!("{sender_name} sent an audio file.")
|
format!("{sender_name} sent an audio file.")
|
||||||
},
|
},
|
||||||
MessageType::Emote(content) => {
|
MessageType::Emote(content) => content.body,
|
||||||
let message = &content.body;
|
|
||||||
format!("{sender_name}: {message}")
|
|
||||||
},
|
|
||||||
MessageType::File(_) => {
|
MessageType::File(_) => {
|
||||||
format!("{sender_name} sent a file.")
|
format!("{sender_name} sent a file.")
|
||||||
},
|
},
|
||||||
|
@ -220,22 +282,9 @@ pub fn event_notification_body(
|
||||||
MessageType::Location(_) => {
|
MessageType::Location(_) => {
|
||||||
format!("{sender_name} sent their location.")
|
format!("{sender_name} sent their location.")
|
||||||
},
|
},
|
||||||
MessageType::Notice(content) => {
|
MessageType::Notice(content) => content.body,
|
||||||
let message = &content.body;
|
MessageType::ServerNotice(content) => content.body,
|
||||||
format!("{sender_name}: {message}")
|
MessageType::Text(content) => content.body,
|
||||||
},
|
|
||||||
MessageType::ServerNotice(content) => {
|
|
||||||
let message = &content.body;
|
|
||||||
format!("{sender_name}: {message}")
|
|
||||||
},
|
|
||||||
MessageType::Text(content) => {
|
|
||||||
if is_direct {
|
|
||||||
content.body
|
|
||||||
} else {
|
|
||||||
let message = &content.body;
|
|
||||||
format!("{sender_name}: {message}")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MessageType::Video(_) => {
|
MessageType::Video(_) => {
|
||||||
format!("{sender_name} sent a video.")
|
format!("{sender_name} sent a video.")
|
||||||
},
|
},
|
||||||
|
@ -254,7 +303,7 @@ pub fn event_notification_body(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn truncate(s: String) -> String {
|
fn truncate(s: String) -> String {
|
||||||
static MAX_LENGTH: usize = 100;
|
static MAX_LENGTH: usize = 5000;
|
||||||
if s.graphemes(true).count() > MAX_LENGTH {
|
if s.graphemes(true).count() > MAX_LENGTH {
|
||||||
let truncated: String = s.graphemes(true).take(MAX_LENGTH).collect();
|
let truncated: String = s.graphemes(true).take(MAX_LENGTH).collect();
|
||||||
truncated + "..."
|
truncated + "..."
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
media::{MediaFormat, MediaRequest},
|
media::{MediaFormat, MediaRequestParameters},
|
||||||
ruma::{
|
ruma::{
|
||||||
events::{
|
events::{
|
||||||
room::{
|
room::{
|
||||||
|
@ -63,7 +63,7 @@ pub fn spawn_insert_preview(
|
||||||
let img = download_or_load(event_id.to_owned(), source, media, cache_dir)
|
let img = download_or_load(event_id.to_owned(), source, media, cache_dir)
|
||||||
.await
|
.await
|
||||||
.map(std::io::Cursor::new)
|
.map(std::io::Cursor::new)
|
||||||
.map(image::io::Reader::new)
|
.map(image::ImageReader::new)
|
||||||
.map_err(IambError::Matrix)
|
.map_err(IambError::Matrix)
|
||||||
.and_then(|reader| reader.with_guessed_format().map_err(IambError::IOError))
|
.and_then(|reader| reader.with_guessed_format().map_err(IambError::IOError))
|
||||||
.and_then(|reader| reader.decode().map_err(IambError::Image));
|
.and_then(|reader| reader.decode().map_err(IambError::Image));
|
||||||
|
@ -157,7 +157,10 @@ async fn download_or_load(
|
||||||
},
|
},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
media
|
media
|
||||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
.get_media_content(
|
||||||
|
&MediaRequestParameters { source, format: MediaFormat::File },
|
||||||
|
true,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.and_then(|buffer| {
|
.and_then(|buffer| {
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
|
|
|
@ -137,7 +137,7 @@ pub fn mock_keys() -> HashMap<OwnedEventId, EventLocation> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mock_messages() -> Messages {
|
pub fn mock_messages() -> Messages {
|
||||||
let mut messages = Messages::default();
|
let mut messages = Messages::main();
|
||||||
|
|
||||||
messages.insert(MSG1_KEY.clone(), mock_message1());
|
messages.insert(MSG1_KEY.clone(), mock_message1());
|
||||||
messages.insert(MSG2_KEY.clone(), mock_message2());
|
messages.insert(MSG2_KEY.clone(), mock_message2());
|
||||||
|
@ -171,12 +171,14 @@ pub fn mock_tunables() -> TunableValues {
|
||||||
default_room: None,
|
default_room: None,
|
||||||
log_level: Level::INFO,
|
log_level: Level::INFO,
|
||||||
message_shortcode_display: false,
|
message_shortcode_display: false,
|
||||||
|
normal_after_send: true,
|
||||||
reaction_display: true,
|
reaction_display: true,
|
||||||
reaction_shortcode_display: false,
|
reaction_shortcode_display: false,
|
||||||
read_receipt_send: true,
|
read_receipt_send: true,
|
||||||
read_receipt_display: true,
|
read_receipt_display: true,
|
||||||
request_timeout: 120,
|
request_timeout: 120,
|
||||||
sort: SortOverrides::default().values(),
|
sort: SortOverrides::default().values(),
|
||||||
|
state_event_display: true,
|
||||||
typing_notice_send: true,
|
typing_notice_send: true,
|
||||||
typing_notice_display: true,
|
typing_notice_display: true,
|
||||||
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
||||||
|
@ -188,14 +190,17 @@ pub fn mock_tunables() -> TunableValues {
|
||||||
open_command: None,
|
open_command: None,
|
||||||
external_edit_file_suffix: String::from(".md"),
|
external_edit_file_suffix: String::from(".md"),
|
||||||
username_display: UserDisplayStyle::Username,
|
username_display: UserDisplayStyle::Username,
|
||||||
|
username_display_regex: Some(String::from(".*")),
|
||||||
message_user_color: false,
|
message_user_color: false,
|
||||||
|
mouse: Default::default(),
|
||||||
notifications: Notifications {
|
notifications: Notifications {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
via: NotifyVia::Desktop,
|
via: NotifyVia::default(),
|
||||||
show_message: true,
|
show_message: true,
|
||||||
},
|
},
|
||||||
image_preview: None,
|
image_preview: None,
|
||||||
user_gutter_width: 30,
|
user_gutter_width: 30,
|
||||||
|
tabstop: 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
//!
|
//!
|
||||||
//! Additionally, some of the iamb commands delegate behaviour to the current UI element. For
|
//! Additionally, some of the iamb commands delegate behaviour to the current UI element. For
|
||||||
//! example, [sending messages][crate::base::SendAction] delegate to the [room window][RoomState],
|
//! example, [sending messages][crate::base::SendAction] delegate to the [room window][RoomState],
|
||||||
//! where we have the message bar and room ID easily accesible and resetable.
|
//! where we have the message bar and room ID easily accessible and resettable.
|
||||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
@ -23,6 +23,7 @@ use matrix_sdk::{
|
||||||
RoomAliasId,
|
RoomAliasId,
|
||||||
RoomId,
|
RoomId,
|
||||||
},
|
},
|
||||||
|
RoomState as MatrixRoomState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
@ -75,11 +76,13 @@ use crate::base::{
|
||||||
SortFieldRoom,
|
SortFieldRoom,
|
||||||
SortFieldUser,
|
SortFieldUser,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
|
SpaceAction,
|
||||||
UnreadInfo,
|
UnreadInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::{room::RoomState, welcome::WelcomeState};
|
use self::{room::RoomState, welcome::WelcomeState};
|
||||||
use crate::message::MessageTimeStamp;
|
use crate::message::MessageTimeStamp;
|
||||||
|
use feruca::Collator;
|
||||||
|
|
||||||
pub mod room;
|
pub mod room;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
@ -168,7 +171,12 @@ fn user_cmp(a: &MemberItem, b: &MemberItem, field: &SortFieldUser) -> Ordering {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
|
fn room_cmp<T: RoomLikeItem>(
|
||||||
|
a: &T,
|
||||||
|
b: &T,
|
||||||
|
field: &SortFieldRoom,
|
||||||
|
collator: &mut Collator,
|
||||||
|
) -> Ordering {
|
||||||
match field {
|
match field {
|
||||||
SortFieldRoom::Favorite => {
|
SortFieldRoom::Favorite => {
|
||||||
let fava = a.has_tag(TagName::Favorite);
|
let fava = a.has_tag(TagName::Favorite);
|
||||||
|
@ -184,7 +192,7 @@ fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
|
||||||
// If a has LowPriority and b doesn't, it should sort later in room list.
|
// If a has LowPriority and b doesn't, it should sort later in room list.
|
||||||
lowa.cmp(&lowb)
|
lowa.cmp(&lowb)
|
||||||
},
|
},
|
||||||
SortFieldRoom::Name => a.name().cmp(b.name()),
|
SortFieldRoom::Name => collator.collate(a.name(), b.name()),
|
||||||
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias(), Ord::cmp),
|
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias(), Ord::cmp),
|
||||||
SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()),
|
SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()),
|
||||||
SortFieldRoom::Unread => {
|
SortFieldRoom::Unread => {
|
||||||
|
@ -195,6 +203,10 @@ fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
|
||||||
// sort larger timestamps towards the top.
|
// sort larger timestamps towards the top.
|
||||||
some_cmp(a.recent_ts(), b.recent_ts(), |a, b| b.cmp(a))
|
some_cmp(a.recent_ts(), b.recent_ts(), |a, b| b.cmp(a))
|
||||||
},
|
},
|
||||||
|
SortFieldRoom::Invite => {
|
||||||
|
// sort invites before other rooms.
|
||||||
|
b.is_invite().cmp(&a.is_invite())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,9 +215,10 @@ fn room_fields_cmp<T: RoomLikeItem>(
|
||||||
a: &T,
|
a: &T,
|
||||||
b: &T,
|
b: &T,
|
||||||
fields: &[SortColumn<SortFieldRoom>],
|
fields: &[SortColumn<SortFieldRoom>],
|
||||||
|
collator: &mut Collator,
|
||||||
) -> Ordering {
|
) -> Ordering {
|
||||||
for SortColumn(field, order) in fields {
|
for SortColumn(field, order) in fields {
|
||||||
match (room_cmp(a, b, field), order) {
|
match (room_cmp(a, b, field, collator), order) {
|
||||||
(Ordering::Equal, _) => continue,
|
(Ordering::Equal, _) => continue,
|
||||||
(o, SortOrder::Ascending) => return o,
|
(o, SortOrder::Ascending) => return o,
|
||||||
(o, SortOrder::Descending) => return o.reverse(),
|
(o, SortOrder::Descending) => return o.reverse(),
|
||||||
|
@ -213,7 +226,7 @@ fn room_fields_cmp<T: RoomLikeItem>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Break ties on ascending room id.
|
// Break ties on ascending room id.
|
||||||
room_cmp(a, b, &SortFieldRoom::RoomId)
|
room_cmp(a, b, &SortFieldRoom::RoomId, collator)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_fields_cmp(
|
fn user_fields_cmp(
|
||||||
|
@ -273,6 +286,7 @@ trait RoomLikeItem {
|
||||||
fn recent_ts(&self) -> Option<&MessageTimeStamp>;
|
fn recent_ts(&self) -> Option<&MessageTimeStamp>;
|
||||||
fn alias(&self) -> Option<&RoomAliasId>;
|
fn alias(&self) -> Option<&RoomAliasId>;
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
fn is_invite(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -354,6 +368,19 @@ impl IambWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn space_command(
|
||||||
|
&mut self,
|
||||||
|
act: SpaceAction,
|
||||||
|
ctx: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
if let IambWindow::Room(w) = self {
|
||||||
|
w.space_command(act, ctx, store).await
|
||||||
|
} else {
|
||||||
|
return Err(IambError::NoSelectedRoom.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn room_command(
|
pub async fn room_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
act: RoomAction,
|
act: RoomAction,
|
||||||
|
@ -496,7 +523,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||||
.map(|room_info| DirectItem::new(room_info, store))
|
.map(|room_info| DirectItem::new(room_info, store))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let fields = &store.application.settings.tunables.sort.dms;
|
let fields = &store.application.settings.tunables.sort.dms;
|
||||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
let collator = &mut store.application.collator;
|
||||||
|
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
|
||||||
state.set(items);
|
state.set(items);
|
||||||
|
|
||||||
|
@ -541,7 +569,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||||
.map(|room_info| RoomItem::new(room_info, store))
|
.map(|room_info| RoomItem::new(room_info, store))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let fields = &store.application.settings.tunables.sort.rooms;
|
let fields = &store.application.settings.tunables.sort.rooms;
|
||||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
let collator = &mut store.application.collator;
|
||||||
|
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
|
||||||
state.set(items);
|
state.set(items);
|
||||||
|
|
||||||
|
@ -572,7 +601,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||||
items.extend(dms);
|
items.extend(dms);
|
||||||
|
|
||||||
let fields = &store.application.settings.tunables.sort.chats;
|
let fields = &store.application.settings.tunables.sort.chats;
|
||||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
let collator = &mut store.application.collator;
|
||||||
|
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
|
||||||
state.set(items);
|
state.set(items);
|
||||||
|
|
||||||
|
@ -605,7 +635,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||||
items.extend(dms);
|
items.extend(dms);
|
||||||
|
|
||||||
let fields = &store.application.settings.tunables.sort.chats;
|
let fields = &store.application.settings.tunables.sort.chats;
|
||||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
let collator = &mut store.application.collator;
|
||||||
|
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
|
||||||
state.set(items);
|
state.set(items);
|
||||||
|
|
||||||
|
@ -625,7 +656,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||||
.map(|room| SpaceItem::new(room, store))
|
.map(|room| SpaceItem::new(room, store))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let fields = &store.application.settings.tunables.sort.spaces;
|
let fields = &store.application.settings.tunables.sort.spaces;
|
||||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
let collator = &mut store.application.collator;
|
||||||
|
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
|
||||||
state.set(items);
|
state.set(items);
|
||||||
|
|
||||||
|
@ -914,6 +946,10 @@ impl RoomLikeItem for GenericChatItem {
|
||||||
fn is_unread(&self) -> bool {
|
fn is_unread(&self) -> bool {
|
||||||
self.unread.is_unread()
|
self.unread.is_unread()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_invite(&self) -> bool {
|
||||||
|
self.room().state() == MatrixRoomState::Invited
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for GenericChatItem {
|
impl Display for GenericChatItem {
|
||||||
|
@ -1024,11 +1060,15 @@ impl RoomLikeItem for RoomItem {
|
||||||
fn is_unread(&self) -> bool {
|
fn is_unread(&self) -> bool {
|
||||||
self.unread.is_unread()
|
self.unread.is_unread()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_invite(&self) -> bool {
|
||||||
|
self.room().state() == MatrixRoomState::Invited
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for RoomItem {
|
impl Display for RoomItem {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, ":verify request {}", self.name)
|
write!(f, "{}", self.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1124,6 +1164,10 @@ impl RoomLikeItem for DirectItem {
|
||||||
fn is_unread(&self) -> bool {
|
fn is_unread(&self) -> bool {
|
||||||
self.unread.is_unread()
|
self.unread.is_unread()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_invite(&self) -> bool {
|
||||||
|
self.room().state() == MatrixRoomState::Invited
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for DirectItem {
|
impl Display for DirectItem {
|
||||||
|
@ -1223,11 +1267,15 @@ impl RoomLikeItem for SpaceItem {
|
||||||
// XXX: this needs to check whether the space contains rooms with unread messages
|
// XXX: this needs to check whether the space contains rooms with unread messages
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_invite(&self) -> bool {
|
||||||
|
self.room().state() == MatrixRoomState::Invited
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for SpaceItem {
|
impl Display for SpaceItem {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, ":verify request {}", self.room_id())
|
write!(f, "{}", self.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1556,6 +1604,7 @@ mod tests {
|
||||||
alias: Option<OwnedRoomAliasId>,
|
alias: Option<OwnedRoomAliasId>,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
unread: UnreadInfo,
|
unread: UnreadInfo,
|
||||||
|
invite: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoomLikeItem for &TestRoomItem {
|
impl RoomLikeItem for &TestRoomItem {
|
||||||
|
@ -1582,10 +1631,16 @@ mod tests {
|
||||||
fn is_unread(&self) -> bool {
|
fn is_unread(&self) -> bool {
|
||||||
self.unread.is_unread()
|
self.unread.is_unread()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_invite(&self) -> bool {
|
||||||
|
self.invite
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sort_rooms() {
|
fn test_sort_rooms() {
|
||||||
|
let mut collator = Collator::default();
|
||||||
|
let collator = &mut collator;
|
||||||
let server = server_name!("example.com");
|
let server = server_name!("example.com");
|
||||||
|
|
||||||
let room1 = TestRoomItem {
|
let room1 = TestRoomItem {
|
||||||
|
@ -1594,6 +1649,7 @@ mod tests {
|
||||||
alias: Some(room_alias_id!("#room1:example.com").to_owned()),
|
alias: Some(room_alias_id!("#room1:example.com").to_owned()),
|
||||||
name: "Z",
|
name: "Z",
|
||||||
unread: UnreadInfo::default(),
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let room2 = TestRoomItem {
|
let room2 = TestRoomItem {
|
||||||
|
@ -1602,6 +1658,7 @@ mod tests {
|
||||||
alias: Some(room_alias_id!("#a:example.com").to_owned()),
|
alias: Some(room_alias_id!("#a:example.com").to_owned()),
|
||||||
name: "Unnamed Room",
|
name: "Unnamed Room",
|
||||||
unread: UnreadInfo::default(),
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let room3 = TestRoomItem {
|
let room3 = TestRoomItem {
|
||||||
|
@ -1610,18 +1667,19 @@ mod tests {
|
||||||
alias: None,
|
alias: None,
|
||||||
name: "Cool Room",
|
name: "Cool Room",
|
||||||
unread: UnreadInfo::default(),
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort by Name ascending.
|
// Sort by Name ascending.
|
||||||
let mut rooms = vec![&room1, &room2, &room3];
|
let mut rooms = vec![&room1, &room2, &room3];
|
||||||
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Ascending)];
|
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Ascending)];
|
||||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
assert_eq!(rooms, vec![&room3, &room2, &room1]);
|
assert_eq!(rooms, vec![&room3, &room2, &room1]);
|
||||||
|
|
||||||
// Sort by Name descending.
|
// Sort by Name descending.
|
||||||
let mut rooms = vec![&room1, &room2, &room3];
|
let mut rooms = vec![&room1, &room2, &room3];
|
||||||
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Descending)];
|
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Descending)];
|
||||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
||||||
|
|
||||||
// Sort by Favorite and Alias before Name to show order matters.
|
// Sort by Favorite and Alias before Name to show order matters.
|
||||||
|
@ -1631,7 +1689,7 @@ mod tests {
|
||||||
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
||||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||||
];
|
];
|
||||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
||||||
|
|
||||||
// Now flip order of Favorite with Descending
|
// Now flip order of Favorite with Descending
|
||||||
|
@ -1641,12 +1699,14 @@ mod tests {
|
||||||
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
||||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||||
];
|
];
|
||||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
assert_eq!(rooms, vec![&room2, &room3, &room1]);
|
assert_eq!(rooms, vec![&room2, &room3, &room1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sort_room_recents() {
|
fn test_sort_room_recents() {
|
||||||
|
let mut collator = Collator::default();
|
||||||
|
let collator = &mut collator;
|
||||||
let server = server_name!("example.com");
|
let server = server_name!("example.com");
|
||||||
|
|
||||||
let room1 = TestRoomItem {
|
let room1 = TestRoomItem {
|
||||||
|
@ -1655,6 +1715,7 @@ mod tests {
|
||||||
alias: None,
|
alias: None,
|
||||||
name: "Room 1",
|
name: "Room 1",
|
||||||
unread: UnreadInfo { unread: false, latest: None },
|
unread: UnreadInfo { unread: false, latest: None },
|
||||||
|
invite: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let room2 = TestRoomItem {
|
let room2 = TestRoomItem {
|
||||||
|
@ -1666,6 +1727,7 @@ mod tests {
|
||||||
unread: false,
|
unread: false,
|
||||||
latest: Some(MessageTimeStamp::OriginServer(40u32.into())),
|
latest: Some(MessageTimeStamp::OriginServer(40u32.into())),
|
||||||
},
|
},
|
||||||
|
invite: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let room3 = TestRoomItem {
|
let room3 = TestRoomItem {
|
||||||
|
@ -1677,18 +1739,71 @@ mod tests {
|
||||||
unread: false,
|
unread: false,
|
||||||
latest: Some(MessageTimeStamp::OriginServer(20u32.into())),
|
latest: Some(MessageTimeStamp::OriginServer(20u32.into())),
|
||||||
},
|
},
|
||||||
|
invite: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort by Recent ascending.
|
// Sort by Recent ascending.
|
||||||
let mut rooms = vec![&room1, &room2, &room3];
|
let mut rooms = vec![&room1, &room2, &room3];
|
||||||
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Ascending)];
|
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Ascending)];
|
||||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
assert_eq!(rooms, vec![&room2, &room3, &room1]);
|
assert_eq!(rooms, vec![&room2, &room3, &room1]);
|
||||||
|
|
||||||
// Sort by Recent descending.
|
// Sort by Recent descending.
|
||||||
let mut rooms = vec![&room1, &room2, &room3];
|
let mut rooms = vec![&room1, &room2, &room3];
|
||||||
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Descending)];
|
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Descending)];
|
||||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
assert_eq!(rooms, vec![&room1, &room3, &room2]);
|
assert_eq!(rooms, vec![&room1, &room3, &room2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_room_invites() {
|
||||||
|
let mut collator = Collator::default();
|
||||||
|
let collator = &mut collator;
|
||||||
|
let server = server_name!("example.com");
|
||||||
|
|
||||||
|
let room1 = TestRoomItem {
|
||||||
|
room_id: RoomId::new(server).to_owned(),
|
||||||
|
tags: vec![],
|
||||||
|
alias: None,
|
||||||
|
name: "Old room 1",
|
||||||
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let room2 = TestRoomItem {
|
||||||
|
room_id: RoomId::new(server).to_owned(),
|
||||||
|
tags: vec![],
|
||||||
|
alias: None,
|
||||||
|
name: "Old room 2",
|
||||||
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let room3 = TestRoomItem {
|
||||||
|
room_id: RoomId::new(server).to_owned(),
|
||||||
|
tags: vec![],
|
||||||
|
alias: None,
|
||||||
|
name: "New Fancy Room",
|
||||||
|
unread: UnreadInfo::default(),
|
||||||
|
invite: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort invites first
|
||||||
|
let mut rooms = vec![&room1, &room2, &room3];
|
||||||
|
let fields = &[
|
||||||
|
SortColumn(SortFieldRoom::Invite, SortOrder::Ascending),
|
||||||
|
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||||
|
];
|
||||||
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
assert_eq!(rooms, vec![&room3, &room1, &room2]);
|
||||||
|
|
||||||
|
// Sort invites after
|
||||||
|
let mut rooms = vec![&room1, &room2, &room3];
|
||||||
|
let fields = &[
|
||||||
|
SortColumn(SortFieldRoom::Invite, SortOrder::Descending),
|
||||||
|
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||||
|
];
|
||||||
|
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use url::Url;
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
attachment::AttachmentConfig,
|
attachment::AttachmentConfig,
|
||||||
media::{MediaFormat, MediaRequest},
|
media::{MediaFormat, MediaRequestParameters},
|
||||||
room::Room as MatrixRoom,
|
room::Room as MatrixRoom,
|
||||||
ruma::{
|
ruma::{
|
||||||
events::reaction::ReactionEventContent,
|
events::reaction::ReactionEventContent,
|
||||||
|
@ -86,7 +86,14 @@ use crate::base::{
|
||||||
SendAction,
|
SendAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::message::{text_to_message, Message, MessageEvent, MessageKey, MessageTimeStamp};
|
use crate::message::{
|
||||||
|
text_to_message,
|
||||||
|
Message,
|
||||||
|
MessageEvent,
|
||||||
|
MessageKey,
|
||||||
|
MessageTimeStamp,
|
||||||
|
TreeGenState,
|
||||||
|
};
|
||||||
use crate::worker::Requester;
|
use crate::worker::Requester;
|
||||||
|
|
||||||
use super::scrollback::{Scrollback, ScrollbackState};
|
use super::scrollback::{Scrollback, ScrollbackState};
|
||||||
|
@ -226,10 +233,14 @@ impl ChatState {
|
||||||
|
|
||||||
let links = if let Some(html) = &msg.html {
|
let links = if let Some(html) = &msg.html {
|
||||||
html.get_links()
|
html.get_links()
|
||||||
} else if let Ok(url) = Url::parse(&msg.event.body()) {
|
|
||||||
vec![('0', url)]
|
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
linkify::LinkFinder::new()
|
||||||
|
.links(&msg.event.body())
|
||||||
|
.filter_map(|u| Url::parse(u.as_str()).ok())
|
||||||
|
.scan(TreeGenState { link_num: 0 }, |state, u| {
|
||||||
|
state.next_link_char().map(|c| (c, u))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
if links.is_empty() {
|
if links.is_empty() {
|
||||||
|
@ -276,7 +287,7 @@ impl ChatState {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filename.exists() || flags.contains(DownloadFlags::FORCE) {
|
if !filename.exists() || flags.contains(DownloadFlags::FORCE) {
|
||||||
let req = MediaRequest { source, format: MediaFormat::File };
|
let req = MediaRequestParameters { source, format: MediaFormat::File };
|
||||||
|
|
||||||
let bytes =
|
let bytes =
|
||||||
media.get_media_content(&req, true).await.map_err(IambError::from)?;
|
media.get_media_content(&req, true).await.map_err(IambError::from)?;
|
||||||
|
@ -380,6 +391,7 @@ impl ChatState {
|
||||||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||||
|
MessageEvent::State(ev) => ev.event_id().to_owned(),
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) => {
|
||||||
let msg = "Cannot react to a redacted message";
|
let msg = "Cannot react to a redacted message";
|
||||||
let err = UIError::Failure(msg.into());
|
let err = UIError::Failure(msg.into());
|
||||||
|
@ -417,6 +429,7 @@ impl ChatState {
|
||||||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||||
|
MessageEvent::State(ev) => ev.event_id().to_owned(),
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) => {
|
||||||
let msg = "Cannot redact already redacted message";
|
let msg = "Cannot redact already redacted message";
|
||||||
let err = UIError::Failure(msg.into());
|
let err = UIError::Failure(msg.into());
|
||||||
|
@ -464,6 +477,7 @@ impl ChatState {
|
||||||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||||
|
MessageEvent::State(ev) => ev.event_id().to_owned(),
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) => {
|
||||||
let msg = "Cannot unreact to a redacted message";
|
let msg = "Cannot unreact to a redacted message";
|
||||||
let err = UIError::Failure(msg.into());
|
let err = UIError::Failure(msg.into());
|
||||||
|
@ -596,7 +610,7 @@ impl ChatState {
|
||||||
let dynimage = image::DynamicImage::ImageRgba8(imagebuf);
|
let dynimage = image::DynamicImage::ImageRgba8(imagebuf);
|
||||||
let bytes = Vec::<u8>::new();
|
let bytes = Vec::<u8>::new();
|
||||||
let mut buff = std::io::Cursor::new(bytes);
|
let mut buff = std::io::Cursor::new(bytes);
|
||||||
dynimage.write_to(&mut buff, image::ImageOutputFormat::Png)?;
|
dynimage.write_to(&mut buff, image::ImageFormat::Png)?;
|
||||||
Ok(buff.into_inner())
|
Ok(buff.into_inner())
|
||||||
})
|
})
|
||||||
.map_err(IambError::from)?;
|
.map_err(IambError::from)?;
|
||||||
|
@ -606,7 +620,7 @@ impl ChatState {
|
||||||
let config = AttachmentConfig::new();
|
let config = AttachmentConfig::new();
|
||||||
|
|
||||||
let resp = room
|
let resp = room
|
||||||
.send_attachment(name.as_ref(), &mime, bytes, config)
|
.send_attachment(name, &mime, bytes, config)
|
||||||
.await
|
.await
|
||||||
.map_err(IambError::from)?;
|
.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
@ -635,10 +649,7 @@ impl ChatState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus_toggle(&mut self) {
|
pub fn focus_toggle(&mut self) {
|
||||||
self.focus = match self.focus {
|
self.focus.toggle();
|
||||||
RoomFocus::Scrollback => RoomFocus::MessageBar,
|
|
||||||
RoomFocus::MessageBar => RoomFocus::Scrollback,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room(&self) -> &MatrixRoom {
|
pub fn room(&self) -> &MatrixRoom {
|
||||||
|
@ -649,6 +660,14 @@ impl ChatState {
|
||||||
&self.room_id
|
&self.room_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auto_toggle_focus(
|
||||||
|
&mut self,
|
||||||
|
act: &EditorAction,
|
||||||
|
ctx: &ProgramContext,
|
||||||
|
) -> Option<EditorAction> {
|
||||||
|
auto_toggle_focus(&mut self.focus, act, ctx, &self.scrollback, &mut self.tbox)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn typing_notice(
|
pub fn typing_notice(
|
||||||
&self,
|
&self,
|
||||||
act: &EditorAction,
|
act: &EditorAction,
|
||||||
|
@ -751,8 +770,15 @@ impl Editable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
||||||
ctx: &ProgramContext,
|
ctx: &ProgramContext,
|
||||||
store: &mut ProgramStore,
|
store: &mut ProgramStore,
|
||||||
) -> EditResult<EditInfo, IambInfo> {
|
) -> EditResult<EditInfo, IambInfo> {
|
||||||
|
// Check whether we should automatically switch between the message bar
|
||||||
|
// or message scrollback, and use an adjusted action if we do so.
|
||||||
|
let adjusted = self.auto_toggle_focus(act, ctx);
|
||||||
|
let act = adjusted.as_ref().unwrap_or(act);
|
||||||
|
|
||||||
|
// Send typing notice if needed.
|
||||||
self.typing_notice(act, ctx, store);
|
self.typing_notice(act, ctx, store);
|
||||||
|
|
||||||
|
// And now we can finally run the editor command.
|
||||||
match delegate!(self, w => w.editor_command(act, ctx, store)) {
|
match delegate!(self, w => w.editor_command(act, ctx, store)) {
|
||||||
res @ Ok(_) => res,
|
res @ Ok(_) => res,
|
||||||
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, thread, focus)))
|
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, thread, focus)))
|
||||||
|
@ -849,16 +875,16 @@ impl PromptActions<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
||||||
|
|
||||||
fn recall(
|
fn recall(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
filter: &RecallFilter,
|
||||||
dir: &MoveDir1D,
|
dir: &MoveDir1D,
|
||||||
count: &Count,
|
count: &Count,
|
||||||
prefixed: bool,
|
|
||||||
ctx: &ProgramContext,
|
ctx: &ProgramContext,
|
||||||
_: &mut ProgramStore,
|
_: &mut ProgramStore,
|
||||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||||
let count = ctx.resolve(count);
|
let count = ctx.resolve(count);
|
||||||
let rope = self.tbox.get();
|
let rope = self.tbox.get();
|
||||||
|
|
||||||
let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, prefixed, count);
|
let text = self.sent.recall(&rope, &mut self.sent_scrollback, filter, *dir, count);
|
||||||
|
|
||||||
if let Some(text) = text {
|
if let Some(text) = text {
|
||||||
self.tbox.set_text(text);
|
self.tbox.set_text(text);
|
||||||
|
@ -882,9 +908,7 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
||||||
match act {
|
match act {
|
||||||
PromptAction::Submit => self.submit(ctx, store),
|
PromptAction::Submit => self.submit(ctx, store),
|
||||||
PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
|
PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
|
||||||
PromptAction::Recall(dir, count, prefixed) => {
|
PromptAction::Recall(filter, dir, count) => self.recall(filter, dir, count, ctx, store),
|
||||||
self.recall(dir, count, *prefixed, ctx, store)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -906,7 +930,7 @@ impl<'a> Chat<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StatefulWidget for Chat<'a> {
|
impl StatefulWidget for Chat<'_> {
|
||||||
type State = ChatState;
|
type State = ChatState;
|
||||||
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
@ -990,3 +1014,158 @@ fn cmd(open_command: &Vec<String>) -> Option<Command> {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auto_toggle_focus(
|
||||||
|
focus: &mut RoomFocus,
|
||||||
|
act: &EditorAction,
|
||||||
|
ctx: &ProgramContext,
|
||||||
|
scrollback: &ScrollbackState,
|
||||||
|
tbox: &mut TextBoxState<IambInfo>,
|
||||||
|
) -> Option<EditorAction> {
|
||||||
|
let is_insert = ctx.get_insert_style().is_some();
|
||||||
|
|
||||||
|
match (focus, act) {
|
||||||
|
(f @ RoomFocus::Scrollback, _) if is_insert => {
|
||||||
|
// Insert mode commands should switch focus.
|
||||||
|
f.toggle();
|
||||||
|
None
|
||||||
|
},
|
||||||
|
(f @ RoomFocus::Scrollback, EditorAction::InsertText(_)) => {
|
||||||
|
// Pasting or otherwise inserting text should switch.
|
||||||
|
f.toggle();
|
||||||
|
None
|
||||||
|
},
|
||||||
|
(
|
||||||
|
f @ RoomFocus::Scrollback,
|
||||||
|
EditorAction::Edit(
|
||||||
|
op,
|
||||||
|
EditTarget::Motion(mov @ MoveType::Line(MoveDir1D::Next), count),
|
||||||
|
),
|
||||||
|
) if ctx.resolve(op).is_motion() => {
|
||||||
|
let count = ctx.resolve(count);
|
||||||
|
|
||||||
|
if count > 0 && scrollback.is_latest() {
|
||||||
|
// Trying to move down a line when already at the end of room history should
|
||||||
|
// switch.
|
||||||
|
f.toggle();
|
||||||
|
|
||||||
|
// And decrement the count for the action.
|
||||||
|
let count = count.saturating_sub(1).into();
|
||||||
|
let target = EditTarget::Motion(mov.clone(), count);
|
||||||
|
let dec = EditorAction::Edit(op.clone(), target);
|
||||||
|
|
||||||
|
Some(dec)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(
|
||||||
|
f @ RoomFocus::MessageBar,
|
||||||
|
EditorAction::Edit(
|
||||||
|
op,
|
||||||
|
EditTarget::Motion(mov @ MoveType::Line(MoveDir1D::Previous), count),
|
||||||
|
),
|
||||||
|
) if !is_insert && ctx.resolve(op).is_motion() => {
|
||||||
|
let count = ctx.resolve(count);
|
||||||
|
|
||||||
|
if count > 0 && tbox.get_cursor().y == 0 {
|
||||||
|
// Trying to move up a line when already at the top of the msgbar should
|
||||||
|
// switch as long as we're not in Insert mode.
|
||||||
|
f.toggle();
|
||||||
|
|
||||||
|
// And decrement the count for the action.
|
||||||
|
let count = count.saturating_sub(1).into();
|
||||||
|
let target = EditTarget::Motion(mov.clone(), count);
|
||||||
|
let dec = EditorAction::Edit(op.clone(), target);
|
||||||
|
|
||||||
|
Some(dec)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(RoomFocus::Scrollback, _) | (RoomFocus::MessageBar, _) => {
|
||||||
|
// Do not switch.
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use modalkit::actions::{EditAction, InsertTextAction};
|
||||||
|
|
||||||
|
use crate::tests::{mock_store, TEST_ROOM1_ID};
|
||||||
|
|
||||||
|
macro_rules! move_line {
|
||||||
|
($dir: expr, $count: expr) => {
|
||||||
|
EditorAction::Edit(
|
||||||
|
EditAction::Motion.into(),
|
||||||
|
EditTarget::Motion(MoveType::Line($dir), $count.into()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_auto_focus() {
|
||||||
|
let mut store = mock_store().await;
|
||||||
|
let ctx = ProgramContext::default();
|
||||||
|
|
||||||
|
let room_id = TEST_ROOM1_ID.clone();
|
||||||
|
let scrollback = ScrollbackState::new(room_id.clone(), None);
|
||||||
|
|
||||||
|
let id = IambBufferId::Room(room_id, None, RoomFocus::MessageBar);
|
||||||
|
let ebuf = store.load_buffer(id);
|
||||||
|
let mut tbox = TextBoxState::new(ebuf);
|
||||||
|
|
||||||
|
// Start out focused on the scrollback.
|
||||||
|
let mut focused = RoomFocus::Scrollback;
|
||||||
|
|
||||||
|
// Inserting text toggles:
|
||||||
|
let act = EditorAction::InsertText(InsertTextAction::Type(
|
||||||
|
Char::from('a').into(),
|
||||||
|
MoveDir1D::Next,
|
||||||
|
1.into(),
|
||||||
|
));
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::MessageBar);
|
||||||
|
assert!(res.is_none());
|
||||||
|
|
||||||
|
// Going down in message bar doesn't toggle:
|
||||||
|
let act = move_line!(MoveDir1D::Next, 1);
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::MessageBar);
|
||||||
|
assert!(res.is_none());
|
||||||
|
|
||||||
|
// But going up will:
|
||||||
|
let act = move_line!(MoveDir1D::Previous, 1);
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::Scrollback);
|
||||||
|
assert_eq!(res, Some(move_line!(MoveDir1D::Previous, 0)));
|
||||||
|
|
||||||
|
// Going up in scrollback doesn't toggle:
|
||||||
|
let act = move_line!(MoveDir1D::Previous, 1);
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::Scrollback);
|
||||||
|
assert_eq!(res, None);
|
||||||
|
|
||||||
|
// And then go back down:
|
||||||
|
let act = move_line!(MoveDir1D::Next, 1);
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::MessageBar);
|
||||||
|
assert_eq!(res, Some(move_line!(MoveDir1D::Next, 0)));
|
||||||
|
|
||||||
|
// Go up 2 will go up 1 in scrollback:
|
||||||
|
let act = move_line!(MoveDir1D::Previous, 2);
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::Scrollback);
|
||||||
|
assert_eq!(res, Some(move_line!(MoveDir1D::Previous, 1)));
|
||||||
|
|
||||||
|
// Go down 3 will go down 2 in messagebar:
|
||||||
|
let act = move_line!(MoveDir1D::Next, 3);
|
||||||
|
let res = auto_toggle_focus(&mut focused, &act, &ctx, &scrollback, &mut tbox);
|
||||||
|
assert_eq!(focused, RoomFocus::MessageBar);
|
||||||
|
assert_eq!(res, Some(move_line!(MoveDir1D::Next, 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ use matrix_sdk::{
|
||||||
OwnedUserId,
|
OwnedUserId,
|
||||||
RoomId,
|
RoomId,
|
||||||
},
|
},
|
||||||
DisplayName,
|
RoomDisplayName,
|
||||||
RoomState as MatrixRoomState,
|
RoomState as MatrixRoomState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,6 +66,7 @@ use crate::base::{
|
||||||
RoomAction,
|
RoomAction,
|
||||||
RoomField,
|
RoomField,
|
||||||
SendAction,
|
SendAction,
|
||||||
|
SpaceAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::chat::ChatState;
|
use self::chat::ChatState;
|
||||||
|
@ -139,7 +140,7 @@ impl RoomState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
thread: Option<OwnedEventId>,
|
thread: Option<OwnedEventId>,
|
||||||
name: DisplayName,
|
name: RoomDisplayName,
|
||||||
tags: Option<Tags>,
|
tags: Option<Tags>,
|
||||||
store: &mut ProgramStore,
|
store: &mut ProgramStore,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -214,6 +215,18 @@ impl RoomState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn space_command(
|
||||||
|
&mut self,
|
||||||
|
act: SpaceAction,
|
||||||
|
ctx: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
match self {
|
||||||
|
RoomState::Space(space) => space.space_command(act, ctx, store).await,
|
||||||
|
RoomState::Chat(_) => Err(IambError::NoSelectedSpace.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_command(
|
pub async fn send_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
act: SendAction,
|
act: SendAction,
|
||||||
|
@ -406,7 +419,7 @@ impl RoomState {
|
||||||
// Try creating the room alias on the server.
|
// Try creating the room alias on the server.
|
||||||
let alias_create_req =
|
let alias_create_req =
|
||||||
CreateAliasRequest::new(orai.clone(), room.room_id().into());
|
CreateAliasRequest::new(orai.clone(), room.room_id().into());
|
||||||
if let Err(e) = client.send(alias_create_req, None).await {
|
if let Err(e) = client.send(alias_create_req).await {
|
||||||
if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() {
|
if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() {
|
||||||
// Ignore when it already exists.
|
// Ignore when it already exists.
|
||||||
} else {
|
} else {
|
||||||
|
@ -447,7 +460,7 @@ impl RoomState {
|
||||||
|
|
||||||
// If the room alias does not exist on the server, create it
|
// If the room alias does not exist on the server, create it
|
||||||
let alias_create_req = CreateAliasRequest::new(orai, room.room_id().into());
|
let alias_create_req = CreateAliasRequest::new(orai, room.room_id().into());
|
||||||
if let Err(e) = client.send(alias_create_req, None).await {
|
if let Err(e) = client.send(alias_create_req).await {
|
||||||
if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() {
|
if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() {
|
||||||
// Ignore when it already exists.
|
// Ignore when it already exists.
|
||||||
} else {
|
} else {
|
||||||
|
@ -464,6 +477,9 @@ impl RoomState {
|
||||||
RoomField::Aliases => {
|
RoomField::Aliases => {
|
||||||
// This never happens, aliases is only used for showing
|
// This never happens, aliases is only used for showing
|
||||||
},
|
},
|
||||||
|
RoomField::Id => {
|
||||||
|
// This never happens, id is only used for showing
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
|
@ -519,7 +535,7 @@ impl RoomState {
|
||||||
.application
|
.application
|
||||||
.worker
|
.worker
|
||||||
.client
|
.client
|
||||||
.send(del_req, None)
|
.send(del_req)
|
||||||
.await
|
.await
|
||||||
.map_err(IambError::from)?;
|
.map_err(IambError::from)?;
|
||||||
},
|
},
|
||||||
|
@ -552,13 +568,16 @@ impl RoomState {
|
||||||
.application
|
.application
|
||||||
.worker
|
.worker
|
||||||
.client
|
.client
|
||||||
.send(del_req, None)
|
.send(del_req)
|
||||||
.await
|
.await
|
||||||
.map_err(IambError::from)?;
|
.map_err(IambError::from)?;
|
||||||
},
|
},
|
||||||
RoomField::Aliases => {
|
RoomField::Aliases => {
|
||||||
// This will not happen, you cannot unset all aliases
|
// This will not happen, you cannot unset all aliases
|
||||||
},
|
},
|
||||||
|
RoomField::Id => {
|
||||||
|
// This never happens, id is only used for showing
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
|
@ -572,7 +591,12 @@ impl RoomState {
|
||||||
let msg = match field {
|
let msg = match field {
|
||||||
RoomField::History => {
|
RoomField::History => {
|
||||||
let visibility = room.history_visibility();
|
let visibility = room.history_visibility();
|
||||||
format!("Room history visibility: {visibility}")
|
let visibility = visibility.as_ref().map(|v| v.as_str());
|
||||||
|
format!("Room history visibility: {}", visibility.unwrap_or("<unknown>"))
|
||||||
|
},
|
||||||
|
RoomField::Id => {
|
||||||
|
let id = room.room_id();
|
||||||
|
format!("Room identifier: {id}")
|
||||||
},
|
},
|
||||||
RoomField::Name => {
|
RoomField::Name => {
|
||||||
match room.name() {
|
match room.name() {
|
||||||
|
|
|
@ -79,14 +79,20 @@ fn nth_key_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nth_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
fn nth_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
||||||
nth_key_before(pos, n, thread).into()
|
let key = nth_key_before(pos, n, thread);
|
||||||
|
|
||||||
|
if matches!(thread.last_key_value(), Some((last, _)) if &key == last) {
|
||||||
|
MessageCursor::latest()
|
||||||
|
} else {
|
||||||
|
MessageCursor::from(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> Option<MessageKey> {
|
||||||
let mut end = &pos;
|
let mut end = &pos;
|
||||||
let iter = thread.range(&pos..).enumerate();
|
let mut iter = thread.range(&pos..).enumerate();
|
||||||
|
|
||||||
for (i, (key, _)) in iter {
|
for (i, (key, _)) in iter.by_ref() {
|
||||||
end = key;
|
end = key;
|
||||||
|
|
||||||
if i >= n {
|
if i >= n {
|
||||||
|
@ -94,11 +100,12 @@ fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
end.clone()
|
// Avoid returning the key if it's at the end.
|
||||||
|
iter.next().map(|_| end.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nth_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
fn nth_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
||||||
nth_key_after(pos, n, thread).into()
|
nth_key_after(pos, n, thread).map(MessageCursor::from).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prevmsg<'a>(key: &MessageKey, thread: &'a Messages) -> Option<&'a Message> {
|
fn prevmsg<'a>(key: &MessageKey, thread: &'a Messages) -> Option<&'a Message> {
|
||||||
|
@ -150,6 +157,10 @@ impl ScrollbackState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_latest(&self) -> bool {
|
||||||
|
self.cursor.timestamp.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn goto_latest(&mut self) {
|
pub fn goto_latest(&mut self) {
|
||||||
self.cursor = MessageCursor::latest();
|
self.cursor = MessageCursor::latest();
|
||||||
}
|
}
|
||||||
|
@ -829,8 +840,8 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
|
|
||||||
fn complete(
|
fn complete(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
_: &CompletionStyle,
|
||||||
_: &CompletionType,
|
_: &CompletionType,
|
||||||
_: &CompletionSelection,
|
|
||||||
_: &CompletionDisplay,
|
_: &CompletionDisplay,
|
||||||
_: &ProgramContext,
|
_: &ProgramContext,
|
||||||
_: &mut ProgramStore,
|
_: &mut ProgramStore,
|
||||||
|
@ -1284,7 +1295,7 @@ impl<'a> Scrollback<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StatefulWidget for Scrollback<'a> {
|
impl StatefulWidget for Scrollback<'_> {
|
||||||
type State = ScrollbackState;
|
type State = ScrollbackState;
|
||||||
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
@ -1340,7 +1351,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
|
|
||||||
for (key, item) in thread.range(&corner_key..) {
|
for (key, item) in thread.range(&corner_key..) {
|
||||||
let sel = key == cursor_key;
|
let sel = key == cursor_key;
|
||||||
let (txt, mut msg_preview) =
|
let (txt, [mut msg_preview, mut reply_preview]) =
|
||||||
item.show_with_preview(prev, foc && sel, &state.viewctx, info, settings);
|
item.show_with_preview(prev, foc && sel, &state.viewctx, info, settings);
|
||||||
|
|
||||||
let incomplete_ok = !full || !sel;
|
let incomplete_ok = !full || !sel;
|
||||||
|
@ -1357,11 +1368,17 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only take the preview into the matching row number.
|
||||||
|
// `reply` and `msg` previews are on rows,
|
||||||
|
// so an `or` works to pick the one that matches (if any)
|
||||||
let line_preview = match msg_preview {
|
let line_preview = match msg_preview {
|
||||||
// Only take the preview into the matching row number.
|
|
||||||
Some((_, _, y)) if y as usize == row => msg_preview.take(),
|
Some((_, _, y)) if y as usize == row => msg_preview.take(),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
}
|
||||||
|
.or(match reply_preview {
|
||||||
|
Some((_, _, y)) if y as usize == row => reply_preview.take(),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
lines.push((key, row, line, line_preview));
|
lines.push((key, row, line, line_preview));
|
||||||
sawit |= sel;
|
sawit |= sel;
|
||||||
|
@ -1396,7 +1413,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
// line.
|
// line.
|
||||||
for (x, y, backend) in image_previews {
|
for (x, y, backend) in image_previews {
|
||||||
let image_widget = Image::new(backend);
|
let image_widget = Image::new(backend);
|
||||||
let mut rect = backend.rect();
|
let mut rect = backend.area();
|
||||||
rect.x = x;
|
rect.x = x;
|
||||||
rect.y = y;
|
rect.y = y;
|
||||||
// Don't render outside of scrollback area
|
// Don't render outside of scrollback area
|
||||||
|
@ -1411,7 +1428,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
{
|
{
|
||||||
// If the cursor is at the last message, then update the read marker.
|
// If the cursor is at the last message, then update the read marker.
|
||||||
if let Some((k, _)) = thread.last_key_value() {
|
if let Some((k, _)) = thread.last_key_value() {
|
||||||
info.set_receipt(settings.profile.user_id.clone(), k.1.clone());
|
info.set_receipt(thread.1.clone(), settings.profile.user_id.clone(), k.1.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1518,8 +1535,9 @@ mod tests {
|
||||||
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
||||||
assert_eq!(scrollback.cursor, MSG5_KEY.clone().into());
|
assert_eq!(scrollback.cursor, MSG5_KEY.clone().into());
|
||||||
|
|
||||||
|
// And one more becomes "latest" cursor:
|
||||||
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
||||||
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -1553,7 +1571,7 @@ mod tests {
|
||||||
// MSG1: | XXXday, Month NN 20XX |
|
// MSG1: | XXXday, Month NN 20XX |
|
||||||
// | @user1:example.com writhe |
|
// | @user1:example.com writhe |
|
||||||
// |------------------------------------------------------------|
|
// |------------------------------------------------------------|
|
||||||
let area = Rect::new(0, 0, 60, 4);
|
let area = Rect::new(0, 0, 60, 5);
|
||||||
let mut buffer = Buffer::empty(area);
|
let mut buffer = Buffer::empty(area);
|
||||||
scrollback.draw(area, &mut buffer, true, &mut store);
|
scrollback.draw(area, &mut buffer, true, &mut store);
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
|
||||||
|
use matrix_sdk::ruma::events::StateEventType;
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
room::Room as MatrixRoom,
|
room::Room as MatrixRoom,
|
||||||
ruma::{OwnedRoomId, RoomId},
|
ruma::{OwnedRoomId, RoomId},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use modalkit::prelude::{EditInfo, InfoMessage};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
@ -22,9 +25,18 @@ use modalkit_ratatui::{
|
||||||
WindowOps,
|
WindowOps,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus};
|
use crate::base::{
|
||||||
|
IambBufferId,
|
||||||
|
IambError,
|
||||||
|
IambInfo,
|
||||||
|
IambResult,
|
||||||
|
ProgramContext,
|
||||||
|
ProgramStore,
|
||||||
|
RoomFocus,
|
||||||
|
SpaceAction,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::windows::{room_fields_cmp, RoomItem};
|
use crate::windows::{room_fields_cmp, RoomItem, RoomLikeItem};
|
||||||
|
|
||||||
const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5);
|
const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
@ -68,6 +80,71 @@ impl SpaceState {
|
||||||
last_fetch: self.last_fetch,
|
last_fetch: self.last_fetch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn space_command(
|
||||||
|
&mut self,
|
||||||
|
act: SpaceAction,
|
||||||
|
_: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
match act {
|
||||||
|
SpaceAction::SetChild(child_id, order, suggested) => {
|
||||||
|
if !self
|
||||||
|
.room
|
||||||
|
.can_user_send_state(
|
||||||
|
&store.application.settings.profile.user_id,
|
||||||
|
StateEventType::SpaceChild,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(IambError::from)?
|
||||||
|
{
|
||||||
|
return Err(IambError::InsufficientPermission.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let via = self.room.route().await.map_err(IambError::from)?;
|
||||||
|
let mut ev = SpaceChildEventContent::new(via);
|
||||||
|
ev.order = order;
|
||||||
|
ev.suggested = suggested;
|
||||||
|
let _ = self
|
||||||
|
.room
|
||||||
|
.send_state_event_for_key(&child_id, ev)
|
||||||
|
.await
|
||||||
|
.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
Ok(InfoMessage::from("Space updated").into())
|
||||||
|
},
|
||||||
|
SpaceAction::RemoveChild => {
|
||||||
|
let space = self.list.get().ok_or(IambError::NoSelectedRoomOrSpaceItem)?;
|
||||||
|
if !self
|
||||||
|
.room
|
||||||
|
.can_user_send_state(
|
||||||
|
&store.application.settings.profile.user_id,
|
||||||
|
StateEventType::SpaceChild,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(IambError::from)?
|
||||||
|
{
|
||||||
|
return Err(IambError::InsufficientPermission.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ev = SpaceChildEventContent::new(vec![]);
|
||||||
|
let event_id = self
|
||||||
|
.room
|
||||||
|
.send_state_event_for_key(&space.room_id().to_owned(), ev)
|
||||||
|
.await
|
||||||
|
.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
// Fix for element (see https://github.com/element-hq/element-web/issues/29606)
|
||||||
|
let _ = self
|
||||||
|
.room
|
||||||
|
.redact(&event_id.event_id, Some("workaround for element bug"), None)
|
||||||
|
.await
|
||||||
|
.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
Ok(InfoMessage::from("Room removed").into())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalCursor for SpaceState {
|
impl TerminalCursor for SpaceState {
|
||||||
|
@ -107,7 +184,7 @@ impl<'a> Space<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> StatefulWidget for Space<'a> {
|
impl StatefulWidget for Space<'_> {
|
||||||
type State = SpaceState;
|
type State = SpaceState;
|
||||||
|
|
||||||
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) {
|
||||||
|
@ -137,7 +214,8 @@ impl<'a> StatefulWidget for Space<'a> {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let fields = &self.store.application.settings.tunables.sort.rooms;
|
let fields = &self.store.application.settings.tunables.sort.rooms;
|
||||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
let collator = &mut self.store.application.collator;
|
||||||
|
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||||
|
|
||||||
state.list.set(items);
|
state.list.set(items);
|
||||||
state.last_fetch = Some(Instant::now());
|
state.last_fetch = Some(Instant::now());
|
||||||
|
|
129
src/worker.rs
129
src/worker.rs
|
@ -20,11 +20,12 @@ use tracing::{error, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
|
authentication::matrix::MatrixSession,
|
||||||
config::{RequestConfig, SyncSettings},
|
config::{RequestConfig, SyncSettings},
|
||||||
|
deserialized_responses::DisplayName,
|
||||||
encryption::verification::{SasVerification, Verification},
|
encryption::verification::{SasVerification, Verification},
|
||||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||||
event_handler::Ctx,
|
event_handler::Ctx,
|
||||||
matrix_auth::MatrixSession,
|
|
||||||
reqwest,
|
reqwest,
|
||||||
room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
|
room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
|
||||||
ruma::{
|
ruma::{
|
||||||
|
@ -58,6 +59,7 @@ use matrix_sdk::{
|
||||||
typing::SyncTypingEvent,
|
typing::SyncTypingEvent,
|
||||||
AnyInitialStateEvent,
|
AnyInitialStateEvent,
|
||||||
AnyMessageLikeEvent,
|
AnyMessageLikeEvent,
|
||||||
|
AnySyncStateEvent,
|
||||||
AnyTimelineEvent,
|
AnyTimelineEvent,
|
||||||
EmptyStateKey,
|
EmptyStateKey,
|
||||||
InitialStateEvent,
|
InitialStateEvent,
|
||||||
|
@ -78,8 +80,8 @@ use matrix_sdk::{
|
||||||
},
|
},
|
||||||
Client,
|
Client,
|
||||||
ClientBuildError,
|
ClientBuildError,
|
||||||
DisplayName,
|
|
||||||
Error as MatrixError,
|
Error as MatrixError,
|
||||||
|
RoomDisplayName,
|
||||||
RoomMemberships,
|
RoomMemberships,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,8 +116,7 @@ const IAMB_DEVICE_NAME: &str = "iamb";
|
||||||
const IAMB_USER_AGENT: &str = "iamb";
|
const IAMB_USER_AGENT: &str = "iamb";
|
||||||
const MIN_MSG_LOAD: u32 = 50;
|
const MIN_MSG_LOAD: u32 = 50;
|
||||||
|
|
||||||
type MessageFetchResult =
|
type MessageFetchResult = IambResult<(Option<String>, Vec<(AnyTimelineEvent, Vec<OwnedUserId>)>)>;
|
||||||
IambResult<(Option<String>, Vec<(AnyMessageLikeEvent, Vec<OwnedUserId>)>)>;
|
|
||||||
|
|
||||||
fn initial_devname() -> String {
|
fn initial_devname() -> String {
|
||||||
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
||||||
|
@ -209,7 +210,7 @@ async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id:
|
||||||
};
|
};
|
||||||
|
|
||||||
for (user_id, _) in receipts {
|
for (user_id, _) in receipts {
|
||||||
info.set_receipt(user_id, event_id.to_owned());
|
info.set_receipt(ReceiptThread::Main, user_id, event_id.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,10 +294,8 @@ async fn load_older_one(
|
||||||
let mut msgs = vec![];
|
let mut msgs = vec![];
|
||||||
|
|
||||||
for ev in chunk.into_iter() {
|
for ev in chunk.into_iter() {
|
||||||
let msg = match ev.event.deserialize() {
|
let Ok(msg) = ev.into_raw().deserialize() else {
|
||||||
Ok(AnyTimelineEvent::MessageLike(msg)) => msg,
|
continue;
|
||||||
Ok(AnyTimelineEvent::State(_)) => continue,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let event_id = msg.event_id();
|
let event_id = msg.event_id();
|
||||||
|
@ -311,6 +310,7 @@ async fn load_older_one(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let msg = msg.into_full_event(room_id.to_owned());
|
||||||
msgs.push((msg, receipts));
|
msgs.push((msg, receipts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,27 +338,34 @@ fn load_insert(
|
||||||
let _ = presences.get_or_default(sender);
|
let _ = presences.get_or_default(sender);
|
||||||
|
|
||||||
for user_id in receipts {
|
for user_id in receipts {
|
||||||
info.set_receipt(user_id, msg.event_id().to_owned());
|
info.set_receipt(ReceiptThread::Main, user_id, msg.event_id().to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
AnyMessageLikeEvent::RoomEncrypted(msg) => {
|
AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomEncrypted(msg)) => {
|
||||||
info.insert_encrypted(msg);
|
info.insert_encrypted(msg);
|
||||||
},
|
},
|
||||||
AnyMessageLikeEvent::RoomMessage(msg) => {
|
AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(msg)) => {
|
||||||
info.insert_with_preview(
|
info.insert_with_preview(
|
||||||
room_id.clone(),
|
room_id.clone(),
|
||||||
store.clone(),
|
store.clone(),
|
||||||
*picker,
|
picker.clone(),
|
||||||
msg,
|
msg,
|
||||||
settings,
|
settings,
|
||||||
client.media(),
|
client.media(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
AnyMessageLikeEvent::Reaction(ev) => {
|
AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::Reaction(ev)) => {
|
||||||
info.insert_reaction(ev);
|
info.insert_reaction(ev);
|
||||||
},
|
},
|
||||||
_ => continue,
|
AnyTimelineEvent::MessageLike(_) => {
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
AnyTimelineEvent::State(msg) => {
|
||||||
|
if settings.tunables.state_event_display {
|
||||||
|
info.insert_any_state(msg.into());
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,7 +447,7 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
|
||||||
let mut dms = vec![];
|
let mut dms = vec![];
|
||||||
|
|
||||||
for room in client.invited_rooms().into_iter() {
|
for room in client.invited_rooms().into_iter() {
|
||||||
let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string();
|
let name = room.cached_display_name().unwrap_or(RoomDisplayName::Empty).to_string();
|
||||||
let tags = room.tags().await.unwrap_or_default();
|
let tags = room.tags().await.unwrap_or_default();
|
||||||
|
|
||||||
names.push((room.room_id().to_owned(), name));
|
names.push((room.room_id().to_owned(), name));
|
||||||
|
@ -455,7 +462,7 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for room in client.joined_rooms().into_iter() {
|
for room in client.joined_rooms().into_iter() {
|
||||||
let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string();
|
let name = room.cached_display_name().unwrap_or(RoomDisplayName::Empty).to_string();
|
||||||
let tags = room.tags().await.unwrap_or_default();
|
let tags = room.tags().await.unwrap_or_default();
|
||||||
|
|
||||||
names.push((room.room_id().to_owned(), name));
|
names.push((room.room_id().to_owned(), name));
|
||||||
|
@ -490,31 +497,36 @@ async fn refresh_rooms_forever(client: &Client, store: &AsyncProgramStore) {
|
||||||
|
|
||||||
async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
|
async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(2));
|
let mut interval = tokio::time::interval(Duration::from_secs(2));
|
||||||
let mut sent = HashMap::<OwnedRoomId, OwnedEventId>::default();
|
let mut sent: HashMap<OwnedRoomId, HashMap<ReceiptThread, OwnedEventId>> = Default::default();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
let locked = store.lock().await;
|
let mut locked = store.lock().await;
|
||||||
let user_id = &locked.application.settings.profile.user_id;
|
let ChatStore { settings, open_notifications, rooms, .. } = &mut locked.application;
|
||||||
let updates = client
|
let user_id = &settings.profile.user_id;
|
||||||
.joined_rooms()
|
|
||||||
.into_iter()
|
let mut updates = Vec::new();
|
||||||
.filter_map(|room| {
|
for room in client.joined_rooms() {
|
||||||
let room_id = room.room_id().to_owned();
|
let room_id = room.room_id();
|
||||||
let info = locked.application.rooms.get(&room_id)?;
|
let Some(info) = rooms.get(room_id) else {
|
||||||
let new_receipt = info.get_receipt(user_id)?;
|
continue;
|
||||||
let old_receipt = sent.get(&room_id);
|
};
|
||||||
if Some(new_receipt) != old_receipt {
|
|
||||||
Some((room_id, new_receipt.clone()))
|
let changed = info.receipts(user_id).filter_map(|(thread, new_receipt)| {
|
||||||
} else {
|
let old_receipt = sent.get(room_id).and_then(|ts| ts.get(thread));
|
||||||
None
|
let changed = Some(new_receipt) != old_receipt;
|
||||||
|
if changed {
|
||||||
|
open_notifications.remove(room_id);
|
||||||
}
|
}
|
||||||
})
|
changed.then(|| (room_id.to_owned(), thread.to_owned(), new_receipt.to_owned()))
|
||||||
.collect::<Vec<_>>();
|
});
|
||||||
|
|
||||||
|
updates.extend(changed);
|
||||||
|
}
|
||||||
drop(locked);
|
drop(locked);
|
||||||
|
|
||||||
for (room_id, new_receipt) in updates {
|
for (room_id, thread, new_receipt) in updates {
|
||||||
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
|
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
|
||||||
|
|
||||||
let Some(room) = client.get_room(&room_id) else {
|
let Some(room) = client.get_room(&room_id) else {
|
||||||
|
@ -522,15 +534,11 @@ async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
|
||||||
};
|
};
|
||||||
|
|
||||||
match room
|
match room
|
||||||
.send_single_receipt(
|
.send_single_receipt(ReceiptType::Read, thread.to_owned(), new_receipt.clone())
|
||||||
ReceiptType::Read,
|
|
||||||
ReceiptThread::Unthreaded,
|
|
||||||
new_receipt.clone(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
sent.insert(room_id, new_receipt);
|
sent.entry(room_id).or_default().insert(thread, new_receipt);
|
||||||
},
|
},
|
||||||
Err(e) => tracing::warn!(?room_id, "Failed to set read receipt: {e}"),
|
Err(e) => tracing::warn!(?room_id, "Failed to set read receipt: {e}"),
|
||||||
}
|
}
|
||||||
|
@ -603,7 +611,7 @@ fn oneshot<T>() -> (ClientReply<T>, ClientResponse<T>) {
|
||||||
return (reply, response);
|
return (reply, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FetchedRoom = (MatrixRoom, DisplayName, Option<Tags>);
|
pub type FetchedRoom = (MatrixRoom, RoomDisplayName, Option<Tags>);
|
||||||
|
|
||||||
pub enum WorkerTask {
|
pub enum WorkerTask {
|
||||||
Init(AsyncProgramStore, ClientReply<()>),
|
Init(AsyncProgramStore, ClientReply<()>),
|
||||||
|
@ -1001,7 +1009,7 @@ impl ClientWorker {
|
||||||
info.insert_with_preview(
|
info.insert_with_preview(
|
||||||
room_id.to_owned(),
|
room_id.to_owned(),
|
||||||
store.clone(),
|
store.clone(),
|
||||||
*picker,
|
picker.clone(),
|
||||||
full_ev,
|
full_ev,
|
||||||
settings,
|
settings,
|
||||||
client.media(),
|
client.media(),
|
||||||
|
@ -1043,14 +1051,32 @@ impl ClientWorker {
|
||||||
let Some(receipts) = receipts.get(&ReceiptType::Read) else {
|
let Some(receipts) = receipts.get(&ReceiptType::Read) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for user_id in receipts.keys() {
|
for (user_id, rcpt) in receipts.iter() {
|
||||||
info.set_receipt(user_id.to_owned(), event_id.clone());
|
info.set_receipt(
|
||||||
|
rcpt.thread.clone(),
|
||||||
|
user_id.to_owned(),
|
||||||
|
event_id.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if self.settings.tunables.state_event_display {
|
||||||
|
let _ = self.client.add_event_handler(
|
||||||
|
|ev: AnySyncStateEvent, room: MatrixRoom, store: Ctx<AsyncProgramStore>| {
|
||||||
|
async move {
|
||||||
|
let room_id = room.room_id();
|
||||||
|
let mut locked = store.lock().await;
|
||||||
|
|
||||||
|
let info = locked.application.get_room_info(room_id.to_owned());
|
||||||
|
info.insert_any_state(ev);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let _ = self.client.add_event_handler(
|
let _ = self.client.add_event_handler(
|
||||||
|ev: OriginalSyncRoomRedactionEvent,
|
|ev: OriginalSyncRoomRedactionEvent,
|
||||||
room: MatrixRoom,
|
room: MatrixRoom,
|
||||||
|
@ -1076,11 +1102,12 @@ impl ClientWorker {
|
||||||
let room_id = room.room_id();
|
let room_id = room.room_id();
|
||||||
let user_id = ev.state_key;
|
let user_id = ev.state_key;
|
||||||
|
|
||||||
let ambiguous_name =
|
let ambiguous_name = DisplayName::new(
|
||||||
ev.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart());
|
ev.content.displayname.as_deref().unwrap_or_else(|| user_id.as_str()),
|
||||||
|
);
|
||||||
let ambiguous = client
|
let ambiguous = client
|
||||||
.store()
|
.store()
|
||||||
.get_users_with_display_name(room_id, ambiguous_name)
|
.get_users_with_display_name(room_id, &ambiguous_name)
|
||||||
.await
|
.await
|
||||||
.map(|users| users.len() > 1)
|
.map(|users| users.len() > 1)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
@ -1309,7 +1336,7 @@ impl ClientWorker {
|
||||||
// Remove the session.json file.
|
// Remove the session.json file.
|
||||||
std::fs::remove_file(&self.settings.session_json)?;
|
std::fs::remove_file(&self.settings.session_json)?;
|
||||||
|
|
||||||
Ok(Some(InfoMessage::from("Sucessfully logged out")))
|
Ok(Some(InfoMessage::from("Successfully logged out")))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<OwnedRoomId> {
|
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<OwnedRoomId> {
|
||||||
|
@ -1346,7 +1373,7 @@ impl ClientWorker {
|
||||||
|
|
||||||
async fn get_room(&mut self, room_id: OwnedRoomId) -> IambResult<FetchedRoom> {
|
async fn get_room(&mut self, room_id: OwnedRoomId) -> IambResult<FetchedRoom> {
|
||||||
if let Some(room) = self.client.get_room(&room_id) {
|
if let Some(room) = self.client.get_room(&room_id) {
|
||||||
let name = room.display_name().await.map_err(IambError::from)?;
|
let name = room.cached_display_name().ok_or_else(|| IambError::UnknownRoom(room_id))?;
|
||||||
let tags = room.tags().await.map_err(IambError::from)?;
|
let tags = room.tags().await.map_err(IambError::from)?;
|
||||||
|
|
||||||
Ok((room, name, tags))
|
Ok((room, name, tags))
|
||||||
|
@ -1389,7 +1416,7 @@ impl ClientWorker {
|
||||||
req.limit = Some(1000u32.into());
|
req.limit = Some(1000u32.into());
|
||||||
req.max_depth = Some(1u32.into());
|
req.max_depth = Some(1u32.into());
|
||||||
|
|
||||||
let resp = self.client.send(req, None).await.map_err(IambError::from)?;
|
let resp = self.client.send(req).await.map_err(IambError::from)?;
|
||||||
|
|
||||||
let rooms = resp.rooms.into_iter().map(|chunk| chunk.room_id).collect();
|
let rooms = resp.rooms.into_iter().map(|chunk| chunk.room_id).collect();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue