[fractal/fractal-next] content: Add support for displaying rich replies



commit cf7bc0c90c2880f1fa03653283602c3096cf00fc
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Thu Jan 13 14:24:54 2022 +0100

    content: Add support for displaying rich replies

 data/resources/resources.gresource.xml             |   1 +
 data/resources/style.css                           |   1 +
 data/resources/ui/content-message-reply.ui         |  27 ++
 .../content/room_history/message_row/mod.rs        | 390 +++++++++++----------
 .../content/room_history/message_row/reply.rs      |  73 ++++
 .../content/room_history/message_row/text.rs       |  23 +-
 src/session/room/event.rs                          |  38 ++
 src/session/room/timeline.rs                       |  42 ++-
 8 files changed, 400 insertions(+), 195 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index ffb2867d..be75fc61 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -18,6 +18,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invitee-item.ui">ui/content-invitee-item.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invitee-row.ui">ui/content-invitee-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-media.ui">ui/content-message-media.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-reply.ui">ui/content-message-reply.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-row.ui">ui/content-message-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-divider-row.ui">ui/content-divider-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-room-details.ui">ui/content-room-details.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index bc460033..20fd0bdd 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -232,6 +232,7 @@ headerbar.flat {
 .room-history .event-content .quote {
   border-left: 2px solid @theme_selected_bg_color;
   padding-left: 6px;
+  opacity: 0.7;
 }
 
 .divider-row {
diff --git a/data/resources/ui/content-message-reply.ui b/data/resources/ui/content-message-reply.ui
new file mode 100644
index 00000000..ac147b1f
--- /dev/null
+++ b/data/resources/ui/content-message-reply.ui
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ContentMessageReply" parent="GtkBox">
+    <property name="orientation">vertical</property>
+    <property name="spacing">3</property>
+    <child>
+      <object class="GtkBox">
+        <style>
+          <class name="quote"/>
+        </style>
+        <property name="orientation">vertical</property>
+        <property name="spacing">3</property>
+        <child>
+          <object class="Pill" id="pill">
+            <property name="halign">start</property>
+          </object>
+        </child>
+        <child>
+          <object class="AdwBin" id="related_content"/>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="AdwBin" id="content"/>
+    </child>
+  </template>
+</interface>
diff --git a/src/session/content/room_history/message_row/mod.rs 
b/src/session/content/room_history/message_row/mod.rs
index 336b96f3..f5a9b57d 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -1,8 +1,9 @@
 mod file;
 mod media;
+mod reply;
 mod text;
 
-use crate::{components::Avatar, utils::filename_for_mime};
+use crate::{components::Avatar, spawn, utils::filename_for_mime};
 use adw::{prelude::*, subclass::prelude::*};
 use gettextrs::gettext;
 use gtk::{
@@ -14,7 +15,7 @@ use matrix_sdk::ruma::events::{
     AnyMessageEventContent,
 };
 
-use self::{file::MessageFile, media::MessageMedia, text::MessageText};
+use self::{file::MessageFile, media::MessageMedia, reply::MessageReply, text::MessageText};
 use crate::prelude::*;
 use crate::session::room::Event;
 
@@ -189,202 +190,217 @@ impl MessageRow {
 
     fn update_content(&self, event: &Event) {
         let priv_ = imp::MessageRow::from_instance(self);
-        let content = event.content();
 
-        // TODO: create widgets for all event types
-        // TODO: display reaction events from event.relates_to()
-        // TODO: we should reuse the already present child widgets when possible
+        if event.is_reply() {
+            spawn!(
+                glib::PRIORITY_HIGH,
+                clone!(@weak self as obj, @weak event => async move {
+                    let priv_ = imp::MessageRow::from_instance(&obj);
 
-        match content {
-            Some(AnyMessageEventContent::RoomMessage(message)) => {
-                let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to {
-                    replacement.new_content.msgtype
-                } else {
-                    message.msgtype
-                };
-                match msgtype {
-                    MessageType::Audio(_message) => {}
-                    MessageType::Emote(message) => {
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageText>())
-                        {
-                            child
-                        } else {
-                            let child = MessageText::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.emote(message.formatted, message.body, event.sender());
+                    if let Ok(Some(related_event)) = event.reply_to_event().await {
+                        let reply = MessageReply::new();
+                        reply.set_related_content_sender(related_event.sender().upcast());
+                        build_content(reply.related_content(), &related_event);
+                        build_content(reply.content(), &event);
+                        priv_.content.set_child(Some(&reply));
+                    } else {
+                        build_content(&*priv_.content, &event);
                     }
-                    MessageType::File(message) => {
-                        let info = message.info.as_ref();
-                        let filename = message
-                            .filename
-                            .filter(|name| !name.is_empty())
-                            .or(Some(message.body))
-                            .filter(|name| !name.is_empty())
-                            .unwrap_or_else(|| {
-                                filename_for_mime(
-                                    info.and_then(|info| info.mimetype.as_deref()),
-                                    None,
-                                )
-                            });
+                })
+            );
+        } else {
+            build_content(&*priv_.content, event);
+        }
+    }
+}
 
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageFile>())
-                        {
-                            child
-                        } else {
-                            let child = MessageFile::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.set_filename(Some(filename));
-                    }
-                    MessageType::Image(message) => {
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageMedia>())
-                        {
-                            child
-                        } else {
-                            let child = MessageMedia::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.image(message, &event.room().session());
-                    }
-                    MessageType::Location(_message) => {}
-                    MessageType::Notice(message) => {
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageText>())
-                        {
-                            child
-                        } else {
-                            let child = MessageText::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.markup(message.formatted, message.body);
-                    }
-                    MessageType::ServerNotice(message) => {
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageText>())
-                        {
-                            child
-                        } else {
-                            let child = MessageText::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.text(message.body);
-                    }
-                    MessageType::Text(message) => {
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageText>())
-                        {
-                            child
-                        } else {
-                            let child = MessageText::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.markup(message.formatted, message.body);
-                    }
-                    MessageType::Video(message) => {
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageMedia>())
-                        {
-                            child
-                        } else {
-                            let child = MessageMedia::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.video(message, &event.room().session());
-                    }
-                    MessageType::VerificationRequest(_) => {
-                        // TODO: show more information about the verification
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageText>())
-                        {
-                            child
-                        } else {
-                            let child = MessageText::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.text(gettext("Identity verification was started"));
-                    }
-                    _ => {
-                        warn!("Event not supported: {:?}", msgtype);
-                        let child = if let Some(Ok(child)) =
-                            priv_.content.child().map(|w| w.downcast::<MessageText>())
-                        {
-                            child
-                        } else {
-                            let child = MessageText::new();
-                            priv_.content.set_child(Some(&child));
-                            child
-                        };
-                        child.text(gettext("Unsupported event"));
-                    }
+impl Default for MessageRow {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// Build the content widget of `event` as a child of `parent`.
+fn build_content(parent: &adw::Bin, event: &Event) {
+    // TODO: create widgets for all event types
+    // TODO: display reaction events from event.relates_to()
+    // TODO: we should reuse the already present child widgets when possible
+    match event.content() {
+        Some(AnyMessageEventContent::RoomMessage(message)) => {
+            let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to {
+                replacement.new_content.msgtype
+            } else {
+                message.msgtype
+            };
+            match msgtype {
+                MessageType::Audio(_message) => {}
+                MessageType::Emote(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.emote(message.formatted, message.body, event.sender());
+                }
+                MessageType::File(message) => {
+                    let info = message.info.as_ref();
+                    let filename = message
+                        .filename
+                        .filter(|name| !name.is_empty())
+                        .or(Some(message.body))
+                        .filter(|name| !name.is_empty())
+                        .unwrap_or_else(|| {
+                            filename_for_mime(info.and_then(|info| info.mimetype.as_deref()), None)
+                        });
+
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageFile>())
+                    {
+                        child
+                    } else {
+                        let child = MessageFile::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.set_filename(Some(filename));
+                }
+                MessageType::Image(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageMedia>())
+                    {
+                        child
+                    } else {
+                        let child = MessageMedia::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.image(message, &event.room().session());
+                }
+                MessageType::Location(_message) => {}
+                MessageType::Notice(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.markup(message.formatted, message.body);
+                }
+                MessageType::ServerNotice(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.text(message.body);
+                }
+                MessageType::Text(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.markup(message.formatted, message.body);
+                }
+                MessageType::Video(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageMedia>())
+                    {
+                        child
+                    } else {
+                        let child = MessageMedia::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.video(message, &event.room().session());
+                }
+                MessageType::VerificationRequest(_) => {
+                    // TODO: show more information about the verification
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.text(gettext("Identity verification was started"));
+                }
+                _ => {
+                    warn!("Event not supported: {:?}", msgtype);
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageText>())
+                    {
+                        child
+                    } else {
+                        let child = MessageText::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.text(gettext("Unsupported event"));
                 }
             }
-            Some(AnyMessageEventContent::Sticker(content)) => {
-                let child = if let Some(Ok(child)) =
-                    priv_.content.child().map(|w| w.downcast::<MessageMedia>())
-                {
+        }
+        Some(AnyMessageEventContent::Sticker(content)) => {
+            let child =
+                if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageMedia>()) {
                     child
                 } else {
                     let child = MessageMedia::new();
-                    priv_.content.set_child(Some(&child));
-                    child
-                };
-                child.sticker(content, &event.room().session());
-            }
-            Some(AnyMessageEventContent::RoomEncrypted(content)) => {
-                warn!("Couldn't decrypt event {:?}", content);
-                let child = if let Some(Ok(child)) =
-                    priv_.content.child().map(|w| w.downcast::<MessageText>())
-                {
-                    child
-                } else {
-                    let child = MessageText::new();
-                    priv_.content.set_child(Some(&child));
+                    parent.set_child(Some(&child));
                     child
                 };
-                child.text(gettext("Fractal couldn't decrypt this message."));
-            }
-            Some(AnyMessageEventContent::RoomRedaction(_)) => {
-                let child = if let Some(Ok(child)) =
-                    priv_.content.child().map(|w| w.downcast::<MessageText>())
-                {
-                    child
-                } else {
-                    let child = MessageText::new();
-                    priv_.content.set_child(Some(&child));
-                    child
-                };
-                child.text(gettext("This message was removed."));
-            }
-            _ => {
-                let child = if let Some(Ok(child)) =
-                    priv_.content.child().map(|w| w.downcast::<MessageText>())
-                {
-                    child
-                } else {
-                    let child = MessageText::new();
-                    priv_.content.set_child(Some(&child));
-                    child
-                };
-                child.text(gettext("Unsupported event"));
-            }
+            child.sticker(content, &event.room().session());
+        }
+        Some(AnyMessageEventContent::RoomEncrypted(content)) => {
+            warn!("Couldn't decrypt event {:?}", content);
+            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+            {
+                child
+            } else {
+                let child = MessageText::new();
+                parent.set_child(Some(&child));
+                child
+            };
+            child.text(gettext("Fractal couldn't decrypt this message."));
+        }
+        Some(AnyMessageEventContent::RoomRedaction(_)) => {
+            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+            {
+                child
+            } else {
+                let child = MessageText::new();
+                parent.set_child(Some(&child));
+                child
+            };
+            child.text(gettext("This message was removed."));
+        }
+        _ => {
+            let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+            {
+                child
+            } else {
+                let child = MessageText::new();
+                parent.set_child(Some(&child));
+                child
+            };
+            child.text(gettext("Unsupported event"));
         }
-    }
-}
-
-impl Default for MessageRow {
-    fn default() -> Self {
-        Self::new()
     }
 }
diff --git a/src/session/content/room_history/message_row/reply.rs 
b/src/session/content/room_history/message_row/reply.rs
new file mode 100644
index 00000000..353d2092
--- /dev/null
+++ b/src/session/content/room_history/message_row/reply.rs
@@ -0,0 +1,73 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{glib, subclass::prelude::*, CompositeTemplate};
+
+use crate::{components::Pill, session::User};
+
+mod imp {
+    use super::*;
+    use glib::subclass::InitializingObject;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/content-message-reply.ui")]
+    pub struct MessageReply {
+        #[template_child]
+        pub pill: TemplateChild<Pill>,
+        #[template_child]
+        pub related_content: TemplateChild<adw::Bin>,
+        #[template_child]
+        pub content: TemplateChild<adw::Bin>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MessageReply {
+        const NAME: &'static str = "ContentMessageReply";
+        type Type = super::MessageReply;
+        type ParentType = gtk::Box;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for MessageReply {}
+
+    impl WidgetImpl for MessageReply {}
+
+    impl BoxImpl for MessageReply {}
+}
+
+glib::wrapper! {
+    pub struct MessageReply(ObjectSubclass<imp::MessageReply>)
+        @extends gtk::Widget, gtk::Box, @implements gtk::Accessible;
+}
+
+impl MessageReply {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create MessageReply")
+    }
+
+    pub fn set_related_content_sender(&self, user: User) {
+        let priv_ = imp::MessageReply::from_instance(self);
+        priv_.pill.set_user(Some(user));
+    }
+
+    pub fn related_content(&self) -> &adw::Bin {
+        let priv_ = imp::MessageReply::from_instance(self);
+        priv_.related_content.as_ref()
+    }
+
+    pub fn content(&self) -> &adw::Bin {
+        let priv_ = imp::MessageReply::from_instance(self);
+        priv_.content.as_ref()
+    }
+}
+
+impl Default for MessageReply {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/session/content/room_history/message_row/text.rs 
b/src/session/content/room_history/message_row/text.rs
index 31a26b2c..e54a75a7 100644
--- a/src/session/content/room_history/message_row/text.rs
+++ b/src/session/content/room_history/message_row/text.rs
@@ -4,6 +4,7 @@ use html2pango::{
     block::{markup_html, HtmlBlock},
     html_escape, markup_links,
 };
+use log::warn;
 use matrix_sdk::ruma::events::room::message::{FormattedBody, MessageFormat};
 use once_cell::sync::Lazy;
 use regex::Regex;
@@ -131,14 +132,14 @@ impl MessageText {
         if let Some((html_blocks, body)) = formatted
             .filter(|formatted| is_valid_formatted_body(formatted))
             .and_then(|formatted| {
-                parse_formatted_body(&formatted.body)
+                parse_formatted_body(strip_reply(&formatted.body))
                     .and_then(|blocks| Some((blocks, formatted.body)))
             })
         {
             self.build_html(html_blocks);
             self.set_body(Some(body));
         } else {
-            let body = linkify(&body);
+            let body = linkify(strip_reply(&body));
             self.build_text(&body, true);
             self.set_body(Some(body));
         }
@@ -189,7 +190,7 @@ impl MessageText {
         {
             // TODO: we need to bind the display name to the sender
             let formatted = FormattedBody {
-                body: format!("<b>{}</b> {}", sender.display_name(), &body),
+                body: format!("<b>{}</b> {}", sender.display_name(), strip_reply(&body)),
                 format: MessageFormat::Html,
             };
 
@@ -324,7 +325,6 @@ fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
         HtmlBlock::Quote(blocks) => {
             let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
             bx.add_css_class("quote");
-            bx.add_css_class("dim-label");
             for block in blocks.iter() {
                 let w = create_widget_for_html_block(block);
                 bx.append(&w);
@@ -340,6 +340,21 @@ fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
     }
 }
 
+/// Remove the content between `mx-reply` tags.
+///
+/// Returns the unchanged string if none was found to be able to chain calls.
+fn strip_reply(text: &str) -> &str {
+    if let Some(end) = text.find("</mx-reply>") {
+        if !text.starts_with("<mx-reply>") {
+            warn!("Received a rich reply that doesn't start with '<mx-reply>'");
+        }
+
+        &text[end + 11..]
+    } else {
+        text
+    }
+}
+
 impl Default for MessageText {
     fn default() -> Self {
         Self::new()
diff --git a/src/session/room/event.rs b/src/session/room/event.rs
index cb717506..784a3bc0 100644
--- a/src/session/room/event.rs
+++ b/src/session/room/event.rs
@@ -13,6 +13,7 @@ use matrix_sdk::{
         identifiers::{EventId, UserId},
         MilliSecondsSinceUnixEpoch,
     },
+    Error as MatrixError,
 };
 
 use crate::{
@@ -706,4 +707,41 @@ impl Event {
             _ => false,
         }
     }
+
+    /// Get the id of the event this `Event` replies to, if any.
+    pub fn reply_to_id(&self) -> Option<EventId> {
+        match self.original_content()? {
+            AnyMessageEventContent::RoomMessage(message) => {
+                if let Some(Relation::Reply { in_reply_to }) = message.relates_to {
+                    Some(in_reply_to.event_id)
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
+    /// Whether this `Event` is a reply to another event.
+    pub fn is_reply(&self) -> bool {
+        self.reply_to_id().is_some()
+    }
+
+    /// Get the `Event` this `Event` replies to, if any.
+    ///
+    /// Returns `Ok(None)` if this event is not a reply.
+    pub async fn reply_to_event(&self) -> Result<Option<Event>, MatrixError> {
+        let related_event_id = match self.reply_to_id() {
+            Some(related_event_id) => related_event_id,
+            None => {
+                return Ok(None);
+            }
+        };
+        let event = self
+            .room()
+            .timeline()
+            .fetch_event_by_id(&related_event_id)
+            .await?;
+        Ok(Some(event))
+    }
 }
diff --git a/src/session/room/timeline.rs b/src/session/room/timeline.rs
index 7da6b202..eb46d04c 100644
--- a/src/session/room/timeline.rs
+++ b/src/session/room/timeline.rs
@@ -4,13 +4,16 @@ use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
 use log::{error, warn};
 use matrix_sdk::{
     ruma::{
-        api::client::r0::message::get_message_events::Direction,
+        api::client::r0::{
+            message::get_message_events::Direction, room::get_room_event::Request as EventRequest,
+        },
         events::{
             room::message::MessageType, AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent,
         },
         identifiers::EventId,
     },
     uuid::Uuid,
+    Error as MatrixError,
 };
 
 use crate::session::{
@@ -482,14 +485,45 @@ impl Timeline {
         }
     }
 
-    /// Returns the event with the given id
+    /// Get the event with the given id from the local store.
+    ///
+    /// Use this method if you are sure the event has already been received.
+    /// Otherwise use `fetch_event_by_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
-        // from the sdk or the matrix homeserver
         let priv_ = imp::Timeline::from_instance(self);
         priv_.event_map.borrow().get(event_id).cloned()
     }
 
+    /// Fetch the event with the given id.
+    ///
+    /// If the event can't be found locally, a request will be made to the
+    /// homeserver.
+    ///
+    /// Use this method if you are not sure the event has already been received.
+    /// Otherwise use `event_by_id`.
+    pub async fn fetch_event_by_id(&self, event_id: &EventId) -> Result<Event, MatrixError> {
+        if let Some(event) = self.event_by_id(event_id) {
+            Ok(event)
+        } else {
+            let room = self.room();
+            let matrix_room = room.matrix_room();
+            let event_id_clone = event_id.clone();
+            let handle = spawn_tokio!(async move {
+                matrix_room
+                    .event(EventRequest::new(matrix_room.room_id(), &event_id_clone))
+                    .await
+            });
+            match handle.await.unwrap() {
+                Ok(room_event) => Ok(Event::new(room_event.event.into(), &room)),
+                Err(error) => {
+                    // TODO: Retry on connection error?
+                    warn!("Could not fetch event {}: {}", event_id, error);
+                    Err(error)
+                }
+            }
+        }
+    }
+
     /// Prepends a batch of events
     // TODO: This should be lazy, see: https://blogs.gnome.org/ebassi/documentation/lazy-loading/
     pub fn prepend(&self, batch: Vec<Event>) {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]