From eaf8c0b82204203b515ac79182e17b6c11efa56f Mon Sep 17 00:00:00 2001 From: Sumit Mohanty Date: Thu, 17 Jul 2025 08:38:50 +0530 Subject: [PATCH 1/3] feat: preserve user settings when visiting random instance - Add /settings.json API endpoint to export current preferences - Enhance random instance redirect to include settings transfer - Implement comprehensive error handling with graceful fallbacks - Maintain full backward compatibility with existing functionality Fixes user experience issue where custom settings were lost when switching instances during errors. Users now seamlessly retain their personalized experience across instances. --- src/main.rs | 1 + src/settings.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ static/check_update.js | 30 ++++++++++++++++++-- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 00b86ae7..e9ea5f65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -299,6 +299,7 @@ async fn main() { app.at("/settings/restore").get(|r| settings::restore(r).boxed()); app.at("/settings/encoded-restore").post(|r| settings::encoded_restore(r).boxed()); app.at("/settings/update").get(|r| settings::update(r).boxed()); + app.at("/settings.json").get(|r| settings::get_json(r).boxed()); // RSS Subscriptions app.at("/r/:sub.rss").get(|r| subreddit::rss(r).boxed()); diff --git a/src/settings.rs b/src/settings.rs index bb9bb99e..7fd98bf9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,6 +10,7 @@ use cookie::Cookie; use futures_lite::StreamExt; use hyper::{Body, Request, Response}; use rinja::Template; +use serde_json; use time::{Duration, OffsetDateTime}; use tokio::time::timeout; use url::form_urlencoded; @@ -296,3 +297,66 @@ pub async fn encoded_restore(req: Request) -> Result, Strin Ok(redirect(&url)) } + +// Get current user settings as JSON for API consumption +pub async fn get_json(req: Request) -> Result, String> { + // Create preferences - this should be safe but we wrap it for completeness + let prefs = Preferences::new(&req); + + // Try to encode preferences, but provide fallback if it fails + let url_encoded = match prefs.to_urlencoded() { + Ok(encoded) => Some(encoded), + Err(e) => { + eprintln!("Warning: Failed to encode preferences for settings transfer: {e}"); + None + } + }; + + // Create a response structure with encoded preferences for easy transfer + let settings_data = serde_json::json!({ + "url_encoded": url_encoded, + "success": url_encoded.is_some(), + "preferences": { + "theme": prefs.theme, + "front_page": prefs.front_page, + "layout": prefs.layout, + "wide": prefs.wide, + "comment_sort": prefs.comment_sort, + "post_sort": prefs.post_sort, + "blur_spoiler": prefs.blur_spoiler, + "show_nsfw": prefs.show_nsfw, + "blur_nsfw": prefs.blur_nsfw, + "use_hls": prefs.use_hls, + "hide_hls_notification": prefs.hide_hls_notification, + "autoplay_videos": prefs.autoplay_videos, + "hide_sidebar_and_summary": prefs.hide_sidebar_and_summary, + "fixed_navbar": prefs.fixed_navbar, + "hide_awards": prefs.hide_awards, + "hide_score": prefs.hide_score, + "disable_visit_reddit_confirmation": prefs.disable_visit_reddit_confirmation, + "video_quality": prefs.video_quality, + "remove_default_feeds": prefs.remove_default_feeds, + "subscriptions": prefs.subscriptions, + "filters": prefs.filters + } + }); + + let body = match serde_json::to_string(&settings_data) { + Ok(json) => json, + Err(e) => { + eprintln!("Error serializing settings to JSON: {e}"); + return Response::builder() + .status(500) + .header("content-type", "application/json") + .body(r#"{"error": "Failed to serialize settings", "success": false}"#.into()) + .map_err(|e| format!("Failed to build error response: {e}")); + } + }; + + Response::builder() + .status(200) + .header("content-type", "application/json") + .header("cache-control", "no-cache, no-store, must-revalidate") + .body(body.into()) + .map_err(|e| format!("Failed to build response: {e}")) +} diff --git a/static/check_update.js b/static/check_update.js index 3dcb406e..9cc2d8d6 100644 --- a/static/check_update.js +++ b/static/check_update.js @@ -39,15 +39,39 @@ async function checkInstanceUpdateStatus() { async function checkOtherInstances() { try { + // Fetch list of available instances const response = await fetch('/instances.json'); const data = await response.json(); const instances = window.location.host.endsWith('.onion') ? data.instances.filter(i => i.onion) : data.instances.filter(i => i.url); if (instances.length == 0) return; const randomInstance = instances[Math.floor(Math.random() * instances.length)]; const instanceUrl = randomInstance.url ?? randomInstance.onion; - // Set the href of the tag to the instance URL with path included - document.getElementById('random-instance').href = instanceUrl + window.location.pathname; - document.getElementById('random-instance').innerText = "Visit Random Instance"; + + // Fetch current user settings to transfer them to the new instance + let targetUrl = instanceUrl + window.location.pathname; + let text = "Visit Random Instance"; + + try { + const settingsResponse = await fetch('/settings.json'); + if (settingsResponse.ok) { + const urlEncoded = await settingsResponse.text(); + if (urlEncoded && urlEncoded.trim()) { + targetUrl = instanceUrl + '/settings/restore/?' + urlEncoded + '&redirect=' + encodeURIComponent(window.location.pathname.substring(1)); + text += " (bringing preferences)"; + } else { + console.warn('Settings encoding returned empty - visiting random instance without settings transfer'); + } + } else { + console.warn('Could not fetch user settings (HTTP', settingsResponse.status + ') - visiting random instance without settings transfer'); + } + } catch (settingsError) { + console.warn('Error fetching user settings:', settingsError); + console.warn('Visiting random instance without settings transfer'); + } + + // Set the href of the tag to the instance URL with path and settings included + document.getElementById('random-instance').href = targetUrl; + document.getElementById('random-instance').innerText = text; } catch (error) { console.error('Error fetching instances:', error); document.getElementById('update-status').innerText = '⚠️ Error checking other instances: ' + error; From 765721def35beb934528cc5f2c69a82e2730d15f Mon Sep 17 00:00:00 2001 From: Sumit Mohanty Date: Mon, 21 Jul 2025 09:19:26 +0530 Subject: [PATCH 2/3] refactor: address PR review feedback - Simplify settings.json endpoint to return URL-encoded string directly - Use HTTP status codes for error handling (200/500) - Update button text to show '(bringing preferences)' when transferring - Revert unnecessary variable renaming (response, data) - Remove confusing comments per maintainer feedback --- src/settings.rs | 68 +++++++++++-------------------------------------- 1 file changed, 15 insertions(+), 53 deletions(-) diff --git a/src/settings.rs b/src/settings.rs index 7fd98bf9..9bb73653 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -300,63 +300,25 @@ pub async fn encoded_restore(req: Request) -> Result, Strin // Get current user settings as JSON for API consumption pub async fn get_json(req: Request) -> Result, String> { - // Create preferences - this should be safe but we wrap it for completeness let prefs = Preferences::new(&req); - // Try to encode preferences, but provide fallback if it fails - let url_encoded = match prefs.to_urlencoded() { - Ok(encoded) => Some(encoded), - Err(e) => { - eprintln!("Warning: Failed to encode preferences for settings transfer: {e}"); - None - } - }; - - // Create a response structure with encoded preferences for easy transfer - let settings_data = serde_json::json!({ - "url_encoded": url_encoded, - "success": url_encoded.is_some(), - "preferences": { - "theme": prefs.theme, - "front_page": prefs.front_page, - "layout": prefs.layout, - "wide": prefs.wide, - "comment_sort": prefs.comment_sort, - "post_sort": prefs.post_sort, - "blur_spoiler": prefs.blur_spoiler, - "show_nsfw": prefs.show_nsfw, - "blur_nsfw": prefs.blur_nsfw, - "use_hls": prefs.use_hls, - "hide_hls_notification": prefs.hide_hls_notification, - "autoplay_videos": prefs.autoplay_videos, - "hide_sidebar_and_summary": prefs.hide_sidebar_and_summary, - "fixed_navbar": prefs.fixed_navbar, - "hide_awards": prefs.hide_awards, - "hide_score": prefs.hide_score, - "disable_visit_reddit_confirmation": prefs.disable_visit_reddit_confirmation, - "video_quality": prefs.video_quality, - "remove_default_feeds": prefs.remove_default_feeds, - "subscriptions": prefs.subscriptions, - "filters": prefs.filters + // Try to encode preferences and return directly or error with HTTP status + match prefs.to_urlencoded() { + Ok(encoded) => { + Response::builder() + .status(200) + .header("content-type", "text/plain") + .header("cache-control", "no-cache, no-store, must-revalidate") + .body(encoded.into()) + .map_err(|e| format!("Failed to build response: {e}")) } - }); - - let body = match serde_json::to_string(&settings_data) { - Ok(json) => json, Err(e) => { - eprintln!("Error serializing settings to JSON: {e}"); - return Response::builder() + eprintln!("Warning: Failed to encode preferences for settings transfer: {e}"); + Response::builder() .status(500) - .header("content-type", "application/json") - .body(r#"{"error": "Failed to serialize settings", "success": false}"#.into()) - .map_err(|e| format!("Failed to build error response: {e}")); + .header("content-type", "text/plain") + .body("Failed to encode preferences".into()) + .map_err(|e| format!("Failed to build error response: {e}")) } - }; - - Response::builder() - .status(200) - .header("content-type", "application/json") - .header("cache-control", "no-cache, no-store, must-revalidate") - .body(body.into()) - .map_err(|e| format!("Failed to build response: {e}")) + } } From a49a94b7c43a605130c261264cf8405517724c66 Mon Sep 17 00:00:00 2001 From: Sumit Mohanty Date: Mon, 21 Jul 2025 09:23:14 +0530 Subject: [PATCH 3/3] fix: remove unused serde_json import Cleanup unused import after simplifying settings endpoint to return plain text --- src/settings.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/settings.rs b/src/settings.rs index 9bb73653..137798f9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -10,7 +10,6 @@ use cookie::Cookie; use futures_lite::StreamExt; use hyper::{Body, Request, Response}; use rinja::Template; -use serde_json; use time::{Duration, OffsetDateTime}; use tokio::time::timeout; use url::form_urlencoded;