From 7402fb6e78d8f7ef15c1a1ff9238dca325897147 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Fri, 2 Oct 2026 00:07:44 -0400 Subject: [PATCH] exclude opted out bundles from package list (#879) * exclude opted out bundles from package list * count inferred bundle installs as opted in * let opted-in bundles own shared rows --- .../src/components/optional_mods_popup.rs | 1 + .../src/components/package_row.rs | 2 + .../oneclient_app/src/view/app/browser/mod.rs | 1 + .../view/app/cluster/package_manager/mod.rs | 72 ++++++++++++++++++- .../oneclient_content/src/bundles/types.rs | 1 + .../oneclient_content/src/bundles/updates.rs | 40 +++++++---- .../oneclient_core/tests/bundle_updates.rs | 29 ++++++++ 7 files changed, 131 insertions(+), 15 deletions(-) diff --git a/packages/oneclient_app/src/components/optional_mods_popup.rs b/packages/oneclient_app/src/components/optional_mods_popup.rs index c830623b..22ac00ef 100644 --- a/packages/oneclient_app/src/components/optional_mods_popup.rs +++ b/packages/oneclient_app/src/components/optional_mods_popup.rs @@ -126,6 +126,7 @@ fn entry_from_item( hash: None, manifest_default: false, hidden: false, + opted_in: true, advanced: false, update_available: false, // Offered, not installed, so there is no recency badge to show diff --git a/packages/oneclient_app/src/components/package_row.rs b/packages/oneclient_app/src/components/package_row.rs index 73d8642b..e7da4bf3 100644 --- a/packages/oneclient_app/src/components/package_row.rs +++ b/packages/oneclient_app/src/components/package_row.rs @@ -60,6 +60,8 @@ pub struct PackageEntry { pub manifest_default: bool, /// Private bundle dependency only set for bundle rows pub hidden: bool, + /// False for uninstalled rows of bundles the cluster never took keeps them out of the All tab + pub opted_in: bool, pub advanced: bool, /// Only set for browser-installed content bundle packages use the bundle update flow pub update_available: bool, diff --git a/packages/oneclient_app/src/view/app/browser/mod.rs b/packages/oneclient_app/src/view/app/browser/mod.rs index 682ec09d..fa233755 100644 --- a/packages/oneclient_app/src/view/app/browser/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/mod.rs @@ -560,6 +560,7 @@ mod tests { .collect(), archive: archive("performance", true, files), has_updates: false, + opted_in: true, }] } diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs index 53df41f8..2848101e 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs @@ -144,10 +144,13 @@ pub fn bundle_packages( } } + let mut ordered: Vec<&BundleWithUpdateStatus> = bundles.iter().collect(); + ordered.sort_by_key(|b| !b.opted_in); + let mut rows = Vec::new(); let mut seen: HashSet = HashSet::new(); - for bundle in bundles { + for bundle in ordered { let bundle_name = &bundle.archive.manifest.name; let category = bundle.archive.manifest.category.clone(); for (file, status) in &bundle.files { @@ -199,6 +202,7 @@ pub fn bundle_packages( vec![category.clone()] }; + let opted_in = installed_info.is_some() || bundle.opted_in; let mut row = make_row( pid, Some(bundle_name.clone()), @@ -214,6 +218,7 @@ pub fn bundle_packages( false, // Flagged rather than dropped `HiddenFilter` filters on the row and the seen id stops the loose-content pass resurrecting it as a local file file.hidden, + opted_in, ); row.advanced = advanced; rows.push(row); @@ -246,6 +251,7 @@ pub fn bundle_packages( .unwrap_or_else(|| info.file_name.clone()), outdated, false, + true, )); } @@ -267,6 +273,7 @@ fn make_row( fallback_name: String, update_available: bool, hidden: bool, + opted_in: bool, ) -> PackageEntry { let name = m .map(|p| p.name.clone()) @@ -305,6 +312,7 @@ fn make_row( hash: installed_info.map(|i| i.hash.clone()), update_available, hidden, + opted_in, advanced: false, seen_status: installed_info.map(|i| i.seen_status).unwrap_or_default(), } @@ -330,7 +338,7 @@ impl Tab { pub(super) fn matches(&self, p: &PackageEntry) -> bool { match self { - Tab::All => true, + Tab::All => p.opted_in, Tab::Category(c) => p.categories.iter().any(|pc| pc == c), Tab::Browser => (p.is_remote() || p.github_hosted) && !p.in_bundle(), Tab::Local => !p.is_remote() && !p.github_hosted, @@ -687,3 +695,63 @@ impl Component for PackageManager { .maybe_child(delete_dialog) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::view::onboarding::test_support::{archive, file}; + use oneclient_core::{BundleFile, FileUpdateStatus}; + + fn bundle(category: &str, opted_in: bool, files: Vec) -> BundleWithUpdateStatus { + BundleWithUpdateStatus { + files: files + .iter() + .cloned() + .map(|f| (f, FileUpdateStatus::NotInstalled)) + .collect(), + archive: archive(category, true, files), + has_updates: false, + opted_in, + } + } + + #[test] + fn all_tab_skips_bundles_not_opted_into() { + let bundles = [ + bundle( + "Declined", + false, + vec![ + file("only-declined", true, false), + file("shared", true, false), + ], + ), + bundle( + "Taken", + true, + vec![file("shared", true, false), file("only-taken", true, false)], + ), + ]; + let rows = bundle_packages( + Vec::new(), + &bundles, + &HashMap::new(), + &PackageMetaMap::new(), + &HashSet::new(), + ContentType::Mod, + ); + let all: Vec<&str> = rows + .iter() + .filter(|p| Tab::All.matches(p)) + .map(|p| p.package_id.as_str()) + .collect(); + assert_eq!(all, ["shared", "only-taken"]); + let shared = rows.iter().find(|p| p.package_id == "shared").unwrap(); + assert_eq!(shared.categories, ["Taken"]); + let declined = Tab::Category("Declined".into()); + assert!( + rows.iter() + .any(|p| p.package_id == "only-declined" && declined.matches(p)) + ); + } +} diff --git a/packages/oneclient_content/src/bundles/types.rs b/packages/oneclient_content/src/bundles/types.rs index 676385ee..28912c46 100644 --- a/packages/oneclient_content/src/bundles/types.rs +++ b/packages/oneclient_content/src/bundles/types.rs @@ -253,4 +253,5 @@ pub struct BundleWithUpdateStatus { pub archive: BundleArchive, pub files: Vec<(BundleFile, FileUpdateStatus)>, pub has_updates: bool, + pub opted_in: bool, } diff --git a/packages/oneclient_content/src/bundles/updates.rs b/packages/oneclient_content/src/bundles/updates.rs index 120710b3..ea84a8b6 100644 --- a/packages/oneclient_content/src/bundles/updates.rs +++ b/packages/oneclient_content/src/bundles/updates.rs @@ -843,6 +843,14 @@ pub async fn get_bundles_with_update_status( }) .collect(); + // Same liveness the updater uses so an untracked older install is not hidden from the list while it still takes on new files + let live: Vec<_> = all_linked.iter().filter(|item| item.enabled).collect(); + let (live_managed_keys, _) = installed_bundle_keys(ctx, live).await?; + let mut live_bundles = live_bundle_names(&bundle_packages, &overrides); + live_bundles.extend(infer_subscribed_from_archives( + &archives, + &live_managed_keys, + )); let mut results = Vec::new(); for archive in archives { @@ -874,6 +882,7 @@ pub async fn get_bundles_with_update_status( } results.push(BundleWithUpdateStatus { + opted_in: live_bundles.contains(&archive.manifest.name), archive, files, has_updates, @@ -918,27 +927,32 @@ async fn addition_eligible_bundles( let live: Vec<_> = all_linked.iter().filter(|item| item.enabled).collect(); let (live_managed_keys, _) = installed_bundle_keys(ctx, live).await?; - let mut eligible: HashSet = bundle_packages - .iter() - .filter(|bp| bp.enabled != 0) - .filter_map(|bp| bp.bundle_name.clone()) - .collect(); - + let mut eligible = live_bundle_names(bundle_packages, overrides); eligible.extend(infer_bundle_names_from_unique_installed_keys( candidate_keys_by_bundle, &live_managed_keys, )); - eligible.extend( - overrides - .iter() - .filter(|o| OverrideType::parse(&o.override_type) == Some(OverrideType::Enabled)) - .map(|o| o.bundle_name.clone()), - ); - Ok(eligible) } +fn live_bundle_names( + bundle_packages: &[BundleTrackedArtifactRow], + overrides: &[ClusterBundleOverrideRow], +) -> HashSet { + bundle_packages + .iter() + .filter(|bp| bp.enabled != 0) + .filter_map(|bp| bp.bundle_name.clone()) + .chain( + overrides + .iter() + .filter(|o| OverrideType::parse(&o.override_type) == Some(OverrideType::Enabled)) + .map(|o| o.bundle_name.clone()), + ) + .collect() +} + #[tracing::instrument(level = "debug", skip_all)] async fn installed_bundle_keys<'a>( ctx: &ContentCtx, diff --git a/packages/oneclient_core/tests/bundle_updates.rs b/packages/oneclient_core/tests/bundle_updates.rs index 7a616aa7..7fdf80f2 100644 --- a/packages/oneclient_core/tests/bundle_updates.rs +++ b/packages/oneclient_core/tests/bundle_updates.rs @@ -1,6 +1,7 @@ use oneclient_common::domain::{ContentType, GameLoader, ProviderId}; use oneclient_content::bundles::{ BundleFile, BundleFileKind, BundleFileType, BundleManifest, check_bundle_updates, + get_bundles_with_update_status, }; use oneclient_core::LauncherState; use oneclient_core::clusters::CreateClusterOptions; @@ -487,3 +488,31 @@ async fn tracked_bundle_that_never_synced_is_not_removed() { check.removals_available ); } + +#[tokio::test] +async fn untracked_older_install_counts_as_opted_in() { + let state = oneclient_core::dev::ephemeral_state().await.unwrap(); + oneclient_core::dev::seed_bundle_archive( + &state, + manifest(vec![managed_file(true), newly_shipped_file()]), + ) + .await + .unwrap(); + let cluster_id = cluster_with_tracked_mod(&state).await; + bundle_dao::clear_bundle_tracking(&state.services.db, cluster_id, HASH) + .await + .unwrap(); + + let bundles = get_bundles_with_update_status( + cluster_id, + state.bundles.as_ref(), + &state.services.content(), + ) + .await + .unwrap(); + + assert!( + bundles.iter().all(|b| b.opted_in), + "a bundle the updater infers from its installed mods must not have its files hidden from the All tab" + ); +}