Reflect real sync state on the Sync settings page

`SyncConnectionState` was a one-variant enum (`SignedOut`), so the
Sync page rendered "Local-only · sign-in coming soon" even after the
bearer token landed on disk and the upload thread completed. The
state machine now mirrors the actual lifecycle.

What lands:
- `SyncConnectionState` gains `SignedIn`, `AwaitingDeviceApproval`,
  `SyncReady { last_synced_at_secs }`, `SyncError { message }`.
  `SyncObjectState::Synced` joins the per-object enum so individual
  rows can advertise "Synced" once a successful upload lands.
- `BrowserCore` stores the current `SyncConnectionState` and exposes
  `set_sync_connection_state`. `sync_status` now propagates the live
  state into the snapshot the UI reads.
- `ElyShell::probe_initial_sync_state` inspects
  `<profile_data>/sync/bearer.token` synchronously at construction
  so the first render of the sync page is honest about whether the
  user is signed in.
- A `std::sync::mpsc` channel ferries upload outcomes from the
  off-thread worker back to the shell; the existing 8 ms tick
  drains it and stamps `core.set_sync_connection_state` with the
  freshest result. The UI now shows "Signed in · awaiting first
  sync", "Synced · last upload Xm ago", "Sync error · …", and the
  worker-special "Signed in · waiting for device approval" when the
  server returns `device_not_approved`.
This commit is contained in:
2026-05-15 19:58:42 -04:00
parent 6b2578c3a8
commit 467dcb1f87
7 changed files with 260 additions and 79 deletions
@@ -266,6 +266,7 @@ fn render_state_dot(state: SyncObjectState) -> AnyElement {
SyncObjectState::LocalOnly => colors::ink_4(),
SyncObjectState::Paused => colors::ink_5(),
SyncObjectState::PrivacyControlled => colors::accent(),
SyncObjectState::Synced => colors::success(),
};
div().size(px(8.0)).rounded_full().bg(rgb(color)).into_any_element()
@@ -304,12 +305,48 @@ fn render_policy_toggle(
.into_any_element()
}
fn connection_label(connection: &SyncConnectionState) -> &'static str {
fn connection_label(connection: &SyncConnectionState) -> String {
match connection {
SyncConnectionState::SignedOut => "Local-only · sign-in coming soon",
SyncConnectionState::SignedOut => "Local-only · drop a session token to enable".to_string(),
SyncConnectionState::SignedIn => "Signed in · awaiting first sync".to_string(),
SyncConnectionState::AwaitingDeviceApproval => {
"Signed in · waiting for device approval".to_string()
}
SyncConnectionState::SyncReady { last_synced_at_secs } => {
format!("Synced · last upload {}", relative_time_since(*last_synced_at_secs))
}
SyncConnectionState::SyncError { message } => {
format!("Sync error · {}", short_message(message))
}
}
}
fn relative_time_since(secs: u64) -> String {
use std::time::{Duration, SystemTime, UNIX_EPOCH};
let when = UNIX_EPOCH + Duration::from_secs(secs);
let elapsed = SystemTime::now().duration_since(when).unwrap_or_default();
let total_secs = elapsed.as_secs();
if total_secs < 60 {
return format!("{total_secs}s ago");
}
if total_secs < 3600 {
return format!("{}m ago", total_secs / 60);
}
if total_secs < 86400 {
return format!("{}h ago", total_secs / 3600);
}
format!("{}d ago", total_secs / 86400)
}
fn short_message(message: &str) -> String {
const MAX_LEN: usize = 72;
if message.len() <= MAX_LEN {
return message.to_string();
}
let truncated: String = message.chars().take(MAX_LEN - 1).collect();
format!("{truncated}")
}
fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
match kind {
SyncObjectKind::Spaces => "Spaces",
@@ -329,6 +366,7 @@ fn sync_object_state_label(state: SyncObjectState) -> &'static str {
SyncObjectState::LocalOnly => "Local only",
SyncObjectState::Paused => "Paused",
SyncObjectState::PrivacyControlled => "Privacy controlled",
SyncObjectState::Synced => "Synced",
}
}