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
This commit is contained in:
Julian Chang 2026-10-02 00:07:44 -04:00 • committed by GitHub
parent e74eb60ba2
commit 7402fb6e78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 131 additions and 15 deletions

View file

@ -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

View file

@ -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,

View file

@ -560,6 +560,7 @@ mod tests {
.collect(),
archive: archive("performance", true, files),
has_updates: false,
opted_in: true,
}]
}

View file

@ -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<String> = 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<BundleFile>) -> 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))
);
}
}

View file

@ -253,4 +253,5 @@ pub struct BundleWithUpdateStatus {
pub archive: BundleArchive,
pub files: Vec<(BundleFile, FileUpdateStatus)>,
pub has_updates: bool,
pub opted_in: bool,
}

View file

@ -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<String> = 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<String> {
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,

View file

@ -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"
);
}