Skip to content

Commit 930e6a7

Browse files
authored
Synced tabs now includes more metadata about tab, tab-groups and windows on a device. (#7089)
1 parent 1c41558 commit 930e6a7

File tree

10 files changed

+285
-44
lines changed

10 files changed

+285
-44
lines changed

components/tabs/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ on the request and re-upload its tabs.
3333

3434
## Payload format
3535

36-
Every remote sync record is roughly a list of tabs with their URL history (think of the back button). There is one record for each client.
36+
The sync payload has 3 distinct concepts:
37+
38+
* Tabs: a title, a URL history (think back button), whether it is pinned/active etc, if it is in a tab group, etc.
39+
* Windows: So remote clients can display our tabs based on Window.
40+
* Tab groups: So tab-groups can be recreated (or at last reflected) on remote devices.
41+
42+
These are distinct data-structures - eg, a tab has a "window id" and a "tab group id", and there
43+
are separate maps for these groups and windows.
3744

3845
### Association with device IDs
3946

components/tabs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ uniffi::custom_type!(TabsGuid, String, {
3030
lower: |obj| obj.into(),
3131
});
3232

33-
pub use crate::storage::{ClientRemoteTabs, RemoteTabRecord, TabsDeviceType};
33+
pub use crate::storage::{ClientRemoteTabs, LocalTabsInfo, RemoteTabRecord, TabsDeviceType};
3434
pub use crate::store::{RemoteCommandStore, TabsStore};
3535
pub use error::{ApiResult, Error, Result, TabsApiError};
3636
use sync15::DeviceType;

components/tabs/src/storage.rs

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ pub struct RemoteTabRecord {
3737
pub last_used: i64, // In ms.
3838
#[uniffi(default = false)]
3939
pub inactive: bool,
40+
#[uniffi(default = false)]
41+
pub pinned: bool,
42+
/// The index within the window_id.
43+
#[uniffi(default = 0)]
44+
pub index: u32,
45+
#[uniffi(default = "")]
46+
pub window_id: String,
47+
#[uniffi(default = "")]
48+
pub tab_group_id: String,
4049
}
4150
pub type RemoteTab = RemoteTabRecord;
4251

@@ -49,6 +58,54 @@ pub struct ClientRemoteTabs {
4958
/// Number of ms since the unix epoch (as reported by the server's clock)
5059
pub last_modified: i64,
5160
pub remote_tabs: Vec<RemoteTab>,
61+
pub tab_groups: HashMap<String, TabGroup>,
62+
pub windows: HashMap<String, Window>,
63+
}
64+
65+
#[derive(uniffi::Enum, Clone, Debug, Default)]
66+
#[repr(u8)]
67+
pub enum WindowType {
68+
#[default]
69+
Normal = 0,
70+
}
71+
72+
impl From<u8> for WindowType {
73+
fn from(value: u8) -> Self {
74+
match value {
75+
0 => WindowType::Normal,
76+
_ => {
77+
warn!("Unknown window type {}, defaulting to Normal", value);
78+
WindowType::Normal
79+
}
80+
}
81+
}
82+
}
83+
84+
#[derive(uniffi::Record, Debug, Clone)]
85+
#[cfg_attr(test, derive(Default))]
86+
pub struct Window {
87+
pub id: String,
88+
pub last_used: Timestamp,
89+
pub index: u32,
90+
pub window_type: WindowType,
91+
}
92+
93+
/// A tab-group, representing a session store `TabGroupStateData`.
94+
#[derive(uniffi::Record, Debug, Clone)]
95+
#[cfg_attr(test, derive(Default))]
96+
pub struct TabGroup {
97+
pub id: String,
98+
pub name: String,
99+
pub color: String,
100+
pub collapsed: bool,
101+
}
102+
103+
// This is what we expect clients to supply as their own tabs.
104+
#[derive(uniffi::Record, Debug, Clone, Default)]
105+
pub struct LocalTabsInfo {
106+
pub tabs: Vec<RemoteTab>,
107+
pub tab_groups: HashMap<String, TabGroup>,
108+
pub windows: HashMap<String, Window>,
52109
}
53110

54111
pub(crate) enum DbConnection {
@@ -70,7 +127,7 @@ pub(crate) enum DbConnection {
70127
// (Note however we don't attempt to remove the database when no remote tabs exist, so having
71128
// no remote tabs in an existing DB is also a normal situation)
72129
pub struct TabsStorage {
73-
pub(crate) local_tabs: RefCell<Option<Vec<RemoteTab>>>,
130+
pub(crate) local_tabs: RefCell<Option<LocalTabsInfo>>,
74131
db_path: PathBuf,
75132
db_connection: DbConnection,
76133
}
@@ -159,8 +216,8 @@ impl TabsStorage {
159216
}
160217
}
161218

162-
pub fn update_local_state(&mut self, local_state: Vec<RemoteTab>) {
163-
let num_tabs = local_state.len();
219+
pub fn update_local_state(&mut self, local_state: LocalTabsInfo) {
220+
let num_tabs = local_state.tabs.len();
164221
self.local_tabs.borrow_mut().replace(local_state);
165222
info!("update_local_state has {num_tabs} tab entries");
166223
}
@@ -761,14 +818,7 @@ mod tests {
761818
guid: "device-1".to_string(),
762819
record: TabsRecord {
763820
id: "device-1".to_string(),
764-
client_name: "Device #1".to_string(),
765-
tabs: vec![TabsRecordTab {
766-
title: "the title".to_string(),
767-
url_history: vec!["https://mozilla.org/".to_string()],
768-
icon: Some("https://mozilla.org/icon".to_string()),
769-
last_used: 1643764207000,
770-
..Default::default()
771-
}],
821+
..Default::default()
772822
},
773823
last_modified: 1643764207000,
774824
},
@@ -777,13 +827,7 @@ mod tests {
777827
record: TabsRecord {
778828
id: "device-outdated".to_string(),
779829
client_name: "Device outdated".to_string(),
780-
tabs: vec![TabsRecordTab {
781-
title: "the title".to_string(),
782-
url_history: vec!["https://mozilla.org/".to_string()],
783-
icon: Some("https://mozilla.org/icon".to_string()),
784-
last_used: 1643764207000,
785-
..Default::default()
786-
}],
830+
..Default::default()
787831
},
788832
last_modified: 1443764207000, // old
789833
},
@@ -918,6 +962,7 @@ mod tests {
918962
last_used: 1711929600015, // 4/1/2024
919963
..Default::default()
920964
}],
965+
..Default::default()
921966
},
922967
last_modified: 1711929600015, // 4/1/2024
923968
},
@@ -952,6 +997,7 @@ mod tests {
952997
..Default::default()
953998
},
954999
],
1000+
..Default::default()
9551001
},
9561002
last_modified: 1711929600015, // 4/1/2024
9571003
},
@@ -1100,11 +1146,11 @@ mod tests {
11001146
let new_records = vec![(
11011147
TabsRecord {
11021148
id: "device-not-synced".to_string(),
1103-
client_name: "".to_string(),
11041149
tabs: vec![TabsRecordTab {
11051150
url_history: vec!["https://example2.com".to_string()],
11061151
..Default::default()
11071152
}],
1153+
..Default::default()
11081154
},
11091155
ServerTimestamp::from_millis(now.as_millis_i64()),
11101156
)];
@@ -1175,7 +1221,6 @@ mod tests {
11751221
let new_records = vec![(
11761222
TabsRecord {
11771223
id: "device-recent".to_string(),
1178-
client_name: "".to_string(),
11791224
tabs: vec![
11801225
TabsRecordTab {
11811226
url_history: vec!["https://example99.com".to_string()],
@@ -1186,6 +1231,7 @@ mod tests {
11861231
..Default::default()
11871232
},
11881233
],
1234+
..Default::default()
11891235
},
11901236
ServerTimestamp::default(),
11911237
)];
@@ -1331,11 +1377,11 @@ mod tests {
13311377
let new_records = vec![(
13321378
TabsRecord {
13331379
id: "device-1".to_string(),
1334-
client_name: "".to_string(),
13351380
tabs: vec![TabsRecordTab {
13361381
url_history: vec!["https://example1.com".to_string()],
13371382
..Default::default()
13381383
}],
1384+
..Default::default()
13391385
},
13401386
ServerTimestamp::default(),
13411387
)];

components/tabs/src/store.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
use crate::storage::{ClientRemoteTabs, RemoteTab, TabsStorage};
5+
use crate::storage::{ClientRemoteTabs, LocalTabsInfo, RemoteTab, TabsStorage};
66
use crate::{ApiResult, PendingCommand, RemoteCommand};
7+
use std::collections::HashMap;
78
use std::path::Path;
89
use std::sync::{Arc, Mutex};
910

@@ -31,8 +32,18 @@ impl TabsStore {
3132
self.storage.lock().unwrap().close()
3233
}
3334

35+
/// For when we have no concept of tab groups or windows.
36+
/// Deprecated for desktop, hopefully one day deprecated on mobile.
3437
pub fn set_local_tabs(&self, local_state: Vec<RemoteTab>) {
35-
self.storage.lock().unwrap().update_local_state(local_state);
38+
self.set_local_tabs_info(LocalTabsInfo {
39+
tabs: local_state,
40+
tab_groups: HashMap::new(),
41+
windows: HashMap::new(),
42+
})
43+
}
44+
45+
pub fn set_local_tabs_info(&self, info: LocalTabsInfo) {
46+
self.storage.lock().unwrap().update_local_state(info);
3647
}
3748

3849
// like remote_tabs, but serves the uniffi layer

components/tabs/src/sync/engine.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ impl ClientRemoteTabs {
5454
device_type: remote_client.device_type,
5555
last_modified: last_modified.as_millis(),
5656
remote_tabs: record.tabs.into_iter().map(Into::into).collect(),
57+
tab_groups: record
58+
.tab_groups
59+
.into_iter()
60+
.map(|(n, v)| (n, v.into()))
61+
.collect(),
62+
windows: record
63+
.windows
64+
.into_iter()
65+
.map(|(n, v)| (n, v.into()))
66+
.collect(),
5767
}
5868
}
5969
}
@@ -167,7 +177,7 @@ impl SyncEngine for TabsEngine {
167177
}
168178
};
169179

170-
let Some(ref tabs) = *storage.local_tabs.borrow() else {
180+
let Some(ref tabs_info) = *storage.local_tabs.borrow() else {
171181
// It's a less than ideal outcome if at startup (or any time) we are asked to
172182
// sync tabs before the app has told us what the tabs are, so make noise, but
173183
// don't actually write that we have no tabs.
@@ -183,7 +193,22 @@ impl SyncEngine for TabsEngine {
183193
let mut record = TabsRecord {
184194
id: local_id.clone(),
185195
client_name,
186-
tabs: tabs.iter().map(Clone::clone).map(Into::into).collect(),
196+
tabs: tabs_info
197+
.tabs
198+
.iter()
199+
.map(Clone::clone)
200+
.map(Into::into)
201+
.collect(),
202+
windows: tabs_info
203+
.windows
204+
.iter()
205+
.map(|(n, v)| (n.clone(), v.clone().into()))
206+
.collect(),
207+
tab_groups: tabs_info
208+
.tab_groups
209+
.iter()
210+
.map(|(n, v)| (n.clone(), v.clone().into()))
211+
.collect(),
187212
};
188213
super::prepare_for_upload(&mut record);
189214

components/tabs/src/sync/mod.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ pub fn prepare_for_upload(record: &mut TabsRecord) {
4242
sanitized_history.push(url);
4343
}
4444
}
45-
4645
tab.url_history = sanitized_history;
4746
// Potentially truncate the title to some limit
4847
tab.title = slice_up_to(tab.title, MAX_TITLE_CHAR_LENGTH);
@@ -51,7 +50,14 @@ pub fn prepare_for_upload(record: &mut TabsRecord) {
5150
.collect::<Vec<_>>();
5251
// Sort the tabs so when we trim tabs it's the oldest tabs
5352
sanitized_tabs.sort_by(|a, b| b.last_used.cmp(&a.last_used));
54-
trim_tabs_length(&mut sanitized_tabs, MAX_PAYLOAD_SIZE);
53+
// deduct tab group and window info from the total.
54+
let used = payload_support::compute_serialized_size(&record.windows)
55+
.unwrap_or_default()
56+
.saturating_add(
57+
payload_support::compute_serialized_size(&record.tab_groups).unwrap_or_default(),
58+
);
59+
let size = MAX_PAYLOAD_SIZE.saturating_sub(used);
60+
trim_tabs_length(&mut sanitized_tabs, size);
5561
record.tabs = sanitized_tabs;
5662
info!("prepare_for_upload found {} tabs", record.tabs.len());
5763
}
@@ -123,6 +129,7 @@ mod tests {
123129
url_history: vec!["https://foo.bar".to_owned()],
124130
..Default::default()
125131
}],
132+
..Default::default()
126133
};
127134
prepare_for_upload(&mut record);
128135

@@ -160,6 +167,7 @@ mod tests {
160167
..Default::default()
161168
},
162169
],
170+
..Default::default()
163171
};
164172
prepare_for_upload(&mut record);
165173
// check we truncated correctly.
@@ -202,7 +210,7 @@ mod tests {
202210
let mut record = TabsRecord {
203211
id: "me".to_string(),
204212
client_name: "name".to_string(),
205-
tabs: Vec::new(),
213+
..Default::default()
206214
};
207215

208216
// Given the example, we know that we can fit 440 tabs of this size.
@@ -260,6 +268,7 @@ mod tests {
260268
..Default::default()
261269
},
262270
],
271+
..Default::default()
263272
};
264273

265274
prepare_for_upload(&mut record);

0 commit comments

Comments
 (0)