1use std::collections::BTreeSet;
2
3use anyhow::Result;
4
5use crate::db::{Database, begin_immediate, get_meta, set_meta};
6use crate::ids::WorkspaceId;
7
8const IOS_QUEUE_WORKSPACE_META_KEY: &str = "ios_queue_workspace_id";
9const ONBOARDING_META_KEY: &str = "tui_onboarding_version";
10const SYNC_ACCESS_REFUSED_AT_META_KEY: &str = "sync_access_refused_at";
11const FIRST_LAUNCH_ONBOARDING_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SyncAccessRefusal {
15 pub at: String,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum OnboardingStatus {
20 Due,
21 Complete,
22 Established,
23}
24
25impl Database {
26 pub async fn sync_access_refusal(&self) -> Result<Option<SyncAccessRefusal>> {
28 Ok(self
29 .meta(SYNC_ACCESS_REFUSED_AT_META_KEY)
30 .await?
31 .map(|at| SyncAccessRefusal { at }))
32 }
33
34 pub async fn record_sync_access_refusal(&self) -> Result<SyncAccessRefusal> {
36 let at = crate::ids::now();
37 let mut conn = self.acquire_writer().await?;
38 set_meta(&mut conn, SYNC_ACCESS_REFUSED_AT_META_KEY, &at).await?;
39 Ok(SyncAccessRefusal { at })
40 }
41
42 pub async fn clear_sync_access_refusal(&self) -> Result<()> {
44 let mut conn = self.acquire_writer().await?;
45 sqlx::query("DELETE FROM meta WHERE key = ?")
46 .bind(SYNC_ACCESS_REFUSED_AT_META_KEY)
47 .execute(&mut *conn)
48 .await?;
49 Ok(())
50 }
51
52 pub async fn restore_queue_workspace(&self) -> Result<crate::workspaces::Workspace> {
53 let mut conn = self.acquire_writer().await?;
54 let mut tx = begin_immediate(&mut conn).await?;
55 let workspaces = crate::workspaces::list_workspaces(&mut tx).await?;
56 let stored_id = get_meta(&mut tx, IOS_QUEUE_WORKSPACE_META_KEY)
57 .await?
58 .and_then(|value| value.parse::<WorkspaceId>().ok());
59 let selected = stored_id
60 .and_then(|id| workspaces.iter().find(|workspace| workspace.id == id))
61 .or_else(|| workspaces.first())
62 .cloned()
63 .ok_or_else(|| anyhow::anyhow!("no available workspace"))?;
64 set_meta(&mut tx, IOS_QUEUE_WORKSPACE_META_KEY, selected.id.as_str()).await?;
65 tx.commit().await?;
66 Ok(selected)
67 }
68
69 pub async fn select_queue_workspace(
70 &self,
71 workspace_id: &WorkspaceId,
72 ) -> Result<crate::workspaces::Workspace> {
73 let mut conn = self.acquire_writer().await?;
74 let mut tx = begin_immediate(&mut conn).await?;
75 let workspace = crate::workspaces::workspace_for_id(&mut tx, workspace_id).await?;
76 set_meta(&mut tx, IOS_QUEUE_WORKSPACE_META_KEY, workspace.id.as_str()).await?;
77 tx.commit().await?;
78 Ok(workspace)
79 }
80
81 pub async fn onboarding_status(&self) -> Result<OnboardingStatus> {
82 let mut conn = self.acquire_reader().await?;
83 let marker = get_meta(&mut conn, ONBOARDING_META_KEY).await?;
84 if marker_version(marker.as_deref())
85 .is_some_and(|version| version >= FIRST_LAUNCH_ONBOARDING_VERSION)
86 {
87 return Ok(OnboardingStatus::Complete);
88 }
89
90 let has_tasks: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks)")
91 .fetch_one(&mut *conn)
92 .await?;
93 Ok(if has_tasks {
94 OnboardingStatus::Established
95 } else {
96 OnboardingStatus::Due
97 })
98 }
99
100 pub async fn complete_onboarding(&self) -> Result<()> {
101 let mut conn = self.acquire_writer().await?;
102 let mut tx = begin_immediate(&mut conn).await?;
103 let marker = get_meta(&mut tx, ONBOARDING_META_KEY).await?;
104 if marker_version(marker.as_deref())
105 .is_none_or(|version| version < FIRST_LAUNCH_ONBOARDING_VERSION)
106 {
107 set_meta(
108 &mut tx,
109 ONBOARDING_META_KEY,
110 &FIRST_LAUNCH_ONBOARDING_VERSION.to_string(),
111 )
112 .await?;
113 }
114 tx.commit().await?;
115 Ok(())
116 }
117}
118
119fn marker_version(value: Option<&str>) -> Option<u32> {
120 value?.trim().parse().ok()
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
125pub enum SidebarSection {
126 Views,
127 Scope,
128 Projects,
129}
130
131impl SidebarSection {
132 fn meta_key(self) -> &'static str {
133 match self {
134 Self::Views => "tui_sidebar_views_collapsed",
135 Self::Scope => "tui_sidebar_scope_collapsed",
136 Self::Projects => "tui_sidebar_projects_collapsed",
137 }
138 }
139}
140
141impl Database {
142 pub async fn collapsed_sidebar_sections(&self) -> Result<BTreeSet<SidebarSection>> {
143 let mut conn = self.acquire_reader().await?;
144 let mut sections = BTreeSet::new();
145 for section in [
146 SidebarSection::Views,
147 SidebarSection::Scope,
148 SidebarSection::Projects,
149 ] {
150 if get_meta(&mut conn, section.meta_key())
151 .await?
152 .as_deref()
153 .map(str::trim)
154 == Some("true")
155 {
156 sections.insert(section);
157 }
158 }
159 Ok(sections)
160 }
161
162 pub async fn set_sidebar_section_collapsed(
163 &self,
164 section: SidebarSection,
165 collapsed: bool,
166 ) -> Result<()> {
167 let mut conn = self.acquire_writer().await?;
168 let mut tx = begin_immediate(&mut conn).await?;
169 set_meta(
170 &mut tx,
171 section.meta_key(),
172 if collapsed { "true" } else { "false" },
173 )
174 .await?;
175 tx.commit().await?;
176 Ok(())
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[tokio::test]
185 async fn sync_access_refusal_persists_until_cleared() {
186 let database = Database::open(std::path::Path::new(":memory:"))
187 .await
188 .unwrap();
189 assert_eq!(database.sync_access_refusal().await.unwrap(), None);
190
191 let recorded = database.record_sync_access_refusal().await.unwrap();
192 assert!(!recorded.at.is_empty());
193 assert_eq!(
194 database.sync_access_refusal().await.unwrap(),
195 Some(recorded)
196 );
197
198 database.clear_sync_access_refusal().await.unwrap();
199 assert_eq!(database.sync_access_refusal().await.unwrap(), None);
200 }
201
202 #[tokio::test]
203 async fn sidebar_preferences_are_local_and_invalid_values_default_to_expanded() {
204 let database = Database::open(std::path::Path::new(":memory:"))
205 .await
206 .unwrap();
207 let before: i64 = {
208 let mut conn = database.acquire_reader().await.unwrap();
209 sqlx::query_scalar("SELECT COUNT(*) FROM changes")
210 .fetch_one(&mut *conn)
211 .await
212 .unwrap()
213 };
214 database
215 .set_sidebar_section_collapsed(SidebarSection::Scope, true)
216 .await
217 .unwrap();
218 {
219 let mut conn = database.acquire_writer().await.unwrap();
220 set_meta(&mut conn, SidebarSection::Views.meta_key(), "invalid")
221 .await
222 .unwrap();
223 }
224 assert_eq!(
225 database.collapsed_sidebar_sections().await.unwrap(),
226 BTreeSet::from([SidebarSection::Scope])
227 );
228 let mut conn = database.acquire_reader().await.unwrap();
229 let after: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM changes")
230 .fetch_one(&mut *conn)
231 .await
232 .unwrap();
233 assert_eq!(before, after);
234 }
235}