Skip to content

Commit

Permalink
Add account switch functionality (#55)
Browse files Browse the repository at this point in the history
* Add signin/index to select sessions

* Add signin loader, update commands

* Update sidebar

* Add AccountSwitch modal

* Fix root
  • Loading branch information
sugyan authored Apr 20, 2024
1 parent 2fa0892 commit 7c664ea
Show file tree
Hide file tree
Showing 17 changed files with 446 additions and 148 deletions.
1 change: 0 additions & 1 deletion src-tauri/capabilities/dialog.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"windows"
],
"permissions": [
"dialog:allow-confirm",
"dialog:allow-message"
]
}
2 changes: 1 addition & 1 deletion src-tauri/gen/schemas/capabilities.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 26 additions & 2 deletions src-tauri/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::appdata;
use crate::consts::EmitEvent;
use crate::error::{Error, Result};
use crate::event::NotificationEvent;
use crate::session::{TauriPluginStore, APPDATA_CURRENT};
use crate::session::{values, TauriPluginStore, APPDATA_CURRENT};
use crate::state::State;
use crate::task::{poll_feed, poll_notifications, poll_unread_notifications};
use atrium_api::agent::store::SessionStore;
Expand All @@ -11,8 +11,9 @@ use atrium_api::records::Record;
use atrium_api::types::string::{AtIdentifier, Datetime, Did, Language};
use atrium_api::types::{Collection, Union};
use atrium_xrpc_client::reqwest::ReqwestClient;
use itertools::Itertools;
use serde::Deserialize;
use serde_json::from_value;
use serde_json::{from_value, to_value};
use std::sync::Arc;
use tauri::{AppHandle, Manager, Runtime};

Expand Down Expand Up @@ -49,6 +50,29 @@ pub async fn me<R: Runtime>(app: AppHandle<R>) -> Result<Did> {
Ok(did)
}

#[tauri::command]
pub async fn list_sessions<R: Runtime>(
app: AppHandle<R>,
) -> Result<Vec<atrium_api::agent::Session>> {
log::info!("list_sessions");
Ok(values(app.clone())?
.into_iter()
.sorted_by(|a, b| Ord::cmp(&b.updated_at, &a.updated_at))
.map(|data| data.session)
.collect())
}

#[tauri::command]
pub async fn switch_session<R: Runtime>(did: String, app: AppHandle<R>) -> Result<()> {
log::info!("switch_session: {did}");
appdata::set_appdata(app.clone(), APPDATA_CURRENT.into(), to_value(&did)?)?;
*app.state::<State<R>>().agent.lock().await = Arc::new(AtpAgent::new(
ReqwestClient::new("https://bsky.social"),
TauriPluginStore::new(app.clone()),
));
Ok(())
}

