mirror of
https://github.com/youwen5/iamb.git
synced 2025-08-04 19:48:28 -07:00
Compare commits
No commits in common. "main" and "latest" have entirely different histories.
28 changed files with 1754 additions and 5081 deletions
12
.github/workflows/binaries.yml
vendored
12
.github/workflows/binaries.yml
vendored
|
@ -60,9 +60,9 @@ jobs:
|
|||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
uses: mozilla-actions/sccache-action@v0.0.3
|
||||
- name: 'Build: binary'
|
||||
run: cargo +stable build --release --locked --target ${{ env.TARGET }}
|
||||
run: cargo build --release --locked --target ${{ env.TARGET }}
|
||||
- name: 'Upload: binary'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
@ -73,8 +73,8 @@ jobs:
|
|||
- name: 'Package: deb'
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
cargo +stable install --locked cargo-deb
|
||||
cargo +stable deb --no-strip --target ${{ env.TARGET }}
|
||||
cargo install --locked cargo-deb
|
||||
cargo deb --no-strip --target ${{ env.TARGET }}
|
||||
- name: 'Upload: deb'
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -84,8 +84,8 @@ jobs:
|
|||
- name: 'Package: rpm'
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
cargo +stable install --locked cargo-generate-rpm
|
||||
cargo +stable generate-rpm --target ${{ env.TARGET }}
|
||||
cargo install --locked cargo-generate-rpm
|
||||
cargo generate-rpm --target ${{ env.TARGET }}
|
||||
- name: 'Upload: rpm'
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -22,8 +22,8 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install Rust (1.83 w/ clippy)
|
||||
uses: dtolnay/rust-toolchain@1.83
|
||||
- name: Install Rust (1.70 w/ clippy)
|
||||
uses: dtolnay/rust-toolchain@1.70
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install Rust (nightly w/ rustfmt)
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Run sccache-cache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
uses: mozilla-actions/sccache-action@v0.0.3
|
||||
- name: Check formatting
|
||||
run: cargo +nightly fmt --all -- --check
|
||||
- name: Check Clippy
|
||||
|
|
3532
Cargo.lock
generated
3532
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
24
Cargo.toml
24
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "iamb"
|
||||
version = "0.0.11-alpha.1"
|
||||
version = "0.0.10"
|
||||
edition = "2018"
|
||||
authors = ["Ulyssa <git@ulyssa.dev>"]
|
||||
repository = "https://github.com/ulyssa/iamb"
|
||||
|
@ -11,7 +11,7 @@ license = "Apache-2.0"
|
|||
exclude = [".github", "CONTRIBUTING.md"]
|
||||
keywords = ["matrix", "chat", "tui", "vim"]
|
||||
categories = ["command-line-utilities"]
|
||||
rust-version = "1.83"
|
||||
rust-version = "1.70"
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
|
@ -34,11 +34,10 @@ clap = {version = "~4.3", features = ["derive"]}
|
|||
css-color-parser = "0.1.2"
|
||||
dirs = "4.0.0"
|
||||
emojis = "0.5"
|
||||
feruca = "0.10.1"
|
||||
futures = "0.3"
|
||||
gethostname = "0.4.1"
|
||||
html5ever = "0.26.0"
|
||||
image = "^0.25.6"
|
||||
image = "0.24.5"
|
||||
libc = "0.2"
|
||||
markup5ever_rcdom = "0.2.0"
|
||||
mime = "^0.3.16"
|
||||
|
@ -46,8 +45,8 @@ mime_guess = "^2.0.4"
|
|||
nom = "7.0.0"
|
||||
open = "3.2.0"
|
||||
rand = "0.8.5"
|
||||
ratatui = "0.29.0"
|
||||
ratatui-image = { version = "~8.0.1", features = ["serde"] }
|
||||
ratatui = "0.26"
|
||||
ratatui-image = { version = "1.0.0", features = ["serde"] }
|
||||
regex = "^1.5"
|
||||
rpassword = "^7.2"
|
||||
serde = "^1.0"
|
||||
|
@ -64,7 +63,6 @@ unicode-width = "0.1.10"
|
|||
url = {version = "^2.2.2", features = ["serde"]}
|
||||
edit = "0.1.4"
|
||||
humansize = "2.0.0"
|
||||
linkify = "0.10.0"
|
||||
|
||||
[dependencies.comrak]
|
||||
version = "0.22.0"
|
||||
|
@ -72,24 +70,24 @@ default-features = false
|
|||
features = ["shortcodes"]
|
||||
|
||||
[dependencies.notify-rust]
|
||||
version = "~4.10.0"
|
||||
version = "4.10.0"
|
||||
default-features = false
|
||||
features = ["zbus", "serde"]
|
||||
optional = true
|
||||
|
||||
[dependencies.modalkit]
|
||||
version = "0.0.23"
|
||||
version = "0.0.20"
|
||||
default-features = false
|
||||
#git = "https://github.com/ulyssa/modalkit"
|
||||
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
|
||||
#rev = "24f3ec11c7f634005a27b26878d0fbbdcc08f272"
|
||||
|
||||
[dependencies.modalkit-ratatui]
|
||||
version = "0.0.23"
|
||||
version = "0.0.20"
|
||||
#git = "https://github.com/ulyssa/modalkit"
|
||||
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
|
||||
#rev = "24f3ec11c7f634005a27b26878d0fbbdcc08f272"
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
version = "0.10.0"
|
||||
version = "0.7.1"
|
||||
default-features = false
|
||||
features = ["e2e-encryption", "sqlite", "sso-login"]
|
||||
|
||||
|
|
22
README.md
22
README.md
|
@ -53,7 +53,7 @@ user_id = "@user:example.com"
|
|||
|
||||
## Installation (via `crates.io`)
|
||||
|
||||
Install Rust (1.83.0 or above) and Cargo, and then run:
|
||||
Install Rust (1.70.0 or above) and Cargo, and then run:
|
||||
|
||||
```
|
||||
cargo install --locked iamb
|
||||
|
@ -80,27 +80,9 @@ On FreeBSD a package is available from the official repositories. To install it
|
|||
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
|
||||
|
||||
On macOS a [package](https://formulae.brew.sh/formula/iamb#default) is available in Homebrew's
|
||||
On macOS a [package](https://formulae.brew.sh/formula/iamb#default) is availabe in Homebrew's
|
||||
repository. To install it simply run:
|
||||
|
||||
```
|
||||
|
|
67
docs/iamb.1
67
docs/iamb.1
|
@ -54,7 +54,7 @@ version and quit.
|
|||
View a list of joined rooms and direct messages.
|
||||
.It Sy ":dms"
|
||||
View a list of direct messages.
|
||||
.It Sy ":logout [user id]"
|
||||
.It Sy ":logout"
|
||||
Log out of
|
||||
.Nm .
|
||||
.It Sy ":rooms"
|
||||
|
@ -63,8 +63,6 @@ View a list of joined rooms.
|
|||
View a list of joined spaces.
|
||||
.It Sy ":unreads"
|
||||
View a list of unread rooms.
|
||||
.It Sy ":unreads clear"
|
||||
Mark all rooms as read.
|
||||
.It Sy ":welcome"
|
||||
View the startup Welcome window.
|
||||
.El
|
||||
|
@ -79,54 +77,39 @@ Import and decrypt keys from
|
|||
.Pa path .
|
||||
.It Sy ":verify"
|
||||
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
|
||||
|
||||
.Sh "MESSAGE COMMANDS"
|
||||
.Bl -tag -width Ds
|
||||
.It Sy ":download [path]"
|
||||
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 ":download"
|
||||
Download an attachment from the selected message.
|
||||
.It Sy ":edit"
|
||||
Edit the selected message.
|
||||
.It Sy ":editor"
|
||||
Open an external
|
||||
.Ev $EDITOR
|
||||
to compose a message.
|
||||
.It Sy ":open"
|
||||
Download and then open an attachment, or open a link in a message.
|
||||
.It Sy ":react [shortcode]"
|
||||
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]"
|
||||
Remove your reaction from the selected message.
|
||||
When no arguments are given, remove all of your reactions from the message.
|
||||
.It Sy ":redact [reason]"
|
||||
Redact the selected message with the optional reason.
|
||||
.It Sy ":reply"
|
||||
Reply to the selected message.
|
||||
.It Sy ":cancel"
|
||||
Cancel the currently drafted message including replies.
|
||||
.It Sy ":upload [path]"
|
||||
.It Sy ":upload"
|
||||
Upload an attachment and send it to the currently selected room.
|
||||
.El
|
||||
|
||||
.Sh "ROOM COMMANDS"
|
||||
.Bl -tag -width Ds
|
||||
.It Sy ":create [arguments]"
|
||||
Create a new room. Arguments can be
|
||||
.Dq ++alias=[alias] ,
|
||||
.Dq ++public ,
|
||||
.Dq ++space ,
|
||||
and
|
||||
.Dq ++encrypted .
|
||||
.It Sy ":create"
|
||||
Create a new room.
|
||||
.It Sy ":invite accept"
|
||||
Accept an invitation to the currently focused room.
|
||||
.It Sy ":invite reject"
|
||||
|
@ -134,7 +117,7 @@ Reject an invitation to the currently focused room.
|
|||
.It Sy ":invite send [user]"
|
||||
Send an invitation to a user to join the currently focused room.
|
||||
.It Sy ":join [room]"
|
||||
Join a room or open it if you are already joined.
|
||||
Join a room.
|
||||
.It Sy ":leave"
|
||||
Leave the currently focused room.
|
||||
.It Sy ":members"
|
||||
|
@ -143,10 +126,6 @@ View a list of members of the currently focused room.
|
|||
Set the name of the currently focused room.
|
||||
.It Sy ":room name unset"
|
||||
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]"
|
||||
Set a notification level for the currently focused room.
|
||||
Valid levels are
|
||||
|
@ -174,16 +153,12 @@ Remove a tag from the currently focused room.
|
|||
Set the topic of the currently focused room.
|
||||
.It Sy ":room topic unset"
|
||||
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]"
|
||||
Create and point the given alias to the room.
|
||||
.It Sy ":room alias unset [alias]"
|
||||
Delete the provided alias from the room's alternative alias list.
|
||||
.It Sy ":room alias show"
|
||||
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]"
|
||||
Set the room's canonical alias to the one provided, and make the previous one an alternative alias.
|
||||
.It Sy ":room canon unset [alias]"
|
||||
|
@ -198,18 +173,6 @@ Unban a user from this room with an optional reason.
|
|||
Kick a user from this room with an optional reason.
|
||||
.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"
|
||||
.Bl -tag -width Ds
|
||||
.It Sy ":horizontal [cmd]"
|
||||
|
|
34
docs/iamb.5
34
docs/iamb.5
|
@ -173,9 +173,6 @@ respective shortcodes.
|
|||
.It Sy message_user_color
|
||||
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
|
||||
When this subsection is present, you can enable and configure push notifications.
|
||||
See
|
||||
|
@ -211,9 +208,6 @@ See
|
|||
.Sx "SORTING LISTS"
|
||||
for more details.
|
||||
|
||||
.It Sy state_event_display
|
||||
Defines whether the state events like joined or left are shown.
|
||||
|
||||
.It Sy typing_notice_send
|
||||
Defines whether or not the typing state is sent.
|
||||
|
||||
|
@ -237,10 +231,6 @@ Possible values are
|
|||
Specify the width of the column where usernames are displayed in a room.
|
||||
Usernames that are too long are truncated.
|
||||
Defaults to 30.
|
||||
|
||||
.It Sy tabstop
|
||||
Number of spaces that a <Tab> counts for.
|
||||
Defaults to 4.
|
||||
.El
|
||||
|
||||
.Ss Example 1: Avoid showing Emojis (useful for terminals w/o support)
|
||||
|
@ -279,8 +269,6 @@ to use the desktop mechanism (default).
|
|||
Setting this field to
|
||||
.Dq Sy bell
|
||||
will use the terminal bell instead.
|
||||
Both can be used via
|
||||
.Dq Sy desktop|bell .
|
||||
|
||||
.It Sy show_message
|
||||
controls whether to show the message in the desktop notification, and defaults to
|
||||
|
@ -344,29 +332,9 @@ window.
|
|||
Defaults to
|
||||
.Sy ["power",\ "id"] .
|
||||
.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
|
||||
|
||||
.Ss Example 1: Group room members by their server first
|
||||
.Ss Example 1: Group room members by ther server first
|
||||
.Bd -literal -offset indent
|
||||
[settings.sort]
|
||||
members = ["server", "localpart"]
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="console-application">
|
||||
<id>chat.iamb.iamb</id>
|
||||
<id>iamb</id>
|
||||
|
||||
<name>iamb</name>
|
||||
<summary>A terminal Matrix client for Vim addicts</summary>
|
||||
<url type="homepage">https://iamb.chat</url>
|
||||
|
||||
<releases>
|
||||
<release version="0.0.10" date="2024-08-20"/>
|
||||
<release version="0.0.9" date="2024-03-28"/>
|
||||
</releases>
|
||||
|
||||
|
@ -15,7 +14,6 @@
|
|||
<name>Ulyssa</name>
|
||||
</developer>
|
||||
|
||||
<developer_name>Ulyssa</developer_name>
|
||||
<metadata_license>CC-BY-SA-4.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
|
||||
|
@ -25,8 +23,8 @@
|
|||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://iamb.chat/static/images/metainfo-screenshot.png</image>
|
||||
<caption>Example screenshot of room and lists of rooms, spaces and members within iamb</caption>
|
||||
<image>https://iamb.chat/static/images/iamb-demo.gif</image>
|
||||
<caption>Example conversation within iamb</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
|
@ -39,6 +37,7 @@
|
|||
</p>
|
||||
</description>
|
||||
|
||||
<icon type="remote">https://iamb.chat/images/iamb.svg</icon>
|
||||
<launchable type="desktop-id">iamb.desktop</launchable>
|
||||
|
||||
<categories>
|
||||
|
|
58
flake.lock
generated
58
flake.lock
generated
|
@ -5,11 +5,29 @@
|
|||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"lastModified": 1709126324,
|
||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||
"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"
|
||||
},
|
||||
"original": {
|
||||
|
@ -20,11 +38,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1736883708,
|
||||
"narHash": "sha256-uQ+NQ0/xYU0N1CnXsa2zghgNaOPxWpMJXSUJJ9W7140=",
|
||||
"lastModified": 1709703039,
|
||||
"narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "eb62e6aa39ea67e0b8018ba8ea077efe65807dc8",
|
||||
"rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -36,11 +54,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1736320768,
|
||||
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||
"lastModified": 1706487304,
|
||||
"narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||
"rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -59,14 +77,15 @@
|
|||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1736994333,
|
||||
"narHash": "sha256-v4Jrok5yXsZ6dwj2+2uo5cSyUi9fBTurHqHvNHLT1XA=",
|
||||
"lastModified": 1709863839,
|
||||
"narHash": "sha256-QpEL5FmZNi2By3sKZY55wGniFXc4wEn9PQczlE8TG0o=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "848db855cb9e88785996e961951659570fc58814",
|
||||
"rev": "e5ab9ee98f479081ad971473d2bc13c59e9fbc0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -89,6 +108,21 @@
|
|||
"repo": "default",
|
||||
"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",
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# 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-12-12".default;
|
||||
rustNightly = pkgs.rust-bin.nightly."2024-03-08".default;
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
|
@ -27,7 +27,7 @@
|
|||
};
|
||||
nativeBuildInputs = [ pkg-config ];
|
||||
buildInputs = [ openssl ] ++ lib.optionals stdenv.isDarwin
|
||||
(with darwin.apple_sdk.frameworks; [ AppKit Security Cocoa ]);
|
||||
(with darwin.apple_sdk.frameworks; [ AppKit Security Cocoa]);
|
||||
};
|
||||
|
||||
devShell = mkShell {
|
||||
|
@ -38,7 +38,6 @@
|
|||
pkg-config
|
||||
cargo-tarpaulin
|
||||
cargo-watch
|
||||
sqlite
|
||||
];
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[toolchain]
|
||||
channel = "1.83"
|
||||
components = [ "clippy" ]
|
282
src/base.rs
282
src/base.rs
|
@ -12,7 +12,6 @@ use std::sync::Arc;
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
use emojis::Emoji;
|
||||
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
|
@ -48,7 +47,6 @@ use matrix_sdk::{
|
|||
},
|
||||
room::redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent},
|
||||
tag::{TagName, Tags},
|
||||
AnySyncStateEvent,
|
||||
MessageLikeEvent,
|
||||
},
|
||||
presence::PresenceState,
|
||||
|
@ -74,7 +72,7 @@ use modalkit::{
|
|||
ApplicationStore,
|
||||
ApplicationWindowId,
|
||||
},
|
||||
completion::{complete_path, Completer, CompletionMap},
|
||||
completion::{complete_path, CompletionMap},
|
||||
context::EditContext,
|
||||
cursor::Cursor,
|
||||
rope::EditRope,
|
||||
|
@ -92,7 +90,6 @@ use modalkit::{
|
|||
|
||||
use crate::config::ImagePreviewProtocolValues;
|
||||
use crate::message::ImageStatus;
|
||||
use crate::notifications::NotificationHandle;
|
||||
use crate::preview::{source_from_event, spawn_insert_preview};
|
||||
use crate::{
|
||||
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
|
||||
|
@ -180,19 +177,6 @@ pub enum MessageAction {
|
|||
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.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum CreateRoomType {
|
||||
|
@ -259,9 +243,6 @@ pub enum SortFieldRoom {
|
|||
|
||||
/// Sort rooms by the timestamps of their most recent messages.
|
||||
Recent,
|
||||
|
||||
/// Sort rooms by whether they are invites.
|
||||
Invite,
|
||||
}
|
||||
|
||||
/// Fields that users can be sorted by.
|
||||
|
@ -296,7 +277,7 @@ impl<'de> Deserialize<'de> for SortColumn<SortFieldRoom> {
|
|||
/// [serde] visitor for deserializing [SortColumn] for rooms and spaces.
|
||||
struct SortRoomVisitor;
|
||||
|
||||
impl Visitor<'_> for SortRoomVisitor {
|
||||
impl<'de> Visitor<'de> for SortRoomVisitor {
|
||||
type Value = SortColumn<SortFieldRoom>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -326,7 +307,6 @@ impl Visitor<'_> for SortRoomVisitor {
|
|||
"name" => SortFieldRoom::Name,
|
||||
"alias" => SortFieldRoom::Alias,
|
||||
"id" => SortFieldRoom::RoomId,
|
||||
"invite" => SortFieldRoom::Invite,
|
||||
_ => {
|
||||
let msg = format!("Unknown sort field: {value:?}");
|
||||
return Err(E::custom(msg));
|
||||
|
@ -349,7 +329,7 @@ impl<'de> Deserialize<'de> for SortColumn<SortFieldUser> {
|
|||
/// [serde] visitor for deserializing [SortColumn] for users.
|
||||
struct SortUserVisitor;
|
||||
|
||||
impl Visitor<'_> for SortUserVisitor {
|
||||
impl<'de> Visitor<'de> for SortUserVisitor {
|
||||
type Value = SortColumn<SortFieldUser>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -395,9 +375,6 @@ pub enum RoomField {
|
|||
/// The room name.
|
||||
Name,
|
||||
|
||||
/// The room id.
|
||||
Id,
|
||||
|
||||
/// A room tag.
|
||||
Tag(TagName),
|
||||
|
||||
|
@ -516,9 +493,6 @@ pub enum IambAction {
|
|||
/// Perform an action on the currently selected message.
|
||||
Message(MessageAction),
|
||||
|
||||
/// Perform an action on the current space.
|
||||
Space(SpaceAction),
|
||||
|
||||
/// Open a URL.
|
||||
OpenLink(String),
|
||||
|
||||
|
@ -560,12 +534,6 @@ impl From<MessageAction> for IambAction {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SpaceAction> for IambAction {
|
||||
fn from(act: SpaceAction) -> Self {
|
||||
IambAction::Space(act)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomAction> for IambAction {
|
||||
fn from(act: RoomAction) -> Self {
|
||||
IambAction::Room(act)
|
||||
|
@ -585,7 +553,6 @@ impl ApplicationAction for IambAction {
|
|||
IambAction::Homeserver(..) => SequenceStatus::Break,
|
||||
IambAction::Keys(..) => SequenceStatus::Break,
|
||||
IambAction::Message(..) => SequenceStatus::Break,
|
||||
IambAction::Space(..) => SequenceStatus::Break,
|
||||
IambAction::Room(..) => SequenceStatus::Break,
|
||||
IambAction::OpenLink(..) => SequenceStatus::Break,
|
||||
IambAction::Send(..) => SequenceStatus::Break,
|
||||
|
@ -601,7 +568,6 @@ impl ApplicationAction for IambAction {
|
|||
IambAction::Homeserver(..) => SequenceStatus::Atom,
|
||||
IambAction::Keys(..) => SequenceStatus::Atom,
|
||||
IambAction::Message(..) => SequenceStatus::Atom,
|
||||
IambAction::Space(..) => SequenceStatus::Atom,
|
||||
IambAction::OpenLink(..) => SequenceStatus::Atom,
|
||||
IambAction::Room(..) => SequenceStatus::Atom,
|
||||
IambAction::Send(..) => SequenceStatus::Atom,
|
||||
|
@ -617,7 +583,6 @@ impl ApplicationAction for IambAction {
|
|||
IambAction::Homeserver(..) => SequenceStatus::Ignore,
|
||||
IambAction::Keys(..) => SequenceStatus::Ignore,
|
||||
IambAction::Message(..) => SequenceStatus::Ignore,
|
||||
IambAction::Space(..) => SequenceStatus::Ignore,
|
||||
IambAction::Room(..) => SequenceStatus::Ignore,
|
||||
IambAction::OpenLink(..) => SequenceStatus::Ignore,
|
||||
IambAction::Send(..) => SequenceStatus::Ignore,
|
||||
|
@ -632,7 +597,6 @@ impl ApplicationAction for IambAction {
|
|||
IambAction::ClearUnreads => false,
|
||||
IambAction::Homeserver(..) => false,
|
||||
IambAction::Message(..) => false,
|
||||
IambAction::Space(..) => false,
|
||||
IambAction::Room(..) => false,
|
||||
IambAction::Keys(..) => false,
|
||||
IambAction::Send(..) => false,
|
||||
|
@ -650,12 +614,6 @@ impl From<RoomAction> for ProgramAction {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SpaceAction> for ProgramAction {
|
||||
fn from(act: SpaceAction) -> Self {
|
||||
IambAction::from(act).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IambAction> for ProgramAction {
|
||||
fn from(act: IambAction) -> Self {
|
||||
Action::Application(act)
|
||||
|
@ -751,22 +709,10 @@ pub enum IambError {
|
|||
#[error("Current window is not a room or space")]
|
||||
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.
|
||||
#[error("Current window is not a room")]
|
||||
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.
|
||||
#[error("You do not have a current invitation to this room")]
|
||||
NotInvited,
|
||||
|
@ -839,9 +785,6 @@ pub enum EventLocation {
|
|||
|
||||
/// The [EventId] belongs to a reaction to the given event.
|
||||
Reaction(OwnedEventId),
|
||||
|
||||
/// The [EventId] belongs to a state event in the main timeline of the room.
|
||||
State(MessageKey),
|
||||
}
|
||||
|
||||
impl EventLocation {
|
||||
|
@ -871,6 +814,7 @@ impl UnreadInfo {
|
|||
}
|
||||
|
||||
/// Information about room's the user's joined.
|
||||
#[derive(Default)]
|
||||
pub struct RoomInfo {
|
||||
/// The display name for this room.
|
||||
pub name: Option<String>,
|
||||
|
@ -885,13 +829,15 @@ pub struct RoomInfo {
|
|||
messages: Messages,
|
||||
|
||||
/// A map of read markers to display on different events.
|
||||
pub event_receipts: HashMap<ReceiptThread, HashMap<OwnedEventId, HashSet<OwnedUserId>>>,
|
||||
pub event_receipts: HashMap<OwnedEventId, HashSet<OwnedUserId>>,
|
||||
|
||||
/// A map of the most recent read marker for each user.
|
||||
///
|
||||
/// Every receipt in this map should also have an entry in [`event_receipts`](`Self::event_receipts`),
|
||||
/// Every receipt in this map should also have an entry in [`event_receipts`],
|
||||
/// 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.
|
||||
pub user_receipts: HashMap<ReceiptThread, HashMap<OwnedUserId, OwnedEventId>>,
|
||||
pub user_receipts: HashMap<OwnedUserId, OwnedEventId>,
|
||||
|
||||
/// A map of message identifiers to a map of reaction events.
|
||||
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
||||
|
||||
|
@ -917,28 +863,6 @@ pub struct RoomInfo {
|
|||
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 {
|
||||
pub fn get_thread(&self, root: Option<&EventId>) -> Option<&Messages> {
|
||||
if let Some(thread_root) = root {
|
||||
|
@ -950,9 +874,7 @@ impl RoomInfo {
|
|||
|
||||
pub fn get_thread_mut(&mut self, root: Option<OwnedEventId>) -> &mut Messages {
|
||||
if let Some(thread_root) = root {
|
||||
self.threads
|
||||
.entry(thread_root.clone())
|
||||
.or_insert_with(|| Messages::thread(thread_root))
|
||||
self.threads.entry(thread_root).or_default()
|
||||
} else {
|
||||
&mut self.messages
|
||||
}
|
||||
|
@ -1030,12 +952,6 @@ impl RoomInfo {
|
|||
|
||||
match self.keys.get(redacts) {
|
||||
None => return,
|
||||
Some(EventLocation::State(key)) => {
|
||||
if let Some(msg) = self.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
msg.redact(ev, room_version);
|
||||
}
|
||||
},
|
||||
Some(EventLocation::Message(None, key)) => {
|
||||
if let Some(msg) = self.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
|
@ -1092,9 +1008,7 @@ impl RoomInfo {
|
|||
};
|
||||
|
||||
let source = if let Some(thread) = thread {
|
||||
self.threads
|
||||
.entry(thread.clone())
|
||||
.or_insert_with(|| Messages::thread(thread.clone()))
|
||||
self.threads.entry(thread.clone()).or_default()
|
||||
} else {
|
||||
&mut self.messages
|
||||
};
|
||||
|
@ -1111,7 +1025,6 @@ impl RoomInfo {
|
|||
content.apply_replacement(new_msgtype);
|
||||
},
|
||||
MessageEvent::Redacted(_) |
|
||||
MessageEvent::State(_) |
|
||||
MessageEvent::EncryptedOriginal(_) |
|
||||
MessageEvent::EncryptedRedacted(_) => {
|
||||
return;
|
||||
|
@ -1121,32 +1034,16 @@ impl RoomInfo {
|
|||
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.
|
||||
pub fn unreads(&self, settings: &ApplicationSettings) -> UnreadInfo {
|
||||
let last_message = self.messages.last_key_value();
|
||||
let last_receipt = self
|
||||
.user_receipts
|
||||
.get(&ReceiptThread::Main)
|
||||
.and_then(|receipts| receipts.get(&settings.profile.user_id));
|
||||
let last_receipt = self.get_receipt(&settings.profile.user_id);
|
||||
|
||||
match (last_message, last_receipt) {
|
||||
(Some(((ts, recent), _)), Some(last_read)) => {
|
||||
UnreadInfo { unread: last_read != recent, 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(),
|
||||
}
|
||||
}
|
||||
|
@ -1174,10 +1071,7 @@ impl RoomInfo {
|
|||
let event_id = msg.event_id().to_owned();
|
||||
let key = (msg.origin_server_ts().into(), event_id.clone());
|
||||
|
||||
let replies = self
|
||||
.threads
|
||||
.entry(thread_root.clone())
|
||||
.or_insert_with(|| Messages::thread(thread_root.clone()));
|
||||
let replies = self.threads.entry(thread_root.clone()).or_default();
|
||||
let loc = EventLocation::Message(Some(thread_root), key.clone());
|
||||
self.keys.insert(event_id, loc);
|
||||
replies.insert_message(key, msg);
|
||||
|
@ -1237,73 +1131,40 @@ impl RoomInfo {
|
|||
|
||||
/// Indicates whether we've recently fetched scrollback for this room.
|
||||
pub fn recently_fetched(&self) -> bool {
|
||||
self.fetch_last.is_some_and(|i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
||||
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
||||
}
|
||||
|
||||
fn clear_receipt(&mut self, thread: &ReceiptThread, user_id: &OwnedUserId) -> Option<()> {
|
||||
let 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)?;
|
||||
fn clear_receipt(&mut self, user_id: &OwnedUserId) -> Option<()> {
|
||||
let old_event_id = self.user_receipts.get(user_id)?;
|
||||
let old_receipts = self.event_receipts.get_mut(old_event_id)?;
|
||||
old_receipts.remove(user_id);
|
||||
|
||||
if old_receipts.is_empty() {
|
||||
old_thread.remove(old_event_id);
|
||||
}
|
||||
if old_thread.is_empty() {
|
||||
self.event_receipts.remove(thread);
|
||||
self.event_receipts.remove(old_event_id);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_receipt(
|
||||
&mut self,
|
||||
thread: ReceiptThread,
|
||||
user_id: OwnedUserId,
|
||||
event_id: OwnedEventId,
|
||||
) {
|
||||
self.clear_receipt(&thread, &user_id);
|
||||
pub fn set_receipt(&mut self, user_id: OwnedUserId, event_id: OwnedEventId) {
|
||||
self.clear_receipt(&user_id);
|
||||
self.event_receipts
|
||||
.entry(thread.clone())
|
||||
.or_default()
|
||||
.entry(event_id.clone())
|
||||
.or_default()
|
||||
.insert(user_id.clone());
|
||||
self.user_receipts.entry(thread).or_default().insert(user_id, event_id);
|
||||
self.user_receipts.insert(user_id, event_id);
|
||||
}
|
||||
|
||||
pub fn fully_read(&mut self, user_id: &UserId) {
|
||||
pub fn fully_read(&mut self, user_id: OwnedUserId) {
|
||||
let Some(((_, event_id), _)) = self.messages.last_key_value() else {
|
||||
return;
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
self.set_receipt(user_id, event_id.clone());
|
||||
}
|
||||
|
||||
pub fn receipts<'a>(
|
||||
&'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)))
|
||||
pub fn get_receipt(&self, user_id: &UserId) -> Option<&OwnedEventId> {
|
||||
self.user_receipts.get(user_id)
|
||||
}
|
||||
|
||||
fn get_typers(&self) -> &[OwnedUserId] {
|
||||
|
@ -1362,9 +1223,7 @@ impl RoomInfo {
|
|||
}
|
||||
|
||||
if !settings.tunables.typing_notice_display {
|
||||
// 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);
|
||||
return area;
|
||||
}
|
||||
|
||||
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
||||
|
@ -1409,7 +1268,7 @@ fn emoji_map() -> CompletionMap<String, &'static Emoji> {
|
|||
|
||||
#[cfg(unix)]
|
||||
fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
|
||||
let mut picker = match Picker::from_query_stdio() {
|
||||
let mut picker = match Picker::from_termios() {
|
||||
Ok(picker) => picker,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to setup image previews: {e}");
|
||||
|
@ -1418,7 +1277,9 @@ fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
|
|||
};
|
||||
|
||||
if let Some(protocol_type) = protocol_type {
|
||||
picker.set_protocol_type(protocol_type);
|
||||
picker.protocol_type = protocol_type;
|
||||
} else {
|
||||
picker.guess_protocol();
|
||||
}
|
||||
|
||||
Some(picker)
|
||||
|
@ -1441,8 +1302,8 @@ fn picker_from_settings(settings: &ApplicationSettings) -> Option<Picker> {
|
|||
}) = image_preview_protocol
|
||||
{
|
||||
// User forced type and font_size: use that.
|
||||
let mut picker = Picker::from_fontsize(font_size);
|
||||
picker.set_protocol_type(protocol_type);
|
||||
let mut picker = Picker::new(font_size);
|
||||
picker.protocol_type = protocol_type;
|
||||
Some(picker)
|
||||
} else {
|
||||
// Guess, but use type if forced.
|
||||
|
@ -1556,12 +1417,6 @@ pub struct ChatStore {
|
|||
|
||||
/// Whether the application is currently focused
|
||||
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 {
|
||||
|
@ -1576,7 +1431,6 @@ impl ChatStore {
|
|||
cmds: crate::commands::setup_commands(),
|
||||
emojis: emoji_map(),
|
||||
|
||||
collator: Default::default(),
|
||||
names: Default::default(),
|
||||
rooms: Default::default(),
|
||||
presences: Default::default(),
|
||||
|
@ -1586,7 +1440,6 @@ impl ChatStore {
|
|||
draw_curr: None,
|
||||
ring_bell: false,
|
||||
focused: true,
|
||||
open_notifications: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1707,7 +1560,7 @@ impl<'de> Deserialize<'de> for IambId {
|
|||
/// [serde] visitor for deserializing [IambId].
|
||||
struct IambIdVisitor;
|
||||
|
||||
impl Visitor<'_> for IambIdVisitor {
|
||||
impl<'de> Visitor<'de> for IambIdVisitor {
|
||||
type Value = IambId;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -1844,13 +1697,6 @@ impl RoomFocus {
|
|||
pub fn is_msgbar(&self) -> bool {
|
||||
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.
|
||||
|
@ -1919,20 +1765,11 @@ impl ApplicationInfo for IambInfo {
|
|||
type WindowId = IambId;
|
||||
type ContentId = IambBufferId;
|
||||
|
||||
fn content_of_command(ct: CommandType) -> IambBufferId {
|
||||
IambBufferId::Command(ct)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IambCompleter;
|
||||
|
||||
impl Completer<IambInfo> for IambCompleter {
|
||||
fn complete(
|
||||
&mut self,
|
||||
text: &EditRope,
|
||||
cursor: &mut Cursor,
|
||||
content: &IambBufferId,
|
||||
store: &mut ChatStore,
|
||||
store: &mut ProgramStore,
|
||||
) -> Vec<String> {
|
||||
match content {
|
||||
IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store),
|
||||
|
@ -1950,16 +1787,21 @@ impl Completer<IambInfo> for IambCompleter {
|
|||
IambBufferId::UnreadList => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn content_of_command(ct: CommandType) -> IambBufferId {
|
||||
IambBufferId::Command(ct)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tab completion for user IDs.
|
||||
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
||||
let id = text
|
||||
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
||||
.unwrap_or_else(EditRope::empty);
|
||||
let id = Cow::from(&id);
|
||||
|
||||
store
|
||||
.application
|
||||
.presences
|
||||
.complete(id.as_ref())
|
||||
.into_iter()
|
||||
|
@ -1968,7 +1810,7 @@ fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Ve
|
|||
}
|
||||
|
||||
/// Tab completion within the message bar.
|
||||
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
||||
let id = text
|
||||
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
||||
.unwrap_or_else(EditRope::empty);
|
||||
|
@ -1977,12 +1819,13 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> V
|
|||
match id.chars().next() {
|
||||
// Complete room aliases.
|
||||
Some('#') => {
|
||||
return store.names.complete(id.as_ref());
|
||||
return store.application.names.complete(id.as_ref());
|
||||
},
|
||||
|
||||
// Complete room identifiers.
|
||||
Some('!') => {
|
||||
return store
|
||||
.application
|
||||
.rooms
|
||||
.complete(id.as_ref())
|
||||
.into_iter()
|
||||
|
@ -1992,7 +1835,7 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> V
|
|||
|
||||
// Complete Emoji shortcodes.
|
||||
Some(':') => {
|
||||
let list = store.emojis.complete(&id[1..]);
|
||||
let list = store.application.emojis.complete(&id[1..]);
|
||||
let iter = list.into_iter().take(200).map(|s| format!(":{}:", s));
|
||||
|
||||
return iter.collect();
|
||||
|
@ -2001,6 +1844,7 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> V
|
|||
// Complete usernames for @ and empty strings.
|
||||
Some('@') | None => {
|
||||
return store
|
||||
.application
|
||||
.presences
|
||||
.complete(id.as_ref())
|
||||
.into_iter()
|
||||
|
@ -2014,23 +1858,28 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> V
|
|||
}
|
||||
|
||||
/// Tab completion for Matrix identifiers (usernames, room aliases, etc.)
|
||||
fn complete_matrix_names(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||
fn complete_matrix_names(
|
||||
text: &EditRope,
|
||||
cursor: &mut Cursor,
|
||||
store: &ProgramStore,
|
||||
) -> Vec<String> {
|
||||
let id = text
|
||||
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
||||
.unwrap_or_else(EditRope::empty);
|
||||
let id = Cow::from(&id);
|
||||
|
||||
let list = store.names.complete(id.as_ref());
|
||||
let list = store.application.names.complete(id.as_ref());
|
||||
if !list.is_empty() {
|
||||
return list;
|
||||
}
|
||||
|
||||
let list = store.presences.complete(id.as_ref());
|
||||
let list = store.application.presences.complete(id.as_ref());
|
||||
if !list.is_empty() {
|
||||
return list.into_iter().map(|i| i.to_string()).collect();
|
||||
}
|
||||
|
||||
store
|
||||
.application
|
||||
.rooms
|
||||
.complete(id.as_ref())
|
||||
.into_iter()
|
||||
|
@ -2039,12 +1888,12 @@ fn complete_matrix_names(text: &EditRope, cursor: &mut Cursor, store: &ChatStore
|
|||
}
|
||||
|
||||
/// Tab completion for Emoji shortcode names.
|
||||
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
||||
let sc = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
||||
let sc = sc.unwrap_or_else(EditRope::empty);
|
||||
let sc = Cow::from(&sc);
|
||||
|
||||
store.emojis.complete(sc.as_ref())
|
||||
store.application.emojis.complete(sc.as_ref())
|
||||
}
|
||||
|
||||
/// Tab completion for command names.
|
||||
|
@ -2052,11 +1901,11 @@ fn complete_cmdname(
|
|||
desc: CommandDescription,
|
||||
text: &EditRope,
|
||||
cursor: &mut Cursor,
|
||||
store: &ChatStore,
|
||||
store: &ProgramStore,
|
||||
) -> Vec<String> {
|
||||
// Complete command name and set cursor position.
|
||||
let _ = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
||||
store.cmds.complete_name(desc.command.as_str())
|
||||
store.application.cmds.complete_name(desc.command.as_str())
|
||||
}
|
||||
|
||||
/// Tab completion for command arguments.
|
||||
|
@ -2064,9 +1913,9 @@ fn complete_cmdarg(
|
|||
desc: CommandDescription,
|
||||
text: &EditRope,
|
||||
cursor: &mut Cursor,
|
||||
store: &ChatStore,
|
||||
store: &ProgramStore,
|
||||
) -> Vec<String> {
|
||||
let cmd = match store.cmds.get(desc.command.as_str()) {
|
||||
let cmd = match store.application.cmds.get(desc.command.as_str()) {
|
||||
Ok(cmd) => cmd,
|
||||
Err(_) => return vec![],
|
||||
};
|
||||
|
@ -2089,7 +1938,12 @@ fn complete_cmdarg(
|
|||
}
|
||||
|
||||
/// Tab completion for commands.
|
||||
fn complete_cmd(cmd: &str, text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||
fn complete_cmd(
|
||||
cmd: &str,
|
||||
text: &EditRope,
|
||||
cursor: &mut Cursor,
|
||||
store: &ProgramStore,
|
||||
) -> Vec<String> {
|
||||
match CommandDescription::from_str(cmd) {
|
||||
Ok(desc) => {
|
||||
if desc.arg.untrimmed.is_empty() {
|
||||
|
@ -2106,7 +1960,7 @@ fn complete_cmd(cmd: &str, text: &EditRope, cursor: &mut Cursor, store: &ChatSto
|
|||
}
|
||||
|
||||
/// Tab completion for the command bar.
|
||||
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
|
||||
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
||||
let eo = text.cursor_to_offset(cursor);
|
||||
let slice = text.slice(..eo);
|
||||
let cow = Cow::from(&slice);
|
||||
|
@ -2286,7 +2140,6 @@ pub mod tests {
|
|||
#[tokio::test]
|
||||
async fn test_complete_msgbar() {
|
||||
let store = mock_store().await;
|
||||
let store = store.application;
|
||||
|
||||
let text = EditRope::from("going for a walk :walk ");
|
||||
let mut cursor = Cursor::new(0, 22);
|
||||
|
@ -2310,7 +2163,6 @@ pub mod tests {
|
|||
#[tokio::test]
|
||||
async fn test_complete_cmdbar() {
|
||||
let store = mock_store().await;
|
||||
let store = store.application;
|
||||
let users = vec![
|
||||
"@user1:example.com",
|
||||
"@user2:example.com",
|
||||
|
|
218
src/commands.rs
218
src/commands.rs
|
@ -2,9 +2,9 @@
|
|||
//!
|
||||
//! The command-bar commands are set up here, and iamb-specific commands are defined here. See
|
||||
//! [modalkit::env::vim::command] for additional Vim commands we pull in.
|
||||
use std::{convert::TryFrom, str::FromStr as _};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use matrix_sdk::ruma::{events::tag::TagName, OwnedRoomId, OwnedUserId};
|
||||
use matrix_sdk::ruma::{events::tag::TagName, OwnedUserId};
|
||||
|
||||
use modalkit::{
|
||||
commands::{CommandError, CommandResult, CommandStep},
|
||||
|
@ -27,7 +27,6 @@ use crate::base::{
|
|||
RoomAction,
|
||||
RoomField,
|
||||
SendAction,
|
||||
SpaceAction,
|
||||
VerifyAction,
|
||||
};
|
||||
|
||||
|
@ -476,18 +475,10 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||
("topic", "unset", None) => RoomAction::Unset(RoomField::Topic).into(),
|
||||
("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>
|
||||
("tag", "set", Some(s)) => RoomAction::Set(RoomField::Tag(tag_name(s)?), "".into()).into(),
|
||||
("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>
|
||||
("notify", "set", Some(s)) => RoomAction::Set(RoomField::NotificationMode, s).into(),
|
||||
("notify", "set", None) => return Result::Err(CommandError::InvalidArgument),
|
||||
|
@ -500,6 +491,10 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||
("notify", "show", None) => RoomAction::Show(RoomField::NotificationMode).into(),
|
||||
("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
|
||||
("alias", "show", None) => RoomAction::Show(RoomField::Aliases).into(),
|
||||
("alias", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument),
|
||||
|
@ -536,91 +531,6 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
|||
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),
|
||||
};
|
||||
|
||||
|
@ -757,11 +667,6 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
|||
f: iamb_rooms,
|
||||
});
|
||||
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 {
|
||||
name: "spaces".into(),
|
||||
aliases: vec![],
|
||||
|
@ -816,7 +721,7 @@ pub fn setup_commands() -> ProgramCommands {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use matrix_sdk::ruma::{room_id, user_id};
|
||||
use matrix_sdk::ruma::user_id;
|
||||
use modalkit::actions::WindowAction;
|
||||
use modalkit::editing::context::EditContext;
|
||||
|
||||
|
@ -1142,119 +1047,22 @@ mod tests {
|
|||
let mut cmds = setup_commands();
|
||||
let ctx = EditContext::default();
|
||||
|
||||
let cmd = "room notify set mute";
|
||||
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||
let cmd = format!("room notify set mute");
|
||||
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
|
||||
let act = RoomAction::Set(RoomField::NotificationMode, "mute".into());
|
||||
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||
|
||||
let cmd = "room notify unset";
|
||||
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||
let cmd = format!("room notify unset");
|
||||
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
|
||||
let act = RoomAction::Unset(RoomField::NotificationMode);
|
||||
assert_eq!(res, vec![(act.into(), ctx.clone())]);
|
||||
|
||||
let cmd = "room notify show";
|
||||
let res = cmds.input_cmd(cmd, ctx.clone()).unwrap();
|
||||
let cmd = format!("room notify show");
|
||||
let res = cmds.input_cmd(&cmd, ctx.clone()).unwrap();
|
||||
let act = RoomAction::Show(RoomField::NotificationMode);
|
||||
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]
|
||||
fn test_cmd_invite() {
|
||||
let mut cmds = setup_commands();
|
||||
|
|
258
src/config.rs
258
src/config.rs
|
@ -1,17 +1,16 @@
|
|||
//! # Logic for loading and validating application configuration
|
||||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::env;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{BufReader, BufWriter, Write};
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
use matrix_sdk::authentication::matrix::MatrixSession;
|
||||
use matrix_sdk::matrix_auth::MatrixSession;
|
||||
use matrix_sdk::ruma::{OwnedDeviceId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId};
|
||||
use ratatui::style::{Color, Modifier as StyleModifier, Style};
|
||||
use ratatui::text::Span;
|
||||
|
@ -46,9 +45,8 @@ const DEFAULT_MEMBERS_SORT: [SortColumn<SortFieldUser>; 2] = [
|
|||
SortColumn(SortFieldUser::UserId, SortOrder::Ascending),
|
||||
];
|
||||
|
||||
const DEFAULT_ROOM_SORT: [SortColumn<SortFieldRoom>; 5] = [
|
||||
const DEFAULT_ROOM_SORT: [SortColumn<SortFieldRoom>; 4] = [
|
||||
SortColumn(SortFieldRoom::Favorite, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::Invite, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::LowPriority, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::Unread, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||
|
@ -99,14 +97,14 @@ fn validate_profile_name(name: &str) -> bool {
|
|||
|
||||
let mut chars = name.chars();
|
||||
|
||||
if !chars.next().is_some_and(|c| c.is_ascii_alphanumeric()) {
|
||||
if !chars.next().map_or(false, |c| c.is_ascii_alphanumeric()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
name.chars().all(is_profile_char)
|
||||
}
|
||||
|
||||
fn validate_profile_names(names: &BTreeMap<String, ProfileConfig>) {
|
||||
fn validate_profile_names(names: &HashMap<String, ProfileConfig>) {
|
||||
for name in names.keys() {
|
||||
if validate_profile_name(name.as_str()) {
|
||||
continue;
|
||||
|
@ -153,7 +151,7 @@ pub enum ConfigError {
|
|||
pub struct Keys(pub Vec<TerminalKey>, pub String);
|
||||
pub struct KeysVisitor;
|
||||
|
||||
impl Visitor<'_> for KeysVisitor {
|
||||
impl<'de> Visitor<'de> for KeysVisitor {
|
||||
type Value = Keys;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -184,7 +182,7 @@ impl<'de> Deserialize<'de> for Keys {
|
|||
pub struct VimModes(pub Vec<VimMode>);
|
||||
pub struct VimModesVisitor;
|
||||
|
||||
impl Visitor<'_> for VimModesVisitor {
|
||||
impl<'de> Visitor<'de> for VimModesVisitor {
|
||||
type Value = VimModes;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -234,7 +232,7 @@ impl From<LogLevel> for Level {
|
|||
}
|
||||
}
|
||||
|
||||
impl Visitor<'_> for LogLevelVisitor {
|
||||
impl<'de> Visitor<'de> for LogLevelVisitor {
|
||||
type Value = LogLevel;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -269,7 +267,7 @@ impl<'de> Deserialize<'de> for LogLevel {
|
|||
pub struct UserColor(pub Color);
|
||||
pub struct UserColorVisitor;
|
||||
|
||||
impl Visitor<'_> for UserColorVisitor {
|
||||
impl<'de> Visitor<'de> for UserColorVisitor {
|
||||
type Value = UserColor;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
@ -323,7 +321,7 @@ pub struct Session {
|
|||
impl From<Session> for MatrixSession {
|
||||
fn from(session: Session) -> Self {
|
||||
MatrixSession {
|
||||
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
|
||||
access_token: session.access_token,
|
||||
refresh_token: session.refresh_token,
|
||||
},
|
||||
|
@ -354,31 +352,29 @@ pub struct UserDisplayTunables {
|
|||
|
||||
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
|
||||
|
||||
fn merge_sorts(profile: SortOverrides, global: SortOverrides) -> SortOverrides {
|
||||
fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides {
|
||||
SortOverrides {
|
||||
chats: profile.chats.or(global.chats),
|
||||
dms: profile.dms.or(global.dms),
|
||||
rooms: profile.rooms.or(global.rooms),
|
||||
spaces: profile.spaces.or(global.spaces),
|
||||
members: profile.members.or(global.members),
|
||||
chats: b.chats.or(a.chats),
|
||||
dms: b.dms.or(a.dms),
|
||||
rooms: b.rooms.or(a.rooms),
|
||||
spaces: b.spaces.or(a.spaces),
|
||||
members: b.members.or(a.members),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_maps<K, V>(
|
||||
profile: Option<HashMap<K, V>>,
|
||||
global: Option<HashMap<K, V>>,
|
||||
) -> Option<HashMap<K, V>>
|
||||
fn merge_maps<K, V>(a: Option<HashMap<K, V>>, b: Option<HashMap<K, V>>) -> Option<HashMap<K, V>>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
{
|
||||
match (global, profile) {
|
||||
(Some(m), None) | (None, Some(m)) => Some(m),
|
||||
(Some(mut global), Some(profile)) => {
|
||||
for (k, v) in profile {
|
||||
global.insert(k, v);
|
||||
match (a, b) {
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(Some(mut a), Some(b)) => {
|
||||
for (k, v) in b {
|
||||
a.insert(k, v);
|
||||
}
|
||||
|
||||
Some(global)
|
||||
Some(a)
|
||||
},
|
||||
(None, None) => None,
|
||||
}
|
||||
|
@ -400,84 +396,28 @@ pub enum UserDisplayStyle {
|
|||
// it can wind up being the Matrix username if there are display name collisions in the room,
|
||||
// in order to avoid any confusion.
|
||||
DisplayName,
|
||||
|
||||
// Acts like Username, except when the username matches given regex, then acts like DisplayName
|
||||
Regex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct NotifyVia {
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NotifyVia {
|
||||
/// Deliver notifications via terminal bell.
|
||||
pub bell: bool,
|
||||
Bell,
|
||||
/// Deliver notifications via desktop mechanism.
|
||||
#[cfg(feature = "desktop")]
|
||||
pub desktop: bool,
|
||||
Desktop,
|
||||
}
|
||||
pub struct NotifyViaVisitor;
|
||||
|
||||
impl Default for NotifyVia {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bell: cfg!(not(feature = "desktop")),
|
||||
#[cfg(feature = "desktop")]
|
||||
desktop: true,
|
||||
}
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
return NotifyVia::Bell;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
return NotifyVia::Desktop;
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor<'_> for NotifyViaVisitor {
|
||||
type Value = NotifyVia;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a valid notify destination (e.g. \"bell\" or \"desktop\")")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
let mut via = NotifyVia {
|
||||
bell: false,
|
||||
#[cfg(feature = "desktop")]
|
||||
desktop: false,
|
||||
};
|
||||
|
||||
for value in value.split('|') {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"bell" => {
|
||||
via.bell = true;
|
||||
},
|
||||
#[cfg(feature = "desktop")]
|
||||
"desktop" => {
|
||||
via.desktop = true;
|
||||
},
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
"desktop" => {
|
||||
return Err(E::custom("desktop notification support was compiled out"))
|
||||
},
|
||||
_ => return Err(E::custom("could not parse into a notify destination")),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(via)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NotifyVia {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(NotifyViaVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
pub struct Mouse {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
pub struct Notifications {
|
||||
#[serde(default)]
|
||||
|
@ -561,35 +501,29 @@ impl SortOverrides {
|
|||
pub struct TunableValues {
|
||||
pub log_level: Level,
|
||||
pub message_shortcode_display: bool,
|
||||
pub normal_after_send: bool,
|
||||
pub reaction_display: bool,
|
||||
pub reaction_shortcode_display: bool,
|
||||
pub read_receipt_send: bool,
|
||||
pub read_receipt_display: bool,
|
||||
pub request_timeout: u64,
|
||||
pub sort: SortValues,
|
||||
pub state_event_display: bool,
|
||||
pub typing_notice_send: bool,
|
||||
pub typing_notice_display: bool,
|
||||
pub users: UserOverrides,
|
||||
pub username_display: UserDisplayStyle,
|
||||
pub username_display_regex: Option<String>,
|
||||
pub message_user_color: bool,
|
||||
pub default_room: Option<String>,
|
||||
pub open_command: Option<Vec<String>>,
|
||||
pub mouse: Mouse,
|
||||
pub notifications: Notifications,
|
||||
pub image_preview: Option<ImagePreviewValues>,
|
||||
pub user_gutter_width: usize,
|
||||
pub external_edit_file_suffix: String,
|
||||
pub tabstop: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Tunables {
|
||||
pub log_level: Option<LogLevel>,
|
||||
pub message_shortcode_display: Option<bool>,
|
||||
pub normal_after_send: Option<bool>,
|
||||
pub reaction_display: Option<bool>,
|
||||
pub reaction_shortcode_display: Option<bool>,
|
||||
pub read_receipt_send: Option<bool>,
|
||||
|
@ -597,21 +531,17 @@ pub struct Tunables {
|
|||
pub request_timeout: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub sort: SortOverrides,
|
||||
pub state_event_display: Option<bool>,
|
||||
pub typing_notice_send: Option<bool>,
|
||||
pub typing_notice_display: Option<bool>,
|
||||
pub users: Option<UserOverrides>,
|
||||
pub username_display: Option<UserDisplayStyle>,
|
||||
pub username_display_regex: Option<String>,
|
||||
pub message_user_color: Option<bool>,
|
||||
pub default_room: Option<String>,
|
||||
pub open_command: Option<Vec<String>>,
|
||||
pub mouse: Option<Mouse>,
|
||||
pub notifications: Option<Notifications>,
|
||||
pub image_preview: Option<ImagePreview>,
|
||||
pub user_gutter_width: Option<usize>,
|
||||
pub external_edit_file_suffix: Option<String>,
|
||||
pub tabstop: Option<usize>,
|
||||
}
|
||||
|
||||
impl Tunables {
|
||||
|
@ -621,7 +551,6 @@ impl Tunables {
|
|||
message_shortcode_display: self
|
||||
.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_shortcode_display: self
|
||||
.reaction_shortcode_display
|
||||
|
@ -630,23 +559,19 @@ impl Tunables {
|
|||
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
||||
request_timeout: self.request_timeout.or(other.request_timeout),
|
||||
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_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||
users: merge_maps(self.users, other.users),
|
||||
username_display: self.username_display.or(other.username_display),
|
||||
username_display_regex: self.username_display_regex.or(other.username_display_regex),
|
||||
message_user_color: self.message_user_color.or(other.message_user_color),
|
||||
default_room: self.default_room.or(other.default_room),
|
||||
open_command: self.open_command.or(other.open_command),
|
||||
mouse: self.mouse.or(other.mouse),
|
||||
notifications: self.notifications.or(other.notifications),
|
||||
image_preview: self.image_preview.or(other.image_preview),
|
||||
user_gutter_width: self.user_gutter_width.or(other.user_gutter_width),
|
||||
external_edit_file_suffix: self
|
||||
.external_edit_file_suffix
|
||||
.or(other.external_edit_file_suffix),
|
||||
tabstop: self.tabstop.or(other.tabstop),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -654,30 +579,25 @@ impl Tunables {
|
|||
TunableValues {
|
||||
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
||||
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_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
||||
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
||||
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
||||
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT),
|
||||
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_display: self.typing_notice_display.unwrap_or(true),
|
||||
users: self.users.unwrap_or_default(),
|
||||
username_display: self.username_display.unwrap_or_default(),
|
||||
username_display_regex: self.username_display_regex,
|
||||
message_user_color: self.message_user_color.unwrap_or(false),
|
||||
default_room: self.default_room,
|
||||
open_command: self.open_command,
|
||||
mouse: self.mouse.unwrap_or_default(),
|
||||
notifications: self.notifications.unwrap_or_default(),
|
||||
image_preview: self.image_preview.map(ImagePreview::values),
|
||||
user_gutter_width: self.user_gutter_width.unwrap_or(30),
|
||||
external_edit_file_suffix: self
|
||||
.external_edit_file_suffix
|
||||
.unwrap_or_else(|| ".md".to_string()),
|
||||
tabstop: self.tabstop.unwrap_or(4),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -809,7 +729,7 @@ pub struct ProfileConfig {
|
|||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct IambConfig {
|
||||
pub profiles: BTreeMap<String, ProfileConfig>,
|
||||
pub profiles: HashMap<String, ProfileConfig>,
|
||||
pub default_profile: Option<String>,
|
||||
pub settings: Option<Tunables>,
|
||||
pub dirs: Option<Directories>,
|
||||
|
@ -849,22 +769,14 @@ pub struct 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>> {
|
||||
let mut config_dir = cli
|
||||
.config_directory
|
||||
.or_else(Self::get_xdg_config_home)
|
||||
.or_else(dirs::config_dir)
|
||||
.unwrap_or_else(|| {
|
||||
usage!(
|
||||
"No user configuration directory found;\
|
||||
please specify one via -C.\n\n
|
||||
For more information try '--help'"
|
||||
);
|
||||
});
|
||||
let mut config_dir = cli.config_directory.or_else(dirs::config_dir).unwrap_or_else(|| {
|
||||
usage!(
|
||||
"No user configuration directory found;\
|
||||
please specify one via -C.\n\n
|
||||
For more information try '--help'"
|
||||
);
|
||||
});
|
||||
|
||||
config_dir.push("iamb");
|
||||
let config_json = config_dir.join("config.json");
|
||||
|
@ -904,36 +816,14 @@ impl ApplicationSettings {
|
|||
} else if profiles.len() == 1 {
|
||||
profiles.into_iter().next().unwrap()
|
||||
} 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!(
|
||||
"No profile specified. \
|
||||
Please use -P or add \"default_profile\" to your configuration.\n\n\
|
||||
For more information try '--help'",
|
||||
);
|
||||
}
|
||||
if let Ok(i) = input.trim().parse::<usize>() {
|
||||
if i < profiles.len() {
|
||||
break profiles.into_iter().nth(i).unwrap();
|
||||
}
|
||||
}
|
||||
println!("\nInvalid index.");
|
||||
}
|
||||
usage!(
|
||||
"No profile specified. \
|
||||
Please use -P or add \"default_profile\" to your configuration.\n\n\
|
||||
For more information try '--help'",
|
||||
);
|
||||
};
|
||||
|
||||
let macros = merge_maps(profile.macros.take(), macros).unwrap_or_default();
|
||||
let macros = merge_maps(macros, profile.macros.take()).unwrap_or_default();
|
||||
let layout = profile.layout.take().or(layout).unwrap_or_default();
|
||||
|
||||
let tunables = global.unwrap_or_default();
|
||||
|
@ -1008,7 +898,7 @@ impl ApplicationSettings {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_user_char_span(&self, user_id: &UserId) -> Span {
|
||||
pub fn get_user_char_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
|
||||
let (color, c) = self
|
||||
.tunables
|
||||
.users
|
||||
|
@ -1068,20 +958,6 @@ impl ApplicationSettings {
|
|||
Cow::Borrowed(user_id.as_str())
|
||||
}
|
||||
},
|
||||
(None, UserDisplayStyle::Regex) => {
|
||||
let re = regex::Regex::new(
|
||||
&self.tunables.username_display_regex.clone().unwrap_or("*".into()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !re.is_match(user_id.as_str()) {
|
||||
Cow::Borrowed(user_id.as_str())
|
||||
} else if let Some(display) = info.display_names.get(user_id) {
|
||||
Cow::Borrowed(display.as_str())
|
||||
} else {
|
||||
Cow::Borrowed(user_id.as_str())
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Span::styled(name, style)
|
||||
|
@ -1146,10 +1022,10 @@ mod tests {
|
|||
assert_eq!(res, Some(b.clone()));
|
||||
|
||||
let res = merge_maps(Some(b.clone()), Some(c.clone()));
|
||||
assert_eq!(res, Some(b.clone()));
|
||||
assert_eq!(res, Some(c.clone()));
|
||||
|
||||
let res = merge_maps(Some(c.clone()), Some(b.clone()));
|
||||
assert_eq!(res, Some(c.clone()));
|
||||
assert_eq!(res, Some(b.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1198,13 +1074,6 @@ mod tests {
|
|||
let res: Tunables =
|
||||
serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap();
|
||||
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
|
||||
|
||||
let res: Tunables = serde_json::from_str(
|
||||
"{\"username_display\": \"regex\",\n\"username_display_regex\": \"foo\"}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res.username_display, Some(UserDisplayStyle::Regex));
|
||||
assert_eq!(res.username_display_regex.unwrap_or("FAILED".into()), "foo".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1320,29 +1189,6 @@ mod tests {
|
|||
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]
|
||||
fn test_load_example_config_toml() {
|
||||
let path = PathBuf::from("config.example.toml");
|
||||
|
|
79
src/main.rs
79
src/main.rs
|
@ -44,14 +44,11 @@ use modalkit::crossterm::{
|
|||
read,
|
||||
DisableBracketedPaste,
|
||||
DisableFocusChange,
|
||||
DisableMouseCapture,
|
||||
EnableBracketedPaste,
|
||||
EnableFocusChange,
|
||||
EnableMouseCapture,
|
||||
Event,
|
||||
KeyEventKind,
|
||||
KeyboardEnhancementFlags,
|
||||
MouseEventKind,
|
||||
PopKeyboardEnhancementFlags,
|
||||
PushKeyboardEnhancementFlags,
|
||||
},
|
||||
|
@ -62,7 +59,7 @@ use modalkit::crossterm::{
|
|||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
|
@ -89,7 +86,6 @@ use crate::{
|
|||
ChatStore,
|
||||
HomeserverAction,
|
||||
IambAction,
|
||||
IambCompleter,
|
||||
IambError,
|
||||
IambId,
|
||||
IambInfo,
|
||||
|
@ -314,7 +310,7 @@ impl Application {
|
|||
}
|
||||
|
||||
term.draw(|f| {
|
||||
let area = f.area();
|
||||
let area = f.size();
|
||||
|
||||
let modestr = bindings.show_mode();
|
||||
let cursor = bindings.get_cursor_indicator();
|
||||
|
@ -328,9 +324,6 @@ impl Application {
|
|||
.show_dialog(dialogstr)
|
||||
.show_mode(modestr)
|
||||
.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);
|
||||
f.render_stateful_widget(screen, area, sstate);
|
||||
|
||||
|
@ -346,7 +339,7 @@ impl Application {
|
|||
let inner = Rect::new(cx, cy, 1, 1);
|
||||
f.render_widget(para, inner)
|
||||
}
|
||||
f.set_cursor_position((cx, cy));
|
||||
f.set_cursor(cx, cy);
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -371,30 +364,8 @@ impl Application {
|
|||
|
||||
return Ok(ke.into());
|
||||
},
|
||||
Event::Mouse(me) => {
|
||||
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::Mouse(_) => {
|
||||
// Do nothing for now.
|
||||
},
|
||||
Event::FocusGained => {
|
||||
let mut store = self.store.lock().await;
|
||||
|
@ -533,7 +504,7 @@ impl Application {
|
|||
},
|
||||
|
||||
// Unimplemented.
|
||||
Action::KeywordLookup(_) => {
|
||||
Action::KeywordLookup => {
|
||||
// XXX: implement
|
||||
None
|
||||
},
|
||||
|
@ -561,12 +532,9 @@ impl Application {
|
|||
IambAction::ClearUnreads => {
|
||||
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() {
|
||||
if let Some(room) = store.application.rooms.get_mut(room_id) {
|
||||
room.fully_read(user_id);
|
||||
room.fully_read(user_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -589,9 +557,6 @@ impl Application {
|
|||
IambAction::Message(act) => {
|
||||
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) => {
|
||||
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store).await?;
|
||||
self.action_prepend(acts);
|
||||
|
@ -599,9 +564,6 @@ impl Application {
|
|||
None
|
||||
},
|
||||
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?
|
||||
},
|
||||
|
||||
|
@ -885,7 +847,7 @@ async fn check_import_keys(
|
|||
let encrypted = match encrypt_room_key_export(&keys, &passphrase, 500000) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
println!("* Failed to encrypt room keys during export: {e}");
|
||||
format!("* Failed to encrypt room keys during export: {e}");
|
||||
process::exit(2);
|
||||
},
|
||||
};
|
||||
|
@ -967,8 +929,8 @@ async fn login_normal(
|
|||
}
|
||||
|
||||
/// Set up the terminal for drawing the TUI, and getting additional info.
|
||||
fn setup_tty(settings: &ApplicationSettings, enable_enhanced_keys: bool) -> std::io::Result<()> {
|
||||
let title = format!("iamb ({})", settings.profile.user_id.as_str());
|
||||
fn setup_tty(title: &str, enable_enhanced_keys: bool) -> std::io::Result<()> {
|
||||
let title = format!("iamb ({})", title);
|
||||
|
||||
// Enable raw mode and enter the alternate screen.
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
|
@ -982,23 +944,15 @@ fn setup_tty(settings: &ApplicationSettings, enable_enhanced_keys: bool) -> std:
|
|||
)?;
|
||||
}
|
||||
|
||||
if settings.tunables.mouse.enabled {
|
||||
crossterm::execute!(stdout(), EnableMouseCapture)?;
|
||||
}
|
||||
|
||||
crossterm::execute!(stdout(), EnableBracketedPaste, EnableFocusChange, SetTitle(title))
|
||||
}
|
||||
|
||||
// Do our best to reverse what we did in setup_tty() when we exit or crash.
|
||||
fn restore_tty(enable_enhanced_keys: bool, enable_mouse: bool) {
|
||||
fn restore_tty(enable_enhanced_keys: bool) {
|
||||
if enable_enhanced_keys {
|
||||
let _ = crossterm::queue!(stdout(), PopKeyboardEnhancementFlags);
|
||||
}
|
||||
|
||||
if enable_mouse {
|
||||
let _ = crossterm::queue!(stdout(), DisableMouseCapture);
|
||||
}
|
||||
|
||||
let _ = crossterm::execute!(
|
||||
stdout(),
|
||||
DisableBracketedPaste,
|
||||
|
@ -1021,9 +975,7 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
|||
// Set up the async worker thread and global store.
|
||||
let worker = ClientWorker::spawn(client.clone(), settings.clone()).await;
|
||||
let store = ChatStore::new(worker.clone(), settings.clone());
|
||||
let mut store = Store::new(store);
|
||||
store.completer = Box::new(IambCompleter);
|
||||
|
||||
let store = Store::new(store);
|
||||
let store = Arc::new(AsyncMutex::new(store));
|
||||
worker.init(store.clone());
|
||||
|
||||
|
@ -1054,12 +1006,11 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
|||
false
|
||||
},
|
||||
};
|
||||
setup_tty(&settings, enable_enhanced_keys)?;
|
||||
setup_tty(settings.profile.user_id.as_str(), enable_enhanced_keys)?;
|
||||
|
||||
let orig_hook = std::panic::take_hook();
|
||||
let enable_mouse = settings.tunables.mouse.enabled;
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
restore_tty(enable_enhanced_keys, enable_mouse);
|
||||
restore_tty(enable_enhanced_keys);
|
||||
orig_hook(panic_info);
|
||||
process::exit(1);
|
||||
}));
|
||||
|
@ -1069,7 +1020,7 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
|||
application.run().await?;
|
||||
|
||||
// Clean up the terminal on exit.
|
||||
restore_tty(enable_enhanced_keys, enable_mouse);
|
||||
restore_tty(enable_enhanced_keys);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
//!
|
||||
//! 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.
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Deref;
|
||||
|
||||
use css_color_parser::Color as CssColor;
|
||||
use markup5ever_rcdom::{Handle, NodeData, RcDom};
|
||||
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId, OwnedUserId};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use url::Url;
|
||||
|
||||
|
@ -36,13 +34,10 @@ use ratatui::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
config::ApplicationSettings,
|
||||
message::printer::TextPrinter,
|
||||
util::{join_cell_text, space_text},
|
||||
};
|
||||
|
||||
const QUOTE_COLOR: Color = Color::Indexed(236);
|
||||
|
||||
/// Generate bullet points from a [ListStyle].
|
||||
pub struct BulletIterator {
|
||||
style: ListStyle,
|
||||
|
@ -153,12 +148,7 @@ impl Table {
|
|||
}
|
||||
}
|
||||
|
||||
fn to_text<'a>(
|
||||
&'a self,
|
||||
width: usize,
|
||||
style: Style,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Text<'a> {
|
||||
fn to_text(&self, width: usize, style: Style, emoji_shortcodes: bool) -> Text {
|
||||
let mut text = Text::default();
|
||||
let columns = self.columns();
|
||||
let cell_total = width.saturating_sub(columns).saturating_sub(1);
|
||||
|
@ -177,7 +167,7 @@ impl Table {
|
|||
if let Some(caption) = &self.caption {
|
||||
let subw = width.saturating_sub(6);
|
||||
let mut printer =
|
||||
TextPrinter::new(subw, style, true, settings).align(Alignment::Center);
|
||||
TextPrinter::new(subw, style, true, emoji_shortcodes).align(Alignment::Center);
|
||||
caption.print(&mut printer, style);
|
||||
|
||||
for mut line in printer.finish().lines {
|
||||
|
@ -224,7 +214,7 @@ impl Table {
|
|||
CellType::Data => style,
|
||||
};
|
||||
|
||||
cell.to_text(*w, style, settings)
|
||||
cell.to_text(*w, style, emoji_shortcodes)
|
||||
} else {
|
||||
space_text(*w, style)
|
||||
};
|
||||
|
@ -281,22 +271,13 @@ pub enum StyleTreeNode {
|
|||
Ruler,
|
||||
Style(Box<StyleTreeNode>, Style),
|
||||
Table(Table),
|
||||
Text(Cow<'static, str>),
|
||||
Text(String),
|
||||
Sequence(StyleTreeChildren),
|
||||
RoomAlias(OwnedRoomAliasId),
|
||||
RoomId(OwnedRoomId),
|
||||
UserId(OwnedUserId),
|
||||
DisplayName(String, OwnedUserId),
|
||||
}
|
||||
|
||||
impl StyleTreeNode {
|
||||
pub fn to_text<'a>(
|
||||
&'a self,
|
||||
width: usize,
|
||||
style: Style,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Text<'a> {
|
||||
let mut printer = TextPrinter::new(width, style, true, settings);
|
||||
pub fn to_text(&self, width: usize, style: Style, emoji_shortcodes: bool) -> Text {
|
||||
let mut printer = TextPrinter::new(width, style, true, emoji_shortcodes);
|
||||
self.print(&mut printer, style);
|
||||
printer.finish()
|
||||
}
|
||||
|
@ -331,12 +312,6 @@ impl StyleTreeNode {
|
|||
StyleTreeNode::Ruler => {},
|
||||
StyleTreeNode::Text(_) => {},
|
||||
StyleTreeNode::Break => {},
|
||||
|
||||
// TODO: eventually these should turn into internal links:
|
||||
StyleTreeNode::UserId(_) => {},
|
||||
StyleTreeNode::RoomId(_) => {},
|
||||
StyleTreeNode::RoomAlias(_) => {},
|
||||
StyleTreeNode::DisplayName(_, _) => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,14 +328,11 @@ impl StyleTreeNode {
|
|||
printer.push_span_nobreak(span);
|
||||
},
|
||||
StyleTreeNode::Blockquote(child) => {
|
||||
let mut subp = printer.sub(3);
|
||||
let mut subp = printer.sub(4);
|
||||
child.print(&mut subp, style);
|
||||
|
||||
for mut line in subp.finish() {
|
||||
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));
|
||||
line.spans.insert(0, Span::styled(" ", style));
|
||||
printer.push_line(line);
|
||||
}
|
||||
},
|
||||
|
@ -458,14 +430,14 @@ impl StyleTreeNode {
|
|||
}
|
||||
},
|
||||
StyleTreeNode::Table(table) => {
|
||||
let text = table.to_text(width, style, printer.settings);
|
||||
let text = table.to_text(width, style, printer.emoji_shortcodes());
|
||||
printer.push_text(text);
|
||||
},
|
||||
StyleTreeNode::Break => {
|
||||
printer.push_break();
|
||||
},
|
||||
StyleTreeNode::Text(s) => {
|
||||
printer.push_str(s.as_ref(), style);
|
||||
printer.push_str(s.as_str(), style);
|
||||
},
|
||||
|
||||
StyleTreeNode::Style(child, patch) => child.print(printer, style.patch(*patch)),
|
||||
|
@ -474,30 +446,13 @@ impl StyleTreeNode {
|
|||
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.
|
||||
pub struct StyleTree {
|
||||
pub(super) children: StyleTreeChildren,
|
||||
children: StyleTreeChildren,
|
||||
}
|
||||
|
||||
impl StyleTree {
|
||||
|
@ -511,14 +466,14 @@ impl StyleTree {
|
|||
return links;
|
||||
}
|
||||
|
||||
pub fn to_text<'a>(
|
||||
&'a self,
|
||||
pub fn to_text(
|
||||
&self,
|
||||
width: usize,
|
||||
style: Style,
|
||||
hide_reply: bool,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Text<'a> {
|
||||
let mut printer = TextPrinter::new(width, style, hide_reply, settings);
|
||||
emoji_shortcodes: bool,
|
||||
) -> Text<'_> {
|
||||
let mut printer = TextPrinter::new(width, style, hide_reply, emoji_shortcodes);
|
||||
|
||||
for child in self.children.iter() {
|
||||
child.print(&mut printer, style);
|
||||
|
@ -529,11 +484,11 @@ impl StyleTree {
|
|||
}
|
||||
|
||||
pub struct TreeGenState {
|
||||
pub link_num: u8,
|
||||
link_num: u8,
|
||||
}
|
||||
|
||||
impl TreeGenState {
|
||||
pub fn next_link_char(&mut self) -> Option<char> {
|
||||
fn next_link_char(&mut self) -> Option<char> {
|
||||
let num = self.link_num;
|
||||
|
||||
if num < 62 {
|
||||
|
@ -706,7 +661,7 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
|
|||
|
||||
let tree = match &node.data {
|
||||
NodeData::Document => *c2t(node.children.borrow().as_slice(), state),
|
||||
NodeData::Text { contents } => StyleTreeNode::Text(contents.borrow().to_string().into()),
|
||||
NodeData::Text { contents } => StyleTreeNode::Text(contents.borrow().to_string()),
|
||||
NodeData::Element { name, attrs, .. } => {
|
||||
match name.local.as_ref() {
|
||||
// Message that this one replies to.
|
||||
|
@ -753,7 +708,7 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
|
|||
|
||||
StyleTreeNode::Style(c, s)
|
||||
},
|
||||
"del" | "s" | "strike" => {
|
||||
"del" | "strike" => {
|
||||
let c = c2t(&node.children.borrow(), state);
|
||||
let s = Style::default().add_modifier(StyleModifier::CROSSED_OUT);
|
||||
|
||||
|
@ -856,19 +811,17 @@ pub fn parse_matrix_html(s: &str) -> StyleTree {
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::tests::mock_settings;
|
||||
use crate::util::space_span;
|
||||
use pretty_assertions::assert_eq;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[test]
|
||||
fn test_header() {
|
||||
let settings = mock_settings();
|
||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||
|
||||
let s = "<h1>Header 1</h1>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("#", bold),
|
||||
Span::styled(" ", bold),
|
||||
|
@ -880,7 +833,7 @@ pub mod tests {
|
|||
|
||||
let s = "<h2>Header 2</h2>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("#", bold),
|
||||
Span::styled("#", bold),
|
||||
|
@ -893,7 +846,7 @@ pub mod tests {
|
|||
|
||||
let s = "<h3>Header 3</h3>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("#", bold),
|
||||
Span::styled("#", bold),
|
||||
|
@ -907,7 +860,7 @@ pub mod tests {
|
|||
|
||||
let s = "<h4>Header 4</h4>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("#", bold),
|
||||
Span::styled("#", bold),
|
||||
|
@ -922,7 +875,7 @@ pub mod tests {
|
|||
|
||||
let s = "<h5>Header 5</h5>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("#", bold),
|
||||
Span::styled("#", bold),
|
||||
|
@ -938,7 +891,7 @@ pub mod tests {
|
|||
|
||||
let s = "<h6>Header 6</h6>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("#", bold),
|
||||
Span::styled("#", bold),
|
||||
|
@ -956,7 +909,6 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_style() {
|
||||
let settings = mock_settings();
|
||||
let def = Style::default();
|
||||
let bold = def.add_modifier(StyleModifier::BOLD);
|
||||
let italic = def.add_modifier(StyleModifier::ITALIC);
|
||||
|
@ -966,7 +918,7 @@ pub mod tests {
|
|||
|
||||
let s = "<b>Bold!</b>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Bold", bold),
|
||||
Span::styled("!", bold),
|
||||
|
@ -975,7 +927,7 @@ pub mod tests {
|
|||
|
||||
let s = "<strong>Bold!</strong>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Bold", bold),
|
||||
Span::styled("!", bold),
|
||||
|
@ -984,7 +936,7 @@ pub mod tests {
|
|||
|
||||
let s = "<i>Italic!</i>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Italic", italic),
|
||||
Span::styled("!", italic),
|
||||
|
@ -993,7 +945,7 @@ pub mod tests {
|
|||
|
||||
let s = "<em>Italic!</em>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Italic", italic),
|
||||
Span::styled("!", italic),
|
||||
|
@ -1002,7 +954,7 @@ pub mod tests {
|
|||
|
||||
let s = "<del>Strikethrough!</del>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Strikethrough", strike),
|
||||
Span::styled("!", strike),
|
||||
|
@ -1011,7 +963,7 @@ pub mod tests {
|
|||
|
||||
let s = "<strike>Strikethrough!</strike>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Strikethrough", strike),
|
||||
Span::styled("!", strike),
|
||||
|
@ -1020,7 +972,7 @@ pub mod tests {
|
|||
|
||||
let s = "<u>Underline!</u>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Underline", underl),
|
||||
Span::styled("!", underl),
|
||||
|
@ -1029,7 +981,7 @@ pub mod tests {
|
|||
|
||||
let s = "<font color=\"#ff0000\">Red!</u>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Red", red),
|
||||
Span::styled("!", red),
|
||||
|
@ -1038,7 +990,7 @@ pub mod tests {
|
|||
|
||||
let s = "<font color=\"red\">Red!</u>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(20, Style::default(), false, &settings);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::styled("Red", red),
|
||||
Span::styled("!", red),
|
||||
|
@ -1048,10 +1000,9 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_paragraph() {
|
||||
let settings = mock_settings();
|
||||
let s = "<p>Hello world!</p><p>Content</p><p>Goodbye world!</p>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||
let text = tree.to_text(10, Style::default(), false, false);
|
||||
assert_eq!(text.lines.len(), 7);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
|
@ -1076,42 +1027,25 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_blockquote() {
|
||||
let settings = mock_settings();
|
||||
let s = "<blockquote>Hello world!</blockquote>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||
let style = Style::new().fg(QUOTE_COLOR);
|
||||
let text = tree.to_text(10, Style::default(), false, false);
|
||||
assert_eq!(text.lines.len(), 2);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(line::THICK_VERTICAL, style),
|
||||
Span::raw(" "),
|
||||
Span::raw("Hello"),
|
||||
Span::raw(" "),
|
||||
Span::raw(" "),
|
||||
])
|
||||
Line::from(vec![Span::raw(" "), Span::raw("Hello"), Span::raw(" ")])
|
||||
);
|
||||
assert_eq!(
|
||||
text.lines[1],
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(line::THICK_VERTICAL, style),
|
||||
Span::raw(" "),
|
||||
Span::raw("world"),
|
||||
Span::raw("!"),
|
||||
Span::raw(" "),
|
||||
])
|
||||
Line::from(vec![Span::raw(" "), Span::raw("world"), Span::raw("!")])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(8, Style::default(), false, &settings);
|
||||
let text = tree.to_text(8, Style::default(), false, false);
|
||||
assert_eq!(text.lines.len(), 6);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
|
@ -1171,10 +1105,9 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
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 tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(9, Style::default(), false, &settings);
|
||||
let text = tree.to_text(9, Style::default(), false, false);
|
||||
assert_eq!(text.lines.len(), 6);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
|
@ -1234,7 +1167,6 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_table() {
|
||||
let settings = mock_settings();
|
||||
let s = "<table>\
|
||||
<thead>\
|
||||
<tr><th>Column 1</th><th>Column 2</th><th>Column 3</th></tr>
|
||||
|
@ -1245,7 +1177,7 @@ pub mod tests {
|
|||
<tr><td>a</td><td>b</td><td>c</td></tr>\
|
||||
</tbody></table>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(15, Style::default(), false, &settings);
|
||||
let text = tree.to_text(15, Style::default(), false, false);
|
||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||
assert_eq!(text.lines.len(), 11);
|
||||
|
||||
|
@ -1335,11 +1267,10 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_matrix_reply() {
|
||||
let settings = mock_settings();
|
||||
let s = "<mx-reply>This was replied to</mx-reply>This is the reply";
|
||||
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(10, Style::default(), false, &settings);
|
||||
let text = tree.to_text(10, Style::default(), false, false);
|
||||
assert_eq!(text.lines.len(), 4);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
|
@ -1376,7 +1307,7 @@ pub mod tests {
|
|||
);
|
||||
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(10, Style::default(), true, &settings);
|
||||
let text = tree.to_text(10, Style::default(), true, false);
|
||||
assert_eq!(text.lines.len(), 2);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
|
@ -1401,10 +1332,9 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_self_closing() {
|
||||
let settings = mock_settings();
|
||||
let s = "Hello<br>World<br>Goodbye";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(7, Style::default(), true, &settings);
|
||||
let text = tree.to_text(7, Style::default(), true, false);
|
||||
assert_eq!(text.lines.len(), 3);
|
||||
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(" "),]));
|
||||
|
@ -1413,10 +1343,9 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_embedded_newline() {
|
||||
let settings = mock_settings();
|
||||
let s = "<p>Hello\nWorld</p>";
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(15, Style::default(), true, &settings);
|
||||
let text = tree.to_text(15, Style::default(), true, false);
|
||||
assert_eq!(text.lines.len(), 1);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
|
@ -1431,18 +1360,16 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_pre_tag() {
|
||||
let settings = mock_settings();
|
||||
let s = concat!(
|
||||
"<pre><code class=\"language-rust\">",
|
||||
"fn hello() -> usize {\n",
|
||||
" \t// weired\n",
|
||||
" return 5;\n",
|
||||
"}\n",
|
||||
"</code></pre>\n"
|
||||
);
|
||||
let tree = parse_matrix_html(s);
|
||||
let text = tree.to_text(25, Style::default(), true, &settings);
|
||||
assert_eq!(text.lines.len(), 6);
|
||||
let text = tree.to_text(25, Style::default(), true, false);
|
||||
assert_eq!(text.lines.len(), 5);
|
||||
assert_eq!(
|
||||
text.lines[0],
|
||||
Line::from(vec![
|
||||
|
@ -1473,20 +1400,6 @@ pub mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
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![
|
||||
Span::raw(line::VERTICAL),
|
||||
Span::raw(" "),
|
||||
|
@ -1499,7 +1412,7 @@ pub mod tests {
|
|||
])
|
||||
);
|
||||
assert_eq!(
|
||||
text.lines[4],
|
||||
text.lines[3],
|
||||
Line::from(vec![
|
||||
Span::raw(line::VERTICAL),
|
||||
Span::raw("}"),
|
||||
|
@ -1508,7 +1421,7 @@ pub mod tests {
|
|||
])
|
||||
);
|
||||
assert_eq!(
|
||||
text.lines[5],
|
||||
text.lines[4],
|
||||
Line::from(vec![
|
||||
Span::raw(line::BOTTOM_LEFT),
|
||||
Span::raw(line::HORIZONTAL.repeat(23)),
|
||||
|
@ -1519,11 +1432,6 @@ pub mod tests {
|
|||
|
||||
#[test]
|
||||
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"] {
|
||||
let emoji = emojis::get_by_shortcode(shortcode).unwrap().as_str();
|
||||
let emoji_width = UnicodeWidthStr::width(emoji);
|
||||
|
@ -1532,13 +1440,13 @@ pub mod tests {
|
|||
let s = format!("<p>{emoji}</p>");
|
||||
let tree = parse_matrix_html(s.as_str());
|
||||
// Test with emojis_shortcodes set to false
|
||||
let text = tree.to_text(20, Style::default(), false, &disabled);
|
||||
let text = tree.to_text(20, Style::default(), false, false);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::raw(emoji),
|
||||
space_span(20 - emoji_width, Style::default()),
|
||||
]),]);
|
||||
// Test with emojis_shortcodes set to true
|
||||
let text = tree.to_text(20, Style::default(), false, &enabled);
|
||||
let text = tree.to_text(20, Style::default(), false, true);
|
||||
assert_eq!(text.lines, vec![Line::from(vec![
|
||||
Span::raw(replacement.as_str()),
|
||||
space_span(20 - replacement_width, Style::default()),
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
use std::borrow::Cow;
|
||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::hash_set;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt::{self, Display};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use chrono::{DateTime, Local as LocalTz};
|
||||
use chrono::{DateTime, Local as LocalTz, NaiveDateTime, TimeZone};
|
||||
use humansize::{format_size, DECIMAL};
|
||||
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||
use serde_json::json;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
|
@ -35,7 +35,6 @@ use matrix_sdk::ruma::{
|
|||
},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncStateEvent,
|
||||
RedactContent,
|
||||
RedactedUnsigned,
|
||||
},
|
||||
|
@ -68,17 +67,13 @@ use crate::{
|
|||
mod compose;
|
||||
mod html;
|
||||
mod printer;
|
||||
mod state;
|
||||
|
||||
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 struct Messages(BTreeMap<MessageKey, Message>, pub ReceiptThread);
|
||||
#[derive(Default)]
|
||||
pub struct Messages(BTreeMap<MessageKey, Message>);
|
||||
|
||||
impl Deref for Messages {
|
||||
type Target = BTreeMap<MessageKey, Message>;
|
||||
|
@ -95,18 +90,6 @@ impl DerefMut for 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>) {
|
||||
let event_id = key.1.clone();
|
||||
let msg = msg.into();
|
||||
|
@ -177,9 +160,7 @@ fn placeholder_frame(
|
|||
}
|
||||
let mut placeholder = "\u{230c}".to_string();
|
||||
placeholder.push_str(&" ".repeat(width - 2));
|
||||
placeholder.push('\u{230d}');
|
||||
placeholder.push_str(&"\n".repeat((height - 1) / 2));
|
||||
|
||||
placeholder.push_str("\u{230d}\n");
|
||||
if *height > 2 {
|
||||
if let Some(text) = text {
|
||||
if text.width() <= width - 2 {
|
||||
|
@ -189,7 +170,7 @@ fn placeholder_frame(
|
|||
}
|
||||
}
|
||||
|
||||
placeholder.push_str(&"\n".repeat(height / 2));
|
||||
placeholder.push_str(&"\n".repeat(height - 2));
|
||||
placeholder.push('\u{230e}');
|
||||
placeholder.push_str(&" ".repeat(width - 2));
|
||||
placeholder.push_str("\u{230f}\n");
|
||||
|
@ -199,8 +180,9 @@ fn placeholder_frame(
|
|||
#[inline]
|
||||
fn millis_to_datetime(ms: UInt) -> DateTime<LocalTz> {
|
||||
let time = i64::from(ms) / 1000;
|
||||
let time = DateTime::from_timestamp(time, 0).unwrap_or_default();
|
||||
time.into()
|
||||
let time = NaiveDateTime::from_timestamp_opt(time, 0).unwrap_or_default();
|
||||
|
||||
LocalTz.from_utc_datetime(&time)
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
@ -444,7 +426,6 @@ pub enum MessageEvent {
|
|||
EncryptedRedacted(Box<RedactedRoomEncryptedEvent>),
|
||||
Original(Box<OriginalRoomMessageEvent>),
|
||||
Redacted(Box<RedactedRoomMessageEvent>),
|
||||
State(Box<AnySyncStateEvent>),
|
||||
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
||||
}
|
||||
|
||||
|
@ -455,7 +436,6 @@ impl MessageEvent {
|
|||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.as_ref(),
|
||||
MessageEvent::Original(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(),
|
||||
}
|
||||
}
|
||||
|
@ -466,7 +446,6 @@ impl MessageEvent {
|
|||
MessageEvent::Original(ev) => Some(&ev.content),
|
||||
MessageEvent::EncryptedRedacted(_) => None,
|
||||
MessageEvent::Redacted(_) => None,
|
||||
MessageEvent::State(_) => None,
|
||||
MessageEvent::Local(_, content) => Some(content),
|
||||
}
|
||||
}
|
||||
|
@ -484,7 +463,6 @@ impl MessageEvent {
|
|||
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
||||
MessageEvent::EncryptedRedacted(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),
|
||||
}
|
||||
}
|
||||
|
@ -495,7 +473,6 @@ impl MessageEvent {
|
|||
MessageEvent::EncryptedRedacted(_) => return None,
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
MessageEvent::State(ev) => return Some(html_state(ev)),
|
||||
MessageEvent::Local(_, content) => content,
|
||||
};
|
||||
|
||||
|
@ -515,7 +492,6 @@ impl MessageEvent {
|
|||
MessageEvent::EncryptedOriginal(_) => return,
|
||||
MessageEvent::EncryptedRedacted(_) => return,
|
||||
MessageEvent::Redacted(_) => return,
|
||||
MessageEvent::State(_) => return,
|
||||
MessageEvent::Local(_, _) => return,
|
||||
MessageEvent::Original(ev) => {
|
||||
let redacted = RedactedRoomMessageEvent {
|
||||
|
@ -647,8 +623,8 @@ struct MessageFormatter<'a> {
|
|||
/// The date the message was sent.
|
||||
date: Option<Span<'a>>,
|
||||
|
||||
/// The users who have read up to this message.
|
||||
read: Vec<OwnedUserId>,
|
||||
/// Iterator over the users who have read up to this message.
|
||||
read: Option<hash_set::Iter<'a, OwnedUserId>>,
|
||||
}
|
||||
|
||||
impl<'a> MessageFormatter<'a> {
|
||||
|
@ -681,11 +657,13 @@ impl<'a> MessageFormatter<'a> {
|
|||
line.push(time);
|
||||
|
||||
// Show read receipts.
|
||||
let user_char = |user: OwnedUserId| -> Span { settings.get_user_char_span(&user) };
|
||||
let user_char =
|
||||
|user: &'a OwnedUserId| -> Span<'a> { settings.get_user_char_span(user) };
|
||||
let mut read = self.read.iter_mut().flatten();
|
||||
|
||||
let a = self.read.pop().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let b = self.read.pop().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let c = self.read.pop().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let a = read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let b = read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
let c = read.next().map(user_char).unwrap_or_else(|| Span::raw(" "));
|
||||
|
||||
line.push(Span::raw(" "));
|
||||
line.push(c);
|
||||
|
@ -738,11 +716,11 @@ impl<'a> MessageFormatter<'a> {
|
|||
style: Style,
|
||||
text: &mut Text<'a>,
|
||||
info: &'a RoomInfo,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Option<ProtocolPreview<'a>> {
|
||||
) {
|
||||
let width = self.width();
|
||||
let w = width.saturating_sub(2);
|
||||
let (mut replied, proto) = msg.show_msg(w, style, true, settings);
|
||||
let shortcodes = self.settings.tunables.message_shortcode_display;
|
||||
let (mut replied, _) = msg.show_msg(w, style, true, shortcodes);
|
||||
let mut sender = msg.sender_span(info, self.settings);
|
||||
let sender_width = UnicodeWidthStr::width(sender.content.as_ref());
|
||||
let trailing = w.saturating_sub(sender_width + 1);
|
||||
|
@ -761,26 +739,16 @@ impl<'a> MessageFormatter<'a> {
|
|||
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() {
|
||||
line.spans.insert(0, Span::styled(THICK_VERTICAL, style));
|
||||
line.spans.insert(0, Span::styled(" ", style));
|
||||
}
|
||||
|
||||
self.push_text(replied, style, text);
|
||||
|
||||
proto
|
||||
}
|
||||
|
||||
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, self.settings);
|
||||
let mut emojis = printer::TextPrinter::new(self.width(), style, false, false);
|
||||
let mut reactions = 0;
|
||||
|
||||
for (key, count) in counts {
|
||||
|
@ -829,7 +797,7 @@ impl<'a> MessageFormatter<'a> {
|
|||
let plural = len != 1;
|
||||
let style = Style::default();
|
||||
let mut threaded =
|
||||
printer::TextPrinter::new(self.width(), style, false, self.settings).literal(true);
|
||||
printer::TextPrinter::new(self.width(), style, false, false).literal(true);
|
||||
let len = Span::styled(len.to_string(), style.add_modifier(StyleModifier::BOLD));
|
||||
threaded.push_str(" \u{2937} ", style);
|
||||
threaded.push_span_nobreak(len);
|
||||
|
@ -846,7 +814,7 @@ impl<'a> MessageFormatter<'a> {
|
|||
pub enum ImageStatus {
|
||||
None,
|
||||
Downloading(ImagePreviewSize),
|
||||
Loaded(Protocol),
|
||||
Loaded(Box<dyn Protocol>),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
|
@ -881,7 +849,6 @@ impl Message {
|
|||
MessageEvent::Local(_, content) => content,
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
MessageEvent::State(_) => return None,
|
||||
};
|
||||
|
||||
match &content.relates_to {
|
||||
|
@ -902,7 +869,6 @@ impl Message {
|
|||
MessageEvent::Local(_, content) => content,
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
MessageEvent::State(_) => return None,
|
||||
};
|
||||
|
||||
match &content.relates_to {
|
||||
|
@ -956,13 +922,7 @@ impl Message {
|
|||
let fill = width - user_gutter - TIME_GUTTER - READ_GUTTER;
|
||||
let user = self.show_sender(prev, true, info, settings);
|
||||
let time = self.timestamp.show_time();
|
||||
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();
|
||||
let read = info.event_receipts.get(self.event.event_id()).map(|read| read.iter());
|
||||
|
||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||
} else if user_gutter + TIME_GUTTER + MIN_MSG_LEN <= width {
|
||||
|
@ -970,7 +930,7 @@ impl Message {
|
|||
let fill = width - user_gutter - TIME_GUTTER;
|
||||
let user = self.show_sender(prev, true, info, settings);
|
||||
let time = self.timestamp.show_time();
|
||||
let read = Vec::new();
|
||||
let read = None;
|
||||
|
||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||
} else if user_gutter + MIN_MSG_LEN <= width {
|
||||
|
@ -978,7 +938,7 @@ impl Message {
|
|||
let fill = width - user_gutter;
|
||||
let user = self.show_sender(prev, true, info, settings);
|
||||
let time = None;
|
||||
let read = Vec::new();
|
||||
let read = None;
|
||||
|
||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||
} else {
|
||||
|
@ -986,7 +946,7 @@ impl Message {
|
|||
let fill = width.saturating_sub(2);
|
||||
let user = self.show_sender(prev, false, info, settings);
|
||||
let time = None;
|
||||
let read = Vec::new();
|
||||
let read = None;
|
||||
|
||||
MessageFormatter { settings, cols, orig, fill, user, date, time, read }
|
||||
}
|
||||
|
@ -1002,7 +962,7 @@ impl Message {
|
|||
vwctx: &ViewportContext<MessageCursor>,
|
||||
info: &'a RoomInfo,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> (Text<'a>, [Option<ProtocolPreview<'a>>; 2]) {
|
||||
) -> (Text<'a>, Option<(&dyn Protocol, u16, u16)>) {
|
||||
let width = vwctx.get_width();
|
||||
|
||||
let style = self.get_render_style(selected, settings);
|
||||
|
@ -1015,20 +975,24 @@ impl Message {
|
|||
.reply_to()
|
||||
.or_else(|| self.thread_root())
|
||||
.and_then(|e| info.get_event(&e));
|
||||
let proto_reply = reply.as_ref().and_then(|r| {
|
||||
// Format the reply header, push it into the `Text` buffer, and get any image.
|
||||
fmt.push_in_reply(r, style, &mut text, info, settings)
|
||||
});
|
||||
|
||||
if let Some(r) = &reply {
|
||||
fmt.push_in_reply(r, style, &mut text, info);
|
||||
}
|
||||
|
||||
// Now show the message contents, and the inlined reply if we couldn't find it above.
|
||||
let (msg, proto) = self.show_msg(width, style, reply.is_some(), settings);
|
||||
let (msg, proto) = self.show_msg(
|
||||
width,
|
||||
style,
|
||||
reply.is_some(),
|
||||
settings.tunables.message_shortcode_display,
|
||||
);
|
||||
|
||||
// Given our text so far, determine the image offset.
|
||||
let proto_main = proto.map(|p| {
|
||||
let proto = proto.map(|p| {
|
||||
let y_off = text.lines.len() as u16;
|
||||
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 we're going to print.
|
||||
// Adjust y_off by 1 if a date was printed before the message to account for the extra line.
|
||||
let y_off = if fmt.date.is_some() { y_off + 1 } else { y_off };
|
||||
(p, x_off, y_off)
|
||||
});
|
||||
|
@ -1049,7 +1013,7 @@ impl Message {
|
|||
fmt.push_thread_reply_count(thread.len(), &mut text);
|
||||
}
|
||||
|
||||
(text, [proto_main, proto_reply])
|
||||
(text, proto)
|
||||
}
|
||||
|
||||
pub fn show<'a>(
|
||||
|
@ -1063,18 +1027,18 @@ impl Message {
|
|||
self.show_with_preview(prev, selected, vwctx, info, settings).0
|
||||
}
|
||||
|
||||
fn show_msg<'a>(
|
||||
&'a self,
|
||||
fn show_msg(
|
||||
&self,
|
||||
width: usize,
|
||||
style: Style,
|
||||
hide_reply: bool,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> (Text<'a>, Option<&'a Protocol>) {
|
||||
emoji_shortcodes: bool,
|
||||
) -> (Text, Option<&dyn Protocol>) {
|
||||
if let Some(html) = &self.html {
|
||||
(html.to_text(width, style, hide_reply, settings), None)
|
||||
(html.to_text(width, style, hide_reply, emoji_shortcodes), None)
|
||||
} else {
|
||||
let mut msg = self.event.body();
|
||||
if settings.tunables.message_shortcode_display {
|
||||
if emoji_shortcodes {
|
||||
msg = Cow::Owned(replace_emojis_in_str(msg.as_ref()));
|
||||
}
|
||||
|
||||
|
@ -1089,8 +1053,8 @@ impl Message {
|
|||
placeholder_frame(Some("Downloading..."), width, image_preview_size)
|
||||
},
|
||||
ImageStatus::Loaded(backend) => {
|
||||
proto = Some(backend);
|
||||
placeholder_frame(Some("Cut off..."), width, &backend.area().into())
|
||||
proto = Some(backend.as_ref());
|
||||
placeholder_frame(None, width, &backend.rect().into())
|
||||
},
|
||||
ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")),
|
||||
};
|
||||
|
@ -1133,9 +1097,9 @@ impl Message {
|
|||
let padding = user_gutter - 2 - width;
|
||||
|
||||
let sender = if align_right {
|
||||
format!("{}{} ", space(padding), truncated)
|
||||
space(padding) + &truncated + " "
|
||||
} else {
|
||||
format!("{}{} ", truncated, space(padding))
|
||||
truncated.into_owned() + &space(padding) + " "
|
||||
};
|
||||
|
||||
Span::styled(sender, style).into()
|
||||
|
@ -1144,8 +1108,6 @@ impl Message {
|
|||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
||||
self.event.redact(redaction, version);
|
||||
self.html = None;
|
||||
self.downloaded = false;
|
||||
self.image_preview = ImageStatus::None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1191,16 +1153,6 @@ 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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.event.body())
|
||||
|
@ -1299,7 +1251,7 @@ pub mod tests {
|
|||
assert_eq!(k6, &MSG1_KEY.clone());
|
||||
|
||||
// MessageCursor::latest() fails to convert for a room w/o messages.
|
||||
let messages_empty = Messages::new(ReceiptThread::Main);
|
||||
let messages_empty = Messages::default();
|
||||
assert_eq!(mc6.to_key(&messages_empty), None);
|
||||
}
|
||||
|
||||
|
@ -1361,33 +1313,6 @@ pub mod tests {
|
|||
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,7 +11,6 @@ use ratatui::text::{Line, Span, Text};
|
|||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::config::{ApplicationSettings, TunableValues};
|
||||
use crate::util::{
|
||||
replace_emojis_in_line,
|
||||
replace_emojis_in_span,
|
||||
|
@ -26,34 +25,28 @@ pub struct TextPrinter<'a> {
|
|||
width: usize,
|
||||
base_style: Style,
|
||||
hide_reply: bool,
|
||||
emoji_shortcodes: bool,
|
||||
|
||||
alignment: Alignment,
|
||||
curr_spans: Vec<Span<'a>>,
|
||||
curr_width: usize,
|
||||
literal: bool,
|
||||
|
||||
pub(super) settings: &'a ApplicationSettings,
|
||||
}
|
||||
|
||||
impl<'a> TextPrinter<'a> {
|
||||
/// Create a new printer.
|
||||
pub fn new(
|
||||
width: usize,
|
||||
base_style: Style,
|
||||
hide_reply: bool,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Self {
|
||||
pub fn new(width: usize, base_style: Style, hide_reply: bool, emoji_shortcodes: bool) -> Self {
|
||||
TextPrinter {
|
||||
text: Text::default(),
|
||||
width,
|
||||
base_style,
|
||||
hide_reply,
|
||||
emoji_shortcodes,
|
||||
|
||||
alignment: Alignment::Left,
|
||||
curr_spans: vec![],
|
||||
curr_width: 0,
|
||||
literal: false,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,15 +69,7 @@ impl<'a> TextPrinter<'a> {
|
|||
|
||||
/// Indicates whether emojis should be replaced by shortcodes
|
||||
pub fn emoji_shortcodes(&self) -> bool {
|
||||
self.tunables().message_shortcode_display
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> &ApplicationSettings {
|
||||
self.settings
|
||||
}
|
||||
|
||||
pub fn tunables(&self) -> &TunableValues {
|
||||
&self.settings.tunables
|
||||
self.emoji_shortcodes
|
||||
}
|
||||
|
||||
/// Indicates the current printer's width.
|
||||
|
@ -99,12 +84,12 @@ impl<'a> TextPrinter<'a> {
|
|||
width: self.width.saturating_sub(indent),
|
||||
base_style: self.base_style,
|
||||
hide_reply: self.hide_reply,
|
||||
emoji_shortcodes: self.emoji_shortcodes,
|
||||
|
||||
alignment: self.alignment,
|
||||
curr_spans: vec![],
|
||||
curr_width: 0,
|
||||
literal: self.literal,
|
||||
settings: self.settings,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +179,7 @@ impl<'a> TextPrinter<'a> {
|
|||
|
||||
/// Push a [Span] that isn't allowed to break across lines.
|
||||
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);
|
||||
}
|
||||
let sw = UnicodeWidthStr::width(span.content.as_ref());
|
||||
|
@ -216,8 +201,6 @@ impl<'a> TextPrinter<'a> {
|
|||
return;
|
||||
}
|
||||
|
||||
let tabstop = self.settings().tunables.tabstop;
|
||||
|
||||
for mut word in UnicodeSegmentation::split_word_bounds(s) {
|
||||
if let "\n" | "\r\n" = word {
|
||||
if self.literal {
|
||||
|
@ -234,17 +217,11 @@ impl<'a> TextPrinter<'a> {
|
|||
continue;
|
||||
}
|
||||
|
||||
let mut cow = if self.emoji_shortcodes() {
|
||||
let cow = if self.emoji_shortcodes {
|
||||
Cow::Owned(replace_emojis_in_str(word))
|
||||
} else {
|
||||
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());
|
||||
|
||||
if sw > self.width {
|
||||
|
@ -276,7 +253,7 @@ impl<'a> TextPrinter<'a> {
|
|||
/// Push a [Line] into the printer.
|
||||
pub fn push_line(&mut self, mut line: Line<'a>) {
|
||||
self.commit();
|
||||
if self.emoji_shortcodes() {
|
||||
if self.emoji_shortcodes {
|
||||
replace_emojis_in_line(&mut line);
|
||||
}
|
||||
self.text.lines.push(line);
|
||||
|
@ -285,7 +262,7 @@ impl<'a> TextPrinter<'a> {
|
|||
/// Push multiline [Text] into the printer.
|
||||
pub fn push_text(&mut self, mut text: Text<'a>) {
|
||||
self.commit();
|
||||
if self.emoji_shortcodes() {
|
||||
if self.emoji_shortcodes {
|
||||
for line in &mut text.lines {
|
||||
replace_emojis_in_line(line);
|
||||
}
|
||||
|
@ -303,12 +280,10 @@ impl<'a> TextPrinter<'a> {
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::tests::mock_settings;
|
||||
|
||||
#[test]
|
||||
fn test_push_nobreak() {
|
||||
let settings = mock_settings();
|
||||
let mut printer = TextPrinter::new(5, Style::default(), false, &settings);
|
||||
let mut printer = TextPrinter::new(5, Style::default(), false, false);
|
||||
printer.push_span_nobreak("hello world".into());
|
||||
let text = printer.finish();
|
||||
assert_eq!(text.lines.len(), 1);
|
||||
|
|
|
@ -1,956 +0,0 @@
|
|||
//! Code for displaying state events.
|
||||
use std::borrow::Cow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use matrix_sdk::ruma::{
|
||||
events::{
|
||||
room::member::MembershipChange,
|
||||
AnyFullStateEventContent,
|
||||
AnySyncStateEvent,
|
||||
FullStateEventContent,
|
||||
},
|
||||
OwnedRoomId,
|
||||
UserId,
|
||||
};
|
||||
|
||||
use super::html::{StyleTree, StyleTreeNode};
|
||||
use ratatui::style::{Modifier as StyleModifier, Style};
|
||||
|
||||
fn bold(s: impl Into<Cow<'static, str>>) -> StyleTreeNode {
|
||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||
let text = StyleTreeNode::Text(s.into());
|
||||
StyleTreeNode::Style(Box::new(text), bold)
|
||||
}
|
||||
|
||||
pub fn body_cow_state(ev: &AnySyncStateEvent) -> Cow<'static, str> {
|
||||
let event = match ev.content() {
|
||||
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let mut m = format!(
|
||||
"* updated the room policy rule for {:?} to {:?}",
|
||||
content.0.entity,
|
||||
content.0.recommendation.as_str()
|
||||
);
|
||||
|
||||
if !content.0.reason.is_empty() {
|
||||
m.push_str(" (reason: ");
|
||||
m.push_str(&content.0.reason);
|
||||
m.push(')');
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let mut m = format!(
|
||||
"* updated the server policy rule for {:?} to {:?}",
|
||||
content.0.entity,
|
||||
content.0.recommendation.as_str()
|
||||
);
|
||||
|
||||
if !content.0.reason.is_empty() {
|
||||
m.push_str(" (reason: ");
|
||||
m.push_str(&content.0.reason);
|
||||
m.push(')');
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let mut m = format!(
|
||||
"* updated the user policy rule for {:?} to {:?}",
|
||||
content.0.entity,
|
||||
content.0.recommendation.as_str()
|
||||
);
|
||||
|
||||
if !content.0.reason.is_empty() {
|
||||
m.push_str(" (reason: ");
|
||||
m.push_str(&content.0.reason);
|
||||
m.push(')');
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
let mut m = String::from("* set the room aliases to: ");
|
||||
|
||||
for (i, alias) in content.aliases.iter().enumerate() {
|
||||
if i != 0 {
|
||||
m.push_str(", ");
|
||||
}
|
||||
|
||||
m.push_str(alias.as_str());
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Original {
|
||||
content,
|
||||
prev_content,
|
||||
}) => {
|
||||
let prev_url = prev_content.as_ref().and_then(|p| p.url.as_ref());
|
||||
|
||||
match (prev_url, content.url) {
|
||||
(None, Some(_)) => return Cow::Borrowed("* added a room avatar"),
|
||||
(Some(old), Some(new)) => {
|
||||
if old != &new {
|
||||
return Cow::Borrowed("* replaced the room avatar");
|
||||
}
|
||||
|
||||
return Cow::Borrowed("* updated the room avatar state");
|
||||
},
|
||||
(Some(_), None) => return Cow::Borrowed("* removed the room avatar"),
|
||||
(None, None) => return Cow::Borrowed("* updated the room avatar state"),
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original {
|
||||
content,
|
||||
prev_content,
|
||||
}) => {
|
||||
let old_canon = prev_content.as_ref().and_then(|p| p.alias.as_ref());
|
||||
let new_canon = content.alias.as_ref();
|
||||
|
||||
match (old_canon, new_canon) {
|
||||
(None, Some(canon)) => {
|
||||
format!("* updated the canonical alias for the room to: {}", canon)
|
||||
},
|
||||
(Some(old), Some(new)) => {
|
||||
if old != new {
|
||||
format!("* updated the canonical alias for the room to: {}", new)
|
||||
} else {
|
||||
return Cow::Borrowed("* removed the canonical alias for the room");
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
return Cow::Borrowed("* removed the canonical alias for the room");
|
||||
},
|
||||
(None, None) => {
|
||||
return Cow::Borrowed("* did not change the canonical alias");
|
||||
},
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
if content.federate {
|
||||
return Cow::Borrowed("* created a federated room");
|
||||
} else {
|
||||
return Cow::Borrowed("* created a non-federated room");
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Original { .. }) => {
|
||||
return Cow::Borrowed("* updated the encryption settings for the room");
|
||||
},
|
||||
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
format!("* set guest access for the room to {:?}", content.guest_access.as_str())
|
||||
},
|
||||
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
format!(
|
||||
"* updated history visibility for the room to {:?}",
|
||||
content.history_visibility.as_str()
|
||||
)
|
||||
},
|
||||
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
format!("* update the join rules for the room to {:?}", content.join_rule.as_str())
|
||||
},
|
||||
AnyFullStateEventContent::RoomMember(FullStateEventContent::Original {
|
||||
content,
|
||||
prev_content,
|
||||
}) => {
|
||||
let Ok(state_key) = UserId::parse(ev.state_key()) else {
|
||||
return Cow::Owned(format!(
|
||||
"* failed to calculate membership change for {:?}",
|
||||
ev.state_key()
|
||||
));
|
||||
};
|
||||
|
||||
let prev_details = prev_content.as_ref().map(|p| p.details());
|
||||
let change = content.membership_change(prev_details, ev.sender(), &state_key);
|
||||
|
||||
match change {
|
||||
MembershipChange::None => {
|
||||
format!("* did nothing to {}", state_key)
|
||||
},
|
||||
MembershipChange::Error => {
|
||||
format!("* failed to calculate membership change to {}", state_key)
|
||||
},
|
||||
MembershipChange::Joined => {
|
||||
return Cow::Borrowed("* joined the room");
|
||||
},
|
||||
MembershipChange::Left => {
|
||||
return Cow::Borrowed("* left the room");
|
||||
},
|
||||
MembershipChange::Banned => {
|
||||
format!("* banned {} from the room", state_key)
|
||||
},
|
||||
MembershipChange::Unbanned => {
|
||||
format!("* unbanned {} from the room", state_key)
|
||||
},
|
||||
MembershipChange::Kicked => {
|
||||
format!("* kicked {} from the room", state_key)
|
||||
},
|
||||
MembershipChange::Invited => {
|
||||
format!("* invited {} to the room", state_key)
|
||||
},
|
||||
MembershipChange::KickedAndBanned => {
|
||||
format!("* kicked and banned {} from the room", state_key)
|
||||
},
|
||||
MembershipChange::InvitationAccepted => {
|
||||
return Cow::Borrowed("* accepted an invitation to join the room");
|
||||
},
|
||||
MembershipChange::InvitationRejected => {
|
||||
return Cow::Borrowed("* rejected an invitation to join the room");
|
||||
},
|
||||
MembershipChange::InvitationRevoked => {
|
||||
format!("* revoked an invitation for {} to join the room", state_key)
|
||||
},
|
||||
MembershipChange::Knocked => {
|
||||
return Cow::Borrowed("* would like to join the room");
|
||||
},
|
||||
MembershipChange::KnockAccepted => {
|
||||
format!("* accepted the room knock from {}", state_key)
|
||||
},
|
||||
MembershipChange::KnockRetracted => {
|
||||
return Cow::Borrowed("* retracted their room knock");
|
||||
},
|
||||
MembershipChange::KnockDenied => {
|
||||
format!("* rejected the room knock from {}", state_key)
|
||||
},
|
||||
MembershipChange::ProfileChanged { displayname_change, avatar_url_change } => {
|
||||
match (displayname_change, avatar_url_change) {
|
||||
(Some(change), avatar_change) => {
|
||||
let mut m = match (change.old, change.new) {
|
||||
(None, Some(new)) => {
|
||||
format!("* set their display name to {:?}", new)
|
||||
},
|
||||
(Some(old), Some(new)) => {
|
||||
format!("* changed their display name from {old} to {new}")
|
||||
},
|
||||
(Some(_), None) => "* unset their display name".to_string(),
|
||||
(None, None) => {
|
||||
"* made an unknown change to their display name".to_string()
|
||||
},
|
||||
};
|
||||
|
||||
if avatar_change.is_some() {
|
||||
m.push_str(" and changed their user avatar");
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
(None, Some(change)) => {
|
||||
match (change.old, change.new) {
|
||||
(None, Some(_)) => {
|
||||
return Cow::Borrowed("* added a user avatar");
|
||||
},
|
||||
(Some(_), Some(_)) => {
|
||||
return Cow::Borrowed("* changed their user avatar");
|
||||
},
|
||||
(Some(_), None) => {
|
||||
return Cow::Borrowed("* removed their user avatar");
|
||||
},
|
||||
(None, None) => {
|
||||
return Cow::Borrowed(
|
||||
"* made an unknown change to their user avatar",
|
||||
);
|
||||
},
|
||||
}
|
||||
},
|
||||
(None, None) => {
|
||||
return Cow::Borrowed("* changed their user profile");
|
||||
},
|
||||
}
|
||||
},
|
||||
ev => {
|
||||
format!("* made an unknown membership change to {}: {:?}", state_key, ev)
|
||||
},
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => {
|
||||
format!("* updated the room name to {:?}", content.name)
|
||||
},
|
||||
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { .. }) => {
|
||||
return Cow::Borrowed("* updated the pinned events for the room");
|
||||
},
|
||||
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Original { .. }) => {
|
||||
return Cow::Borrowed("* updated the power levels for the room");
|
||||
},
|
||||
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Original { .. }) => {
|
||||
return Cow::Borrowed("* updated the room's server ACLs");
|
||||
},
|
||||
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
format!("* sent a third-party invite to {:?}", content.display_name)
|
||||
},
|
||||
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
format!(
|
||||
"* upgraded the room; replacement room is {}",
|
||||
content.replacement_room.as_str()
|
||||
)
|
||||
},
|
||||
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
format!("* set the room topic to {:?}", content.topic)
|
||||
},
|
||||
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Original { .. }) => {
|
||||
format!("* added a space child: {}", ev.state_key())
|
||||
},
|
||||
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
if content.canonical {
|
||||
format!("* added a canonical parent space: {}", ev.state_key())
|
||||
} else {
|
||||
format!("* added a parent space: {}", ev.state_key())
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Original { .. }) => {
|
||||
return Cow::Borrowed("* shared beacon information");
|
||||
},
|
||||
AnyFullStateEventContent::CallMember(FullStateEventContent::Original { .. }) => {
|
||||
return Cow::Borrowed("* updated membership for room call");
|
||||
},
|
||||
AnyFullStateEventContent::MemberHints(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
let mut m = String::from("* updated the list of service members in the room hints: ");
|
||||
|
||||
for (i, member) in content.service_members.iter().enumerate() {
|
||||
if i != 0 {
|
||||
m.push_str(", ");
|
||||
}
|
||||
|
||||
m.push_str(member.as_str());
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
|
||||
// Redacted variants of state events:
|
||||
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated a room policy rule (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated a server policy rule (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated a user policy rule (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the room aliases for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the room avatar (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the canonical alias for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* created the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the encryption settings for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed(
|
||||
"* updated the guest access configuration for the room (redacted)",
|
||||
);
|
||||
},
|
||||
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated history visilibity for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the join rules for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomMember(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the room membership (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomName(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the room name (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the pinned events for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the power levels for the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the room's server ACLs (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* sent a third-party invite (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* upgraded the room (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* updated the room topic (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* added a space child (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* added a parent space (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("* shared beacon information (redacted)");
|
||||
},
|
||||
AnyFullStateEventContent::CallMember(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("Call membership changed");
|
||||
},
|
||||
AnyFullStateEventContent::MemberHints(FullStateEventContent::Redacted(_)) => {
|
||||
return Cow::Borrowed("Member hints changed");
|
||||
},
|
||||
|
||||
// Handle unknown events:
|
||||
e => {
|
||||
format!("* sent an unknown state event: {:?}", e.event_type())
|
||||
},
|
||||
};
|
||||
|
||||
return Cow::Owned(event);
|
||||
}
|
||||
|
||||
pub fn html_state(ev: &AnySyncStateEvent) -> StyleTree {
|
||||
let children = match ev.content() {
|
||||
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* updated the room policy rule for ".into());
|
||||
let entity = bold(format!("{:?}", content.0.entity));
|
||||
let middle = StyleTreeNode::Text(" to ".into());
|
||||
let rec =
|
||||
StyleTreeNode::Text(format!("{:?}", content.0.recommendation.as_str()).into());
|
||||
let mut cs = vec![prefix, entity, middle, rec];
|
||||
|
||||
if !content.0.reason.is_empty() {
|
||||
let reason = format!(" (reason: {})", content.0.reason);
|
||||
cs.push(StyleTreeNode::Text(reason.into()));
|
||||
}
|
||||
|
||||
cs
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* updated the server policy rule for ".into());
|
||||
let entity = bold(format!("{:?}", content.0.entity));
|
||||
let middle = StyleTreeNode::Text(" to ".into());
|
||||
let rec =
|
||||
StyleTreeNode::Text(format!("{:?}", content.0.recommendation.as_str()).into());
|
||||
let mut cs = vec![prefix, entity, middle, rec];
|
||||
|
||||
if !content.0.reason.is_empty() {
|
||||
let reason = format!(" (reason: {})", content.0.reason);
|
||||
cs.push(StyleTreeNode::Text(reason.into()));
|
||||
}
|
||||
|
||||
cs
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* updated the user policy rule for ".into());
|
||||
let entity = bold(format!("{:?}", content.0.entity));
|
||||
let middle = StyleTreeNode::Text(" to ".into());
|
||||
let rec =
|
||||
StyleTreeNode::Text(format!("{:?}", content.0.recommendation.as_str()).into());
|
||||
let mut cs = vec![prefix, entity, middle, rec];
|
||||
|
||||
if !content.0.reason.is_empty() {
|
||||
let reason = format!(" (reason: {})", content.0.reason);
|
||||
cs.push(StyleTreeNode::Text(reason.into()));
|
||||
}
|
||||
|
||||
cs
|
||||
},
|
||||
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* set the room aliases to: ".into());
|
||||
let mut cs = vec![prefix];
|
||||
|
||||
for (i, alias) in content.aliases.iter().enumerate() {
|
||||
if i != 0 {
|
||||
cs.push(StyleTreeNode::Text(", ".into()));
|
||||
}
|
||||
|
||||
cs.push(StyleTreeNode::RoomAlias(alias.clone()));
|
||||
}
|
||||
|
||||
cs
|
||||
},
|
||||
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Original {
|
||||
content,
|
||||
prev_content,
|
||||
}) => {
|
||||
let prev_url = prev_content.as_ref().and_then(|p| p.url.as_ref());
|
||||
|
||||
let node = match (prev_url, content.url) {
|
||||
(None, Some(_)) => StyleTreeNode::Text("* added a room avatar".into()),
|
||||
(Some(old), Some(new)) => {
|
||||
if old != &new {
|
||||
StyleTreeNode::Text("* replaced the room avatar".into())
|
||||
} else {
|
||||
StyleTreeNode::Text("* updated the room avatar state".into())
|
||||
}
|
||||
},
|
||||
(Some(_), None) => StyleTreeNode::Text("* removed the room avatar".into()),
|
||||
(None, None) => StyleTreeNode::Text("* updated the room avatar state".into()),
|
||||
};
|
||||
|
||||
vec![node]
|
||||
},
|
||||
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
if let Some(canon) = content.alias.as_ref() {
|
||||
let canon = bold(canon.to_string());
|
||||
let prefix =
|
||||
StyleTreeNode::Text("* updated the canonical alias for the room to: ".into());
|
||||
vec![prefix, canon]
|
||||
} else {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* removed the canonical alias for the room".into(),
|
||||
)]
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
if content.federate {
|
||||
vec![StyleTreeNode::Text("* created a federated room".into())]
|
||||
} else {
|
||||
vec![StyleTreeNode::Text("* created a non-federated room".into())]
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Original { .. }) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the encryption settings for the room".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let access = bold(format!("{:?}", content.guest_access.as_str()));
|
||||
let prefix = StyleTreeNode::Text("* set guest access for the room to ".into());
|
||||
vec![prefix, access]
|
||||
},
|
||||
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix =
|
||||
StyleTreeNode::Text("* updated history visibility for the room to ".into());
|
||||
let vis = bold(format!("{:?}", content.history_visibility.as_str()));
|
||||
vec![prefix, vis]
|
||||
},
|
||||
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* update the join rules for the room to ".into());
|
||||
let rule = bold(format!("{:?}", content.join_rule.as_str()));
|
||||
vec![prefix, rule]
|
||||
},
|
||||
AnyFullStateEventContent::RoomMember(FullStateEventContent::Original {
|
||||
content,
|
||||
prev_content,
|
||||
}) => {
|
||||
let Ok(state_key) = UserId::parse(ev.state_key()) else {
|
||||
let prefix =
|
||||
StyleTreeNode::Text("* failed to calculate membership change for ".into());
|
||||
let user_id = bold(format!("{:?}", ev.state_key()));
|
||||
let children = vec![prefix, user_id];
|
||||
|
||||
return StyleTree { children };
|
||||
};
|
||||
|
||||
let prev_details = prev_content.as_ref().map(|p| p.details());
|
||||
let change = content.membership_change(prev_details, ev.sender(), &state_key);
|
||||
let user_id = StyleTreeNode::UserId(state_key.clone());
|
||||
|
||||
match change {
|
||||
MembershipChange::None => {
|
||||
let prefix = StyleTreeNode::Text("* did nothing to ".into());
|
||||
vec![prefix, user_id]
|
||||
},
|
||||
MembershipChange::Error => {
|
||||
let prefix =
|
||||
StyleTreeNode::Text("* failed to calculate membership change to ".into());
|
||||
vec![prefix, user_id]
|
||||
},
|
||||
MembershipChange::Joined => {
|
||||
vec![StyleTreeNode::Text("* joined the room".into())]
|
||||
},
|
||||
MembershipChange::Left => {
|
||||
vec![StyleTreeNode::Text("* left the room".into())]
|
||||
},
|
||||
MembershipChange::Banned => {
|
||||
let prefix = StyleTreeNode::Text("* banned ".into());
|
||||
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
MembershipChange::Unbanned => {
|
||||
let prefix = StyleTreeNode::Text("* unbanned ".into());
|
||||
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
MembershipChange::Kicked => {
|
||||
let prefix = StyleTreeNode::Text("* kicked ".into());
|
||||
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
MembershipChange::Invited => {
|
||||
let prefix = StyleTreeNode::Text("* invited ".into());
|
||||
let suffix = StyleTreeNode::Text(" to the room".into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
MembershipChange::KickedAndBanned => {
|
||||
let prefix = StyleTreeNode::Text("* kicked and banned ".into());
|
||||
let suffix = StyleTreeNode::Text(" from the room".into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
MembershipChange::InvitationAccepted => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* accepted an invitation to join the room".into(),
|
||||
)]
|
||||
},
|
||||
MembershipChange::InvitationRejected => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* rejected an invitation to join the room".into(),
|
||||
)]
|
||||
},
|
||||
MembershipChange::InvitationRevoked => {
|
||||
let prefix = StyleTreeNode::Text("* revoked an invitation for ".into());
|
||||
let suffix = StyleTreeNode::Text(" to join the room".into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
MembershipChange::Knocked => {
|
||||
vec![StyleTreeNode::Text("* would like to join the room".into())]
|
||||
},
|
||||
MembershipChange::KnockAccepted => {
|
||||
let prefix = StyleTreeNode::Text("* accepted the room knock from ".into());
|
||||
vec![prefix, user_id]
|
||||
},
|
||||
MembershipChange::KnockRetracted => {
|
||||
vec![StyleTreeNode::Text("* retracted their room knock".into())]
|
||||
},
|
||||
MembershipChange::KnockDenied => {
|
||||
let prefix = StyleTreeNode::Text("* rejected the room knock from ".into());
|
||||
vec![prefix, user_id]
|
||||
},
|
||||
MembershipChange::ProfileChanged { displayname_change, avatar_url_change } => {
|
||||
match (displayname_change, avatar_url_change) {
|
||||
(Some(change), avatar_change) => {
|
||||
let mut m = match (change.old, change.new) {
|
||||
(None, Some(new)) => {
|
||||
vec![
|
||||
StyleTreeNode::Text("* set their display name to ".into()),
|
||||
StyleTreeNode::DisplayName(new.into(), state_key),
|
||||
]
|
||||
},
|
||||
(Some(old), Some(new)) => {
|
||||
vec![
|
||||
StyleTreeNode::Text(
|
||||
"* changed their display name from ".into(),
|
||||
),
|
||||
StyleTreeNode::DisplayName(old.into(), state_key.clone()),
|
||||
StyleTreeNode::Text(" to ".into()),
|
||||
StyleTreeNode::DisplayName(new.into(), state_key),
|
||||
]
|
||||
},
|
||||
(Some(_), None) => {
|
||||
vec![StyleTreeNode::Text("* unset their display name".into())]
|
||||
},
|
||||
(None, None) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* made an unknown change to their display name".into(),
|
||||
)]
|
||||
},
|
||||
};
|
||||
|
||||
if avatar_change.is_some() {
|
||||
m.push(StyleTreeNode::Text(
|
||||
" and changed their user avatar".into(),
|
||||
));
|
||||
}
|
||||
|
||||
m
|
||||
},
|
||||
(None, Some(change)) => {
|
||||
let m = match (change.old, change.new) {
|
||||
(None, Some(_)) => Cow::Borrowed("* added a user avatar"),
|
||||
(Some(_), Some(_)) => Cow::Borrowed("* changed their user avatar"),
|
||||
(Some(_), None) => Cow::Borrowed("* removed their user avatar"),
|
||||
(None, None) => {
|
||||
Cow::Borrowed("* made an unknown change to their user avatar")
|
||||
},
|
||||
};
|
||||
|
||||
vec![StyleTreeNode::Text(m)]
|
||||
},
|
||||
(None, None) => {
|
||||
vec![StyleTreeNode::Text("* changed their user profile".into())]
|
||||
},
|
||||
}
|
||||
},
|
||||
ev => {
|
||||
let prefix =
|
||||
StyleTreeNode::Text("* made an unknown membership change to ".into());
|
||||
let suffix = StyleTreeNode::Text(format!(": {:?}", ev).into());
|
||||
vec![prefix, user_id, suffix]
|
||||
},
|
||||
}
|
||||
},
|
||||
AnyFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => {
|
||||
let prefix = StyleTreeNode::Text("* updated the room name to ".into());
|
||||
let name = bold(format!("{:?}", content.name));
|
||||
vec![prefix, name]
|
||||
},
|
||||
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { .. }) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the pinned events for the room".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Original { .. }) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the power levels for the room".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Original { .. }) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room's server ACLs".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* sent a third-party invite to ".into());
|
||||
let name = bold(format!("{:?}", content.display_name));
|
||||
vec![prefix, name]
|
||||
},
|
||||
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Original {
|
||||
content,
|
||||
..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* upgraded the room; replacement room is ".into());
|
||||
let room = StyleTreeNode::RoomId(content.replacement_room.clone());
|
||||
vec![prefix, room]
|
||||
},
|
||||
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text("* set the room topic to ".into());
|
||||
let topic = bold(format!("{:?}", content.topic));
|
||||
vec![prefix, topic]
|
||||
},
|
||||
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Original { .. }) => {
|
||||
let prefix = StyleTreeNode::Text("* added a space child: ".into());
|
||||
|
||||
let room_id = if let Ok(room_id) = OwnedRoomId::from_str(ev.state_key()) {
|
||||
StyleTreeNode::RoomId(room_id)
|
||||
} else {
|
||||
bold(ev.state_key().to_string())
|
||||
};
|
||||
|
||||
vec![prefix, room_id]
|
||||
},
|
||||
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
let prefix = if content.canonical {
|
||||
StyleTreeNode::Text("* added a canonical parent space: ".into())
|
||||
} else {
|
||||
StyleTreeNode::Text("* added a parent space: ".into())
|
||||
};
|
||||
|
||||
let room_id = if let Ok(room_id) = OwnedRoomId::from_str(ev.state_key()) {
|
||||
StyleTreeNode::RoomId(room_id)
|
||||
} else {
|
||||
bold(ev.state_key().to_string())
|
||||
};
|
||||
|
||||
vec![prefix, room_id]
|
||||
},
|
||||
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Original { .. }) => {
|
||||
vec![StyleTreeNode::Text("* shared beacon information".into())]
|
||||
},
|
||||
AnyFullStateEventContent::CallMember(FullStateEventContent::Original { .. }) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated membership for room call".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::MemberHints(FullStateEventContent::Original {
|
||||
content, ..
|
||||
}) => {
|
||||
let prefix = StyleTreeNode::Text(
|
||||
"* updated the list of service members in the room hints: ".into(),
|
||||
);
|
||||
let mut cs = vec![prefix];
|
||||
|
||||
for (i, member) in content.service_members.iter().enumerate() {
|
||||
if i != 0 {
|
||||
cs.push(StyleTreeNode::Text(", ".into()));
|
||||
}
|
||||
|
||||
cs.push(StyleTreeNode::UserId(member.clone()));
|
||||
}
|
||||
|
||||
cs
|
||||
},
|
||||
|
||||
// Redacted variants of state events:
|
||||
AnyFullStateEventContent::PolicyRuleRoom(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated a room policy rule (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleServer(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated a server policy rule (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::PolicyRuleUser(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated a user policy rule (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomAliases(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room aliases for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomAvatar(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room avatar (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the canonical alias for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomCreate(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text("* created the room (redacted)".into())]
|
||||
},
|
||||
AnyFullStateEventContent::RoomEncryption(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the encryption settings for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomGuestAccess(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the guest access configuration for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated history visilibity for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomJoinRules(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the join rules for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomMember(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room membership (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomName(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room name (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the pinned events for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomPowerLevels(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the power levels for the room (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomServerAcl(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room's server ACLs (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomThirdPartyInvite(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* sent a third-party invite (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::RoomTombstone(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text("* upgraded the room (redacted)".into())]
|
||||
},
|
||||
AnyFullStateEventContent::RoomTopic(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* updated the room topic (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::SpaceChild(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* added a space child (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::SpaceParent(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* added a parent space (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::BeaconInfo(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text(
|
||||
"* shared beacon information (redacted)".into(),
|
||||
)]
|
||||
},
|
||||
AnyFullStateEventContent::CallMember(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text("Call membership changed".into())]
|
||||
},
|
||||
AnyFullStateEventContent::MemberHints(FullStateEventContent::Redacted(_)) => {
|
||||
vec![StyleTreeNode::Text("Member hints changed".into())]
|
||||
},
|
||||
|
||||
// Handle unknown events:
|
||||
e => {
|
||||
let prefix = StyleTreeNode::Text("* sent an unknown state event: ".into());
|
||||
let event = bold(format!("{:?}", e.event_type()));
|
||||
vec![prefix, event]
|
||||
},
|
||||
};
|
||||
|
||||
StyleTree { children }
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
notification_settings::{IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode},
|
||||
room::Room as MatrixRoom,
|
||||
ruma::{
|
||||
api::client::push::get_notifications::v3::Notification,
|
||||
events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent},
|
||||
serde::Raw,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
OwnedRoomId,
|
||||
RoomId,
|
||||
},
|
||||
Client,
|
||||
|
@ -25,21 +23,6 @@ const IAMB_XDG_NAME: &str = match option_env!("IAMB_XDG_NAME") {
|
|||
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(
|
||||
client: &Client,
|
||||
settings: &ApplicationSettings,
|
||||
|
@ -70,103 +53,51 @@ pub async fn register_notifications(
|
|||
return;
|
||||
}
|
||||
|
||||
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)) => {
|
||||
if server_ts < startup_ts {
|
||||
return;
|
||||
}
|
||||
match parse_notification(notification, room, show_message).await {
|
||||
Ok((summary, body, server_ts)) => {
|
||||
if server_ts < startup_ts {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_missing_mention(&body, mode, &client) {
|
||||
return;
|
||||
}
|
||||
if is_missing_mention(&body, mode, &client) {
|
||||
return;
|
||||
}
|
||||
|
||||
send_notification(
|
||||
¬ify_via,
|
||||
&summary,
|
||||
body.as_deref(),
|
||||
room_id,
|
||||
&store,
|
||||
)
|
||||
.await;
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to extract notification data: {err}")
|
||||
},
|
||||
match notify_via {
|
||||
#[cfg(feature = "desktop")]
|
||||
NotifyVia::Desktop => send_notification_desktop(summary, body),
|
||||
NotifyVia::Bell => send_notification_bell(&store).await,
|
||||
}
|
||||
},
|
||||
// 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(_) => (),
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to extract notification data: {err}")
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_notification(
|
||||
via: &NotifyVia,
|
||||
summary: &str,
|
||||
body: Option<&str>,
|
||||
room_id: OwnedRoomId,
|
||||
store: &AsyncProgramStore,
|
||||
) {
|
||||
#[cfg(feature = "desktop")]
|
||||
if via.desktop {
|
||||
send_notification_desktop(summary, body, room_id, store).await;
|
||||
}
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
{
|
||||
let _ = (summary, body, IAMB_XDG_NAME);
|
||||
}
|
||||
|
||||
if via.bell {
|
||||
send_notification_bell(store).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_notification_bell(store: &AsyncProgramStore) {
|
||||
let mut locked = store.lock().await;
|
||||
locked.application.ring_bell = true;
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
async fn send_notification_desktop(
|
||||
summary: &str,
|
||||
body: Option<&str>,
|
||||
room_id: OwnedRoomId,
|
||||
_store: &AsyncProgramStore,
|
||||
) {
|
||||
fn send_notification_desktop(summary: String, body: Option<String>) {
|
||||
let mut desktop_notification = notify_rust::Notification::new();
|
||||
desktop_notification
|
||||
.summary(summary)
|
||||
.summary(&summary)
|
||||
.appname(IAMB_XDG_NAME)
|
||||
.icon(IAMB_XDG_NAME)
|
||||
.action("default", "default");
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
desktop_notification.urgency(notify_rust::Urgency::Normal);
|
||||
|
||||
if let Some(body) = body {
|
||||
desktop_notification.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)));
|
||||
},
|
||||
if let Err(err) = desktop_notification.show() {
|
||||
tracing::error!("Failed to send notification: {err}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,12 +155,12 @@ async fn is_visible_room(store: &AsyncProgramStore, room_id: &RoomId) -> bool {
|
|||
is_focused(&locked) && is_open(&mut locked, room_id)
|
||||
}
|
||||
|
||||
pub async fn parse_full_notification(
|
||||
event: Raw<AnySyncTimelineEvent>,
|
||||
pub async fn parse_notification(
|
||||
notification: Notification,
|
||||
room: MatrixRoom,
|
||||
show_body: bool,
|
||||
) -> IambResult<(String, Option<String>, MilliSecondsSinceUnixEpoch)> {
|
||||
let event = event.deserialize().map_err(IambError::from)?;
|
||||
let event = notification.event.deserialize().map_err(IambError::from)?;
|
||||
|
||||
let server_ts = event.origin_server_ts();
|
||||
|
||||
|
@ -241,19 +172,19 @@ pub async fn parse_full_notification(
|
|||
.and_then(|m| m.display_name())
|
||||
.unwrap_or_else(|| sender_id.localpart());
|
||||
|
||||
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}")
|
||||
}
|
||||
let summary = if let Ok(room_name) = room.display_name().await {
|
||||
format!("{sender_name} in {room_name}")
|
||||
} else {
|
||||
sender_name.to_string()
|
||||
};
|
||||
|
||||
let body = if show_body {
|
||||
event_notification_body(&event, sender_name).map(truncate)
|
||||
event_notification_body(
|
||||
&event,
|
||||
sender_name,
|
||||
room.is_direct().await.map_err(IambError::from)?,
|
||||
)
|
||||
.map(truncate)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -261,7 +192,11 @@ pub async fn parse_full_notification(
|
|||
return Ok((summary, body, server_ts));
|
||||
}
|
||||
|
||||
pub fn event_notification_body(event: &AnySyncTimelineEvent, sender_name: &str) -> Option<String> {
|
||||
pub fn event_notification_body(
|
||||
event: &AnySyncTimelineEvent,
|
||||
sender_name: &str,
|
||||
is_direct: bool,
|
||||
) -> Option<String> {
|
||||
let AnySyncTimelineEvent::MessageLike(event) = event else {
|
||||
return None;
|
||||
};
|
||||
|
@ -272,7 +207,10 @@ pub fn event_notification_body(event: &AnySyncTimelineEvent, sender_name: &str)
|
|||
MessageType::Audio(_) => {
|
||||
format!("{sender_name} sent an audio file.")
|
||||
},
|
||||
MessageType::Emote(content) => content.body,
|
||||
MessageType::Emote(content) => {
|
||||
let message = &content.body;
|
||||
format!("{sender_name}: {message}")
|
||||
},
|
||||
MessageType::File(_) => {
|
||||
format!("{sender_name} sent a file.")
|
||||
},
|
||||
|
@ -282,9 +220,22 @@ pub fn event_notification_body(event: &AnySyncTimelineEvent, sender_name: &str)
|
|||
MessageType::Location(_) => {
|
||||
format!("{sender_name} sent their location.")
|
||||
},
|
||||
MessageType::Notice(content) => content.body,
|
||||
MessageType::ServerNotice(content) => content.body,
|
||||
MessageType::Text(content) => content.body,
|
||||
MessageType::Notice(content) => {
|
||||
let message = &content.body;
|
||||
format!("{sender_name}: {message}")
|
||||
},
|
||||
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(_) => {
|
||||
format!("{sender_name} sent a video.")
|
||||
},
|
||||
|
@ -303,7 +254,7 @@ pub fn event_notification_body(event: &AnySyncTimelineEvent, sender_name: &str)
|
|||
}
|
||||
|
||||
fn truncate(s: String) -> String {
|
||||
static MAX_LENGTH: usize = 5000;
|
||||
static MAX_LENGTH: usize = 100;
|
||||
if s.graphemes(true).count() > MAX_LENGTH {
|
||||
let truncated: String = s.graphemes(true).take(MAX_LENGTH).collect();
|
||||
truncated + "..."
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
|||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
media::{MediaFormat, MediaRequestParameters},
|
||||
media::{MediaFormat, MediaRequest},
|
||||
ruma::{
|
||||
events::{
|
||||
room::{
|
||||
|
@ -63,7 +63,7 @@ pub fn spawn_insert_preview(
|
|||
let img = download_or_load(event_id.to_owned(), source, media, cache_dir)
|
||||
.await
|
||||
.map(std::io::Cursor::new)
|
||||
.map(image::ImageReader::new)
|
||||
.map(image::io::Reader::new)
|
||||
.map_err(IambError::Matrix)
|
||||
.and_then(|reader| reader.with_guessed_format().map_err(IambError::IOError))
|
||||
.and_then(|reader| reader.decode().map_err(IambError::Image));
|
||||
|
@ -157,10 +157,7 @@ async fn download_or_load(
|
|||
},
|
||||
Err(_) => {
|
||||
media
|
||||
.get_media_content(
|
||||
&MediaRequestParameters { source, format: MediaFormat::File },
|
||||
true,
|
||||
)
|
||||
.get_media_content(&MediaRequest { source, format: MediaFormat::File }, true)
|
||||
.await
|
||||
.and_then(|buffer| {
|
||||
if let Err(err) =
|
||||
|
|
|
@ -137,7 +137,7 @@ pub fn mock_keys() -> HashMap<OwnedEventId, EventLocation> {
|
|||
}
|
||||
|
||||
pub fn mock_messages() -> Messages {
|
||||
let mut messages = Messages::main();
|
||||
let mut messages = Messages::default();
|
||||
|
||||
messages.insert(MSG1_KEY.clone(), mock_message1());
|
||||
messages.insert(MSG2_KEY.clone(), mock_message2());
|
||||
|
@ -171,14 +171,12 @@ pub fn mock_tunables() -> TunableValues {
|
|||
default_room: None,
|
||||
log_level: Level::INFO,
|
||||
message_shortcode_display: false,
|
||||
normal_after_send: true,
|
||||
reaction_display: true,
|
||||
reaction_shortcode_display: false,
|
||||
read_receipt_send: true,
|
||||
read_receipt_display: true,
|
||||
request_timeout: 120,
|
||||
sort: SortOverrides::default().values(),
|
||||
state_event_display: true,
|
||||
typing_notice_send: true,
|
||||
typing_notice_display: true,
|
||||
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
||||
|
@ -190,17 +188,14 @@ pub fn mock_tunables() -> TunableValues {
|
|||
open_command: None,
|
||||
external_edit_file_suffix: String::from(".md"),
|
||||
username_display: UserDisplayStyle::Username,
|
||||
username_display_regex: Some(String::from(".*")),
|
||||
message_user_color: false,
|
||||
mouse: Default::default(),
|
||||
notifications: Notifications {
|
||||
enabled: false,
|
||||
via: NotifyVia::default(),
|
||||
via: NotifyVia::Desktop,
|
||||
show_message: true,
|
||||
},
|
||||
image_preview: None,
|
||||
user_gutter_width: 30,
|
||||
tabstop: 4,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
//!
|
||||
//! 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],
|
||||
//! where we have the message bar and room ID easily accessible and resettable.
|
||||
//! where we have the message bar and room ID easily accesible and resetable.
|
||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::Deref;
|
||||
|
@ -23,7 +23,6 @@ use matrix_sdk::{
|
|||
RoomAliasId,
|
||||
RoomId,
|
||||
},
|
||||
RoomState as MatrixRoomState,
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
|
@ -76,13 +75,11 @@ use crate::base::{
|
|||
SortFieldRoom,
|
||||
SortFieldUser,
|
||||
SortOrder,
|
||||
SpaceAction,
|
||||
UnreadInfo,
|
||||
};
|
||||
|
||||
use self::{room::RoomState, welcome::WelcomeState};
|
||||
use crate::message::MessageTimeStamp;
|
||||
use feruca::Collator;
|
||||
|
||||
pub mod room;
|
||||
pub mod welcome;
|
||||
|
@ -171,12 +168,7 @@ fn user_cmp(a: &MemberItem, b: &MemberItem, field: &SortFieldUser) -> Ordering {
|
|||
}
|
||||
}
|
||||
|
||||
fn room_cmp<T: RoomLikeItem>(
|
||||
a: &T,
|
||||
b: &T,
|
||||
field: &SortFieldRoom,
|
||||
collator: &mut Collator,
|
||||
) -> Ordering {
|
||||
fn room_cmp<T: RoomLikeItem>(a: &T, b: &T, field: &SortFieldRoom) -> Ordering {
|
||||
match field {
|
||||
SortFieldRoom::Favorite => {
|
||||
let fava = a.has_tag(TagName::Favorite);
|
||||
|
@ -192,7 +184,7 @@ fn room_cmp<T: RoomLikeItem>(
|
|||
// If a has LowPriority and b doesn't, it should sort later in room list.
|
||||
lowa.cmp(&lowb)
|
||||
},
|
||||
SortFieldRoom::Name => collator.collate(a.name(), b.name()),
|
||||
SortFieldRoom::Name => a.name().cmp(b.name()),
|
||||
SortFieldRoom::Alias => some_cmp(a.alias(), b.alias(), Ord::cmp),
|
||||
SortFieldRoom::RoomId => a.room_id().cmp(b.room_id()),
|
||||
SortFieldRoom::Unread => {
|
||||
|
@ -203,10 +195,6 @@ fn room_cmp<T: RoomLikeItem>(
|
|||
// sort larger timestamps towards the top.
|
||||
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())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,10 +203,9 @@ fn room_fields_cmp<T: RoomLikeItem>(
|
|||
a: &T,
|
||||
b: &T,
|
||||
fields: &[SortColumn<SortFieldRoom>],
|
||||
collator: &mut Collator,
|
||||
) -> Ordering {
|
||||
for SortColumn(field, order) in fields {
|
||||
match (room_cmp(a, b, field, collator), order) {
|
||||
match (room_cmp(a, b, field), order) {
|
||||
(Ordering::Equal, _) => continue,
|
||||
(o, SortOrder::Ascending) => return o,
|
||||
(o, SortOrder::Descending) => return o.reverse(),
|
||||
|
@ -226,7 +213,7 @@ fn room_fields_cmp<T: RoomLikeItem>(
|
|||
}
|
||||
|
||||
// Break ties on ascending room id.
|
||||
room_cmp(a, b, &SortFieldRoom::RoomId, collator)
|
||||
room_cmp(a, b, &SortFieldRoom::RoomId)
|
||||
}
|
||||
|
||||
fn user_fields_cmp(
|
||||
|
@ -286,7 +273,6 @@ trait RoomLikeItem {
|
|||
fn recent_ts(&self) -> Option<&MessageTimeStamp>;
|
||||
fn alias(&self) -> Option<&RoomAliasId>;
|
||||
fn name(&self) -> &str;
|
||||
fn is_invite(&self) -> bool;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -368,19 +354,6 @@ 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(
|
||||
&mut self,
|
||||
act: RoomAction,
|
||||
|
@ -523,8 +496,7 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||
.map(|room_info| DirectItem::new(room_info, store))
|
||||
.collect::<Vec<_>>();
|
||||
let fields = &store.application.settings.tunables.sort.dms;
|
||||
let collator = &mut store.application.collator;
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
|
||||
state.set(items);
|
||||
|
||||
|
@ -569,8 +541,7 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||
.map(|room_info| RoomItem::new(room_info, store))
|
||||
.collect::<Vec<_>>();
|
||||
let fields = &store.application.settings.tunables.sort.rooms;
|
||||
let collator = &mut store.application.collator;
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
|
||||
state.set(items);
|
||||
|
||||
|
@ -601,8 +572,7 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||
items.extend(dms);
|
||||
|
||||
let fields = &store.application.settings.tunables.sort.chats;
|
||||
let collator = &mut store.application.collator;
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
|
||||
state.set(items);
|
||||
|
||||
|
@ -635,8 +605,7 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||
items.extend(dms);
|
||||
|
||||
let fields = &store.application.settings.tunables.sort.chats;
|
||||
let collator = &mut store.application.collator;
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
|
||||
state.set(items);
|
||||
|
||||
|
@ -656,8 +625,7 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||
.map(|room| SpaceItem::new(room, store))
|
||||
.collect::<Vec<_>>();
|
||||
let fields = &store.application.settings.tunables.sort.spaces;
|
||||
let collator = &mut store.application.collator;
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
|
||||
state.set(items);
|
||||
|
||||
|
@ -946,10 +914,6 @@ impl RoomLikeItem for GenericChatItem {
|
|||
fn is_unread(&self) -> bool {
|
||||
self.unread.is_unread()
|
||||
}
|
||||
|
||||
fn is_invite(&self) -> bool {
|
||||
self.room().state() == MatrixRoomState::Invited
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for GenericChatItem {
|
||||
|
@ -1060,15 +1024,11 @@ impl RoomLikeItem for RoomItem {
|
|||
fn is_unread(&self) -> bool {
|
||||
self.unread.is_unread()
|
||||
}
|
||||
|
||||
fn is_invite(&self) -> bool {
|
||||
self.room().state() == MatrixRoomState::Invited
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RoomItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
write!(f, ":verify request {}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1164,10 +1124,6 @@ impl RoomLikeItem for DirectItem {
|
|||
fn is_unread(&self) -> bool {
|
||||
self.unread.is_unread()
|
||||
}
|
||||
|
||||
fn is_invite(&self) -> bool {
|
||||
self.room().state() == MatrixRoomState::Invited
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DirectItem {
|
||||
|
@ -1267,15 +1223,11 @@ impl RoomLikeItem for SpaceItem {
|
|||
// XXX: this needs to check whether the space contains rooms with unread messages
|
||||
false
|
||||
}
|
||||
|
||||
fn is_invite(&self) -> bool {
|
||||
self.room().state() == MatrixRoomState::Invited
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SpaceItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
write!(f, ":verify request {}", self.room_id())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1604,7 +1556,6 @@ mod tests {
|
|||
alias: Option<OwnedRoomAliasId>,
|
||||
name: &'static str,
|
||||
unread: UnreadInfo,
|
||||
invite: bool,
|
||||
}
|
||||
|
||||
impl RoomLikeItem for &TestRoomItem {
|
||||
|
@ -1631,16 +1582,10 @@ mod tests {
|
|||
fn is_unread(&self) -> bool {
|
||||
self.unread.is_unread()
|
||||
}
|
||||
|
||||
fn is_invite(&self) -> bool {
|
||||
self.invite
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_rooms() {
|
||||
let mut collator = Collator::default();
|
||||
let collator = &mut collator;
|
||||
let server = server_name!("example.com");
|
||||
|
||||
let room1 = TestRoomItem {
|
||||
|
@ -1649,7 +1594,6 @@ mod tests {
|
|||
alias: Some(room_alias_id!("#room1:example.com").to_owned()),
|
||||
name: "Z",
|
||||
unread: UnreadInfo::default(),
|
||||
invite: false,
|
||||
};
|
||||
|
||||
let room2 = TestRoomItem {
|
||||
|
@ -1658,7 +1602,6 @@ mod tests {
|
|||
alias: Some(room_alias_id!("#a:example.com").to_owned()),
|
||||
name: "Unnamed Room",
|
||||
unread: UnreadInfo::default(),
|
||||
invite: false,
|
||||
};
|
||||
|
||||
let room3 = TestRoomItem {
|
||||
|
@ -1667,19 +1610,18 @@ mod tests {
|
|||
alias: None,
|
||||
name: "Cool Room",
|
||||
unread: UnreadInfo::default(),
|
||||
invite: false,
|
||||
};
|
||||
|
||||
// Sort by Name ascending.
|
||||
let mut rooms = vec![&room1, &room2, &room3];
|
||||
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Ascending)];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
assert_eq!(rooms, vec![&room3, &room2, &room1]);
|
||||
|
||||
// Sort by Name descending.
|
||||
let mut rooms = vec![&room1, &room2, &room3];
|
||||
let fields = &[SortColumn(SortFieldRoom::Name, SortOrder::Descending)];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
||||
|
||||
// Sort by Favorite and Alias before Name to show order matters.
|
||||
|
@ -1689,7 +1631,7 @@ mod tests {
|
|||
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||
];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
||||
|
||||
// Now flip order of Favorite with Descending
|
||||
|
@ -1699,14 +1641,12 @@ mod tests {
|
|||
SortColumn(SortFieldRoom::Alias, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||
];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
assert_eq!(rooms, vec![&room2, &room3, &room1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_room_recents() {
|
||||
let mut collator = Collator::default();
|
||||
let collator = &mut collator;
|
||||
let server = server_name!("example.com");
|
||||
|
||||
let room1 = TestRoomItem {
|
||||
|
@ -1715,7 +1655,6 @@ mod tests {
|
|||
alias: None,
|
||||
name: "Room 1",
|
||||
unread: UnreadInfo { unread: false, latest: None },
|
||||
invite: false,
|
||||
};
|
||||
|
||||
let room2 = TestRoomItem {
|
||||
|
@ -1727,7 +1666,6 @@ mod tests {
|
|||
unread: false,
|
||||
latest: Some(MessageTimeStamp::OriginServer(40u32.into())),
|
||||
},
|
||||
invite: false,
|
||||
};
|
||||
|
||||
let room3 = TestRoomItem {
|
||||
|
@ -1739,71 +1677,18 @@ mod tests {
|
|||
unread: false,
|
||||
latest: Some(MessageTimeStamp::OriginServer(20u32.into())),
|
||||
},
|
||||
invite: false,
|
||||
};
|
||||
|
||||
// Sort by Recent ascending.
|
||||
let mut rooms = vec![&room1, &room2, &room3];
|
||||
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Ascending)];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
assert_eq!(rooms, vec![&room2, &room3, &room1]);
|
||||
|
||||
// Sort by Recent descending.
|
||||
let mut rooms = vec![&room1, &room2, &room3];
|
||||
let fields = &[SortColumn(SortFieldRoom::Recent, SortOrder::Descending)];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
assert_eq!(rooms, vec![&room1, &room3, &room2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_room_invites() {
|
||||
let mut collator = Collator::default();
|
||||
let collator = &mut collator;
|
||||
let server = server_name!("example.com");
|
||||
|
||||
let room1 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Old room 1",
|
||||
unread: UnreadInfo::default(),
|
||||
invite: false,
|
||||
};
|
||||
|
||||
let room2 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Old room 2",
|
||||
unread: UnreadInfo::default(),
|
||||
invite: false,
|
||||
};
|
||||
|
||||
let room3 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "New Fancy Room",
|
||||
unread: UnreadInfo::default(),
|
||||
invite: true,
|
||||
};
|
||||
|
||||
// Sort invites first
|
||||
let mut rooms = vec![&room1, &room2, &room3];
|
||||
let fields = &[
|
||||
SortColumn(SortFieldRoom::Invite, SortOrder::Ascending),
|
||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||
];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
assert_eq!(rooms, vec![&room3, &room1, &room2]);
|
||||
|
||||
// Sort invites after
|
||||
let mut rooms = vec![&room1, &room2, &room3];
|
||||
let fields = &[
|
||||
SortColumn(SortFieldRoom::Invite, SortOrder::Descending),
|
||||
SortColumn(SortFieldRoom::Name, SortOrder::Ascending),
|
||||
];
|
||||
rooms.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
assert_eq!(rooms, vec![&room1, &room2, &room3]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use url::Url;
|
|||
|
||||
use matrix_sdk::{
|
||||
attachment::AttachmentConfig,
|
||||
media::{MediaFormat, MediaRequestParameters},
|
||||
media::{MediaFormat, MediaRequest},
|
||||
room::Room as MatrixRoom,
|
||||
ruma::{
|
||||
events::reaction::ReactionEventContent,
|
||||
|
@ -86,14 +86,7 @@ use crate::base::{
|
|||
SendAction,
|
||||
};
|
||||
|
||||
use crate::message::{
|
||||
text_to_message,
|
||||
Message,
|
||||
MessageEvent,
|
||||
MessageKey,
|
||||
MessageTimeStamp,
|
||||
TreeGenState,
|
||||
};
|
||||
use crate::message::{text_to_message, Message, MessageEvent, MessageKey, MessageTimeStamp};
|
||||
use crate::worker::Requester;
|
||||
|
||||
use super::scrollback::{Scrollback, ScrollbackState};
|
||||
|
@ -233,14 +226,10 @@ impl ChatState {
|
|||
|
||||
let links = if let Some(html) = &msg.html {
|
||||
html.get_links()
|
||||
} else if let Ok(url) = Url::parse(&msg.event.body()) {
|
||||
vec![('0', url)]
|
||||
} else {
|
||||
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()
|
||||
vec![]
|
||||
};
|
||||
|
||||
if links.is_empty() {
|
||||
|
@ -287,7 +276,7 @@ impl ChatState {
|
|||
}
|
||||
|
||||
if !filename.exists() || flags.contains(DownloadFlags::FORCE) {
|
||||
let req = MediaRequestParameters { source, format: MediaFormat::File };
|
||||
let req = MediaRequest { source, format: MediaFormat::File };
|
||||
|
||||
let bytes =
|
||||
media.get_media_content(&req, true).await.map_err(IambError::from)?;
|
||||
|
@ -391,7 +380,6 @@ impl ChatState {
|
|||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::State(ev) => ev.event_id().to_owned(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "Cannot react to a redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
@ -429,7 +417,6 @@ impl ChatState {
|
|||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::State(ev) => ev.event_id().to_owned(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "Cannot redact already redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
@ -477,7 +464,6 @@ impl ChatState {
|
|||
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::State(ev) => ev.event_id().to_owned(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
let msg = "Cannot unreact to a redacted message";
|
||||
let err = UIError::Failure(msg.into());
|
||||
|
@ -610,7 +596,7 @@ impl ChatState {
|
|||
let dynimage = image::DynamicImage::ImageRgba8(imagebuf);
|
||||
let bytes = Vec::<u8>::new();
|
||||
let mut buff = std::io::Cursor::new(bytes);
|
||||
dynimage.write_to(&mut buff, image::ImageFormat::Png)?;
|
||||
dynimage.write_to(&mut buff, image::ImageOutputFormat::Png)?;
|
||||
Ok(buff.into_inner())
|
||||
})
|
||||
.map_err(IambError::from)?;
|
||||
|
@ -620,7 +606,7 @@ impl ChatState {
|
|||
let config = AttachmentConfig::new();
|
||||
|
||||
let resp = room
|
||||
.send_attachment(name, &mime, bytes, config)
|
||||
.send_attachment(name.as_ref(), &mime, bytes, config)
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
|
||||
|
@ -649,7 +635,10 @@ impl ChatState {
|
|||
}
|
||||
|
||||
pub fn focus_toggle(&mut self) {
|
||||
self.focus.toggle();
|
||||
self.focus = match self.focus {
|
||||
RoomFocus::Scrollback => RoomFocus::MessageBar,
|
||||
RoomFocus::MessageBar => RoomFocus::Scrollback,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn room(&self) -> &MatrixRoom {
|
||||
|
@ -660,14 +649,6 @@ impl ChatState {
|
|||
&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(
|
||||
&self,
|
||||
act: &EditorAction,
|
||||
|
@ -770,15 +751,8 @@ impl Editable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|||
ctx: &ProgramContext,
|
||||
store: &mut ProgramStore,
|
||||
) -> 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);
|
||||
|
||||
// And now we can finally run the editor command.
|
||||
match delegate!(self, w => w.editor_command(act, ctx, store)) {
|
||||
res @ Ok(_) => res,
|
||||
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, thread, focus)))
|
||||
|
@ -875,16 +849,16 @@ impl PromptActions<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|||
|
||||
fn recall(
|
||||
&mut self,
|
||||
filter: &RecallFilter,
|
||||
dir: &MoveDir1D,
|
||||
count: &Count,
|
||||
prefixed: bool,
|
||||
ctx: &ProgramContext,
|
||||
_: &mut ProgramStore,
|
||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||
let count = ctx.resolve(count);
|
||||
let rope = self.tbox.get();
|
||||
|
||||
let text = self.sent.recall(&rope, &mut self.sent_scrollback, filter, *dir, count);
|
||||
let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, prefixed, count);
|
||||
|
||||
if let Some(text) = text {
|
||||
self.tbox.set_text(text);
|
||||
|
@ -908,7 +882,9 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|||
match act {
|
||||
PromptAction::Submit => self.submit(ctx, store),
|
||||
PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
|
||||
PromptAction::Recall(filter, dir, count) => self.recall(filter, dir, count, ctx, store),
|
||||
PromptAction::Recall(dir, count, prefixed) => {
|
||||
self.recall(dir, count, *prefixed, ctx, store)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -930,7 +906,7 @@ impl<'a> Chat<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for Chat<'_> {
|
||||
impl<'a> StatefulWidget for Chat<'a> {
|
||||
type State = ChatState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
|
@ -1014,158 +990,3 @@ fn cmd(open_command: &Vec<String>) -> Option<Command> {
|
|||
}
|
||||
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,
|
||||
RoomId,
|
||||
},
|
||||
RoomDisplayName,
|
||||
DisplayName,
|
||||
RoomState as MatrixRoomState,
|
||||
};
|
||||
|
||||
|
@ -66,7 +66,6 @@ use crate::base::{
|
|||
RoomAction,
|
||||
RoomField,
|
||||
SendAction,
|
||||
SpaceAction,
|
||||
};
|
||||
|
||||
use self::chat::ChatState;
|
||||
|
@ -140,7 +139,7 @@ impl RoomState {
|
|||
pub fn new(
|
||||
room: MatrixRoom,
|
||||
thread: Option<OwnedEventId>,
|
||||
name: RoomDisplayName,
|
||||
name: DisplayName,
|
||||
tags: Option<Tags>,
|
||||
store: &mut ProgramStore,
|
||||
) -> Self {
|
||||
|
@ -215,18 +214,6 @@ 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(
|
||||
&mut self,
|
||||
act: SendAction,
|
||||
|
@ -419,7 +406,7 @@ impl RoomState {
|
|||
// Try creating the room alias on the server.
|
||||
let alias_create_req =
|
||||
CreateAliasRequest::new(orai.clone(), room.room_id().into());
|
||||
if let Err(e) = client.send(alias_create_req).await {
|
||||
if let Err(e) = client.send(alias_create_req, None).await {
|
||||
if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() {
|
||||
// Ignore when it already exists.
|
||||
} else {
|
||||
|
@ -460,7 +447,7 @@ impl RoomState {
|
|||
|
||||
// If the room alias does not exist on the server, create it
|
||||
let alias_create_req = CreateAliasRequest::new(orai, room.room_id().into());
|
||||
if let Err(e) = client.send(alias_create_req).await {
|
||||
if let Err(e) = client.send(alias_create_req, None).await {
|
||||
if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() {
|
||||
// Ignore when it already exists.
|
||||
} else {
|
||||
|
@ -477,9 +464,6 @@ impl RoomState {
|
|||
RoomField::Aliases => {
|
||||
// This never happens, aliases is only used for showing
|
||||
},
|
||||
RoomField::Id => {
|
||||
// This never happens, id is only used for showing
|
||||
},
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
|
@ -535,7 +519,7 @@ impl RoomState {
|
|||
.application
|
||||
.worker
|
||||
.client
|
||||
.send(del_req)
|
||||
.send(del_req, None)
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
},
|
||||
|
@ -568,16 +552,13 @@ impl RoomState {
|
|||
.application
|
||||
.worker
|
||||
.client
|
||||
.send(del_req)
|
||||
.send(del_req, None)
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
},
|
||||
RoomField::Aliases => {
|
||||
// This will not happen, you cannot unset all aliases
|
||||
},
|
||||
RoomField::Id => {
|
||||
// This never happens, id is only used for showing
|
||||
},
|
||||
}
|
||||
|
||||
Ok(vec![])
|
||||
|
@ -591,12 +572,7 @@ impl RoomState {
|
|||
let msg = match field {
|
||||
RoomField::History => {
|
||||
let visibility = room.history_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}")
|
||||
format!("Room history visibility: {visibility}")
|
||||
},
|
||||
RoomField::Name => {
|
||||
match room.name() {
|
||||
|
|
|
@ -79,20 +79,14 @@ fn nth_key_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
|||
}
|
||||
|
||||
fn nth_before(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
||||
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)
|
||||
}
|
||||
nth_key_before(pos, n, thread).into()
|
||||
}
|
||||
|
||||
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> Option<MessageKey> {
|
||||
fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageKey {
|
||||
let mut end = &pos;
|
||||
let mut iter = thread.range(&pos..).enumerate();
|
||||
let iter = thread.range(&pos..).enumerate();
|
||||
|
||||
for (i, (key, _)) in iter.by_ref() {
|
||||
for (i, (key, _)) in iter {
|
||||
end = key;
|
||||
|
||||
if i >= n {
|
||||
|
@ -100,12 +94,11 @@ fn nth_key_after(pos: MessageKey, n: usize, thread: &Messages) -> Option<Message
|
|||
}
|
||||
}
|
||||
|
||||
// Avoid returning the key if it's at the end.
|
||||
iter.next().map(|_| end.clone())
|
||||
end.clone()
|
||||
}
|
||||
|
||||
fn nth_after(pos: MessageKey, n: usize, thread: &Messages) -> MessageCursor {
|
||||
nth_key_after(pos, n, thread).map(MessageCursor::from).unwrap_or_default()
|
||||
nth_key_after(pos, n, thread).into()
|
||||
}
|
||||
|
||||
fn prevmsg<'a>(key: &MessageKey, thread: &'a Messages) -> Option<&'a Message> {
|
||||
|
@ -157,10 +150,6 @@ impl ScrollbackState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_latest(&self) -> bool {
|
||||
self.cursor.timestamp.is_none()
|
||||
}
|
||||
|
||||
pub fn goto_latest(&mut self) {
|
||||
self.cursor = MessageCursor::latest();
|
||||
}
|
||||
|
@ -840,8 +829,8 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
|||
|
||||
fn complete(
|
||||
&mut self,
|
||||
_: &CompletionStyle,
|
||||
_: &CompletionType,
|
||||
_: &CompletionSelection,
|
||||
_: &CompletionDisplay,
|
||||
_: &ProgramContext,
|
||||
_: &mut ProgramStore,
|
||||
|
@ -1295,7 +1284,7 @@ impl<'a> Scrollback<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for Scrollback<'_> {
|
||||
impl<'a> StatefulWidget for Scrollback<'a> {
|
||||
type State = ScrollbackState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
|
@ -1351,7 +1340,7 @@ impl StatefulWidget for Scrollback<'_> {
|
|||
|
||||
for (key, item) in thread.range(&corner_key..) {
|
||||
let sel = key == cursor_key;
|
||||
let (txt, [mut msg_preview, mut reply_preview]) =
|
||||
let (txt, mut msg_preview) =
|
||||
item.show_with_preview(prev, foc && sel, &state.viewctx, info, settings);
|
||||
|
||||
let incomplete_ok = !full || !sel;
|
||||
|
@ -1368,17 +1357,11 @@ impl StatefulWidget for Scrollback<'_> {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Only take the preview into the matching row number.
|
||||
// `reply` and `msg` previews are on rows,
|
||||
// so an `or` works to pick the one that matches (if any)
|
||||
let line_preview = match msg_preview {
|
||||
// Only take the preview into the matching row number.
|
||||
Some((_, _, y)) if y as usize == row => msg_preview.take(),
|
||||
_ => None,
|
||||
}
|
||||
.or(match reply_preview {
|
||||
Some((_, _, y)) if y as usize == row => reply_preview.take(),
|
||||
_ => None,
|
||||
});
|
||||
};
|
||||
|
||||
lines.push((key, row, line, line_preview));
|
||||
sawit |= sel;
|
||||
|
@ -1413,7 +1396,7 @@ impl StatefulWidget for Scrollback<'_> {
|
|||
// line.
|
||||
for (x, y, backend) in image_previews {
|
||||
let image_widget = Image::new(backend);
|
||||
let mut rect = backend.area();
|
||||
let mut rect = backend.rect();
|
||||
rect.x = x;
|
||||
rect.y = y;
|
||||
// Don't render outside of scrollback area
|
||||
|
@ -1428,7 +1411,7 @@ impl StatefulWidget for Scrollback<'_> {
|
|||
{
|
||||
// If the cursor is at the last message, then update the read marker.
|
||||
if let Some((k, _)) = thread.last_key_value() {
|
||||
info.set_receipt(thread.1.clone(), settings.profile.user_id.clone(), k.1.clone());
|
||||
info.set_receipt(settings.profile.user_id.clone(), k.1.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1535,9 +1518,8 @@ mod tests {
|
|||
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
||||
assert_eq!(scrollback.cursor, MSG5_KEY.clone().into());
|
||||
|
||||
// And one more becomes "latest" cursor:
|
||||
scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap();
|
||||
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
||||
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1571,7 +1553,7 @@ mod tests {
|
|||
// MSG1: | XXXday, Month NN 20XX |
|
||||
// | @user1:example.com writhe |
|
||||
// |------------------------------------------------------------|
|
||||
let area = Rect::new(0, 0, 60, 5);
|
||||
let area = Rect::new(0, 0, 60, 4);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
scrollback.draw(area, &mut buffer, true, &mut store);
|
||||
|
||||
|
|
|
@ -2,14 +2,11 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
|
||||
use matrix_sdk::ruma::events::StateEventType;
|
||||
use matrix_sdk::{
|
||||
room::Room as MatrixRoom,
|
||||
ruma::{OwnedRoomId, RoomId},
|
||||
};
|
||||
|
||||
use modalkit::prelude::{EditInfo, InfoMessage};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
|
@ -25,18 +22,9 @@ use modalkit_ratatui::{
|
|||
WindowOps,
|
||||
};
|
||||
|
||||
use crate::base::{
|
||||
IambBufferId,
|
||||
IambError,
|
||||
IambInfo,
|
||||
IambResult,
|
||||
ProgramContext,
|
||||
ProgramStore,
|
||||
RoomFocus,
|
||||
SpaceAction,
|
||||
};
|
||||
use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus};
|
||||
|
||||
use crate::windows::{room_fields_cmp, RoomItem, RoomLikeItem};
|
||||
use crate::windows::{room_fields_cmp, RoomItem};
|
||||
|
||||
const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(5);
|
||||
|
||||
|
@ -80,71 +68,6 @@ impl SpaceState {
|
|||
last_fetch: self.last_fetch,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn space_command(
|
||||
&mut self,
|
||||
act: SpaceAction,
|
||||
_: ProgramContext,
|
||||
store: &mut ProgramStore,
|
||||
) -> IambResult<EditInfo> {
|
||||
match act {
|
||||
SpaceAction::SetChild(child_id, order, suggested) => {
|
||||
if !self
|
||||
.room
|
||||
.can_user_send_state(
|
||||
&store.application.settings.profile.user_id,
|
||||
StateEventType::SpaceChild,
|
||||
)
|
||||
.await
|
||||
.map_err(IambError::from)?
|
||||
{
|
||||
return Err(IambError::InsufficientPermission.into());
|
||||
}
|
||||
|
||||
let via = self.room.route().await.map_err(IambError::from)?;
|
||||
let mut ev = SpaceChildEventContent::new(via);
|
||||
ev.order = order;
|
||||
ev.suggested = suggested;
|
||||
let _ = self
|
||||
.room
|
||||
.send_state_event_for_key(&child_id, ev)
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
|
||||
Ok(InfoMessage::from("Space updated").into())
|
||||
},
|
||||
SpaceAction::RemoveChild => {
|
||||
let space = self.list.get().ok_or(IambError::NoSelectedRoomOrSpaceItem)?;
|
||||
if !self
|
||||
.room
|
||||
.can_user_send_state(
|
||||
&store.application.settings.profile.user_id,
|
||||
StateEventType::SpaceChild,
|
||||
)
|
||||
.await
|
||||
.map_err(IambError::from)?
|
||||
{
|
||||
return Err(IambError::InsufficientPermission.into());
|
||||
}
|
||||
|
||||
let ev = SpaceChildEventContent::new(vec![]);
|
||||
let event_id = self
|
||||
.room
|
||||
.send_state_event_for_key(&space.room_id().to_owned(), ev)
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
|
||||
// Fix for element (see https://github.com/element-hq/element-web/issues/29606)
|
||||
let _ = self
|
||||
.room
|
||||
.redact(&event_id.event_id, Some("workaround for element bug"), None)
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
|
||||
Ok(InfoMessage::from("Room removed").into())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalCursor for SpaceState {
|
||||
|
@ -184,7 +107,7 @@ impl<'a> Space<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for Space<'_> {
|
||||
impl<'a> StatefulWidget for Space<'a> {
|
||||
type State = SpaceState;
|
||||
|
||||
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) {
|
||||
|
@ -214,8 +137,7 @@ impl StatefulWidget for Space<'_> {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
let fields = &self.store.application.settings.tunables.sort.rooms;
|
||||
let collator = &mut self.store.application.collator;
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator));
|
||||
items.sort_by(|a, b| room_fields_cmp(a, b, fields));
|
||||
|
||||
state.list.set(items);
|
||||
state.last_fetch = Some(Instant::now());
|
||||
|
|
129
src/worker.rs
129
src/worker.rs
|
@ -20,12 +20,11 @@ use tracing::{error, warn};
|
|||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
authentication::matrix::MatrixSession,
|
||||
config::{RequestConfig, SyncSettings},
|
||||
deserialized_responses::DisplayName,
|
||||
encryption::verification::{SasVerification, Verification},
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_handler::Ctx,
|
||||
matrix_auth::MatrixSession,
|
||||
reqwest,
|
||||
room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
|
||||
ruma::{
|
||||
|
@ -59,7 +58,6 @@ use matrix_sdk::{
|
|||
typing::SyncTypingEvent,
|
||||
AnyInitialStateEvent,
|
||||
AnyMessageLikeEvent,
|
||||
AnySyncStateEvent,
|
||||
AnyTimelineEvent,
|
||||
EmptyStateKey,
|
||||
InitialStateEvent,
|
||||
|
@ -80,8 +78,8 @@ use matrix_sdk::{
|
|||
},
|
||||
Client,
|
||||
ClientBuildError,
|
||||
DisplayName,
|
||||
Error as MatrixError,
|
||||
RoomDisplayName,
|
||||
RoomMemberships,
|
||||
};
|
||||
|
||||
|
@ -116,7 +114,8 @@ const IAMB_DEVICE_NAME: &str = "iamb";
|
|||
const IAMB_USER_AGENT: &str = "iamb";
|
||||
const MIN_MSG_LOAD: u32 = 50;
|
||||
|
||||
type MessageFetchResult = IambResult<(Option<String>, Vec<(AnyTimelineEvent, Vec<OwnedUserId>)>)>;
|
||||
type MessageFetchResult =
|
||||
IambResult<(Option<String>, Vec<(AnyMessageLikeEvent, Vec<OwnedUserId>)>)>;
|
||||
|
||||
fn initial_devname() -> String {
|
||||
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
||||
|
@ -210,7 +209,7 @@ async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id:
|
|||
};
|
||||
|
||||
for (user_id, _) in receipts {
|
||||
info.set_receipt(ReceiptThread::Main, user_id, event_id.to_owned());
|
||||
info.set_receipt(user_id, event_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,8 +293,10 @@ async fn load_older_one(
|
|||
let mut msgs = vec![];
|
||||
|
||||
for ev in chunk.into_iter() {
|
||||
let Ok(msg) = ev.into_raw().deserialize() else {
|
||||
continue;
|
||||
let msg = match ev.event.deserialize() {
|
||||
Ok(AnyTimelineEvent::MessageLike(msg)) => msg,
|
||||
Ok(AnyTimelineEvent::State(_)) => continue,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let event_id = msg.event_id();
|
||||
|
@ -310,7 +311,6 @@ async fn load_older_one(
|
|||
},
|
||||
};
|
||||
|
||||
let msg = msg.into_full_event(room_id.to_owned());
|
||||
msgs.push((msg, receipts));
|
||||
}
|
||||
|
||||
|
@ -338,34 +338,27 @@ fn load_insert(
|
|||
let _ = presences.get_or_default(sender);
|
||||
|
||||
for user_id in receipts {
|
||||
info.set_receipt(ReceiptThread::Main, user_id, msg.event_id().to_owned());
|
||||
info.set_receipt(user_id, msg.event_id().to_owned());
|
||||
}
|
||||
|
||||
match msg {
|
||||
AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomEncrypted(msg)) => {
|
||||
AnyMessageLikeEvent::RoomEncrypted(msg) => {
|
||||
info.insert_encrypted(msg);
|
||||
},
|
||||
AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(msg)) => {
|
||||
AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||
info.insert_with_preview(
|
||||
room_id.clone(),
|
||||
store.clone(),
|
||||
picker.clone(),
|
||||
*picker,
|
||||
msg,
|
||||
settings,
|
||||
client.media(),
|
||||
);
|
||||
},
|
||||
AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::Reaction(ev)) => {
|
||||
AnyMessageLikeEvent::Reaction(ev) => {
|
||||
info.insert_reaction(ev);
|
||||
},
|
||||
AnyTimelineEvent::MessageLike(_) => {
|
||||
continue;
|
||||
},
|
||||
AnyTimelineEvent::State(msg) => {
|
||||
if settings.tunables.state_event_display {
|
||||
info.insert_any_state(msg.into());
|
||||
}
|
||||
},
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -447,7 +440,7 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
|
|||
let mut dms = vec![];
|
||||
|
||||
for room in client.invited_rooms().into_iter() {
|
||||
let name = room.cached_display_name().unwrap_or(RoomDisplayName::Empty).to_string();
|
||||
let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string();
|
||||
let tags = room.tags().await.unwrap_or_default();
|
||||
|
||||
names.push((room.room_id().to_owned(), name));
|
||||
|
@ -462,7 +455,7 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
|
|||
}
|
||||
|
||||
for room in client.joined_rooms().into_iter() {
|
||||
let name = room.cached_display_name().unwrap_or(RoomDisplayName::Empty).to_string();
|
||||
let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string();
|
||||
let tags = room.tags().await.unwrap_or_default();
|
||||
|
||||
names.push((room.room_id().to_owned(), name));
|
||||
|
@ -497,36 +490,31 @@ async fn refresh_rooms_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 sent: HashMap<OwnedRoomId, HashMap<ReceiptThread, OwnedEventId>> = Default::default();
|
||||
let mut sent = HashMap::<OwnedRoomId, OwnedEventId>::default();
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let mut locked = store.lock().await;
|
||||
let ChatStore { settings, open_notifications, rooms, .. } = &mut locked.application;
|
||||
let user_id = &settings.profile.user_id;
|
||||
|
||||
let mut updates = Vec::new();
|
||||
for room in client.joined_rooms() {
|
||||
let room_id = room.room_id();
|
||||
let Some(info) = rooms.get(room_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let changed = info.receipts(user_id).filter_map(|(thread, new_receipt)| {
|
||||
let old_receipt = sent.get(room_id).and_then(|ts| ts.get(thread));
|
||||
let changed = Some(new_receipt) != old_receipt;
|
||||
if changed {
|
||||
open_notifications.remove(room_id);
|
||||
let locked = store.lock().await;
|
||||
let user_id = &locked.application.settings.profile.user_id;
|
||||
let updates = client
|
||||
.joined_rooms()
|
||||
.into_iter()
|
||||
.filter_map(|room| {
|
||||
let room_id = room.room_id().to_owned();
|
||||
let info = locked.application.rooms.get(&room_id)?;
|
||||
let new_receipt = info.get_receipt(user_id)?;
|
||||
let old_receipt = sent.get(&room_id);
|
||||
if Some(new_receipt) != old_receipt {
|
||||
Some((room_id, new_receipt.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
changed.then(|| (room_id.to_owned(), thread.to_owned(), new_receipt.to_owned()))
|
||||
});
|
||||
|
||||
updates.extend(changed);
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
drop(locked);
|
||||
|
||||
for (room_id, thread, new_receipt) in updates {
|
||||
for (room_id, new_receipt) in updates {
|
||||
use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
|
||||
|
||||
let Some(room) = client.get_room(&room_id) else {
|
||||
|
@ -534,11 +522,15 @@ async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
|
|||
};
|
||||
|
||||
match room
|
||||
.send_single_receipt(ReceiptType::Read, thread.to_owned(), new_receipt.clone())
|
||||
.send_single_receipt(
|
||||
ReceiptType::Read,
|
||||
ReceiptThread::Unthreaded,
|
||||
new_receipt.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
sent.entry(room_id).or_default().insert(thread, new_receipt);
|
||||
sent.insert(room_id, new_receipt);
|
||||
},
|
||||
Err(e) => tracing::warn!(?room_id, "Failed to set read receipt: {e}"),
|
||||
}
|
||||
|
@ -611,7 +603,7 @@ fn oneshot<T>() -> (ClientReply<T>, ClientResponse<T>) {
|
|||
return (reply, response);
|
||||
}
|
||||
|
||||
pub type FetchedRoom = (MatrixRoom, RoomDisplayName, Option<Tags>);
|
||||
pub type FetchedRoom = (MatrixRoom, DisplayName, Option<Tags>);
|
||||
|
||||
pub enum WorkerTask {
|
||||
Init(AsyncProgramStore, ClientReply<()>),
|
||||
|
@ -1009,7 +1001,7 @@ impl ClientWorker {
|
|||
info.insert_with_preview(
|
||||
room_id.to_owned(),
|
||||
store.clone(),
|
||||
picker.clone(),
|
||||
*picker,
|
||||
full_ev,
|
||||
settings,
|
||||
client.media(),
|
||||
|
@ -1051,32 +1043,14 @@ impl ClientWorker {
|
|||
let Some(receipts) = receipts.get(&ReceiptType::Read) else {
|
||||
continue;
|
||||
};
|
||||
for (user_id, rcpt) in receipts.iter() {
|
||||
info.set_receipt(
|
||||
rcpt.thread.clone(),
|
||||
user_id.to_owned(),
|
||||
event_id.clone(),
|
||||
);
|
||||
for user_id in receipts.keys() {
|
||||
info.set_receipt(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(
|
||||
|ev: OriginalSyncRoomRedactionEvent,
|
||||
room: MatrixRoom,
|
||||
|
@ -1102,12 +1076,11 @@ impl ClientWorker {
|
|||
let room_id = room.room_id();
|
||||
let user_id = ev.state_key;
|
||||
|
||||
let ambiguous_name = DisplayName::new(
|
||||
ev.content.displayname.as_deref().unwrap_or_else(|| user_id.as_str()),
|
||||
);
|
||||
let ambiguous_name =
|
||||
ev.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart());
|
||||
let ambiguous = client
|
||||
.store()
|
||||
.get_users_with_display_name(room_id, &ambiguous_name)
|
||||
.get_users_with_display_name(room_id, ambiguous_name)
|
||||
.await
|
||||
.map(|users| users.len() > 1)
|
||||
.unwrap_or_default();
|
||||
|
@ -1336,7 +1309,7 @@ impl ClientWorker {
|
|||
// Remove the session.json file.
|
||||
std::fs::remove_file(&self.settings.session_json)?;
|
||||
|
||||
Ok(Some(InfoMessage::from("Successfully logged out")))
|
||||
Ok(Some(InfoMessage::from("Sucessfully logged out")))
|
||||
}
|
||||
|
||||
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<OwnedRoomId> {
|
||||
|
@ -1373,7 +1346,7 @@ impl ClientWorker {
|
|||
|
||||
async fn get_room(&mut self, room_id: OwnedRoomId) -> IambResult<FetchedRoom> {
|
||||
if let Some(room) = self.client.get_room(&room_id) {
|
||||
let name = room.cached_display_name().ok_or_else(|| IambError::UnknownRoom(room_id))?;
|
||||
let name = room.display_name().await.map_err(IambError::from)?;
|
||||
let tags = room.tags().await.map_err(IambError::from)?;
|
||||
|
||||
Ok((room, name, tags))
|
||||
|
@ -1416,7 +1389,7 @@ impl ClientWorker {
|
|||
req.limit = Some(1000u32.into());
|
||||
req.max_depth = Some(1u32.into());
|
||||
|
||||
let resp = self.client.send(req).await.map_err(IambError::from)?;
|
||||
let resp = self.client.send(req, None).await.map_err(IambError::from)?;
|
||||
|
||||
let rooms = resp.rooms.into_iter().map(|chunk| chunk.room_id).collect();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue