Skip to main content

aven_core/
local_state.rs

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    /// Returns the last time this installation's sync credentials were refused.
27    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    /// Remembers an authentication refusal until a later sync succeeds.
35    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    /// Clears a previous authentication refusal after a successful sync.
43    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/// Database-local sidebar preferences shared across workspaces and projects.
124#[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}