[fractal/bilelmoussaoui/location-event] Room history: Send/display static location events




commit b19cc5e7c719aef1e0e6bf3402874c9e9a9c44f4
Author: Bilal Elmoussaoui <belmouss redhat com>
Date:   Tue Apr 12 01:18:38 2022 +0200

    Room history: Send/display static location events
    
    Fixes #952

 Cargo.lock                                         |  35 +++++-
 Cargo.toml                                         |   4 +
 build-aux/org.gnome.Fractal.Devel.json             |  16 +++
 .../icons/scalable/actions/map-marker-symbolic.svg |  28 +++++
 data/resources/resources.gresource.xml             |   2 +
 data/resources/style.css                           |   9 ++
 data/resources/ui/content-message-location.ui      |  23 ++++
 data/resources/ui/content-room-history.ui          |  29 +++--
 po/POTFILES.in                                     |   1 +
 .../content/room_history/message_row/location.rs   | 118 +++++++++++++++++++++
 .../content/room_history/message_row/mod.rs        |  16 ++-
 src/session/content/room_history/mod.rs            |  77 +++++++++++++-
 12 files changed, 346 insertions(+), 12 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index e6e806286..b821a10f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -140,9 +140,9 @@ dependencies = [
 
 [[package]]
 name = "ashpd"
-version = "0.2.4"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "098dee97729c0164b39a8a7de9c20e4b0eb9cd57f87c8bb465224587b44b1683"
+checksum = "5eea0a7a98b3bd2832eb087e1078f6f58db5a54447574d3007cdac6309c1e9f1"
 dependencies = [
  "enumflags2",
  "futures",
@@ -1021,6 +1021,7 @@ dependencies = [
  "indexmap",
  "libadwaita",
  "libsecret",
+ "libshumate",
  "log",
  "matrix-sdk",
  "mime",
@@ -2204,6 +2205,36 @@ dependencies = [
  "system-deps 6.0.2",
 ]
 
+[[package]]
+name = "libshumate"
+version = "0.1.0-alpha.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "d5c0b7cb25a837204c7eda0879877e0716924c2f970c08e60228bd8410ddc372"
+dependencies = [
+ "gdk4",
+ "gio",
+ "glib",
+ "gtk4",
+ "libc",
+ "libshumate-sys",
+ "once_cell",
+]
+
+[[package]]
+name = "libshumate-sys"
+version = "0.1.0-alpha.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9380bffe8a69af1cd5c6ae0b6dfd2942017d89d9565f64d0e3579629e3921f07"
+dependencies = [
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk4-sys",
+ "libc",
+ "system-deps 6.0.2",
+]
+
 [[package]]
 name = "libspa"
 version = "0.4.1"
diff --git a/Cargo.toml b/Cargo.toml
index 4e9f3b3f4..ec4aa6c66 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,6 +63,10 @@ features = ["v4_6"]
 package = "libadwaita"
 version = "0.1.0"
 
+[dependencies.shumate]
+package = "libshumate"
+version = "0.1.0-alpha.4"
+
 [dependencies.matrix-sdk]
 git = "https://github.com/matrix-org/matrix-rust-sdk.git";
 features = [
diff --git a/build-aux/org.gnome.Fractal.Devel.json b/build-aux/org.gnome.Fractal.Devel.json
index f216f2100..79a8168c8 100644
--- a/build-aux/org.gnome.Fractal.Devel.json
+++ b/build-aux/org.gnome.Fractal.Devel.json
@@ -31,6 +31,22 @@
         ]
     },
     "modules" : [
+        {
+            "name": "libshumate",
+            "buildsystem": "meson",
+            "config-opts": [
+                "-Dgir=false",
+                "-Dvapi=false",
+                "-Dgtk_doc=false"
+            ],
+            "sources": [
+                {
+                    "type": "git",
+                    "url": "https://gitlab.gnome.org/GNOME/libshumate/";,
+                    "tag": "1.0.0.alpha.1"
+                }
+            ]
+        },
         {
             "name" : "fractal",
             "buildsystem" : "meson",
diff --git a/data/resources/icons/scalable/actions/map-marker-symbolic.svg 
b/data/resources/icons/scalable/actions/map-marker-symbolic.svg
new file mode 100644
index 000000000..6511c8744
--- /dev/null
+++ b/data/resources/icons/scalable/actions/map-marker-symbolic.svg
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"; xmlns:dc="http://purl.org/dc/elements/1.1/"; 
xmlns:cc="http://creativecommons.org/ns#"; xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 
xmlns:svg="http://www.w3.org/2000/svg"; xmlns="http://www.w3.org/2000/svg"; width="16" viewBox="0 0 16 16" 
version="1.1" id="svg7384" height="16">
+  <metadata id="metadata90">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <title id="title9167">Gnome Symbolic Icon Theme</title>
+  <defs id="defs7386">
+    <linearGradient osb:paint="solid" id="linearGradient7212">
+      <stop style="stop-color:#000000;stop-opacity:1;" offset="0" id="stop7214"/>
+    </linearGradient>
+  </defs>
+  <g transform="translate(-380.00852,79.9875)" style="display:inline" id="layer1"/>
+  <g transform="translate(-621.00872,446.9875)" style="display:inline" id="layer9"/>
+  <g transform="translate(-621.00872,446.9875)" style="display:inline" id="g7628"/>
+  <g transform="translate(-380.00852,-120.0125)" id="layer13">
+    <path d="m 388.00867,121.00914 c -2.76142,0 -5,2.23858 -5,5 0,0.17259 0.0142,0.33191 0.0312,0.5 
0.0137,0.16725 0.0358,0.33617 0.0625,0.5 0.57248,3.51444 2.9063,6.00336 4.9063,8.00336 2,-2 4.33372,-4.48892 
4.9062,-8.00336 0.0267,-0.16383 0.0488,-0.33275 0.0625,-0.5 0.0171,-0.16809 0.0312,-0.32741 0.0312,-0.5 
0,-2.76142 -2.23858,-5 -5,-5 z m 0,3 c 1.10457,0 2,0.89543 2,2 0,1.10457 -0.89543,2 -2,2 -1.10457,0 
-2,-0.89543 -2,-2 0,-1.10457 0.89543,-2 2,-2 z" id="path5874-9" 
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:
 auto;enable-background:new"/>
+  </g>
+  <g transform="translate(-380.00852,79.9875)" style="display:inline" id="g6387"/>
+  <g transform="translate(-380.00852,79.9875)" style="display:inline" id="layer10"/>
+  <g transform="translate(-380.00852,79.9875)" id="layer12"/>
+  <g transform="translate(-380.00852,79.9875)" style="display:inline" id="layer11"/>
+</svg>
\ No newline at end of file
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 79008ec34..8f23ec3f8 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -14,6 +14,7 @@
     <file preprocess="xml-stripblanks">icons/scalable/actions/idp-google-dark.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/idp-google.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/idp-twitter.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/map-marker-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/devices-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/empty-page.svg</file>
@@ -54,6 +55,7 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-member-row.ui">ui/content-member-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-audio.ui">ui/content-message-audio.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-file.ui">ui/content-message-file.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-location.ui">ui/content-message-location.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-reaction-list.ui">ui/content-message-reaction-list.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-message-reaction.ui">ui/content-message-reaction.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index e79105199..64e3e1dda 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -375,6 +375,15 @@ login {
   font-size: 3em;
 }
 
+.room-history .event-content .location .map {
+  border-radius: 6px;
+  background-color: @borders;
+}
+
+.room-history .event-content .location .map-marker {
+  color: @accent_color;
+}
+
 .room-history .event-content .thumbnail {
   border-radius: 6px;
   background-color: @borders;
diff --git a/data/resources/ui/content-message-location.ui b/data/resources/ui/content-message-location.ui
new file mode 100644
index 000000000..c4eb804ea
--- /dev/null
+++ b/data/resources/ui/content-message-location.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkImage" id="marker_img">
+    <property name="icon-name">map-marker-symbolic</property>
+    <property name="pixel-size">32</property>
+    <style>
+      <class name="map-marker" />
+    </style>
+  </object>
+  <template class="ContentMessageLocation" parent="GtkWidget">
+    <child>
+      <object class="ShumateSimpleMap" id="map">
+        <property name="overflow">GTK_OVERFLOW_HIDDEN</property>
+        <style>
+          <class name="map"/>
+        </style>
+      </object>
+    </child>
+    <style>
+      <class name="location" />
+    </style>
+  </template>
+</interface>
diff --git a/data/resources/ui/content-room-history.ui b/data/resources/ui/content-room-history.ui
index 3d3baa56f..aeb7d0b88 100644
--- a/data/resources/ui/content-room-history.ui
+++ b/data/resources/ui/content-room-history.ui
@@ -21,6 +21,20 @@
       </item>
     </section>
   </menu>
+  <menu id="message-menu-model">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Location</attribute>
+        <attribute name="action">room-history.send-location</attribute>
+        <attribute name="icon">map-marker-symbolic</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Attachment</attribute>
+        <attribute name="action">room-history.select-file</attribute>
+        <attribute name="icon">mail-attachment-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
   <template class="ContentRoomHistory" parent="AdwBin">
     <property name="vexpand">True</property>
     <property name="hexpand">True</property>
@@ -209,13 +223,6 @@
                 <style>
                   <class name="toolbar"/>
                 </style>
-                <child>
-                  <object class="GtkButton">
-                    <property name="valign">end</property>
-                    <property name="icon-name">mail-attachment-symbolic</property>
-                    <property name="action-name">room-history.select-file</property>
-                  </object>
-                </child>
                 <child>
                   <object class="GtkMenuButton" id="markdown_button">
                     <property name="valign">end</property>
@@ -250,6 +257,14 @@
                     </child>
                   </object>
                 </child>
+                <child>
+                  <object class="GtkMenuButton">
+                    <property name="valign">end</property>
+                    <property name="direction">up</property>
+                    <property name="icon-name">view-more-horizontal-symbolic</property>
+                    <property name="menu-model">message-menu-model</property>
+                  </object>
+                </child>
                 <child>
                   <object class="GtkButton">
                     <property name="valign">end</property>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0110d7b5a..1f3367553 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -57,6 +57,7 @@ src/session/content/room_details/member_page/mod.rs
 src/session/content/room_details/mod.rs
 src/session/content/room_history/item_row.rs
 src/session/content/room_history/message_row/audio.rs
+src/session/content/room_history/message_row/location.rs
 src/session/content/room_history/message_row/media.rs
 src/session/content/room_history/message_row/mod.rs
 src/session/content/room_history/mod.rs
diff --git a/src/session/content/room_history/message_row/location.rs 
b/src/session/content/room_history/message_row/location.rs
new file mode 100644
index 000000000..7abd86da0
--- /dev/null
+++ b/src/session/content/room_history/message_row/location.rs
@@ -0,0 +1,118 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{glib, subclass::prelude::*, CompositeTemplate};
+use shumate::prelude::*;
+
+use crate::i18n::gettext_f;
+
+mod imp {
+    use glib::subclass::InitializingObject;
+
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/Fractal/content-message-location.ui")]
+    pub struct MessageLocation {
+        #[template_child]
+        pub map: TemplateChild<shumate::SimpleMap>,
+        #[template_child]
+        pub marker_img: TemplateChild<gtk::Image>,
+        pub marker: shumate::Marker,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for MessageLocation {
+        const NAME: &'static str = "ContentMessageLocation";
+        type Type = super::MessageLocation;
+        type ParentType = gtk::Widget;
+
+        fn class_init(klass: &mut Self::Class) {
+            Self::bind_template(klass);
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for MessageLocation {
+        fn constructed(&self, obj: &Self::Type) {
+            self.marker.set_child(Some(&*self.marker_img));
+
+            let registry = shumate::MapSourceRegistry::with_defaults();
+            let source = registry.by_id(&shumate::MAP_SOURCE_OSM_MAPNIK).unwrap();
+            self.map.set_map_source(Some(&source));
+
+            let viewport = self.map.viewport().unwrap();
+            viewport.set_zoom_level(12.0);
+            let marker_layer = shumate::MarkerLayer::new(&viewport);
+            marker_layer.add_marker(&self.marker);
+            self.map.add_overlay_layer(&marker_layer);
+
+            // Hide the scale
+            self.map.scale().unwrap().hide();
+            self.parent_constructed(obj);
+        }
+
+        fn dispose(&self, _obj: &Self::Type) {
+            self.map.unparent();
+        }
+    }
+
+    impl WidgetImpl for MessageLocation {
+        fn measure(
+            &self,
+            _widget: &Self::Type,
+            _orientation: gtk::Orientation,
+            _for_size: i32,
+        ) -> (i32, i32, i32, i32) {
+            (300, 300, -1, -1)
+        }
+
+        fn size_allocate(&self, _widget: &Self::Type, width: i32, height: i32, baseline: i32) {
+            self.map
+                .size_allocate(&gtk::Allocation::new(0, 0, width, height), baseline)
+        }
+    }
+}
+
+glib::wrapper! {
+    /// A widget displaying a location message in the timeline.
+    pub struct MessageLocation(ObjectSubclass<imp::MessageLocation>)
+        @extends gtk::Widget, @implements gtk::Accessible;
+}
+
+impl MessageLocation {
+    /// Create a new location message.
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create MessageLocation")
+    }
+
+    pub fn set_geo_uri(&self, uri: &str) {
+        let imp = self.imp();
+
+        let mut uri = uri.trim_start_matches("geo:").split(',');
+        let latitude = uri
+            .next()
+            .and_then(|lat_s| lat_s.parse::<f64>().ok())
+            .unwrap_or_default();
+        let longitude = uri
+            .next()
+            .and_then(|lon_s| lon_s.parse::<f64>().ok())
+            .unwrap_or_default();
+
+        imp.map
+            .viewport()
+            .unwrap()
+            .set_location(latitude, longitude);
+        imp.marker.set_location(latitude, longitude);
+
+        self.update_property(&[gtk::accessible::Property::Description(&gettext_f(
+            "Location at latitude {latitude} and longitude {longitude}",
+            &[
+                ("latitude", &latitude.to_string()),
+                ("longitude", &longitude.to_string()),
+            ],
+        ))]);
+    }
+}
diff --git a/src/session/content/room_history/message_row/mod.rs 
b/src/session/content/room_history/message_row/mod.rs
index e5ae47c7b..5cca6a6ce 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -1,5 +1,6 @@
 mod audio;
 mod file;
+mod location;
 mod media;
 mod reaction;
 mod reaction_list;
@@ -21,7 +22,7 @@ use matrix_sdk::ruma::events::{
 };
 
 use self::{
-    audio::MessageAudio, file::MessageFile, media::MessageMedia,
+    audio::MessageAudio, file::MessageFile, location::MessageLocation, media::MessageMedia,
     reaction_list::MessageReactionList, reply::MessageReply, text::MessageText,
 };
 use crate::{
@@ -305,7 +306,18 @@ fn build_content(parent: &adw::Bin, event: &Event, compact: bool) {
                     };
                     child.image(message, &event.room().session(), compact);
                 }
-                MessageType::Location(_message) => {}
+                MessageType::Location(message) => {
+                    let child = if let Some(Ok(child)) =
+                        parent.child().map(|w| w.downcast::<MessageLocation>())
+                    {
+                        child
+                    } else {
+                        let child = MessageLocation::new();
+                        parent.set_child(Some(&child));
+                        child
+                    };
+                    child.set_geo_uri(&message.geo_uri);
+                }
                 MessageType::Notice(message) => {
                     let child = if let Some(Ok(child)) =
                         parent.child().map(|w| w.downcast::<MessageText>())
diff --git a/src/session/content/room_history/mod.rs b/src/session/content/room_history/mod.rs
index 24f45f854..5493dcb42 100644
--- a/src/session/content/room_history/mod.rs
+++ b/src/session/content/room_history/mod.rs
@@ -8,6 +8,11 @@ mod verification_info_bar;
 use std::str::FromStr;
 
 use adw::subclass::prelude::*;
+use ashpd::{
+    desktop::location::{Accuracy, LocationProxy},
+    WindowIdentifier,
+};
+use futures::TryFutureExt;
 use gettextrs::gettext;
 use gtk::{
     gdk, gio, glib,
@@ -20,6 +25,7 @@ use matrix_sdk::ruma::events::room::message::{
     EmoteMessageEventContent, FormattedBody, MessageType, RoomMessageEventContent,
     TextMessageEventContent,
 };
+use ruma::events::{room::message::LocationMessageEventContent, AnyMessageLikeEventContent};
 use sourceview::prelude::*;
 
 use self::{
@@ -28,6 +34,7 @@ use self::{
 };
 use crate::{
     components::{CustomEntry, DragOverlay, Pill, RoomTitle},
+    i18n::gettext_f,
     session::{
         content::{MarkdownPopover, RoomDetails},
         room::{Event, Room, RoomType, Timeline, TimelineState},
@@ -50,7 +57,7 @@ mod imp {
     use glib::{signal::SignalHandlerId, subclass::InitializingObject};
 
     use super::*;
-    use crate::Application;
+    use crate::{components::Toast, window::Window, Application};
 
     #[derive(Debug, Default, CompositeTemplate)]
     #[template(resource = "/org/gnome/Fractal/content-room-history.ui")]
@@ -141,6 +148,33 @@ mod imp {
             klass.install_action("room-history.open-emoji", None, move |widget, _, _| {
                 widget.open_emoji();
             });
+
+            klass.install_action("room-history.send-location", None, move |widget, _, _| {
+                spawn!(clone!(@weak widget => async move {
+                    let toast_error = match widget.send_location().await {
+                        // Do nothing if the request was canceled by the user
+                        Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {
+                            log::error!("Location request was cancelled by the user");
+                            Some(gettext("The location request has been cancelled."))
+                        },
+                        Err(err) => {
+                            log::error!("Failed to send location {}", err);
+                            Some(gettext("Failed to retrieve current location."))
+                        }
+                        _ => None,
+                    };
+
+                    if let Some(window) = widget
+                        .root()
+                        .as_ref()
+                        .and_then(|root| root.downcast_ref::<Window>())
+                    {
+                        if let Some(message) = toast_error {
+                            window.add_toast(&Toast::new(&message));
+                        }
+                    }
+                }));
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -771,6 +805,47 @@ impl RoomHistory {
         self.imp().message_entry.emit_insert_emoji();
     }
 
+    async fn send_location(&self) -> ashpd::Result<()> {
+        if let Some(room) = self.room() {
+            let connection = ashpd::zbus::Connection::session().await?;
+            let proxy = LocationProxy::new(&connection).await?;
+            let identifier = WindowIdentifier::default();
+
+            let session = proxy
+                .create_session(Some(0), Some(0), Some(Accuracy::Exact))
+                .await?;
+
+            // We want to be listening for new locations whenever the session is up
+            // otherwise we might lose the first response and will have to wait for a future
+            // update by geoclue
+            let (_, location) = futures::try_join!(
+                proxy.start(&session, &identifier).into_future(),
+                proxy.receive_location_updated().into_future()
+            )?;
+
+            let iso8601_datetime =
+                glib::DateTime::from_unix_local(location.timestamp().as_secs() as i64)
+                    .expect("Valid location timestamp");
+            let geo_uri = format!("geo:{},{}", location.latitude(), location.longitude());
+            let location_body = gettext_f(
+                "User Location {geo_uri} at {iso8601_datetime}",
+                &[
+                    ("geo_uri", &geo_uri),
+                    (
+                        "iso8601_datetime",
+                        iso8601_datetime.format_iso8601().unwrap().as_str(),
+                    ),
+                ],
+            );
+            room.send_room_message_event(AnyMessageLikeEventContent::RoomMessage(
+                RoomMessageEventContent::new(MessageType::Location(
+                    LocationMessageEventContent::new(location_body, geo_uri),
+                )),
+            ));
+        }
+        Ok(())
+    }
+
     fn open_attach_dialog(&self, bytes: Vec<u8>, mime: mime::Mime, title: &str) {
         let window = self.root().unwrap().downcast::<gtk::Window>().unwrap();
         let dialog = AttachmentDialog::new(&window);


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