feat(sync): include reading list in snapshots

This commit is contained in:
2026-05-16 06:17:38 -04:00
parent b2b3d5deca
commit 323271fc1f
6 changed files with 284 additions and 3 deletions
+1
View File
@@ -31,6 +31,7 @@ mod spaces;
mod splits;
mod sync;
mod sync_notes;
mod sync_reading_list;
mod tab_group_order;
mod tab_groups;
mod tab_lifecycle;
@@ -123,6 +123,9 @@ impl BrowserCore {
for record in body.notes {
self.apply_note_sync_record(record, &mut summary)?;
}
for record in body.reading_list {
self.apply_reading_list_sync_record(record, &mut summary)?;
}
Ok(summary)
}
@@ -0,0 +1,89 @@
use std::time::{Duration, UNIX_EPOCH};
use ely_domain::{
ReadingListEntry, ReadingListId, ReadingProgress, ReadingProgressPercent, SyncObjectKind,
SyncObjectPolicy, UrlText,
};
use ely_sync_client::SyncClientError;
use super::{BrowserCore, sync::snapshot_schema_error};
use crate::{
sync_engine::SyncSnapshotApplySummary,
sync_records::{ReadingListSyncRecord, ReadingProgressSyncRecord},
};
impl BrowserCore {
pub(crate) fn visible_reading_list_for_sync(&self) -> Vec<&ReadingListEntry> {
if self.sync_object_policy(SyncObjectKind::ReadingList) == SyncObjectPolicy::Paused {
return Vec::new();
}
self.reading_list
.iter()
.filter(|entry| {
self.profiles
.iter()
.any(|profile| profile.id() == entry.profile_id() && profile.allows_sync())
})
.collect()
}
pub(super) fn apply_reading_list_sync_record(
&mut self,
record: ReadingListSyncRecord,
summary: &mut SyncSnapshotApplySummary,
) -> Result<(), SyncClientError> {
let entry_id = ReadingListId::parse(&record.id).map_err(snapshot_schema_error)?;
let profile_id = self.sync_profile_id(&record.profile_id)?;
let space_id = self.sync_space_id(&record.space_id, record.space_name.as_deref())?;
let source_url = UrlText::parse(&record.source_url).map_err(snapshot_schema_error)?;
let progress = reading_progress_from_sync_record(record.progress)?;
let added_at = UNIX_EPOCH + Duration::from_secs(record.added_at_secs);
let existing_index = self.reading_list_sync_index(&entry_id, &profile_id, &source_url);
let id = existing_index
.and_then(|index| self.reading_list.get(index).map(|entry| entry.id().clone()))
.unwrap_or(entry_id);
let mut entry =
ReadingListEntry::new(profile_id, space_id, record.title, source_url, added_at)
.map_err(snapshot_schema_error)?;
entry.set_progress(progress);
let entry = ReadingListEntry::restore(id, entry);
match existing_index {
Some(index) if self.reading_list[index] == entry => summary.record_skipped(),
Some(index) => {
self.reading_list[index] = entry;
summary.record_updated();
}
None => {
self.reading_list.push(entry);
summary.record_imported();
}
}
Ok(())
}
fn reading_list_sync_index(
&self,
entry_id: &ReadingListId,
profile_id: &ely_domain::ProfileId,
source_url: &UrlText,
) -> Option<usize> {
self.reading_list.iter().position(|entry| entry.id() == entry_id).or_else(|| {
self.reading_list.iter().position(|entry| {
entry.profile_id() == profile_id && entry.source_url() == source_url
})
})
}
}
fn reading_progress_from_sync_record(
record: ReadingProgressSyncRecord,
) -> Result<ReadingProgress, SyncClientError> {
match record {
ReadingProgressSyncRecord::Unread => Ok(ReadingProgress::Unread),
ReadingProgressSyncRecord::InProgress { percent } => ReadingProgressPercent::new(percent)
.map(ReadingProgress::InProgress)
.map_err(snapshot_schema_error),
ReadingProgressSyncRecord::Finished => Ok(ReadingProgress::Finished),
}
}
+60 -1
View File
@@ -1,7 +1,8 @@
use std::time::{SystemTime, UNIX_EPOCH};
use ely_domain::{
ArchivePolicy, BookmarkEntry, BrowserTab, NoteEntry, NoteTarget, Space, TabFlags,
ArchivePolicy, BookmarkEntry, BrowserTab, NoteEntry, NoteTarget, ReadingListEntry,
ReadingProgress, Space, TabFlags,
};
use serde::{Deserialize, Serialize};
@@ -19,6 +20,8 @@ pub(crate) struct SyncSnapshotBody {
pub(crate) tabs: Vec<TabSyncRecord>,
#[serde(default)]
pub(crate) notes: Vec<NoteSyncRecord>,
#[serde(default)]
pub(crate) reading_list: Vec<ReadingListSyncRecord>,
}
impl SyncSnapshotBody {
@@ -54,6 +57,16 @@ impl SyncSnapshotBody {
NoteSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
})
.collect(),
reading_list: core
.visible_reading_list_for_sync()
.into_iter()
.map(|entry| {
ReadingListSyncRecord::from_entry(
entry,
core.sync_space_name_for(entry.space_id()),
)
})
.collect(),
}
}
}
@@ -234,6 +247,52 @@ impl NoteTargetSyncRecord {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct ReadingListSyncRecord {
pub(crate) id: String,
pub(crate) profile_id: String,
pub(crate) space_id: String,
#[serde(default)]
pub(crate) space_name: Option<String>,
pub(crate) title: String,
pub(crate) source_url: String,
pub(crate) progress: ReadingProgressSyncRecord,
pub(crate) added_at_secs: u64,
}
impl ReadingListSyncRecord {
fn from_entry(entry: &ReadingListEntry, space_name: Option<String>) -> Self {
Self {
id: entry.id().as_str().to_string(),
profile_id: entry.profile_id().as_str().to_string(),
space_id: entry.space_id().as_str().to_string(),
space_name,
title: entry.title().to_string(),
source_url: entry.source_url().as_str().to_string(),
progress: ReadingProgressSyncRecord::from_progress(*entry.progress()),
added_at_secs: system_time_secs(entry.added_at()),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum ReadingProgressSyncRecord {
Unread,
InProgress { percent: u8 },
Finished,
}
impl ReadingProgressSyncRecord {
fn from_progress(progress: ReadingProgress) -> Self {
match progress {
ReadingProgress::Unread => Self::Unread,
ReadingProgress::InProgress(percent) => Self::InProgress { percent: percent.value() },
ReadingProgress::Finished => Self::Finished,
}
}
}
fn default_sync_enabled() -> bool {
true
}