Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93fc47d019 | ||
|
|
a32149f604 | ||
|
|
3149f79d11 | ||
|
|
7ccb1cbf2c | ||
|
|
1ec311590d | ||
|
|
0ddded3b8b | ||
|
|
a8cbc352ff | ||
|
|
dfa0937077 | ||
|
|
43485270ee | ||
|
|
28fea03625 | ||
|
|
e021d4a55d | ||
|
|
b01dbe5a5d | ||
|
|
4b2382bf93 | ||
|
|
0f2442566f | ||
|
|
8c9a2714a1 | ||
|
|
d44f861871 | ||
|
|
14aa97251c | ||
|
|
55456dbc1e | ||
|
|
d5c330ac72 | ||
|
|
7b1dc93f3a | ||
|
|
745f547904 | ||
|
|
6ebb7ac7fd | ||
|
|
1bb93c18fb | ||
|
|
e3090e537f | ||
|
|
ad10082c2f | ||
|
|
67603d0623 | ||
|
|
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 |
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
|
||||||
|
|||||||
28
.github/workflows/ci.yml
vendored
28
.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
|
||||||
@@ -45,3 +45,25 @@ jobs:
|
|||||||
reporter: 'github-check'
|
reporter: 'github-check'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --locked
|
run: cargo test --locked
|
||||||
|
|
||||||
|
nix-flake-test:
|
||||||
|
name: Flake checks ❄️
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: cachix/install-nix-action@v31
|
||||||
|
with:
|
||||||
|
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: cachix/cachix-action@v15
|
||||||
|
with:
|
||||||
|
name: iamb-prs
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
- name: Flake check
|
||||||
|
run: |
|
||||||
|
nix flake show
|
||||||
|
nix flake check --print-build-logs
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
unstable_features = true
|
unstable_features = true
|
||||||
max_width = 100
|
max_width = 100
|
||||||
fn_call_width = 90
|
fn_call_width = 88
|
||||||
struct_lit_width = 50
|
struct_lit_width = 50
|
||||||
struct_variant_width = 50
|
struct_variant_width = 50
|
||||||
chain_width = 75
|
chain_width = 75
|
||||||
|
|||||||
4287
Cargo.lock
generated
4287
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iamb"
|
name = "iamb"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
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.88"
|
||||||
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,8 @@ 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"
|
||||||
|
shellexpand = "3.1.1"
|
||||||
|
|
||||||
[dependencies.comrak]
|
[dependencies.comrak]
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -70,24 +73,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.24"
|
||||||
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.24"
|
||||||
#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.14.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["e2e-encryption", "sqlite", "sso-login"]
|
features = ["e2e-encryption", "sqlite", "sso-login"]
|
||||||
|
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -51,9 +51,18 @@ url = "https://example.com"
|
|||||||
user_id = "@user:example.com"
|
user_id = "@user:example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installation (from source)
|
||||||
|
|
||||||
|
Install Rust and Cargo using [rustup], and then run from the directory
|
||||||
|
containing the sources (ie: from a git clone):
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install --locked --path .
|
||||||
|
```
|
||||||
|
|
||||||
## 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 +89,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:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -127,3 +154,4 @@ iamb is released under the [Apache License, Version 2.0].
|
|||||||
[crates-io-iamb]: https://crates.io/crates/iamb
|
[crates-io-iamb]: https://crates.io/crates/iamb
|
||||||
[iamb.chat]: https://iamb.chat
|
[iamb.chat]: https://iamb.chat
|
||||||
[well_known_entry]: https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
|
[well_known_entry]: https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
|
||||||
|
[rustup]: https://rustup.rs/
|
||||||
|
|||||||
71
docs/iamb.1
71
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,8 +63,12 @@ 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.
|
||||||
|
.It Sy ":forget"
|
||||||
|
Remove all left rooms from the internal database.
|
||||||
.El
|
.El
|
||||||
|
|
||||||
.Sh "E2EE COMMANDS"
|
.Sh "E2EE COMMANDS"
|
||||||
@@ -77,39 +81,56 @@ 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 ":replied"
|
||||||
|
Go to the message the current message replied to.
|
||||||
|
.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 +138,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 +147,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 +178,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 +202,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,14 @@
|
|||||||
<?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.11" date="2026-01-19"/>
|
||||||
|
<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 +16,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 +26,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 +40,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>
|
||||||
|
|||||||
124
flake.lock
generated
124
flake.lock
generated
@@ -1,33 +1,51 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1759893430,
|
||||||
|
"narHash": "sha256-yAy4otLYm9iZ+NtQwTMEbqHwswSFUbhn7x826RR6djw=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "1979a2524cb8c801520bd94c38bb3d5692419d93",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1760510549,
|
||||||
|
"narHash": "sha256-NP+kmLMm7zSyv4Fufv+eSJXyqjLMUhUfPT6lXRlg/bU=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "ef7178cf086f267113b5c48fdeb6e510729c8214",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"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 +56,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709703039,
|
"lastModified": 1760284886,
|
||||||
"narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=",
|
"narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d",
|
"rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -52,45 +70,28 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1706487304,
|
|
||||||
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"fenix": "fenix",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs"
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-analyzer-src": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709863839,
|
"lastModified": 1760457219,
|
||||||
"narHash": "sha256-QpEL5FmZNi2By3sKZY55wGniFXc4wEn9PQczlE8TG0o=",
|
"narHash": "sha256-WJOUGx42hrhmvvYcGkwea+BcJuQJLcns849OnewQqX4=",
|
||||||
"owner": "oxalica",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-analyzer",
|
||||||
"rev": "e5ab9ee98f479081ad971473d2bc13c59e9fbc0a",
|
"rev": "8747cf81540bd1bbbab9ee2702f12c33aa887b46",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "oxalica",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-overlay",
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -108,21 +109,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",
|
||||||
|
|||||||
123
flake.nix
123
flake.nix
@@ -5,40 +5,107 @@
|
|||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
crane.url = "github:ipetkov/crane";
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
|
outputs =
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
# We only need the nightly overlay in the devShell because .rs files are formatted with nightly.
|
|
||||||
overlays = [ (import rust-overlay) ];
|
|
||||||
pkgs = import nixpkgs { inherit system overlays; };
|
|
||||||
rustNightly = pkgs.rust-bin.nightly."2024-03-08".default;
|
|
||||||
in
|
|
||||||
with pkgs;
|
|
||||||
{
|
{
|
||||||
packages.default = rustPlatform.buildRustPackage {
|
self,
|
||||||
pname = "iamb";
|
nixpkgs,
|
||||||
version = self.shortRev or self.dirtyShortRev;
|
crane,
|
||||||
src = ./.;
|
flake-utils,
|
||||||
cargoLock = {
|
fenix,
|
||||||
lockFile = ./Cargo.lock;
|
...
|
||||||
};
|
}:
|
||||||
nativeBuildInputs = [ pkg-config ];
|
flake-utils.lib.eachDefaultSystem (
|
||||||
buildInputs = [ openssl ] ++ lib.optionals stdenv.isDarwin
|
system:
|
||||||
(with darwin.apple_sdk.frameworks; [ AppKit Security Cocoa]);
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
|
||||||
|
rustToolchain = fenix.packages.${system}.fromToolchainFile {
|
||||||
|
file = ./rust-toolchain.toml;
|
||||||
|
# When the file changes, this hash must be updated.
|
||||||
|
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
|
||||||
};
|
};
|
||||||
|
|
||||||
devShell = mkShell {
|
# Nightly toolchain for rustfmt (pinned to current flake lock)
|
||||||
buildInputs = [
|
# Note that the github CI uses "current nightly" for formatting, it 's not pinned.
|
||||||
(rustNightly.override {
|
rustNightly = fenix.packages.${system}.latest;
|
||||||
extensions = [ "rust-src" "rust-analyzer-preview" "rustfmt" "clippy" ];
|
|
||||||
})
|
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||||
pkg-config
|
craneLibNightly = (crane.mkLib pkgs).overrideToolchain rustNightly.toolchain;
|
||||||
cargo-tarpaulin
|
|
||||||
cargo-watch
|
src = lib.fileset.toSource {
|
||||||
|
root = ./.;
|
||||||
|
fileset = lib.fileset.unions [
|
||||||
|
(craneLib.fileset.commonCargoSources ./.)
|
||||||
|
./src/windows/welcome.md
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
inherit src;
|
||||||
|
strictDeps = true;
|
||||||
|
pname = "iamb";
|
||||||
|
version = self.shortRev or self.dirtyShortRev;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Build *just* the cargo dependencies, so we can reuse
|
||||||
|
# all of that work (e.g. via cachix) when running in CI
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
|
||||||
|
# Build the actual crate
|
||||||
|
iamb = craneLib.buildPackage (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
});
|
});
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = {
|
||||||
|
# Build the crate as part of `nix flake check`
|
||||||
|
inherit iamb;
|
||||||
|
|
||||||
|
iamb-clippy = craneLib.cargoClippy (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||||
|
});
|
||||||
|
|
||||||
|
iamb-fmt = craneLibNightly.cargoFmt {
|
||||||
|
inherit src;
|
||||||
|
};
|
||||||
|
|
||||||
|
iamb-nextest = craneLib.cargoNextest (commonArgs // {
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
partitions = 1;
|
||||||
|
partitionType = "count";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.default = iamb;
|
||||||
|
|
||||||
|
apps.default = flake-utils.lib.mkApp {
|
||||||
|
drv = iamb;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = craneLib.devShell {
|
||||||
|
# Inherit inputs from checks
|
||||||
|
checks = self.checks.${system};
|
||||||
|
|
||||||
|
packages = with pkgs; [
|
||||||
|
cargo-tarpaulin
|
||||||
|
cargo-watch
|
||||||
|
sqlite
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
# Prepend nightly rustfmt to PATH.
|
||||||
|
export PATH="${rustNightly.rustfmt}/bin:$PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.88"
|
||||||
|
components = [ "clippy" ]
|
||||||
386
src/base.rs
386
src/base.rs
@@ -12,6 +12,8 @@ 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 matrix_sdk::ruma::room_version_rules::RedactionRules;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
@@ -47,6 +49,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,
|
||||||
@@ -55,7 +58,6 @@ use matrix_sdk::{
|
|||||||
OwnedRoomId,
|
OwnedRoomId,
|
||||||
OwnedUserId,
|
OwnedUserId,
|
||||||
RoomId,
|
RoomId,
|
||||||
RoomVersionId,
|
|
||||||
UserId,
|
UserId,
|
||||||
},
|
},
|
||||||
RoomState as MatrixRoomState,
|
RoomState as MatrixRoomState,
|
||||||
@@ -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},
|
||||||
@@ -166,6 +169,9 @@ pub enum MessageAction {
|
|||||||
/// Reply to a message.
|
/// Reply to a message.
|
||||||
Reply,
|
Reply,
|
||||||
|
|
||||||
|
/// Go to the message the hovered message replied to.
|
||||||
|
Replied,
|
||||||
|
|
||||||
/// Unreact to a message.
|
/// Unreact to a message.
|
||||||
///
|
///
|
||||||
/// If no specific Emoji to remove to is specified, then all reactions from the user on the
|
/// If no specific Emoji to remove to is specified, then all reactions from the user on the
|
||||||
@@ -177,6 +183,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 +262,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 +299,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 +329,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 +352,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 +398,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),
|
||||||
|
|
||||||
@@ -468,6 +494,8 @@ pub enum HomeserverAction {
|
|||||||
/// Create a new room with an optional localpart.
|
/// Create a new room with an optional localpart.
|
||||||
CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags),
|
CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags),
|
||||||
Logout(String, bool),
|
Logout(String, bool),
|
||||||
|
/// Forget all left rooms
|
||||||
|
Forget,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An action performed against the user's room keys.
|
/// An action performed against the user's room keys.
|
||||||
@@ -493,6 +521,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 +565,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 +590,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 +606,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 +622,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 +637,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 +655,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 +756,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,
|
||||||
@@ -729,6 +788,10 @@ pub enum IambError {
|
|||||||
#[error("Invalid room alias id: {0}")]
|
#[error("Invalid room alias id: {0}")]
|
||||||
InvalidRoomAliasId(#[from] matrix_sdk::ruma::IdParseError),
|
InvalidRoomAliasId(#[from] matrix_sdk::ruma::IdParseError),
|
||||||
|
|
||||||
|
/// An invalid space child order was specified.
|
||||||
|
#[error("Invalid space child order: {0}")]
|
||||||
|
InvalidSpaceChildOrder(matrix_sdk::ruma::IdParseError),
|
||||||
|
|
||||||
/// A failure occurred during verification.
|
/// A failure occurred during verification.
|
||||||
#[error("Verification request error: {0}")]
|
#[error("Verification request error: {0}")]
|
||||||
VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError),
|
VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError),
|
||||||
@@ -785,6 +848,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 +880,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 +894,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 +926,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 +959,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
|
||||||
}
|
}
|
||||||
@@ -945,24 +1032,30 @@ impl RoomInfo {
|
|||||||
self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?)
|
self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redact(&mut self, ev: OriginalSyncRoomRedactionEvent, room_version: &RoomVersionId) {
|
pub fn redact(&mut self, ev: OriginalSyncRoomRedactionEvent, rules: &RedactionRules) {
|
||||||
let Some(redacts) = &ev.redacts else {
|
let Some(redacts) = &ev.redacts else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
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, rules);
|
||||||
|
}
|
||||||
|
},
|
||||||
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);
|
||||||
msg.redact(ev, room_version);
|
msg.redact(ev, rules);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(EventLocation::Message(Some(root), key)) => {
|
Some(EventLocation::Message(Some(root), key)) => {
|
||||||
if let Some(thread) = self.threads.get_mut(root) {
|
if let Some(thread) = self.threads.get_mut(root) {
|
||||||
if let Some(msg) = thread.get_mut(key) {
|
if let Some(msg) = thread.get_mut(key) {
|
||||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||||
msg.redact(ev, room_version);
|
msg.redact(ev, rules);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1008,7 +1101,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 +1120,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 +1130,52 @@ 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));
|
||||||
|
let last_receipt = last_receipt.as_ref().and_then(|event_id| {
|
||||||
|
match &self.keys.get(*event_id)? {
|
||||||
|
EventLocation::Message(_, key) | EventLocation::State(key) => Some(key),
|
||||||
|
EventLocation::Reaction(_) => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_unthreaded = self
|
||||||
|
.user_receipts
|
||||||
|
.get(&ReceiptThread::Unthreaded)
|
||||||
|
.and_then(|receipts| receipts.get(&settings.profile.user_id));
|
||||||
|
let last_unthreaded = last_unthreaded.as_ref().and_then(|event_id| {
|
||||||
|
match &self.keys.get(*event_id)? {
|
||||||
|
EventLocation::Message(_, key) | EventLocation::State(key) => Some(key),
|
||||||
|
EventLocation::Reaction(_) => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_receipt = std::cmp::max(last_receipt, last_unthreaded);
|
||||||
|
|
||||||
match (last_message, last_receipt) {
|
match (last_message, last_receipt) {
|
||||||
(Some(((ts, recent), _)), Some(last_read)) => {
|
(Some(((ts, _), _)), Some((read_ts, _))) => {
|
||||||
UnreadInfo { unread: last_read != recent, latest: Some(*ts) }
|
UnreadInfo { unread: ts > read_ts, 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) }
|
||||||
},
|
},
|
||||||
(Some(((ts, _), _)), None) => UnreadInfo { unread: false, latest: Some(*ts) },
|
|
||||||
(None, _) => UnreadInfo::default(),
|
(None, _) => UnreadInfo::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1071,7 +1203,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 +1266,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 +1391,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 +1438,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 +1447,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 +1470,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.
|
||||||
@@ -1338,14 +1506,19 @@ impl SyncInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bitflags::bitflags! {
|
static MESSAGE_NEED_TTL: u8 = 30;
|
||||||
/// Load-needs
|
|
||||||
#[derive(Debug, Default, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Need: u32 {
|
/// Load messages until the event is loaded or `ttl` loads are exceeded
|
||||||
const EMPTY = 0b00000000;
|
pub struct MessageNeed {
|
||||||
const MESSAGES = 0b00000001;
|
pub event_id: OwnedEventId,
|
||||||
const MEMBERS = 0b00000010;
|
pub ttl: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, PartialEq)]
|
||||||
|
pub struct Need {
|
||||||
|
pub members: bool,
|
||||||
|
pub messages: Option<Vec<MessageNeed>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Things that need loading for different rooms.
|
/// Things that need loading for different rooms.
|
||||||
@@ -1355,9 +1528,31 @@ pub struct RoomNeeds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RoomNeeds {
|
impl RoomNeeds {
|
||||||
/// Mark a room for needing something to be loaded.
|
/// Mark a room for needing to load members.
|
||||||
pub fn insert(&mut self, room_id: OwnedRoomId, need: Need) {
|
pub fn need_members(&mut self, room_id: OwnedRoomId) {
|
||||||
self.needs.entry(room_id).or_default().insert(need);
|
self.needs.entry(room_id).or_default().members = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a room for needing to load messages.
|
||||||
|
pub fn need_messages(&mut self, room_id: OwnedRoomId) {
|
||||||
|
self.needs.entry(room_id).or_default().messages.get_or_insert_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a room for needing to load messages until the given message is loaded or a retry limit
|
||||||
|
/// is exceeded.
|
||||||
|
pub fn need_message(&mut self, room_id: OwnedRoomId, event_id: OwnedEventId) {
|
||||||
|
let messages = &mut self.needs.entry(room_id).or_default().messages.get_or_insert_default();
|
||||||
|
|
||||||
|
messages.push(MessageNeed { event_id, ttl: MESSAGE_NEED_TTL });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn need_messages_all(&mut self, room_id: OwnedRoomId, message_needs: Vec<MessageNeed>) {
|
||||||
|
self.needs
|
||||||
|
.entry(room_id)
|
||||||
|
.or_default()
|
||||||
|
.messages
|
||||||
|
.get_or_insert_default()
|
||||||
|
.extend(message_needs);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rooms(&self) -> usize {
|
pub fn rooms(&self) -> usize {
|
||||||
@@ -1417,6 +1612,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 +1632,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 +1642,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 +1763,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 +1900,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 +1975,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 +2006,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 +2024,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 +2033,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,8 +2048,8 @@ 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 +2057,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 +2070,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 +2095,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 +2108,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 +2120,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 +2145,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 +2162,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);
|
||||||
@@ -1993,7 +2195,7 @@ pub mod tests {
|
|||||||
));
|
));
|
||||||
|
|
||||||
for i in 0..3 {
|
for i in 0..3 {
|
||||||
let event_id = format!("$house_{}", i);
|
let event_id = format!("$house_{i}");
|
||||||
info.insert_reaction(MessageLikeEvent::Original(
|
info.insert_reaction(MessageLikeEvent::Original(
|
||||||
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
@@ -2012,7 +2214,7 @@ pub mod tests {
|
|||||||
));
|
));
|
||||||
|
|
||||||
for i in 0..2 {
|
for i in 0..2 {
|
||||||
let event_id = format!("$smile_{}", i);
|
let event_id = format!("$smile_{i}");
|
||||||
info.insert_reaction(MessageLikeEvent::Original(
|
info.insert_reaction(MessageLikeEvent::Original(
|
||||||
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
@@ -2026,7 +2228,7 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i in 2..4 {
|
for i in 2..4 {
|
||||||
let event_id = format!("$smile_{}", i);
|
let event_id = format!("$smile_{i}");
|
||||||
info.insert_reaction(MessageLikeEvent::Original(
|
info.insert_reaction(MessageLikeEvent::Original(
|
||||||
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
@@ -2128,18 +2330,19 @@ pub mod tests {
|
|||||||
|
|
||||||
let mut need_load = RoomNeeds::default();
|
let mut need_load = RoomNeeds::default();
|
||||||
|
|
||||||
need_load.insert(room_id.clone(), Need::MESSAGES);
|
need_load.need_messages(room_id.clone());
|
||||||
need_load.insert(room_id.clone(), Need::MEMBERS);
|
need_load.need_members(room_id.clone());
|
||||||
|
|
||||||
assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![(
|
assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![(
|
||||||
room_id,
|
room_id,
|
||||||
Need::MESSAGES | Need::MEMBERS,
|
Need { members: true, messages: Some(Vec::new()) }
|
||||||
)],);
|
)],);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 +2366,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",
|
||||||
|
|||||||
250
src/commands.rs
250
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,6 +200,17 @@ fn iamb_leave(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||||||
return Ok(step);
|
return Ok(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn iamb_forget(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
|
if !desc.arg.text.is_empty() {
|
||||||
|
return Result::Err(CommandError::InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
let forget = IambAction::Homeserver(HomeserverAction::Forget);
|
||||||
|
let step = CommandStep::Continue(forget.into(), ctx.context.clone());
|
||||||
|
|
||||||
|
return Ok(step);
|
||||||
|
}
|
||||||
|
|
||||||
fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
if !desc.arg.text.is_empty() {
|
if !desc.arg.text.is_empty() {
|
||||||
return Result::Err(CommandError::InvalidArgument);
|
return Result::Err(CommandError::InvalidArgument);
|
||||||
@@ -274,6 +286,17 @@ fn iamb_reply(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||||||
return Ok(step);
|
return Ok(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn iamb_replied(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
|
if !desc.arg.text.is_empty() {
|
||||||
|
return Result::Err(CommandError::InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ract = IambAction::from(MessageAction::Replied);
|
||||||
|
let step = CommandStep::Continue(ract.into(), ctx.context.clone());
|
||||||
|
|
||||||
|
return Ok(step);
|
||||||
|
}
|
||||||
|
|
||||||
fn iamb_editor(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
fn iamb_editor(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
if !desc.arg.text.is_empty() {
|
if !desc.arg.text.is_empty() {
|
||||||
return Result::Err(CommandError::InvalidArgument);
|
return Result::Err(CommandError::InvalidArgument);
|
||||||
@@ -475,10 +498,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 +522,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 +558,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),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -641,6 +753,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
|||||||
aliases: vec![],
|
aliases: vec![],
|
||||||
f: iamb_leave,
|
f: iamb_leave,
|
||||||
});
|
});
|
||||||
|
cmds.add_command(ProgramCommand {
|
||||||
|
name: "forget".into(),
|
||||||
|
aliases: vec![],
|
||||||
|
f: iamb_forget,
|
||||||
|
});
|
||||||
cmds.add_command(ProgramCommand {
|
cmds.add_command(ProgramCommand {
|
||||||
name: "members".into(),
|
name: "members".into(),
|
||||||
aliases: vec![],
|
aliases: vec![],
|
||||||
@@ -661,12 +778,22 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
|||||||
aliases: vec![],
|
aliases: vec![],
|
||||||
f: iamb_reply,
|
f: iamb_reply,
|
||||||
});
|
});
|
||||||
|
cmds.add_command(ProgramCommand {
|
||||||
|
name: "replied".into(),
|
||||||
|
aliases: vec![],
|
||||||
|
f: iamb_replied,
|
||||||
|
});
|
||||||
cmds.add_command(ProgramCommand {
|
cmds.add_command(ProgramCommand {
|
||||||
name: "rooms".into(),
|
name: "rooms".into(),
|
||||||
aliases: vec![],
|
aliases: vec![],
|
||||||
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 +848,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 +1174,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();
|
||||||
|
|||||||
252
src/config.rs
252
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::SessionTokens {
|
||||||
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,
|
||||||
}
|
}
|
||||||
@@ -398,24 +402,77 @@ pub enum UserDisplayStyle {
|
|||||||
DisplayName,
|
DisplayName,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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")]
|
||||||
return NotifyVia::Desktop;
|
desktop: true,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
@@ -426,6 +483,8 @@ pub struct Notifications {
|
|||||||
pub via: NotifyVia,
|
pub via: NotifyVia,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub show_message: bool,
|
pub show_message: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sound_hint: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -501,12 +560,14 @@ 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,
|
||||||
@@ -514,16 +575,19 @@ pub struct TunableValues {
|
|||||||
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,6 +595,7 @@ 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>,
|
||||||
@@ -538,10 +603,12 @@ pub struct Tunables {
|
|||||||
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 +618,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,6 +627,7 @@ 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),
|
||||||
@@ -566,12 +635,14 @@ impl Tunables {
|
|||||||
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,12 +650,14 @@ 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(),
|
||||||
@@ -592,12 +665,14 @@ impl Tunables {
|
|||||||
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,11 +707,11 @@ impl DirectoryValues {
|
|||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub struct Directories {
|
pub struct Directories {
|
||||||
pub cache: Option<PathBuf>,
|
pub cache: Option<String>,
|
||||||
pub data: Option<PathBuf>,
|
pub data: Option<String>,
|
||||||
pub logs: Option<PathBuf>,
|
pub logs: Option<String>,
|
||||||
pub downloads: Option<PathBuf>,
|
pub downloads: Option<String>,
|
||||||
pub image_previews: Option<PathBuf>,
|
pub image_previews: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Directories {
|
impl Directories {
|
||||||
@@ -653,6 +728,11 @@ impl Directories {
|
|||||||
fn values(self) -> DirectoryValues {
|
fn values(self) -> DirectoryValues {
|
||||||
let cache = self
|
let cache = self
|
||||||
.cache
|
.cache
|
||||||
|
.map(|dir| {
|
||||||
|
let dir = shellexpand::full(&dir)
|
||||||
|
.expect("unable to expand shell variables in dirs.cache");
|
||||||
|
Path::new(dir.as_ref()).to_owned()
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let mut dir = dirs::cache_dir()?;
|
let mut dir = dirs::cache_dir()?;
|
||||||
dir.push("iamb");
|
dir.push("iamb");
|
||||||
@@ -662,6 +742,11 @@ impl Directories {
|
|||||||
|
|
||||||
let data = self
|
let data = self
|
||||||
.data
|
.data
|
||||||
|
.map(|dir| {
|
||||||
|
let dir = shellexpand::full(&dir)
|
||||||
|
.expect("unable to expand shell variables in dirs.cache");
|
||||||
|
Path::new(dir.as_ref()).to_owned()
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
let mut dir = dirs::data_dir()?;
|
let mut dir = dirs::data_dir()?;
|
||||||
dir.push("iamb");
|
dir.push("iamb");
|
||||||
@@ -669,15 +754,36 @@ impl Directories {
|
|||||||
})
|
})
|
||||||
.expect("no dirs.data value configured!");
|
.expect("no dirs.data value configured!");
|
||||||
|
|
||||||
let logs = self.logs.unwrap_or_else(|| {
|
let logs = self
|
||||||
|
.logs
|
||||||
|
.map(|dir| {
|
||||||
|
let dir = shellexpand::full(&dir)
|
||||||
|
.expect("unable to expand shell variables in dirs.cache");
|
||||||
|
Path::new(dir.as_ref()).to_owned()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
let mut dir = cache.clone();
|
let mut dir = cache.clone();
|
||||||
dir.push("logs");
|
dir.push("logs");
|
||||||
dir
|
dir
|
||||||
});
|
});
|
||||||
|
|
||||||
let downloads = self.downloads.or_else(dirs::download_dir);
|
let downloads = self
|
||||||
|
.downloads
|
||||||
|
.map(|dir| {
|
||||||
|
let dir = shellexpand::full(&dir)
|
||||||
|
.expect("unable to expand shell variables in dirs.cache");
|
||||||
|
Path::new(dir.as_ref()).to_owned()
|
||||||
|
})
|
||||||
|
.or_else(dirs::download_dir);
|
||||||
|
|
||||||
let image_previews = self.image_previews.unwrap_or_else(|| {
|
let image_previews = self
|
||||||
|
.image_previews
|
||||||
|
.map(|dir| {
|
||||||
|
let dir = shellexpand::full(&dir)
|
||||||
|
.expect("unable to expand shell variables in dirs.cache");
|
||||||
|
Path::new(dir.as_ref()).to_owned()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
let mut dir = cache.clone();
|
let mut dir = cache.clone();
|
||||||
dir.push("image_preview_downloads");
|
dir.push("image_preview_downloads");
|
||||||
dir
|
dir
|
||||||
@@ -729,7 +835,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,8 +875,16 @@ 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
|
||||||
|
.config_directory
|
||||||
|
.or_else(Self::get_xdg_config_home)
|
||||||
|
.or_else(dirs::config_dir)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
usage!(
|
usage!(
|
||||||
"No user configuration directory found;\
|
"No user configuration directory found;\
|
||||||
please specify one via -C.\n\n
|
please specify one via -C.\n\n
|
||||||
@@ -816,14 +930,33 @@ impl ApplicationSettings {
|
|||||||
} else if profiles.len() == 1 {
|
} else if profiles.len() == 1 {
|
||||||
profiles.into_iter().next().unwrap()
|
profiles.into_iter().next().unwrap()
|
||||||
} else {
|
} else {
|
||||||
|
loop {
|
||||||
|
println!("\nNo profile specified. Available profiles:");
|
||||||
|
profiles.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!(
|
usage!(
|
||||||
"No profile specified. \
|
"No profile specified. \
|
||||||
Please use -P or add \"default_profile\" to your configuration.\n\n\
|
Please use -P or add \"default_profile\" to your configuration.\n\n\
|
||||||
For more information try '--help'",
|
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 +1031,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
|
||||||
@@ -1022,10 +1155,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]
|
||||||
@@ -1189,6 +1322,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");
|
||||||
|
|||||||
88
src/main.rs
88
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?
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -625,6 +663,13 @@ impl Application {
|
|||||||
|
|
||||||
Err(UIError::NeedConfirm(prompt))
|
Err(UIError::NeedConfirm(prompt))
|
||||||
},
|
},
|
||||||
|
HomeserverAction::Forget => {
|
||||||
|
let client = &store.application.worker.client;
|
||||||
|
for room in client.left_rooms() {
|
||||||
|
room.forget().await.map_err(IambError::from)?;
|
||||||
|
}
|
||||||
|
Ok(vec![])
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,7 +892,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 +974,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 +989,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 +1028,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());
|
||||||
|
|
||||||
@@ -988,7 +1043,7 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
|||||||
match res {
|
match res {
|
||||||
Err(UIError::Application(IambError::Matrix(e))) => {
|
Err(UIError::Application(IambError::Matrix(e))) => {
|
||||||
if let Some(ErrorKind::UnknownToken { .. }) = e.client_api_error_kind() {
|
if let Some(ErrorKind::UnknownToken { .. }) = e.client_api_error_kind() {
|
||||||
print_exit("Server did not recognize our API token; did you log out from this session elsewhere?")
|
print_exit(format!("Server did not recognize our API token; did you log out from this session elsewhere?\nTry deleting `{}` to force a clean login.", settings.session_json.display()))
|
||||||
} else {
|
} else {
|
||||||
print_exit(e)
|
print_exit(e)
|
||||||
}
|
}
|
||||||
@@ -1006,11 +1061,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 +1076,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);
|
||||||
|
|
||||||
@@ -775,7 +820,8 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
|
|||||||
*c2t(&node.children.borrow(), state)
|
*c2t(&node.children.borrow(), state)
|
||||||
},
|
},
|
||||||
|
|
||||||
_ => return vec![],
|
// Treat unknown tags as plain text.
|
||||||
|
_ => *c2t(&node.children.borrow(), state),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -811,17 +857,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 +881,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 +894,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 +908,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 +923,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 +939,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 +957,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 +967,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 +976,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 +985,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 +994,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 +1003,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 +1012,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 +1021,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 +1030,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 +1039,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 +1049,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 +1077,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 +1172,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 +1235,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 +1246,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 +1336,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 +1377,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 +1402,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 +1414,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 +1432,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 +1474,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 +1500,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 +1509,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 +1520,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 +1533,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,16 @@
|
|||||||
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 matrix_sdk::ruma::room_version_rules::RedactionRules;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ use matrix_sdk::ruma::{
|
|||||||
},
|
},
|
||||||
redaction::SyncRoomRedactionEvent,
|
redaction::SyncRoomRedactionEvent,
|
||||||
},
|
},
|
||||||
|
AnySyncStateEvent,
|
||||||
RedactContent,
|
RedactContent,
|
||||||
RedactedUnsigned,
|
RedactedUnsigned,
|
||||||
},
|
},
|
||||||
@@ -42,7 +44,6 @@ use matrix_sdk::ruma::{
|
|||||||
MilliSecondsSinceUnixEpoch,
|
MilliSecondsSinceUnixEpoch,
|
||||||
OwnedEventId,
|
OwnedEventId,
|
||||||
OwnedUserId,
|
OwnedUserId,
|
||||||
RoomVersionId,
|
|
||||||
UInt,
|
UInt,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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();
|
||||||
@@ -155,12 +172,15 @@ fn placeholder_frame(
|
|||||||
image_preview_size: &ImagePreviewSize,
|
image_preview_size: &ImagePreviewSize,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let ImagePreviewSize { width, height } = image_preview_size;
|
let ImagePreviewSize { width, height } = image_preview_size;
|
||||||
if outer_width < *width || (*width < 2 || *height < 2) {
|
let width = usize::min(*width, outer_width);
|
||||||
|
if width < 2 || *height < 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
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 +190,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 +200,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)]
|
||||||
@@ -215,13 +234,13 @@ impl MessageTimeStamp {
|
|||||||
dt1.date_naive() == dt2.date_naive()
|
dt1.date_naive() == dt2.date_naive()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_date(&self) -> Option<Span> {
|
fn show_date(&self) -> Option<Span<'_>> {
|
||||||
let time = self.as_datetime().format("%A, %B %d %Y").to_string();
|
let time = self.as_datetime().format("%A, %B %d %Y").to_string();
|
||||||
|
|
||||||
Span::styled(time, BOLD_STYLE).into()
|
Span::styled(time, BOLD_STYLE).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_time(&self) -> Option<Span> {
|
fn show_time(&self) -> Option<Span<'_>> {
|
||||||
match self {
|
match self {
|
||||||
MessageTimeStamp::OriginServer(ms) => {
|
MessageTimeStamp::OriginServer(ms) => {
|
||||||
let time = millis_to_datetime(*ms).format("%T");
|
let time = millis_to_datetime(*ms).format("%T");
|
||||||
@@ -426,6 +445,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 +456,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 +467,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 +485,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 +496,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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -487,15 +511,16 @@ impl MessageEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
fn redact(&mut self, redaction: SyncRoomRedactionEvent, rules: &RedactionRules) {
|
||||||
match self {
|
match self {
|
||||||
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 {
|
||||||
content: ev.content.clone().redact(version),
|
content: ev.content.clone().redact(rules),
|
||||||
event_id: ev.event_id.clone(),
|
event_id: ev.event_id.clone(),
|
||||||
sender: ev.sender.clone(),
|
sender: ev.sender.clone(),
|
||||||
origin_server_ts: ev.origin_server_ts,
|
origin_server_ts: ev.origin_server_ts,
|
||||||
@@ -549,27 +574,18 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
|
|||||||
MessageType::Video(content) => {
|
MessageType::Video(content) => {
|
||||||
display_file_to_text!(Video, content);
|
display_file_to_text!(Video, content);
|
||||||
},
|
},
|
||||||
_ => {
|
_ => content.body(),
|
||||||
match content.msgtype() {
|
|
||||||
// Just show the body text for the special Element messages.
|
|
||||||
"nic.custom.confetti" |
|
|
||||||
"nic.custom.fireworks" |
|
|
||||||
"io.element.effect.hearts" |
|
|
||||||
"io.element.effect.rainfall" |
|
|
||||||
"io.element.effect.snowfall" |
|
|
||||||
"io.element.effects.space_invaders" => content.body(),
|
|
||||||
other => {
|
|
||||||
return Cow::Owned(format!("[Unknown message type: {other:?}]"));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Cow::Borrowed(s)
|
Cow::Borrowed(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> {
|
fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> {
|
||||||
let reason = unsigned.redacted_because.content.reason.as_ref();
|
let reason = unsigned
|
||||||
|
.redacted_because
|
||||||
|
.deserialize()
|
||||||
|
.ok()
|
||||||
|
.and_then(|ev| ev.content.reason);
|
||||||
|
|
||||||
if let Some(r) = reason {
|
if let Some(r) = reason {
|
||||||
Cow::Owned(format!("[Redacted: {r:?}]"))
|
Cow::Owned(format!("[Redacted: {r:?}]"))
|
||||||
@@ -623,8 +639,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 +673,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,39 +730,55 @@ 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 reply_style = if settings.tunables.message_user_color {
|
||||||
|
style.patch(settings.get_user_color(&msg.sender))
|
||||||
|
} else {
|
||||||
|
style
|
||||||
|
};
|
||||||
|
|
||||||
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, reply_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);
|
||||||
|
|
||||||
sender.style = sender.style.patch(style);
|
sender.style = sender.style.patch(reply_style);
|
||||||
|
|
||||||
self.push_spans(
|
self.push_spans(
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(" ", style),
|
Span::styled(" ", style),
|
||||||
Span::styled(THICK_VERTICAL, style),
|
Span::styled(THICK_VERTICAL, style),
|
||||||
sender,
|
sender,
|
||||||
Span::styled(":", style),
|
Span::styled(":", reply_style),
|
||||||
space_span(trailing, style),
|
space_span(trailing, reply_style),
|
||||||
]),
|
]),
|
||||||
style,
|
style,
|
||||||
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, reply_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 +827,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 +844,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 +879,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 +900,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 +954,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 +968,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 +976,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 +984,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 +1000,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 +1013,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 +1047,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 +1061,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 +1087,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("No Space..."), width, &backend.area().into())
|
||||||
},
|
},
|
||||||
ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")),
|
ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")),
|
||||||
};
|
};
|
||||||
@@ -1097,17 +1131,19 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, rules: &RedactionRules) {
|
||||||
self.event.redact(redaction, version);
|
self.event.redact(redaction, rules);
|
||||||
self.html = None;
|
self.html = None;
|
||||||
|
self.downloaded = false;
|
||||||
|
self.image_preview = ImageStatus::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,6 +1189,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 +1297,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1300,7 +1346,17 @@ pub mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(placeholder_frame(None, 2, &ImagePreviewSize { width: 4, height: 4 }), None);
|
assert_eq!(
|
||||||
|
placeholder_frame(None, 2, &ImagePreviewSize { width: 4, height: 4 }),
|
||||||
|
pretty_frame_test(
|
||||||
|
r#"
|
||||||
|
⌌⌍
|
||||||
|
|
||||||
|
|
||||||
|
⌎⌏
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
);
|
||||||
assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 1, height: 4 }), None);
|
assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 1, height: 4 }), None);
|
||||||
|
|
||||||
assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 4, height: 1 }), None);
|
assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 4, height: 1 }), None);
|
||||||
@@ -1312,6 +1368,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 {state_key} from the room")
|
||||||
|
},
|
||||||
|
MembershipChange::Unbanned => {
|
||||||
|
format!("* unbanned {state_key} from the room")
|
||||||
|
},
|
||||||
|
MembershipChange::Kicked => {
|
||||||
|
format!("* kicked {state_key} from the room")
|
||||||
|
},
|
||||||
|
MembershipChange::Invited => {
|
||||||
|
format!("* invited {state_key} to the room")
|
||||||
|
},
|
||||||
|
MembershipChange::KickedAndBanned => {
|
||||||
|
format!("* kicked and banned {state_key} from the room")
|
||||||
|
},
|
||||||
|
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 {state_key} to join the room")
|
||||||
|
},
|
||||||
|
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,15 +1,18 @@
|
|||||||
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,
|
||||||
|
EncryptionState,
|
||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
@@ -23,6 +26,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,
|
||||||
@@ -33,6 +51,7 @@ pub async fn register_notifications(
|
|||||||
}
|
}
|
||||||
let notify_via = settings.tunables.notifications.via;
|
let notify_via = settings.tunables.notifications.via;
|
||||||
let show_message = settings.tunables.notifications.show_message;
|
let show_message = settings.tunables.notifications.show_message;
|
||||||
|
let sound_hint = settings.tunables.notifications.sound_hint.clone();
|
||||||
let server_settings = client.notification_settings().await;
|
let server_settings = client.notification_settings().await;
|
||||||
let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
|
let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
|
||||||
return;
|
return;
|
||||||
@@ -43,6 +62,7 @@ pub async fn register_notifications(
|
|||||||
.register_notification_handler(move |notification, room: MatrixRoom, client: Client| {
|
.register_notification_handler(move |notification, room: MatrixRoom, client: Client| {
|
||||||
let store = store.clone();
|
let store = store.clone();
|
||||||
let server_settings = server_settings.clone();
|
let server_settings = server_settings.clone();
|
||||||
|
let sound_hint = sound_hint.clone();
|
||||||
async move {
|
async move {
|
||||||
let mode = global_or_room_mode(&server_settings, &room).await;
|
let mode = global_or_room_mode(&server_settings, &room).await;
|
||||||
if mode == RoomNotificationMode::Mute {
|
if mode == RoomNotificationMode::Mute {
|
||||||
@@ -53,7 +73,10 @@ pub async fn register_notifications(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match parse_notification(notification, room, show_message).await {
|
let room_id = room.room_id().to_owned();
|
||||||
|
match notification.event {
|
||||||
|
RawAnySyncOrStrippedTimelineEvent::Sync(e) => {
|
||||||
|
match parse_full_notification(e, room, show_message).await {
|
||||||
Ok((summary, body, server_ts)) => {
|
Ok((summary, body, server_ts)) => {
|
||||||
if server_ts < startup_ts {
|
if server_ts < startup_ts {
|
||||||
return;
|
return;
|
||||||
@@ -63,41 +86,98 @@ pub async fn register_notifications(
|
|||||||
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,
|
||||||
|
sound_hint.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!("Failed to extract notification data: {err}")
|
tracing::error!("Failed to extract notification data: {err}")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// Stripped events may be dropped silently because they're
|
||||||
|
// 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,
|
||||||
|
sound_hint: Option<&str>,
|
||||||
|
) {
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
if via.desktop {
|
||||||
|
send_notification_desktop(summary, body, room_id, store, sound_hint).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>) {
|
#[cfg_attr(target_os = "macos", allow(unused_variables))]
|
||||||
|
async fn send_notification_desktop(
|
||||||
|
summary: &str,
|
||||||
|
body: Option<&str>,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
_store: &AsyncProgramStore,
|
||||||
|
sound_hint: Option<&str>,
|
||||||
|
) {
|
||||||
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");
|
||||||
|
|
||||||
if let Some(body) = body {
|
if let Some(sound_hint) = sound_hint {
|
||||||
desktop_notification.body(&body);
|
desktop_notification.sound_name(sound_hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = desktop_notification.show() {
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
tracing::error!("Failed to send notification: {err}")
|
desktop_notification.urgency(notify_rust::Urgency::Normal);
|
||||||
|
|
||||||
|
if let Some(body) = body {
|
||||||
|
desktop_notification.body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
match desktop_notification.show() {
|
||||||
|
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)));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +193,8 @@ async fn global_or_room_mode(
|
|||||||
Ok(true) => IsOneToOne::Yes,
|
Ok(true) => IsOneToOne::Yes,
|
||||||
_ => IsOneToOne::No,
|
_ => IsOneToOne::No,
|
||||||
};
|
};
|
||||||
let is_encrypted = match room.is_encrypted().await {
|
let is_encrypted = match room.latest_encryption_state().await {
|
||||||
Ok(true) => IsEncrypted::Yes,
|
Ok(EncryptionState::Encrypted) => IsEncrypted::Yes,
|
||||||
_ => IsEncrypted::No,
|
_ => IsEncrypted::No,
|
||||||
};
|
};
|
||||||
settings
|
settings
|
||||||
@@ -155,12 +235,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 +252,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() {
|
||||||
|
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}")
|
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 +272,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 +283,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 +293,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 +314,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) =
|
||||||
|
|||||||
12
src/tests.rs
12
src/tests.rs
@@ -49,7 +49,8 @@ use crate::{
|
|||||||
const TEST_ROOM1_ALIAS: &str = "#room1:example.com";
|
const TEST_ROOM1_ALIAS: &str = "#room1:example.com";
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned();
|
pub static ref TEST_ROOM1_ID: OwnedRoomId =
|
||||||
|
RoomId::new_v1(server_name!("example.com")).to_owned();
|
||||||
pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned();
|
pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned();
|
||||||
pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
||||||
pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned();
|
pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned();
|
||||||
@@ -137,7 +138,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 +172,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 {
|
||||||
@@ -189,13 +192,16 @@ pub fn mock_tunables() -> TunableValues {
|
|||||||
external_edit_file_suffix: String::from(".md"),
|
external_edit_file_suffix: String::from(".md"),
|
||||||
username_display: UserDisplayStyle::Username,
|
username_display: UserDisplayStyle::Username,
|
||||||
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,
|
||||||
|
sound_hint: None,
|
||||||
},
|
},
|
||||||
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::{
|
||||||
@@ -65,7 +66,6 @@ use crate::base::{
|
|||||||
IambInfo,
|
IambInfo,
|
||||||
IambResult,
|
IambResult,
|
||||||
MessageAction,
|
MessageAction,
|
||||||
Need,
|
|
||||||
ProgramAction,
|
ProgramAction,
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
@@ -75,11 +75,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;
|
||||||
@@ -94,12 +96,12 @@ fn bold_style() -> Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn bold_span(s: &str) -> Span {
|
fn bold_span(s: &str) -> Span<'_> {
|
||||||
Span::styled(s, bold_style())
|
Span::styled(s, bold_style())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn bold_spans(s: &str) -> Line {
|
fn bold_spans(s: &str) -> Line<'_> {
|
||||||
bold_span(s).into()
|
bold_span(s).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,12 +115,12 @@ fn selected_style(selected: bool) -> Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn selected_span(s: &str, selected: bool) -> Span {
|
fn selected_span(s: &str, selected: bool) -> Span<'_> {
|
||||||
Span::styled(s, selected_style(selected))
|
Span::styled(s, selected_style(selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn selected_text(s: &str, selected: bool) -> Text {
|
fn selected_text(s: &str, selected: bool) -> Text<'_> {
|
||||||
Text::from(selected_span(s, selected))
|
Text::from(selected_span(s, selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +170,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 +191,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 +202,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 +214,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 +225,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 +285,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 +367,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 +522,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 +568,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 +600,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,12 +634,13 @@ 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);
|
||||||
|
|
||||||
List::new(store)
|
List::new(store)
|
||||||
.empty_message("You do not have rooms or dms yet")
|
.empty_message("You do not have any unreads yet")
|
||||||
.empty_alignment(Alignment::Center)
|
.empty_alignment(Alignment::Center)
|
||||||
.focus(focused)
|
.focus(focused)
|
||||||
.render(area, buf, state);
|
.render(area, buf, state);
|
||||||
@@ -625,7 +655,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);
|
||||||
|
|
||||||
@@ -711,7 +742,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tab_title(&self, store: &mut ProgramStore) -> Line {
|
fn get_tab_title(&self, store: &mut ProgramStore) -> Line<'_> {
|
||||||
match self {
|
match self {
|
||||||
IambWindow::DirectList(_) => bold_spans("Direct Messages"),
|
IambWindow::DirectList(_) => bold_spans("Direct Messages"),
|
||||||
IambWindow::RoomList(_) => bold_spans("Rooms"),
|
IambWindow::RoomList(_) => bold_spans("Rooms"),
|
||||||
@@ -739,7 +770,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_win_title(&self, store: &mut ProgramStore) -> Line {
|
fn get_win_title(&self, store: &mut ProgramStore) -> Line<'_> {
|
||||||
match self {
|
match self {
|
||||||
IambWindow::DirectList(_) => bold_spans("Direct Messages"),
|
IambWindow::DirectList(_) => bold_spans("Direct Messages"),
|
||||||
IambWindow::RoomList(_) => bold_spans("Rooms"),
|
IambWindow::RoomList(_) => bold_spans("Rooms"),
|
||||||
@@ -769,7 +800,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
let (room, name, tags) = store.application.worker.get_room(room_id)?;
|
let (room, name, tags) = store.application.worker.get_room(room_id)?;
|
||||||
let room = RoomState::new(room, thread, name, tags, store);
|
let room = RoomState::new(room, thread, name, tags, store);
|
||||||
|
|
||||||
store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS);
|
store.application.need_load.need_members(room.id().to_owned());
|
||||||
return Ok(room.into());
|
return Ok(room.into());
|
||||||
},
|
},
|
||||||
IambId::DirectList => {
|
IambId::DirectList => {
|
||||||
@@ -831,7 +862,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
let (room, name, tags) = store.application.worker.get_room(room_id)?;
|
let (room, name, tags) = store.application.worker.get_room(room_id)?;
|
||||||
let room = RoomState::new(room, None, name, tags, store);
|
let room = RoomState::new(room, None, name, tags, store);
|
||||||
|
|
||||||
store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS);
|
store.application.need_load.need_members(room.id().to_owned());
|
||||||
Ok(room.into())
|
Ok(room.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -914,6 +945,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 {
|
||||||
@@ -923,7 +958,12 @@ impl Display for GenericChatItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem<IambInfo> for GenericChatItem {
|
impl ListItem<IambInfo> for GenericChatItem {
|
||||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
fn show(
|
||||||
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
_: &ViewportContext<ListCursor>,
|
||||||
|
_: &mut ProgramStore,
|
||||||
|
) -> Text<'_> {
|
||||||
let unread = self.unread.is_unread();
|
let unread = self.unread.is_unread();
|
||||||
let style = selected_style(selected);
|
let style = selected_style(selected);
|
||||||
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
||||||
@@ -1024,16 +1064,25 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem<IambInfo> for RoomItem {
|
impl ListItem<IambInfo> for RoomItem {
|
||||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
fn show(
|
||||||
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
_: &ViewportContext<ListCursor>,
|
||||||
|
_: &mut ProgramStore,
|
||||||
|
) -> Text<'_> {
|
||||||
let unread = self.unread.is_unread();
|
let unread = self.unread.is_unread();
|
||||||
let style = selected_style(selected);
|
let style = selected_style(selected);
|
||||||
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
||||||
@@ -1124,6 +1173,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 {
|
||||||
@@ -1133,7 +1186,12 @@ impl Display for DirectItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem<IambInfo> for DirectItem {
|
impl ListItem<IambInfo> for DirectItem {
|
||||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
fn show(
|
||||||
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
_: &ViewportContext<ListCursor>,
|
||||||
|
_: &mut ProgramStore,
|
||||||
|
) -> Text<'_> {
|
||||||
let unread = self.unread.is_unread();
|
let unread = self.unread.is_unread();
|
||||||
let style = selected_style(selected);
|
let style = selected_style(selected);
|
||||||
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
||||||
@@ -1223,16 +1281,25 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem<IambInfo> for SpaceItem {
|
impl ListItem<IambInfo> for SpaceItem {
|
||||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
fn show(
|
||||||
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
_: &ViewportContext<ListCursor>,
|
||||||
|
_: &mut ProgramStore,
|
||||||
|
) -> Text<'_> {
|
||||||
selected_text(self.name.as_str(), selected)
|
selected_text(self.name.as_str(), selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,7 +1430,12 @@ impl Display for VerifyItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem<IambInfo> for VerifyItem {
|
impl ListItem<IambInfo> for VerifyItem {
|
||||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
fn show(
|
||||||
|
&self,
|
||||||
|
selected: bool,
|
||||||
|
_: &ViewportContext<ListCursor>,
|
||||||
|
_: &mut ProgramStore,
|
||||||
|
) -> Text<'_> {
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
|
|
||||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||||
@@ -1473,7 +1545,7 @@ impl ListItem<IambInfo> for MemberItem {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
_: &ViewportContext<ListCursor>,
|
_: &ViewportContext<ListCursor>,
|
||||||
store: &mut ProgramStore,
|
store: &mut ProgramStore,
|
||||||
) -> Text {
|
) -> Text<'_> {
|
||||||
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
||||||
let user_id = self.member.user_id();
|
let user_id = self.member.user_id();
|
||||||
|
|
||||||
@@ -1517,6 +1589,10 @@ impl ListItem<IambInfo> for MemberItem {
|
|||||||
fn get_word(&self) -> Option<String> {
|
fn get_word(&self) -> Option<String> {
|
||||||
self.member.user_id().to_string().into()
|
self.member.user_id().to_string().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matches(&self, needle: ®ex::Regex) -> bool {
|
||||||
|
needle.is_match(self.member.name()) || needle.is_match(self.member.user_id().as_str())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem {
|
impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem {
|
||||||
@@ -1556,6 +1632,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,46 +1659,55 @@ 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 {
|
||||||
room_id: RoomId::new(server).to_owned(),
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
tags: vec![TagName::Favorite],
|
tags: vec![TagName::Favorite],
|
||||||
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 {
|
||||||
room_id: RoomId::new(server).to_owned(),
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
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 {
|
||||||
room_id: RoomId::new(server).to_owned(),
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
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 +1717,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,24 +1727,27 @@ 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 {
|
||||||
room_id: RoomId::new(server).to_owned(),
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
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 {
|
||||||
room_id: RoomId::new(server).to_owned(),
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
alias: None,
|
alias: None,
|
||||||
name: "Room 2",
|
name: "Room 2",
|
||||||
@@ -1666,10 +1755,11 @@ 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 {
|
||||||
room_id: RoomId::new(server).to_owned(),
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
tags: vec![],
|
tags: vec![],
|
||||||
alias: None,
|
alias: None,
|
||||||
name: "Room 3",
|
name: "Room 3",
|
||||||
@@ -1677,18 +1767,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_v1(server).to_owned(),
|
||||||
|
tags: vec![],
|
||||||
|
alias: None,
|
||||||
|
name: "Old room 1",
|
||||||
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let room2 = TestRoomItem {
|
||||||
|
room_id: RoomId::new_v1(server).to_owned(),
|
||||||
|
tags: vec![],
|
||||||
|
alias: None,
|
||||||
|
name: "Old room 2",
|
||||||
|
unread: UnreadInfo::default(),
|
||||||
|
invite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let room3 = TestRoomItem {
|
||||||
|
room_id: RoomId::new_v1(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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use edit::edit_with_builder as external_edit;
|
use edit::edit_with_builder as external_edit;
|
||||||
use edit::Builder;
|
use edit::Builder;
|
||||||
|
use matrix_sdk::EncryptionState;
|
||||||
use modalkit::editing::store::RegisterError;
|
use modalkit::editing::store::RegisterError;
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tokio;
|
use tokio;
|
||||||
use url::Url;
|
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 +88,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};
|
||||||
@@ -213,12 +222,10 @@ impl ChatState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (source, msg_filename) = match &ev.content.msgtype {
|
let (source, msg_filename) = match &ev.content.msgtype {
|
||||||
MessageType::Audio(c) => (c.source.clone(), c.body.as_str()),
|
MessageType::Audio(c) => (c.source.clone(), c.filename()),
|
||||||
MessageType::File(c) => {
|
MessageType::File(c) => (c.source.clone(), c.filename()),
|
||||||
(c.source.clone(), c.filename.as_deref().unwrap_or(c.body.as_str()))
|
MessageType::Image(c) => (c.source.clone(), c.filename()),
|
||||||
},
|
MessageType::Video(c) => (c.source.clone(), c.filename()),
|
||||||
MessageType::Image(c) => (c.source.clone(), c.body.as_str()),
|
|
||||||
MessageType::Video(c) => (c.source.clone(), c.body.as_str()),
|
|
||||||
_ => {
|
_ => {
|
||||||
if !flags.contains(DownloadFlags::OPEN) {
|
if !flags.contains(DownloadFlags::OPEN) {
|
||||||
return Err(IambError::NoAttachment.into());
|
return Err(IambError::NoAttachment.into());
|
||||||
@@ -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() {
|
||||||
@@ -252,7 +263,7 @@ impl ChatState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if filename.is_dir() {
|
if filename.is_dir() {
|
||||||
filename.push(msg_filename);
|
filename.push(msg_filename.replace(std::path::MAIN_SEPARATOR_STR, "_"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if filename.exists() && !flags.contains(DownloadFlags::FORCE) {
|
if filename.exists() && !flags.contains(DownloadFlags::FORCE) {
|
||||||
@@ -262,9 +273,9 @@ impl ChatState {
|
|||||||
let mut filename_incr = filename.clone();
|
let mut filename_incr = filename.clone();
|
||||||
for n in 1..=1000 {
|
for n in 1..=1000 {
|
||||||
if let Some(ext) = ext.and_then(OsStr::to_str) {
|
if let Some(ext) = ext.and_then(OsStr::to_str) {
|
||||||
filename_incr.set_file_name(format!("{}-{}.{}", stem, n, ext));
|
filename_incr.set_file_name(format!("{stem}-{n}.{ext}"));
|
||||||
} else {
|
} else {
|
||||||
filename_incr.set_file_name(format!("{}-{}", stem, n));
|
filename_incr.set_file_name(format!("{stem}-{n}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filename_incr.exists() {
|
if !filename_incr.exists() {
|
||||||
@@ -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());
|
||||||
@@ -389,7 +401,7 @@ impl ChatState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if info.user_reactions_contains(&settings.profile.user_id, &event_id, &emoji) {
|
if info.user_reactions_contains(&settings.profile.user_id, &event_id, &emoji) {
|
||||||
let msg = format!("You’ve already reacted to this message with {}", emoji);
|
let msg = format!("You’ve already reacted to this message with {emoji}");
|
||||||
let err = UIError::Failure(msg);
|
let err = UIError::Failure(msg);
|
||||||
|
|
||||||
return Err(err);
|
return Err(err);
|
||||||
@@ -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());
|
||||||
@@ -437,6 +450,21 @@ impl ChatState {
|
|||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
},
|
},
|
||||||
|
MessageAction::Replied => {
|
||||||
|
let Some(reply) = msg.reply_to() else {
|
||||||
|
let msg = "Selected message is not a reply";
|
||||||
|
return Err(UIError::Failure(msg.into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(key) = info.get_message_key(&reply) else {
|
||||||
|
store.application.need_load.need_message(self.room_id.clone(), reply);
|
||||||
|
let msg = "Replied to message will be loaded in the background";
|
||||||
|
return Err(UIError::Failure(msg.into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
self.scrollback.goto_message(key.clone());
|
||||||
|
Ok(None)
|
||||||
|
},
|
||||||
MessageAction::Unreact(reaction, literal) => {
|
MessageAction::Unreact(reaction, literal) => {
|
||||||
let emoji = match reaction {
|
let emoji = match reaction {
|
||||||
reaction if literal => reaction,
|
reaction if literal => reaction,
|
||||||
@@ -464,6 +492,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,17 +625,16 @@ 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)?;
|
|
||||||
let mime = mime::IMAGE_PNG;
|
let mime = mime::IMAGE_PNG;
|
||||||
|
|
||||||
let name = "Clipboard.png";
|
let name = "Clipboard.png";
|
||||||
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 +663,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 +674,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 +784,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 +889,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 +922,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 +944,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) {
|
||||||
@@ -954,7 +992,16 @@ impl<'a> StatefulWidget for Chat<'a> {
|
|||||||
Paragraph::new(desc_spans).render(descarea, buf);
|
Paragraph::new(desc_spans).render(descarea, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
let prompt = if self.focused { "> " } else { " " };
|
let prompt = match (self.focused, state.room().encryption_state()) {
|
||||||
|
(false, _) => Span::raw(" "),
|
||||||
|
(_, EncryptionState::Encrypted) => {
|
||||||
|
Span::styled("\u{1F512}\u{FE0E} ", Style::new().fg(Color::LightGreen))
|
||||||
|
},
|
||||||
|
(_, EncryptionState::NotEncrypted) => {
|
||||||
|
Span::styled("\u{1F513}\u{FE0E} ", Style::new().fg(Color::Red))
|
||||||
|
},
|
||||||
|
(_, EncryptionState::Unknown) => Span::styled("> ", Style::new().fg(Color::Red)),
|
||||||
|
};
|
||||||
|
|
||||||
let tbox = TextBox::new().prompt(prompt);
|
let tbox = TextBox::new().prompt(prompt);
|
||||||
tbox.render(textarea, buf, &mut state.tbox);
|
tbox.render(textarea, buf, &mut state.tbox);
|
||||||
@@ -990,3 +1037,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;
|
||||||
@@ -119,19 +120,19 @@ fn hist_visibility_mode(name: impl Into<String>) -> IambResult<HistoryVisibility
|
|||||||
/// that operations like sending and accepting invites, opening the members window, etc., all work
|
/// that operations like sending and accepting invites, opening the members window, etc., all work
|
||||||
/// similarly.
|
/// similarly.
|
||||||
pub enum RoomState {
|
pub enum RoomState {
|
||||||
Chat(ChatState),
|
Chat(Box<ChatState>),
|
||||||
Space(SpaceState),
|
Space(Box<SpaceState>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ChatState> for RoomState {
|
impl From<ChatState> for RoomState {
|
||||||
fn from(chat: ChatState) -> Self {
|
fn from(chat: ChatState) -> Self {
|
||||||
RoomState::Chat(chat)
|
RoomState::Chat(Box::new(chat))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SpaceState> for RoomState {
|
impl From<SpaceState> for RoomState {
|
||||||
fn from(space: SpaceState) -> Self {
|
fn from(space: SpaceState) -> Self {
|
||||||
RoomState::Space(space)
|
RoomState::Space(Box::new(space))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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() {
|
||||||
@@ -634,7 +658,7 @@ impl RoomState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_title(&self, store: &mut ProgramStore) -> Line {
|
pub fn get_title(&self, store: &mut ProgramStore) -> Line<'_> {
|
||||||
let title = store.application.get_room_title(self.id());
|
let title = store.application.get_room_title(self.id());
|
||||||
let style = Style::default().add_modifier(StyleModifier::BOLD);
|
let style = Style::default().add_modifier(StyleModifier::BOLD);
|
||||||
let mut spans = vec![];
|
let mut spans = vec![];
|
||||||
@@ -752,8 +776,8 @@ impl WindowOps<IambInfo> for RoomState {
|
|||||||
|
|
||||||
fn dup(&self, store: &mut ProgramStore) -> Self {
|
fn dup(&self, store: &mut ProgramStore) -> Self {
|
||||||
match self {
|
match self {
|
||||||
RoomState::Chat(chat) => RoomState::Chat(chat.dup(store)),
|
RoomState::Chat(chat) => RoomState::Chat(Box::new(chat.dup(store))),
|
||||||
RoomState::Space(space) => RoomState::Space(space.dup(store)),
|
RoomState::Space(space) => RoomState::Space(Box::new(space.dup(store))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ use crate::{
|
|||||||
IambId,
|
IambId,
|
||||||
IambInfo,
|
IambInfo,
|
||||||
IambResult,
|
IambResult,
|
||||||
Need,
|
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
RoomFetchStatus,
|
RoomFetchStatus,
|
||||||
@@ -79,14 +78,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 +99,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,10 +156,20 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn goto_message(&mut self, target: MessageKey) {
|
||||||
|
let mut cursor = MessageCursor::new(target, 0);
|
||||||
|
std::mem::swap(&mut cursor, &mut self.cursor);
|
||||||
|
self.jumped.push(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the dimensions and placement within the terminal window for this list.
|
/// Set the dimensions and placement within the terminal window for this list.
|
||||||
pub fn set_term_info(&mut self, area: Rect) {
|
pub fn set_term_info(&mut self, area: Rect) {
|
||||||
self.viewctx.dimensions = (area.width as usize, area.height as usize);
|
self.viewctx.dimensions = (area.width as usize, area.height as usize);
|
||||||
@@ -678,10 +694,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|||||||
|
|
||||||
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
||||||
if needs_load {
|
if needs_load {
|
||||||
store
|
store.application.need_load.need_messages(self.room_id.clone());
|
||||||
.application
|
|
||||||
.need_load
|
|
||||||
.insert(self.room_id.clone(), Need::MESSAGES);
|
|
||||||
}
|
}
|
||||||
mc
|
mc
|
||||||
},
|
},
|
||||||
@@ -757,10 +770,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|||||||
|
|
||||||
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
||||||
if needs_load {
|
if needs_load {
|
||||||
store
|
store.application.need_load.need_messages(self.room_id.to_owned());
|
||||||
.application
|
|
||||||
.need_load
|
|
||||||
.insert(self.room_id.to_owned(), Need::MESSAGES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mc.map(|c| self._range_to(c))
|
mc.map(|c| self._range_to(c))
|
||||||
@@ -829,8 +839,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 +1294,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) {
|
||||||
@@ -1317,10 +1327,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
|||||||
k
|
k
|
||||||
} else {
|
} else {
|
||||||
if state.need_more_messages(info) {
|
if state.need_more_messages(info) {
|
||||||
self.store
|
self.store.application.need_load.need_messages(state.room_id.to_owned());
|
||||||
.application
|
|
||||||
.need_load
|
|
||||||
.insert(state.room_id.to_owned(), Need::MESSAGES);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1340,7 +1347,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 +1364,17 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_preview = match msg_preview {
|
|
||||||
// Only take the preview into the matching row number.
|
// 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 {
|
||||||
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 +1409,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,17 +1424,14 @@ 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we should load older messages for this room.
|
// Check whether we should load older messages for this room.
|
||||||
if state.need_more_messages(info) {
|
if state.need_more_messages(info) {
|
||||||
// If the top of the screen is the older message, load more.
|
// If the top of the screen is the older message, load more.
|
||||||
self.store
|
self.store.application.need_load.need_messages(state.room_id.to_owned());
|
||||||
.application
|
|
||||||
.need_load
|
|
||||||
.insert(state.room_id.to_owned(), Need::MESSAGES);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info.draw_last = self.store.application.draw_curr;
|
info.draw_last = self.store.application.draw_curr;
|
||||||
@@ -1431,7 +1441,7 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::*;
|
use crate::{base::Need, tests::*};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_search_messages() {
|
async fn test_search_messages() {
|
||||||
@@ -1476,7 +1486,7 @@ mod tests {
|
|||||||
std::mem::take(&mut store.application.need_load)
|
std::mem::take(&mut store.application.need_load)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<(OwnedRoomId, Need)>>(),
|
.collect::<Vec<(OwnedRoomId, Need)>>(),
|
||||||
vec![(room_id.clone(), Need::MESSAGES)]
|
vec![(room_id.clone(), Need { messages: Some(Vec::new()), members: false })]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Search forward twice to MSG1.
|
// Search forward twice to MSG1.
|
||||||
@@ -1518,8 +1528,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 +1564,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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
//! Window for Matrix spaces
|
//! Window for Matrix spaces
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::str::FromStr;
|
||||||
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::ruma::OwnedSpaceChildOrder;
|
||||||
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 +27,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 +82,79 @@ 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
|
||||||
|
.power_levels()
|
||||||
|
.await
|
||||||
|
.map_err(matrix_sdk::Error::from)
|
||||||
|
.map_err(IambError::from)?
|
||||||
|
.user_can_send_state(
|
||||||
|
&store.application.settings.profile.user_id,
|
||||||
|
StateEventType::SpaceChild,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Err(IambError::InsufficientPermission.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let via = self.room.route().await.map_err(IambError::from)?;
|
||||||
|
let mut ev = SpaceChildEventContent::new(via);
|
||||||
|
ev.order = order
|
||||||
|
.as_deref()
|
||||||
|
.map(OwnedSpaceChildOrder::from_str)
|
||||||
|
.transpose()
|
||||||
|
.map_err(IambError::InvalidSpaceChildOrder)?;
|
||||||
|
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
|
||||||
|
.power_levels()
|
||||||
|
.await
|
||||||
|
.map_err(matrix_sdk::Error::from)
|
||||||
|
.map_err(IambError::from)?
|
||||||
|
.user_can_send_state(
|
||||||
|
&store.application.settings.profile.user_id,
|
||||||
|
StateEventType::SpaceChild,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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 +194,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 +224,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());
|
||||||
|
|||||||
191
src/worker.rs
191
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,15 +80,15 @@ use matrix_sdk::{
|
|||||||
},
|
},
|
||||||
Client,
|
Client,
|
||||||
ClientBuildError,
|
ClientBuildError,
|
||||||
DisplayName,
|
|
||||||
Error as MatrixError,
|
Error as MatrixError,
|
||||||
|
RoomDisplayName,
|
||||||
RoomMemberships,
|
RoomMemberships,
|
||||||
};
|
};
|
||||||
|
|
||||||
use modalkit::errors::UIError;
|
use modalkit::errors::UIError;
|
||||||
use modalkit::prelude::{EditInfo, InfoMessage};
|
use modalkit::prelude::{EditInfo, InfoMessage};
|
||||||
|
|
||||||
use crate::base::Need;
|
use crate::base::MessageNeed;
|
||||||
use crate::notifications::register_notifications;
|
use crate::notifications::register_notifications;
|
||||||
use crate::{
|
use crate::{
|
||||||
base::{
|
base::{
|
||||||
@@ -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,13 +210,13 @@ 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Plan {
|
enum Plan {
|
||||||
Messages(OwnedRoomId, Option<String>),
|
Messages(OwnedRoomId, Option<String>, Vec<MessageNeed>),
|
||||||
Members(OwnedRoomId),
|
Members(OwnedRoomId),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +225,8 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
|
|||||||
let ChatStore { need_load, rooms, .. } = &mut locked.application;
|
let ChatStore { need_load, rooms, .. } = &mut locked.application;
|
||||||
let mut plan = Vec::with_capacity(need_load.rooms() * 2);
|
let mut plan = Vec::with_capacity(need_load.rooms() * 2);
|
||||||
|
|
||||||
for (room_id, mut need) in std::mem::take(need_load).into_iter() {
|
for (room_id, need) in std::mem::take(need_load).into_iter() {
|
||||||
if need.contains(Need::MESSAGES) {
|
if let Some(message_need) = need.messages {
|
||||||
let info = rooms.get_or_default(room_id.clone());
|
let info = rooms.get_or_default(room_id.clone());
|
||||||
|
|
||||||
if !info.recently_fetched() && !info.fetching {
|
if !info.recently_fetched() && !info.fetching {
|
||||||
@@ -238,16 +239,11 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
|
|||||||
RoomFetchStatus::NotStarted => None,
|
RoomFetchStatus::NotStarted => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
plan.push(Plan::Messages(room_id.to_owned(), fetch_id));
|
plan.push(Plan::Messages(room_id.to_owned(), fetch_id, message_need));
|
||||||
need.remove(Need::MESSAGES);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if need.contains(Need::MEMBERS) {
|
if need.members {
|
||||||
plan.push(Plan::Members(room_id.to_owned()));
|
plan.push(Plan::Members(room_id.to_owned()));
|
||||||
need.remove(Need::MEMBERS);
|
|
||||||
}
|
|
||||||
if !need.is_empty() {
|
|
||||||
need_load.insert(room_id, need);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,14 +253,14 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
|
|||||||
async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) {
|
async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) {
|
||||||
let permit = permits.acquire().await;
|
let permit = permits.acquire().await;
|
||||||
match plan {
|
match plan {
|
||||||
Plan::Messages(room_id, fetch_id) => {
|
Plan::Messages(room_id, fetch_id, message_need) => {
|
||||||
let limit = MIN_MSG_LOAD;
|
let limit = MIN_MSG_LOAD;
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
let store_clone = store.clone();
|
let store_clone = store.clone();
|
||||||
|
|
||||||
let res = load_older_one(&client, &room_id, fetch_id, limit).await;
|
let res = load_older_one(&client, &room_id, fetch_id, limit).await;
|
||||||
let mut locked = store.lock().await;
|
let mut locked = store.lock().await;
|
||||||
load_insert(room_id, res, locked.deref_mut(), store_clone);
|
load_insert(room_id, res, locked.deref_mut(), store_clone, message_need);
|
||||||
},
|
},
|
||||||
Plan::Members(room_id) => {
|
Plan::Members(room_id) => {
|
||||||
let res = members_load(client, &room_id).await;
|
let res = members_load(client, &room_id).await;
|
||||||
@@ -282,6 +278,9 @@ async fn load_older_one(
|
|||||||
limit: u32,
|
limit: u32,
|
||||||
) -> MessageFetchResult {
|
) -> MessageFetchResult {
|
||||||
if let Some(room) = client.get_room(room_id) {
|
if let Some(room) = client.get_room(room_id) {
|
||||||
|
// Update cached encryption state. This is a noop if the state is already cached.
|
||||||
|
let _ = room.request_encryption_state().await;
|
||||||
|
|
||||||
let mut opts = match &fetch_id {
|
let mut opts = match &fetch_id {
|
||||||
Some(id) => MessagesOptions::backward().from(id.as_str()),
|
Some(id) => MessagesOptions::backward().from(id.as_str()),
|
||||||
None => MessagesOptions::backward(),
|
None => MessagesOptions::backward(),
|
||||||
@@ -293,10 +292,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 +308,7 @@ async fn load_older_one(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let msg = msg.into_full_event(room_id.to_owned());
|
||||||
msgs.push((msg, receipts));
|
msgs.push((msg, receipts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +323,7 @@ fn load_insert(
|
|||||||
res: MessageFetchResult,
|
res: MessageFetchResult,
|
||||||
locked: &mut ProgramStore,
|
locked: &mut ProgramStore,
|
||||||
store: AsyncProgramStore,
|
store: AsyncProgramStore,
|
||||||
|
message_needs: Vec<MessageNeed>,
|
||||||
) {
|
) {
|
||||||
let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application;
|
let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application;
|
||||||
let info = rooms.get_or_default(room_id.clone());
|
let info = rooms.get_or_default(room_id.clone());
|
||||||
@@ -338,37 +337,57 @@ 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());
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore);
|
info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore);
|
||||||
|
|
||||||
|
// check if more are needed
|
||||||
|
let needs: Vec<_> = message_needs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|need| !info.keys.contains_key(&need.event_id) && need.ttl > 0)
|
||||||
|
.map(|mut need| {
|
||||||
|
need.ttl -= 1;
|
||||||
|
need
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !needs.is_empty() {
|
||||||
|
locked.application.need_load.need_messages_all(room_id, needs);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages");
|
warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages");
|
||||||
|
|
||||||
// Wait and try again.
|
// Wait and try again.
|
||||||
locked.application.need_load.insert(room_id, Need::MESSAGES);
|
locked.application.need_load.need_messages_all(room_id, message_needs);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,7 +459,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 +474,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 +509,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()))
|
||||||
|
});
|
||||||
|
|
||||||
|
updates.extend(changed);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
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 +546,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}"),
|
||||||
}
|
}
|
||||||
@@ -549,7 +569,7 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result
|
|||||||
let mut filter = FilterDefinition::default();
|
let mut filter = FilterDefinition::default();
|
||||||
filter.room = room_ev;
|
filter.room = room_ev;
|
||||||
|
|
||||||
let settings = SyncSettings::new().filter(filter.into());
|
let settings = SyncSettings::new().filter(filter.into()).timeout(Duration::from_secs(0));
|
||||||
|
|
||||||
client.sync_once(settings).await?;
|
client.sync_once(settings).await?;
|
||||||
|
|
||||||
@@ -562,12 +582,12 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result
|
|||||||
|
|
||||||
for room in sync_info.rooms.iter() {
|
for room in sync_info.rooms.iter() {
|
||||||
let room_id = room.as_ref().0.room_id().to_owned();
|
let room_id = room.as_ref().0.room_id().to_owned();
|
||||||
need_load.insert(room_id, Need::MESSAGES);
|
need_load.need_messages(room_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
for room in sync_info.dms.iter() {
|
for room in sync_info.dms.iter() {
|
||||||
let room_id = room.as_ref().0.room_id().to_owned();
|
let room_id = room.as_ref().0.room_id().to_owned();
|
||||||
need_load.insert(room_id, Need::MESSAGES);
|
need_load.need_messages(room_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -603,7 +623,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<()>),
|
||||||
@@ -700,7 +720,7 @@ async fn create_client_inner(
|
|||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let req_config = RequestConfig::new().timeout(req_timeout).retry_timeout(req_timeout);
|
let req_config = RequestConfig::new().timeout(req_timeout).max_retry_time(req_timeout);
|
||||||
|
|
||||||
// Set up the Matrix client for the selected profile.
|
// Set up the Matrix client for the selected profile.
|
||||||
let builder = Client::builder()
|
let builder = Client::builder()
|
||||||
@@ -1001,7 +1021,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 +1063,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,
|
||||||
@@ -1058,11 +1096,15 @@ impl ClientWorker {
|
|||||||
async move {
|
async move {
|
||||||
let room_id = room.room_id();
|
let room_id = room.room_id();
|
||||||
let room_info = room.clone_info();
|
let room_info = room.clone_info();
|
||||||
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1);
|
let rules = &room_info
|
||||||
|
.room_version()
|
||||||
|
.and_then(RoomVersionId::rules)
|
||||||
|
.unwrap_or(RoomVersionId::V1.rules().unwrap())
|
||||||
|
.redaction;
|
||||||
|
|
||||||
let mut locked = store.lock().await;
|
let mut locked = store.lock().await;
|
||||||
let info = locked.application.get_room_info(room_id.to_owned());
|
let info = locked.application.get_room_info(room_id.to_owned());
|
||||||
info.redact(ev, room_version);
|
info.redact(ev, rules);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1076,11 +1118,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()
|
.state_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();
|
||||||
@@ -1217,7 +1260,7 @@ impl ClientWorker {
|
|||||||
let settings = self.settings.clone();
|
let settings = self.settings.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
while !client.logged_in() {
|
while !client.is_active() {
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1309,7 +1352,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 +1389,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,9 +1432,9 @@ 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.summary.room_id).collect();
|
||||||
|
|
||||||
Ok(rooms)
|
Ok(rooms)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user