diff --git a/fractal-gtk/res/app.css b/fractal-gtk/res/app.css index 02a8ec1c2c0df82c94419023381b53c39d5a11d3..b41e8bcfaaa67328638cae0d60276d6b7802b597 100644 --- a/fractal-gtk/res/app.css +++ b/fractal-gtk/res/app.css @@ -310,3 +310,10 @@ stack.titlebar:not(headerbar) > box > separator { .scrollarea-top-border { border-top: 1px solid @borders; } + +.typing_label { + margin: 6px; + margin-left: 78px; + margin-bottom: 8px; + color: @theme_selected_bg_color; +} diff --git a/fractal-gtk/src/app/backend_loop.rs b/fractal-gtk/src/app/backend_loop.rs index 999910e102245aab9c7bf25c032f301cd078c28c..4b70e155feca5cdd57aeede3b0efd71154c4cdc0 100644 --- a/fractal-gtk/src/app/backend_loop.rs +++ b/fractal-gtk/src/app/backend_loop.rs @@ -112,7 +112,7 @@ pub fn backend_loop(rx: Receiver) { APPOP!(set_active_room_by_id, (room_id)); } } - Ok(BKResponse::NewRooms(rooms)) => { + Ok(BKResponse::UpdateRooms(rooms)) => { let clear_room_list = false; APPOP!(set_rooms, (rooms, clear_room_list)); } diff --git a/fractal-gtk/src/appop/room.rs b/fractal-gtk/src/appop/room.rs index f055f6015e5b2f2749187e1c3a06478f6e4d6835..0004f63fc84305381fb3441a2119afdc2f01374b 100644 --- a/fractal-gtk/src/appop/room.rs +++ b/fractal-gtk/src/appop/room.rs @@ -1,4 +1,4 @@ -use crate::i18n::{i18n, i18n_k}; +use crate::i18n::{i18n, i18n_k, ni18n_f}; use log::{error, warn}; use std::fs::remove_file; use std::os::unix::fs; @@ -18,13 +18,15 @@ use crate::actions::AppState; use crate::cache; use crate::widgets; -use crate::types::{Room, RoomMembership, RoomTag}; +use crate::types::{Member, Room, RoomMembership, RoomTag}; use crate::util::markup_text; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; +use glib::functions::markup_escape_text; + pub struct Force(pub bool); impl AppOp { @@ -49,6 +51,14 @@ impl AppOp { } } else if self.rooms.contains_key(&room.id) { // TODO: update the existing rooms + let update_room = self.rooms.get_mut(&room.id).unwrap(); + let typing_users: Vec = room + .typing_users + .iter() + .map(|u| update_room.members.get(&u.uid).unwrap_or(&u).to_owned()) + .collect(); + update_room.typing_users = typing_users; + self.update_typing_notification(); } else { // Request all joined members for each new room self.backend @@ -178,6 +188,7 @@ impl AppOp { self.active_room = Some(active_room); /* Mark the new active room as read */ self.mark_last_message_as_read(Force(false)); + self.update_typing_notification(); } pub fn really_leave_active_room(&mut self) { @@ -514,4 +525,35 @@ impl AppOp { self.backend.send(BKCommand::GetRoomAvatar(roomid)).unwrap(); } + + pub fn update_typing_notification(&mut self) { + if let Some(active_room) = &self + .rooms + .get(&self.active_room.clone().unwrap_or_default()) + { + if let Some(ref mut history) = self.history { + let typing_users = &active_room.typing_users; + if typing_users.len() == 0 { + history.typing_notification(""); + } else if typing_users.len() > 2 { + history.typing_notification(&i18n("Several users are typing…")); + } else { + let typing_string = ni18n_f( + "{} is typing…", + "{} and {} are typing…", + typing_users.len() as u32, + typing_users + .iter() + .map(|user| markup_escape_text(&user.get_alias())) + .collect::>() + .iter() + .map(std::ops::Deref::deref) + .collect::>() + .as_slice(), + ); + history.typing_notification(&typing_string); + } + } + } + } } diff --git a/fractal-gtk/src/widgets/room_history.rs b/fractal-gtk/src/widgets/room_history.rs index bc40e24f05a32a845eba4a6acf2b19d5ba1ce0c7..9ab8a6c970d24c28fcc057f99ad4c609c3d4d791 100644 --- a/fractal-gtk/src/widgets/room_history.rs +++ b/fractal-gtk/src/widgets/room_history.rs @@ -271,6 +271,10 @@ impl RoomHistory { None } + + pub fn typing_notification(&mut self, typing_str: &str) { + self.rows.borrow().view.typing_notification(typing_str); + } } /* This function creates the content for a Row based on the conntent of msg */ diff --git a/fractal-gtk/src/widgets/scroll_widget.rs b/fractal-gtk/src/widgets/scroll_widget.rs index 8fac41fed31c04e700bb18891867682f54ca57e0..8fd53639476e229cf4d3291c75fb215f54d50bf0 100644 --- a/fractal-gtk/src/widgets/scroll_widget.rs +++ b/fractal-gtk/src/widgets/scroll_widget.rs @@ -35,6 +35,7 @@ pub struct Widgets { btn_revealer: gtk::Revealer, listbox: gtk::ListBox, spinner: gtk::Spinner, + typing_label: gtk::Label, } impl Widgets { @@ -67,7 +68,24 @@ impl Widgets { let column = column.downcast::().unwrap(); column.set_hexpand(true); column.set_vexpand(true); - column.add(&messages); + + let typing_label = gtk::Label::new(None); + typing_label.show(); + typing_label + .get_style_context() + .unwrap() + .add_class("typing_label"); + typing_label.set_xalign(0.0); + typing_label.set_property_wrap(true); + typing_label.set_property_wrap_mode(pango::WrapMode::WordChar); + typing_label.set_visible(false); + typing_label.set_use_markup(true); + + let column_box = gtk::Box::new(gtk::Orientation::Vertical, 0); + column_box.add(&messages); + column_box.add(&typing_label); + column_box.show(); + column.add(&column_box); column.show(); messages @@ -93,6 +111,7 @@ impl Widgets { btn_revealer, listbox: messages, spinner, + typing_label, } } } @@ -246,6 +265,15 @@ impl ScrollWidget { self.request_sent.set(false); self.widgets.spinner.stop(); } + + pub fn typing_notification(&self, typing_str: &str) { + if typing_str.len() == 0 { + self.widgets.typing_label.set_visible(false); + } else { + self.widgets.typing_label.set_visible(true); + self.widgets.typing_label.set_markup(typing_str); + } + } } /* Functions to animate the scroll */ diff --git a/fractal-matrix-api/src/backend/sync.rs b/fractal-matrix-api/src/backend/sync.rs index 23c8d91455cb3370838c4ba0de6ec0e078944d5d..f76ce26cf7c9396ed1e4388ff129a6bfa15d1d3e 100644 --- a/fractal-matrix-api/src/backend/sync.rs +++ b/fractal-matrix-api/src/backend/sync.rs @@ -5,16 +5,21 @@ use crate::globals; use crate::types::Event; use crate::types::EventFilter; use crate::types::Filter; +use crate::types::Member; use crate::types::Message; use crate::types::Room; use crate::types::RoomEventFilter; use crate::types::RoomFilter; +use crate::types::RoomMembership; +use crate::types::RoomTag; use crate::types::SyncResponse; use crate::types::UnreadNotificationsCount; use crate::util::json_q; use crate::util::parse_m_direct; + use log::error; use serde_json::json; +use serde_json::value::from_value; use serde_json::Value as JsonValue; use std::{thread, time}; @@ -92,7 +97,7 @@ pub fn sync(bk: &Backend, new_since: Option, initial: bool) -> Result<() // New rooms let rs = Room::from_sync_response(&response, &userid, &baseu); - tx.send(BKResponse::NewRooms(rs)).unwrap(); + tx.send(BKResponse::UpdateRooms(rs)).unwrap(); // Message events let msgs = join @@ -114,6 +119,36 @@ pub fn sync(bk: &Backend, new_since: Option, initial: bool) -> Result<() .unwrap(); } + // Typing notifications + let rooms: Vec = join + .iter() + .map(|(k, room)| { + let ephemerals = &room.ephemeral.events; + let mut typing_room: Room = + Room::new(k.clone(), RoomMembership::Joined(RoomTag::None)); + let mut typing: Vec = Vec::new(); + for event in ephemerals.iter() { + if let Some(typing_users) = event + .get("content") + .and_then(|x| x.get("user_ids")) + .and_then(|x| x.as_array()) + { + for user in typing_users { + let user: String = from_value(user.to_owned()).unwrap(); + typing.push(Member { + uid: user, + alias: None, + avatar: None, + }); + } + } + } + typing_room.typing_users = typing; + typing_room + }) + .collect(); + tx.send(BKResponse::UpdateRooms(rooms)).unwrap(); + // Other events join.iter() .flat_map(|(k, room)| { diff --git a/fractal-matrix-api/src/backend/types.rs b/fractal-matrix-api/src/backend/types.rs index 9f72a41d93f9b54fab98561fbbff53cc801a69bf..f9cda612920517041ecc71bae59bd0208edb9d04 100644 --- a/fractal-matrix-api/src/backend/types.rs +++ b/fractal-matrix-api/src/backend/types.rs @@ -105,7 +105,7 @@ pub enum BKResponse { SetUserAvatar(String), Sync(String), Rooms(Vec, Option), - NewRooms(Vec), + UpdateRooms(Vec), RoomDetail(String, String, String), RoomAvatar(String, Option), NewRoomAvatar(String), diff --git a/fractal-matrix-api/src/model/room.rs b/fractal-matrix-api/src/model/room.rs index b2ff1f1ca7286147edbd7061b9c96e9a8d0399fd..3f4fd9557acdce8fd1d9702ee7302e325844dbb6 100644 --- a/fractal-matrix-api/src/model/room.rs +++ b/fractal-matrix-api/src/model/room.rs @@ -81,6 +81,7 @@ pub struct Room { pub membership: RoomMembership, pub direct: bool, pub prev_batch: Option, + pub typing_users: Vec, /// Hashmap with the room users power levels /// the key will be the userid and the value will be the level