mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-20 05:39:52 -07:00
Support uploading and downloading message attachments (#13)
This commit is contained in:
parent
504b520fe1
commit
b6f4b03c12
14 changed files with 684 additions and 247 deletions
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -1019,15 +1019,6 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.1.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
@ -1137,6 +1128,8 @@ dependencies = [
|
||||||
"gethostname",
|
"gethostname",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"matrix-sdk",
|
"matrix-sdk",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"modalkit",
|
"modalkit",
|
||||||
"regex",
|
"regex",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
|
@ -1271,7 +1264,7 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
|
checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.2.6",
|
"hermit-abi",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
|
@ -1580,6 +1573,16 @@ version = "0.3.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -1658,11 +1661,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.14.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
|
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi 0.1.19",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2589,9 +2592,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.23.0"
|
version = "1.24.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
|
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -2599,9 +2602,7 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"parking_lot 0.12.1",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
|
@ -2752,6 +2753,15 @@ version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
|
|
|
@ -19,6 +19,8 @@ dirs = "4.0.0"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
gethostname = "0.4.1"
|
gethostname = "0.4.1"
|
||||||
matrix-sdk = {version = "0.6", default-features = false, features = ["e2e-encryption", "sled", "rustls-tls"]}
|
matrix-sdk = {version = "0.6", default-features = false, features = ["e2e-encryption", "sled", "rustls-tls"]}
|
||||||
|
mime = "^0.3.16"
|
||||||
|
mime_guess = "^2.0.4"
|
||||||
modalkit = "0.0.9"
|
modalkit = "0.0.9"
|
||||||
regex = "^1.5"
|
regex = "^1.5"
|
||||||
rpassword = "^7.2"
|
rpassword = "^7.2"
|
||||||
|
@ -26,7 +28,7 @@ serde = "^1.0"
|
||||||
serde_json = "^1.0"
|
serde_json = "^1.0"
|
||||||
sled = "0.34"
|
sled = "0.34"
|
||||||
thiserror = "^1.0.37"
|
thiserror = "^1.0.37"
|
||||||
tokio = {version = "1.17.0", features = ["full"]}
|
tokio = {version = "1.24.1", features = ["macros", "net", "rt-multi-thread", "sync", "time"]}
|
||||||
tracing = "~0.1.36"
|
tracing = "~0.1.36"
|
||||||
tracing-appender = "~0.2.2"
|
tracing-appender = "~0.2.2"
|
||||||
tracing-subscriber = "0.3.16"
|
tracing-subscriber = "0.3.16"
|
||||||
|
|
|
@ -63,8 +63,8 @@ two other TUI clients and Element Web:
|
||||||
| Typing Notification | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
| Typing Notification | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| E2E | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
| E2E | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| Replies | :x: ([#3]) | :heavy_check_mark: | :x: | :heavy_check_mark: |
|
| Replies | :x: ([#3]) | :heavy_check_mark: | :x: | :heavy_check_mark: |
|
||||||
| Attachment uploading | :x: ([#13]) | :x: | :heavy_check_mark: | :heavy_check_mark: |
|
| Attachment uploading | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| Attachment downloading | :x: ([#13]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
| Attachment downloading | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| Send stickers | :x: | :x: | :x: | :heavy_check_mark: |
|
| Send stickers | :x: | :x: | :x: | :heavy_check_mark: |
|
||||||
| Send formatted messages (markdown) | :x: ([#10]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
| Send formatted messages (markdown) | :x: ([#10]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| Rich Text Editor for formatted messages | :x: | :x: | :x: | :heavy_check_mark: |
|
| Rich Text Editor for formatted messages | :x: | :x: | :x: | :heavy_check_mark: |
|
||||||
|
|
53
src/base.rs
53
src/base.rs
|
@ -59,6 +59,11 @@ pub enum VerifyAction {
|
||||||
Mismatch,
|
Mismatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MessageAction {
|
||||||
|
Download(Option<String>, bool),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum SetRoomField {
|
pub enum SetRoomField {
|
||||||
Name(String),
|
Name(String),
|
||||||
|
@ -77,26 +82,46 @@ impl From<SetRoomField> for RoomAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum SendAction {
|
||||||
|
Submit,
|
||||||
|
Upload(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum IambAction {
|
pub enum IambAction {
|
||||||
|
Message(MessageAction),
|
||||||
Room(RoomAction),
|
Room(RoomAction),
|
||||||
|
Send(SendAction),
|
||||||
Verify(VerifyAction, String),
|
Verify(VerifyAction, String),
|
||||||
VerifyRequest(String),
|
VerifyRequest(String),
|
||||||
SendMessage(OwnedRoomId, String),
|
|
||||||
ToggleScrollbackFocus,
|
ToggleScrollbackFocus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<MessageAction> for IambAction {
|
||||||
|
fn from(act: MessageAction) -> Self {
|
||||||
|
IambAction::Message(act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<RoomAction> for IambAction {
|
impl From<RoomAction> for IambAction {
|
||||||
fn from(act: RoomAction) -> Self {
|
fn from(act: RoomAction) -> Self {
|
||||||
IambAction::Room(act)
|
IambAction::Room(act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SendAction> for IambAction {
|
||||||
|
fn from(act: SendAction) -> Self {
|
||||||
|
IambAction::Send(act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ApplicationAction for IambAction {
|
impl ApplicationAction for IambAction {
|
||||||
fn is_edit_sequence<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
fn is_edit_sequence<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
||||||
match self {
|
match self {
|
||||||
|
IambAction::Message(..) => SequenceStatus::Break,
|
||||||
IambAction::Room(..) => SequenceStatus::Break,
|
IambAction::Room(..) => SequenceStatus::Break,
|
||||||
IambAction::SendMessage(..) => SequenceStatus::Break,
|
IambAction::Send(..) => SequenceStatus::Break,
|
||||||
IambAction::ToggleScrollbackFocus => SequenceStatus::Break,
|
IambAction::ToggleScrollbackFocus => SequenceStatus::Break,
|
||||||
IambAction::Verify(..) => SequenceStatus::Break,
|
IambAction::Verify(..) => SequenceStatus::Break,
|
||||||
IambAction::VerifyRequest(..) => SequenceStatus::Break,
|
IambAction::VerifyRequest(..) => SequenceStatus::Break,
|
||||||
|
@ -105,8 +130,9 @@ impl ApplicationAction for IambAction {
|
||||||
|
|
||||||
fn is_last_action<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
fn is_last_action<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
||||||
match self {
|
match self {
|
||||||
|
IambAction::Message(..) => SequenceStatus::Atom,
|
||||||
IambAction::Room(..) => SequenceStatus::Atom,
|
IambAction::Room(..) => SequenceStatus::Atom,
|
||||||
IambAction::SendMessage(..) => SequenceStatus::Atom,
|
IambAction::Send(..) => SequenceStatus::Atom,
|
||||||
IambAction::ToggleScrollbackFocus => SequenceStatus::Atom,
|
IambAction::ToggleScrollbackFocus => SequenceStatus::Atom,
|
||||||
IambAction::Verify(..) => SequenceStatus::Atom,
|
IambAction::Verify(..) => SequenceStatus::Atom,
|
||||||
IambAction::VerifyRequest(..) => SequenceStatus::Atom,
|
IambAction::VerifyRequest(..) => SequenceStatus::Atom,
|
||||||
|
@ -115,8 +141,9 @@ impl ApplicationAction for IambAction {
|
||||||
|
|
||||||
fn is_last_selection<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
fn is_last_selection<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
||||||
match self {
|
match self {
|
||||||
|
IambAction::Message(..) => SequenceStatus::Ignore,
|
||||||
IambAction::Room(..) => SequenceStatus::Ignore,
|
IambAction::Room(..) => SequenceStatus::Ignore,
|
||||||
IambAction::SendMessage(..) => SequenceStatus::Ignore,
|
IambAction::Send(..) => SequenceStatus::Ignore,
|
||||||
IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore,
|
IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore,
|
||||||
IambAction::Verify(..) => SequenceStatus::Ignore,
|
IambAction::Verify(..) => SequenceStatus::Ignore,
|
||||||
IambAction::VerifyRequest(..) => SequenceStatus::Ignore,
|
IambAction::VerifyRequest(..) => SequenceStatus::Ignore,
|
||||||
|
@ -125,8 +152,9 @@ impl ApplicationAction for IambAction {
|
||||||
|
|
||||||
fn is_switchable<C: EditContext>(&self, _: &C) -> bool {
|
fn is_switchable<C: EditContext>(&self, _: &C) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
IambAction::Message(..) => false,
|
||||||
IambAction::Room(..) => false,
|
IambAction::Room(..) => false,
|
||||||
IambAction::SendMessage(..) => false,
|
IambAction::Send(..) => false,
|
||||||
IambAction::ToggleScrollbackFocus => false,
|
IambAction::ToggleScrollbackFocus => false,
|
||||||
IambAction::Verify(..) => false,
|
IambAction::Verify(..) => false,
|
||||||
IambAction::VerifyRequest(..) => false,
|
IambAction::VerifyRequest(..) => false,
|
||||||
|
@ -173,6 +201,21 @@ pub enum IambError {
|
||||||
#[error("Serialization/deserialization error: {0}")]
|
#[error("Serialization/deserialization error: {0}")]
|
||||||
Serde(#[from] serde_json::Error),
|
Serde(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Selected message does not have any attachments")]
|
||||||
|
NoAttachment,
|
||||||
|
|
||||||
|
#[error("No message currently selected")]
|
||||||
|
NoSelectedMessage,
|
||||||
|
|
||||||
|
#[error("Current window is not a room or space")]
|
||||||
|
NoSelectedRoomOrSpace,
|
||||||
|
|
||||||
|
#[error("Current window is not a room")]
|
||||||
|
NoSelectedRoom,
|
||||||
|
|
||||||
|
#[error("You need to join the room before you can do that")]
|
||||||
|
NotJoined,
|
||||||
|
|
||||||
#[error("Unknown room identifier: {0}")]
|
#[error("Unknown room identifier: {0}")]
|
||||||
UnknownRoom(OwnedRoomId),
|
UnknownRoom(OwnedRoomId),
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,12 @@ use modalkit::{
|
||||||
use crate::base::{
|
use crate::base::{
|
||||||
IambAction,
|
IambAction,
|
||||||
IambId,
|
IambId,
|
||||||
|
MessageAction,
|
||||||
ProgramCommand,
|
ProgramCommand,
|
||||||
ProgramCommands,
|
ProgramCommands,
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
RoomAction,
|
RoomAction,
|
||||||
|
SendAction,
|
||||||
SetRoomField,
|
SetRoomField,
|
||||||
VerifyAction,
|
VerifyAction,
|
||||||
};
|
};
|
||||||
|
@ -149,13 +151,43 @@ fn iamb_set(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
return Ok(step);
|
return Ok(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn iamb_upload(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
|
let mut args = desc.arg.strings()?;
|
||||||
|
|
||||||
|
if args.len() != 1 {
|
||||||
|
return Result::Err(CommandError::InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sact = SendAction::Upload(args.remove(0));
|
||||||
|
let iact = IambAction::from(sact);
|
||||||
|
let step = CommandStep::Continue(iact.into(), ctx.context.take());
|
||||||
|
|
||||||
|
return Ok(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iamb_download(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult {
|
||||||
|
let mut args = desc.arg.strings()?;
|
||||||
|
|
||||||
|
if args.len() > 1 {
|
||||||
|
return Result::Err(CommandError::InvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mact = MessageAction::Download(args.pop(), desc.bang);
|
||||||
|
let iact = IambAction::from(mact);
|
||||||
|
let step = CommandStep::Continue(iact.into(), ctx.context.take());
|
||||||
|
|
||||||
|
return Ok(step);
|
||||||
|
}
|
||||||
|
|
||||||
fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
fn add_iamb_commands(cmds: &mut ProgramCommands) {
|
||||||
cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms });
|
cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms });
|
||||||
|
cmds.add_command(ProgramCommand { names: vec!["download".into()], f: iamb_download });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join });
|
cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members });
|
cmds.add_command(ProgramCommand { names: vec!["members".into()], f: iamb_members });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms });
|
cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["set".into()], f: iamb_set });
|
cmds.add_command(ProgramCommand { names: vec!["set".into()], f: iamb_set });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces });
|
cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces });
|
||||||
|
cmds.add_command(ProgramCommand { names: vec!["upload".into()], f: iamb_upload });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify });
|
cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify });
|
||||||
cmds.add_command(ProgramCommand { names: vec!["welcome".into()], f: iamb_welcome });
|
cmds.add_command(ProgramCommand { names: vec!["welcome".into()], f: iamb_welcome });
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,7 +237,11 @@ impl Directories {
|
||||||
fn values(self) -> DirectoryValues {
|
fn values(self) -> DirectoryValues {
|
||||||
let cache = self
|
let cache = self
|
||||||
.cache
|
.cache
|
||||||
.or_else(dirs::cache_dir)
|
.or_else(|| {
|
||||||
|
let mut dir = dirs::cache_dir()?;
|
||||||
|
dir.push("iamb");
|
||||||
|
dir.into()
|
||||||
|
})
|
||||||
.expect("no dirs.cache value configured!");
|
.expect("no dirs.cache value configured!");
|
||||||
|
|
||||||
let logs = self.logs.unwrap_or_else(|| {
|
let logs = self.logs.unwrap_or_else(|| {
|
||||||
|
|
99
src/main.rs
99
src/main.rs
|
@ -9,6 +9,7 @@ use std::fs::{create_dir_all, File};
|
||||||
use std::io::{stdout, BufReader, Stdout};
|
use std::io::{stdout, BufReader, Stdout};
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -63,7 +64,6 @@ use crate::{
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
},
|
},
|
||||||
config::{ApplicationSettings, Iamb},
|
config::{ApplicationSettings, Iamb},
|
||||||
message::{Message, MessageContent, MessageTimeStamp},
|
|
||||||
windows::IambWindow,
|
windows::IambWindow,
|
||||||
worker::{ClientWorker, LoginStyle, Requester},
|
worker::{ClientWorker, LoginStyle, Requester},
|
||||||
};
|
};
|
||||||
|
@ -226,7 +226,7 @@ impl Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_run(
|
async fn action_run(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: ProgramAction,
|
action: ProgramAction,
|
||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
|
@ -257,7 +257,7 @@ impl Application {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Simple delegations.
|
// Simple delegations.
|
||||||
Action::Application(act) => self.iamb_run(act, ctx, store)?,
|
Action::Application(act) => self.iamb_run(act, ctx, store).await?,
|
||||||
Action::CommandBar(act) => self.screen.command_bar(&act, &ctx)?,
|
Action::CommandBar(act) => self.screen.command_bar(&act, &ctx)?,
|
||||||
Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?,
|
Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?,
|
||||||
Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?,
|
Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?,
|
||||||
|
@ -314,7 +314,7 @@ impl Application {
|
||||||
return Ok(info);
|
return Ok(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn iamb_run(
|
async fn iamb_run(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: IambAction,
|
action: IambAction,
|
||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
|
@ -327,24 +327,19 @@ impl Application {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
|
||||||
|
IambAction::Message(act) => {
|
||||||
|
self.screen.current_window_mut()?.message_command(act, ctx, store).await?
|
||||||
|
},
|
||||||
IambAction::Room(act) => {
|
IambAction::Room(act) => {
|
||||||
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store)?;
|
let acts = self.screen.current_window_mut()?.room_command(act, ctx, store).await?;
|
||||||
self.action_prepend(acts);
|
self.action_prepend(acts);
|
||||||
|
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
IambAction::Send(act) => {
|
||||||
IambAction::SendMessage(room_id, msg) => {
|
self.screen.current_window_mut()?.send_command(act, ctx, store).await?
|
||||||
let (event_id, msg) = self.worker.send_message(room_id.clone(), msg)?;
|
|
||||||
let user = store.application.settings.profile.user_id.clone();
|
|
||||||
let info = store.application.get_room_info(room_id);
|
|
||||||
let key = (MessageTimeStamp::LocalEcho, event_id);
|
|
||||||
let msg = MessageContent::Original(msg.into());
|
|
||||||
let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho);
|
|
||||||
info.messages.insert(key, msg);
|
|
||||||
|
|
||||||
None
|
|
||||||
},
|
},
|
||||||
|
|
||||||
IambAction::Verify(act, user_dev) => {
|
IambAction::Verify(act, user_dev) => {
|
||||||
if let Some(sas) = store.application.verifications.get(&user_dev) {
|
if let Some(sas) = store.application.verifications.get(&user_dev) {
|
||||||
self.worker.verify(act, sas.clone())?
|
self.worker.verify(act, sas.clone())?
|
||||||
|
@ -378,7 +373,7 @@ impl Application {
|
||||||
let mut keyskip = false;
|
let mut keyskip = false;
|
||||||
|
|
||||||
while let Some((action, ctx)) = self.action_pop(keyskip) {
|
while let Some((action, ctx)) = self.action_pop(keyskip) {
|
||||||
match self.action_run(action, ctx, locked.deref_mut()) {
|
match self.action_run(action, ctx, locked.deref_mut()).await {
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// Continue processing.
|
// Continue processing.
|
||||||
continue;
|
continue;
|
||||||
|
@ -408,7 +403,7 @@ impl Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn login(worker: Requester, settings: &ApplicationSettings) -> IambResult<()> {
|
async fn login(worker: Requester, settings: &ApplicationSettings) -> IambResult<()> {
|
||||||
println!("Logging in for {}...", settings.profile.user_id);
|
println!("Logging in for {}...", settings.profile.user_id);
|
||||||
|
|
||||||
if settings.session_json.is_file() {
|
if settings.session_json.is_file() {
|
||||||
|
@ -447,38 +442,15 @@ fn print_exit<T: Display, N>(v: T) -> N {
|
||||||
process::exit(2);
|
process::exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
async fn run(settings: ApplicationSettings) -> IambResult<()> {
|
||||||
async fn main() -> IambResult<()> {
|
|
||||||
// Parse command-line flags.
|
|
||||||
let iamb = Iamb::parse();
|
|
||||||
|
|
||||||
// Load configuration and set up the Matrix SDK.
|
|
||||||
let settings = ApplicationSettings::load(iamb).unwrap_or_else(print_exit);
|
|
||||||
|
|
||||||
// Set up the tracing subscriber so we can log client messages.
|
|
||||||
let log_prefix = format!("iamb-log-{}", settings.profile_name);
|
|
||||||
let log_dir = settings.dirs.logs.as_path();
|
|
||||||
|
|
||||||
create_dir_all(settings.matrix_dir.as_path())?;
|
|
||||||
create_dir_all(log_dir)?;
|
|
||||||
|
|
||||||
let appender = tracing_appender::rolling::daily(log_dir, log_prefix);
|
|
||||||
let (appender, _) = tracing_appender::non_blocking(appender);
|
|
||||||
|
|
||||||
let subscriber = FmtSubscriber::builder()
|
|
||||||
.with_writer(appender)
|
|
||||||
.with_max_level(Level::WARN)
|
|
||||||
.finish();
|
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
|
||||||
|
|
||||||
// Set up the async worker thread and global store.
|
// Set up the async worker thread and global store.
|
||||||
let worker = ClientWorker::spawn(settings.clone());
|
let worker = ClientWorker::spawn(settings.clone()).await;
|
||||||
let store = ChatStore::new(worker.clone(), settings.clone());
|
let store = ChatStore::new(worker.clone(), settings.clone());
|
||||||
let store = Store::new(store);
|
let store = Store::new(store);
|
||||||
let store = Arc::new(AsyncMutex::new(store));
|
let store = Arc::new(AsyncMutex::new(store));
|
||||||
worker.init(store.clone());
|
worker.init(store.clone());
|
||||||
|
|
||||||
login(worker, &settings).unwrap_or_else(print_exit);
|
login(worker, &settings).await.unwrap_or_else(print_exit);
|
||||||
|
|
||||||
// Make sure panics clean up the terminal properly.
|
// Make sure panics clean up the terminal properly.
|
||||||
let orig_hook = std::panic::take_hook();
|
let orig_hook = std::panic::take_hook();
|
||||||
|
@ -495,5 +467,44 @@ async fn main() -> IambResult<()> {
|
||||||
// We can now run the application.
|
// We can now run the application.
|
||||||
application.run().await?;
|
application.run().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> IambResult<()> {
|
||||||
|
// Parse command-line flags.
|
||||||
|
let iamb = Iamb::parse();
|
||||||
|
|
||||||
|
// Load configuration and set up the Matrix SDK.
|
||||||
|
let settings = ApplicationSettings::load(iamb).unwrap_or_else(print_exit);
|
||||||
|
|
||||||
|
// Set up the tracing subscriber so we can log client messages.
|
||||||
|
let log_prefix = format!("iamb-log-{}", settings.profile_name);
|
||||||
|
let log_dir = settings.dirs.logs.as_path();
|
||||||
|
|
||||||
|
create_dir_all(settings.matrix_dir.as_path())?;
|
||||||
|
create_dir_all(log_dir)?;
|
||||||
|
|
||||||
|
let appender = tracing_appender::rolling::daily(log_dir, log_prefix);
|
||||||
|
let (appender, guard) = tracing_appender::non_blocking(appender);
|
||||||
|
|
||||||
|
let subscriber = FmtSubscriber::builder()
|
||||||
|
.with_writer(appender)
|
||||||
|
.with_max_level(Level::TRACE)
|
||||||
|
.finish();
|
||||||
|
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.thread_name_fn(|| {
|
||||||
|
static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
|
||||||
|
format!("iamb-worker-{}", id)
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
rt.block_on(async move { run(settings).await })?;
|
||||||
|
|
||||||
|
drop(guard);
|
||||||
process::exit(0);
|
process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,46 +309,41 @@ pub enum MessageContent {
|
||||||
Redacted,
|
Redacted,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for MessageContent {
|
impl MessageContent {
|
||||||
fn as_ref(&self) -> &str {
|
pub fn show(&self) -> Cow<'_, str> {
|
||||||
match self {
|
match self {
|
||||||
MessageContent::Original(ev) => {
|
MessageContent::Original(ev) => {
|
||||||
match &ev.msgtype {
|
let s = match &ev.msgtype {
|
||||||
MessageType::Text(content) => {
|
MessageType::Text(content) => content.body.as_ref(),
|
||||||
return content.body.as_ref();
|
MessageType::Emote(content) => content.body.as_ref(),
|
||||||
},
|
MessageType::Notice(content) => content.body.as_str(),
|
||||||
MessageType::Emote(content) => {
|
MessageType::ServerNotice(content) => content.body.as_str(),
|
||||||
return content.body.as_ref();
|
|
||||||
},
|
|
||||||
MessageType::Notice(content) => {
|
|
||||||
return content.body.as_str();
|
|
||||||
},
|
|
||||||
MessageType::ServerNotice(_) => {
|
|
||||||
// XXX: implement
|
|
||||||
|
|
||||||
return "[server notice]";
|
|
||||||
},
|
|
||||||
MessageType::VerificationRequest(_) => {
|
MessageType::VerificationRequest(_) => {
|
||||||
// XXX: implement
|
// XXX: implement
|
||||||
|
|
||||||
return "[verification request]";
|
return Cow::Owned("[verification request]".into());
|
||||||
},
|
},
|
||||||
MessageType::Audio(..) => {
|
MessageType::Audio(content) => {
|
||||||
return "[audio]";
|
return Cow::Owned(format!("[Attached Audio: {}]", content.body));
|
||||||
},
|
},
|
||||||
MessageType::File(..) => {
|
MessageType::File(content) => {
|
||||||
return "[file]";
|
return Cow::Owned(format!("[Attached File: {}]", content.body));
|
||||||
},
|
},
|
||||||
MessageType::Image(..) => {
|
MessageType::Image(content) => {
|
||||||
return "[image]";
|
return Cow::Owned(format!("[Attached Image: {}]", content.body));
|
||||||
},
|
},
|
||||||
MessageType::Video(..) => {
|
MessageType::Video(content) => {
|
||||||
return "[video]";
|
return Cow::Owned(format!("[Attached Video: {}]", content.body));
|
||||||
},
|
},
|
||||||
_ => return "[unknown message type]",
|
_ => {
|
||||||
}
|
return Cow::Owned(format!("[Unknown message type: {:?}]", ev.msgtype()));
|
||||||
},
|
},
|
||||||
MessageContent::Redacted => "[redacted]",
|
};
|
||||||
|
|
||||||
|
Cow::Borrowed(s)
|
||||||
|
},
|
||||||
|
MessageContent::Redacted => Cow::Borrowed("[redacted]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,11 +353,12 @@ pub struct Message {
|
||||||
pub content: MessageContent,
|
pub content: MessageContent,
|
||||||
pub sender: OwnedUserId,
|
pub sender: OwnedUserId,
|
||||||
pub timestamp: MessageTimeStamp,
|
pub timestamp: MessageTimeStamp,
|
||||||
|
pub downloaded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
pub fn new(content: MessageContent, sender: OwnedUserId, timestamp: MessageTimeStamp) -> Self {
|
pub fn new(content: MessageContent, sender: OwnedUserId, timestamp: MessageTimeStamp) -> Self {
|
||||||
Message { content, sender, timestamp }
|
Message { content, sender, timestamp, downloaded: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(
|
pub fn show(
|
||||||
|
@ -373,7 +369,13 @@ impl Message {
|
||||||
settings: &ApplicationSettings,
|
settings: &ApplicationSettings,
|
||||||
) -> Text {
|
) -> Text {
|
||||||
let width = vwctx.get_width();
|
let width = vwctx.get_width();
|
||||||
let msg = self.as_ref();
|
let mut msg = self.content.show();
|
||||||
|
|
||||||
|
if self.downloaded {
|
||||||
|
msg.to_mut().push_str(" \u{2705}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = msg.as_ref();
|
||||||
|
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
|
|
||||||
|
@ -391,7 +393,7 @@ impl Message {
|
||||||
let lw = width - USER_GUTTER - TIME_GUTTER;
|
let lw = width - USER_GUTTER - TIME_GUTTER;
|
||||||
|
|
||||||
for (i, (line, w)) in wrap(msg, lw).enumerate() {
|
for (i, (line, w)) in wrap(msg, lw).enumerate() {
|
||||||
let line = Span::styled(line, style);
|
let line = Span::styled(line.to_string(), style);
|
||||||
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
@ -412,7 +414,7 @@ impl Message {
|
||||||
let lw = width - USER_GUTTER;
|
let lw = width - USER_GUTTER;
|
||||||
|
|
||||||
for (i, (line, w)) in wrap(msg, lw).enumerate() {
|
for (i, (line, w)) in wrap(msg, lw).enumerate() {
|
||||||
let line = Span::styled(line, style);
|
let line = Span::styled(line.to_string(), style);
|
||||||
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
let trailing = Span::styled(space(lw.saturating_sub(w)), style);
|
||||||
|
|
||||||
let prefix = if i == 0 {
|
let prefix = if i == 0 {
|
||||||
|
@ -478,15 +480,9 @@ impl From<MessageEvent> for Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for Message {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
self.content.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for Message {
|
impl ToString for Message {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
self.as_ref().to_string()
|
self.content.show().into_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
src/tests.rs
10
src/tests.rs
|
@ -1,6 +1,5 @@
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::sync_channel;
|
|
||||||
|
|
||||||
use matrix_sdk::ruma::{
|
use matrix_sdk::ruma::{
|
||||||
event_id,
|
event_id,
|
||||||
|
@ -16,6 +15,7 @@ use matrix_sdk::ruma::{
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use modalkit::tui::style::Color;
|
use modalkit::tui::style::Color;
|
||||||
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -154,9 +154,11 @@ pub fn mock_settings() -> ApplicationSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mock_store() -> ProgramStore {
|
pub async fn mock_store() -> ProgramStore {
|
||||||
let (tx, _) = sync_channel(5);
|
let (tx, _) = unbounded_channel();
|
||||||
let worker = Requester { tx };
|
let homeserver = Url::parse("https://localhost").unwrap();
|
||||||
|
let client = matrix_sdk::Client::new(homeserver).await.unwrap();
|
||||||
|
let worker = Requester { tx, client };
|
||||||
|
|
||||||
let mut store = ChatStore::new(worker, mock_settings());
|
let mut store = ChatStore::new(worker, mock_settings());
|
||||||
let room_id = TEST_ROOM1_ID.clone();
|
let room_id = TEST_ROOM1_ID.clone();
|
||||||
|
|
|
@ -54,13 +54,16 @@ use modalkit::{
|
||||||
use crate::base::{
|
use crate::base::{
|
||||||
ChatStore,
|
ChatStore,
|
||||||
IambBufferId,
|
IambBufferId,
|
||||||
|
IambError,
|
||||||
IambId,
|
IambId,
|
||||||
IambInfo,
|
IambInfo,
|
||||||
IambResult,
|
IambResult,
|
||||||
|
MessageAction,
|
||||||
ProgramAction,
|
ProgramAction,
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
RoomAction,
|
RoomAction,
|
||||||
|
SendAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::{room::RoomState, welcome::WelcomeState};
|
use self::{room::RoomState, welcome::WelcomeState};
|
||||||
|
@ -102,6 +105,20 @@ fn selected_text(s: &str, selected: bool) -> Text {
|
||||||
Text::from(selected_span(s, selected))
|
Text::from(selected_span(s, selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn room_cmp(a: &MatrixRoom, b: &MatrixRoom) -> Ordering {
|
||||||
|
let ca1 = a.canonical_alias();
|
||||||
|
let ca2 = b.canonical_alias();
|
||||||
|
|
||||||
|
let ord = match (ca1, ca2) {
|
||||||
|
(None, None) => Ordering::Equal,
|
||||||
|
(None, Some(_)) => Ordering::Greater,
|
||||||
|
(Some(_), None) => Ordering::Less,
|
||||||
|
(Some(ca1), Some(ca2)) => ca1.cmp(&ca2),
|
||||||
|
};
|
||||||
|
|
||||||
|
ord.then_with(|| a.room_id().cmp(b.room_id()))
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn room_prompt(
|
fn room_prompt(
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
|
@ -165,19 +182,42 @@ impl IambWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room_command(
|
pub async fn message_command(
|
||||||
|
&mut self,
|
||||||
|
act: MessageAction,
|
||||||
|
ctx: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
if let IambWindow::Room(w) = self {
|
||||||
|
w.message_command(act, ctx, store).await
|
||||||
|
} else {
|
||||||
|
return Err(IambError::NoSelectedRoom.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn room_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
act: RoomAction,
|
act: RoomAction,
|
||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
store: &mut ProgramStore,
|
store: &mut ProgramStore,
|
||||||
) -> IambResult<Vec<(Action<IambInfo>, ProgramContext)>> {
|
) -> IambResult<Vec<(Action<IambInfo>, ProgramContext)>> {
|
||||||
if let IambWindow::Room(w) = self {
|
if let IambWindow::Room(w) = self {
|
||||||
w.room_command(act, ctx, store)
|
w.room_command(act, ctx, store).await
|
||||||
} else {
|
} else {
|
||||||
let msg = "No room currently focused!";
|
return Err(IambError::NoSelectedRoomOrSpace.into());
|
||||||
let err = UIError::Failure(msg.into());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Err(err);
|
pub async fn send_command(
|
||||||
|
&mut self,
|
||||||
|
act: SendAction,
|
||||||
|
ctx: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
if let IambWindow::Room(w) = self {
|
||||||
|
w.send_command(act, ctx, store).await
|
||||||
|
} else {
|
||||||
|
return Err(IambError::NoSelectedRoom.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,8 +344,13 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||||
},
|
},
|
||||||
IambWindow::RoomList(state) => {
|
IambWindow::RoomList(state) => {
|
||||||
let joined = store.application.worker.joined_rooms();
|
let joined = store.application.worker.joined_rooms();
|
||||||
let items = joined.into_iter().map(|(id, name)| RoomItem::new(id, name, store));
|
let mut items = joined
|
||||||
state.set(items.collect());
|
.into_iter()
|
||||||
|
.map(|(id, name)| RoomItem::new(id, name, store))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
items.sort();
|
||||||
|
|
||||||
|
state.set(items);
|
||||||
|
|
||||||
List::new(store)
|
List::new(store)
|
||||||
.empty_message("You haven't joined any rooms yet")
|
.empty_message("You haven't joined any rooms yet")
|
||||||
|
@ -515,6 +560,26 @@ impl RoomItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for RoomItem {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.room.room_id() == other.room.room_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for RoomItem {}
|
||||||
|
|
||||||
|
impl Ord for RoomItem {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
room_cmp(&self.room, &other.room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for RoomItem {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.cmp(other).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToString for RoomItem {
|
impl ToString for RoomItem {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
return self.name.clone();
|
return self.name.clone();
|
||||||
|
@ -601,6 +666,26 @@ impl SpaceItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for SpaceItem {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.room.room_id() == other.room.room_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for SpaceItem {}
|
||||||
|
|
||||||
|
impl Ord for SpaceItem {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
room_cmp(&self.room, &other.room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for SpaceItem {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.cmp(other).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ToString for SpaceItem {
|
impl ToString for SpaceItem {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
return self.room.room_id().to_string();
|
return self.room.room_id().to_string();
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
|
attachment::AttachmentConfig,
|
||||||
|
media::{MediaFormat, MediaRequest},
|
||||||
room::Room as MatrixRoom,
|
room::Room as MatrixRoom,
|
||||||
ruma::{OwnedRoomId, RoomId},
|
ruma::{
|
||||||
|
events::room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
|
||||||
|
OwnedRoomId,
|
||||||
|
RoomId,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget};
|
|
||||||
|
|
||||||
use modalkit::{
|
use modalkit::{
|
||||||
|
tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget},
|
||||||
widgets::textbox::{TextBox, TextBoxState},
|
widgets::textbox::{TextBox, TextBoxState},
|
||||||
widgets::TerminalCursor,
|
widgets::TerminalCursor,
|
||||||
widgets::{PromptActions, WindowOps},
|
widgets::{PromptActions, WindowOps},
|
||||||
|
@ -18,10 +28,12 @@ use modalkit::editing::{
|
||||||
EditResult,
|
EditResult,
|
||||||
Editable,
|
Editable,
|
||||||
EditorAction,
|
EditorAction,
|
||||||
|
InfoMessage,
|
||||||
Jumpable,
|
Jumpable,
|
||||||
PromptAction,
|
PromptAction,
|
||||||
Promptable,
|
Promptable,
|
||||||
Scrollable,
|
Scrollable,
|
||||||
|
UIError,
|
||||||
},
|
},
|
||||||
base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle},
|
base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle},
|
||||||
context::Resolve,
|
context::Resolve,
|
||||||
|
@ -32,14 +44,19 @@ use modalkit::editing::{
|
||||||
use crate::base::{
|
use crate::base::{
|
||||||
IambAction,
|
IambAction,
|
||||||
IambBufferId,
|
IambBufferId,
|
||||||
|
IambError,
|
||||||
IambInfo,
|
IambInfo,
|
||||||
IambResult,
|
IambResult,
|
||||||
|
MessageAction,
|
||||||
ProgramAction,
|
ProgramAction,
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
RoomFocus,
|
RoomFocus,
|
||||||
|
SendAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::message::{Message, MessageContent, MessageTimeStamp};
|
||||||
|
|
||||||
use super::scrollback::{Scrollback, ScrollbackState};
|
use super::scrollback::{Scrollback, ScrollbackState};
|
||||||
|
|
||||||
pub struct ChatState {
|
pub struct ChatState {
|
||||||
|
@ -75,6 +92,169 @@ impl ChatState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn message_command(
|
||||||
|
&mut self,
|
||||||
|
act: MessageAction,
|
||||||
|
_: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
let client = &store.application.worker.client;
|
||||||
|
|
||||||
|
let settings = &store.application.settings;
|
||||||
|
let info = store.application.rooms.entry(self.room_id.clone()).or_default();
|
||||||
|
|
||||||
|
let msg = self.scrollback.get_mut(info).ok_or(IambError::NoSelectedMessage)?;
|
||||||
|
|
||||||
|
match act {
|
||||||
|
MessageAction::Download(filename, force) => {
|
||||||
|
if let MessageContent::Original(ev) = &msg.content {
|
||||||
|
let media = client.media();
|
||||||
|
|
||||||
|
let mut filename = match filename {
|
||||||
|
Some(f) => PathBuf::from(f),
|
||||||
|
None => settings.dirs.downloads.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let source = match &ev.msgtype {
|
||||||
|
MessageType::Audio(c) => {
|
||||||
|
if filename.is_dir() {
|
||||||
|
filename.push(c.body.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
c.source.clone()
|
||||||
|
},
|
||||||
|
MessageType::File(c) => {
|
||||||
|
if filename.is_dir() {
|
||||||
|
if let Some(name) = &c.filename {
|
||||||
|
filename.push(name);
|
||||||
|
} else {
|
||||||
|
filename.push(c.body.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.source.clone()
|
||||||
|
},
|
||||||
|
MessageType::Image(c) => {
|
||||||
|
if filename.is_dir() {
|
||||||
|
filename.push(c.body.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
c.source.clone()
|
||||||
|
},
|
||||||
|
MessageType::Video(c) => {
|
||||||
|
if filename.is_dir() {
|
||||||
|
filename.push(c.body.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
c.source.clone()
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(IambError::NoAttachment.into());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if !force && filename.exists() {
|
||||||
|
let msg = format!(
|
||||||
|
"The file {} already exists; use :download! to overwrite it.",
|
||||||
|
filename.display()
|
||||||
|
);
|
||||||
|
let err = UIError::Failure(msg);
|
||||||
|
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let req = MediaRequest { source, format: MediaFormat::File };
|
||||||
|
|
||||||
|
let bytes =
|
||||||
|
media.get_media_content(&req, true).await.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
fs::write(filename.as_path(), bytes.as_slice())?;
|
||||||
|
|
||||||
|
msg.downloaded = true;
|
||||||
|
|
||||||
|
let info = InfoMessage::from(format!(
|
||||||
|
"Attachment downloaded to {}",
|
||||||
|
filename.display()
|
||||||
|
));
|
||||||
|
|
||||||
|
return Ok(info.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(IambError::NoAttachment.into())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_command(
|
||||||
|
&mut self,
|
||||||
|
act: SendAction,
|
||||||
|
_: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
let room = store
|
||||||
|
.application
|
||||||
|
.worker
|
||||||
|
.client
|
||||||
|
.get_joined_room(self.id())
|
||||||
|
.ok_or(IambError::NotJoined)?;
|
||||||
|
|
||||||
|
let (event_id, msg) = match act {
|
||||||
|
SendAction::Submit => {
|
||||||
|
let msg = self.tbox.get_text();
|
||||||
|
|
||||||
|
if msg.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = TextMessageEventContent::plain(msg);
|
||||||
|
let msg = MessageType::Text(msg);
|
||||||
|
let msg = RoomMessageEventContent::new(msg);
|
||||||
|
|
||||||
|
// XXX: second parameter can be a locally unique transaction id.
|
||||||
|
// Useful for doing retries.
|
||||||
|
let resp = room.send(msg.clone(), None).await.map_err(IambError::from)?;
|
||||||
|
let event_id = resp.event_id;
|
||||||
|
|
||||||
|
// Clear the TextBoxState contents now that the message is sent.
|
||||||
|
self.tbox.reset();
|
||||||
|
|
||||||
|
(event_id, msg)
|
||||||
|
},
|
||||||
|
SendAction::Upload(file) => {
|
||||||
|
let path = Path::new(file.as_str());
|
||||||
|
let mime = mime_guess::from_path(path).first_or(mime::APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
|
let bytes = fs::read(path)?;
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.map(OsStr::to_string_lossy)
|
||||||
|
.unwrap_or_else(|| Cow::from("Attachment"));
|
||||||
|
let config = AttachmentConfig::new();
|
||||||
|
|
||||||
|
let resp = room
|
||||||
|
.send_attachment(name.as_ref(), &mime, bytes.as_ref(), config)
|
||||||
|
.await
|
||||||
|
.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
// Mock up the local echo message for the scrollback.
|
||||||
|
let msg = TextMessageEventContent::plain(format!("[Attached File: {}]", name));
|
||||||
|
let msg = MessageType::Text(msg);
|
||||||
|
let msg = RoomMessageEventContent::new(msg);
|
||||||
|
|
||||||
|
(resp.event_id, msg)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = store.application.settings.profile.user_id.clone();
|
||||||
|
let info = store.application.get_room_info(self.id().to_owned());
|
||||||
|
let key = (MessageTimeStamp::LocalEcho, event_id);
|
||||||
|
let msg = MessageContent::Original(msg.into());
|
||||||
|
let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho);
|
||||||
|
info.messages.insert(key, msg);
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn focus_toggle(&mut self) {
|
pub fn focus_toggle(&mut self) {
|
||||||
self.focus = match self.focus {
|
self.focus = match self.focus {
|
||||||
RoomFocus::Scrollback => RoomFocus::MessageBar,
|
RoomFocus::Scrollback => RoomFocus::MessageBar,
|
||||||
|
@ -229,17 +409,9 @@ impl PromptActions<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
||||||
ctx: &ProgramContext,
|
ctx: &ProgramContext,
|
||||||
_: &mut ProgramStore,
|
_: &mut ProgramStore,
|
||||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||||
let txt = self.tbox.reset_text();
|
let act = SendAction::Submit;
|
||||||
|
|
||||||
let act = if txt.is_empty() {
|
Ok(vec![(IambAction::from(act).into(), ctx.clone())])
|
||||||
vec![]
|
|
||||||
} else {
|
|
||||||
let act = IambAction::SendMessage(self.room_id.clone(), txt).into();
|
|
||||||
|
|
||||||
vec![(act, ctx.clone())]
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(act)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abort(
|
fn abort(
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use matrix_sdk::room::Room as MatrixRoom;
|
use matrix_sdk::{room::Room as MatrixRoom, ruma::RoomId, DisplayName};
|
||||||
use matrix_sdk::ruma::RoomId;
|
|
||||||
use matrix_sdk::DisplayName;
|
|
||||||
|
|
||||||
use modalkit::tui::{
|
use modalkit::tui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
|
@ -37,13 +35,16 @@ use modalkit::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::base::{
|
use crate::base::{
|
||||||
|
IambError,
|
||||||
IambId,
|
IambId,
|
||||||
IambInfo,
|
IambInfo,
|
||||||
IambResult,
|
IambResult,
|
||||||
|
MessageAction,
|
||||||
ProgramAction,
|
ProgramAction,
|
||||||
ProgramContext,
|
ProgramContext,
|
||||||
ProgramStore,
|
ProgramStore,
|
||||||
RoomAction,
|
RoomAction,
|
||||||
|
SendAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::chat::ChatState;
|
use self::chat::ChatState;
|
||||||
|
@ -92,7 +93,31 @@ impl RoomState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room_command(
|
pub async fn message_command(
|
||||||
|
&mut self,
|
||||||
|
act: MessageAction,
|
||||||
|
ctx: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
match self {
|
||||||
|
RoomState::Chat(chat) => chat.message_command(act, ctx, store).await,
|
||||||
|
RoomState::Space(_) => Err(IambError::NoSelectedMessage.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_command(
|
||||||
|
&mut self,
|
||||||
|
act: SendAction,
|
||||||
|
ctx: ProgramContext,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<EditInfo> {
|
||||||
|
match self {
|
||||||
|
RoomState::Chat(chat) => chat.send_command(act, ctx, store).await,
|
||||||
|
RoomState::Space(_) => Err(IambError::NoSelectedRoom.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn room_command(
|
||||||
&mut self,
|
&mut self,
|
||||||
act: RoomAction,
|
act: RoomAction,
|
||||||
_: ProgramContext,
|
_: ProgramContext,
|
||||||
|
|
|
@ -126,6 +126,14 @@ impl ScrollbackState {
|
||||||
self.viewctx.dimensions = (area.width as usize, area.height as usize);
|
self.viewctx.dimensions = (area.width as usize, area.height as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_mut<'a>(&mut self, info: &'a mut RoomInfo) -> Option<&'a mut Message> {
|
||||||
|
if let Some(k) = &self.cursor.timestamp {
|
||||||
|
info.messages.get_mut(k)
|
||||||
|
} else {
|
||||||
|
info.messages.last_entry().map(|o| o.into_mut())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn messages<'a>(
|
pub fn messages<'a>(
|
||||||
&self,
|
&self,
|
||||||
range: EditRange<MessageCursor>,
|
range: EditRange<MessageCursor>,
|
||||||
|
@ -389,7 +397,7 @@ impl ScrollbackState {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if needle.is_match(msg.as_ref()) {
|
if needle.is_match(msg.content.show().as_ref()) {
|
||||||
mc = MessageCursor::from(key.clone()).into();
|
mc = MessageCursor::from(key.clone()).into();
|
||||||
count -= 1;
|
count -= 1;
|
||||||
}
|
}
|
||||||
|
@ -413,7 +421,7 @@ impl ScrollbackState {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if needle.is_match(msg.as_ref()) {
|
if needle.is_match(msg.content.show().as_ref()) {
|
||||||
mc = MessageCursor::from(key.clone()).into();
|
mc = MessageCursor::from(key.clone()).into();
|
||||||
count -= 1;
|
count -= 1;
|
||||||
}
|
}
|
||||||
|
@ -659,7 +667,7 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
|
||||||
let mut yanked = EditRope::from("");
|
let mut yanked = EditRope::from("");
|
||||||
|
|
||||||
for (_, msg) in self.messages(range, info) {
|
for (_, msg) in self.messages(range, info) {
|
||||||
yanked += EditRope::from(msg.as_ref());
|
yanked += EditRope::from(msg.content.show().into_owned());
|
||||||
yanked += EditRope::from('\n');
|
yanked += EditRope::from('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1159,12 +1167,14 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let corner = &state.viewctx.corner;
|
let corner = &state.viewctx.corner;
|
||||||
let corner_key = match (&corner.timestamp, &cursor.timestamp) {
|
let corner_key = if let Some(k) = &corner.timestamp {
|
||||||
(_, None) => nth_key_before(cursor_key.clone(), height, info),
|
k.clone()
|
||||||
(None, _) => nth_key_before(cursor_key.clone(), height, info),
|
} else {
|
||||||
(Some(k), _) => k.clone(),
|
nth_key_before(cursor_key.clone(), height, info)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let full = cursor.timestamp.is_none();
|
||||||
|
|
||||||
let mut lines = vec![];
|
let mut lines = vec![];
|
||||||
let mut sawit = false;
|
let mut sawit = false;
|
||||||
let mut prev = None;
|
let mut prev = None;
|
||||||
|
@ -1175,8 +1185,10 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
||||||
|
|
||||||
prev = Some(item);
|
prev = Some(item);
|
||||||
|
|
||||||
|
let incomplete_ok = !full || !sel;
|
||||||
|
|
||||||
for (row, line) in txt.lines.into_iter().enumerate() {
|
for (row, line) in txt.lines.into_iter().enumerate() {
|
||||||
if sawit && lines.len() >= height {
|
if sawit && lines.len() >= height && incomplete_ok {
|
||||||
// Check whether we've seen the first line of the
|
// Check whether we've seen the first line of the
|
||||||
// selected message and can fill the screen.
|
// selected message and can fill the screen.
|
||||||
break;
|
break;
|
||||||
|
@ -1224,10 +1236,10 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::*;
|
use crate::tests::*;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_search_messages() {
|
async fn test_search_messages() {
|
||||||
let room_id = TEST_ROOM1_ID.clone();
|
let room_id = TEST_ROOM1_ID.clone();
|
||||||
let mut store = mock_store();
|
let mut store = mock_store().await;
|
||||||
let mut scrollback = ScrollbackState::new(room_id.clone());
|
let mut scrollback = ScrollbackState::new(room_id.clone());
|
||||||
let ctx = ProgramContext::default();
|
let ctx = ProgramContext::default();
|
||||||
|
|
||||||
|
@ -1268,9 +1280,9 @@ mod tests {
|
||||||
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_movement() {
|
async fn test_movement() {
|
||||||
let mut store = mock_store();
|
let mut store = mock_store().await;
|
||||||
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
||||||
let ctx = ProgramContext::default();
|
let ctx = ProgramContext::default();
|
||||||
|
|
||||||
|
@ -1302,9 +1314,9 @@ mod tests {
|
||||||
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
assert_eq!(scrollback.cursor, MSG1_KEY.clone().into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_dirscroll() {
|
async fn test_dirscroll() {
|
||||||
let mut store = mock_store();
|
let mut store = mock_store().await;
|
||||||
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
||||||
let ctx = ProgramContext::default();
|
let ctx = ProgramContext::default();
|
||||||
|
|
||||||
|
@ -1436,9 +1448,9 @@ mod tests {
|
||||||
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4));
|
assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_cursorpos() {
|
async fn test_cursorpos() {
|
||||||
let mut store = mock_store();
|
let mut store = mock_store().await;
|
||||||
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
||||||
let ctx = ProgramContext::default();
|
let ctx = ProgramContext::default();
|
||||||
|
|
||||||
|
|
169
src/worker.rs
169
src/worker.rs
|
@ -1,12 +1,14 @@
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::mpsc::{sync_channel, Receiver, RecvTimeoutError, SyncSender};
|
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gethostname::gethostname;
|
use gethostname::gethostname;
|
||||||
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ use matrix_sdk::{
|
||||||
VerificationMethod,
|
VerificationMethod,
|
||||||
},
|
},
|
||||||
room::{
|
room::{
|
||||||
message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
|
message::{MessageType, RoomMessageEventContent},
|
||||||
name::RoomNameEventContent,
|
name::RoomNameEventContent,
|
||||||
topic::RoomTopicEventContent,
|
topic::RoomTopicEventContent,
|
||||||
},
|
},
|
||||||
|
@ -41,7 +43,6 @@ use matrix_sdk::{
|
||||||
SyncMessageLikeEvent,
|
SyncMessageLikeEvent,
|
||||||
SyncStateEvent,
|
SyncStateEvent,
|
||||||
},
|
},
|
||||||
OwnedEventId,
|
|
||||||
OwnedRoomId,
|
OwnedRoomId,
|
||||||
OwnedRoomOrAliasId,
|
OwnedRoomOrAliasId,
|
||||||
OwnedUserId,
|
OwnedUserId,
|
||||||
|
@ -67,6 +68,7 @@ fn initial_devname() -> String {
|
||||||
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum LoginStyle {
|
pub enum LoginStyle {
|
||||||
SessionRestore(Session),
|
SessionRestore(Session),
|
||||||
Password(String),
|
Password(String),
|
||||||
|
@ -95,8 +97,6 @@ fn oneshot<T>() -> (ClientReply<T>, ClientResponse<T>) {
|
||||||
return (reply, response);
|
return (reply, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
type EchoPair = (OwnedEventId, RoomMessageEventContent);
|
|
||||||
|
|
||||||
pub enum WorkerTask {
|
pub enum WorkerTask {
|
||||||
DirectMessages(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
|
DirectMessages(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
|
||||||
Init(AsyncProgramStore, ClientReply<()>),
|
Init(AsyncProgramStore, ClientReply<()>),
|
||||||
|
@ -108,16 +108,101 @@ pub enum WorkerTask {
|
||||||
Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>),
|
Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>),
|
||||||
SpaceMembers(OwnedRoomId, ClientReply<IambResult<Vec<OwnedRoomId>>>),
|
SpaceMembers(OwnedRoomId, ClientReply<IambResult<Vec<OwnedRoomId>>>),
|
||||||
Spaces(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
|
Spaces(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
|
||||||
SendMessage(OwnedRoomId, String, ClientReply<IambResult<EchoPair>>),
|
|
||||||
SetRoom(OwnedRoomId, SetRoomField, ClientReply<IambResult<()>>),
|
SetRoom(OwnedRoomId, SetRoomField, ClientReply<IambResult<()>>),
|
||||||
TypingNotice(OwnedRoomId),
|
TypingNotice(OwnedRoomId),
|
||||||
Verify(VerifyAction, SasVerification, ClientReply<IambResult<EditInfo>>),
|
Verify(VerifyAction, SasVerification, ClientReply<IambResult<EditInfo>>),
|
||||||
VerifyRequest(OwnedUserId, ClientReply<IambResult<EditInfo>>),
|
VerifyRequest(OwnedUserId, ClientReply<IambResult<EditInfo>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for WorkerTask {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
match self {
|
||||||
|
WorkerTask::DirectMessages(_) => {
|
||||||
|
f.debug_tuple("WorkerTask::DirectMessages")
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::Init(_, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::Init")
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::LoadOlder(room_id, from, n, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::LoadOlder")
|
||||||
|
.field(room_id)
|
||||||
|
.field(from)
|
||||||
|
.field(n)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::Login(style, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::Login")
|
||||||
|
.field(style)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::GetRoom(room_id, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::GetRoom")
|
||||||
|
.field(room_id)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::JoinRoom(s, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::JoinRoom")
|
||||||
|
.field(s)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::JoinedRooms(_) => {
|
||||||
|
f.debug_tuple("WorkerTask::JoinedRooms").field(&format_args!("_")).finish()
|
||||||
|
},
|
||||||
|
WorkerTask::Members(room_id, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::Members")
|
||||||
|
.field(room_id)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::SpaceMembers(room_id, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::SpaceMembers")
|
||||||
|
.field(room_id)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::Spaces(_) => {
|
||||||
|
f.debug_tuple("WorkerTask::Spaces").field(&format_args!("_")).finish()
|
||||||
|
},
|
||||||
|
WorkerTask::SetRoom(room_id, field, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::SetRoom")
|
||||||
|
.field(room_id)
|
||||||
|
.field(field)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::TypingNotice(room_id) => {
|
||||||
|
f.debug_tuple("WorkerTask::TypingNotice").field(room_id).finish()
|
||||||
|
},
|
||||||
|
WorkerTask::Verify(act, sasv1, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::Verify")
|
||||||
|
.field(act)
|
||||||
|
.field(sasv1)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
WorkerTask::VerifyRequest(user_id, _) => {
|
||||||
|
f.debug_tuple("WorkerTask::VerifyRequest")
|
||||||
|
.field(user_id)
|
||||||
|
.field(&format_args!("_"))
|
||||||
|
.finish()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Requester {
|
pub struct Requester {
|
||||||
pub tx: SyncSender<WorkerTask>,
|
pub client: Client,
|
||||||
|
pub tx: UnboundedSender<WorkerTask>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Requester {
|
impl Requester {
|
||||||
|
@ -152,14 +237,6 @@ impl Requester {
|
||||||
return response.recv();
|
return response.recv();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_message(&self, room_id: OwnedRoomId, msg: String) -> IambResult<EchoPair> {
|
|
||||||
let (reply, response) = oneshot();
|
|
||||||
|
|
||||||
self.tx.send(WorkerTask::SendMessage(room_id, msg, reply)).unwrap();
|
|
||||||
|
|
||||||
return response.recv();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn direct_messages(&self) -> Vec<(MatrixRoom, DisplayName)> {
|
pub fn direct_messages(&self) -> Vec<(MatrixRoom, DisplayName)> {
|
||||||
let (reply, response) = oneshot();
|
let (reply, response) = oneshot();
|
||||||
|
|
||||||
|
@ -253,10 +330,8 @@ pub struct ClientWorker {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientWorker {
|
impl ClientWorker {
|
||||||
pub fn spawn(settings: ApplicationSettings) -> Requester {
|
pub async fn spawn(settings: ApplicationSettings) -> Requester {
|
||||||
let (tx, rx) = sync_channel(5);
|
let (tx, rx) = unbounded_channel();
|
||||||
|
|
||||||
let _ = tokio::spawn(async move {
|
|
||||||
let account = &settings.profile;
|
let account = &settings.profile;
|
||||||
|
|
||||||
// Set up a custom client that only uses HTTP/1.
|
// Set up a custom client that only uses HTTP/1.
|
||||||
|
@ -265,9 +340,10 @@ impl ClientWorker {
|
||||||
// will need to be revisited in the future.
|
// will need to be revisited in the future.
|
||||||
let http = reqwest::Client::builder()
|
let http = reqwest::Client::builder()
|
||||||
.user_agent(IAMB_USER_AGENT)
|
.user_agent(IAMB_USER_AGENT)
|
||||||
.timeout(Duration::from_secs(60))
|
.timeout(Duration::from_secs(30))
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
.pool_idle_timeout(Duration::from_secs(60))
|
||||||
.pool_max_idle_per_host(5)
|
.pool_max_idle_per_host(10)
|
||||||
|
.tcp_keepalive(Duration::from_secs(10))
|
||||||
.http1_only()
|
.http1_only()
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -279,9 +355,7 @@ impl ClientWorker {
|
||||||
.store_config(StoreConfig::default())
|
.store_config(StoreConfig::default())
|
||||||
.sled_store(settings.matrix_dir.as_path(), None)
|
.sled_store(settings.matrix_dir.as_path(), None)
|
||||||
.expect("Failed to setup up sled store for Matrix SDK")
|
.expect("Failed to setup up sled store for Matrix SDK")
|
||||||
.request_config(
|
.request_config(RequestConfig::new().timeout(REQ_TIMEOUT).retry_timeout(REQ_TIMEOUT))
|
||||||
RequestConfig::new().timeout(REQ_TIMEOUT).retry_timeout(REQ_TIMEOUT),
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to instantiate Matrix client");
|
.expect("Failed to instantiate Matrix client");
|
||||||
|
@ -289,24 +363,24 @@ impl ClientWorker {
|
||||||
let mut worker = ClientWorker {
|
let mut worker = ClientWorker {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
settings,
|
settings,
|
||||||
client,
|
client: client.clone(),
|
||||||
sync_handle: None,
|
sync_handle: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let _ = tokio::spawn(async move {
|
||||||
worker.work(rx).await;
|
worker.work(rx).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Requester { tx };
|
return Requester { client, tx };
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn work(&mut self, rx: Receiver<WorkerTask>) {
|
async fn work(&mut self, mut rx: UnboundedReceiver<WorkerTask>) {
|
||||||
loop {
|
loop {
|
||||||
let t = rx.recv_timeout(Duration::from_secs(1));
|
let t = rx.recv().await;
|
||||||
|
|
||||||
match t {
|
match t {
|
||||||
Ok(task) => self.run(task).await,
|
Some(task) => self.run(task).await,
|
||||||
Err(RecvTimeoutError::Timeout) => {},
|
None => {
|
||||||
Err(RecvTimeoutError::Disconnected) => {
|
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -364,10 +438,6 @@ impl ClientWorker {
|
||||||
assert!(self.initialized);
|
assert!(self.initialized);
|
||||||
reply.send(self.spaces().await);
|
reply.send(self.spaces().await);
|
||||||
},
|
},
|
||||||
WorkerTask::SendMessage(room_id, msg, reply) => {
|
|
||||||
assert!(self.initialized);
|
|
||||||
reply.send(self.send_message(room_id, msg).await);
|
|
||||||
},
|
|
||||||
WorkerTask::TypingNotice(room_id) => {
|
WorkerTask::TypingNotice(room_id) => {
|
||||||
assert!(self.initialized);
|
assert!(self.initialized);
|
||||||
self.typing_notice(room_id).await;
|
self.typing_notice(room_id).await;
|
||||||
|
@ -615,33 +685,6 @@ impl ClientWorker {
|
||||||
Ok(Some(InfoMessage::from("Successfully logged in!")))
|
Ok(Some(InfoMessage::from("Successfully logged in!")))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_message(&mut self, room_id: OwnedRoomId, msg: String) -> IambResult<EchoPair> {
|
|
||||||
let room = if let r @ Some(_) = self.client.get_joined_room(&room_id) {
|
|
||||||
r
|
|
||||||
} else if self.client.join_room_by_id(&room_id).await.is_ok() {
|
|
||||||
self.client.get_joined_room(&room_id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(room) = room {
|
|
||||||
let msg = TextMessageEventContent::plain(msg);
|
|
||||||
let msg = MessageType::Text(msg);
|
|
||||||
let msg = RoomMessageEventContent::new(msg);
|
|
||||||
|
|
||||||
// XXX: second parameter can be a locally unique transaction id.
|
|
||||||
// Useful for doing retries.
|
|
||||||
let resp = room.send(msg.clone(), None).await.map_err(IambError::from)?;
|
|
||||||
let event_id = resp.event_id;
|
|
||||||
|
|
||||||
// XXX: need to either give error messages and retry when needed!
|
|
||||||
|
|
||||||
return Ok((event_id, msg));
|
|
||||||
} else {
|
|
||||||
Err(IambError::UnknownRoom(room_id).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<(MatrixRoom, DisplayName)> {
|
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<(MatrixRoom, DisplayName)> {
|
||||||
for (room, name) in self.direct_messages().await {
|
for (room, name) in self.direct_messages().await {
|
||||||
if room.get_member(user.as_ref()).await.map_err(IambError::from)?.is_some() {
|
if room.get_member(user.as_ref()).await.map_err(IambError::from)?.is_some() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue