[fractal/fractal-next] content: Add invitation widget and implement accept/reject



commit fc5f97448ab80d19a9ca497549ca3fd9c0cf42ee
Author: Julian Sparber <julian sparber net>
Date:   Fri May 21 19:50:47 2021 +0200

    content: Add invitation widget and implement accept/reject
    
    To fully work this will need some work on the sdk side, since the
    inviter isn't available after dropping a SyncResponse, therefore after a
    restart the inviter is forgotten. Also the display name isn't shown
    always correclty because the sdk doesn't calculate it correclty.

 data/resources/resources.gresource.xml |   1 +
 data/resources/style.css               |   4 +
 data/resources/ui/content-invite.ui    | 138 +++++++++++++++++
 data/resources/ui/content.ui           |  10 +-
 po/POTFILES.in                         |   2 +
 src/meson.build                        |   1 +
 src/session/content/content.rs         |  43 +++++-
 src/session/content/invite.rs          | 261 +++++++++++++++++++++++++++++++++
 src/session/content/mod.rs             |   2 +
 src/session/mod.rs                     |  21 ++-
 src/session/room/room.rs               |  83 ++++++++++-
 src/session/user.rs                    |  39 ++++-
 12 files changed, 596 insertions(+), 9 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 0d47f3e8..7e7b6a2b 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -9,6 +9,7 @@
     <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-state-row.ui">ui/content-state-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-markdown-popover.ui">ui/content-markdown-popover.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="content-invite.ui">ui/content-invite.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 3f8b404e..b4e3ba64 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -145,3 +145,7 @@ headerbar.flat {
   border: 2px solid @theme_selected_bg_color;
   padding: 5px;
 }
+
+.invite-room-name {
+  font-size: 24px;
+}
diff --git a/data/resources/ui/content-invite.ui b/data/resources/ui/content-invite.ui
new file mode 100644
index 00000000..4902d65b
--- /dev/null
+++ b/data/resources/ui/content-invite.ui
@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ContentInvite" parent="AdwBin">
+    <property name="vexpand">True</property>
+    <property name="hexpand">True</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="AdwHeaderBar" id="headerbar">
+            <property name="show-start-title-buttons" bind-source="ContentInvite" bind-property="compact" 
bind-flags="sync-create"/>
+            <child type="start">
+              <object class="GtkButton" id="back">
+                <property name="visible" bind-source="ContentInvite" bind-property="compact" 
bind-flags="sync-create"/>
+                <property name="icon-name">go-previous-symbolic</property>
+                <property name="action-name">content.go-back</property>
+              </object>
+            </child>
+            <child type="title">
+              <object class="AdwWindowTitle">
+                <property name="title" translatable="yes">Invite</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="vexpand">True</property>
+            <property name="hscrollbar-policy">never</property>
+            <property name="child">
+              <object class="AdwClamp">
+                <property name="maximum-size">400</property>
+                <property name="tightening-threshold">200</property>
+                <property name="vexpand">True</property>
+                <property name="margin-top">24</property>
+                <property name="margin-bottom">24</property>
+                <property name="margin-start">24</property>
+                <property name="margin-end">24</property>
+                <property name="child">
+                  <object class="GtkBox">
+                    <property name="valign">center</property>
+                    <property name="halign">center</property>
+                    <property name="spacing">24</property>
+                    <property name="orientation">vertical</property>
+                    <accessibility>
+                      <property name="label" translatable="yes">Invite</property>
+                    </accessibility>
+                    <child>
+                      <object class="AdwAvatar">
+                        <property name="show-initials">True</property>
+                        <property name="size">150</property>
+                        <property name="text" bind-source="display_name" bind-property="label" 
bind-flags="sync-create"/>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="display_name">
+                        <property name="ellipsize">end</property>
+                        <binding name="label">
+                          <lookup name="display-name">
+                            <lookup name="room">ContentInvite</lookup>
+                          </lookup>
+                        </binding>
+                        <style>
+                          <class name="invite-room-name"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="room_topic">
+                        <property name="wrap">True</property>
+                        <property name="justify">center</property>
+                        <binding name="label">
+                          <lookup name="topic">
+                            <lookup name="room">ContentInvite</lookup>
+                          </lookup>
+                        </binding>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="halign">center</property>
+                        <child>
+                          <object class="UserPill" id="inviter">
+                            <binding name="user">
+                              <lookup name="inviter">
+                                <lookup name="room">ContentInvite</lookup>
+                              </lookup>
+                            </binding>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel">
+                            <!-- Translators: the space at the beginning is there on purpose -->
+                            <property name="label" translatable="yes"> invited you</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="halign">center</property>
+                        <property name="spacing">24</property>
+                        <property name="margin-top">24</property>
+                        <child>
+                          <object class="SpinnerButton" id="reject_button">
+                            <property name="label" translatable="yes">_Reject</property>
+                            <property name="action-name">invite.reject</property>
+                            <style>
+                              <class name="pill-button"/>
+                            </style>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="SpinnerButton" id="accept_button">
+                            <property name="label" translatable="yes">_Accept</property>
+                            <property name="action-name">invite.accept</property>
+                            <style>
+                              <class name="suggested-action"/>
+                              <class name="pill-button"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/data/resources/ui/content.ui b/data/resources/ui/content.ui
index 332a6054..f30599b8 100644
--- a/data/resources/ui/content.ui
+++ b/data/resources/ui/content.ui
@@ -4,9 +4,15 @@
     <property name="vexpand">True</property>
     <property name="hexpand">True</property>
     <property name="child">
