diff --git a/Cargo.lock b/Cargo.lock index 94153d3d2..9dd879914 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2826,7 +2826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -5050,12 +5050,12 @@ dependencies = [ [[package]] name = "robius-common" version = "0.3.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" [[package]] name = "robius-directories" version = "6.0.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" dependencies = [ "dirs-sys", "jni", @@ -5065,7 +5065,7 @@ dependencies = [ [[package]] name = "robius-file-picker" version = "0.3.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" dependencies = [ "android-build", "block2", @@ -5087,7 +5087,7 @@ dependencies = [ [[package]] name = "robius-location" version = "0.3.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" dependencies = [ "android-build", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5102,7 +5102,7 @@ dependencies = [ [[package]] name = "robius-open" version = "0.3.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" dependencies = [ "block2", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5119,7 +5119,7 @@ dependencies = [ [[package]] name = "robius-share" version = "0.3.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" dependencies = [ "android-build", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -5147,7 +5147,7 @@ dependencies = [ [[package]] name = "robius-web-auth-session" version = "0.3.0" -source = "git+https://github.com/project-robius/robius#f697e566310a201528afbac665359d53914ffc0c" +source = "git+https://github.com/project-robius/robius#b766e62b0600f5d2ee21cc6995648346fc277bd8" dependencies = [ "block2", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 6a01c52f8..3b4705423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,12 @@ makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "gl_ makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "gl_sampling_fallback" } -## Including this crate automatically configures all `robius-*` crates to work with Makepad. -robius-use-makepad = "0.1.1" robius-directories = { git = "https://github.com/project-robius/robius" } robius-file-picker = { git = "https://github.com/project-robius/robius" } -robius-location = { git = "https://github.com/project-robius/robius" } -robius-open = { git = "https://github.com/project-robius/robius" } -robius-share = { git = "https://github.com/project-robius/robius" } +robius-location = { git = "https://github.com/project-robius/robius" } +robius-open = { git = "https://github.com/project-robius/robius" } +robius-share = { git = "https://github.com/project-robius/robius" } +robius-use-makepad = "0.1.1" ## auto-configures all `robius-*` crates to work with Makepad. anyhow = "1.0" diff --git a/README.md b/README.md index 0ab5bc97a..411845c1f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ The following table shows which host systems can currently be used to build Robr ## Known issues - Matrix-specific links (`https://matrix.to/...`) aren't fully handled in-app yet. - Ignoring/unignoring a user clears all timelines (see: https://github.com/matrix-org/matrix-rust-sdk/issues/1703); the timeline will be re-filled gradually via back pagination, but the viewport position is not maintained. - - Currently, accessing system geolocation on Android may not succeed due to failing to prompt the user for permission. Please enable the location permission in the App Info settings page for Robrix, and then it should work as expected. > [!IMPORTANT] diff --git a/src/home/location_preview.rs b/src/home/location_preview.rs index 958b4416f..f549e11c2 100644 --- a/src/home/location_preview.rs +++ b/src/home/location_preview.rs @@ -8,9 +8,13 @@ use std::time::SystemTime; use makepad_widgets::*; +use matrix_sdk::ruma::MilliSecondsSinceUnixEpoch; use robius_location::Coordinates; -use crate::location::{get_latest_location, request_location_update, LocationAction, LocationRequest, LocationUpdate}; +use crate::{ + location::{request_location_update, LocationAction, LocationRequest, LocationUpdate}, + utils::time_ago, +}; script_mod! { use mod.prelude.widgets.* @@ -97,6 +101,22 @@ struct LocationPreview { #[rust] timestamp: Option, } +fn location_error_message(error: robius_location::Error) -> &'static str { + use robius_location::Error; + match error { + Error::AuthorizationDenied => + "Permission denied. Give Robrix location access in your device's settings, then try again.", + Error::TemporarilyUnavailable => + "Your location isn't available right now. Please try again in a moment.", + Error::Network => + "A network problem prevented getting your location. Check your connection and try again.", + Error::PermanentlyUnavailable => + "Location services aren't available on this device.", + Error::AndroidEnvironment | Error::NotMainThread | Error::Unknown => + "Couldn't get your location. Please try again.", + } +} + impl Widget for LocationPreview { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { let mut needs_redraw = false; @@ -139,10 +159,16 @@ impl Widget for LocationPreview { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { let text = match self.coords { Some(Ok(c)) => { - format!("➡ Current location: {:.6}, {:.6}", c.latitude, c.longitude) + match self.timestamp + .and_then(MilliSecondsSinceUnixEpoch::from_system_time) + .and_then(time_ago) + { + Some(age) => format!("📍 Your location (from {}): {:.6}, {:.6}", age.to_ascii_lowercase(), c.latitude, c.longitude), + None => format!("📍 Your location: {:.6}, {:.6}", c.latitude, c.longitude), + } } - Some(Err(e)) => format!("➡ Error getting location: {e:?}"), - None => String::from("➡ Current location is not yet available."), + Some(Err(e)) => format!("⚠️ {}", location_error_message(e)), + None => String::from("📍 Getting your location…"), }; self.label(cx, ids!(location_label)).set_text(cx, &text); self.view.draw_walk(cx, scope, walk) @@ -151,12 +177,11 @@ impl Widget for LocationPreview { impl LocationPreview { - fn show(&mut self) { - request_location_update(LocationRequest::UpdateOnce); - if let Some(loc) = get_latest_location() { - self.coords = Some(Ok(loc.coordinates)); - self.timestamp = loc.time; - } + fn show(&mut self, cx: &mut Cx) { + request_location_update(cx, LocationRequest::UpdateOnce); + self.coords = None; + self.timestamp = None; + self.button(cx, ids!(send_location_button)).set_enabled(cx, false); self.visible = true; } @@ -175,9 +200,9 @@ impl LocationPreview { } impl LocationPreviewRef { - pub fn show(&self) { + pub fn show(&self, cx: &mut Cx) { if let Some(mut inner) = self.borrow_mut() { - inner.show(); + inner.show(cx); } } diff --git a/src/location.rs b/src/location.rs index 515d00322..7c6feef78 100644 --- a/src/location.rs +++ b/src/location.rs @@ -1,6 +1,6 @@ //! Functions for querying the device's current location. -use std::{sync::{mpsc::{self, Receiver, Sender}, Mutex}, time::SystemTime}; +use std::{sync::Mutex, time::SystemTime}; use makepad_widgets::{Cx, error, log}; use robius_location::{Access, Accuracy, Coordinates, Location, Manager}; @@ -12,7 +12,6 @@ pub enum LocationAction { Update(LocationUpdate), /// The location handler encountered an error. Error(robius_location::Error), - None } /// An updated location sample, including coordinates and a system timestamp. @@ -24,7 +23,7 @@ pub struct LocationUpdate { static LATEST_LOCATION: Mutex> = Mutex::new(None); -/// Returns the latest location update's coordinates, if available. +/// Returns the latest location update, if one has been received. /// /// Note that this function is guaranteed to return `None` if /// [`init_location_subscriber`] has not been called yet. @@ -62,90 +61,51 @@ impl robius_location::Handler for LocationHandler { } -fn location_request_loop( - request_receiver: Receiver, - mut manager: ManagerWrapper, -) -> Result<(), robius_location::Error> { - - manager.update_once()?; - - while let Ok(request) = request_receiver.recv() { - match request { - LocationRequest::UpdateOnce => { - manager.update_once()?; - } - LocationRequest::StartUpdates => { - manager.start_updates()?; - } - LocationRequest::StopUpdates => { - manager.stop_updates()?; - } - } - } - - error!("Location request loop exited unexpectedly (the senders all died)."); - Err(robius_location::Error::Unknown) -} - - pub enum LocationRequest { UpdateOnce, StartUpdates, StopUpdates, } -static LOCATION_REQUEST_SENDER: Mutex>> = Mutex::new(None); +/// A wrapper struct for storing the singleton location manager in Cx globals. +#[derive(Default)] +struct LocationManagerGlobal(Option); /// Submits a request to start, stop, or get a single new location update(s). -pub fn request_location_update(request: LocationRequest) { - if let Some(sender) = LOCATION_REQUEST_SENDER.lock().unwrap().as_ref() { - if let Err(err) = sender.send(request) { - error!("Error sending location request: {err:?}"); +pub fn request_location_update(cx: &mut Cx, request: LocationRequest) { + let Some(manager) = cx.global::().0.as_mut() else { + error!("Location subscriber not initialized on this thread."); + Cx::post_action(LocationAction::Error(robius_location::Error::Unknown)); + return; + }; + let (result, show_error) = match request { + LocationRequest::UpdateOnce => (manager.update_once(), true), + LocationRequest::StartUpdates => (manager.start_updates(), true), + LocationRequest::StopUpdates => (manager.stop_updates(), false), + }; + if let Err(e) = result { + error!("Error handling location request: {e:?}"); + if show_error { + Cx::post_action(LocationAction::Error(e)); } - } else { - error!("No location request sender available."); } } -/// Spawns a thread to listen for location requests and updates to the latest location. +/// Initializes the location manager and requests a single location update. /// -/// This will request a single location update immediately upon starting. /// To request additional updates, use [`request_location_update`]. /// -/// It is okay to call this function multiple times, as it will only re-initialize -/// the location subscriber thread if it has not been initialized yet -/// or if it has died and needs to be restarted. -/// -/// This function requires passing in a reference to `Cx`, -/// which isn't used, but acts as a guarantee that this function -/// must only be called by the main UI thread. -pub fn init_location_subscriber(_cx: &mut Cx) -> Result<(), robius_location::Error> { - let mut lrs = LOCATION_REQUEST_SENDER.lock().unwrap(); - if lrs.is_some() { - log!("Location subscriber already initialized."); +/// It is okay to call this function multiple times; it only initializes the manager once. +pub fn init_location_subscriber(cx: &mut Cx) -> Result<(), robius_location::Error> { + let lm = cx.global::(); + if lm.0.is_some() { + // log!("Location subscriber already initialized."); return Ok(()); } - let manager = ManagerWrapper(Manager::new(LocationHandler)?); - manager.request_authorization(Access::Foreground, Accuracy::Precise)?; - let _ = manager.update_once(); - let (request_sender, request_receiver) = mpsc::channel::(); - *lrs = Some(request_sender); - std::thread::spawn(|| location_request_loop(request_receiver, manager)); + let new_manager = Manager::new(LocationHandler)?; + new_manager.request_authorization(Access::Foreground, Accuracy::Precise)?; + let _ = new_manager.update_once(); + lm.0 = Some(new_manager); Ok(()) } - -struct ManagerWrapper(Manager); -unsafe impl Send for ManagerWrapper {} -unsafe impl Sync for ManagerWrapper {} -impl std::ops::Deref for ManagerWrapper { - type Target = Manager; - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl std::ops::DerefMut for ManagerWrapper { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 0b1760caf..609c7c6b0 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -447,12 +447,12 @@ impl RoomInputBar { if let Err(_e) = init_location_subscriber(cx) { error!("Failed to initialize location subscriber"); enqueue_popup_notification( - "Failed to initialize location services.", + "Couldn't start location services. Please try again.", PopupKind::Error, None, ); } - self.view.location_preview(cx, ids!(location_preview)).show(); + self.view.location_preview(cx, ids!(location_preview)).show(cx); self.redraw(cx); } diff --git a/src/utils.rs b/src/utils.rs index f1e682bb3..b8d2cd743 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -564,15 +564,12 @@ pub fn stringify_pagination_error( /// /// # Cases: /// - **Less than 60 seconds ago**: Returns `"Just now"`. -/// - **Less than 60 minutes ago**: Returns `"X minutes ago"`, where X is the number of minutes. +/// - **Less than 60 minutes ago**: Returns `"X min(s) ago"`, where X is the number of minutes. /// - **Same day**: Returns `"HH:MM"` (current time format for today). /// - **Yesterday**: Returns `"Yesterday at HH:MM"` for messages from the previous day. /// - **Within the past week**: Returns the name of the day (e.g., "Tuesday"). /// - **Older than a week**: Returns `"DD/MM/YY"` as the absolute date. /// -/// # Arguments: -/// - `millis`: The Unix timestamp in milliseconds to format. -/// /// # Returns: /// - `Option` representing the human-readable time or `None` if formatting fails. pub fn relative_format(millis: MilliSecondsSinceUnixEpoch) -> Option { @@ -584,7 +581,7 @@ pub fn relative_format(millis: MilliSecondsSinceUnixEpoch) -> Option { // Handle different time ranges and format accordingly if duration < Duration::seconds(60) { - Some("Now".to_string()) + Some("Just now".to_string()) } else if duration < Duration::minutes(60) { let minutes_text = if duration.num_minutes() == 1 { "min" } else { "mins" }; Some(format!("{} {} ago", duration.num_minutes(), minutes_text)) @@ -604,6 +601,32 @@ pub fn relative_format(millis: MilliSecondsSinceUnixEpoch) -> Option { } } +/// Formats the given Unix timestamp in milliseconds into a fully-relative "time ago" label, +/// +/// # Cases: +/// - **Less than 1 hour ago**: Delegates to [`relative_format`] (`"Just now"` or `"X mins ago"`). +/// - **Less than 24 hours ago**: Returns `"X hours ago"`, where X is the number of hours. +/// - **Yesterday** (24 to 48 hours ago): Returns `"yesterday"`. +/// - **Older than that**: Returns `"X days ago"`, where X is the number of days. +/// +/// # Returns: +/// - `Option` representing the human-readable time or `None` if formatting fails. +pub fn time_ago(millis: MilliSecondsSinceUnixEpoch) -> Option { + let datetime = unix_time_millis_to_datetime(millis)?; + let duration = Local::now() - datetime; + if duration < Duration::hours(1) { + // Same as the rooms-list format for recent times ("Now", "X mins ago"). + relative_format(millis) + } else if duration < Duration::hours(24) { + let hours = duration.num_hours(); + Some(format!("{hours} {} ago", if hours == 1 { "hour" } else { "hours" })) + } else if duration < Duration::hours(48) { + Some("yesterday".to_string()) + } else { + Some(format!("{} days ago", duration.num_days())) + } +} + /// Returns the first "letter" (Unicode grapheme) of given user name, /// skipping any leading "@" characters. pub fn user_name_first_letter(user_name: &str) -> Option<&str> {