Browse Source

content: Send room messages

merge-requests/1327/merge
Julian Sparber 5 years ago
parent
commit
d529e0d576
  1. 12
      data/resources/style.css
  2. 22
      data/resources/ui/content.ui
  3. 47
      src/session/content/content.rs
  4. 47
      src/session/room/event.rs
  5. 6
      src/session/room/item.rs
  6. 101
      src/session/room/room.rs
  7. 61
      src/session/room/timeline.rs

12
data/resources/style.css

@ -59,3 +59,15 @@
background-color: @text_view_bg;
color: @theme_text_color;
}
.message-entry > .view {
background-color: @theme_base_color;
border-radius: 5px;
border: 1px solid @borders;
padding: 6px;
}
.message-entry > .view:focus {
border: 2px solid @theme_selected_bg_color;
padding: 5px;
}

22
data/resources/ui/content.ui

@ -8,7 +8,7 @@
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="headerbar">
<property name="show-start-title-buttons" bind-source="Content" bind-property="compact" bind-flags="sync-create" />
<property name="show-start-title-buttons" bind-source="Content" bind-property="compact" bind-flags="sync-create"/>
<child type="end">
<object class="GtkMenuButton" id="room_menu">
<property name="icon-name">view-more-symbolic</property>
@ -78,6 +78,7 @@
<child>
<object class="GtkButton">
<property name="icon-name">mail-attachment-symbolic</property>
<property name="action-name">content.select-file</property>
</object>
</child>
<child>
@ -86,13 +87,30 @@
</object>
</child>
<child>
<object class="GtkEntry" id="message_entry">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="vexpand">True</property>
<property name="hexpand">True</property>
<property name="vscrollbar-policy">external</property>
<property name="max-content-height">200</property>
<property name="propagate-natural-height">True</property>
<property name="child">
<object class="GtkSourceView" id="message_entry">
<property name="hexpand">True</property>
</object>
</property>
<style>
<class name="message-entry"/>
</style>
</object>
</child>
<child>
<object class="GtkButton">
<property name="icon-name">send-symbolic</property>
<property name="focus-on-click">False</property>
<property name="action-name">content.send-text-message</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>

47
src/session/content/content.rs

@ -1,5 +1,8 @@
use adw::subclass::prelude::*;
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk::{
gdk, glib, glib::clone, glib::signal::Inhibit, prelude::*, subclass::prelude::*,
CompositeTemplate,
};
use crate::session::{content::ItemRow, room::Room};
@ -13,12 +16,15 @@ mod imp {
pub struct Content {
pub compact: Cell<bool>,
pub room: RefCell<Option<Room>>,
pub md_enabled: Cell<bool>,
#[template_child]
pub headerbar: TemplateChild<adw::HeaderBar>,
#[template_child]
pub listview: TemplateChild<gtk::ListView>,
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub message_entry: TemplateChild<sourceview::View>,
}
#[glib::object_subclass]
@ -31,6 +37,10 @@ mod imp {
ItemRow::static_type();
Self::bind_template(klass);
klass.set_accessible_role(gtk::AccessibleRole::Group);
klass.install_action("content.send-text-message", None, move |widget, _, _| {
widget.send_text_message();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -105,6 +115,28 @@ mod imp {
}
}));
let key_events = gtk::EventControllerKey::new();
self.message_entry.add_controller(&key_events);
key_events
.connect_key_pressed(clone!(@weak obj => @default-return Inhibit(false), move |_, key, _, modifier| {
if !modifier.contains(gdk::ModifierType::SHIFT_MASK) && (key == gdk::keys::constants::Return || key == gdk::keys::constants::KP_Enter) {
obj.activate_action("content.send-text-message", None);
Inhibit(true)
} else {
Inhibit(false)
}
}));
self.message_entry.buffer().connect_property_text_notify(
clone!(@weak obj => move |buffer| {
let (start_iter, end_iter) = buffer.bounds();
obj.action_set_enabled("content.send-text-message", start_iter != end_iter);
}),
);
let (start_iter, end_iter) = self.message_entry.buffer().bounds();
obj.action_set_enabled("content.send-text-message", start_iter != end_iter);
self.parent_constructed(obj);
}
}
@ -144,4 +176,17 @@ impl Content {
let priv_ = imp::Content::from_instance(self);
priv_.room.borrow().clone()
}
pub fn send_text_message(&self) {
let priv_ = imp::Content::from_instance(self);
let buffer = priv_.message_entry.buffer();
let (start_iter, end_iter) = buffer.bounds();
let body = buffer.text(&start_iter, &end_iter, true);
if let Some(room) = &*priv_.room.borrow() {
room.send_text_message(body.as_str(), priv_.md_enabled.get());
}
buffer.set_text("");
}
}

