mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-19 05:09:51 -07:00
Support sending and displaying typing notifications (#9)
This commit is contained in:
parent
c744d74e42
commit
d038da6844
12 changed files with 348 additions and 30 deletions
|
@ -1 +1,27 @@
|
|||
# Contributing to iamb
|
||||
|
||||
## Building
|
||||
|
||||
You can build `iamb` locally by using `cargo build`.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
When making changes to `iamb`, please make sure to:
|
||||
|
||||
- Add new tests for fixed bugs and new features whenever possible
|
||||
- Add new documentation with new features
|
||||
|
||||
If you're adding a large amount of new code, please make sure to look at a test
|
||||
coverage report and ensure that your tests sufficiently cover your changes.
|
||||
|
||||
You can generate an HTML report with [cargo-tarpaulin] by running:
|
||||
|
||||
```
|
||||
% cargo tarpaulin --avoid-cfg-tarpaulin --out html
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
You can run the unit tests and documentation tests using `cargo test`.
|
||||
|
||||
[cargo-tarpaulin]: https://github.com/xd009642/tarpaulin
|
||||
|
|
|
@ -10,7 +10,7 @@ description = "A Matrix chat client that uses Vim keybindings"
|
|||
license = "Apache-2.0"
|
||||
exclude = [".github", "CONTRIBUTING.md"]
|
||||
keywords = ["matrix", "chat", "tui", "vim"]
|
||||
rust-version = "1.65"
|
||||
rust-version = "1.66"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
|
|
|
@ -24,7 +24,7 @@ You can create a basic configuration in `$CONFIG_DIR/iamb/config.json` that look
|
|||
"profiles": {
|
||||
"example.com": {
|
||||
"url": "https://example.com",
|
||||
"@user:example.com"
|
||||
"user_id": "@user:example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ two other TUI clients and Element Web:
|
|||
| Display read markers | :x: ([#11]) | :x: | :x: | :heavy_check_mark: |
|
||||
| Sending Invites | :x: ([#7]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Accepting Invites | :x: ([#7]) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Typing Notification | :x: ([#9]) | :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: |
|
||||
| Replies | :x: ([#3]) | :heavy_check_mark: | :x: | :heavy_check_mark: |
|
||||
| Attachment uploading | :x: ([#13]) | :x: | :heavy_check_mark: | :heavy_check_mark: |
|
||||
|
|
153
src/base.rs
153
src/base.rs
|
@ -8,7 +8,7 @@ use tracing::warn;
|
|||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::SasVerification,
|
||||
ruma::{OwnedRoomId, RoomId},
|
||||
ruma::{OwnedRoomId, OwnedUserId, RoomId},
|
||||
};
|
||||
|
||||
use modalkit::{
|
||||
|
@ -32,10 +32,16 @@ use modalkit::{
|
|||
},
|
||||
input::bindings::SequenceStatus,
|
||||
input::key::TerminalKey,
|
||||
tui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
text::{Span, Spans},
|
||||
widgets::{Paragraph, Widget},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
message::{Message, Messages},
|
||||
message::{user_style, Message, Messages},
|
||||
worker::Requester,
|
||||
ApplicationSettings,
|
||||
};
|
||||
|
@ -167,12 +173,84 @@ pub struct RoomInfo {
|
|||
pub messages: Messages,
|
||||
pub fetch_id: RoomFetchStatus,
|
||||
pub fetch_last: Option<Instant>,
|
||||
pub users_typing: Option<(Instant, Vec<OwnedUserId>)>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
fn recently_fetched(&self) -> bool {
|
||||
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
||||
}
|
||||
|
||||
fn get_typers(&self) -> &[OwnedUserId] {
|
||||
if let Some((t, users)) = &self.users_typing {
|
||||
if t.elapsed() < Duration::from_secs(4) {
|
||||
return users.as_ref();
|
||||
} else {
|
||||
return &[];
|
||||
}
|
||||
} else {
|
||||
return &[];
|
||||
}
|
||||
}
|
||||
|
||||
fn get_typing_spans(&self) -> Spans {
|
||||
let typers = self.get_typers();
|
||||
let n = typers.len();
|
||||
|
||||
match n {
|
||||
0 => Spans(vec![]),
|
||||
1 => {
|
||||
let user = typers[0].as_str();
|
||||
let user = Span::styled(user, user_style(user));
|
||||
|
||||
Spans(vec![user, Span::from(" is typing...")])
|
||||
},
|
||||
2 => {
|
||||
let user1 = typers[0].as_str();
|
||||
let user1 = Span::styled(user1, user_style(user1));
|
||||
|
||||
let user2 = typers[1].as_str();
|
||||
let user2 = Span::styled(user2, user_style(user2));
|
||||
|
||||
Spans(vec![
|
||||
user1,
|
||||
Span::raw(" and "),
|
||||
user2,
|
||||
Span::from(" are typing..."),
|
||||
])
|
||||
},
|
||||
n if n < 5 => Spans::from("Several people are typing..."),
|
||||
_ => Spans::from("Many people are typing..."),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_typing(&mut self, user_ids: Vec<OwnedUserId>) {
|
||||
self.users_typing = (Instant::now(), user_ids).into();
|
||||
}
|
||||
|
||||
pub fn render_typing(
|
||||
&mut self,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
settings: &ApplicationSettings,
|
||||
) -> Rect {
|
||||
if area.height <= 2 || area.width <= 20 {
|
||||
return area;
|
||||
}
|
||||
|
||||
if !settings.tunables.typing_notice_display {
|
||||
return area;
|
||||
}
|
||||
|
||||
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
||||
let bar = Rect::new(area.x, area.y + top.height, area.width, 1);
|
||||
|
||||
Paragraph::new(self.get_typing_spans())
|
||||
.alignment(Alignment::Center)
|
||||
.render(bar, buf);
|
||||
|
||||
return top;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChatStore {
|
||||
|
@ -326,3 +404,74 @@ impl ApplicationInfo for IambInfo {
|
|||
type WindowId = IambId;
|
||||
type ContentId = IambBufferId;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::tests::*;
|
||||
|
||||
#[test]
|
||||
fn test_typing_spans() {
|
||||
let mut info = RoomInfo::default();
|
||||
|
||||
let users0 = vec![];
|
||||
let users1 = vec![TEST_USER1.clone()];
|
||||
let users2 = vec![TEST_USER1.clone(), TEST_USER2.clone()];
|
||||
let users4 = vec![
|
||||
TEST_USER1.clone(),
|
||||
TEST_USER2.clone(),
|
||||
TEST_USER3.clone(),
|
||||
TEST_USER4.clone(),
|
||||
];
|
||||
let users5 = vec![
|
||||
TEST_USER1.clone(),
|
||||
TEST_USER2.clone(),
|
||||
TEST_USER3.clone(),
|
||||
TEST_USER4.clone(),
|
||||
TEST_USER5.clone(),
|
||||
];
|
||||
|
||||
// Nothing set.
|
||||
assert_eq!(info.users_typing, None);
|
||||
assert_eq!(info.get_typing_spans(), Spans(vec![]));
|
||||
|
||||
// Empty typing list.
|
||||
info.set_typing(users0);
|
||||
assert!(info.users_typing.is_some());
|
||||
assert_eq!(info.get_typing_spans(), Spans(vec![]));
|
||||
|
||||
// Single user typing.
|
||||
info.set_typing(users1);
|
||||
assert!(info.users_typing.is_some());
|
||||
assert_eq!(
|
||||
info.get_typing_spans(),
|
||||
Spans(vec![
|
||||
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
||||
Span::from(" is typing...")
|
||||
])
|
||||
);
|
||||
|
||||
// Two users typing.
|
||||
info.set_typing(users2);
|
||||
assert!(info.users_typing.is_some());
|
||||
assert_eq!(
|
||||
info.get_typing_spans(),
|
||||
Spans(vec![
|
||||
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
||||
Span::raw(" and "),
|
||||
Span::styled("@user2:example.com", user_style("@user2:example.com")),
|
||||
Span::raw(" are typing...")
|
||||
])
|
||||
);
|
||||
|
||||
// Four users typing.
|
||||
info.set_typing(users4);
|
||||
assert!(info.users_typing.is_some());
|
||||
assert_eq!(info.get_typing_spans(), Spans::from("Several people are typing..."));
|
||||
|
||||
// Five users typing.
|
||||
info.set_typing(users5);
|
||||
assert!(info.users_typing.is_some());
|
||||
assert_eq!(info.get_typing_spans(), Spans::from("Many people are typing..."));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,17 +69,73 @@ pub enum ConfigError {
|
|||
Invalid(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TunableValues {
|
||||
pub typing_notice: bool,
|
||||
pub typing_notice_display: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Tunables {
|
||||
pub typing_notice: Option<bool>,
|
||||
pub typing_notice_display: Option<bool>,
|
||||
}
|
||||
|
||||
impl Tunables {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Tunables {
|
||||
typing_notice: self.typing_notice.or(other.typing_notice),
|
||||
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||
}
|
||||
}
|
||||
|
||||
fn values(self) -> TunableValues {
|
||||
TunableValues {
|
||||
typing_notice: self.typing_notice.unwrap_or(true),
|
||||
typing_notice_display: self.typing_notice.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DirectoryValues {
|
||||
pub cache: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Directories {
|
||||
pub cache: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Directories {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Directories { cache: self.cache.or(other.cache) }
|
||||
}
|
||||
|
||||
fn values(self) -> DirectoryValues {
|
||||
DirectoryValues {
|
||||
cache: self
|
||||
.cache
|
||||
.or_else(dirs::cache_dir)
|
||||
.expect("no dirs.cache value configured!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct ProfileConfig {
|
||||
pub user_id: OwnedUserId,
|
||||
pub url: Url,
|
||||
pub settings: Option<Tunables>,
|
||||
pub dirs: Option<Directories>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct IambConfig {
|
||||
pub profiles: HashMap<String, ProfileConfig>,
|
||||
pub default_profile: Option<String>,
|
||||
pub cache: Option<PathBuf>,
|
||||
pub settings: Option<Tunables>,
|
||||
pub dirs: Option<Directories>,
|
||||
}
|
||||
|
||||
impl IambConfig {
|
||||
|
@ -103,10 +159,11 @@ impl IambConfig {
|
|||
#[derive(Clone)]
|
||||
pub struct ApplicationSettings {
|
||||
pub matrix_dir: PathBuf,
|
||||
pub cache_dir: PathBuf,
|
||||
pub session_json: PathBuf,
|
||||
pub profile_name: String,
|
||||
pub profile: ProfileConfig,
|
||||
pub tunables: TunableValues,
|
||||
pub dirs: DirectoryValues,
|
||||
}
|
||||
|
||||
impl ApplicationSettings {
|
||||
|
@ -122,12 +179,16 @@ impl ApplicationSettings {
|
|||
let mut config_json = config_dir.clone();
|
||||
config_json.push("config.json");
|
||||
|
||||
let IambConfig { mut profiles, default_profile, cache } =
|
||||
IambConfig::load(config_json.as_path())?;
|
||||
let IambConfig {
|
||||
mut profiles,
|
||||
default_profile,
|
||||
dirs,
|
||||
settings: global,
|
||||
} = IambConfig::load(config_json.as_path())?;
|
||||
|
||||
validate_profile_names(&profiles);
|
||||
|
||||
let (profile_name, profile) = if let Some(profile) = cli.profile.or(default_profile) {
|
||||
let (profile_name, mut profile) = if let Some(profile) = cli.profile.or(default_profile) {
|
||||
profiles.remove_entry(&profile).unwrap_or_else(|| {
|
||||
usage!(
|
||||
"No configured profile with the name {:?} in {}",
|
||||
|
@ -146,6 +207,10 @@ impl ApplicationSettings {
|
|||
);
|
||||
};
|
||||
|
||||
let tunables = global.unwrap_or_default();
|
||||
let tunables = profile.settings.take().unwrap_or_default().merge(tunables);
|
||||
let tunables = tunables.values();
|
||||
|
||||
let mut profile_dir = config_dir.clone();
|
||||
profile_dir.push("profiles");
|
||||
profile_dir.push(profile_name.as_str());
|
||||
|
@ -156,18 +221,17 @@ impl ApplicationSettings {
|
|||
let mut session_json = profile_dir;
|
||||
session_json.push("session.json");
|
||||
|
||||
let cache_dir = cache.unwrap_or_else(|| {
|
||||
let mut cache = dirs::cache_dir().expect("no user cache directory");
|
||||
cache.push("iamb");
|
||||
cache
|
||||
});
|
||||
let dirs = dirs.unwrap_or_default();
|
||||
let dirs = profile.dirs.take().unwrap_or_default().merge(dirs);
|
||||
let dirs = dirs.values();
|
||||
|
||||
let settings = ApplicationSettings {
|
||||
matrix_dir,
|
||||
cache_dir,
|
||||
session_json,
|
||||
profile_name,
|
||||
profile,
|
||||
tunables,
|
||||
dirs,
|
||||
};
|
||||
|
||||
Ok(settings)
|
||||
|
|
|
@ -450,7 +450,7 @@ async fn main() -> IambResult<()> {
|
|||
|
||||
// Set up the tracing subscriber so we can log client messages.
|
||||
let log_prefix = format!("iamb-log-{}", settings.profile_name);
|
||||
let mut log_dir = settings.cache_dir.clone();
|
||||
let mut log_dir = settings.dirs.cache.clone();
|
||||
log_dir.push("logs");
|
||||
|
||||
create_dir_all(settings.matrix_dir.as_path())?;
|
||||
|
|
|
@ -66,6 +66,18 @@ const USER_GUTTER_EMPTY_SPAN: Span<'static> = Span {
|
|||
},
|
||||
};
|
||||
|
||||
pub(crate) fn user_color(user: &str) -> Color {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
user.hash(&mut hasher);
|
||||
let color = hasher.finish() as usize % COLORS.len();
|
||||
|
||||
COLORS[color]
|
||||
}
|
||||
|
||||
pub(crate) fn user_style(user: &str) -> Style {
|
||||
Style::default().fg(user_color(user)).add_modifier(StyleModifier::BOLD)
|
||||
}
|
||||
|
||||
struct WrappedLinesIterator<'a> {
|
||||
iter: Lines<'a>,
|
||||
curr: Option<&'a str>,
|
||||
|
@ -446,13 +458,7 @@ impl Message {
|
|||
|
||||
fn show_sender(&self, align_right: bool) -> Span {
|
||||
let sender = self.sender.to_string();
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
sender.hash(&mut hasher);
|
||||
let color = hasher.finish() as usize % COLORS.len();
|
||||
let color = COLORS[color];
|
||||
|
||||
let bold = Style::default().fg(color).add_modifier(StyleModifier::BOLD);
|
||||
let style = user_style(sender.as_str());
|
||||
|
||||
let sender = if align_right {
|
||||
format!("{: >width$} ", sender, width = 28)
|
||||
|
@ -460,7 +466,7 @@ impl Message {
|
|||
format!("{: <width$} ", sender, width = 28)
|
||||
};
|
||||
|
||||
Span::styled(sender, bold)
|
||||
Span::styled(sender, style)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
11
src/tests.rs
11
src/tests.rs
|
@ -20,7 +20,7 @@ use lazy_static::lazy_static;
|
|||
|
||||
use crate::{
|
||||
base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo},
|
||||
config::{ApplicationSettings, ProfileConfig},
|
||||
config::{ApplicationSettings, DirectoryValues, ProfileConfig, TunableValues},
|
||||
message::{
|
||||
Message,
|
||||
MessageContent,
|
||||
|
@ -35,6 +35,9 @@ lazy_static! {
|
|||
pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(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!("@user2:example.com").to_owned();
|
||||
pub static ref TEST_USER4: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
||||
pub static ref TEST_USER5: OwnedUserId = user_id!("@user2:example.com").to_owned();
|
||||
pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com")));
|
||||
pub static ref MSG2_KEY: MessageKey =
|
||||
(OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com")));
|
||||
|
@ -103,19 +106,23 @@ pub fn mock_room() -> RoomInfo {
|
|||
messages: mock_messages(),
|
||||
fetch_id: RoomFetchStatus::NotStarted,
|
||||
fetch_last: None,
|
||||
users_typing: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_settings() -> ApplicationSettings {
|
||||
ApplicationSettings {
|
||||
matrix_dir: PathBuf::new(),
|
||||
cache_dir: PathBuf::new(),
|
||||
session_json: PathBuf::new(),
|
||||
profile_name: "test".into(),
|
||||
profile: ProfileConfig {
|
||||
user_id: user_id!("@user:example.com").to_owned(),
|
||||
url: Url::parse("https://example.com").unwrap(),
|
||||
settings: None,
|
||||
dirs: None,
|
||||
},
|
||||
tunables: TunableValues { typing_notice: true, typing_notice_display: true },
|
||||
dirs: DirectoryValues { cache: PathBuf::new() },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,27 @@ impl ChatState {
|
|||
pub fn id(&self) -> &RoomId {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
pub fn typing_notice(
|
||||
&self,
|
||||
act: &EditorAction,
|
||||
ctx: &ProgramContext,
|
||||
store: &mut ProgramStore,
|
||||
) {
|
||||
if !self.focus.is_msgbar() || act.is_readonly(ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(act, EditorAction::History(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !store.application.settings.tunables.typing_notice {
|
||||
return;
|
||||
}
|
||||
|
||||
store.application.worker.typing_notice(self.room_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! delegate {
|
||||
|
@ -148,6 +169,8 @@ impl Editable<ProgramContext, ProgramStore, IambInfo> for ChatState {
|
|||
ctx: &ProgramContext,
|
||||
store: &mut ProgramStore,
|
||||
) -> EditResult<EditInfo, IambInfo> {
|
||||
self.typing_notice(act, ctx, store);
|
||||
|
||||
match delegate!(self, w => w.editor_command(act, ctx, store)) {
|
||||
res @ Ok(_) => res,
|
||||
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, focus)))
|
||||
|
|
|
@ -1125,6 +1125,9 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
|||
type State = ScrollbackState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let info = self.store.application.rooms.entry(state.room_id.clone()).or_default();
|
||||
let area = info.render_typing(area, buf, &self.store.application.settings);
|
||||
|
||||
state.set_term_info(area);
|
||||
|
||||
let height = state.viewctx.get_height();
|
||||
|
@ -1137,8 +1140,6 @@ impl<'a> StatefulWidget for Scrollback<'a> {
|
|||
state.viewctx.corner = state.cursor.clone();
|
||||
}
|
||||
|
||||
let info = self.store.application.get_room_info(state.room_id.clone());
|
||||
|
||||
let cursor = &state.cursor;
|
||||
let cursor_key = if let Some(k) = cursor.to_key(info) {
|
||||
k
|
||||
|
@ -1297,6 +1298,9 @@ mod tests {
|
|||
let prev = MoveDir2D::Up;
|
||||
let next = MoveDir2D::Down;
|
||||
|
||||
// Skip rendering typing notices.
|
||||
store.application.settings.tunables.typing_notice_display = false;
|
||||
|
||||
assert_eq!(scrollback.cursor, MessageCursor::latest());
|
||||
assert_eq!(scrollback.viewctx.dimensions, (0, 0));
|
||||
assert_eq!(scrollback.viewctx.corner, MessageCursor::latest());
|
||||
|
@ -1425,6 +1429,9 @@ mod tests {
|
|||
let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone());
|
||||
let ctx = ProgramContext::default();
|
||||
|
||||
// Skip rendering typing notices.
|
||||
store.application.settings.tunables.typing_notice_display = false;
|
||||
|
||||
// Set a terminal width of 60, and height of 3, rendering in scrollback as:
|
||||
//
|
||||
// |------------------------------------------------------------|
|
||||
|
|
|
@ -21,8 +21,10 @@ pub struct WelcomeState {
|
|||
impl WelcomeState {
|
||||
pub fn new(store: &mut ProgramStore) -> Self {
|
||||
let buf = store.buffers.load_str(IambBufferId::Welcome, WELCOME_TEXT);
|
||||
let mut tbox = TextBoxState::new(buf);
|
||||
tbox.set_readonly(true);
|
||||
|
||||
WelcomeState { tbox: TextBoxState::new(buf) }
|
||||
WelcomeState { tbox }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ use matrix_sdk::{
|
|||
},
|
||||
room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent},
|
||||
room::name::RoomNameEventContent,
|
||||
typing::SyncTypingEvent,
|
||||
AnyMessageLikeEvent,
|
||||
AnyTimelineEvent,
|
||||
SyncMessageLikeEvent,
|
||||
|
@ -104,6 +105,7 @@ pub enum WorkerTask {
|
|||
SpaceMembers(OwnedRoomId, ClientReply<IambResult<Vec<OwnedRoomId>>>),
|
||||
Spaces(ClientReply<Vec<(MatrixRoom, DisplayName)>>),
|
||||
SendMessage(OwnedRoomId, String, ClientReply<IambResult<EchoPair>>),
|
||||
TypingNotice(OwnedRoomId),
|
||||
Verify(VerifyAction, SasVerification, ClientReply<IambResult<EditInfo>>),
|
||||
VerifyRequest(OwnedUserId, ClientReply<IambResult<EditInfo>>),
|
||||
}
|
||||
|
@ -201,6 +203,10 @@ impl Requester {
|
|||
return response.recv();
|
||||
}
|
||||
|
||||
pub fn typing_notice(&self, room_id: OwnedRoomId) {
|
||||
self.tx.send(WorkerTask::TypingNotice(room_id)).unwrap();
|
||||
}
|
||||
|
||||
pub fn verify(&self, act: VerifyAction, sas: SasVerification) -> IambResult<EditInfo> {
|
||||
let (reply, response) = oneshot();
|
||||
|
||||
|
@ -333,6 +339,10 @@ impl ClientWorker {
|
|||
assert!(self.initialized);
|
||||
reply.send(self.send_message(room_id, msg).await);
|
||||
},
|
||||
WorkerTask::TypingNotice(room_id) => {
|
||||
assert!(self.initialized);
|
||||
self.typing_notice(room_id).await;
|
||||
},
|
||||
WorkerTask::Verify(act, sas, reply) => {
|
||||
assert!(self.initialized);
|
||||
reply.send(self.verify(act, sas).await);
|
||||
|
@ -347,6 +357,24 @@ impl ClientWorker {
|
|||
async fn init(&mut self, store: AsyncProgramStore) {
|
||||
self.client.add_event_handler_context(store);
|
||||
|
||||
let _ = self.client.add_event_handler(
|
||||
|ev: SyncTypingEvent, room: MatrixRoom, store: Ctx<AsyncProgramStore>| {
|
||||
async move {
|
||||
let room_id = room.room_id().to_owned();
|
||||
let mut locked = store.lock().await;
|
||||
|
||||
let users = ev
|
||||
.content
|
||||
.user_ids
|
||||
.into_iter()
|
||||
.filter(|u| u != &locked.application.settings.profile.user_id)
|
||||
.collect();
|
||||
|
||||
locked.application.get_room_info(room_id).set_typing(users);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let _ = self.client.add_event_handler(
|
||||
|ev: SyncStateEvent<RoomNameEventContent>,
|
||||
room: MatrixRoom,
|
||||
|
@ -744,6 +772,12 @@ impl ClientWorker {
|
|||
return spaces;
|
||||
}
|
||||
|
||||
async fn typing_notice(&mut self, room_id: OwnedRoomId) {
|
||||
if let Some(room) = self.client.get_joined_room(room_id.as_ref()) {
|
||||
let _ = room.typing_notice(true).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify(&self, action: VerifyAction, sas: SasVerification) -> IambResult<EditInfo> {
|
||||
match action {
|
||||
VerifyAction::Accept => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue