25 Commits

Author SHA1 Message Date
Ulyssa
b5d356e741 Bump MSRV to 1.89 2025-10-26 07:41:37 -07:00
vaw
3149f79d11 Add :replied to go to the message the selected message replied to (#452) 2025-10-26 14:36:46 +00:00
vaw
7ccb1cbf2c Upgrade Matrix SDK to 0.14 (#521) 2025-10-25 16:23:59 -07:00
Benjamin Grosse
1ec311590d Use cargo crane in Nix flake and set up cachix action (#539) 2025-10-25 22:44:19 +00:00
Thierry Delafontaine
0ddded3b8b Remove deprecated Apple SDK frameworks pattern (#543) 2025-10-25 14:43:25 -07:00
vaw
a8cbc352ff Indicate encryption state of room in messagebar (#522) 2025-10-25 14:41:08 -07:00
vaw
dfa0937077 Remove blocking timeout for first sync on startup (#529) 2025-10-25 13:54:47 -07:00
Sandro Santilli
43485270ee Document how to build from sources (#513) 2025-10-25 20:54:19 +00:00
vaw
28fea03625 Improve error message for UnknownToken on login (#514) 2025-10-25 13:53:47 -07:00
vaw
e021d4a55d Add :forget to forget all left rooms (#507) 2025-10-25 13:41:34 -07:00
vaw
b01dbe5a5d Add more compatibility for unreads (#451) 2025-10-25 20:22:14 +00:00
vaw
4b2382bf93 Fix image preview placeholder rendering (#483) 2025-10-25 13:00:49 -07:00
Electria
0f2442566f Fix incorrect empty unreads window message (#541) 2025-10-25 19:59:06 +00:00
vaw
8c9a2714a1 Fix rustfmt warning (#523) 2025-10-25 12:55:23 -07:00
vaw
d44f861871 Respect user color of replied message with message_user_color (#532) 2025-10-25 12:54:16 -07:00
vaw
14aa97251c Expand ~ and shell variables in dirs config (#538) 2025-10-25 12:52:14 -07:00
vaw
55456dbc1e Treat unknown html tags as plain text (#509) 2025-09-13 13:38:47 -07:00
vaw
d5c330ac72 Fix most clippy warnigs (#501) 2025-09-13 13:32:25 -07:00
weird
7b1dc93f3a Update Nix flake and its lockfile (#500) 2025-09-02 22:10:16 -07:00
vaw
745f547904 Fall back to showing body for unknown message types (#496) 2025-09-02 22:02:21 -07:00
Akseli
6ebb7ac7fd Add config option for playing sound-hints with desktop notifications (#481) 2025-08-22 14:47:33 -07:00
vaw
1bb93c18fb Search :members by display name and user id (#482) 2025-08-22 14:30:57 -07:00
vaw
e3090e537f Handle attachment file names more robustly (#494) 2025-08-22 14:24:35 -07:00
vaw
ad10082c2f Upgrade matrix sdk 0.13 (#485)
Co-authored-by: Ken Rachynski <chief@troublemaker.dev>
2025-08-22 14:16:01 -07:00
Ulyssa
67603d0623 Update to modalkit{,-ratatui}@0.0.24 (#492) 2025-08-16 23:40:59 +00:00
23 changed files with 951 additions and 440 deletions

View File

@@ -45,3 +45,25 @@ jobs:
reporter: 'github-check' reporter: 'github-check'
- name: Run tests - name: Run tests
run: cargo test --locked run: cargo test --locked
nix-flake-test:
name: Flake checks ❄️
strategy:
matrix:
platform: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: cachix/cachix-action@v15
with:
name: iamb-prs
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Flake check
run: |
nix flake show
nix flake check --print-build-logs

View File

@@ -1,6 +1,6 @@
unstable_features = true unstable_features = true
max_width = 100 max_width = 100
fn_call_width = 90 fn_call_width = 88
struct_lit_width = 50 struct_lit_width = 50
struct_variant_width = 50 struct_variant_width = 50
chain_width = 75 chain_width = 75

599
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ license = "Apache-2.0"
exclude = [".github", "CONTRIBUTING.md"] exclude = [".github", "CONTRIBUTING.md"]
keywords = ["matrix", "chat", "tui", "vim"] keywords = ["matrix", "chat", "tui", "vim"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
rust-version = "1.83" rust-version = "1.89"
build = "build.rs" build = "build.rs"
[features] [features]
@@ -65,6 +65,7 @@ url = {version = "^2.2.2", features = ["serde"]}
edit = "0.1.4" edit = "0.1.4"
humansize = "2.0.0" humansize = "2.0.0"
linkify = "0.10.0" linkify = "0.10.0"
shellexpand = "3.1.1"
[dependencies.comrak] [dependencies.comrak]
version = "0.22.0" version = "0.22.0"
@@ -78,18 +79,18 @@ features = ["zbus", "serde"]
optional = true optional = true
[dependencies.modalkit] [dependencies.modalkit]
version = "0.0.23" version = "0.0.24"
default-features = false default-features = false
#git = "https://github.com/ulyssa/modalkit" #git = "https://github.com/ulyssa/modalkit"
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75" #rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
[dependencies.modalkit-ratatui] [dependencies.modalkit-ratatui]
version = "0.0.23" version = "0.0.24"
#git = "https://github.com/ulyssa/modalkit" #git = "https://github.com/ulyssa/modalkit"
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75" #rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
[dependencies.matrix-sdk] [dependencies.matrix-sdk]
version = "0.10.0" version = "0.14.0"
default-features = false default-features = false
features = ["e2e-encryption", "sqlite", "sso-login"] features = ["e2e-encryption", "sqlite", "sso-login"]

View File

@@ -51,9 +51,18 @@ url = "https://example.com"
user_id = "@user:example.com" user_id = "@user:example.com"
``` ```
## Installation (from source)
Install Rust and Cargo using [rustup], and then run from the directory
containing the sources (ie: from a git clone):
```
cargo install --locked --path .
```
## Installation (via `crates.io`) ## Installation (via `crates.io`)
Install Rust (1.83.0 or above) and Cargo, and then run: Install Rust (1.89.0 or above) and Cargo, and then run:
``` ```
cargo install --locked iamb cargo install --locked iamb
@@ -145,3 +154,4 @@ iamb is released under the [Apache License, Version 2.0].
[crates-io-iamb]: https://crates.io/crates/iamb [crates-io-iamb]: https://crates.io/crates/iamb
[iamb.chat]: https://iamb.chat [iamb.chat]: https://iamb.chat
[well_known_entry]: https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient [well_known_entry]: https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
[rustup]: https://rustup.rs/

View File

@@ -67,6 +67,8 @@ View a list of unread rooms.
Mark all rooms as read. Mark all rooms as read.
.It Sy ":welcome" .It Sy ":welcome"
View the startup Welcome window. View the startup Welcome window.
.It Sy ":forget"
Remove all left rooms from the internal database.
.El .El
.Sh "E2EE COMMANDS" .Sh "E2EE COMMANDS"
@@ -114,6 +116,8 @@ Redact the selected message with the optional reason.
Reply to the selected message. Reply to the selected message.
.It Sy ":cancel" .It Sy ":cancel"
Cancel the currently drafted message including replies. Cancel the currently drafted message including replies.
.It Sy ":replied"
Go to the message the current message replied to.
.It Sy ":upload [path]" .It Sy ":upload [path]"
Upload an attachment and send it to the currently selected room. Upload an attachment and send it to the currently selected room.
.El .El

84
flake.lock generated
View File

@@ -1,5 +1,41 @@
{ {
"nodes": { "nodes": {
"crane": {
"locked": {
"lastModified": 1759893430,
"narHash": "sha256-yAy4otLYm9iZ+NtQwTMEbqHwswSFUbhn7x826RR6djw=",
"owner": "ipetkov",
"repo": "crane",
"rev": "1979a2524cb8c801520bd94c38bb3d5692419d93",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1760510549,
"narHash": "sha256-NP+kmLMm7zSyv4Fufv+eSJXyqjLMUhUfPT6lXRlg/bU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "ef7178cf086f267113b5c48fdeb6e510729c8214",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@@ -20,11 +56,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1736883708, "lastModified": 1760284886,
"narHash": "sha256-uQ+NQ0/xYU0N1CnXsa2zghgNaOPxWpMJXSUJJ9W7140=", "narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "eb62e6aa39ea67e0b8018ba8ea077efe65807dc8", "rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -34,44 +70,28 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"rust-overlay": "rust-overlay"
} }
}, },
"rust-overlay": { "rust-analyzer-src": {
"inputs": { "flake": false,
"nixpkgs": "nixpkgs_2"
},
"locked": { "locked": {
"lastModified": 1736994333, "lastModified": 1760457219,
"narHash": "sha256-v4Jrok5yXsZ6dwj2+2uo5cSyUi9fBTurHqHvNHLT1XA=", "narHash": "sha256-WJOUGx42hrhmvvYcGkwea+BcJuQJLcns849OnewQqX4=",
"owner": "oxalica", "owner": "rust-lang",
"repo": "rust-overlay", "repo": "rust-analyzer",
"rev": "848db855cb9e88785996e961951659570fc58814", "rev": "8747cf81540bd1bbbab9ee2702f12c33aa887b46",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "oxalica", "owner": "rust-lang",
"repo": "rust-overlay", "ref": "nightly",
"repo": "rust-analyzer",
"type": "github" "type": "github"
} }
}, },

117
flake.nix
View File

@@ -5,41 +5,104 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay"; crane.url = "github:ipetkov/crane";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }: outputs =
flake-utils.lib.eachDefaultSystem (system: {
self,
nixpkgs,
crane,
flake-utils,
fenix,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let let
# We only need the nightly overlay in the devShell because .rs files are formatted with nightly. pkgs = nixpkgs.legacyPackages.${system};
overlays = [ (import rust-overlay) ]; inherit (pkgs) lib;
pkgs = import nixpkgs { inherit system overlays; };
rustNightly = pkgs.rust-bin.nightly."2024-12-12".default; rustToolchain = fenix.packages.${system}.fromToolchainFile {
in file = ./rust-toolchain.toml;
with pkgs; # When the file changes, this hash must be updated.
{ sha256 = "sha256-Hn2uaQzRLidAWpfmRwSRdImifGUCAb9HeAqTYFXWeQk=";
packages.default = rustPlatform.buildRustPackage {
pname = "iamb";
version = self.shortRev or self.dirtyShortRev;
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
nativeBuildInputs = [ pkg-config ];
buildInputs = [ openssl ] ++ lib.optionals stdenv.isDarwin
(with darwin.apple_sdk.frameworks; [ AppKit Security Cocoa ]);
}; };
devShell = mkShell { # Nightly toolchain for rustfmt (pinned to current flake lock)
buildInputs = [ # Note that the github CI uses "current nightly" for formatting, it 's not pinned.
(rustNightly.override { rustNightly = fenix.packages.${system}.latest.toolchain;
extensions = [ "rust-src" "rust-analyzer-preview" "rustfmt" "clippy" ]; rustNightlyFmt = fenix.packages.${system}.latest.rustfmt;
})
pkg-config craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibNightly = (crane.mkLib pkgs).overrideToolchain rustNightly;
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
(craneLib.fileset.commonCargoSources ./.)
./src/windows/welcome.md
];
};
commonArgs = {
inherit src;
strictDeps = true;
pname = "iamb";
version = self.shortRev or self.dirtyShortRev;
};
# Build *just* the cargo dependencies, so we can reuse
# all of that work (e.g. via cachix) when running in CI
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
# Build the actual crate
iamb = craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
});
in
{
checks = {
# Build the crate as part of `nix flake check`
inherit iamb;
iamb-clippy = craneLib.cargoClippy (commonArgs // {
inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
});
iamb-fmt = craneLibNightly.cargoFmt {
inherit src;
};
iamb-nextest = craneLib.cargoNextest (commonArgs // {
inherit cargoArtifacts;
partitions = 1;
partitionType = "count";
});
};
packages.default = iamb;
apps.default = flake-utils.lib.mkApp {
drv = iamb;
};
devShells.default = craneLib.devShell {
# Inherit inputs from checks
checks = self.checks.${system};
packages = with pkgs; [
rustNightlyFmt
cargo-tarpaulin cargo-tarpaulin
cargo-watch cargo-watch
sqlite sqlite
]; ];
}; };
}); }
);
} }

View File

@@ -1,3 +1,3 @@
[toolchain] [toolchain]
channel = "1.83" channel = "1.89"
components = [ "clippy" ] components = [ "clippy" ]

View File

@@ -13,6 +13,7 @@ use std::time::{Duration, Instant};
use emojis::Emoji; use emojis::Emoji;
use matrix_sdk::ruma::events::receipt::ReceiptThread; use matrix_sdk::ruma::events::receipt::ReceiptThread;
use matrix_sdk::ruma::room_version_rules::RedactionRules;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
@@ -57,7 +58,6 @@ use matrix_sdk::{
OwnedRoomId, OwnedRoomId,
OwnedUserId, OwnedUserId,
RoomId, RoomId,
RoomVersionId,
UserId, UserId,
}, },
RoomState as MatrixRoomState, RoomState as MatrixRoomState,
@@ -169,6 +169,9 @@ pub enum MessageAction {
/// Reply to a message. /// Reply to a message.
Reply, Reply,
/// Go to the message the hovered message replied to.
Replied,
/// Unreact to a message. /// Unreact to a message.
/// ///
/// If no specific Emoji to remove to is specified, then all reactions from the user on the /// If no specific Emoji to remove to is specified, then all reactions from the user on the
@@ -491,6 +494,8 @@ pub enum HomeserverAction {
/// Create a new room with an optional localpart. /// Create a new room with an optional localpart.
CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags), CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags),
Logout(String, bool), Logout(String, bool),
/// Forget all left rooms
Forget,
} }
/// An action performed against the user's room keys. /// An action performed against the user's room keys.
@@ -783,6 +788,10 @@ pub enum IambError {
#[error("Invalid room alias id: {0}")] #[error("Invalid room alias id: {0}")]
InvalidRoomAliasId(#[from] matrix_sdk::ruma::IdParseError), InvalidRoomAliasId(#[from] matrix_sdk::ruma::IdParseError),
/// An invalid space child order was specified.
#[error("Invalid space child order: {0}")]
InvalidSpaceChildOrder(matrix_sdk::ruma::IdParseError),
/// A failure occurred during verification. /// A failure occurred during verification.
#[error("Verification request error: {0}")] #[error("Verification request error: {0}")]
VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError), VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError),
@@ -1023,7 +1032,7 @@ impl RoomInfo {
self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?) self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?)
} }
pub fn redact(&mut self, ev: OriginalSyncRoomRedactionEvent, room_version: &RoomVersionId) { pub fn redact(&mut self, ev: OriginalSyncRoomRedactionEvent, rules: &RedactionRules) {
let Some(redacts) = &ev.redacts else { let Some(redacts) = &ev.redacts else {
return; return;
}; };
@@ -1033,20 +1042,20 @@ impl RoomInfo {
Some(EventLocation::State(key)) => { Some(EventLocation::State(key)) => {
if let Some(msg) = self.messages.get_mut(key) { if let Some(msg) = self.messages.get_mut(key) {
let ev = SyncRoomRedactionEvent::Original(ev); let ev = SyncRoomRedactionEvent::Original(ev);
msg.redact(ev, room_version); msg.redact(ev, rules);
} }
}, },
Some(EventLocation::Message(None, key)) => { Some(EventLocation::Message(None, key)) => {
if let Some(msg) = self.messages.get_mut(key) { if let Some(msg) = self.messages.get_mut(key) {
let ev = SyncRoomRedactionEvent::Original(ev); let ev = SyncRoomRedactionEvent::Original(ev);
msg.redact(ev, room_version); msg.redact(ev, rules);
} }
}, },
Some(EventLocation::Message(Some(root), key)) => { Some(EventLocation::Message(Some(root), key)) => {
if let Some(thread) = self.threads.get_mut(root) { if let Some(thread) = self.threads.get_mut(root) {
if let Some(msg) = thread.get_mut(key) { if let Some(msg) = thread.get_mut(key) {
let ev = SyncRoomRedactionEvent::Original(ev); let ev = SyncRoomRedactionEvent::Original(ev);
msg.redact(ev, room_version); msg.redact(ev, rules);
} }
} }
}, },
@@ -1133,14 +1142,34 @@ impl RoomInfo {
/// Indicates whether this room has unread messages. /// Indicates whether this room has unread messages.
pub fn unreads(&self, settings: &ApplicationSettings) -> UnreadInfo { pub fn unreads(&self, settings: &ApplicationSettings) -> UnreadInfo {
let last_message = self.messages.last_key_value(); let last_message = self.messages.last_key_value();
let last_receipt = self let last_receipt = self
.user_receipts .user_receipts
.get(&ReceiptThread::Main) .get(&ReceiptThread::Main)
.and_then(|receipts| receipts.get(&settings.profile.user_id)); .and_then(|receipts| receipts.get(&settings.profile.user_id));
let last_receipt = last_receipt.as_ref().and_then(|event_id| {
match &self.keys.get(*event_id)? {
EventLocation::Message(_, key) | EventLocation::State(key) => Some(key),
EventLocation::Reaction(_) => None,
}
});
let last_unthreaded = self
.user_receipts
.get(&ReceiptThread::Unthreaded)
.and_then(|receipts| receipts.get(&settings.profile.user_id));
let last_unthreaded = last_unthreaded.as_ref().and_then(|event_id| {
match &self.keys.get(*event_id)? {
EventLocation::Message(_, key) | EventLocation::State(key) => Some(key),
EventLocation::Reaction(_) => None,
}
});
let last_receipt = std::cmp::max(last_receipt, last_unthreaded);
match (last_message, last_receipt) { match (last_message, last_receipt) {
(Some(((ts, recent), _)), Some(last_read)) => { (Some(((ts, _), _)), Some((read_ts, _))) => {
UnreadInfo { unread: last_read != recent, latest: Some(*ts) } UnreadInfo { unread: ts > read_ts, latest: Some(*ts) }
}, },
(Some(((ts, _), _)), None) => { (Some(((ts, _), _)), None) => {
// If we've never loaded/generated a room's receipt (example, // If we've never loaded/generated a room's receipt (example,
@@ -1477,14 +1506,19 @@ impl SyncInfo {
} }
} }
bitflags::bitflags! { static MESSAGE_NEED_TTL: u8 = 30;
/// Load-needs
#[derive(Debug, Default, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Need: u32 { /// Load messages until the event is loaded or `ttl` loads are exceeded
const EMPTY = 0b00000000; pub struct MessageNeed {
const MESSAGES = 0b00000001; pub event_id: OwnedEventId,
const MEMBERS = 0b00000010; pub ttl: u8,
} }
#[derive(Default, Debug, PartialEq)]
pub struct Need {
pub members: bool,
pub messages: Option<Vec<MessageNeed>>,
} }
/// Things that need loading for different rooms. /// Things that need loading for different rooms.
@@ -1494,9 +1528,31 @@ pub struct RoomNeeds {
} }
impl RoomNeeds { impl RoomNeeds {
/// Mark a room for needing something to be loaded. /// Mark a room for needing to load members.
pub fn insert(&mut self, room_id: OwnedRoomId, need: Need) { pub fn need_members(&mut self, room_id: OwnedRoomId) {
self.needs.entry(room_id).or_default().insert(need); self.needs.entry(room_id).or_default().members = true;
}
/// Mark a room for needing to load messages.
pub fn need_messages(&mut self, room_id: OwnedRoomId) {
self.needs.entry(room_id).or_default().messages.get_or_insert_default();
}
/// Mark a room for needing to load messages until the given message is loaded or a retry limit
/// is exceeded.
pub fn need_message(&mut self, room_id: OwnedRoomId, event_id: OwnedEventId) {
let messages = &mut self.needs.entry(room_id).or_default().messages.get_or_insert_default();
messages.push(MessageNeed { event_id, ttl: MESSAGE_NEED_TTL });
}
pub fn need_messages_all(&mut self, room_id: OwnedRoomId, message_needs: Vec<MessageNeed>) {
self.needs
.entry(room_id)
.or_default()
.messages
.get_or_insert_default()
.extend(message_needs);
} }
pub fn rooms(&self) -> usize { pub fn rooms(&self) -> usize {
@@ -2274,12 +2330,12 @@ pub mod tests {
let mut need_load = RoomNeeds::default(); let mut need_load = RoomNeeds::default();
need_load.insert(room_id.clone(), Need::MESSAGES); need_load.need_messages(room_id.clone());
need_load.insert(room_id.clone(), Need::MEMBERS); need_load.need_members(room_id.clone());
assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![( assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![(
room_id, room_id,
Need::MESSAGES | Need::MEMBERS, Need { members: true, messages: Some(Vec::new()) }
)],); )],);
} }

View File

@@ -200,6 +200,17 @@ fn iamb_leave(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step); return Ok(step);
} }
fn iamb_forget(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument);
}
let forget = IambAction::Homeserver(HomeserverAction::Forget);
let step = CommandStep::Continue(forget.into(), ctx.context.clone());
return Ok(step);
}
fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() { if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
@@ -275,6 +286,17 @@ fn iamb_reply(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
return Ok(step); return Ok(step);
} }
fn iamb_replied(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument);
}
let ract = IambAction::from(MessageAction::Replied);
let step = CommandStep::Continue(ract.into(), ctx.context.clone());
return Ok(step);
}
fn iamb_editor(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { fn iamb_editor(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
if !desc.arg.text.is_empty() { if !desc.arg.text.is_empty() {
return Result::Err(CommandError::InvalidArgument); return Result::Err(CommandError::InvalidArgument);
@@ -731,6 +753,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
aliases: vec![], aliases: vec![],
f: iamb_leave, f: iamb_leave,
}); });
cmds.add_command(ProgramCommand {
name: "forget".into(),
aliases: vec![],
f: iamb_forget,
});
cmds.add_command(ProgramCommand { cmds.add_command(ProgramCommand {
name: "members".into(), name: "members".into(),
aliases: vec![], aliases: vec![],
@@ -751,6 +778,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
aliases: vec![], aliases: vec![],
f: iamb_reply, f: iamb_reply,
}); });
cmds.add_command(ProgramCommand {
name: "replied".into(),
aliases: vec![],
f: iamb_replied,
});
cmds.add_command(ProgramCommand { cmds.add_command(ProgramCommand {
name: "rooms".into(), name: "rooms".into(),
aliases: vec![], aliases: vec![],

View File

@@ -323,7 +323,7 @@ pub struct Session {
impl From<Session> for MatrixSession { impl From<Session> for MatrixSession {
fn from(session: Session) -> Self { fn from(session: Session) -> Self {
MatrixSession { MatrixSession {
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens { tokens: matrix_sdk::authentication::SessionTokens {
access_token: session.access_token, access_token: session.access_token,
refresh_token: session.refresh_token, refresh_token: session.refresh_token,
}, },
@@ -483,6 +483,8 @@ pub struct Notifications {
pub via: NotifyVia, pub via: NotifyVia,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub show_message: bool, pub show_message: bool,
#[serde(default)]
pub sound_hint: Option<String>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -705,11 +707,11 @@ impl DirectoryValues {
#[derive(Clone, Default, Deserialize)] #[derive(Clone, Default, Deserialize)]
pub struct Directories { pub struct Directories {
pub cache: Option<PathBuf>, pub cache: Option<String>,
pub data: Option<PathBuf>, pub data: Option<String>,
pub logs: Option<PathBuf>, pub logs: Option<String>,
pub downloads: Option<PathBuf>, pub downloads: Option<String>,
pub image_previews: Option<PathBuf>, pub image_previews: Option<String>,
} }
impl Directories { impl Directories {
@@ -726,6 +728,11 @@ impl Directories {
fn values(self) -> DirectoryValues { fn values(self) -> DirectoryValues {
let cache = self let cache = self
.cache .cache
.map(|dir| {
let dir = shellexpand::full(&dir)
.expect("unable to expand shell variables in dirs.cache");
Path::new(dir.as_ref()).to_owned()
})
.or_else(|| { .or_else(|| {
let mut dir = dirs::cache_dir()?; let mut dir = dirs::cache_dir()?;
dir.push("iamb"); dir.push("iamb");
@@ -735,6 +742,11 @@ impl Directories {
let data = self let data = self
.data .data
.map(|dir| {
let dir = shellexpand::full(&dir)
.expect("unable to expand shell variables in dirs.cache");
Path::new(dir.as_ref()).to_owned()
})
.or_else(|| { .or_else(|| {
let mut dir = dirs::data_dir()?; let mut dir = dirs::data_dir()?;
dir.push("iamb"); dir.push("iamb");
@@ -742,19 +754,40 @@ impl Directories {
}) })
.expect("no dirs.data value configured!"); .expect("no dirs.data value configured!");
let logs = self.logs.unwrap_or_else(|| { let logs = self
let mut dir = cache.clone(); .logs
dir.push("logs"); .map(|dir| {
dir let dir = shellexpand::full(&dir)
}); .expect("unable to expand shell variables in dirs.cache");
Path::new(dir.as_ref()).to_owned()
})
.unwrap_or_else(|| {
let mut dir = cache.clone();
dir.push("logs");
dir
});
let downloads = self.downloads.or_else(dirs::download_dir); let downloads = self
.downloads
.map(|dir| {
let dir = shellexpand::full(&dir)
.expect("unable to expand shell variables in dirs.cache");
Path::new(dir.as_ref()).to_owned()
})
.or_else(dirs::download_dir);
let image_previews = self.image_previews.unwrap_or_else(|| { let image_previews = self
let mut dir = cache.clone(); .image_previews
dir.push("image_preview_downloads"); .map(|dir| {
dir let dir = shellexpand::full(&dir)
}); .expect("unable to expand shell variables in dirs.cache");
Path::new(dir.as_ref()).to_owned()
})
.unwrap_or_else(|| {
let mut dir = cache.clone();
dir.push("image_preview_downloads");
dir
});
DirectoryValues { cache, data, logs, downloads, image_previews } DirectoryValues { cache, data, logs, downloads, image_previews }
} }
@@ -1001,7 +1034,7 @@ impl ApplicationSettings {
Ok(()) Ok(())
} }
pub fn get_user_char_span(&self, user_id: &UserId) -> Span { pub fn get_user_char_span(&self, user_id: &UserId) -> Span<'_> {
let (color, c) = self let (color, c) = self
.tunables .tunables
.users .users

View File

@@ -663,6 +663,13 @@ impl Application {
Err(UIError::NeedConfirm(prompt)) Err(UIError::NeedConfirm(prompt))
}, },
HomeserverAction::Forget => {
let client = &store.application.worker.client;
for room in client.left_rooms() {
room.forget().await.map_err(IambError::from)?;
}
Ok(vec![])
},
} }
} }
@@ -1036,7 +1043,7 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
match res { match res {
Err(UIError::Application(IambError::Matrix(e))) => { Err(UIError::Application(IambError::Matrix(e))) => {
if let Some(ErrorKind::UnknownToken { .. }) = e.client_api_error_kind() { if let Some(ErrorKind::UnknownToken { .. }) = e.client_api_error_kind() {
print_exit("Server did not recognize our API token; did you log out from this session elsewhere?") print_exit(format!("Server did not recognize our API token; did you log out from this session elsewhere?\nTry deleting `{}` to force a clean login.", settings.session_json.display()))
} else { } else {
print_exit(e) print_exit(e)
} }

View File

@@ -820,7 +820,8 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
*c2t(&node.children.borrow(), state) *c2t(&node.children.borrow(), state)
}, },
_ => return vec![], // Treat unknown tags as plain text.
_ => *c2t(&node.children.borrow(), state),
} }
}, },

View File

@@ -11,6 +11,7 @@ use std::ops::{Deref, DerefMut};
use chrono::{DateTime, Local as LocalTz}; use chrono::{DateTime, Local as LocalTz};
use humansize::{format_size, DECIMAL}; use humansize::{format_size, DECIMAL};
use matrix_sdk::ruma::events::receipt::ReceiptThread; use matrix_sdk::ruma::events::receipt::ReceiptThread;
use matrix_sdk::ruma::room_version_rules::RedactionRules;
use serde_json::json; use serde_json::json;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@@ -43,7 +44,6 @@ use matrix_sdk::ruma::{
MilliSecondsSinceUnixEpoch, MilliSecondsSinceUnixEpoch,
OwnedEventId, OwnedEventId,
OwnedUserId, OwnedUserId,
RoomVersionId,
UInt, UInt,
}; };
@@ -172,7 +172,8 @@ fn placeholder_frame(
image_preview_size: &ImagePreviewSize, image_preview_size: &ImagePreviewSize,
) -> Option<String> { ) -> Option<String> {
let ImagePreviewSize { width, height } = image_preview_size; let ImagePreviewSize { width, height } = image_preview_size;
if outer_width < *width || (*width < 2 || *height < 2) { let width = usize::min(*width, outer_width);
if width < 2 || *height < 2 {
return None; return None;
} }
let mut placeholder = "\u{230c}".to_string(); let mut placeholder = "\u{230c}".to_string();
@@ -233,13 +234,13 @@ impl MessageTimeStamp {
dt1.date_naive() == dt2.date_naive() dt1.date_naive() == dt2.date_naive()
} }
fn show_date(&self) -> Option<Span> { fn show_date(&self) -> Option<Span<'_>> {
let time = self.as_datetime().format("%A, %B %d %Y").to_string(); let time = self.as_datetime().format("%A, %B %d %Y").to_string();
Span::styled(time, BOLD_STYLE).into() Span::styled(time, BOLD_STYLE).into()
} }
fn show_time(&self) -> Option<Span> { fn show_time(&self) -> Option<Span<'_>> {
match self { match self {
MessageTimeStamp::OriginServer(ms) => { MessageTimeStamp::OriginServer(ms) => {
let time = millis_to_datetime(*ms).format("%T"); let time = millis_to_datetime(*ms).format("%T");
@@ -510,7 +511,7 @@ impl MessageEvent {
} }
} }
fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) { fn redact(&mut self, redaction: SyncRoomRedactionEvent, rules: &RedactionRules) {
match self { match self {
MessageEvent::EncryptedOriginal(_) => return, MessageEvent::EncryptedOriginal(_) => return,
MessageEvent::EncryptedRedacted(_) => return, MessageEvent::EncryptedRedacted(_) => return,
@@ -519,7 +520,7 @@ impl MessageEvent {
MessageEvent::Local(_, _) => return, MessageEvent::Local(_, _) => return,
MessageEvent::Original(ev) => { MessageEvent::Original(ev) => {
let redacted = RedactedRoomMessageEvent { let redacted = RedactedRoomMessageEvent {
content: ev.content.clone().redact(version), content: ev.content.clone().redact(rules),
event_id: ev.event_id.clone(), event_id: ev.event_id.clone(),
sender: ev.sender.clone(), sender: ev.sender.clone(),
origin_server_ts: ev.origin_server_ts, origin_server_ts: ev.origin_server_ts,
@@ -573,27 +574,18 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
MessageType::Video(content) => { MessageType::Video(content) => {
display_file_to_text!(Video, content); display_file_to_text!(Video, content);
}, },
_ => { _ => content.body(),
match content.msgtype() {
// Just show the body text for the special Element messages.
"nic.custom.confetti" |
"nic.custom.fireworks" |
"io.element.effect.hearts" |
"io.element.effect.rainfall" |
"io.element.effect.snowfall" |
"io.element.effects.space_invaders" => content.body(),
other => {
return Cow::Owned(format!("[Unknown message type: {other:?}]"));
},
}
},
}; };
Cow::Borrowed(s) Cow::Borrowed(s)
} }
fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> { fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> {
let reason = unsigned.redacted_because.content.reason.as_ref(); let reason = unsigned
.redacted_because
.deserialize()
.ok()
.and_then(|ev| ev.content.reason);
if let Some(r) = reason { if let Some(r) = reason {
Cow::Owned(format!("[Redacted: {r:?}]")) Cow::Owned(format!("[Redacted: {r:?}]"))
@@ -740,22 +732,28 @@ impl<'a> MessageFormatter<'a> {
info: &'a RoomInfo, info: &'a RoomInfo,
settings: &'a ApplicationSettings, settings: &'a ApplicationSettings,
) -> Option<ProtocolPreview<'a>> { ) -> Option<ProtocolPreview<'a>> {
let reply_style = if settings.tunables.message_user_color {
style.patch(settings.get_user_color(&msg.sender))
} else {
style
};
let width = self.width(); let width = self.width();
let w = width.saturating_sub(2); let w = width.saturating_sub(2);
let (mut replied, proto) = msg.show_msg(w, style, true, settings); let (mut replied, proto) = msg.show_msg(w, reply_style, true, settings);
let mut sender = msg.sender_span(info, self.settings); let mut sender = msg.sender_span(info, self.settings);
let sender_width = UnicodeWidthStr::width(sender.content.as_ref()); let sender_width = UnicodeWidthStr::width(sender.content.as_ref());
let trailing = w.saturating_sub(sender_width + 1); let trailing = w.saturating_sub(sender_width + 1);
sender.style = sender.style.patch(style); sender.style = sender.style.patch(reply_style);
self.push_spans( self.push_spans(
Line::from(vec![ Line::from(vec![
Span::styled(" ", style), Span::styled(" ", style),
Span::styled(THICK_VERTICAL, style), Span::styled(THICK_VERTICAL, style),
sender, sender,
Span::styled(":", style), Span::styled(":", reply_style),
space_span(trailing, style), space_span(trailing, reply_style),
]), ]),
style, style,
text, text,
@@ -774,7 +772,7 @@ impl<'a> MessageFormatter<'a> {
line.spans.insert(0, Span::styled(" ", style)); line.spans.insert(0, Span::styled(" ", style));
} }
self.push_text(replied, style, text); self.push_text(replied, reply_style, text);
proto proto
} }
@@ -1090,7 +1088,7 @@ impl Message {
}, },
ImageStatus::Loaded(backend) => { ImageStatus::Loaded(backend) => {
proto = Some(backend); proto = Some(backend);
placeholder_frame(Some("Cut off..."), width, &backend.area().into()) placeholder_frame(Some("No Space..."), width, &backend.area().into())
}, },
ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")), ImageStatus::Error(err) => Some(format!("[Image error: {err}]\n")),
}; };
@@ -1141,8 +1139,8 @@ impl Message {
Span::styled(sender, style).into() Span::styled(sender, style).into()
} }
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) { pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, rules: &RedactionRules) {
self.event.redact(redaction, version); self.event.redact(redaction, rules);
self.html = None; self.html = None;
self.downloaded = false; self.downloaded = false;
self.image_preview = ImageStatus::None; self.image_preview = ImageStatus::None;
@@ -1348,7 +1346,17 @@ pub mod tests {
) )
); );
assert_eq!(placeholder_frame(None, 2, &ImagePreviewSize { width: 4, height: 4 }), None); assert_eq!(
placeholder_frame(None, 2, &ImagePreviewSize { width: 4, height: 4 }),
pretty_frame_test(
r#"
⌌⌍
⌎⌏
"#
)
);
assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 1, height: 4 }), None); assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 1, height: 4 }), None);
assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 4, height: 1 }), None); assert_eq!(placeholder_frame(None, 4, &ImagePreviewSize { width: 4, height: 1 }), None);

View File

@@ -12,6 +12,7 @@ use matrix_sdk::{
RoomId, RoomId,
}, },
Client, Client,
EncryptionState,
}; };
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -50,6 +51,7 @@ pub async fn register_notifications(
} }
let notify_via = settings.tunables.notifications.via; let notify_via = settings.tunables.notifications.via;
let show_message = settings.tunables.notifications.show_message; let show_message = settings.tunables.notifications.show_message;
let sound_hint = settings.tunables.notifications.sound_hint.clone();
let server_settings = client.notification_settings().await; let server_settings = client.notification_settings().await;
let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else { let Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
return; return;
@@ -60,6 +62,7 @@ pub async fn register_notifications(
.register_notification_handler(move |notification, room: MatrixRoom, client: Client| { .register_notification_handler(move |notification, room: MatrixRoom, client: Client| {
let store = store.clone(); let store = store.clone();
let server_settings = server_settings.clone(); let server_settings = server_settings.clone();
let sound_hint = sound_hint.clone();
async move { async move {
let mode = global_or_room_mode(&server_settings, &room).await; let mode = global_or_room_mode(&server_settings, &room).await;
if mode == RoomNotificationMode::Mute { if mode == RoomNotificationMode::Mute {
@@ -89,6 +92,7 @@ pub async fn register_notifications(
body.as_deref(), body.as_deref(),
room_id, room_id,
&store, &store,
sound_hint.as_deref(),
) )
.await; .await;
}, },
@@ -113,10 +117,11 @@ async fn send_notification(
body: Option<&str>, body: Option<&str>,
room_id: OwnedRoomId, room_id: OwnedRoomId,
store: &AsyncProgramStore, store: &AsyncProgramStore,
sound_hint: Option<&str>,
) { ) {
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
if via.desktop { if via.desktop {
send_notification_desktop(summary, body, room_id, store).await; send_notification_desktop(summary, body, room_id, store, sound_hint).await;
} }
#[cfg(not(feature = "desktop"))] #[cfg(not(feature = "desktop"))]
{ {
@@ -134,11 +139,13 @@ async fn send_notification_bell(store: &AsyncProgramStore) {
} }
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
#[cfg_attr(target_os = "macos", allow(unused_variables))]
async fn send_notification_desktop( async fn send_notification_desktop(
summary: &str, summary: &str,
body: Option<&str>, body: Option<&str>,
room_id: OwnedRoomId, room_id: OwnedRoomId,
_store: &AsyncProgramStore, _store: &AsyncProgramStore,
sound_hint: Option<&str>,
) { ) {
let mut desktop_notification = notify_rust::Notification::new(); let mut desktop_notification = notify_rust::Notification::new();
desktop_notification desktop_notification
@@ -147,6 +154,10 @@ async fn send_notification_desktop(
.icon(IAMB_XDG_NAME) .icon(IAMB_XDG_NAME)
.action("default", "default"); .action("default", "default");
if let Some(sound_hint) = sound_hint {
desktop_notification.sound_name(sound_hint);
}
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
desktop_notification.urgency(notify_rust::Urgency::Normal); desktop_notification.urgency(notify_rust::Urgency::Normal);
@@ -182,8 +193,8 @@ async fn global_or_room_mode(
Ok(true) => IsOneToOne::Yes, Ok(true) => IsOneToOne::Yes,
_ => IsOneToOne::No, _ => IsOneToOne::No,
}; };
let is_encrypted = match room.is_encrypted().await { let is_encrypted = match room.latest_encryption_state().await {
Ok(true) => IsEncrypted::Yes, Ok(EncryptionState::Encrypted) => IsEncrypted::Yes,
_ => IsEncrypted::No, _ => IsEncrypted::No,
}; };
settings settings

View File

@@ -49,7 +49,8 @@ use crate::{
const TEST_ROOM1_ALIAS: &str = "#room1:example.com"; const TEST_ROOM1_ALIAS: &str = "#room1:example.com";
lazy_static! { lazy_static! {
pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned(); pub static ref TEST_ROOM1_ID: OwnedRoomId =
RoomId::new_v1(server_name!("example.com")).to_owned();
pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned(); pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned();
pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned(); pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned();
pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned(); pub static ref TEST_USER3: OwnedUserId = user_id!("@user3:example.com").to_owned();
@@ -196,6 +197,7 @@ pub fn mock_tunables() -> TunableValues {
enabled: false, enabled: false,
via: NotifyVia::default(), via: NotifyVia::default(),
show_message: true, show_message: true,
sound_hint: None,
}, },
image_preview: None, image_preview: None,
user_gutter_width: 30, user_gutter_width: 30,

View File

@@ -66,7 +66,6 @@ use crate::base::{
IambInfo, IambInfo,
IambResult, IambResult,
MessageAction, MessageAction,
Need,
ProgramAction, ProgramAction,
ProgramContext, ProgramContext,
ProgramStore, ProgramStore,
@@ -97,12 +96,12 @@ fn bold_style() -> Style {
} }
#[inline] #[inline]
fn bold_span(s: &str) -> Span { fn bold_span(s: &str) -> Span<'_> {
Span::styled(s, bold_style()) Span::styled(s, bold_style())
} }
#[inline] #[inline]
fn bold_spans(s: &str) -> Line { fn bold_spans(s: &str) -> Line<'_> {
bold_span(s).into() bold_span(s).into()
} }
@@ -116,12 +115,12 @@ fn selected_style(selected: bool) -> Style {
} }
#[inline] #[inline]
fn selected_span(s: &str, selected: bool) -> Span { fn selected_span(s: &str, selected: bool) -> Span<'_> {
Span::styled(s, selected_style(selected)) Span::styled(s, selected_style(selected))
} }
#[inline] #[inline]
fn selected_text(s: &str, selected: bool) -> Text { fn selected_text(s: &str, selected: bool) -> Text<'_> {
Text::from(selected_span(s, selected)) Text::from(selected_span(s, selected))
} }
@@ -641,7 +640,7 @@ impl WindowOps<IambInfo> for IambWindow {
state.set(items); state.set(items);
List::new(store) List::new(store)
.empty_message("You do not have rooms or dms yet") .empty_message("You do not have any unreads yet")
.empty_alignment(Alignment::Center) .empty_alignment(Alignment::Center)
.focus(focused) .focus(focused)
.render(area, buf, state); .render(area, buf, state);
@@ -743,7 +742,7 @@ impl Window<IambInfo> for IambWindow {
} }
} }
fn get_tab_title(&self, store: &mut ProgramStore) -> Line { fn get_tab_title(&self, store: &mut ProgramStore) -> Line<'_> {
match self { match self {
IambWindow::DirectList(_) => bold_spans("Direct Messages"), IambWindow::DirectList(_) => bold_spans("Direct Messages"),
IambWindow::RoomList(_) => bold_spans("Rooms"), IambWindow::RoomList(_) => bold_spans("Rooms"),
@@ -771,7 +770,7 @@ impl Window<IambInfo> for IambWindow {
} }
} }
fn get_win_title(&self, store: &mut ProgramStore) -> Line { fn get_win_title(&self, store: &mut ProgramStore) -> Line<'_> {
match self { match self {
IambWindow::DirectList(_) => bold_spans("Direct Messages"), IambWindow::DirectList(_) => bold_spans("Direct Messages"),
IambWindow::RoomList(_) => bold_spans("Rooms"), IambWindow::RoomList(_) => bold_spans("Rooms"),
@@ -801,7 +800,7 @@ impl Window<IambInfo> for IambWindow {
let (room, name, tags) = store.application.worker.get_room(room_id)?; let (room, name, tags) = store.application.worker.get_room(room_id)?;
let room = RoomState::new(room, thread, name, tags, store); let room = RoomState::new(room, thread, name, tags, store);
store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS); store.application.need_load.need_members(room.id().to_owned());
return Ok(room.into()); return Ok(room.into());
}, },
IambId::DirectList => { IambId::DirectList => {
@@ -863,7 +862,7 @@ impl Window<IambInfo> for IambWindow {
let (room, name, tags) = store.application.worker.get_room(room_id)?; let (room, name, tags) = store.application.worker.get_room(room_id)?;
let room = RoomState::new(room, None, name, tags, store); let room = RoomState::new(room, None, name, tags, store);
store.application.need_load.insert(room.id().to_owned(), Need::MEMBERS); store.application.need_load.need_members(room.id().to_owned());
Ok(room.into()) Ok(room.into())
} }
} }
@@ -959,7 +958,12 @@ impl Display for GenericChatItem {
} }
impl ListItem<IambInfo> for GenericChatItem { impl ListItem<IambInfo> for GenericChatItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(
&self,
selected: bool,
_: &ViewportContext<ListCursor>,
_: &mut ProgramStore,
) -> Text<'_> {
let unread = self.unread.is_unread(); let unread = self.unread.is_unread();
let style = selected_style(selected); let style = selected_style(selected);
let (name, mut labels) = name_and_labels(&self.name, unread, style); let (name, mut labels) = name_and_labels(&self.name, unread, style);
@@ -1073,7 +1077,12 @@ impl Display for RoomItem {
} }
impl ListItem<IambInfo> for RoomItem { impl ListItem<IambInfo> for RoomItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(
&self,
selected: bool,
_: &ViewportContext<ListCursor>,
_: &mut ProgramStore,
) -> Text<'_> {
let unread = self.unread.is_unread(); let unread = self.unread.is_unread();
let style = selected_style(selected); let style = selected_style(selected);
let (name, mut labels) = name_and_labels(&self.name, unread, style); let (name, mut labels) = name_and_labels(&self.name, unread, style);
@@ -1177,7 +1186,12 @@ impl Display for DirectItem {
} }
impl ListItem<IambInfo> for DirectItem { impl ListItem<IambInfo> for DirectItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(
&self,
selected: bool,
_: &ViewportContext<ListCursor>,
_: &mut ProgramStore,
) -> Text<'_> {
let unread = self.unread.is_unread(); let unread = self.unread.is_unread();
let style = selected_style(selected); let style = selected_style(selected);
let (name, mut labels) = name_and_labels(&self.name, unread, style); let (name, mut labels) = name_and_labels(&self.name, unread, style);
@@ -1280,7 +1294,12 @@ impl Display for SpaceItem {
} }
impl ListItem<IambInfo> for SpaceItem { impl ListItem<IambInfo> for SpaceItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(
&self,
selected: bool,
_: &ViewportContext<ListCursor>,
_: &mut ProgramStore,
) -> Text<'_> {
selected_text(self.name.as_str(), selected) selected_text(self.name.as_str(), selected)
} }
@@ -1411,7 +1430,12 @@ impl Display for VerifyItem {
} }
impl ListItem<IambInfo> for VerifyItem { impl ListItem<IambInfo> for VerifyItem {
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text { fn show(
&self,
selected: bool,
_: &ViewportContext<ListCursor>,
_: &mut ProgramStore,
) -> Text<'_> {
let mut lines = vec![]; let mut lines = vec![];
let bold = Style::default().add_modifier(StyleModifier::BOLD); let bold = Style::default().add_modifier(StyleModifier::BOLD);
@@ -1521,7 +1545,7 @@ impl ListItem<IambInfo> for MemberItem {
selected: bool, selected: bool,
_: &ViewportContext<ListCursor>, _: &ViewportContext<ListCursor>,
store: &mut ProgramStore, store: &mut ProgramStore,
) -> Text { ) -> Text<'_> {
let info = store.application.rooms.get_or_default(self.room_id.clone()); let info = store.application.rooms.get_or_default(self.room_id.clone());
let user_id = self.member.user_id(); let user_id = self.member.user_id();
@@ -1565,6 +1589,10 @@ impl ListItem<IambInfo> for MemberItem {
fn get_word(&self) -> Option<String> { fn get_word(&self) -> Option<String> {
self.member.user_id().to_string().into() self.member.user_id().to_string().into()
} }
fn matches(&self, needle: &regex::Regex) -> bool {
needle.is_match(self.member.name()) || needle.is_match(self.member.user_id().as_str())
}
} }
impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem { impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem {
@@ -1644,7 +1672,7 @@ mod tests {
let server = server_name!("example.com"); let server = server_name!("example.com");
let room1 = TestRoomItem { let room1 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![TagName::Favorite], tags: vec![TagName::Favorite],
alias: Some(room_alias_id!("#room1:example.com").to_owned()), alias: Some(room_alias_id!("#room1:example.com").to_owned()),
name: "Z", name: "Z",
@@ -1653,7 +1681,7 @@ mod tests {
}; };
let room2 = TestRoomItem { let room2 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: Some(room_alias_id!("#a:example.com").to_owned()), alias: Some(room_alias_id!("#a:example.com").to_owned()),
name: "Unnamed Room", name: "Unnamed Room",
@@ -1662,7 +1690,7 @@ mod tests {
}; };
let room3 = TestRoomItem { let room3 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "Cool Room", name: "Cool Room",
@@ -1710,7 +1738,7 @@ mod tests {
let server = server_name!("example.com"); let server = server_name!("example.com");
let room1 = TestRoomItem { let room1 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "Room 1", name: "Room 1",
@@ -1719,7 +1747,7 @@ mod tests {
}; };
let room2 = TestRoomItem { let room2 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "Room 2", name: "Room 2",
@@ -1731,7 +1759,7 @@ mod tests {
}; };
let room3 = TestRoomItem { let room3 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "Room 3", name: "Room 3",
@@ -1762,7 +1790,7 @@ mod tests {
let server = server_name!("example.com"); let server = server_name!("example.com");
let room1 = TestRoomItem { let room1 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "Old room 1", name: "Old room 1",
@@ -1771,7 +1799,7 @@ mod tests {
}; };
let room2 = TestRoomItem { let room2 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "Old room 2", name: "Old room 2",
@@ -1780,7 +1808,7 @@ mod tests {
}; };
let room3 = TestRoomItem { let room3 = TestRoomItem {
room_id: RoomId::new(server).to_owned(), room_id: RoomId::new_v1(server).to_owned(),
tags: vec![], tags: vec![],
alias: None, alias: None,
name: "New Fancy Room", name: "New Fancy Room",

View File

@@ -7,7 +7,9 @@ use std::path::{Path, PathBuf};
use edit::edit_with_builder as external_edit; use edit::edit_with_builder as external_edit;
use edit::Builder; use edit::Builder;
use matrix_sdk::EncryptionState;
use modalkit::editing::store::RegisterError; use modalkit::editing::store::RegisterError;
use ratatui::style::{Color, Style};
use std::process::Command; use std::process::Command;
use tokio; use tokio;
use url::Url; use url::Url;
@@ -220,12 +222,10 @@ impl ChatState {
}; };
let (source, msg_filename) = match &ev.content.msgtype { let (source, msg_filename) = match &ev.content.msgtype {
MessageType::Audio(c) => (c.source.clone(), c.body.as_str()), MessageType::Audio(c) => (c.source.clone(), c.filename()),
MessageType::File(c) => { MessageType::File(c) => (c.source.clone(), c.filename()),
(c.source.clone(), c.filename.as_deref().unwrap_or(c.body.as_str())) MessageType::Image(c) => (c.source.clone(), c.filename()),
}, MessageType::Video(c) => (c.source.clone(), c.filename()),
MessageType::Image(c) => (c.source.clone(), c.body.as_str()),
MessageType::Video(c) => (c.source.clone(), c.body.as_str()),
_ => { _ => {
if !flags.contains(DownloadFlags::OPEN) { if !flags.contains(DownloadFlags::OPEN) {
return Err(IambError::NoAttachment.into()); return Err(IambError::NoAttachment.into());
@@ -263,7 +263,7 @@ impl ChatState {
}; };
if filename.is_dir() { if filename.is_dir() {
filename.push(msg_filename); filename.push(msg_filename.replace(std::path::MAIN_SEPARATOR_STR, "_"));
} }
if filename.exists() && !flags.contains(DownloadFlags::FORCE) { if filename.exists() && !flags.contains(DownloadFlags::FORCE) {
@@ -450,6 +450,21 @@ impl ChatState {
Ok(None) Ok(None)
}, },
MessageAction::Replied => {
let Some(reply) = msg.reply_to() else {
let msg = "Selected message is not a reply";
return Err(UIError::Failure(msg.into()));
};
let Some(key) = info.get_message_key(&reply) else {
store.application.need_load.need_message(self.room_id.clone(), reply);
let msg = "Replied to message will be loaded in the background";
return Err(UIError::Failure(msg.into()));
};
self.scrollback.goto_message(key.clone());
Ok(None)
},
MessageAction::Unreact(reaction, literal) => { MessageAction::Unreact(reaction, literal) => {
let emoji = match reaction { let emoji = match reaction {
reaction if literal => reaction, reaction if literal => reaction,
@@ -612,8 +627,7 @@ impl ChatState {
let mut buff = std::io::Cursor::new(bytes); let mut buff = std::io::Cursor::new(bytes);
dynimage.write_to(&mut buff, image::ImageFormat::Png)?; dynimage.write_to(&mut buff, image::ImageFormat::Png)?;
Ok(buff.into_inner()) Ok(buff.into_inner())
}) })?;
.map_err(IambError::from)?;
let mime = mime::IMAGE_PNG; let mime = mime::IMAGE_PNG;
let name = "Clipboard.png"; let name = "Clipboard.png";
@@ -978,7 +992,16 @@ impl StatefulWidget for Chat<'_> {
Paragraph::new(desc_spans).render(descarea, buf); Paragraph::new(desc_spans).render(descarea, buf);
} }
let prompt = if self.focused { "> " } else { " " }; let prompt = match (self.focused, state.room().encryption_state()) {
(false, _) => Span::raw(" "),
(_, EncryptionState::Encrypted) => {
Span::styled("\u{1F512}\u{FE0E} ", Style::new().fg(Color::LightGreen))
},
(_, EncryptionState::NotEncrypted) => {
Span::styled("\u{1F513}\u{FE0E} ", Style::new().fg(Color::Red))
},
(_, EncryptionState::Unknown) => Span::styled("> ", Style::new().fg(Color::Red)),
};
let tbox = TextBox::new().prompt(prompt); let tbox = TextBox::new().prompt(prompt);
tbox.render(textarea, buf, &mut state.tbox); tbox.render(textarea, buf, &mut state.tbox);

View File

@@ -658,7 +658,7 @@ impl RoomState {
} }
} }
pub fn get_title(&self, store: &mut ProgramStore) -> Line { pub fn get_title(&self, store: &mut ProgramStore) -> Line<'_> {
let title = store.application.get_room_title(self.id()); let title = store.application.get_room_title(self.id());
let style = Style::default().add_modifier(StyleModifier::BOLD); let style = Style::default().add_modifier(StyleModifier::BOLD);
let mut spans = vec![]; let mut spans = vec![];

View File

@@ -47,7 +47,6 @@ use crate::{
IambId, IambId,
IambInfo, IambInfo,
IambResult, IambResult,
Need,
ProgramContext, ProgramContext,
ProgramStore, ProgramStore,
RoomFetchStatus, RoomFetchStatus,
@@ -165,6 +164,12 @@ impl ScrollbackState {
self.cursor = MessageCursor::latest(); self.cursor = MessageCursor::latest();
} }
pub fn goto_message(&mut self, target: MessageKey) {
let mut cursor = MessageCursor::new(target, 0);
std::mem::swap(&mut cursor, &mut self.cursor);
self.jumped.push(cursor);
}
/// Set the dimensions and placement within the terminal window for this list. /// Set the dimensions and placement within the terminal window for this list.
pub fn set_term_info(&mut self, area: Rect) { pub fn set_term_info(&mut self, area: Rect) {
self.viewctx.dimensions = (area.width as usize, area.height as usize); self.viewctx.dimensions = (area.width as usize, area.height as usize);
@@ -689,10 +694,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info); let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
if needs_load { if needs_load {
store store.application.need_load.need_messages(self.room_id.clone());
.application
.need_load
.insert(self.room_id.clone(), Need::MESSAGES);
} }
mc mc
}, },
@@ -768,10 +770,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info); let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
if needs_load { if needs_load {
store store.application.need_load.need_messages(self.room_id.to_owned());
.application
.need_load
.insert(self.room_id.to_owned(), Need::MESSAGES);
} }
mc.map(|c| self._range_to(c)) mc.map(|c| self._range_to(c))
@@ -1328,10 +1327,7 @@ impl StatefulWidget for Scrollback<'_> {
k k
} else { } else {
if state.need_more_messages(info) { if state.need_more_messages(info) {
self.store self.store.application.need_load.need_messages(state.room_id.to_owned());
.application
.need_load
.insert(state.room_id.to_owned(), Need::MESSAGES);
} }
return; return;
}; };
@@ -1435,10 +1431,7 @@ impl StatefulWidget for Scrollback<'_> {
// Check whether we should load older messages for this room. // Check whether we should load older messages for this room.
if state.need_more_messages(info) { if state.need_more_messages(info) {
// If the top of the screen is the older message, load more. // If the top of the screen is the older message, load more.
self.store self.store.application.need_load.need_messages(state.room_id.to_owned());
.application
.need_load
.insert(state.room_id.to_owned(), Need::MESSAGES);
} }
info.draw_last = self.store.application.draw_curr; info.draw_last = self.store.application.draw_curr;
@@ -1448,7 +1441,7 @@ impl StatefulWidget for Scrollback<'_> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::*; use crate::{base::Need, tests::*};
#[tokio::test] #[tokio::test]
async fn test_search_messages() { async fn test_search_messages() {
@@ -1493,7 +1486,7 @@ mod tests {
std::mem::take(&mut store.application.need_load) std::mem::take(&mut store.application.need_load)
.into_iter() .into_iter()
.collect::<Vec<(OwnedRoomId, Need)>>(), .collect::<Vec<(OwnedRoomId, Need)>>(),
vec![(room_id.clone(), Need::MESSAGES)] vec![(room_id.clone(), Need { messages: Some(Vec::new()), members: false })]
); );
// Search forward twice to MSG1. // Search forward twice to MSG1.

View File

@@ -1,9 +1,11 @@
//! Window for Matrix spaces //! Window for Matrix spaces
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent; use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
use matrix_sdk::ruma::events::StateEventType; use matrix_sdk::ruma::events::StateEventType;
use matrix_sdk::ruma::OwnedSpaceChildOrder;
use matrix_sdk::{ use matrix_sdk::{
room::Room as MatrixRoom, room::Room as MatrixRoom,
ruma::{OwnedRoomId, RoomId}, ruma::{OwnedRoomId, RoomId},
@@ -91,19 +93,25 @@ impl SpaceState {
SpaceAction::SetChild(child_id, order, suggested) => { SpaceAction::SetChild(child_id, order, suggested) => {
if !self if !self
.room .room
.can_user_send_state( .power_levels()
.await
.map_err(matrix_sdk::Error::from)
.map_err(IambError::from)?
.user_can_send_state(
&store.application.settings.profile.user_id, &store.application.settings.profile.user_id,
StateEventType::SpaceChild, StateEventType::SpaceChild,
) )
.await
.map_err(IambError::from)?
{ {
return Err(IambError::InsufficientPermission.into()); return Err(IambError::InsufficientPermission.into());
} }
let via = self.room.route().await.map_err(IambError::from)?; let via = self.room.route().await.map_err(IambError::from)?;
let mut ev = SpaceChildEventContent::new(via); let mut ev = SpaceChildEventContent::new(via);
ev.order = order; ev.order = order
.as_deref()
.map(OwnedSpaceChildOrder::from_str)
.transpose()
.map_err(IambError::InvalidSpaceChildOrder)?;
ev.suggested = suggested; ev.suggested = suggested;
let _ = self let _ = self
.room .room
@@ -117,12 +125,14 @@ impl SpaceState {
let space = self.list.get().ok_or(IambError::NoSelectedRoomOrSpaceItem)?; let space = self.list.get().ok_or(IambError::NoSelectedRoomOrSpaceItem)?;
if !self if !self
.room .room
.can_user_send_state( .power_levels()
.await
.map_err(matrix_sdk::Error::from)
.map_err(IambError::from)?
.user_can_send_state(
&store.application.settings.profile.user_id, &store.application.settings.profile.user_id,
StateEventType::SpaceChild, StateEventType::SpaceChild,
) )
.await
.map_err(IambError::from)?
{ {
return Err(IambError::InsufficientPermission.into()); return Err(IambError::InsufficientPermission.into());
} }

View File

@@ -88,7 +88,7 @@ use matrix_sdk::{
use modalkit::errors::UIError; use modalkit::errors::UIError;
use modalkit::prelude::{EditInfo, InfoMessage}; use modalkit::prelude::{EditInfo, InfoMessage};
use crate::base::Need; use crate::base::MessageNeed;
use crate::notifications::register_notifications; use crate::notifications::register_notifications;
use crate::{ use crate::{
base::{ base::{
@@ -216,7 +216,7 @@ async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id:
#[derive(Debug)] #[derive(Debug)]
enum Plan { enum Plan {
Messages(OwnedRoomId, Option<String>), Messages(OwnedRoomId, Option<String>, Vec<MessageNeed>),
Members(OwnedRoomId), Members(OwnedRoomId),
} }
@@ -225,8 +225,8 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
let ChatStore { need_load, rooms, .. } = &mut locked.application; let ChatStore { need_load, rooms, .. } = &mut locked.application;
let mut plan = Vec::with_capacity(need_load.rooms() * 2); let mut plan = Vec::with_capacity(need_load.rooms() * 2);
for (room_id, mut need) in std::mem::take(need_load).into_iter() { for (room_id, need) in std::mem::take(need_load).into_iter() {
if need.contains(Need::MESSAGES) { if let Some(message_need) = need.messages {
let info = rooms.get_or_default(room_id.clone()); let info = rooms.get_or_default(room_id.clone());
if !info.recently_fetched() && !info.fetching { if !info.recently_fetched() && !info.fetching {
@@ -239,16 +239,11 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
RoomFetchStatus::NotStarted => None, RoomFetchStatus::NotStarted => None,
}; };
plan.push(Plan::Messages(room_id.to_owned(), fetch_id)); plan.push(Plan::Messages(room_id.to_owned(), fetch_id, message_need));
need.remove(Need::MESSAGES);
} }
} }
if need.contains(Need::MEMBERS) { if need.members {
plan.push(Plan::Members(room_id.to_owned())); plan.push(Plan::Members(room_id.to_owned()));
need.remove(Need::MEMBERS);
}
if !need.is_empty() {
need_load.insert(room_id, need);
} }
} }
@@ -258,14 +253,14 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) { async fn run_plan(client: &Client, store: &AsyncProgramStore, plan: Plan, permits: &Semaphore) {
let permit = permits.acquire().await; let permit = permits.acquire().await;
match plan { match plan {
Plan::Messages(room_id, fetch_id) => { Plan::Messages(room_id, fetch_id, message_need) => {
let limit = MIN_MSG_LOAD; let limit = MIN_MSG_LOAD;
let client = client.clone(); let client = client.clone();
let store_clone = store.clone(); let store_clone = store.clone();
let res = load_older_one(&client, &room_id, fetch_id, limit).await; let res = load_older_one(&client, &room_id, fetch_id, limit).await;
let mut locked = store.lock().await; let mut locked = store.lock().await;
load_insert(room_id, res, locked.deref_mut(), store_clone); load_insert(room_id, res, locked.deref_mut(), store_clone, message_need);
}, },
Plan::Members(room_id) => { Plan::Members(room_id) => {
let res = members_load(client, &room_id).await; let res = members_load(client, &room_id).await;
@@ -283,6 +278,9 @@ async fn load_older_one(
limit: u32, limit: u32,
) -> MessageFetchResult { ) -> MessageFetchResult {
if let Some(room) = client.get_room(room_id) { if let Some(room) = client.get_room(room_id) {
// Update cached encryption state. This is a noop if the state is already cached.
let _ = room.request_encryption_state().await;
let mut opts = match &fetch_id { let mut opts = match &fetch_id {
Some(id) => MessagesOptions::backward().from(id.as_str()), Some(id) => MessagesOptions::backward().from(id.as_str()),
None => MessagesOptions::backward(), None => MessagesOptions::backward(),
@@ -325,6 +323,7 @@ fn load_insert(
res: MessageFetchResult, res: MessageFetchResult,
locked: &mut ProgramStore, locked: &mut ProgramStore,
store: AsyncProgramStore, store: AsyncProgramStore,
message_needs: Vec<MessageNeed>,
) { ) {
let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application; let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application;
let info = rooms.get_or_default(room_id.clone()); let info = rooms.get_or_default(room_id.clone());
@@ -370,12 +369,25 @@ fn load_insert(
} }
info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore); info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore);
// check if more are needed
let needs: Vec<_> = message_needs
.into_iter()
.filter(|need| !info.keys.contains_key(&need.event_id) && need.ttl > 0)
.map(|mut need| {
need.ttl -= 1;
need
})
.collect();
if !needs.is_empty() {
locked.application.need_load.need_messages_all(room_id, needs);
}
}, },
Err(e) => { Err(e) => {
warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages"); warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages");
// Wait and try again. // Wait and try again.
locked.application.need_load.insert(room_id, Need::MESSAGES); locked.application.need_load.need_messages_all(room_id, message_needs);
}, },
} }
} }
@@ -557,7 +569,7 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result
let mut filter = FilterDefinition::default(); let mut filter = FilterDefinition::default();
filter.room = room_ev; filter.room = room_ev;
let settings = SyncSettings::new().filter(filter.into()); let settings = SyncSettings::new().filter(filter.into()).timeout(Duration::from_secs(0));
client.sync_once(settings).await?; client.sync_once(settings).await?;
@@ -570,12 +582,12 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result
for room in sync_info.rooms.iter() { for room in sync_info.rooms.iter() {
let room_id = room.as_ref().0.room_id().to_owned(); let room_id = room.as_ref().0.room_id().to_owned();
need_load.insert(room_id, Need::MESSAGES); need_load.need_messages(room_id);
} }
for room in sync_info.dms.iter() { for room in sync_info.dms.iter() {
let room_id = room.as_ref().0.room_id().to_owned(); let room_id = room.as_ref().0.room_id().to_owned();
need_load.insert(room_id, Need::MESSAGES); need_load.need_messages(room_id);
} }
Ok(()) Ok(())
@@ -708,7 +720,7 @@ async fn create_client_inner(
.build() .build()
.unwrap(); .unwrap();
let req_config = RequestConfig::new().timeout(req_timeout).retry_timeout(req_timeout); let req_config = RequestConfig::new().timeout(req_timeout).max_retry_time(req_timeout);
// Set up the Matrix client for the selected profile. // Set up the Matrix client for the selected profile.
let builder = Client::builder() let builder = Client::builder()
@@ -1084,11 +1096,15 @@ impl ClientWorker {
async move { async move {
let room_id = room.room_id(); let room_id = room.room_id();
let room_info = room.clone_info(); let room_info = room.clone_info();
let room_version = room_info.room_version().unwrap_or(&RoomVersionId::V1); let rules = &room_info
.room_version()
.and_then(RoomVersionId::rules)
.unwrap_or(RoomVersionId::V1.rules().unwrap())
.redaction;
let mut locked = store.lock().await; let mut locked = store.lock().await;
let info = locked.application.get_room_info(room_id.to_owned()); let info = locked.application.get_room_info(room_id.to_owned());
info.redact(ev, room_version); info.redact(ev, rules);
} }
}, },
); );
@@ -1106,7 +1122,7 @@ impl ClientWorker {
ev.content.displayname.as_deref().unwrap_or_else(|| user_id.as_str()), ev.content.displayname.as_deref().unwrap_or_else(|| user_id.as_str()),
); );
let ambiguous = client let ambiguous = client
.store() .state_store()
.get_users_with_display_name(room_id, &ambiguous_name) .get_users_with_display_name(room_id, &ambiguous_name)
.await .await
.map(|users| users.len() > 1) .map(|users| users.len() > 1)
@@ -1244,7 +1260,7 @@ impl ClientWorker {
let settings = self.settings.clone(); let settings = self.settings.clone();
async move { async move {
while !client.logged_in() { while !client.is_active() {
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
@@ -1418,7 +1434,7 @@ impl ClientWorker {
let resp = self.client.send(req).await.map_err(IambError::from)?; let resp = self.client.send(req).await.map_err(IambError::from)?;
let rooms = resp.rooms.into_iter().map(|chunk| chunk.room_id).collect(); let rooms = resp.rooms.into_iter().map(|chunk| chunk.summary.room_id).collect();
Ok(rooms) Ok(rooms)
} }