[fractal] Separate the use of URLs and local paths



commit 66a642530feadd44cf91552544acabb6532d87fd
Author: Alejandro Domínguez <adomu net-c com>
Date:   Wed Jul 15 19:29:11 2020 +0200

    Separate the use of URLs and local paths

 Cargo.lock                               |  13 +-
 fractal-gtk/Cargo.toml                   |   5 +
 fractal-gtk/src/actions/message.rs       | 142 +++++++++++----------
 fractal-gtk/src/appop/member.rs          |  18 +--
 fractal-gtk/src/appop/message.rs         | 180 +++++++++++++-------------
 fractal-gtk/src/appop/mod.rs             |   4 +-
 fractal-gtk/src/appop/notify.rs          |   5 +-
 fractal-gtk/src/appop/room.rs            |   2 +-
 fractal-gtk/src/appop/user.rs            |   5 +-
 fractal-gtk/src/backend/media.rs         |  17 +--
 fractal-gtk/src/backend/mod.rs           |  68 +++++-----
 fractal-gtk/src/backend/room.rs          |  12 +-
 fractal-gtk/src/backend/user.rs          |  26 ++--
 fractal-gtk/src/cache/mod.rs             |   7 +-
 fractal-gtk/src/cache/state.rs           |   8 +-
 fractal-gtk/src/error.rs                 |   5 -
 fractal-gtk/src/model/fileinfo.rs        |   3 +-
 fractal-gtk/src/model/member.rs          |   8 +-
 fractal-gtk/src/model/message.rs         |  46 +++----
 fractal-gtk/src/model/room.rs            |  26 ++--
 fractal-gtk/src/uitypes.rs               |  11 +-
 fractal-gtk/src/util.rs                  |  10 +-
 fractal-gtk/src/widgets/avatar.rs        |   3 +-
 fractal-gtk/src/widgets/image.rs         | 107 ++++++++--------
 fractal-gtk/src/widgets/inline_player.rs |  62 ++++-----
 fractal-gtk/src/widgets/media_viewer.rs  |  27 ++--
 fractal-gtk/src/widgets/message.rs       | 210 ++++++++++++++++---------------
 fractal-gtk/src/widgets/room_history.rs  |  26 ++--
 fractal-gtk/src/widgets/roomlist.rs      |   4 +-
 fractal-gtk/src/widgets/roomrow.rs       |   3 +-
 30 files changed, 555 insertions(+), 508 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 2eab7ae8..f9057630 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -495,6 +495,14 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3"
 
+[[package]]
+name = "either"
+version = "1.5.3"
+source = 
"git+https://github.com/aledomu/either.git?rev=111c9bd4905c6b51d3d904ba42625f2b32246b59#111c9bd4905c6b51d3d904ba42625f2b32246b59";
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "either"
 version = "1.5.3"