47
src/session/room/event.rs

@ -11,6 +11,7 @@ use matrix_sdk::{
use crate::fn_event;
use crate::session::User;
use std::cell::RefCell;
#[derive(Clone, Debug, glib::GBoxed)]
#[gboxed(type_name = "BoxedAnyRoomEvent")]
@ -24,7 +25,7 @@ mod imp {
#[derive(Debug, Default)]
pub struct Event {
pub event: OnceCell<AnyRoomEvent>,
pub event: OnceCell<RefCell<AnyRoomEvent>>,
pub relates_to: RefCell<Vec<super::Event>>,
pub show_header: Cell<bool>,
pub sender: OnceCell<User>,
@ -53,7 +54,7 @@ mod imp {
"event",
"The matrix event of this Event",
BoxedAnyRoomEvent::static_type(),
glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY,
glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT,
),
glib::ParamSpec::new_boolean(
"show-header",
@ -92,7 +93,7 @@ mod imp {
match pspec.name() {
"event" => {
let event = value.get::<BoxedAnyRoomEvent>().unwrap();
self.event.set(event.0).unwrap();
obj.set_matrix_event(event.0);
}
"show-header" => {
let show_header = value.get().unwrap();
@ -139,32 +140,45 @@ impl Event {
priv_.sender.get().unwrap()
}
pub fn matrix_event(&self) -> &AnyRoomEvent {
pub fn matrix_event(&self) -> AnyRoomEvent {
let priv_ = imp::Event::from_instance(&self);
priv_.event.get().unwrap()
priv_.event.get().unwrap().borrow().clone()
}
pub fn matrix_sender(&self) -> &UserId {
pub fn set_matrix_event(&self, event: AnyRoomEvent) {
let priv_ = imp::Event::from_instance(&self);
let event = priv_.event.get().unwrap();
fn_event!(event, sender)
if let Some(value) = priv_.event.get() {
value.replace(event);
} else {
priv_.event.set(RefCell::new(event)).unwrap();
}
self.notify("event");
}
pub fn matrix_event_id(&self) -> &EventId {
pub fn matrix_sender(&self) -> UserId {
let priv_ = imp::Event::from_instance(&self);
let event = priv_.event.get().unwrap();
fn_event!(event, event_id)
let event = &*priv_.event.get().unwrap().borrow();
fn_event!(event, sender).clone()
}
pub fn matrix_event_id(&self) -> EventId {
let priv_ = imp::Event::from_instance(&self);
let event = &*priv_.event.get().unwrap().borrow();
fn_event!(event, event_id).clone()
}
pub fn timestamp(&self) -> DateTime<Local> {
let priv_ = imp::Event::from_instance(&self);
let event = priv_.event.get().unwrap();
let event = &*priv_.event.get().unwrap().borrow();
fn_event!(event, origin_server_ts).clone().into()
}
/// Find the related event if any
pub fn related_matrix_event(&self) -> Option<EventId> {
match self.matrix_event() {
let priv_ = imp::Event::from_instance(&self);
match *priv_.event.get().unwrap().borrow() {
AnyRoomEvent::Message(ref message) => match message {
AnyMessageEvent::RoomRedaction(event) => Some(event.redacts.clone()),
_ => match message.content() {
@ -189,11 +203,13 @@ impl Event {
/// Whether this event is hidden from the user or displayed in the room history.
pub fn is_hidden_event(&self) -> bool {
let priv_ = imp::Event::from_instance(&self);
if self.related_matrix_event().is_some() {
return true;
}
match self.matrix_event() {
match &*priv_.event.get().unwrap().borrow() {
AnyRoomEvent::Message(message) => match message {
AnyMessageEvent::CallAnswer(_) => true,
AnyMessageEvent::CallInvite(_) => true,
@ -286,7 +302,8 @@ impl Event {
pub fn can_hide_header(&self) -> bool {
let priv_ = imp::Event::from_instance(&self);
match priv_.event.get().unwrap() {
match &*priv_.event.get().unwrap().borrow() {
AnyRoomEvent::Message(ref message) => match message.content() {
AnyMessageEventContent::RoomMessage(message) => match message.msgtype {
MessageType::Audio(_) => true,

6
src/session/room/item.rs

@ -142,7 +142,7 @@ impl Item {
}
}
pub fn matrix_event(&self) -> Option<&AnyRoomEvent> {
pub fn matrix_event(&self) -> Option<AnyRoomEvent> {
let priv_ = imp::Item::from_instance(&self);
if let ItemType::Event(event) = priv_.type_.get().unwrap() {
Some(event.matrix_event())
@ -163,7 +163,7 @@ impl Item {
pub fn matrix_sender(&self) -> Option<UserId> {
let priv_ = imp::Item::from_instance(&self);
if let ItemType::Event(event) = priv_.type_.get().unwrap() {
Some(event.matrix_sender().clone())
Some(event.matrix_sender())
} else {
None
}
@ -173,7 +173,7 @@ impl Item {
let priv_ = imp::Item::from_instance(&self);
if let ItemType::Event(event) = priv_.type_.get().unwrap() {
Some(event.matrix_event_id().clone())
Some(event.matrix_event_id())
} else {
None
}

101
src/session/room/room.rs

@ -1,12 +1,24 @@
use comrak::{markdown_to_html, ComrakOptions};
use gettextrs::gettext;
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use log::{error, warn};
use matrix_sdk::{
events::{room::member::MemberEventContent, AnyRoomEvent, AnyStateEvent, StateEvent},
identifiers::UserId,
events::{
room::{
member::MemberEventContent,
message::{
EmoteMessageEventContent, FormattedBody, MessageEventContent, MessageType,
TextMessageEventContent,
},
},
AnyMessageEvent, AnyRoomEvent, AnyStateEvent, MessageEvent, StateEvent, Unsigned,
},
identifiers::{EventId, UserId},
room::Room as MatrixRoom,
uuid::Uuid,
RoomMember,
};
use std::time::SystemTime;
use crate::session::{
categories::CategoryType,
@ -30,6 +42,8 @@ mod imp {
pub category: Cell<CategoryType>,
pub timeline: OnceCell<Timeline>,
pub room_members: RefCell<HashMap<UserId, User>>,
/// The user of this room
pub user_id: OnceCell<UserId>,
}
#[glib::object_subclass]
@ -366,4 +380,87 @@ impl Room {
);
*/
}
pub fn send_text_message(&self, body: &str, markdown_enabled: bool) {
use std::convert::TryFrom;
let priv_ = imp::Room::from_instance(self);
if let MatrixRoom::Joined(matrix_room) = priv_.matrix_room.get().unwrap().clone() {
let is_emote = body.starts_with("/me ");
// Don't use markdown for emotes
let body = if is_emote {
body.trim_start_matches("/me ")
} else {
body
};
let formatted = if markdown_enabled {
let mut md_options = ComrakOptions::default();
md_options.render.hardbreaks = true;
Some(markdown_to_html(&body, &md_options))
} else {
None
};
let content = if is_emote {
let emote = EmoteMessageEventContent {
body: body.to_string(),
formatted: formatted
.filter(|formatted| formatted.as_str() == body)
.map(|f| FormattedBody::html(f)),
};
MessageEventContent::new(MessageType::Emote(emote))
} else {
let text = if let Some(formatted) =
formatted.filter(|formatted| formatted.as_str() == body)
{
TextMessageEventContent::html(body, formatted)
} else {
TextMessageEventContent::plain(body)
};
MessageEventContent::new(MessageType::Text(text))
};
let txn_id = Uuid::new_v4();
let pending_event = AnyMessageEvent::RoomMessage(MessageEvent {
content,
event_id: EventId::try_from(format!("${}:fractal.gnome.org", txn_id)).unwrap(),
sender: self.user().user_id().clone(),
origin_server_ts: SystemTime::now(),
room_id: matrix_room.room_id().clone(),
unsigned: Unsigned::default(),
});
self.send_message(txn_id, pending_event);
}
}
pub fn send_message(&self, txn_id: Uuid, event: AnyMessageEvent) {
let priv_ = imp::Room::from_instance(self);
let content = event.content();
if let MatrixRoom::Joined(matrix_room) = priv_.matrix_room.get().unwrap().clone() {
let pending_id = event.event_id().clone();
priv_
.timeline
.get()
.unwrap()
.append_pending(AnyRoomEvent::Message(event));
do_async(
async move { matrix_room.send(content, Some(txn_id)).await },
clone!(@weak self as obj => move |result| async move {
// FIXME: We should retry the request if it fails
match result {
Ok(result) => {
let priv_ = imp::Room::from_instance(&obj);
priv_.timeline.get().unwrap().set_event_id_for_pending(pending_id, result.event_id)
},
Err(error) => error!("Couldn't send message: {}", error),
};
}),
);
}
}
}

61
src/session/room/timeline.rs

@ -19,6 +19,8 @@ mod imp {
pub list: RefCell<VecDeque<Item>>,
/// A Hashmap linking `EventId` to correspondenting `Event`
pub event_map: RefCell<HashMap<EventId, Event>>,
/// Maps the temporary `EventId` of the pending Event to the real `EventId`
pub pending_events: RefCell<HashMap<EventId, EventId>>,
}
#[glib::object_subclass]
@ -198,7 +200,7 @@ impl Timeline {
}
}
if let Some(relates_to) = relates_to_events.remove(event.matrix_event_id()) {
if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) {
event.add_relates_to(
relates_to
.into_iter()
@ -233,7 +235,7 @@ impl Timeline {
}
}
if let Some(relates_to) = relates_to_events.remove(event.matrix_event_id()) {
if let Some(relates_to) = relates_to_events.remove(&event.matrix_event_id()) {
event.add_relates_to(
relates_to
.into_iter()
@ -264,18 +266,28 @@ impl Timeline {
list.len()
};
let mut pending_events = priv_.pending_events.borrow_mut();
for event in batch.into_iter() {
let event_id = fn_event!(event, event_id).clone();
let user = self.room().member_by_id(fn_event!(event, sender));
let event = Event::new(&event, &user);
priv_.event_map.borrow_mut().insert(event_id, event.clone());
if event.is_hidden_event() {
self.add_hidden_event(event);
if let Some(pending_id) = pending_events.remove(&event_id) {
if let Some(event_obj) = priv_.event_map.borrow_mut().remove(&pending_id) {
event_obj.set_matrix_event(event);
priv_.event_map.borrow_mut().insert(event_id, event_obj);
}
added -= 1;
} else {
priv_.list.borrow_mut().push_back(Item::for_event(event));
let event = Event::new(&event, &user);
priv_.event_map.borrow_mut().insert(event_id, event.clone());
if event.is_hidden_event() {
self.add_hidden_event(event);
added -= 1;
} else {
priv_.list.borrow_mut().push_back(Item::for_event(event));
}
}
}
@ -285,6 +297,39 @@ impl Timeline {
self.items_changed(index as u32, 0, added as u32);
}
/// Append an event that wasn't yet fully send and received via a sync
pub fn append_pending(&self, event: AnyRoomEvent) {
let priv_ = imp::Timeline::from_instance(self);
let index = {
let mut list = priv_.list.borrow_mut();
let index = list.len();
let user = self.room().member_by_id(fn_event!(event, sender));
let event = Event::new(&event, &user);
if event.is_hidden_event() {
self.add_hidden_event(event);
None
} else {
list.push_back(Item::for_event(event));
Some(index)
}
};
if let Some(index) = index {
self.items_changed(index as u32, 0, 1);
}
}
pub fn set_event_id_for_pending(&self, pending_event_id: EventId, event_id: EventId) {
let priv_ = imp::Timeline::from_instance(self);
priv_
.pending_events
.borrow_mut()
.insert(event_id, pending_event_id);
}
/// Returns the event with the given id
pub fn event_by_id(&self, event_id: &EventId) -> Option<Event> {
// TODO: if the referenced event isn't known to us we will need to request it

Loading…
Cancel
Save