-      <object class="GtkStack">
+      <object class="GtkStack" id="stack">
         <child>
-          <object class="ContentRoomHistory">
+          <object class="ContentRoomHistory" id="room_history">
+            <property name="compact" bind-source="Content" bind-property="compact" bind-flags="sync-create"/>
+            <property name="room" bind-source="Content" bind-property="room" bind-flags="sync-create"/>
+          </object>
+        </child>
+        <child>
+          <object class="ContentInvite" id="invite">
             <property name="compact" bind-source="Content" bind-property="compact" bind-flags="sync-create"/>
             <property name="room" bind-source="Content" bind-property="room" bind-flags="sync-create"/>
           </object>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1e51db6c..9ad215dd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -8,6 +8,7 @@ data/org.gnome.FractalNext.metainfo.xml.in.in
 data/resources/ui/content-divider-row.ui
 data/resources/ui/content-item-row-menu.ui
 data/resources/ui/content-item.ui
+data/resources/ui/content-invite.ui
 data/resources/ui/content-markdown-popover.ui
 data/resources/ui/content-message-row.ui
 data/resources/ui/content-room-history.ui
@@ -41,6 +42,7 @@ src/session/categories/mod.rs
 src/session/content/content.rs
 src/session/content/divider_row.rs
 src/session/content/item_row.rs
+src/session/content/invite.rs
 src/session/content/markdown_popover.rs
 src/session/content/message_row.rs
 src/session/content/mod.rs