@@ -587,6 +595,7 @@ dependencies = [
  "comrak",
  "directories",
  "dirs",
+ "either 1.5.3 (git+https://github.com/aledomu/either.git?rev=111c9bd4905c6b51d3d904ba42625f2b32246b59)",
  "failure",
  "fractal-matrix-api",
  "fragile",
@@ -1377,7 +1386,7 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
 dependencies = [
- "either",
+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -2727,7 +2736,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "1997788a0e25e09300e44680ba1ef9d44d6f634a883641f80109e8b59c928daf"
 dependencies = [
  "bytes 0.4.12",
- "either",
+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "futures",
  "thiserror",
  "tokio",
diff --git a/fractal-gtk/Cargo.toml b/fractal-gtk/Cargo.toml
index 2ca72e5f..f3c09e8c 100644
--- a/fractal-gtk/Cargo.toml
+++ b/fractal-gtk/Cargo.toml
@@ -75,3 +75,8 @@ features = ["derive"]
 [dependencies.gio]
 version = "0.8.1"
 features = ["v2_56"]
+
+[dependencies.either]
+git = "https://github.com/aledomu/either.git";
+rev = "111c9bd4905c6b51d3d904ba42625f2b32246b59"
+features = ["serde_untagged"]
diff --git a/fractal-gtk/src/actions/message.rs b/fractal-gtk/src/actions/message.rs
index 17f6ae66..5abf33e8 100644
--- a/fractal-gtk/src/actions/message.rs
+++ b/fractal-gtk/src/actions/message.rs
@@ -114,21 +114,21 @@ pub fn new(
 
     open_with.connect_activate(clone!(@strong server_url => move |_, data| {
         if let Some(m) = get_message(data) {
-            let url = m.url.unwrap_or_default();
-            let server_url = server_url.clone();
-            thread::spawn(move || {
-                match dw_media(server_url, &url, ContentType::Download, None) {
-                    Ok(fname) => {
-                        Command::new("xdg-open")
-                            .arg(&fname)
-                            .spawn()
-                            .expect("failed to execute process");
-                    }
-                    Err(err) => {
-                        err.handle_error()
+            if let Some(url) = m.url {
+                thread::spawn(clone!(@strong server_url => move || {
+                    match dw_media(server_url, &url, ContentType::Download, None) {
+                        Ok(fname) => {
+                            Command::new("xdg-open")
+                                .arg(&fname)
+                                .spawn()
+                                .expect("failed to execute process");
+                        }
+                        Err(err) => {
+                            err.handle_error()
+                        }
                     }
-                }
-            });
+                }));
+            }
         }
     }));
 
@@ -138,77 +138,83 @@ pub fn new(
     @weak parent as window
     => move |_, data| {
         if let Some(m) = get_message(data) {
-            let name = m.body;
-            let url = m.url.unwrap_or_default();
+            if let Some(url) = m.url {
+                let name = m.body;
+
+                let (tx, rx): (
+                    Sender<media::MediaResult>,
+                    Receiver<media::MediaResult>,
+                ) = channel();
 
-            let (tx, rx): (Sender<media::MediaResult>, Receiver<media::MediaResult>) = channel();
-            media::get_media_async(thread_pool.clone(), server_url.clone(), url, tx);
+                media::get_media_async(thread_pool.clone(), server_url.clone(), url, tx);
 
-            gtk::timeout_add(
-                50,
-                clone!(
-                    @strong name,
-                    @weak window
-                    => @default-return Continue(true), move || match rx.try_recv() {
-                        Err(TryRecvError::Empty) => Continue(true),
-                        Err(TryRecvError::Disconnected) => {
-                            let msg = i18n("Could not download the file");
-                            ErrorDialog::new(false, &msg);
+                gtk::timeout_add(
+                    50,
+                    clone!(
+                        @strong name,
+                        @weak window
+                        => @default-return Continue(true), move || match rx.try_recv() {
+                            Err(TryRecvError::Empty) => Continue(true),
+                            Err(TryRecvError::Disconnected) => {
+                                let msg = i18n("Could not download the file");
+                                ErrorDialog::new(false, &msg);
 
-                            Continue(true)
-                        },
-                        Ok(Ok(fname)) => {
-                            if let Some(path) = save(&window, &name, &[]) {
-                                // TODO use glib to copy file
-                                if fs::copy(fname, path).is_err() {
-                                    ErrorDialog::new(false, &i18n("Couldn’t save file"));
+                                Continue(true)
+                            },
+                            Ok(Ok(fname)) => {
+                                if let Some(path) = save(&window, &name, &[]) {
+                                    // TODO use glib to copy file
+                                    if fs::copy(fname, path).is_err() {
+                                        ErrorDialog::new(false, &i18n("Couldn’t save file"));
+                                    }
                                 }
+                                Continue(false)
+                            }
+                            Ok(Err(err)) => {
+                                error!("Media path could not be found due to error: {:?}", err);
+                                Continue(false)
                             }
-                            Continue(false)
-                        }
-                        Ok(Err(err)) => {
-                            error!("Media path could not be found due to error: {:?}", err);
-                            Continue(false)
                         }
-                    }),
-            );
+                    ),
+                );
+            }
         }
     }));
 
     copy_image.connect_activate(clone!(@strong server_url => move |_, data| {
         if let Some(m) = get_message(data) {
-            let url = m.url.unwrap_or_default();
+            if let Some(url) = m.url {
+                let (tx, rx): (
+                    Sender<media::MediaResult>,
+                    Receiver<media::MediaResult>,
+                ) = channel();
 
-            let (tx, rx): (
-                Sender<media::MediaResult>,
-                Receiver<media::MediaResult>,
-            ) = channel();
+                media::get_media_async(thread_pool.clone(), server_url.clone(), url, tx);
 
-            media::get_media_async(thread_pool.clone(), server_url.clone(), url, tx);
+                gtk::timeout_add(50, move || match rx.try_recv() {
+                    Err(TryRecvError::Empty) => Continue(true),
+                    Err(TryRecvError::Disconnected) => {
+                        let msg = i18n("Could not download the file");
+                        ErrorDialog::new(false, &msg);
 
-            gtk::timeout_add(50, move || match rx.try_recv() {
-                Err(TryRecvError::Empty) => Continue(true),
-                Err(TryRecvError::Disconnected) => {
-                    let msg = i18n("Could not download the file");
-                    ErrorDialog::new(false, &msg);
+                        Continue(true)
+                    }
+                    Ok(Ok(fname)) => {
+                        if let Ok(pixbuf) = gdk_pixbuf::Pixbuf::new_from_file(fname) {
+                            let atom = gdk::Atom::intern("CLIPBOARD");
+                            let clipboard = gtk::Clipboard::get(&atom);
 
-                    Continue(true)
-                }
-                Ok(Ok(fname)) => {
-                    if let Ok(pixbuf) = gdk_pixbuf::Pixbuf::new_from_file(fname) {
-                        let atom = gdk::Atom::intern("CLIPBOARD");
-                        let clipboard = gtk::Clipboard::get(&atom);
+                            clipboard.set_image(&pixbuf);
+                        }
 
-                        clipboard.set_image(&pixbuf);
+                        Continue(false)
                     }
-
-                    Continue(false)
-                }
-                Ok(Err(err)) => {
-                    error!("Image path could not be found due to error: {:?}", err);
-                    Continue(false)
-                }
-            });
+                    Ok(Err(err)) => {
+                        error!("Image path could not be found due to error: {:?}", err);
+                        Continue(false)
+                    }
+                });
+            }
         }
     }));
 
diff --git a/fractal-gtk/src/appop/member.rs b/fractal-gtk/src/appop/member.rs
index a8f4926a..e929bc0f 100644
--- a/fractal-gtk/src/appop/member.rs
+++ b/fractal-gtk/src/appop/member.rs
@@ -1,5 +1,9 @@
 use crate::backend::{user, HandleError};
-use fractal_api::identifiers::{RoomId, UserId};
+use either::Either;
+use fractal_api::{
+    identifiers::{RoomId, UserId},
+    url::Url,
+};
 use glib::clone;
 use gtk::prelude::*;
 
@@ -59,12 +63,12 @@ impl AppOp {
             }
             Some("join") => {
                 let m = Member {
-                    avatar: Some(String::from(
-                        ev.content["avatar_url"].as_str().unwrap_or_default(),
-                    )),
-                    alias: Some(String::from(
-                        ev.content["displayname"].as_str().unwrap_or_default(),
-                    )),
+                    avatar: ev.content["avatar_url"]
+                        .as_str()
+                        .map(Url::parse)
+                        .and_then(Result::ok)
+                        .map(Either::Left),
+                    alias: ev.content["displayname"].as_str().map(String::from),
                     uid: sender,
                 };
                 if let Some(r) = self.rooms.get_mut(&ev.room.clone()) {
diff --git a/fractal-gtk/src/appop/message.rs b/fractal-gtk/src/appop/message.rs
index a1d1365e..bc22a22f 100644
--- a/fractal-gtk/src/appop/message.rs
+++ b/fractal-gtk/src/appop/message.rs
@@ -16,7 +16,7 @@ use serde_json::json;
 use serde_json::Value as JsonValue;
 use std::env::temp_dir;
 use std::fs;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::thread;
 
 use crate::appop::room::Force;
@@ -298,46 +298,49 @@ impl AppOp {
     pub fn attach_message(&mut self, path: PathBuf) {
         if let Some(room) = self.active_room.clone() {
             if let Some(sender) = self.login_data.as_ref().map(|ld| ld.uid.clone()) {
-                if let Ok(info) = gio::File::new_for_path(&path).query_info(
-                    &gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-                    gio::FileQueryInfoFlags::NONE,
-                    gio::NONE_CANCELLABLE,
-                ) {
-                    // This should always return a type
-                    let mime = info
-                        .get_content_type()
-                        .expect("Could not parse content type from file");
-                    let mtype = match mime.as_ref() {
-                        m if m.starts_with("image") => "m.image",
-                        m if m.starts_with("audio") => "m.audio",
-                        "application/x-riff" => "m.audio",
-                        m if m.starts_with("video") => "m.video",
-                        "application/x-mpegURL" => "m.video",
-                        _ => "m.file",
-                    };
-                    let body = path
-                        .file_name()
-                        .and_then(|s| s.to_str())
-                        .unwrap_or_default();
-                    let path_string = path.to_str().unwrap_or_default();
-
-                    let mut m =
-                        Message::new(room, sender, body.to_string(), mtype.to_string(), None);
-                    let info = match mtype {
-                        "m.image" => get_image_media_info(path_string, mime.as_ref()),
-                        "m.audio" => get_audio_video_media_info(path_string, mime.as_ref()),
-                        "m.video" => get_audio_video_media_info(path_string, mime.as_ref()),
-                        "m.file" => get_file_media_info(path_string, mime.as_ref()),
-                        _ => None,
-                    };
-
-                    m.extra_content = info;
-                    m.url = Some(path_string.to_string());
-
-                    self.add_tmp_room_message(m);
-                    self.dequeue_message();
+                if let Ok(uri) = Url::from_file_path(&path) {
+                    if let Ok(info) = gio::File::new_for_path(&path).query_info(
+                        &gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+                        gio::FileQueryInfoFlags::NONE,
+                        gio::NONE_CANCELLABLE,
+                    ) {
+                        // This should always return a type
+                        let mime = info
+                            .get_content_type()
+                            .expect("Could not parse content type from file");
+                        let mtype = match mime.as_ref() {
+                            m if m.starts_with("image") => "m.image",
+                            m if m.starts_with("audio") => "m.audio",
+                            "application/x-riff" => "m.audio",
+                            m if m.starts_with("video") => "m.video",
+                            "application/x-mpegURL" => "m.video",
+                            _ => "m.file",
+                        };
+                        let body = path
+                            .file_name()
+                            .and_then(|s| s.to_str())
+                            .map(Into::into)
+                            .unwrap_or_default();
+
+                        let mut m = Message::new(room, sender, body, mtype.to_string(), None);
+                        let info = match mtype {
+                            "m.image" => get_image_media_info(&path, mime.as_ref()),
+                            "m.audio" => get_audio_video_media_info(&uri, mime.as_ref()),
+                            "m.video" => get_audio_video_media_info(&uri, mime.as_ref()),
+                            "m.file" => get_file_media_info(&path, mime.as_ref()),
+                            _ => None,
+                        };
+
+                        m.extra_content = info;
+                        m.local_path = Some(path);
+
+                        self.add_tmp_room_message(m);
+                        self.dequeue_message();
+                    } else {
+                        error!("Can't send message: Could not query info");
+                    }
                 } else {
-                    error!("Can't send message: Could not query info");
+                    error!("Can't send message: Path is not absolute")
                 }
             } else {
                 error!("Can't send message: No user is logged in");
@@ -560,6 +563,7 @@ fn create_ui_message(
         },
         thumb: msg.thumb,
         url: msg.url,
+        local_path: msg.local_path,
         formatted_body: msg.formatted_body,
         format: msg.format,
         last_viewed,
@@ -572,9 +576,9 @@ fn create_ui_message(
 /// This function opens the image, creates a thumbnail
 /// and populates the info Json with the information it has
 
-fn get_image_media_info(file: &str, mimetype: &str) -> Option<JsonValue> {
-    let (_, w, h) = Pixbuf::get_file_info(&file)?;
-    let size = fs::metadata(&file).ok()?.len();
+fn get_image_media_info(file: &Path, mimetype: &str) -> Option<JsonValue> {
+    let (_, w, h) = Pixbuf::get_file_info(file)?;
+    let size = fs::metadata(file).ok()?.len();
 
     // make thumbnail max 800x600
     let thumb = Pixbuf::new_from_file_at_scale(&file, 800, 600, true).ok()?;
@@ -608,10 +612,10 @@ fn get_image_media_info(file: &str, mimetype: &str) -> Option<JsonValue> {
     Some(info)
 }
 
-fn get_audio_video_media_info(file: &str, mimetype: &str) -> Option<JsonValue> {
-    let size = fs::metadata(file).ok()?.len();
+fn get_audio_video_media_info(uri: &Url, mimetype: &str) -> Option<JsonValue> {
+    let size = fs::metadata(uri.to_file_path().ok()?).ok()?.len();
 
-    if let Some(duration) = widgets::inline_player::get_media_duration(file)
+    if let Some(duration) = widgets::inline_player::get_media_duration(uri)
         .ok()
         .and_then(|d| d.mseconds())
     {
@@ -632,7 +636,7 @@ fn get_audio_video_media_info(file: &str, mimetype: &str) -> Option<JsonValue> {
     }
 }
 
-fn get_file_media_info(file: &str, mimetype: &str) -> Option<JsonValue> {
+fn get_file_media_info(file: &Path, mimetype: &str) -> Option<JsonValue> {
     let size = fs::metadata(file).ok()?.len();
 
     let info = json!({
@@ -645,59 +649,63 @@ fn get_file_media_info(file: &str, mimetype: &str) -> Option<JsonValue> {
     Some(info)
 }
 
-fn attach_file(baseu: Url, tk: AccessToken, mut msg: Message) {
-    let fname = msg.url.clone().unwrap_or_default();
+struct NonMediaMsg;
+
+fn attach_file(baseu: Url, tk: AccessToken, mut msg: Message) -> Result<(), NonMediaMsg> {
     let mut extra_content: Option<ExtraContent> = msg
-        .clone()
         .extra_content
+        .clone()
         .and_then(|c| serde_json::from_value(c).ok());
 
-    let thumb = extra_content
-        .clone()
-        .and_then(|c| c.info.thumbnail_url)
-        .unwrap_or_default();
+    let thumb_url = extra_content.clone().and_then(|c| c.info.thumbnail_url);
 
-    if fname.starts_with("mxc://") && thumb.starts_with("mxc://") {
-        send_msg_and_manage(baseu, tk, msg);
+    match (msg.url.clone(), msg.local_path.as_ref(), thumb_url) {
+        (Some(url), _, Some(thumb)) if url.scheme() == "mxc" && thumb.scheme() == "mxc" => {
+            send_msg_and_manage(baseu, tk, msg);
 
-        return;
-    }
+            Ok(())
+        }
+        (_, Some(local_path), _) => {
+            if let Some(ref local_path_thumb) = msg.local_path_thumb {
+                match room::upload_file(baseu.clone(), tk.clone(), local_path_thumb) {
+                    Ok(response) => {
+                        let thumb_uri = response.content_uri;
+                        msg.thumb = Some(thumb_uri.clone());
+                        if let Some(ref mut xctx) = extra_content {
+                            xctx.info.thumbnail_url = Some(thumb_uri);
+                        }
+                        msg.extra_content = serde_json::to_value(&extra_content).ok();
+                    }
+                    Err(err) => {
+                        err.handle_error();
+                    }
+                }
 
-    if !thumb.is_empty() {
-        match room::upload_file(baseu.clone(), tk.clone(), &thumb) {
-            Ok(response) => {
-                let thumb_uri = response.content_uri.to_string();
-                msg.thumb = Some(thumb_uri.clone());
-                if let Some(ref mut xctx) = extra_content {
-                    xctx.info.thumbnail_url = Some(thumb_uri);
+                if let Err(_e) = std::fs::remove_file(local_path_thumb) {
+                    error!("Can't remove thumbnail: {}", local_path_thumb.display());
                 }
-                msg.extra_content = serde_json::to_value(&extra_content).ok();
             }
-            Err(err) => {
-                err.handle_error();
-            }
-        }
 
-        if let Err(_e) = std::fs::remove_file(&thumb) {
-            error!("Can't remove thumbnail: {}", thumb);
-        }
-    }
+            let query = room::upload_file(baseu.clone(), tk.clone(), local_path).map(|response| {
+                msg.url = Some(response.content_uri);
+                thread::spawn(clone!(@strong msg => move || send_msg_and_manage(baseu, tk, msg)));
 
-    let query = room::upload_file(baseu.clone(), tk.clone(), &fname).map(|response| {
-        msg.url = Some(response.content_uri.to_string());
-        thread::spawn(clone!(@strong msg => move || send_msg_and_manage(baseu, tk, msg)));
+                msg
+            });
 
-        msg
-    });
+            match query {
+                Ok(msg) => {
+                    APPOP!(attached_file, (msg));
+                }
+                Err(err) => {
+                    err.handle_error();
+                }
+            };
 
-    match query {
-        Ok(msg) => {
-            APPOP!(attached_file, (msg));
-        }
-        Err(err) => {
-            err.handle_error();
+            Ok(())
         }
-    };
+        _ => Err(NonMediaMsg),
+    }
 }
 
 fn send_msg_and_manage(baseu: Url, tk: AccessToken, msg: Message) {
diff --git a/fractal-gtk/src/appop/mod.rs b/fractal-gtk/src/appop/mod.rs
index 18979a0e..cb314d35 100644
--- a/fractal-gtk/src/appop/mod.rs
+++ b/fractal-gtk/src/appop/mod.rs
@@ -48,6 +48,8 @@ mod user;
 use self::member::SearchType;
 use self::message::TmpMsg;
 
+pub type UserInfoCache = Arc<Mutex<CacheMap<UserId, (String, PathBuf)>>>;
+
 #[derive(Clone, Debug, PartialEq)]
 pub enum RoomSearchPagination {
     Initial,
@@ -116,7 +118,7 @@ pub struct AppOp {
     pub leaflet: libhandy::Leaflet,
 
     pub thread_pool: ThreadPool,
-    pub user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+    pub user_info_cache: UserInfoCache,
 }
 
 impl PasswordStorage for AppOp {}
diff --git a/fractal-gtk/src/appop/notify.rs b/fractal-gtk/src/appop/notify.rs
index c6de2de5..eed7b68b 100644
--- a/fractal-gtk/src/appop/notify.rs
+++ b/fractal-gtk/src/appop/notify.rs
@@ -7,6 +7,7 @@ use glib::clone;
 use glib::source::Continue;
 use gtk::prelude::*;
 use log::info;
+use std::path::Path;
 use std::sync::mpsc::channel;
 use std::sync::mpsc::TryRecvError;
 use std::sync::mpsc::{Receiver, Sender};
@@ -107,11 +108,11 @@ fn dirty_truncate(s: &str, num_chars: usize) -> &str {
     }
 }
 
-fn create_notification(room_id: &str, title: &str, body: &str, avatar: &str) -> Notification {
+fn create_notification(room_id: &str, title: &str, body: &str, avatar: &Path) -> Notification {
     let notification = Notification::new(title);
     notification.set_body(Some(body));
     notification.set_priority(gio::NotificationPriority::High);
-    info!("Creating notification with avatar: {}", avatar);
+    info!("Creating notification with avatar: {}", avatar.display());
     let file = gio::File::new_for_path(avatar);
     let _ = file.load_bytes(gio::NONE_CANCELLABLE).map(|(b, _)| {
         let avatar = gio::BytesIcon::new(&b);
diff --git a/fractal-gtk/src/appop/room.rs b/fractal-gtk/src/appop/room.rs
index 1231d20c..fdd7365a 100644
--- a/fractal-gtk/src/appop/room.rs
+++ b/fractal-gtk/src/appop/room.rs
@@ -485,7 +485,7 @@ impl AppOp {
                     }
                 }
             }
-            r.avatar = avatar.map(|s| s.into_string());
+            r.avatar = avatar;
             self.roomlist
                 .set_room_avatar(room_id.clone(), r.avatar.clone());
         }
diff --git a/fractal-gtk/src/appop/user.rs b/fractal-gtk/src/appop/user.rs
index ab37b1f4..a6c08d74 100644
--- a/fractal-gtk/src/appop/user.rs
+++ b/fractal-gtk/src/appop/user.rs
@@ -3,6 +3,7 @@ use gtk::prelude::*;
 use crate::backend::{user, HandleError};
 use glib::clone;
 
+use std::path::PathBuf;
 use std::thread;
 
 use crate::app::App;
@@ -145,10 +146,10 @@ impl AppOp {
         });
     }
 
-    pub fn set_avatar(&mut self, path: String) {
+    pub fn set_avatar(&mut self, path: PathBuf) {
         let login_data = unwrap_or_unit_return!(self.login_data.clone());
         self.set_login_data(LoginData {
-            avatar: Some(path.into()),
+            avatar: Some(path),
             ..login_data
         });
     }
diff --git a/fractal-gtk/src/backend/media.rs b/fractal-gtk/src/backend/media.rs
index a88fe1e7..166c7dd8 100644
--- a/fractal-gtk/src/backend/media.rs
+++ b/fractal-gtk/src/backend/media.rs
@@ -3,6 +3,7 @@ use crate::globals;
 use fractal_api::identifiers::{Error as IdError, EventId, RoomId};
 use fractal_api::reqwest::Error as ReqwestError;
 use fractal_api::url::Url;
+use std::path::PathBuf;
 use std::sync::mpsc::Sender;
 
 use crate::backend::HTTP_CLIENT;
@@ -18,27 +19,17 @@ use fractal_api::r0::message::get_message_events::Response as GetMessagesEventsR
 
 use super::{dw_media, get_prev_batch_from, ContentType, ThreadPool};
 
-pub type MediaResult = Result<String, MediaError>;
+pub type MediaResult = Result<PathBuf, MediaError>;
 pub type MediaList = (Vec<Message>, String);
 
-pub fn get_thumb_async(
-    thread_pool: ThreadPool,
-    baseu: Url,
-    media: String,
-    tx: Sender<MediaResult>,
-) {
+pub fn get_thumb_async(thread_pool: ThreadPool, baseu: Url, media: Url, tx: Sender<MediaResult>) {
     thread_pool.run(move || {
         let fname = dw_media(baseu, &media, ContentType::default_thumbnail(), None);
         tx.send(fname).expect_log("Connection closed");
     });
 }
 
-pub fn get_media_async(
-    thread_pool: ThreadPool,
-    baseu: Url,
-    media: String,
-    tx: Sender<MediaResult>,
-) {
+pub fn get_media_async(thread_pool: ThreadPool, baseu: Url, media: Url, tx: Sender<MediaResult>) {
     thread_pool.run(move || {
         let fname = dw_media(baseu, &media, ContentType::Download, None);
         tx.send(fname).expect_log("Connection closed");
diff --git a/fractal-gtk/src/backend/mod.rs b/fractal-gtk/src/backend/mod.rs
index af2f8499..cf45abcc 100644
--- a/fractal-gtk/src/backend/mod.rs
+++ b/fractal-gtk/src/backend/mod.rs
@@ -1,19 +1,17 @@
 use fractal_api::identifiers::{EventId, RoomId};
 use fractal_api::reqwest::Error as ReqwestError;
-use fractal_api::url::{ParseError as UrlError, Url};
+use fractal_api::url::Url;
 use lazy_static::lazy_static;
 use log::error;
 use regex::Regex;
 use std::fmt::Debug;
 use std::fs::write;
 use std::io::{Error as IoError, Read};
-use std::path::Path;
+use std::path::PathBuf;
 use std::sync::{Arc, Condvar, Mutex};
 use std::thread;
-use std::time::SystemTimeError;
 
 use crate::client::Client;
-use crate::error::Error;
 use crate::util::cache_dir_path;
 use fractal_api::r0::context::get_context::request as get_context;
 use fractal_api::r0::context::get_context::Parameters as GetContextParameters;
@@ -118,11 +116,9 @@ pub fn get_prev_batch_from(
 
 #[derive(Debug)]
 pub enum MediaError {
-    InvalidMxcUrl(Option<UrlError>),
-    InvalidDownloadPath(Error),
-    Reqwest(ReqwestError),
+    MalformedMxcUrl,
     Io(IoError),
-    SystemTime(SystemTimeError),
+    Reqwest(ReqwestError),
 }
 
 impl From<ReqwestError> for MediaError {
@@ -137,35 +133,25 @@ impl From<IoError> for MediaError {
     }
 }
 
-impl From<SystemTimeError> for MediaError {
-    fn from(err: SystemTimeError) -> Self {
-        Self::SystemTime(err)
-    }
-}
-
 impl HandleError for MediaError {}
 
 pub fn dw_media(
     base: Url,
-    mxc: &str,
+    mxc: &Url,
     media_type: ContentType,
-    dest: Option<String>,
-) -> Result<String, MediaError> {
-    let mxc_url = Url::parse(mxc).map_err(|url_err| MediaError::InvalidMxcUrl(Some(url_err)))?;
-
-    if mxc_url.scheme() != "mxc" {
-        return Err(MediaError::InvalidMxcUrl(None));
+    dest: Option<PathBuf>,
+) -> Result<PathBuf, MediaError> {
+    if mxc.scheme() != "mxc" {
+        return Err(MediaError::MalformedMxcUrl);
     }
 
-    let server = mxc_url
-        .host()
-        .ok_or(MediaError::InvalidMxcUrl(None))?
-        .to_owned();
-    let media_id = mxc_url
+    let server = mxc.host().ok_or(MediaError::MalformedMxcUrl)?.to_owned();
+
+    let media_id = mxc
         .path_segments()
         .and_then(|mut ps| ps.next())
         .filter(|s| !s.is_empty())
-        .ok_or(MediaError::InvalidMxcUrl(None))?;
+        .ok_or(MediaError::MalformedMxcUrl)?;
 
     let request = if let ContentType::Thumbnail(width, height) = media_type {
         let params = GetContentThumbnailParameters {
@@ -180,19 +166,25 @@ pub fn dw_media(
         get_content(base, &params, &server, &media_id)
     }?;
 
-    let fname = match dest {
-        None if media_type.is_thumbnail() => cache_dir_path(Some("thumbs"), &media_id),
-        None => cache_dir_path(Some("medias"), &media_id),
-        Some(ref d) => Ok(d.clone()),
-    }
-    .map_err(MediaError::InvalidDownloadPath)?;
-
-    let fpath = Path::new(&fname);
+    let default_fname = || {
+        let dir = if media_type.is_thumbnail() {
+            "thumbs"
+        } else {
+            "medias"
+        };
+        cache_dir_path(Some(dir), &media_id)
+    };
+    let fname = dest.clone().map_or_else(default_fname, Ok)?;
 
     // If the file is already cached and recent enough, don't download it
-    if fpath.is_file()
-        && (dest.is_none() || fpath.metadata()?.modified()?.elapsed()?.as_secs() < 60)
-    {
+    let is_fname_recent = fname
+        .metadata()
+        .ok()
+        .and_then(|md| md.modified().ok())
+        .and_then(|modf| modf.elapsed().ok())
+        .map_or(false, |dur| dur.as_secs() < 60);
+
+    if fname.is_file() && (dest.is_none() || is_fname_recent) {
         Ok(fname)
     } else {
         HTTP_CLIENT
diff --git a/fractal-gtk/src/backend/room.rs b/fractal-gtk/src/backend/room.rs
index 5ff84740..aec0c336 100644
--- a/fractal-gtk/src/backend/room.rs
+++ b/fractal-gtk/src/backend/room.rs
@@ -6,6 +6,7 @@ use fractal_api::reqwest::Error as ReqwestError;
 use fractal_api::url::Url;
 use std::fs;
 use std::io::Error as IoError;
+use std::path::{Path, PathBuf};
 
 use std::collections::HashMap;
 use std::convert::TryFrom;
@@ -139,12 +140,7 @@ pub fn get_room_avatar(
     let avatar = response["url"].as_str().and_then(|s| Url::parse(s).ok());
     let dest = cache_dir_path(None, &room_id.to_string()).ok();
     if let Some(ref avatar) = avatar {
-        let _ = dw_media(
-            base,
-            avatar.as_str(),
-            ContentType::default_thumbnail(),
-            dest,
-        );
+        let _ = dw_media(base, avatar, ContentType::default_thumbnail(), dest);
     }
 
     Ok((room_id, avatar))
@@ -545,7 +541,7 @@ pub fn set_room_avatar(
     base: Url,
     access_token: AccessToken,
     room_id: RoomId,
-    avatar: String,
+    avatar: PathBuf,
 ) -> Result<(), SetRoomAvatarError> {
     let params = CreateStateEventsForKeyParameters {
         access_token: access_token.clone(),
@@ -592,7 +588,7 @@ impl HandleError for AttachedFileError {
 pub fn upload_file(
     base: Url,
     access_token: AccessToken,
-    fname: &str,
+    fname: &Path,
 ) -> Result<CreateContentResponse, AttachedFileError> {
     let params_upload = CreateContentParameters {
         access_token,
diff --git a/fractal-gtk/src/backend/user.rs b/fractal-gtk/src/backend/user.rs
index 32a59799..0e3efed9 100644
--- a/fractal-gtk/src/backend/user.rs
+++ b/fractal-gtk/src/backend/user.rs
@@ -6,16 +6,15 @@ use std::io::Error as IoError;
 
 use super::MediaError;
 use crate::actions::global::activate_action;
+use crate::appop::UserInfoCache;
 use crate::backend::ThreadPool;
 use crate::backend::HTTP_CLIENT;
-use crate::cache::CacheMap;
 use crate::util::cache_dir_path;
 use crate::util::ResultExpectLog;
 use log::error;
 use std::convert::TryInto;
 use std::path::PathBuf;
 use std::sync::mpsc::Sender;
-use std::sync::{Arc, Mutex};
 use std::thread;
 
 use crate::types::Member;
@@ -78,7 +77,7 @@ use crate::app::App;
 use crate::i18n::i18n;
 use crate::APPOP;
 
-pub type UserInfo = (String, String);
+pub type UserInfo = (String, PathBuf);
 
 #[derive(Debug)]
 pub struct NameError(ReqwestError);
@@ -564,7 +563,7 @@ pub fn set_user_avatar(
 
 pub fn get_user_info_async(
     thread_pool: ThreadPool,
-    user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+    user_info_cache: UserInfoCache,
     baseu: Url,
     uid: UserId,
     tx: Sender<UserInfo>,
@@ -628,12 +627,18 @@ impl From<ReqwestError> for GetUserAvatarError {
     }
 }
 
+impl From<MediaError> for GetUserAvatarError {
+    fn from(err: MediaError) -> Self {
+        Self::Download(err)
+    }
+}
+
 impl HandleError for GetUserAvatarError {}
 
 pub fn get_user_avatar(
     base: Url,
     user_id: &UserId,
-) -> Result<(String, String), GetUserAvatarError> {
+) -> Result<(String, PathBuf), GetUserAvatarError> {
     let request = get_profile(base.clone(), user_id)?;
     let response: GetProfileResponse = HTTP_CLIENT.get_client().execute(request)?.json()?;
 
@@ -644,15 +649,10 @@ pub fn get_user_avatar(
 
     let img = response
         .avatar_url
+        .as_ref()
         .map(|url| {
-            let dest = cache_dir_path(None, &user_id.to_string())
-                .map_err(MediaError::InvalidDownloadPath)?;
-            dw_media(
-                base,
-                url.as_str(),
-                ContentType::default_thumbnail(),
-                Some(dest),
-            )
+            let dest = cache_dir_path(None, &user_id.to_string())?;
+            dw_media(base, url, ContentType::default_thumbnail(), Some(dest))
         })
         .unwrap_or_else(|| Ok(Default::default()))
         .map_err(GetUserAvatarError::Download)?;
diff --git a/fractal-gtk/src/cache/mod.rs b/fractal-gtk/src/cache/mod.rs
index f17ef22c..980f0e82 100644
--- a/fractal-gtk/src/cache/mod.rs
+++ b/fractal-gtk/src/cache/mod.rs
@@ -1,3 +1,4 @@
+use crate::appop::UserInfoCache;
 use crate::backend::user;
 use crate::backend::ThreadPool;
 use crate::util::ResultExpectLog;
@@ -14,6 +15,7 @@ use failure::Error;
 use fractal_api::identifiers::UserId;
 use std::collections::HashMap;
 use std::hash::Hash;
+use std::path::PathBuf;
 use std::time::{Duration, Instant};
 
 use crate::globals;
@@ -23,7 +25,6 @@ use std::sync::mpsc::channel;
 use std::sync::mpsc::Receiver;
 use std::sync::mpsc::Sender;
 use std::sync::mpsc::TryRecvError;
-use std::sync::{Arc, Mutex};
 
 use crate::widgets::AvatarData;
 use std::cell::RefCell;
@@ -145,12 +146,12 @@ pub fn load() -> Result<CacheData, Error> {
 /// this downloads a avatar and stores it in the cache folder
 pub fn download_to_cache(
     thread_pool: ThreadPool,
-    user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+    user_info_cache: UserInfoCache,
     server_url: Url,
     uid: UserId,
     data: Rc<RefCell<AvatarData>>,
 ) {
-    let (tx, rx) = channel::<(String, String)>();
+    let (tx, rx) = channel::<(String, PathBuf)>();
     user::get_user_info_async(thread_pool, user_info_cache, server_url, uid, tx);
 
     gtk::timeout_add(50, move || match rx.try_recv() {
diff --git a/fractal-gtk/src/cache/state.rs b/fractal-gtk/src/cache/state.rs
index f86dc1fe..699e133b 100644
--- a/fractal-gtk/src/cache/state.rs
+++ b/fractal-gtk/src/cache/state.rs
@@ -110,8 +110,12 @@ impl FCache {
     fn get_store(&self) -> MutexGuard<Option<Cache>> {
         let mut guard = self.cache.lock().unwrap();
         if guard.is_none() {
-            let db: String =
-                cache_dir_path(None, "cache.mdl").expect("Fatal error: Can't start the cache");
+            let maybe_db_path = cache_dir_path(None, "cache.mdl").ok();
+            let db: String = maybe_db_path
+                .and_then(|p| p.to_str().map(Into::into))
+                .expect("Fatal error: Can't start the cache");
+            // TODO: Replace Cache with another library. Not expecting a proper
+            //       Path type for the path of the DB is bonkers.
             let mdl_cache = Cache::new(&db).expect("Fatal error: Can't start the cache");
             *guard = Some(mdl_cache);
         }
diff --git a/fractal-gtk/src/error.rs b/fractal-gtk/src/error.rs
index 6c87ffac..e9034489 100644
--- a/fractal-gtk/src/error.rs
+++ b/fractal-gtk/src/error.rs
@@ -1,5 +1,4 @@
 use serde::Deserialize;
-use std::io;
 
 #[derive(Clone, Debug, Deserialize)]
 pub struct StandardErrorResponse {
@@ -23,7 +22,6 @@ macro_rules! derror {
 #[derive(Debug)]
 pub enum Error {
     BackendError,
-    CacheError,
     ReqwestError(fractal_api::reqwest::Error),
     NetworkError(fractal_api::reqwest::StatusCode),
     MatrixError(MatrixErrorCode, String),
@@ -41,8 +39,5 @@ impl From<StandardErrorResponse> for Error {
     }
 }
 
-derror!(fractal_api::url::ParseError, Error::BackendError);
-derror!(io::Error, Error::BackendError);
 derror!(glib::error::Error, Error::BackendError);
 derror!(fractal_api::identifiers::Error, Error::BackendError);
-derror!(serde_json::Error, Error::CacheError);
diff --git a/fractal-gtk/src/model/fileinfo.rs b/fractal-gtk/src/model/fileinfo.rs
index ceb6bf85..f5df5305 100644
--- a/fractal-gtk/src/model/fileinfo.rs
+++ b/fractal-gtk/src/model/fileinfo.rs
@@ -1,9 +1,10 @@
+use fractal_api::url::Url;
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Info {
-    pub thumbnail_url: Option<String>,
+    pub thumbnail_url: Option<Url>,
     pub thumbnail_info: Option<JsonValue>,
     pub w: Option<u32>,
     pub h: Option<u32>,
diff --git a/fractal-gtk/src/model/member.rs b/fractal-gtk/src/model/member.rs
index f8c807f8..2f9885fb 100644
--- a/fractal-gtk/src/model/member.rs
+++ b/fractal-gtk/src/model/member.rs
@@ -1,9 +1,11 @@
+use either::Either;
 use fractal_api::identifiers::UserId;
 use fractal_api::r0::search::user::User;
 use fractal_api::r0::sync::get_joined_members::RoomMember;
 use fractal_api::url::Url;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
+use std::path::PathBuf;
 
 // TODO: Make this non-(de)serializable
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -12,7 +14,7 @@ pub struct Member {
     #[serde(rename = "display_name")]
     pub alias: Option<String>,
     #[serde(rename = "avatar_url")]
-    pub avatar: Option<String>,
+    pub avatar: Option<Either<Url, PathBuf>>,
 }
 
 impl Member {
@@ -37,7 +39,7 @@ impl From<User> for Member {
         Self {
             uid: user.user_id,
             alias: user.display_name,
-            avatar: user.avatar_url.map(Url::into_string),
+            avatar: user.avatar_url.map(Either::Left),
         }
     }
 }
@@ -47,7 +49,7 @@ impl From<(UserId, RoomMember)> for Member {
         Member {
             uid,
             alias: roommember.display_name,
-            avatar: roommember.avatar_url.as_ref().map(ToString::to_string),
+            avatar: roommember.avatar_url.map(Either::Left),
         }
     }
 }
diff --git a/fractal-gtk/src/model/message.rs b/fractal-gtk/src/model/message.rs
index 60bc4761..66d1d72a 100644
--- a/fractal-gtk/src/model/message.rs
+++ b/fractal-gtk/src/model/message.rs
@@ -1,12 +1,16 @@
 use chrono::prelude::*;
 use chrono::DateTime;
 use chrono::TimeZone;
-use fractal_api::identifiers::{Error as IdError, EventId, RoomId, UserId};
+use fractal_api::{
+    identifiers::{Error as IdError, EventId, RoomId, UserId},
+    url::Url,
+};
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
 use std::cmp::Ordering;
 use std::collections::HashMap;
 use std::convert::TryInto;
+use std::path::PathBuf;
 
 //FIXME make properties private
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -16,8 +20,10 @@ pub struct Message {
     pub body: String,
     pub date: DateTime<Local>,
     pub room: RoomId,
-    pub thumb: Option<String>,
-    pub url: Option<String>,
+    pub thumb: Option<Url>,
+    pub local_path_thumb: Option<PathBuf>,
+    pub url: Option<Url>,
+    pub local_path: Option<PathBuf>,
     // FIXME: This should be a required field but it is mandatory
     // to do it this way because because this struct is used both
     // for received messages and messages to send. At the moment
@@ -72,7 +78,9 @@ impl Message {
             date,
             room,
             thumb: None,
+            local_path_thumb: None,
             url: None,
+            local_path: None,
             formatted_body: None,
             format: None,
             source: None,
@@ -143,7 +151,9 @@ impl Message {
             mtype: type_.to_string(),
             body: String::new(),
             url: None,
+            local_path: None,
             thumb: None,
+            local_path_thumb: None,
             formatted_body: None,
             format: None,
             source: serde_json::to_string_pretty(&msg).ok(),
@@ -184,17 +194,12 @@ impl Message {
 
         match mtype.as_str() {
             "m.image" | "m.file" | "m.video" | "m.audio" => {
-                let url = c["url"].as_str().map(String::from).unwrap_or_default();
-                let mut t = c["info"]["thumbnail_url"]
+                self.url = c["url"].as_str().map(Url::parse).and_then(Result::ok);
+                self.thumb = c["info"]["thumbnail_url"]
                     .as_str()
-                    .map(String::from)
-                    .unwrap_or_default();
-                if t.is_empty() && !url.is_empty() {
-                    t = url.clone();
-                }
-
-                self.url = Some(url);
-                self.thumb = Some(t);
+                    .map(Url::parse)
+                    .and_then(Result::ok)
+                    .or_else(|| Some(self.url.clone()?));
             }
             "m.text" => {
                 // Only m.text messages can be replies for backward compatibility
@@ -213,18 +218,13 @@ impl Message {
     }
 
     fn parse_m_sticker(&mut self, c: &JsonValue) {
-        let url = c["url"].as_str().map(String::from).unwrap_or_default();
-        let mut t = c["info"]["thumbnail_url"]
+        self.url = c["url"].as_str().map(Url::parse).and_then(Result::ok);
+        self.thumb = c["info"]["thumbnail_url"]
             .as_str()
-            .map(String::from)
-            .unwrap_or_default();
-        if t.is_empty() && !url.is_empty() {
-            t = url.clone();
-        }
-
+            .map(Url::parse)
+            .and_then(Result::ok)
+            .or_else(|| Some(self.url.clone()?));
         self.body = c["body"].as_str().map(String::from).unwrap_or_default();
-        self.url = Some(url);
-        self.thumb = Some(t);
     }
 
     /// Create a vec of Message from a json event list
diff --git a/fractal-gtk/src/model/room.rs b/fractal-gtk/src/model/room.rs
index 80856f4e..029a5cbd 100644
--- a/fractal-gtk/src/model/room.rs
+++ b/fractal-gtk/src/model/room.rs
@@ -4,6 +4,7 @@ use crate::backend::user::get_user_avatar;
 use crate::model::member::Member;
 use crate::model::member::MemberList;
 use crate::model::message::Message;
+use either::Either;
 use fractal_api::identifiers::{Error as IdError, EventId, RoomId, UserId};
 use fractal_api::r0::directory::post_public_rooms::Chunk as PublicRoomsChunk;
 use fractal_api::r0::sync::sync_events::Response as SyncResponse;
@@ -12,6 +13,7 @@ use log::{debug, info};
 use serde::{Deserialize, Serialize};
 use std::collections::{HashMap, HashSet};
 use std::convert::{TryFrom, TryInto};
+use std::path::PathBuf;
 
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 pub enum RoomMembership {
@@ -81,7 +83,7 @@ pub enum RoomTag {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Room {
     pub id: RoomId,
-    pub avatar: Option<String>, // TODO: Use Option<Url>
+    pub avatar: Option<Url>,
     pub name: Option<String>,
     pub topic: Option<String>,
     pub alias: Option<String>,
@@ -160,7 +162,8 @@ impl Room {
 
             let mut r = Self {
                 name: calculate_room_name(stevents, &user_id),
-                avatar: evc(stevents, "m.room.avatar", "url"),
+                avatar: evc(stevents, "m.room.avatar", "url")
+                    .and_then(|ref url| Url::parse(url).ok()),
                 alias: evc(stevents, "m.room.canonical_alias", "alias"),
                 topic: evc(stevents, "m.room.topic", "topic"),
                 direct: direct.contains(&k),
@@ -209,7 +212,7 @@ impl Room {
                     {
                         let kicker = Member {
                             alias: Some(kicker_alias),
-                            avatar: Some(kicker_avatar),
+                            avatar: Some(Either::Right(kicker_avatar)),
                             uid: leave_id,
                         };
                         let reason = Reason::Kicked(
@@ -236,7 +239,7 @@ impl Room {
             .iter()
             .map(|(k, room)| {
                 let stevents = &room.invite_state.events;
-                let alias_avatar: Result<Option<(String, String)>, IdError> = stevents
+                let alias_avatar: Result<Option<(String, PathBuf)>, IdError> = stevents
                     .iter()
                     .find(|x| {
                         x["content"]["membership"] == "invite"
@@ -252,13 +255,14 @@ impl Room {
                 if let Some((alias, avatar)) = alias_avatar? {
                     let inv_sender = Member {
                         alias: Some(alias),
-                        avatar: Some(avatar),
+                        avatar: Some(Either::Right(avatar)),
                         uid: user_id.clone(),
                     };
 
                     Ok(Some(Self {
                         name: calculate_room_name(stevents, &user_id),
-                        avatar: evc(stevents, "m.room.avatar", "url"),
+                        avatar: evc(stevents, "m.room.avatar", "url")
+                            .and_then(|ref url| Url::parse(url).ok()),
                         alias: evc(stevents, "m.room.canonical_alias", "alias"),
                         topic: evc(stevents, "m.room.topic", "topic"),
                         direct: direct.contains(&k),
@@ -325,7 +329,7 @@ impl From<PublicRoomsChunk> for Room {
         Self {
             alias: input.canonical_alias.as_ref().map(ToString::to_string),
             name: input.name,
-            avatar: input.avatar_url.map(Url::into_string),
+            avatar: input.avatar_url,
             topic: input.topic,
             n_members: input.num_joined_members,
             world_readable: input.world_readable,
@@ -429,8 +433,12 @@ fn parse_room_member(msg: &JsonValue) -> Option<Member> {
 
     Some(Member {
         uid: msg["sender"].as_str().unwrap_or_default().try_into().ok()?,
-        alias: c["displayname"].as_str().map(Into::into),
-        avatar: c["avatar_url"].as_str().map(Into::into),
+        alias: c["displayname"].as_str().map(String::from),
+        avatar: c["avatar_url"]
+            .as_str()
+            .map(Url::parse)
+            .and_then(Result::ok)
+            .map(Either::Left),
     })
 }
 
diff --git a/fractal-gtk/src/uitypes.rs b/fractal-gtk/src/uitypes.rs
index c6c44b1f..8721c05a 100644
--- a/fractal-gtk/src/uitypes.rs
+++ b/fractal-gtk/src/uitypes.rs
@@ -2,7 +2,11 @@ use crate::types::Message;
 use crate::widgets;
 use chrono::prelude::DateTime;
 use chrono::prelude::Local;
-use fractal_api::identifiers::{EventId, UserId};
+use fractal_api::{
+    identifiers::{EventId, UserId},
+    url::Url,
+};
+use std::path::PathBuf;
 
 /* MessageContent contains all data needed to display one row
  * therefore it should contain only one Message body with one format
@@ -16,8 +20,9 @@ pub struct MessageContent {
     pub body: String,
     pub date: DateTime<Local>,
     pub replace_date: Option<DateTime<Local>>,
-    pub thumb: Option<String>,
-    pub url: Option<String>,
+    pub thumb: Option<Url>,
+    pub url: Option<Url>,
+    pub local_path: Option<PathBuf>,
     pub formatted_body: Option<String>,
     pub format: Option<String>,
     /* in some places we still need the backend message type (e.g. media viewer) */
diff --git a/fractal-gtk/src/util.rs b/fractal-gtk/src/util.rs
index 27ce7335..764a8e8e 100644
--- a/fractal-gtk/src/util.rs
+++ b/fractal-gtk/src/util.rs
@@ -1,4 +1,3 @@
-use crate::error::Error;
 use crate::globals::CACHE_PATH;
 use failure::format_err;
 use failure::Error as FailError;
@@ -8,19 +7,18 @@ use gio::{Settings, SettingsExt, SettingsSchemaSource};
 use html2pango::{html_escape, markup_links};
 use log::error;
 use std::fs::create_dir_all;
+use std::io::Error as IoError;
+use std::path::PathBuf;
 use std::sync::mpsc::SendError;
 
-pub fn cache_dir_path(dir: Option<&str>, name: &str) -> Result<String, Error> {
+pub fn cache_dir_path(dir: Option<&str>, name: &str) -> Result<PathBuf, IoError> {
     let path = CACHE_PATH.join(dir.unwrap_or_default());
 
     if !path.is_dir() {
         create_dir_all(&path)?;
     }
 
-    path.join(name)
-        .to_str()
-        .map(Into::into)
-        .ok_or(Error::CacheError)
+    Ok(path.join(name))
 }
 
 pub fn get_pixbuf_data(pb: &Pixbuf) -> Result<Vec<u8>, FailError> {
diff --git a/fractal-gtk/src/widgets/avatar.rs b/fractal-gtk/src/widgets/avatar.rs
index fcd1fe34..198dca34 100644
--- a/fractal-gtk/src/widgets/avatar.rs
+++ b/fractal-gtk/src/widgets/avatar.rs
@@ -1,4 +1,5 @@
 use std::cell::RefCell;
+use std::path::Path;
 use std::rc::Rc;
 
 use crate::util::cache_dir_path;
@@ -190,7 +191,7 @@ impl AvatarExt for gtk::Overlay {
     }
 }
 
-fn load_pixbuf(path: &str, size: i32) -> Option<Pixbuf> {
+fn load_pixbuf(path: &Path, size: i32) -> Option<Pixbuf> {
     if let Ok(pixbuf) = Pixbuf::new_from_file(&path) {
         // FIXME: We end up loading the file twice but we need to load the file first to find out its 
dimensions to be
         // able to decide wether to scale by width or height and gdk doesn't provide simple API to scale a 
loaded
diff --git a/fractal-gtk/src/widgets/image.rs b/fractal-gtk/src/widgets/image.rs
index ffaafe12..1f056bb7 100644
--- a/fractal-gtk/src/widgets/image.rs
+++ b/fractal-gtk/src/widgets/image.rs
@@ -1,4 +1,5 @@
 use crate::backend::{media, ThreadPool};
+use either::Either;
 use fractal_api::url::Url;
 use gdk::prelude::GdkContextExt;
 use gdk_pixbuf::Pixbuf;
@@ -9,7 +10,7 @@ use glib::source::Continue;
 use gtk::prelude::*;
 use gtk::DrawingArea;
 use log::error;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::sync::mpsc::channel;
 use std::sync::mpsc::TryRecvError;
 use std::sync::mpsc::{Receiver, Sender};
@@ -17,8 +18,8 @@ use std::sync::{Arc, Mutex};
 
 #[derive(Clone, Debug)]
 pub struct Image {
-    pub path: String,
-    pub local_path: Arc<Mutex<Option<String>>>,
+    pub path: Either<Url, PathBuf>,
+    pub local_path: Arc<Mutex<Option<PathBuf>>>,
     pub server_url: Url,
     pub max_size: Option<(i32, i32)>,
     pub widget: DrawingArea,
@@ -46,7 +47,7 @@ impl Image {
     ///           .size(Some((50, 50)))
     ///           .build();
     /// ```
-    pub fn new(server_url: Url, path: &str) -> Image {
+    pub fn new(server_url: Url, path: Either<Url, PathBuf>) -> Image {
         let da = DrawingArea::new();
         da.add_events(gdk::EventMask::ENTER_NOTIFY_MASK);
         da.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK);
@@ -63,7 +64,7 @@ impl Image {
         });
 
         Image {
-            path: path.to_string(),
+            path,
             local_path: Arc::new(Mutex::new(None)),
             server_url,
             max_size: None,
@@ -255,47 +256,48 @@ impl Image {
     /// If `path` starts with mxc this func download the img async, in other case the image is loaded
     /// in the `image` widget scaled to size
     pub fn load_async(&self, thread_pool: ThreadPool) {
-        if self.path.starts_with("mxc:") {
-            // asyn load
-            let (tx, rx): (Sender<media::MediaResult>, Receiver<media::MediaResult>) = channel();
-            let command = if self.thumb {
-                media::get_thumb_async
-            } else {
-                media::get_media_async
-            };
-            command(
-                thread_pool,
-                self.server_url.clone(),
-                self.path.to_string(),
-                tx,
-            );
-            let local_path = self.local_path.clone();
-            let pix = self.pixbuf.clone();
-            let scaled = self.scaled.clone();
-            let da = self.widget.clone();
-
-            da.get_style_context().add_class("image-spinner");
-            gtk::timeout_add(50, move || match rx.try_recv() {
-                Err(TryRecvError::Empty) => Continue(true),
-                Err(TryRecvError::Disconnected) => Continue(false),
-                Ok(Ok(fname)) => {
-                    *local_path.lock().unwrap() = Some(fname.clone());
-                    load_pixbuf(pix.clone(), scaled.clone(), da.clone(), &fname);
-                    da.get_style_context().remove_class("image-spinner");
-                    Continue(false)
-                }
-                Ok(Err(err)) => {
-                    error!("Image path could not be found due to error: {:?}", err);
-                    Continue(false)
-                }
-            });
-        } else {
-            load_pixbuf(
-                self.pixbuf.clone(),
-                self.scaled.clone(),
-                self.widget.clone(),
-                &self.path,
-            );
+        match self.path.as_ref() {
+            Either::Left(url) if url.scheme() == "mxc" => {
+                let mxc = url.clone();
+                // asyn load
+                let (tx, rx): (Sender<media::MediaResult>, Receiver<media::MediaResult>) =
+                    channel();
+                let command = if self.thumb {
+                    media::get_thumb_async
+                } else {
+                    media::get_media_async
+                };
+                command(thread_pool, self.server_url.clone(), mxc, tx);
+                let local_path = self.local_path.clone();
+                let pix = self.pixbuf.clone();
+                let scaled = self.scaled.clone();
+                let da = self.widget.clone();
+
+                da.get_style_context().add_class("image-spinner");
+                gtk::timeout_add(50, move || match rx.try_recv() {
+                    Err(TryRecvError::Empty) => Continue(true),
+                    Err(TryRecvError::Disconnected) => Continue(false),
+                    Ok(Ok(fname)) => {
+                        *local_path.lock().unwrap() = Some(fname.clone());
+                        load_pixbuf(pix.clone(), scaled.clone(), da.clone(), &fname);
+                        da.get_style_context().remove_class("image-spinner");
+                        Continue(false)
+                    }
+                    Ok(Err(err)) => {
+                        error!("Image path could not be found due to error: {:?}", err);
+                        Continue(false)
+                    }
+                });
+            }
+            Either::Right(path) => {
+                load_pixbuf(
+                    self.pixbuf.clone(),
+                    self.scaled.clone(),
+                    self.widget.clone(),
+                    &path,
+                );
+            }
+            _ => error!("The resource URL doesn't have the scheme mxc:"),
         }
     }
 }
@@ -304,10 +306,10 @@ pub fn load_pixbuf(
     pix: Arc<Mutex<Option<Pixbuf>>>,
     scaled: Arc<Mutex<Option<Pixbuf>>>,
     widget: DrawingArea,
-    fname: &str,
+    fname: &Path,
 ) {
     if is_gif(&fname) {
-        load_animation(pix, scaled, widget, &fname);
+        load_animation(pix, scaled, widget, fname);
         return;
     }
 
@@ -338,7 +340,7 @@ pub fn load_animation(
     pix: Arc<Mutex<Option<Pixbuf>>>,
     scaled: Arc<Mutex<Option<Pixbuf>>>,
     widget: DrawingArea,
-    fname: &str,
+    fname: &Path,
 ) {
     let res = PixbufAnimation::new_from_file(fname);
     if res.is_err() {
@@ -362,13 +364,12 @@ pub fn load_animation(
     });
 }
 
-pub fn is_gif(fname: &str) -> bool {
-    let p = &Path::new(fname);
-    if !p.is_file() {
+pub fn is_gif(fname: &Path) -> bool {
+    if !fname.is_file() {
         return false;
     }
 
-    if let Ok(info) = gio::File::new_for_path(&p).query_info(
+    if let Ok(info) = gio::File::new_for_path(fname).query_info(
         &gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
         gio::FileQueryInfoFlags::NONE,
         gio::NONE_CANCELLABLE,
diff --git a/fractal-gtk/src/widgets/inline_player.rs b/fractal-gtk/src/widgets/inline_player.rs
index 8b7502f8..5fce6ed9 100644
--- a/fractal-gtk/src/widgets/inline_player.rs
+++ b/fractal-gtk/src/widgets/inline_player.rs
@@ -23,7 +23,7 @@ use glib::clone;
 use gst::prelude::*;
 use gst::ClockTime;
 use gstreamer_pbutils::Discoverer;
-use log::{error, info, warn};
+use log::{error, warn};
 
 use gtk::prelude::*;
 use gtk::ButtonExt;
@@ -37,6 +37,7 @@ use fragile::Fragile;
 
 use std::cell::RefCell;
 use std::ops::Deref;
+use std::path::PathBuf;
 use std::rc::Rc;
 
 use std::sync::mpsc::channel;
@@ -54,8 +55,8 @@ pub trait PlayerExt {
     fn stop(&self);
     fn initialize_stream(
         player: &Rc<Self>,
-        media_url: &str,
-        server_url: &Url,
+        media_url: Url,
+        server_url: Url,
         thread_pool: ThreadPool,
         bx: &gtk::Box,
         start_playing: bool,
@@ -145,7 +146,7 @@ pub struct PlayerControls {
 pub trait MediaPlayer {
     fn get_player(&self) -> gst_player::Player;
     fn get_controls(&self) -> Option<PlayerControls>;
-    fn get_local_path_access(&self) -> Rc<RefCell<Option<String>>>;
+    fn get_local_path_access(&self) -> Rc<RefCell<Option<PathBuf>>>;
 }
 
 trait ControlsConnection {
@@ -158,7 +159,7 @@ trait ControlsConnection {
 pub struct AudioPlayerWidget {
     player: gst_player::Player,
     controls: PlayerControls,
-    local_path: Rc<RefCell<Option<String>>>,
+    local_path: Rc<RefCell<Option<PathBuf>>>,
 }
 
 impl Default for AudioPlayerWidget {
@@ -228,7 +229,7 @@ impl MediaPlayer for AudioPlayerWidget {
         Some(self.controls.clone())
     }
 
-    fn get_local_path_access(&self) -> Rc<RefCell<Option<String>>> {
+    fn get_local_path_access(&self) -> Rc<RefCell<Option<PathBuf>>> {
         self.local_path.clone()
     }
 }
@@ -237,7 +238,7 @@ impl MediaPlayer for AudioPlayerWidget {
 pub struct VideoPlayerWidget {
     player: gst_player::Player,
     controls: Option<PlayerControls>,
-    local_path: Rc<RefCell<Option<String>>>,
+    local_path: Rc<RefCell<Option<PathBuf>>>,
     dimensions: Rc<RefCell<Option<(i32, i32)>>>,
     state: Rc<RefCell<Option<gst_player::PlayerState>>>,
 }
@@ -465,7 +466,7 @@ impl MediaPlayer for VideoPlayerWidget {
         self.controls.clone()
     }
 
-    fn get_local_path_access(&self) -> Rc<RefCell<Option<String>>> {
+    fn get_local_path_access(&self) -> Rc<RefCell<Option<PathBuf>>> {
         self.local_path.clone()
     }
 }
@@ -501,15 +502,15 @@ impl<T: MediaPlayer + 'static> PlayerExt for T {
 
     fn initialize_stream(
         player: &Rc<Self>,
-        media_url: &str,
-        server_url: &Url,
+        media_url: Url,
+        server_url: Url,
         thread_pool: ThreadPool,
         bx: &gtk::Box,
         start_playing: bool,
     ) {
         bx.set_opacity(0.3);
         let (tx, rx): (Sender<media::MediaResult>, Receiver<media::MediaResult>) = channel();
-        media::get_media_async(thread_pool, server_url.clone(), media_url.to_string(), tx);
+        media::get_media_async(thread_pool, server_url, media_url, tx);
         let local_path = player.get_local_path_access();
         gtk::timeout_add(
             50,
@@ -523,23 +524,28 @@ impl<T: MediaPlayer + 'static> PlayerExt for T {
                         Continue(true)
                     },
                     Ok(Ok(path)) => {
-                        info!("MEDIA PATH: {}", &path);
-                        *local_path.borrow_mut() = Some(path.clone());
-                        if ! start_playing {
-                            if let Some(controls) = player.get_controls() {
-                                if let Ok(duration) = get_media_duration(&path) {
-                                    controls.timer.on_duration_changed(Duration(duration))
+                        match Url::from_file_path(&path) {
+                            Ok(uri) => {
+                                *local_path.borrow_mut() = Some(path);
+                                if !start_playing {
+                                    if let Some(controls) = player.get_controls() {
+                                        if let Ok(duration) = get_media_duration(&uri) {
+                                            controls.timer.on_duration_changed(Duration(duration))
+                                        }
+                                    }
+                                }
+                                player.get_player().set_uri(uri.as_str());
+                                if player.get_controls().is_some() {
+                                    ControlsConnection::init(&player);
+                                }
+                                bx.set_opacity(1.0);
+                                if start_playing {
+                                    player.play();
                                 }
                             }
-                        }
-                        let uri = format!("file://{}", path);
-                        player.get_player().set_uri(&uri);
-                        if player.get_controls().is_some() {
-                            ControlsConnection::init(&player);
-                        }
-                        bx.set_opacity(1.0);
-                        if start_playing {
-                            player.play();
+                            Err(_) => {
+                                error!("Media path is not valid: {:?}", path);
+                            }
                         }
                         Continue(false)
                     }
@@ -702,9 +708,9 @@ fn adjust_box_margins_to_video_dimensions(bx: &gtk::Box, video_width: i32, video
     }
 }
 
-pub fn get_media_duration(file: &str) -> Result<ClockTime, glib::Error> {
+pub fn get_media_duration(uri: &Url) -> Result<ClockTime, glib::Error> {
     let timeout = ClockTime::from_seconds(1);
     let discoverer = Discoverer::new(timeout)?;
-    let info = discoverer.discover_uri(&format!("file://{}", file))?;
+    let info = discoverer.discover_uri(uri.as_str())?;
     Ok(info.get_duration())
 }
diff --git a/fractal-gtk/src/widgets/media_viewer.rs b/fractal-gtk/src/widgets/media_viewer.rs
index e1c3069b..4d55f010 100644
--- a/fractal-gtk/src/widgets/media_viewer.rs
+++ b/fractal-gtk/src/widgets/media_viewer.rs
@@ -9,6 +9,7 @@ use std::collections::HashMap;
 use std::rc::Rc;
 
 use crate::i18n::i18n;
+use either::Either;
 use fractal_api::identifiers::UserId;
 use fractal_api::url::Url;
 use gdk::*;
@@ -314,6 +315,8 @@ impl Data {
     }
 
     pub fn redraw_media_in_viewport(&mut self, thread_pool: ThreadPool) {
+        let msg = &self.media_list[self.current_media_index];
+        let url = unwrap_or_unit_return!(msg.url.clone());
         let media_container = self
             .builder
             .get_object::<gtk::EventBox>("media_container")
@@ -323,11 +326,9 @@ impl Data {
             media_container.remove(&child);
         }
 
-        let msg = &self.media_list[self.current_media_index];
-        let url = msg.url.clone().unwrap_or_default();
         match msg.mtype.as_ref() {
             "m.image" => {
-                let image = image::Image::new(self.server_url.clone(), &url)
+                let image = image::Image::new(self.server_url.clone(), Either::Left(url))
                     .shrink_to_fit(true)
                     .center(true)
                     .build(thread_pool);
@@ -336,7 +337,7 @@ impl Data {
                 self.widget = Widget::Image(image);
             }
             "m.video" => {
-                let widget = self.create_video_widget(thread_pool, &url);
+                let widget = self.create_video_widget(thread_pool, url);
                 media_container.add(&widget.outer_box);
                 self.widget = Widget::Video(widget);
                 media_container.show_all();
@@ -348,7 +349,7 @@ impl Data {
         self.set_nav_btn_visibility();
     }
 
-    fn create_video_widget(&self, thread_pool: ThreadPool, url: &str) -> VideoWidget {
+    fn create_video_widget(&self, thread_pool: ThreadPool, url: Url) -> VideoWidget {
         let with_controls = true;
         let player = VideoPlayerWidget::new(with_controls);
         let bx = gtk::Box::new(gtk::Orientation::Vertical, 0);
@@ -356,7 +357,7 @@ impl Data {
         PlayerExt::initialize_stream(
             &player,
             url,
-            &self.server_url.clone(),
+            self.server_url.clone(),
             thread_pool,
             &bx,
             start_playing,
@@ -661,6 +662,8 @@ impl MediaViewer {
     }
 
     pub fn display_media_viewer(&mut self, thread_pool: ThreadPool, media_msg: Message) {
+        let url = unwrap_or_unit_return!(media_msg.url.clone());
+
         let previous_media_revealer = self
             .builder
             .get_object::<gtk::Revealer>("previous_media_revealer")
@@ -680,13 +683,13 @@ impl MediaViewer {
             .get_object::<gtk::EventBox>("media_container")
             .expect("Cant find media_container in ui file.");
 
-        let url = media_msg.url.clone().unwrap_or_default();
         match media_msg.mtype.as_ref() {
             "m.image" => {
-                let image = image::Image::new(self.data.borrow().server_url.clone(), &url)
-                    .shrink_to_fit(true)
-                    .center(true)
-                    .build(thread_pool);
+                let image =
+                    image::Image::new(self.data.borrow().server_url.clone(), Either::Left(url))
+                        .shrink_to_fit(true)
+                        .center(true)
+                        .build(thread_pool);
 
                 media_container.add(&image.widget);
                 media_container.show_all();
@@ -694,7 +697,7 @@ impl MediaViewer {
                 self.data.borrow_mut().widget = Widget::Image(image);
             }
             "m.video" => {
-                let video_widget = self.data.borrow().create_video_widget(thread_pool, &url);
+                let video_widget = self.data.borrow().create_video_widget(thread_pool, url);
                 media_container.add(&video_widget.outer_box);
                 media_container.show_all();
 
diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs
index 562327f2..9130ef0c 100644
--- a/fractal-gtk/src/widgets/message.rs
+++ b/fractal-gtk/src/widgets/message.rs
@@ -1,17 +1,16 @@
 use crate::i18n::i18n;
 use itertools::Itertools;
 
+use crate::appop::UserInfoCache;
 use crate::backend::ThreadPool;
-use crate::cache::CacheMap;
 use chrono::prelude::*;
-use fractal_api::identifiers::UserId;
+use either::Either;
 use fractal_api::r0::AccessToken;
 use fractal_api::url::Url;
 use glib::clone;
 use gtk::{prelude::*, ButtonExt, ContainerExt, LabelExt, Overlay, WidgetExt};
 use std::cmp::max;
 use std::rc::Rc;
-use std::sync::{Arc, Mutex};
 
 use crate::util::markup_text;
 
@@ -72,7 +71,7 @@ impl MessageBox {
     pub fn create(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         msg: &Message,
         has_header: bool,
         is_temp: bool,
@@ -129,7 +128,7 @@ impl MessageBox {
     pub fn tmpwidget(
         mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         msg: &Message,
     ) -> MessageBox {
         self.create(thread_pool, user_info_cache, msg, true, true);
@@ -143,7 +142,7 @@ impl MessageBox {
     pub fn update_header(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         msg: Message,
         has_header: bool,
     ) {
@@ -168,7 +167,7 @@ impl MessageBox {
     fn widget(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         msg: &Message,
     ) -> gtk::Box {
         // msg
@@ -265,7 +264,7 @@ impl MessageBox {
     fn build_room_msg_avatar(
         &self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         msg: &Message,
     ) -> widgets::Avatar {
         let uid = msg.sender.clone();
@@ -417,29 +416,35 @@ impl MessageBox {
     fn build_room_msg_image(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
         let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
 
-        let img_path = match msg.thumb {
-            // If the thumbnail is not a valid URL we use the msg.url
-            Some(ref m) if m.starts_with("mxc:") || m.starts_with("http") => m.clone(),
-            _ => msg.url.clone().unwrap_or_default(),
-        };
-        let image = widgets::image::Image::new(self.server_url.clone(), &img_path)
-            .size(Some(globals::MAX_IMAGE_SIZE))
-            .build(thread_pool);
+        // If the thumbnail is not a valid URL we use the msg.url
+        let img = msg
+            .thumb
+            .clone()
+            .filter(|m| m.scheme() == "mxc" || m.scheme().starts_with("http"))
+            .or_else(|| msg.url.clone())
+            .map(Either::Left)
+            .or_else(|| Some(Either::Right(msg.local_path.clone()?)));
+
+        if let Some(img_path) = img {
+            let image = widgets::image::Image::new(self.server_url.clone(), img_path)
+                .size(Some(globals::MAX_IMAGE_SIZE))
+                .build(thread_pool);
 
-        image.widget.get_style_context().add_class("image-widget");
+            image.widget.get_style_context().add_class("image-widget");
 
-        bx.pack_start(&image.widget, true, true, 0);
-        bx.show_all();
-        self.image = Some(image.widget);
-        self.connect_media_viewer(msg);
+            bx.pack_start(&image.widget, true, true, 0);
+            bx.show_all();
+            self.image = Some(image.widget);
+            self.connect_media_viewer(msg);
+        }
 
         bx
     }
 
     fn build_room_msg_sticker(&self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
         let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
-        if let Some(url) = msg.url.as_ref() {
-            let image = widgets::image::Image::new(self.server_url.clone(), url)
+        if let Some(url) = msg.url.clone() {
+            let image = widgets::image::Image::new(self.server_url.clone(), Either::Left(url))
                 .size(Some(globals::MAX_STICKER_SIZE))
                 .build(thread_pool);
             image.widget.set_tooltip_text(Some(&msg.body[..]));
@@ -452,16 +457,23 @@ impl MessageBox {
 
     fn build_room_audio_player(&self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
         let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6);
-        let player = AudioPlayerWidget::new();
-        let start_playing = false;
-        PlayerExt::initialize_stream(
-            &player,
-            &msg.url.clone().unwrap_or_default(),
-            &self.server_url,
-            thread_pool,
-            &bx,
-            start_playing,
-        );
+
+        if let Some(url) = msg.url.clone() {
+            let player = AudioPlayerWidget::new();
+            let start_playing = false;
+            PlayerExt::initialize_stream(
+                &player,
+                url,
+                self.server_url.clone(),
+                thread_pool,
+                &bx,
+                start_playing,
+            );
+
+            let control_box = PlayerExt::get_controls_container(&player)
+                .expect("Every AudioPlayer must have controls.");
+            bx.pack_start(&control_box, false, true, 0);
+        }
 
         let download_btn =
             gtk::Button::new_from_icon_name(Some("document-save-symbolic"), gtk::IconSize::Button);
@@ -475,10 +487,6 @@ impl MessageBox {
         let data = glib::Variant::from(evid);
         download_btn.set_action_target_value(Some(&data));
         download_btn.set_action_name(Some("message.save_as"));
-
-        let control_box = PlayerExt::get_controls_container(&player)
-            .expect("Every AudioPlayer must have controls.");
-        bx.pack_start(&control_box, false, true, 0);
         bx.pack_start(&download_btn, false, false, 3);
 
         let outer_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
@@ -494,72 +502,76 @@ impl MessageBox {
     }
 
     fn build_room_video_player(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
-        let with_controls = false;
-        let player = VideoPlayerWidget::new(with_controls);
         let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
-        let start_playing = false;
-        PlayerExt::initialize_stream(
-            &player,
-            &msg.url.clone().unwrap_or_default(),
-            &self.server_url,
-            thread_pool,
-            &bx,
-            start_playing,
-        );
 
-        let overlay = Overlay::new();
-        let video_widget = player.get_video_widget();
-        video_widget.set_size_request(-1, 390);
-        VideoPlayerWidget::auto_adjust_video_dimensions(&player);
-        overlay.add(&video_widget);
+        if let Some(url) = msg.url.clone() {
+            let with_controls = false;
+            let player = VideoPlayerWidget::new(with_controls);
+            let start_playing = false;
+            PlayerExt::initialize_stream(
+                &player,
+                url,
+                self.server_url.clone(),
+                thread_pool,
+                &bx,
+                start_playing,
+            );
 
-        let play_button = gtk::Button::new();
-        let play_icon = gtk::Image::new_from_icon_name(
-            Some("media-playback-start-symbolic"),
-            gtk::IconSize::Dialog,
-        );
-        play_button.set_image(Some(&play_icon));
-        play_button.set_halign(gtk::Align::Center);
-        play_button.set_valign(gtk::Align::Center);
-        play_button.get_style_context().add_class("osd");
-        play_button.get_style_context().add_class("play-icon");
-        play_button.get_style_context().add_class("flat");
-        let evid = msg
-            .id
-            .as_ref()
-            .map(|evid| evid.to_string())
-            .unwrap_or_default();
-        let data = glib::Variant::from(evid);
-        play_button.set_action_name(Some("app.open-media-viewer"));
-        play_button.set_action_target_value(Some(&data));
-        overlay.add_overlay(&play_button);
-
-        let menu_button = gtk::MenuButton::new();
-        let three_dot_icon =
-            gtk::Image::new_from_icon_name(Some("view-more-symbolic"), gtk::IconSize::Button);
-        menu_button.set_image(Some(&three_dot_icon));
-        menu_button.get_style_context().add_class("osd");
-        menu_button.get_style_context().add_class("round-button");
-        menu_button.get_style_context().add_class("flat");
-        menu_button.set_margin_top(12);
-        menu_button.set_margin_end(12);
-        menu_button.set_opacity(0.8);
-        menu_button.set_halign(gtk::Align::End);
-        menu_button.set_valign(gtk::Align::Start);
-        menu_button.connect_size_allocate(|button, allocation| {
-            let diameter = max(allocation.width, allocation.height);
-            button.set_size_request(diameter, diameter);
-        });
-        overlay.add_overlay(&menu_button);
-
-        let evid = msg.id.as_ref();
-        let redactable = msg.redactable;
-        let menu = MessageMenu::new(evid, &RowType::Video, &redactable, None, None);
-        menu_button.set_popover(Some(&menu.get_popover()));
+            let overlay = Overlay::new();
+            let video_widget = player.get_video_widget();
+            video_widget.set_size_request(-1, 390);
+            VideoPlayerWidget::auto_adjust_video_dimensions(&player);
+            overlay.add(&video_widget);
+
+            let play_button = gtk::Button::new();
+            let play_icon = gtk::Image::new_from_icon_name(
+                Some("media-playback-start-symbolic"),
+                gtk::IconSize::Dialog,
+            );
+            play_button.set_image(Some(&play_icon));
+            play_button.set_halign(gtk::Align::Center);
+            play_button.set_valign(gtk::Align::Center);
+            play_button.get_style_context().add_class("osd");
+            play_button.get_style_context().add_class("play-icon");
+            play_button.get_style_context().add_class("flat");
+            let evid = msg
+                .id
+                .as_ref()
+                .map(|evid| evid.to_string())
+                .unwrap_or_default();
+            let data = glib::Variant::from(evid);
+            play_button.set_action_name(Some("app.open-media-viewer"));
+            play_button.set_action_target_value(Some(&data));
+            overlay.add_overlay(&play_button);
+
+            let menu_button = gtk::MenuButton::new();
+            let three_dot_icon =
+                gtk::Image::new_from_icon_name(Some("view-more-symbolic"), gtk::IconSize::Button);
+            menu_button.set_image(Some(&three_dot_icon));
+            menu_button.get_style_context().add_class("osd");
+            menu_button.get_style_context().add_class("round-button");
+            menu_button.get_style_context().add_class("flat");
+            menu_button.set_margin_top(12);
+            menu_button.set_margin_end(12);
+            menu_button.set_opacity(0.8);
+            menu_button.set_halign(gtk::Align::End);
+            menu_button.set_valign(gtk::Align::Start);
+            menu_button.connect_size_allocate(|button, allocation| {
+                let diameter = max(allocation.width, allocation.height);
+                button.set_size_request(diameter, diameter);
+            });
+            overlay.add_overlay(&menu_button);
+
+            let evid = msg.id.as_ref();
+            let redactable = msg.redactable;
+            let menu = MessageMenu::new(evid, &RowType::Video, &redactable, None, None);
+            menu_button.set_popover(Some(&menu.get_popover()));
+
+            bx.pack_start(&overlay, true, true, 0);
+            self.connect_media_viewer(msg);
+            self.video_player = Some(player);
+        }
 
-        bx.pack_start(&overlay, true, true, 0);
-        self.connect_media_viewer(msg);
-        self.video_player = Some(player);
         bx
     }
 
diff --git a/fractal-gtk/src/widgets/room_history.rs b/fractal-gtk/src/widgets/room_history.rs
index bcfd5f4a..6c1c68e9 100644
--- a/fractal-gtk/src/widgets/room_history.rs
+++ b/fractal-gtk/src/widgets/room_history.rs
@@ -7,19 +7,17 @@ use log::warn;
 use std::cell::RefCell;
 use std::collections::VecDeque;
 use std::rc::Rc;
-use std::sync::{Arc, Mutex};
 
-use crate::appop::AppOp;
+use crate::appop::{AppOp, UserInfoCache};
 use crate::i18n::i18n;
 use crate::uitypes::MessageContent;
 use crate::uitypes::RowType;
 
 use crate::backend::ThreadPool;
-use crate::cache::CacheMap;
 use crate::globals;
 use crate::widgets;
 use crate::widgets::{PlayerExt, VideoPlayerWidget};
-use fractal_api::identifiers::{RoomId, UserId};
+use fractal_api::identifiers::RoomId;
 use fractal_api::r0::AccessToken;
 use fractal_api::url::Url;
 use gio::ActionMapExt;
@@ -305,7 +303,7 @@ impl RoomHistory {
     pub fn create(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         mut messages: Vec<MessageContent>,
     ) -> Option<()> {
         let mut position = messages.len();
@@ -435,11 +433,7 @@ impl RoomHistory {
         }));
     }
 
-    fn run_queue(
-        &mut self,
-        thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
-    ) -> Option<()> {
+    fn run_queue(&mut self, thread_pool: ThreadPool, user_info_cache: UserInfoCache) -> Option<()> {
         let queue = self.queue.clone();
         let edit_buffer = self.edit_buffer.clone();
         let rows = self.rows.clone();
@@ -549,7 +543,7 @@ impl RoomHistory {
     pub fn add_new_message(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         mut item: MessageContent,
     ) -> Option<()> {
         if item.msg.replace.is_some() {
@@ -602,7 +596,7 @@ impl RoomHistory {
     pub fn replace_message(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         mut item: MessageContent,
     ) -> Option<()> {
         let mut rows = self.rows.borrow_mut();
@@ -639,7 +633,7 @@ impl RoomHistory {
     pub fn remove_message(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         item: MessageContent,
     ) -> Option<()> {
         let mut rows = self.rows.borrow_mut();
@@ -693,7 +687,7 @@ impl RoomHistory {
     pub fn add_new_messages_in_batch(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         messages: Vec<MessageContent>,
     ) -> Option<()> {
         /* TODO: use lazy loading */
@@ -706,7 +700,7 @@ impl RoomHistory {
     pub fn add_old_messages_in_batch(
         &mut self,
         thread_pool: ThreadPool,
-        user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+        user_info_cache: UserInfoCache,
         messages: Vec<MessageContent>,
     ) -> Option<()> {
         self.rows.borrow().view.reset_request_sent();
@@ -737,7 +731,7 @@ impl RoomHistory {
 /* This function creates the content for a Row based on the content of msg */
 fn create_row(
     thread_pool: ThreadPool,
-    user_info_cache: Arc<Mutex<CacheMap<UserId, (String, String)>>>,
+    user_info_cache: UserInfoCache,
     row: MessageContent,
     has_header: bool,
     server_url: Url,
diff --git a/fractal-gtk/src/widgets/roomlist.rs b/fractal-gtk/src/widgets/roomlist.rs
index 1a9fee5c..0ed74bfd 100644
--- a/fractal-gtk/src/widgets/roomlist.rs
+++ b/fractal-gtk/src/widgets/roomlist.rs
@@ -225,7 +225,7 @@ impl RoomListGroup {
         });
     }
 
-    pub fn set_room_avatar(&mut self, room_id: RoomId, av: Option<String>) {
+    pub fn set_room_avatar(&mut self, room_id: RoomId, av: Option<Url>) {
         if let Some(r) = self.rooms.get_mut(&room_id) {
             r.set_avatar(av.clone());
         }
@@ -635,7 +635,7 @@ impl RoomList {
         });
     }
 
-    pub fn set_room_avatar(&mut self, room_id: RoomId, av: Option<String>) {
+    pub fn set_room_avatar(&mut self, room_id: RoomId, av: Option<Url>) {
         run_in_group!(self, &room_id, set_room_avatar, room_id, av);
     }
 
diff --git a/fractal-gtk/src/widgets/roomrow.rs b/fractal-gtk/src/widgets/roomrow.rs
index 444de44d..dcd0315e 100644
--- a/fractal-gtk/src/widgets/roomrow.rs
+++ b/fractal-gtk/src/widgets/roomrow.rs
@@ -1,3 +1,4 @@
+use fractal_api::url::Url;
 use gtk::prelude::*;
 
 use crate::types::Room;
@@ -116,7 +117,7 @@ impl RoomRow {
         self.text.set_text(&name);
     }
 
-    pub fn set_avatar(&mut self, avatar: Option<String>) {
+    pub fn set_avatar(&mut self, avatar: Option<Url>) {
         self.room.avatar = avatar;
 
         let name = self.room.name.clone().unwrap_or_else(|| "...".to_string());


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