Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Sync Status To Accounts Page #12

Merged
merged 1 commit into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ Potential improvements:
- [Performance] Check performance with high amount of bookmarks spread across multiple instances.
- [Application] Refactor codebase to be more organized.
- [Application] Allow user-provided TLS certificate.
- [UI] Visual indicator for last sync status.
- [UI] Indicators for `archived`, `unread`, `shared` bookmarks.
- [UI] Display account information in accounts page.
- [Distribution] Flatpack release.
- [Distribution] compiled binary in GitHub release.
- [UI] Sort bookmarks.
- [UI] Improve `Accounts` page view.

Things to consider:

Expand All @@ -79,6 +79,7 @@ Things to consider:
- [UI] Pagination (if possible).
- [Application] Consider leveraging linkding's `/check` endpoint when adding bookmarks.
- [Application] Do not block on when executing local database queries.
- [UI] Loading indicator when performing long HTTP calls.

## Thanks

Expand Down
4 changes: 4 additions & 0 deletions i18n/en/cosmicding.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ edit-account = Edit Account
edit-bookmark = Edit Bookmark
enabled-public-sharing = Public bookmarks sharing enabled
enabled-sharing = Bookmarks sharing enabled
failed = failed
failed-to-find-linkding-api-endpoint = Failed to find linkding API endpoint
failed-to-parse-response = Failed to parse response
file = File
git-description = Git commit {$hash} on {$date}
http-error = HTTP error {$http_rc}: {$http_err}
instance = Instance
invalid-api-token = Invalid API token
last-sync-status = Last sync status
last-sync-time = Last sync time
light = Light
match-desktop = Match Desktop
no-accounts = No accounts configured
Expand All @@ -54,6 +57,7 @@ setting-managed-externally = This setting can only be managed from Linkding web
settings = Settings
shared = Shared
shared-disabled = Shared (Disabled)
successful = successful
tags = Tags
tags-helper = Enter any number of tags separated by space.
theme = Theme
Expand Down
81 changes: 51 additions & 30 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::fl;
use crate::http::{self};
use crate::key_binds::key_binds;
use crate::models::account::{Account, AccountApiResponse};
use crate::models::bookmarks::Bookmark;
use crate::models::bookmarks::{Bookmark, DetailedResponse};
use crate::nav::NavPage;
use crate::pages::accounts::{add_account, edit_account, AccountsMessage, AccountsView};
use crate::pages::bookmarks::{
Expand Down Expand Up @@ -73,9 +73,9 @@ pub enum Message {
CompleteRemoveDialog(Account, Option<Bookmark>),
DialogCancel,
DialogUpdate(DialogPage),
DoneRefreshAccountProfile(Account, AccountApiResponse),
DoneRefreshBookmarksForAccount(Account, Vec<Bookmark>),
DoneRefreshBookmarksForAllAccounts(Vec<Bookmark>),
DoneRefreshAccountProfile(Account, Option<AccountApiResponse>),
DoneRefreshBookmarksForAccount(Account, Vec<DetailedResponse>),
DoneRefreshBookmarksForAllAccounts(Vec<DetailedResponse>),
EditAccount(Account),
EditBookmark(Account, Bookmark),
EmpptyMessage,
Expand Down Expand Up @@ -293,7 +293,6 @@ impl Application for Cosmicding {
struct ConfigSubscription;
struct ThemeSubscription;

// FIXME: (vkhitrin) key bindings are not working properly
let subscriptions = vec![
event::listen_with(|event, status, _| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => match status {
Expand Down Expand Up @@ -518,7 +517,7 @@ impl Application for Cosmicding {
Message::BookmarksView(message) => commands.push(self.bookmarks_view.update(message)),
Message::StartRefreshBookmarksForAllAccounts => {
if !self.bookmarks_view.bookmarks.is_empty() {
let message = |x: Vec<Bookmark>| {
let message = |x: Vec<DetailedResponse>| {
cosmic::app::Message::App(Message::DoneRefreshBookmarksForAllAccounts(x))
};
if !self.accounts_view.accounts.is_empty() {
Expand All @@ -536,24 +535,34 @@ impl Application for Cosmicding {
}
}
}
Message::DoneRefreshBookmarksForAllAccounts(remote_bookmarks) => {
block_on(async {
db::SqliteDatabase::cache_all_bookmarks(&mut self.db, &remote_bookmarks).await
});
Message::DoneRefreshBookmarksForAllAccounts(remote_responses) => {
for response in remote_responses {
block_on(async {
db::SqliteDatabase::cache_bookmarks_for_acount(
&mut self.db,
&response.account,
response.bookmarks.unwrap_or_else(|| Vec::new()),
response.timestamp,
response.successful,
)
.await
});
}
commands.push(self.update(Message::LoadAccounts));
commands.push(self.update(Message::LoadBookmarks));
}
Message::StartRefreshBookmarksForAccount(account) => {
let mut acc_vec = self.accounts_view.accounts.clone();
acc_vec.retain(|acc| acc.id == account.id);
let borrowed_acc = acc_vec[0].clone();
let message = move |x: Vec<Bookmark>| {
let message = move |bookmarks: Vec<DetailedResponse>| {
cosmic::app::Message::App(Message::DoneRefreshBookmarksForAccount(
borrowed_acc.clone(),
x,
bookmarks,
))
};
commands.push(self.update(Message::StartRefreshAccountProfile(account.clone())));
if !self.accounts_view.accounts.is_empty() {
if !acc_vec.is_empty() {
commands.push(Task::perform(
http::fetch_bookmarks_from_all_accounts(acc_vec.clone()),
message,
Expand All @@ -568,34 +577,46 @@ impl Application for Cosmicding {
);
}
}
Message::DoneRefreshBookmarksForAccount(account, remote_bookmarks) => {
block_on(async {
db::SqliteDatabase::cache_bookmarks_for_acount(
&mut self.db,
&account,
remote_bookmarks,
)
.await
});
Message::DoneRefreshBookmarksForAccount(account, remote_responses) => {
for response in remote_responses {
block_on(async {
db::SqliteDatabase::cache_bookmarks_for_acount(
&mut self.db,
&account,
response.bookmarks.unwrap_or_else(|| Vec::new()),
response.timestamp,
response.successful,
)
.await
});
}
commands.push(self.update(Message::LoadAccounts));
commands.push(self.update(Message::LoadBookmarks));
}
Message::StartRefreshAccountProfile(account) => {
let borrowed_acc = account.clone();
let message = move |r: Option<AccountApiResponse>| {
let message = move |api_response: Option<AccountApiResponse>| {
cosmic::app::Message::App(Message::DoneRefreshAccountProfile(
borrowed_acc.clone(),
r.unwrap(),
api_response,
))
};
commands.push(Task::perform(http::fetch_account_details(account), message));
}
Message::DoneRefreshAccountProfile(mut account, account_details) => {
account.enable_sharing = account_details.enable_sharing;
account.enable_public_sharing = account_details.enable_public_sharing;
block_on(async {
db::SqliteDatabase::update_account(&mut self.db, &account).await
});
commands.push(self.update(Message::LoadAccounts));
if !account_details.is_none() {
let details = account_details.unwrap();
account.enable_sharing = details.enable_sharing;
account.enable_public_sharing = details.enable_public_sharing;
block_on(async {
db::SqliteDatabase::update_account(&mut self.db, &account).await
});
commands.push(self.update(Message::LoadAccounts));
} else {
block_on(async {
db::SqliteDatabase::update_account(&mut self.db, &account).await
});
}
}
Message::AddBookmarkForm => {
if !self.accounts_view.accounts.is_empty() {
Expand Down
49 changes: 35 additions & 14 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,27 +111,41 @@ impl SqliteDatabase {
.await
.unwrap();
}
//NOTE: (vkhitrin) at the moment, this function is no longer required.
// Perhaps it should be removed/refactored.
//TODO: (vkhitrin) this is a dumb "cache" that wipes all previous entries.
// It should be improved in the future.
pub async fn cache_all_bookmarks(&mut self, bookmarks: &Vec<Bookmark>) {
let truncate_query: &str = "DELETE FROM Bookmarks;";
sqlx::query(truncate_query)
.execute(&mut self.conn)
.await
.unwrap();
if !bookmarks.is_empty() {
for bookmark in bookmarks {
self.add_bookmark(&bookmark).await;
}
}
}
//pub async fn cache_all_bookmarks(&mut self, bookmarks: &Vec<Bookmark>, epoch_timestamp: i64) {
// let truncate_query: &str = "DELETE FROM Bookmarks;";
// let update_timestamp_query =
// "UPDATE UserAccounts SET last_sync_status=$1, last_sync_timestamp=$2";
// sqlx::query(truncate_query)
// .execute(&mut self.conn)
// .await
// .unwrap();
// if !bookmarks.is_empty() {
// for bookmark in bookmarks {
// self.add_bookmark(&bookmark).await;
// }
// }
// sqlx::query(update_timestamp_query)
// .bind(1)
// .bind(epoch_timestamp)
// .execute(&mut self.conn)
// .await
// .unwrap();
//}
pub async fn cache_bookmarks_for_acount(
&mut self,
account: &Account,
bookmarks: Vec<Bookmark>,
epoch_timestamp: i64,
response_successful: bool,
) {
let truncate_query: &str = "DELETE FROM Bookmarks where user_account_id = $1;";
sqlx::query(truncate_query)
let delete_query: &str = "DELETE FROM Bookmarks where user_account_id = $1;";
let update_timestamp_query =
"UPDATE UserAccounts SET last_sync_status=$2, last_sync_timestamp=$3 WHERE id=$1";
sqlx::query(delete_query)
.bind(account.id)
.execute(&mut self.conn)
.await
Expand All @@ -141,6 +155,13 @@ impl SqliteDatabase {
self.add_bookmark(&bookmark).await;
}
}
sqlx::query(update_timestamp_query)
.bind(account.id)
.bind(response_successful)
.bind(epoch_timestamp)
.execute(&mut self.conn)
.await
.unwrap();
}
pub async fn add_bookmark(&mut self, bookmark: &Bookmark) {
let query: &str = r#"
Expand Down
51 changes: 32 additions & 19 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,42 @@
use crate::fl;
use crate::models::account::{Account, AccountApiResponse};
use crate::models::bookmarks::{Bookmark, BookmarksApiResponse};
use crate::models::bookmarks::{Bookmark, BookmarksApiResponse, DetailedResponse};
use anyhow::Result;
use chrono::{DateTime, Utc};
use reqwest::{
header::{HeaderMap, HeaderValue, AUTHORIZATION},
ClientBuilder, StatusCode,
};
use serde_json::Value;
use std::fmt::Write;
use std::time::{SystemTime, UNIX_EPOCH};

pub async fn fetch_bookmarks_from_all_accounts(accounts: Vec<Account>) -> Vec<Bookmark> {
let mut all_bookmarks: Vec<Bookmark> = Vec::new();
pub async fn fetch_bookmarks_from_all_accounts(accounts: Vec<Account>) -> Vec<DetailedResponse> {
let mut all_responses: Vec<DetailedResponse> = Vec::new();
for account in accounts {
match fetch_bookmarks_for_account(&account).await {
Ok(new_bookmarks) => {
all_bookmarks.extend(new_bookmarks);
Ok(new_response) => {
all_responses.push(new_response);
}
Err(e) => {
let epoch_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("")
.as_secs();
let error_response =
DetailedResponse::new(account, epoch_timestamp as i64, false, None);
all_responses.push(error_response);
log::error!("Error fetching bookmarks: {}", e);
}
}
}
return all_bookmarks;
return all_responses;
}

pub async fn fetch_bookmarks_for_account(
account: &Account,
) -> Result<Vec<Bookmark>, Box<dyn std::error::Error>> {
) -> Result<DetailedResponse, Box<dyn std::error::Error>> {
let mut detailed_response = DetailedResponse::new(account.clone(), 0, false, None);
let mut bookmarks: Vec<Bookmark> = Vec::new();
let mut headers = HeaderMap::new();
let rest_api_url: String = account.instance.clone() + "/api/bookmarks/";
Expand All @@ -43,7 +53,12 @@ pub async fn fetch_bookmarks_for_account(
.headers(headers)
.send()
.await?;
let parsed_date = response.headers().get("Date").unwrap().to_str().unwrap();
if response.status().is_success() {
detailed_response.successful = true;
let date: DateTime<Utc> = DateTime::parse_from_rfc2822(parsed_date)?.with_timezone(&Utc);
let unix_timestamp = SystemTime::from(date).duration_since(UNIX_EPOCH)?.as_secs();
detailed_response.timestamp = unix_timestamp as i64;
let response_json = response.json::<BookmarksApiResponse>().await;
// Handle the Result
match response_json {
Expand Down Expand Up @@ -72,6 +87,7 @@ pub async fn fetch_bookmarks_for_account(
);
bookmarks.push(transformed_bookmark);
}
detailed_response.bookmarks = Some(bookmarks);
}
Err(e) => {
log::error!("Error parsing JSON: {:?}", e);
Expand All @@ -84,7 +100,7 @@ pub async fn fetch_bookmarks_for_account(
response.text().await
);
}
Ok(bookmarks)
Ok(detailed_response)
}

pub async fn add_bookmark(
Expand Down Expand Up @@ -237,17 +253,14 @@ pub async fn edit_bookmark(
fl!("failed-to-parse-response"),
))),
},
status => {
// Return an error with the status code
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
fl!(
"http-error",
http_rc = status.to_string(),
http_err = response.text().await.unwrap()
),
)))
}
status => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
fl!(
"http-error",
http_rc = status.to_string(),
http_err = response.text().await.unwrap()
),
))),
}
}

Expand Down
Loading
Loading