diff --git a/src/meson.build b/src/meson.build
index 5a53a121..38b184fa 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -39,6 +39,7 @@ sources = files(
   'session/content/content.rs',
   'session/content/divider_row.rs',
   'session/content/item_row.rs',
+  'session/content/invite.rs',
   'session/content/markdown_popover.rs',
   'session/content/message_row.rs',
   'session/content/mod.rs',
diff --git a/src/session/content/content.rs b/src/session/content/content.rs
index 901dce20..af97a437 100644
--- a/src/session/content/content.rs
+++ b/src/session/content/content.rs
@@ -1,10 +1,10 @@
-use crate::session::{content::RoomHistory, room::Room};
+use crate::session::{categories::CategoryType, content::Invite, content::RoomHistory, room::Room};
 use adw::subclass::prelude::*;
-use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
 
 mod imp {
     use super::*;
-    use glib::subclass::InitializingObject;
+    use glib::{signal::SignalHandlerId, subclass::InitializingObject};
     use std::cell::{Cell, RefCell};
 
     #[derive(Debug, Default, CompositeTemplate)]
@@ -12,6 +12,13 @@ mod imp {
     pub struct Content {
         pub compact: Cell<bool>,
         pub room: RefCell<Option<Room>>,
+        pub category_handler: RefCell<Option<SignalHandlerId>>,
+        #[template_child]
+        pub stack: TemplateChild<gtk::Stack>,
+        #[template_child]
+        pub room_history: TemplateChild<RoomHistory>,
+        #[template_child]
+        pub invite: TemplateChild<Invite>,
     }
 
     #[glib::object_subclass]
@@ -22,6 +29,7 @@ mod imp {
 
         fn class_init(klass: &mut Self::Class) {
             RoomHistory::static_type();
+            Invite::static_type();
             Self::bind_template(klass);
             klass.set_accessible_role(gtk::AccessibleRole::Group);
 
@@ -110,7 +118,26 @@ impl Content {
             return;
         }
 
+        if let Some(category_handler) = priv_.category_handler.take() {
+            if let Some(room) = self.room() {
+                room.disconnect(category_handler);
+            }
+        }
+
+        if let Some(ref room) = room {
+            let handler_id = room.connect_notify_local(
+                Some("category"),
+                clone!(@weak self as obj => move |room, _| {
+                        obj.set_visible_child(room);
+                }),
+            );
+
+            self.set_visible_child(&room);
+            priv_.category_handler.replace(Some(handler_id));
+        }
+
         priv_.room.replace(room);
+
         self.notify("room");
     }
 
@@ -118,4 +145,14 @@ impl Content {
         let priv_ = imp::Content::from_instance(self);
         priv_.room.borrow().clone()
     }
+
+    fn set_visible_child(&self, room: &Room) {
+        let priv_ = imp::Content::from_instance(self);
+
+        if room.category() == CategoryType::Invited {
+            priv_.stack.set_visible_child(&*priv_.invite);
+        } else {
+            priv_.stack.set_visible_child(&*priv_.room_history);
+        }
+    }
 }
diff --git a/src/session/content/invite.rs b/src/session/content/invite.rs
new file mode 100644
index 00000000..3b3fa2c8
--- /dev/null
+++ b/src/session/content/invite.rs
@@ -0,0 +1,261 @@
+use crate::{
+    components::{SpinnerButton, UserPill},
+    session::{categories::CategoryType, room::Room},
+};
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use gtk_macros::spawn;
+use log::error;
+
+mod imp {
+    use super::*;
+    use glib::{signal::SignalHandlerId, subclass::InitializingObject};
+    use std::cell::{Cell, RefCell};
+    use std::collections::HashSet;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/content-invite.ui")]
+    pub struct Invite {
+        pub compact: Cell<bool>,
+        pub room: RefCell<Option<Room>>,
+        pub accept_requests: RefCell<HashSet<Room>>,
+        pub reject_requests: RefCell<HashSet<Room>>,
+        pub category_handler: RefCell<Option<SignalHandlerId>>,
+        #[template_child]
+        pub headerbar: TemplateChild<adw::HeaderBar>,
+        #[template_child]
+        pub inviter: TemplateChild<adw::HeaderBar>,
+        #[template_child]
+        pub room_topic: TemplateChild<gtk::Label>,
+        #[template_child]
+        pub accept_button: TemplateChild<SpinnerButton>,
+        #[template_child]
+        pub reject_button: TemplateChild<SpinnerButton>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for Invite {
+        const NAME: &'static str = "ContentInvite";
+        type Type = super::Invite;
+        type ParentType = adw::Bin;
+
+        fn class_init(klass: &mut Self::Class) {
+            UserPill::static_type();
+            SpinnerButton::static_type();
+            Self::bind_template(klass);
+            klass.set_accessible_role(gtk::AccessibleRole::Group);
+
+            klass.install_action("invite.reject", None, move |widget, _, _| {
+                widget.reject();
+            });
+            klass.install_action("invite.accept", None, move |widget, _, _| {
+                widget.accept();
+            });
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for Invite {
+        fn properties() -> &'static [glib::ParamSpec] {
+            use once_cell::sync::Lazy;
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpec::new_boolean(
+                        "compact",
+                        "Compact",
+                        "Wheter a compact view is used or not",
+                        false,
+                        glib::ParamFlags::READWRITE,
+                    ),
+                    glib::ParamSpec::new_object(
+                        "room",
+                        "Room",
+                        "The room currently shown",
+                        Room::static_type(),
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "compact" => {
+                    let compact = value.get().unwrap();
+                    self.compact.set(compact);
+                }
+                "room" => {
+                    let room = value.get().unwrap();
+                    obj.set_room(room);
+                }
+
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "compact" => self.compact.get().to_value(),
+                "room" => obj.room().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            self.room_topic
+                .connect_notify_local(Some("label"), |room_topic, _| {
+                    room_topic.set_visible(!room_topic.label().is_empty());
+                });
+
+            self.room_topic
+                .set_visible(!self.room_topic.label().is_empty());
+        }
+    }
+
+    impl WidgetImpl for Invite {}
+    impl BinImpl for Invite {}
+}
+
+glib::wrapper! {
+    pub struct Invite(ObjectSubclass<imp::Invite>)
+        @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl Invite {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create Invite")
+    }
+
+    pub fn set_room(&self, room: Option<Room>) {
+        let priv_ = imp::Invite::from_instance(self);
+
+        if self.room() == room {
+            return;
+        }
+
+        match room {
+            Some(ref room) if priv_.accept_requests.borrow().contains(room) => {
+                self.action_set_enabled("invite.accept", false);
+                self.action_set_enabled("invite.reject", false);
+                priv_.accept_button.set_loading(true);
+            }
+            Some(ref room) if priv_.reject_requests.borrow().contains(room) => {
+                self.action_set_enabled("invite.accept", false);
+                self.action_set_enabled("invite.reject", false);
+                priv_.reject_button.set_loading(true);
+            }
+            _ => self.reset(),
+        }
+
+        if let Some(category_handler) = priv_.category_handler.take() {
+            if let Some(room) = self.room() {
+                room.disconnect(category_handler);
+            }
+        }
+
+        // FIXME: remove clousure when room changes
+        if let Some(ref room) = room {
+            let handler_id = room.connect_notify_local(
+                Some("category"),
+                clone!(@weak self as obj => move |room, _| {
+                        if room.category() != CategoryType::Invited {
+                                let priv_ = imp::Invite::from_instance(&obj);
+                                priv_.reject_requests.borrow_mut().remove(&room);
+                                priv_.accept_requests.borrow_mut().remove(&room);
+                                obj.reset();
+                                if let Some(category_handler) = priv_.category_handler.take() {
+                                    room.disconnect(category_handler);
+                                }
+                        }
+                }),
+            );
+            priv_.category_handler.replace(Some(handler_id));
+        }
+
+        priv_.room.replace(room);
+
+        self.notify("room");
+    }
+
+    pub fn room(&self) -> Option<Room> {
+        let priv_ = imp::Invite::from_instance(self);
+        priv_.room.borrow().clone()
+    }
+
+    fn reset(&self) {
+        let priv_ = imp::Invite::from_instance(self);
+        priv_.accept_button.set_loading(false);
+        priv_.reject_button.set_loading(false);
+        self.action_set_enabled("invite.accept", true);
+        self.action_set_enabled("invite.reject", true);
+    }
+
+    fn accept(&self) -> Option<()> {
+        let priv_ = imp::Invite::from_instance(self);
+        let room = self.room()?;
+
+        self.action_set_enabled("invite.accept", false);
+        self.action_set_enabled("invite.reject", false);
+        priv_.accept_button.set_loading(true);
+        priv_.accept_requests.borrow_mut().insert(room.clone());
+
+        spawn!(
+            clone!(@weak self as obj, @strong room => move || async move {
+                    let priv_ = imp::Invite::from_instance(&obj);
+                    let result = room.accept_invite().await;
+                    match result {
+                            Ok(_) => {},
+                            Err(error) => {
+                                // FIXME: display an error to the user
+                                error!("Accepting invitiation failed: {}", error);
+                                priv_.accept_requests.borrow_mut().remove(&room);
+                                obj.reset();
+                            },
+                    }
+            })()
+        );
+
+        Some(())
+    }
+
+    fn reject(&self) -> Option<()> {
+        let priv_ = imp::Invite::from_instance(self);
+        let room = self.room()?;
+
+        self.action_set_enabled("invite.accept", false);
+        self.action_set_enabled("invite.reject", false);
+        priv_.reject_button.set_loading(true);
+        priv_.reject_requests.borrow_mut().insert(room.clone());
+
+        spawn!(
+            clone!(@weak self as obj, @strong room => move || async move {
+                    let priv_ = imp::Invite::from_instance(&obj);
+                    let result = room.reject_invite().await;
+                    match result {
+                            Ok(_) => {},
+                            Err(error) => {
+                                // FIXME: display an error to the user
+                                error!("Rejecting invitiation failed: {}", error);
+                                priv_.reject_requests.borrow_mut().remove(&room);
+                                obj.reset();
+                            },
+                    }
+            })()
+        );
+
+        Some(())
+    }
+}
diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs
index 2aa007f0..3109a2ed 100644
--- a/src/session/content/mod.rs
+++ b/src/session/content/mod.rs
@@ -1,5 +1,6 @@
 mod content;
 mod divider_row;
+mod invite;
 mod item_row;
 mod markdown_popover;
 mod message_row;
@@ -8,6 +9,7 @@ mod state_row;
 
 pub use self::content::Content;
 use self::divider_row::DividerRow;
+use self::invite::Invite;
 use self::item_row::ItemRow;
 use self::markdown_popover::MarkdownPopover;
 use self::message_row::MessageRow;
diff --git a/src/session/mod.rs b/src/session/mod.rs
index fd4038e9..6cef602e 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -466,6 +466,25 @@ impl Session {
                 );
             }
         }
-        // TODO: handle StrippedStateEvents for invited rooms
+
+        for (room_id, matrix_room) in response.rooms.invite {
+            if let Some(room) = rooms_map.get(&room_id) {
+                room.handle_invite_events(
+                    matrix_room
+                        .invite_state
+                        .events
+                        .into_iter()
+                        .filter_map(|event| {
+                            if let Ok(event) = event.deserialize() {
+                                Some(event)
+                            } else {
+                                error!("Couldn't deserialize event: {:?}", event);
+                                None
+                            }
+                        })
+                        .collect(),
+                )
+            }
+        }
     }
 }
diff --git a/src/session/room/room.rs b/src/session/room/room.rs
index 0748cb5a..63e7f045 100644
--- a/src/session/room/room.rs
+++ b/src/session/room/room.rs
@@ -4,13 +4,14 @@ use log::{debug, error, warn};
 use matrix_sdk::{
     events::{
         room::{
-            member::MemberEventContent,
+            member::{MemberEventContent, MembershipState},
             message::{
                 EmoteMessageEventContent, MessageEventContent, MessageType, TextMessageEventContent,
             },
         },
         tag::TagName,
-        AnyMessageEvent, AnyRoomEvent, AnyStateEvent, MessageEvent, StateEvent, Unsigned,
+        AnyMessageEvent, AnyRoomEvent, AnyStateEvent, AnyStrippedStateEvent, MessageEvent,
+        StateEvent, Unsigned,
     },
     identifiers::{EventId, RoomId, UserId},
     room::Room as MatrixRoom,
@@ -25,6 +26,7 @@ use crate::session::{
     User,
 };
 use crate::utils::do_async;
+use crate::RUNTIME;
 
 mod imp {
     use super::*;
@@ -43,6 +45,8 @@ mod imp {
         pub room_members: RefCell<HashMap<UserId, User>>,
         /// The user of this room
         pub user_id: OnceCell<UserId>,
+        /// The user who send the invite to this room. This is only set when this room is an invitiation.
+        pub inviter: RefCell<Option<User>>,
     }
 
     #[glib::object_subclass]
@@ -77,6 +81,13 @@ mod imp {
                         User::static_type(),
                         glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
                     ),
+                    glib::ParamSpec::new_object(
+                        "inviter",
+                        "Inviter",
+                        "The user who send the invite to this room, this is only set when this room 
rapresents an invite",
+                        User::static_type(),
+                        glib::ParamFlags::READABLE,
+                    ),
                     glib::ParamSpec::new_object(
                         "avatar",
                         "Avatar",
@@ -154,6 +165,7 @@ mod imp {
             let matrix_room = matrix_room.as_ref().unwrap();
             match pspec.name() {
                 "user" => obj.user().to_value(),
+                "inviter" => obj.inviter().to_value(),
                 "display-name" => obj.display_name().to_value(),
                 "avatar" => self.avatar.borrow().to_value(),
                 "timeline" => self.timeline.get().unwrap().to_value(),
@@ -348,6 +360,11 @@ impl Room {
             .filter(|topic| !topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some())
     }
 
+    pub fn inviter(&self) -> Option<User> {
+        let priv_ = imp::Room::from_instance(&self);
+        priv_.inviter.borrow().clone()
+    }
+
     /// Returns the room member `User` object
     ///
     /// The returned `User` is specific to this room
@@ -361,6 +378,42 @@ impl Room {
             .clone()
     }
 
+    /// Handle stripped state events.
+    ///
+    /// Events passed to this function arn't added to the timeline.
+    pub fn handle_invite_events(&self, events: Vec<AnyStrippedStateEvent>) {
+        let priv_ = imp::Room::from_instance(self);
+        let invite_event = events
+            .iter()
+            .find(|event| {
+                if let AnyStrippedStateEvent::RoomMember(event) = event {
+                    event.content.membership == MembershipState::Invite
+                        && event.state_key == self.user().user_id().as_str()
+                } else {
+                    false
+                }
+            })
+            .unwrap();
+
+        let inviter_id = invite_event.sender();
+
+        let inviter_event = events.iter().find(|event| {
+            if let AnyStrippedStateEvent::RoomMember(event) = event {
+                &event.sender == inviter_id
+            } else {
+                false
+            }
+        });
+
+        let inviter = User::new(inviter_id);
+        if let Some(AnyStrippedStateEvent::RoomMember(event)) = inviter_event {
+            inviter.update_from_stripped_member_event(event);
+        }
+
+        priv_.inviter.replace(Some(inviter));
+        self.notify("inviter");
+    }
+
     /// Add new events to the timeline
     pub fn append_events(&self, batch: Vec<AnyRoomEvent>) {
         let priv_ = imp::Room::from_instance(self);
@@ -507,4 +560,30 @@ impl Room {
             );
         }
     }
+
+    pub async fn accept_invite(&self) -> matrix_sdk::Result<()> {
+        let matrix_room = self.matrix_room();
+
+        if let MatrixRoom::Invited(matrix_room) = matrix_room {
+            let (sender, receiver) = futures::channel::oneshot::channel();
+            RUNTIME.spawn(async move { sender.send(matrix_room.accept_invitation().await) });
+            receiver.await.unwrap()
+        } else {
+            error!("Can't accept invite, because this room isn't an invited room");
+            Ok(())
+        }
+    }
+
+    pub async fn reject_invite(&self) -> matrix_sdk::Result<()> {
+        let matrix_room = self.matrix_room();
+
+        if let MatrixRoom::Invited(matrix_room) = matrix_room {
+            let (sender, receiver) = futures::channel::oneshot::channel();
+            RUNTIME.spawn(async move { sender.send(matrix_room.reject_invitation().await) });
+            receiver.await.unwrap()
+        } else {
+            error!("Can't reject invite, because this room isn't an invited room");
+            Ok(())
+        }
+    }
 }
diff --git a/src/session/user.rs b/src/session/user.rs
index 41fa12db..d31b8592 100644
--- a/src/session/user.rs
+++ b/src/session/user.rs
@@ -1,7 +1,7 @@
 use gtk::{gio, glib, prelude::*, subclass::prelude::*};
 
 use matrix_sdk::{
-    events::{room::member::MemberEventContent, StateEvent},
+    events::{room::member::MemberEventContent, StateEvent, StrippedStateEvent},
     identifiers::UserId,
     RoomMember,
 };
@@ -174,4 +174,41 @@ impl User {
             self.notify("display-name");
         }
     }
+
+    /// Update the user based on the the stripped room member state event
+    //TODO: create the GLoadableIcon and set `avatar`
+    pub fn update_from_stripped_member_event(
+        &self,
+        event: &StrippedStateEvent<MemberEventContent>,
+    ) {
+        let changed = {
+            let priv_ = imp::User::from_instance(&self);
+            let user_id = priv_.user_id.get().unwrap();
+            if event.sender.as_str() != user_id {
+                return;
+            };
+
+            let display_name = if let Some(display_name) = &event.content.displayname {
+                Some(display_name.to_owned())
+            } else {
+                event
+                    .content
+                    .third_party_invite
+                    .as_ref()
+                    .map(|i| i.display_name.to_owned())
+            };
+
+            let mut current_display_name = priv_.display_name.borrow_mut();
+            if *current_display_name != display_name {
+                *current_display_name = display_name;
+                true
+            } else {
+                false
+            }
+        };
+
+        if changed {
+            self.notify("display-name");
+        }
+    }
 }


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