Compare commits
25 Commits
v0.0.11-al
...
ulyssa/msr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d356e741 | ||
|
|
3149f79d11 | ||
|
|
7ccb1cbf2c | ||
|
|
1ec311590d | ||
|
|
0ddded3b8b | ||
|
|
a8cbc352ff | ||
|
|
dfa0937077 | ||
|
|
43485270ee | ||
|
|
28fea03625 | ||
|
|
e021d4a55d | ||
|
|
b01dbe5a5d | ||
|
|
4b2382bf93 | ||
|
|
0f2442566f | ||
|
|
8c9a2714a1 | ||
|
|
d44f861871 | ||
|
|
14aa97251c | ||
|
|
55456dbc1e | ||
|
|
d5c330ac72 | ||
|
|
7b1dc93f3a | ||
|
|
745f547904 | ||
|
|
6ebb7ac7fd | ||
|
|
1bb93c18fb | ||
|
|
e3090e537f | ||
|
|
ad10082c2f | ||
|
|
67603d0623 |
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -45,3 +45,25 @@ jobs:
|
||||
reporter: 'github-check'
|
||||
- name: Run tests
|
||||
run: cargo test --locked
|
||||
|
||||
nix-flake-test:
|
||||
name: Flake checks ❄️
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: iamb-prs
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Flake check
|
||||
run: |
|
||||
nix flake show
|
||||
nix flake check --print-build-logs
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
unstable_features = true
|
||||
max_width = 100
|
||||
fn_call_width = 90
|
||||
fn_call_width = 88
|
||||
struct_lit_width = 50
|
||||
struct_variant_width = 50
|
||||
chain_width = 75
|
||||
|
||||
599
Cargo.lock
generated
599
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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.89"
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
@@ -65,6 +65,7 @@ url = {version = "^2.2.2", features = ["serde"]}
|
||||
edit = "0.1.4"
|
||||
humansize = "2.0.0"
|
||||
linkify = "0.10.0"
|
||||
shellexpand = "3.1.1"
|
||||
|
||||
[dependencies.comrak]
|
||||
version = "0.22.0"
|
||||
@@ -78,18 +79,18 @@ features = ["zbus", "serde"]
|
||||
optional = true
|
||||
|
||||
[dependencies.modalkit]
|
||||
version = "0.0.23"
|
||||
version = "0.0.24"
|
||||
default-features = false
|
||||
#git = "https://github.com/ulyssa/modalkit"
|
||||
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
|
||||
|
||||
[dependencies.modalkit-ratatui]
|
||||
version = "0.0.23"
|
||||
version = "0.0.24"
|
||||
#git = "https://github.com/ulyssa/modalkit"
|
||||
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
version = "0.10.0"
|
||||
version = "0.14.0"
|
||||
default-features = false
|
||||
features = ["e2e-encryption", "sqlite", "sso-login"]
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -51,9 +51,18 @@ url = "https://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`)
|
||||
|
||||
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
|
||||
@@ -145,3 +154,4 @@ iamb is released under the [Apache License, Version 2.0].
|
||||
[crates-io-iamb]: https://crates.io/crates/iamb
|
||||
[iamb.chat]: https://iamb.chat
|
||||
[well_known_entry]: https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
|
||||
[rustup]: https://rustup.rs/
|
||||
|
||||
@@ -67,6 +67,8 @@ View a list of unread rooms.
|
||||
Mark all rooms as read.
|
||||
.It Sy ":welcome"
|
||||
View the startup Welcome window.
|
||||
.It Sy ":forget"
|
||||
Remove all left rooms from the internal database.
|
||||
.El
|
||||
|
||||
.Sh "E2EE COMMANDS"
|
||||
@@ -114,6 +116,8 @@ Redact the selected message with the optional reason.
|
||||
Reply to the selected message.
|
||||
.It Sy ":cancel"
|
||||
Cancel the currently drafted message including replies.
|
||||
.It Sy ":replied"
|
||||
Go to the message the current message replied to.
|
||||
.It Sy ":upload [path]"
|
||||
Upload an attachment and send it to the currently selected room.
|
||||
.El
|
||||
|
||||
84
flake.lock
generated
84
flake.lock
generated
@@ -1,5 +1,41 @@
|
||||
{
|
||||
"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": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
@@ -20,11 +56,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1736883708,
|
||||
"narHash": "sha256-uQ+NQ0/xYU0N1CnXsa2zghgNaOPxWpMJXSUJJ9W7140=",
|
||||
"lastModified": 1760284886,
|
||||
"narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "eb62e6aa39ea67e0b8018ba8ea077efe65807dc8",
|
||||
"rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -34,44 +70,28 @@
|
||||
"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": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1736994333,
|
||||
"narHash": "sha256-v4Jrok5yXsZ6dwj2+2uo5cSyUi9fBTurHqHvNHLT1XA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "848db855cb9e88785996e961951659570fc58814",
|
||||
"lastModified": 1760457219,
|
||||
"narHash": "sha256-WJOUGx42hrhmvvYcGkwea+BcJuQJLcns849OnewQqX4=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "8747cf81540bd1bbbab9ee2702f12c33aa887b46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
|
||||
117
flake.nix
117
flake.nix
@@ -5,41 +5,104 @@
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
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, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
crane,
|
||||
flake-utils,
|
||||
fenix,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
# We only need the nightly overlay in the devShell because .rs files are formatted with nightly.
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rustNightly = pkgs.rust-bin.nightly."2024-12-12".default;
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
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 ]);
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs) lib;
|
||||
|
||||
rustToolchain = fenix.packages.${system}.fromToolchainFile {
|
||||
file = ./rust-toolchain.toml;
|
||||
# When the file changes, this hash must be updated.
|
||||
sha256 = "sha256-Hn2uaQzRLidAWpfmRwSRdImifGUCAb9HeAqTYFXWeQk=";
|
||||
};
|
||||
|
||||
devShell = mkShell {
|
||||
buildInputs = [
|
||||
(rustNightly.override {
|
||||
extensions = [ "rust-src" "rust-analyzer-preview" "rustfmt" "clippy" ];
|
||||
})
|
||||
pkg-config
|
||||
# Nightly toolchain for rustfmt (pinned to current flake lock)
|
||||
# Note that the github CI uses "current nightly" for formatting, it 's not pinned.
|
||||
rustNightly = fenix.packages.${system}.latest.toolchain;
|
||||
rustNightlyFmt = fenix.packages.${system}.latest.rustfmt;
|
||||
|
||||
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-watch
|
||||
sqlite
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.83"
|
||||
channel = "1.89"
|
||||
components = [ "clippy" ]
|
||||
|
||||
98
src/base.rs
98
src/base.rs
@@ -13,6 +13,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use emojis::Emoji;
|
||||
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||
use matrix_sdk::ruma::room_version_rules::RedactionRules;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
@@ -57,7 +58,6 @@ use matrix_sdk::{
|
||||
OwnedRoomId,
|
||||
OwnedUserId,
|
||||
RoomId,
|
||||
RoomVersionId,
|
||||
UserId,
|
||||
},
|
||||
RoomState as MatrixRoomState,
|
||||
@@ -169,6 +169,9 @@ pub enum MessageAction {
|
||||
/// Reply to a message.
|
||||
Reply,
|
||||
|
||||
/// Go to the message the hovered message replied to.
|
||||
Replied,
|
||||
|
||||
/// Unreact to a message.
|
||||
///
|
||||
/// 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.
|
||||
CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags),
|
||||
Logout(String, bool),
|
||||
/// Forget all left rooms
|
||||
Forget,
|
||||
}
|
||||
|
||||
/// An action performed against the user's room keys.
|
||||
@@ -783,6 +788,10 @@ pub enum IambError {
|
||||
#[error("Invalid room alias id: {0}")]
|
||||
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.
|
||||
#[error("Verification request error: {0}")]
|
||||
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()?)
|
||||
}
|
||||
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
@@ -1033,20 +1042,20 @@ impl RoomInfo {
|
||||
Some(EventLocation::State(key)) => {
|
||||
if let Some(msg) = self.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
msg.redact(ev, room_version);
|
||||
msg.redact(ev, rules);
|
||||
}
|
||||
},
|
||||
Some(EventLocation::Message(None, key)) => {
|
||||
if let Some(msg) = self.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
msg.redact(ev, room_version);
|
||||
msg.redact(ev, rules);
|
||||
}
|
||||
},
|
||||
Some(EventLocation::Message(Some(root), key)) => {
|
||||
if let Some(thread) = self.threads.get_mut(root) {
|
||||
if let Some(msg) = thread.get_mut(key) {
|
||||
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.
|
||||
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 = 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) {
|
||||
(Some(((ts, recent), _)), Some(last_read)) => {
|
||||
UnreadInfo { unread: last_read != recent, latest: Some(*ts) }
|
||||
(Some(((ts, _), _)), Some((read_ts, _))) => {
|
||||
UnreadInfo { unread: ts > read_ts, latest: Some(*ts) }
|
||||
},
|
||||
(Some(((ts, _), _)), None) => {
|
||||
// If we've never loaded/generated a room's receipt (example,
|
||||
@@ -1477,14 +1506,19 @@ impl SyncInfo {
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Load-needs
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Need: u32 {
|
||||
const EMPTY = 0b00000000;
|
||||
const MESSAGES = 0b00000001;
|
||||
const MEMBERS = 0b00000010;
|
||||
}
|
||||
static MESSAGE_NEED_TTL: u8 = 30;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
/// Load messages until the event is loaded or `ttl` loads are exceeded
|
||||
pub struct MessageNeed {
|
||||
pub event_id: OwnedEventId,
|
||||
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.
|
||||
@@ -1494,9 +1528,31 @@ pub struct RoomNeeds {
|
||||
}
|
||||
|
||||
impl RoomNeeds {
|
||||
/// Mark a room for needing something to be loaded.
|
||||
pub fn insert(&mut self, room_id: OwnedRoomId, need: Need) {
|
||||
self.needs.entry(room_id).or_default().insert(need);
|
||||
/// Mark a room for needing to load members.
|
||||
pub fn need_members(&mut self, room_id: OwnedRoomId) {
|
||||
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 {
|
||||
@@ -2274,12 +2330,12 @@ pub mod tests {
|
||||
|
||||
let mut need_load = RoomNeeds::default();
|
||||
|
||||
need_load.insert(room_id.clone(), Need::MESSAGES);
|
||||
need_load.insert(room_id.clone(), Need::MEMBERS);
|
||||
need_load.need_messages(room_id.clone());
|
||||
need_load.need_members(room_id.clone());
|
||||
|
||||
assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![(
|
||||
room_id,
|
||||
Need::MESSAGES | Need::MEMBERS,
|
||||
Need { members: true, messages: Some(Vec::new()) }
|
||||
)],);
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,17 @@ fn iamb_leave(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||
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 {
|
||||
if !desc.arg.text.is_empty() {
|
||||
return Result::Err(CommandError::InvalidArgument);
|
||||
@@ -275,6 +286,17 @@ fn iamb_reply(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||
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 {
|
||||
if !desc.arg.text.is_empty() {
|
||||
return Result::Err(CommandError::InvalidArgument);
|
||||
@@ -731,6 +753,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
||||
aliases: vec![],
|
||||
f: iamb_leave,
|
||||
});
|
||||
cmds.add_command(ProgramCommand {
|
||||
name: "forget".into(),
|
||||
aliases: vec![],
|
||||
f: iamb_forget,
|
||||
});
|
||||
cmds.add_command(ProgramCommand {
|
||||
name: "members".into(),
|
||||
aliases: vec![],
|
||||
@@ -751,6 +778,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
||||
aliases: vec![],
|
||||
f: iamb_reply,
|
||||
});
|
||||
cmds.add_command(ProgramCommand {
|
||||
name: "replied".into(),
|
||||
aliases: vec![],
|
||||
f: iamb_replied,
|
||||
});
|
||||
cmds.add_command(ProgramCommand {
|
||||
name: "rooms".into(),
|
||||
aliases: vec![],
|
||||
|
||||
@@ -323,7 +323,7 @@ pub struct Session {
|
||||
impl From<Session> for MatrixSession {
|
||||
fn from(session: Session) -> Self {
|
||||
MatrixSession {
|
||||
tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens {
|
||||
tokens: matrix_sdk::authentication::SessionTokens {
|
||||
access_token: session.access_token,
|
||||
refresh_token: session.refresh_token,
|
||||
},
|
||||
@@ -483,6 +483,8 @@ pub struct Notifications {
|
||||
pub via: NotifyVia,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_message: bool,
|
||||
#[serde(default)]
|
||||
pub sound_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -705,11 +707,11 @@ impl DirectoryValues {
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Directories {
|
||||
pub cache: Option<PathBuf>,
|
||||
pub data: Option<PathBuf>,
|
||||
pub logs: Option<PathBuf>,
|
||||
pub downloads: Option<PathBuf>,
|
||||
pub image_previews: Option<PathBuf>,
|
||||
pub cache: Option<String>,
|
||||
pub data: Option<String>,
|
||||
pub logs: Option<String>,
|
||||
pub downloads: Option<String>,
|
||||
pub image_previews: Option<String>,
|
||||
}
|
||||
|
||||
impl Directories {
|
||||
@@ -726,6 +728,11 @@ impl Directories {
|
||||
fn values(self) -> DirectoryValues {
|
||||
let cache = self
|
||||
.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(|| {
|
||||
let mut dir = dirs::cache_dir()?;
|
||||
dir.push("iamb");
|
||||
@@ -735,6 +742,11 @@ impl Directories {
|
||||
|
||||
let data = self
|
||||
.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(|| {
|
||||
let mut dir = dirs::data_dir()?;
|
||||
dir.push("iamb");
|
||||
@@ -742,19 +754,40 @@ impl Directories {
|
||||
})
|
||||
.expect("no dirs.data value configured!");
|
||||
|
||||
let logs = self.logs.unwrap_or_else(|| {
|
||||
let mut dir = cache.clone();
|
||||
dir.push("logs");
|
||||
dir
|
||||
});
|
||||
let logs = self
|
||||
.logs
|
||||
.map(|dir| {
|
||||
let dir = shellexpand::full(&dir)
|
||||
.expect("unable to expand shell variables in dirs.cache");
|
||||
Path::new(dir.as_ref()).to_owned()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let mut dir = cache.clone();
|
||||
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 mut dir = cache.clone();
|
||||
dir.push("image_preview_downloads");
|
||||
dir
|
||||
});
|
||||
let image_previews = self
|
||||
.image_previews
|
||||
.map(|dir| {
|
||||
let dir = shellexpand::full(&dir)
|
||||
.expect("unable to expand shell variables in dirs.cache");
|
||||
Path::new(dir.as_ref()).to_owned()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let mut dir = cache.clone();
|
||||
dir.push("image_preview_downloads");
|
||||
dir
|
||||
});
|
||||
|
||||
DirectoryValues { cache, data, logs, downloads, image_previews }
|
||||
}
|
||||
@@ -1001,7 +1034,7 @@ impl ApplicationSettings {
|
||||
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
|
||||
.tunables
|
||||
.users
|
||||
|
||||
@@ -663,6 +663,13 @@ impl Application {
|
||||
|
||||
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 {
|
||||
Err(UIError::Application(IambError::Matrix(e))) => {
|
||||
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 {
|
||||
print_exit(e)
|
||||
}
|
||||
|
||||
@@ -820,7 +820,8 @@ fn h2t(hdl: &Handle, state: &mut TreeGenState) -> StyleTreeChildren {
|
||||
*c2t(&node.children.borrow(), state)
|
||||
},
|
||||
|
||||
_ => return vec![],
|
||||
// Treat unknown tags as plain text.
|
||||
_ => *c2t(&node.children.borrow(), state),
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::ops::{Deref, DerefMut};
|
||||
use chrono::{DateTime, Local as LocalTz};
|
||||
use humansize::{format_size, DECIMAL};
|
||||
use matrix_sdk::ruma::events::receipt::ReceiptThread;
|
||||
use matrix_sdk::ruma::room_version_rules::RedactionRules;
|
||||
use serde_json::json;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -43,7 +44,6 @@ use matrix_sdk::ruma::{
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
OwnedEventId,
|
||||
OwnedUserId,
|
||||
RoomVersionId,
|
||||
UInt,
|
||||
};
|
||||
|
||||
@@ -172,7 +172,8 @@ fn placeholder_frame(
|
||||
image_preview_size: &ImagePreviewSize,
|
||||
) -> Option<String> {
|
||||
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;
|
||||
}
|
||||
let mut placeholder = "\u{230c}".to_string();
|
||||
@@ -233,13 +234,13 @@ impl MessageTimeStamp {
|
||||
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();
|
||||
|
||||
Span::styled(time, BOLD_STYLE).into()
|
||||
}
|
||||
|
||||
fn show_time(&self) -> Option<Span> {
|
||||
fn show_time(&self) -> Option<Span<'_>> {
|
||||
match self {
|
||||
MessageTimeStamp::OriginServer(ms) => {
|
||||
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 {
|
||||
MessageEvent::EncryptedOriginal(_) => return,
|
||||
MessageEvent::EncryptedRedacted(_) => return,
|
||||
@@ -519,7 +520,7 @@ impl MessageEvent {
|
||||
MessageEvent::Local(_, _) => return,
|
||||
MessageEvent::Original(ev) => {
|
||||
let redacted = RedactedRoomMessageEvent {
|
||||
content: ev.content.clone().redact(version),
|
||||
content: ev.content.clone().redact(rules),
|
||||
event_id: ev.event_id.clone(),
|
||||
sender: ev.sender.clone(),
|
||||
origin_server_ts: ev.origin_server_ts,
|
||||
@@ -573,27 +574,18 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
|
||||
MessageType::Video(content) => {
|
||||
display_file_to_text!(Video, content);
|
||||
},
|
||||
_ => {
|
||||
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:?}]"));
|
||||
},
|
||||
}
|
||||
},
|
||||
_ => content.body(),
|
||||
};
|
||||
|
||||
Cow::Borrowed(s)
|
||||
}
|
||||
|
||||
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 {
|
||||
Cow::Owned(format!("[Redacted: {r:?}]"))
|
||||
@@ -740,22 +732,28 @@ impl<'a> MessageFormatter<'a> {
|
||||
info: &'a RoomInfo,
|
||||
settings: &'a ApplicationSettings,
|
||||
) -> Option<ProtocolPreview<'a>> {
|
||||
let reply_style = if settings.tunables.message_user_color {
|
||||
style.patch(settings.get_user_color(&msg.sender))
|
||||
} else {
|
||||
style
|
||||
};
|
||||
|
||||
let width = self.width();
|
||||
let 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 sender_width = UnicodeWidthStr::width(sender.content.as_ref());
|
||||
let trailing = w.saturating_sub(sender_width + 1);
|
||||
|
||||
sender.style = sender.style.patch(style);
|
||||
sender.style = sender.style.patch(reply_style);
|
||||
|
||||
self.push_spans(
|
||||
Line::from(vec![
|
||||
Span::styled(" ", style),
|
||||
Span::styled(THICK_VERTICAL, style),
|
||||
sender,
|
||||
Span::styled(":", style),
|
||||
space_span(trailing, style),
|
||||
Span::styled(":", reply_style),
|
||||
space_span(trailing, reply_style),
|
||||
]),
|
||||
style,
|
||||
text,
|
||||
@@ -774,7 +772,7 @@ impl<'a> MessageFormatter<'a> {
|
||||
line.spans.insert(0, Span::styled(" ", style));
|
||||
}
|
||||
|
||||
self.push_text(replied, style, text);
|
||||
self.push_text(replied, reply_style, text);
|
||||
|
||||
proto
|
||||
}
|
||||
@@ -1090,7 +1088,7 @@ impl Message {
|
||||
},
|
||||
ImageStatus::Loaded(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")),
|
||||
};
|
||||
@@ -1141,8 +1139,8 @@ impl Message {
|
||||
Span::styled(sender, style).into()
|
||||
}
|
||||
|
||||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
||||
self.event.redact(redaction, version);
|
||||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, rules: &RedactionRules) {
|
||||
self.event.redact(redaction, rules);
|
||||
self.html = None;
|
||||
self.downloaded = false;
|
||||
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: 4, height: 1 }), None);
|
||||
|
||||
@@ -12,6 +12,7 @@ use matrix_sdk::{
|
||||
RoomId,
|
||||
},
|
||||
Client,
|
||||
EncryptionState,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
@@ -50,6 +51,7 @@ pub async fn register_notifications(
|
||||
}
|
||||
let notify_via = settings.tunables.notifications.via;
|
||||
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 Some(startup_ts) = MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::now()) else {
|
||||
return;
|
||||
@@ -60,6 +62,7 @@ pub async fn register_notifications(
|
||||
.register_notification_handler(move |notification, room: MatrixRoom, client: Client| {
|
||||
let store = store.clone();
|
||||
let server_settings = server_settings.clone();
|
||||
let sound_hint = sound_hint.clone();
|
||||
async move {
|
||||
let mode = global_or_room_mode(&server_settings, &room).await;
|
||||
if mode == RoomNotificationMode::Mute {
|
||||
@@ -89,6 +92,7 @@ pub async fn register_notifications(
|
||||
body.as_deref(),
|
||||
room_id,
|
||||
&store,
|
||||
sound_hint.as_deref(),
|
||||
)
|
||||
.await;
|
||||
},
|
||||
@@ -113,10 +117,11 @@ async fn send_notification(
|
||||
body: Option<&str>,
|
||||
room_id: OwnedRoomId,
|
||||
store: &AsyncProgramStore,
|
||||
sound_hint: Option<&str>,
|
||||
) {
|
||||
#[cfg(feature = "desktop")]
|
||||
if via.desktop {
|
||||
send_notification_desktop(summary, body, room_id, store).await;
|
||||
send_notification_desktop(summary, body, room_id, store, sound_hint).await;
|
||||
}
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
{
|
||||
@@ -134,11 +139,13 @@ async fn send_notification_bell(store: &AsyncProgramStore) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
#[cfg_attr(target_os = "macos", allow(unused_variables))]
|
||||
async fn send_notification_desktop(
|
||||
summary: &str,
|
||||
body: Option<&str>,
|
||||
room_id: OwnedRoomId,
|
||||
_store: &AsyncProgramStore,
|
||||
sound_hint: Option<&str>,
|
||||
) {
|
||||
let mut desktop_notification = notify_rust::Notification::new();
|
||||
desktop_notification
|
||||
@@ -147,6 +154,10 @@ async fn send_notification_desktop(
|
||||
.icon(IAMB_XDG_NAME)
|
||||
.action("default", "default");
|
||||
|
||||
if let Some(sound_hint) = sound_hint {
|
||||
desktop_notification.sound_name(sound_hint);
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
desktop_notification.urgency(notify_rust::Urgency::Normal);
|
||||
|
||||
@@ -182,8 +193,8 @@ async fn global_or_room_mode(
|
||||
Ok(true) => IsOneToOne::Yes,
|
||||
_ => IsOneToOne::No,
|
||||
};
|
||||
let is_encrypted = match room.is_encrypted().await {
|
||||
Ok(true) => IsEncrypted::Yes,
|
||||
let is_encrypted = match room.latest_encryption_state().await {
|
||||
Ok(EncryptionState::Encrypted) => IsEncrypted::Yes,
|
||||
_ => IsEncrypted::No,
|
||||
};
|
||||
settings
|
||||
|
||||
@@ -49,7 +49,8 @@ use crate::{
|
||||
const TEST_ROOM1_ALIAS: &str = "#room1:example.com";
|
||||
|
||||
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_USER2: OwnedUserId = user_id!("@user2: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,
|
||||
via: NotifyVia::default(),
|
||||
show_message: true,
|
||||
sound_hint: None,
|
||||
},
|
||||
image_preview: None,
|
||||
user_gutter_width: 30,
|
||||
|
||||
@@ -66,7 +66,6 @@ use crate::base::{
|
||||
IambInfo,
|
||||
IambResult,
|
||||
MessageAction,
|
||||
Need,
|
||||
ProgramAction,
|
||||
ProgramContext,
|
||||
ProgramStore,
|
||||
@@ -97,12 +96,12 @@ fn bold_style() -> Style {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bold_span(s: &str) -> Span {
|
||||
fn bold_span(s: &str) -> Span<'_> {
|
||||
Span::styled(s, bold_style())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bold_spans(s: &str) -> Line {
|
||||
fn bold_spans(s: &str) -> Line<'_> {
|
||||
bold_span(s).into()
|
||||
}
|
||||
|
||||
@@ -116,12 +115,12 @@ fn selected_style(selected: bool) -> Style {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn selected_span(s: &str, selected: bool) -> Span {
|
||||
fn selected_span(s: &str, selected: bool) -> Span<'_> {
|
||||
Span::styled(s, selected_style(selected))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn selected_text(s: &str, selected: bool) -> Text {
|
||||
fn selected_text(s: &str, selected: bool) -> Text<'_> {
|
||||
Text::from(selected_span(s, selected))
|
||||
}
|
||||
|
||||
@@ -641,7 +640,7 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||
state.set(items);
|
||||
|
||||
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)
|
||||
.focus(focused)
|
||||
.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 {
|
||||
IambWindow::DirectList(_) => bold_spans("Direct Messages"),
|
||||
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 {
|
||||
IambWindow::DirectList(_) => bold_spans("Direct Messages"),
|
||||
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 = 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());
|
||||
},
|
||||
IambId::DirectList => {
|
||||
@@ -863,7 +862,7 @@ impl Window<IambInfo> for IambWindow {
|
||||
let (room, name, tags) = store.application.worker.get_room(room_id)?;
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -959,7 +958,12 @@ impl Display 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 style = selected_style(selected);
|
||||
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
||||
@@ -1073,7 +1077,12 @@ impl Display 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 style = selected_style(selected);
|
||||
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
||||
@@ -1177,7 +1186,12 @@ impl Display 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 style = selected_style(selected);
|
||||
let (name, mut labels) = name_and_labels(&self.name, unread, style);
|
||||
@@ -1280,7 +1294,12 @@ impl Display 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)
|
||||
}
|
||||
|
||||
@@ -1411,7 +1430,12 @@ impl Display 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 bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||
@@ -1521,7 +1545,7 @@ impl ListItem<IambInfo> for MemberItem {
|
||||
selected: bool,
|
||||
_: &ViewportContext<ListCursor>,
|
||||
store: &mut ProgramStore,
|
||||
) -> Text {
|
||||
) -> Text<'_> {
|
||||
let info = store.application.rooms.get_or_default(self.room_id.clone());
|
||||
let user_id = self.member.user_id();
|
||||
|
||||
@@ -1565,6 +1589,10 @@ impl ListItem<IambInfo> for MemberItem {
|
||||
fn get_word(&self) -> Option<String> {
|
||||
self.member.user_id().to_string().into()
|
||||
}
|
||||
|
||||
fn matches(&self, needle: ®ex::Regex) -> bool {
|
||||
needle.is_match(self.member.name()) || needle.is_match(self.member.user_id().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Promptable<ProgramContext, ProgramStore, IambInfo> for MemberItem {
|
||||
@@ -1644,7 +1672,7 @@ mod tests {
|
||||
let server = server_name!("example.com");
|
||||
|
||||
let room1 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![TagName::Favorite],
|
||||
alias: Some(room_alias_id!("#room1:example.com").to_owned()),
|
||||
name: "Z",
|
||||
@@ -1653,7 +1681,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let room2 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: Some(room_alias_id!("#a:example.com").to_owned()),
|
||||
name: "Unnamed Room",
|
||||
@@ -1662,7 +1690,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let room3 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Cool Room",
|
||||
@@ -1710,7 +1738,7 @@ mod tests {
|
||||
let server = server_name!("example.com");
|
||||
|
||||
let room1 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Room 1",
|
||||
@@ -1719,7 +1747,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let room2 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Room 2",
|
||||
@@ -1731,7 +1759,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let room3 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Room 3",
|
||||
@@ -1762,7 +1790,7 @@ mod tests {
|
||||
let server = server_name!("example.com");
|
||||
|
||||
let room1 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Old room 1",
|
||||
@@ -1771,7 +1799,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let room2 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "Old room 2",
|
||||
@@ -1780,7 +1808,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let room3 = TestRoomItem {
|
||||
room_id: RoomId::new(server).to_owned(),
|
||||
room_id: RoomId::new_v1(server).to_owned(),
|
||||
tags: vec![],
|
||||
alias: None,
|
||||
name: "New Fancy Room",
|
||||
|
||||
@@ -7,7 +7,9 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use edit::edit_with_builder as external_edit;
|
||||
use edit::Builder;
|
||||
use matrix_sdk::EncryptionState;
|
||||
use modalkit::editing::store::RegisterError;
|
||||
use ratatui::style::{Color, Style};
|
||||
use std::process::Command;
|
||||
use tokio;
|
||||
use url::Url;
|
||||
@@ -220,12 +222,10 @@ impl ChatState {
|
||||
};
|
||||
|
||||
let (source, msg_filename) = match &ev.content.msgtype {
|
||||
MessageType::Audio(c) => (c.source.clone(), c.body.as_str()),
|
||||
MessageType::File(c) => {
|
||||
(c.source.clone(), c.filename.as_deref().unwrap_or(c.body.as_str()))
|
||||
},
|
||||
MessageType::Image(c) => (c.source.clone(), c.body.as_str()),
|
||||
MessageType::Video(c) => (c.source.clone(), c.body.as_str()),
|
||||
MessageType::Audio(c) => (c.source.clone(), c.filename()),
|
||||
MessageType::File(c) => (c.source.clone(), c.filename()),
|
||||
MessageType::Image(c) => (c.source.clone(), c.filename()),
|
||||
MessageType::Video(c) => (c.source.clone(), c.filename()),
|
||||
_ => {
|
||||
if !flags.contains(DownloadFlags::OPEN) {
|
||||
return Err(IambError::NoAttachment.into());
|
||||
@@ -263,7 +263,7 @@ impl ChatState {
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -450,6 +450,21 @@ impl ChatState {
|
||||
|
||||
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) => {
|
||||
let emoji = match reaction {
|
||||
reaction if literal => reaction,
|
||||
@@ -612,8 +627,7 @@ impl ChatState {
|
||||
let mut buff = std::io::Cursor::new(bytes);
|
||||
dynimage.write_to(&mut buff, image::ImageFormat::Png)?;
|
||||
Ok(buff.into_inner())
|
||||
})
|
||||
.map_err(IambError::from)?;
|
||||
})?;
|
||||
let mime = mime::IMAGE_PNG;
|
||||
|
||||
let name = "Clipboard.png";
|
||||
@@ -978,7 +992,16 @@ impl StatefulWidget for Chat<'_> {
|
||||
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);
|
||||
tbox.render(textarea, buf, &mut state.tbox);
|
||||
|
||||
@@ -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 style = Style::default().add_modifier(StyleModifier::BOLD);
|
||||
let mut spans = vec![];
|
||||
|
||||
@@ -47,7 +47,6 @@ use crate::{
|
||||
IambId,
|
||||
IambInfo,
|
||||
IambResult,
|
||||
Need,
|
||||
ProgramContext,
|
||||
ProgramStore,
|
||||
RoomFetchStatus,
|
||||
@@ -165,6 +164,12 @@ impl ScrollbackState {
|
||||
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.
|
||||
pub fn set_term_info(&mut self, area: Rect) {
|
||||
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);
|
||||
if needs_load {
|
||||
store
|
||||
.application
|
||||
.need_load
|
||||
.insert(self.room_id.clone(), Need::MESSAGES);
|
||||
store.application.need_load.need_messages(self.room_id.clone());
|
||||
}
|
||||
mc
|
||||
},
|
||||
@@ -768,10 +770,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||
|
||||
let (mc, needs_load) = self.find_message(key, dir, &needle, count, info);
|
||||
if needs_load {
|
||||
store
|
||||
.application
|
||||
.need_load
|
||||
.insert(self.room_id.to_owned(), Need::MESSAGES);
|
||||
store.application.need_load.need_messages(self.room_id.to_owned());
|
||||
}
|
||||
|
||||
mc.map(|c| self._range_to(c))
|
||||
@@ -1328,10 +1327,7 @@ impl StatefulWidget for Scrollback<'_> {
|
||||
k
|
||||
} else {
|
||||
if state.need_more_messages(info) {
|
||||
self.store
|
||||
.application
|
||||
.need_load
|
||||
.insert(state.room_id.to_owned(), Need::MESSAGES);
|
||||
self.store.application.need_load.need_messages(state.room_id.to_owned());
|
||||
}
|
||||
return;
|
||||
};
|
||||
@@ -1435,10 +1431,7 @@ impl StatefulWidget for Scrollback<'_> {
|
||||
// Check whether we should load older messages for this room.
|
||||
if state.need_more_messages(info) {
|
||||
// If the top of the screen is the older message, load more.
|
||||
self.store
|
||||
.application
|
||||
.need_load
|
||||
.insert(state.room_id.to_owned(), Need::MESSAGES);
|
||||
self.store.application.need_load.need_messages(state.room_id.to_owned());
|
||||
}
|
||||
|
||||
info.draw_last = self.store.application.draw_curr;
|
||||
@@ -1448,7 +1441,7 @@ impl StatefulWidget for Scrollback<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::*;
|
||||
use crate::{base::Need, tests::*};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_messages() {
|
||||
@@ -1493,7 +1486,7 @@ mod tests {
|
||||
std::mem::take(&mut store.application.need_load)
|
||||
.into_iter()
|
||||
.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.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//! Window for Matrix spaces
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
|
||||
use matrix_sdk::ruma::events::StateEventType;
|
||||
use matrix_sdk::ruma::OwnedSpaceChildOrder;
|
||||
use matrix_sdk::{
|
||||
room::Room as MatrixRoom,
|
||||
ruma::{OwnedRoomId, RoomId},
|
||||
@@ -91,19 +93,25 @@ impl SpaceState {
|
||||
SpaceAction::SetChild(child_id, order, suggested) => {
|
||||
if !self
|
||||
.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,
|
||||
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.order = order
|
||||
.as_deref()
|
||||
.map(OwnedSpaceChildOrder::from_str)
|
||||
.transpose()
|
||||
.map_err(IambError::InvalidSpaceChildOrder)?;
|
||||
ev.suggested = suggested;
|
||||
let _ = self
|
||||
.room
|
||||
@@ -117,12 +125,14 @@ impl SpaceState {
|
||||
let space = self.list.get().ok_or(IambError::NoSelectedRoomOrSpaceItem)?;
|
||||
if !self
|
||||
.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,
|
||||
StateEventType::SpaceChild,
|
||||
)
|
||||
.await
|
||||
.map_err(IambError::from)?
|
||||
{
|
||||
return Err(IambError::InsufficientPermission.into());
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ use matrix_sdk::{
|
||||
use modalkit::errors::UIError;
|
||||
use modalkit::prelude::{EditInfo, InfoMessage};
|
||||
|
||||
use crate::base::Need;
|
||||
use crate::base::MessageNeed;
|
||||
use crate::notifications::register_notifications;
|
||||
use crate::{
|
||||
base::{
|
||||
@@ -216,7 +216,7 @@ async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id:
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Plan {
|
||||
Messages(OwnedRoomId, Option<String>),
|
||||
Messages(OwnedRoomId, Option<String>, Vec<MessageNeed>),
|
||||
Members(OwnedRoomId),
|
||||
}
|
||||
|
||||
@@ -225,8 +225,8 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
|
||||
let ChatStore { need_load, rooms, .. } = &mut locked.application;
|
||||
let mut plan = Vec::with_capacity(need_load.rooms() * 2);
|
||||
|
||||
for (room_id, mut need) in std::mem::take(need_load).into_iter() {
|
||||
if need.contains(Need::MESSAGES) {
|
||||
for (room_id, need) in std::mem::take(need_load).into_iter() {
|
||||
if let Some(message_need) = need.messages {
|
||||
let info = rooms.get_or_default(room_id.clone());
|
||||
|
||||
if !info.recently_fetched() && !info.fetching {
|
||||
@@ -239,16 +239,11 @@ async fn load_plans(store: &AsyncProgramStore) -> Vec<Plan> {
|
||||
RoomFetchStatus::NotStarted => None,
|
||||
};
|
||||
|
||||
plan.push(Plan::Messages(room_id.to_owned(), fetch_id));
|
||||
need.remove(Need::MESSAGES);
|
||||
plan.push(Plan::Messages(room_id.to_owned(), fetch_id, message_need));
|
||||
}
|
||||
}
|
||||
if need.contains(Need::MEMBERS) {
|
||||
if need.members {
|
||||
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) {
|
||||
let permit = permits.acquire().await;
|
||||
match plan {
|
||||
Plan::Messages(room_id, fetch_id) => {
|
||||
Plan::Messages(room_id, fetch_id, message_need) => {
|
||||
let limit = MIN_MSG_LOAD;
|
||||
let client = client.clone();
|
||||
let store_clone = store.clone();
|
||||
|
||||
let res = load_older_one(&client, &room_id, fetch_id, limit).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) => {
|
||||
let res = members_load(client, &room_id).await;
|
||||
@@ -283,6 +278,9 @@ async fn load_older_one(
|
||||
limit: u32,
|
||||
) -> MessageFetchResult {
|
||||
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 {
|
||||
Some(id) => MessagesOptions::backward().from(id.as_str()),
|
||||
None => MessagesOptions::backward(),
|
||||
@@ -325,6 +323,7 @@ fn load_insert(
|
||||
res: MessageFetchResult,
|
||||
locked: &mut ProgramStore,
|
||||
store: AsyncProgramStore,
|
||||
message_needs: Vec<MessageNeed>,
|
||||
) {
|
||||
let ChatStore { presences, rooms, worker, picker, settings, .. } = &mut locked.application;
|
||||
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);
|
||||
|
||||
// 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) => {
|
||||
warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages");
|
||||
|
||||
// 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();
|
||||
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?;
|
||||
|
||||
@@ -570,12 +582,12 @@ pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) -> Result
|
||||
|
||||
for room in sync_info.rooms.iter() {
|
||||
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() {
|
||||
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(())
|
||||
@@ -708,7 +720,7 @@ async fn create_client_inner(
|
||||
.build()
|
||||
.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.
|
||||
let builder = Client::builder()
|
||||
@@ -1084,11 +1096,15 @@ impl ClientWorker {
|
||||
async move {
|
||||
let room_id = room.room_id();
|
||||
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 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()),
|
||||
);
|
||||
let ambiguous = client
|
||||
.store()
|
||||
.state_store()
|
||||
.get_users_with_display_name(room_id, &ambiguous_name)
|
||||
.await
|
||||
.map(|users| users.len() > 1)
|
||||
@@ -1244,7 +1260,7 @@ impl ClientWorker {
|
||||
let settings = self.settings.clone();
|
||||
|
||||
async move {
|
||||
while !client.logged_in() {
|
||||
while !client.is_active() {
|
||||
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 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user