pub async fn get_preferences<R: Runtime>(
app: AppHandle<R>,
) -> Result<atrium_api::app::bsky::actor::get_preferences::Output> {
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod command;
mod consts;
mod error;
mod event;
mod public;
mod session;
mod setting;
mod state;
Expand Down Expand Up @@ -88,9 +89,12 @@ pub fn run() {
appdata::set_appdata,
setting::get_setting,
setting::set_setting,
public::get_public_profile,
command::login,
command::logout,
command::me,
command::list_sessions,
command::switch_session,
command::get_profile,
command::get_pinned_feed_generators,
command::get_feed_generators,
Expand Down
19 changes: 19 additions & 0 deletions src-tauri/src/public.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::error::Result;
use atrium_api::{client::AtpServiceClient, types::string::AtIdentifier};
use atrium_xrpc_client::reqwest::ReqwestClient;

#[tauri::command]
pub async fn get_public_profile(
actor: AtIdentifier,
) -> Result<atrium_api::app::bsky::actor::get_profile::Output> {
log::info!("get_public_profile: {actor:?}");
Ok(
AtpServiceClient::new(ReqwestClient::new("https://public.api.bsky.app"))
.service
.app
.bsky
.actor
.get_profile(atrium_api::app::bsky::actor::get_profile::Parameters { actor })
.await?,
)
}
115 changes: 74 additions & 41 deletions src-tauri/src/session.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::STORE_APPDATA_PATH;
use crate::appdata::{get_appdata, set_appdata};
use atrium_api::agent::{store::SessionStore, Session};
use atrium_api::types::string::Datetime;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::{from_value, to_value, Value};
use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_store::with_store;
use tauri_plugin_store::{with_store, Result, Store};

#[cfg(debug_assertions)]
pub const STORE_SESSION_PATH: &str = "session.dev.json";
Expand All @@ -11,6 +14,12 @@ pub const STORE_SESSION_PATH: &str = "session.json";

pub const APPDATA_CURRENT: &str = "current";

#[derive(Debug, Serialize, Deserialize)]
pub struct SessionData {
pub session: Session,
pub updated_at: Datetime,
}

pub struct TauriPluginStore<R: Runtime> {
pub app: AppHandle<R>,
}
Expand All @@ -19,76 +28,100 @@ impl<R: Runtime> TauriPluginStore<R> {
pub fn new(app: AppHandle<R>) -> Self {
Self { app }
}
fn current_did(&self) -> Option<String> {
let value = with_store(
self.app.clone(),
self.app.state(),
STORE_APPDATA_PATH,
|store| Ok(store.get(APPDATA_CURRENT).cloned()),
)
.expect("failed to get current session");
if let Some(Value::String(did)) = value {
fn current_did(&self) -> Result<Option<String>> {
let value = get_appdata(self.app.clone(), APPDATA_CURRENT.into())?;
Ok(if let Some(Value::String(did)) = value {
Some(did)
} else {
None
}
})
}
fn set_current(&self, did: String) {
with_store(
self.app.clone(),
self.app.state(),
STORE_APPDATA_PATH,
|store| {
store.insert(APPDATA_CURRENT.into(), to_value(did)?)?;
store.save()
},
)
.expect("failed to set current session");
fn set_current(&self, did: String) -> Result<()> {
set_appdata(self.app.clone(), APPDATA_CURRENT.into(), to_value(did)?)
}
}

#[async_trait::async_trait]
impl<R: Runtime> SessionStore for TauriPluginStore<R> {
async fn get_session(&self) -> Option<Session> {
if let Some(key) = self.current_did() {
let value = with_store(
if let Ok(Some(key)) = &self.current_did() {
match with_store(
self.app.clone(),
self.app.state(),
STORE_SESSION_PATH,
|store| Ok(store.get(key).cloned()),
)
.expect("failed to get session data");
value.map(|v| from_value(v).expect("failed to deserialize"))
) {
Ok(value) => {
if let Some(value) = value {
match from_value::<SessionData>(value) {
Ok(data) => return Some(data.session),
Err(err) => {
log::warn!("failed to deserialize session data: {err}");
}
}
}
}
Err(err) => {
log::warn!("failed to get session data of `{key}`: {err}");
}
}
} else {
None
log::info!("no current session");
}
None
}
async fn set_session(&self, session: Session) {
let did = session.did.to_string();
with_store(
let save_data = |store: &mut Store<R>| {
store.insert(
did.clone(),
to_value(SessionData {
updated_at: Datetime::now(),
session,
})?,
)?;
store.save()
};
match with_store(
self.app.clone(),
self.app.state(),
STORE_SESSION_PATH,
|store| {
store.insert(did.clone(), to_value(session)?)?;
store.save()
},
)
.expect("failed to set session data");
self.set_current(did);
save_data,
) {
Ok(()) => {
self.set_current(did).ok();
}
Err(err) => {
log::warn!("failed to set session data: {err}");
}
}
}
async fn clear_session(&self) {
if let Some(key) = self.current_did() {
with_store(
if let Ok(Some(key)) = self.current_did() {
if let Err(err) = with_store(
self.app.clone(),
self.app.state(),
STORE_SESSION_PATH,
|store| {
store.delete(key)?;
store.save()
},
)
.expect("failed to delete session data");
) {
log::warn!("failed to delete session data: {err}");
}
} else {
log::info!("no current session");
}
}
}

pub fn values<R: Runtime>(app: AppHandle<R>) -> Result<Vec<SessionData>> {
Ok(
with_store(app.clone(), app.state(), STORE_SESSION_PATH, |store| {
Ok(store.values().cloned().collect_vec())
})?
.into_iter()
.filter_map(|value| from_value(value).ok())
.collect(),
)
}
15 changes: 11 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import FeedGenerator from "@/routes/feed-generator";
import Home from "@/routes/home";
import Notifications from "@/routes/notifications";
import Root from "@/routes/root";
import Signin from "@/routes/signin";
import Signin, { loader as SigninLoader } from "@/routes/signin";
import SigninIndex from "@/routes/signin/index";
import SigninNew from "@/routes/signin/new";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrent } from "@tauri-apps/api/window";
Expand Down Expand Up @@ -93,25 +95,30 @@ const App = () => {
{
path: "/",
element: <Root />,
shouldRevalidate: () => false,
children: [
{
index: true,
element: <Home />,
},
{
path: "/notifications",
path: "notifications",
element: <Notifications />,
},
{
path: "/feed_generator",
path: "feed_generator",
element: <FeedGenerator />,
},
],
},
{
path: "/signin",
element: <Signin />,
id: "signin",
loader: SigninLoader,
children: [
{ index: true, element: <SigninIndex /> },
{ path: "new", element: <SigninNew /> },
],
},
];
const router = createBrowserRouter([
Expand Down
Loading

0 comments on commit 7c664ea

Please sign in to comment.