@peers-app/peers-ui 0.18.5 → 0.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,25 +11,27 @@ const input_1 = require("../../components/input");
11
11
  const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
12
12
  const tooltip_1 = require("../../components/tooltip");
13
13
  const hooks_1 = require("../../hooks");
14
- const deviceVersionTagVar = (0, peers_sdk_1.groupDeviceVar)("deviceVersionTag");
15
14
  const PackageInfo = (props) => {
16
15
  const { pkg } = props;
17
- const [activeVersionId] = (0, hooks_1.useObservable)(pkg.qs.activePackageVersionId);
18
- const [versionFollowRange] = (0, hooks_1.useObservable)(pkg.qs.versionFollowRange);
19
16
  const [followVersionTags] = (0, hooks_1.useObservable)(pkg.qs.followVersionTags);
20
- const [deviceTag] = (0, hooks_1.useObservable)(deviceVersionTagVar);
21
17
  const localPathVar = (0, peers_sdk_1.groupDeviceVar)(`packageLocalPath_${pkg.packageId}`, {
22
18
  defaultValue: `${peers_sdk_1.packagesRootDir}/${pkg.name}`,
23
19
  });
24
20
  const [localPath, setLocalPath] = (0, hooks_1.useObservable)(localPathVar);
25
- const [deviceTagDraft, setDeviceTagDraft] = react_1.default.useState(deviceTag || "");
21
+ const [devicePrefs] = (0, hooks_1.useObservable)((0, peers_sdk_1.packagePrefsVar)(pkg.packageId));
22
+ const effective = (0, peers_sdk_1.getEffectivePackagePrefs)(pkg.toJS(), devicePrefs);
23
+ const activeVersionId = effective.activePackageVersionId;
24
+ const deviceFollowTags = devicePrefs?.followTags;
25
+ const isPinned = effective.isPinned;
26
+ const followRange = effective.followRange;
27
+ const [deviceTagDraft, setDeviceTagDraft] = react_1.default.useState(deviceFollowTags || "");
26
28
  const savingRef = react_1.default.useRef(false);
27
29
  react_1.default.useEffect(() => {
28
30
  if (!savingRef.current) {
29
- setDeviceTagDraft(deviceTag || "");
31
+ setDeviceTagDraft(deviceFollowTags || "");
30
32
  }
31
- }, [deviceTag]);
32
- const deviceTagDirty = deviceTagDraft.trim() !== (deviceTag || "");
33
+ }, [deviceFollowTags]);
34
+ const deviceTagDirty = deviceTagDraft.trim() !== (deviceFollowTags || "");
33
35
  const prevDirtyRef = react_1.default.useRef(false);
34
36
  react_1.default.useEffect(() => {
35
37
  if (deviceTagDirty && !prevDirtyRef.current) {
@@ -43,15 +45,12 @@ const PackageInfo = (props) => {
43
45
  if (props.saveDeviceTagRef) {
44
46
  props.saveDeviceTagRef.current = async () => {
45
47
  const val = deviceTagDraft.trim();
46
- if (val !== (deviceTag || "")) {
48
+ if (val !== (deviceFollowTags || "")) {
47
49
  savingRef.current = true;
48
50
  try {
49
- if (val) {
50
- deviceVersionTagVar(val);
51
- }
52
- else {
53
- await deviceVersionTagVar.delete();
54
- }
51
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, {
52
+ followTags: val || undefined,
53
+ });
55
54
  }
56
55
  finally {
57
56
  savingRef.current = false;
@@ -71,13 +70,14 @@ const PackageInfo = (props) => {
71
70
  const active = all.find((v) => v.packageVersionId === activeVersionId);
72
71
  if (!active?.version)
73
72
  return "uptodate";
73
+ const effectiveFollowTags = deviceFollowTags || followVersionTags;
74
74
  const parse = (v) => v.split(".").map(Number);
75
75
  const [aMaj, aMin, aPat] = parse(active.version);
76
76
  let highest = null;
77
77
  for (const v of all) {
78
78
  if (!v.version || v.packageVersionId === activeVersionId)
79
79
  continue;
80
- if (!(0, peers_sdk_1.doesTagMatch)(active.versionTag, v.versionTag, followVersionTags, deviceTag))
80
+ if (!(0, peers_sdk_1.doesTagMatch)(active.versionTag, v.versionTag, effectiveFollowTags, undefined))
81
81
  continue;
82
82
  const [maj, min, pat] = parse(v.version);
83
83
  if (maj > aMaj)
@@ -99,16 +99,18 @@ const PackageInfo = (props) => {
99
99
  }
100
100
  }
101
101
  return highest || "uptodate";
102
- }, undefined, [activeVersionId, pkg.packageId, followVersionTags, deviceTag]);
103
- const isPinned = versionFollowRange === "pinned";
102
+ }, undefined, [activeVersionId, pkg.packageId, followVersionTags, deviceFollowTags]);
104
103
  const remoteRepoUrl = pkg.remoteRepo;
105
104
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("small", { children: "Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.name, className: "form-control mb-3 p-0 ps-2", placeholder: "Package name", title: "Package name", disabled: true }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Local Path:", (0, jsx_runtime_1.jsxs)("small", { children: [(0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `The local path to the directory containing the package. This will open in VS Code if it's installed otherwise this will open in your native file system` }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-link", onClick: () => {
106
105
  peers_sdk_1.rpcServerCalls.openPackage(localPath || peers_sdk_1.packagesRootDir);
107
106
  }, children: "Open Local" })] })] }), (0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control mb-3 p-0 ps-2", placeholder: "~/peers/packages/my-package", value: localPath || "", onChange: (e) => setLocalPath(e.target.value) })] }), remoteRepoUrl ? ((0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Source URL:", (0, jsx_runtime_1.jsxs)("small", { children: [(0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `The remote source of this package. Often a github repository URL or a link to the Peers page for the package.` }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-link", onClick: () => {
108
107
  peers_sdk_1.rpcServerCalls.openLinkInBrowser(remoteRepoUrl);
109
- }, children: "Open Remote" })] })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.remoteRepo, className: "form-control mb-3 p-0 ps-2", disabled: true })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Version:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "The currently active version of this package. Manage versions in the Versions tab." })] }), activeVersion ? ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mt-1 mb-3", children: [(0, jsx_runtime_1.jsxs)("strong", { className: "me-2", children: ["v", activeVersion.version] }), activeVersion.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${activeVersion.versionTag === "dev" ? "text-bg-danger" : activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"} me-2`, children: activeVersion.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: activeVersion.packageVersionHash?.substring(0, 8) }), newerLevel === "uptodate" ? ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-success", children: "Up to date" })) : newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, children: ["Newer ", newerLevel, " version available"] })) : null] })) : ((0, jsx_runtime_1.jsx)("div", { className: "text-muted small mt-1 mb-3", children: "No active version" })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Auto-Update Range:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Controls which new versions are auto-activated. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." })] }), (0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm", value: versionFollowRange || "latest", onChange: (e) => {
108
+ }, children: "Open Remote" })] })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.remoteRepo, className: "form-control mb-3 p-0 ps-2", disabled: true })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Version:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "The currently active version of this package. Manage versions in the Versions tab." })] }), activeVersion ? ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mt-1 mb-3", children: [(0, jsx_runtime_1.jsxs)("strong", { className: "me-2", children: ["v", activeVersion.version] }), activeVersion.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${activeVersion.versionTag === "dev" ? "text-bg-danger" : activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"} me-2`, children: activeVersion.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: activeVersion.packageVersionHash?.substring(0, 8) }), newerLevel === "uptodate" ? ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-success", children: "Up to date" })) : newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, children: ["Newer ", newerLevel, " version available"] })) : null] })) : ((0, jsx_runtime_1.jsx)("div", { className: "text-muted small mt-1 mb-3", children: "No active version" })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Auto-Update Range (this device):", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Controls which new versions are auto-activated on this device. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." })] }), (0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm", value: isPinned ? "pinned" : followRange, onChange: async (e) => {
110
109
  const val = e.target.value;
111
- pkg.versionFollowRange = val === "latest" ? undefined : val;
112
- }, children: [(0, jsx_runtime_1.jsx)("option", { value: "latest", children: "Latest (auto-update to newest)" }), (0, jsx_runtime_1.jsx)("option", { value: "minor", children: "Minor (same major version)" }), (0, jsx_runtime_1.jsx)("option", { value: "patch", children: "Patch (same major.minor version)" }), (0, jsx_runtime_1.jsx)("option", { value: "pinned", children: "Pinned (no auto-updates)" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `mb-3 ${isPinned ? "opacity-50" : ""}`, children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Follow Version Tags:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Which version tags to auto-activate. Leave empty for **current** (follows the active version's tag). Use `*` for any tag, or a comma-separated list like `stable,prod`." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.followVersionTags, className: "form-control form-control-sm p-0 ps-2", placeholder: `Following "${activeVersion?.versionTag || "stable"}"`, disabled: isPinned })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Device-Specific Version Tag:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Override the tag policy for this device only. For example, set to `beta` to test pre-release versions on this device without affecting other devices or group members. This is not synced." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control form-control-sm p-0 ps-2", placeholder: "e.g. beta", value: deviceTagDraft, onChange: (e) => setDeviceTagDraft(e.target.value) }), deviceTagDraft && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-secondary", title: "Clear device tag override", onClick: () => setDeviceTagDraft(""), children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-x-lg" }) }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Description:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `This should be edited in the package's README.md. It will automatically update when you restart Peers or reload the package.` })] }), (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: pkg.description || "" })] })] }));
110
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, {
111
+ pinned: val === "pinned",
112
+ followRange: val === "pinned" ? undefined : val,
113
+ });
114
+ }, children: [(0, jsx_runtime_1.jsx)("option", { value: "latest", children: "Latest (auto-update to newest)" }), (0, jsx_runtime_1.jsx)("option", { value: "minor", children: "Minor (same major version)" }), (0, jsx_runtime_1.jsx)("option", { value: "patch", children: "Patch (same major.minor version)" }), (0, jsx_runtime_1.jsx)("option", { value: "pinned", children: "Pinned (no auto-updates)" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `mb-3 ${isPinned ? "opacity-50" : ""}`, children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Follow Version Tags (this device):", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Which version tags to auto-activate on this device. Leave empty to follow the group's tag policy. Use `*` for any tag, or a comma-separated list like `stable,beta`." })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control form-control-sm p-0 ps-2", placeholder: `Following "${activeVersion?.versionTag || "stable"}"`, value: deviceTagDraft, onChange: (e) => setDeviceTagDraft(e.target.value), disabled: isPinned }), deviceTagDraft && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-secondary", title: "Clear device tag override", onClick: () => setDeviceTagDraft(""), children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-x-lg" }) }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Description:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `This should be edited in the package's README.md. It will automatically update when you restart Peers or reload the package.` })] }), (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: pkg.description || "" })] })] }));
113
115
  };
114
116
  exports.PackageInfo = PackageInfo;
@@ -40,13 +40,14 @@ function tagBadgeClass(tag) {
40
40
  }
41
41
  const PackageVersionsList = (props) => {
42
42
  const { pkg } = props;
43
- const [activeVersionId] = (0, hooks_1.useObservable)(pkg.qs.activePackageVersionId);
44
- const [versionFollowRange] = (0, hooks_1.useObservable)(pkg.qs.versionFollowRange);
45
43
  const refreshKey = (0, hooks_1.useObservableState)(0);
46
44
  const [activating, setActivating] = (0, react_1.useState)(null);
47
45
  const [deleting, setDeleting] = (0, react_1.useState)(null);
48
46
  const [pinning, setPinning] = (0, react_1.useState)(false);
49
- const isPinned = versionFollowRange === "pinned";
47
+ const [devicePrefs] = (0, hooks_1.useObservable)((0, peers_sdk_1.packagePrefsVar)(pkg.packageId));
48
+ const effective = (0, peers_sdk_1.getEffectivePackagePrefs)(pkg.toJS(), devicePrefs);
49
+ const effectiveActiveVersionId = effective.activePackageVersionId;
50
+ const isPinned = effective.isPinned;
50
51
  const versions = (0, hooks_1.usePromise)(async () => {
51
52
  const all = await (0, peers_sdk_1.PackageVersions)().list({ packageId: pkg.packageId });
52
53
  const parse = (v) => v.split(".").map(Number);
@@ -76,13 +77,20 @@ const PackageVersionsList = (props) => {
76
77
  async function activateVersion(pv) {
77
78
  setActivating(pv.packageVersionId);
78
79
  try {
79
- const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
80
- if (current) {
81
- current.activePackageVersionId = pv.packageVersionId;
82
- await (0, peers_sdk_1.Packages)().signAndSave(current);
83
- await pkg.load();
84
- refreshKey(refreshKey() + 1);
80
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, {
81
+ activePackageVersionId: pv.packageVersionId,
82
+ });
83
+ // For non-dev versions, also update the group default so other devices
84
+ // can discover this as the recommended version.
85
+ if (pv.versionTag !== "dev") {
86
+ const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
87
+ if (current) {
88
+ current.activePackageVersionId = pv.packageVersionId;
89
+ await (0, peers_sdk_1.Packages)().signAndSave(current);
90
+ }
85
91
  }
92
+ await pkg.load();
93
+ refreshKey(refreshKey() + 1);
86
94
  }
87
95
  catch (err) {
88
96
  alert(`Failed to activate version: ${err instanceof Error ? err.message : String(err)}`);
@@ -139,12 +147,8 @@ const PackageVersionsList = (props) => {
139
147
  async function pinVersion() {
140
148
  setPinning(true);
141
149
  try {
142
- const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
143
- if (current) {
144
- current.versionFollowRange = "pinned";
145
- await (0, peers_sdk_1.Packages)().signAndSave(current);
146
- await pkg.load();
147
- }
150
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, { pinned: true });
151
+ await pkg.load();
148
152
  }
149
153
  catch (err) {
150
154
  alert(`Failed to pin version: ${err instanceof Error ? err.message : String(err)}`);
@@ -160,7 +164,7 @@ const PackageVersionsList = (props) => {
160
164
  if (sorted.length === 0) {
161
165
  return ((0, jsx_runtime_1.jsxs)("div", { className: "text-muted text-center p-4", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-tag me-1" }), "No versions found for this package."] }));
162
166
  }
163
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [sorted.length, " version", sorted.length !== 1 ? "s" : ""] }), (0, jsx_runtime_1.jsx)("div", { className: "list-group mt-2", children: sorted.map((pv) => ((0, jsx_runtime_1.jsx)(VersionRow, { pv: pv, isActive: pv.packageVersionId === activeVersionId, isPinned: isPinned, pinning: pinning, activating: activating, deleting: deleting, userMap: userMap, onActivate: activateVersion, onDelete: deleteVersion, onPin: pinVersion, onPromote: promoteVersion, onUpdateVersion: updateVersion }, pv.packageVersionId))) })] }));
167
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [sorted.length, " version", sorted.length !== 1 ? "s" : ""] }), (0, jsx_runtime_1.jsx)("div", { className: "list-group mt-2", children: sorted.map((pv) => ((0, jsx_runtime_1.jsx)(VersionRow, { pv: pv, isActive: pv.packageVersionId === effectiveActiveVersionId, isPinned: isPinned, pinning: pinning, activating: activating, deleting: deleting, userMap: userMap, onActivate: activateVersion, onDelete: deleteVersion, onPin: pinVersion, onPromote: promoteVersion, onUpdateVersion: updateVersion }, pv.packageVersionId))) })] }));
164
168
  };
165
169
  exports.PackageVersionsList = PackageVersionsList;
166
170
  /**
@@ -5,32 +5,89 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
5
  require("../ui-defaults");
6
6
  exports.allPackages = (0, peers_sdk_1.observable)([]);
7
7
  let allRoutesLoaded = false;
8
+ const prefsSubscribed = new Set();
9
+ function subscribeToPrefs(packageId) {
10
+ if (prefsSubscribed.has(packageId))
11
+ return;
12
+ prefsSubscribed.add(packageId);
13
+ const pvar = (0, peers_sdk_1.packagePrefsVar)(packageId);
14
+ let lastPvId = pvar()?.activePackageVersionId;
15
+ pvar.subscribe(async (prefs) => {
16
+ const newPvId = prefs?.activePackageVersionId;
17
+ if (newPvId && newPvId !== lastPvId) {
18
+ lastPvId = newPvId;
19
+ const pkg = await (0, peers_sdk_1.Packages)().get(packageId);
20
+ if (pkg) {
21
+ const enrichedPkg = await enrichWithPvAppNavs(pkg);
22
+ const _allPackages = (0, exports.allPackages)();
23
+ const idx = _allPackages.findIndex((p) => p.packageId === packageId);
24
+ if (idx >= 0) {
25
+ _allPackages[idx] = enrichedPkg;
26
+ (0, exports.allPackages)([..._allPackages]);
27
+ }
28
+ }
29
+ reloadPackage(packageId);
30
+ }
31
+ });
32
+ }
33
+ /**
34
+ * Enrich an IPackage with appNavs from the device's active PV record.
35
+ * Falls back to `pkg.appNavs` for backward compat with older data.
36
+ */
37
+ async function enrichWithPvAppNavs(pkg) {
38
+ let pvId = pkg.activePackageVersionId;
39
+ try {
40
+ const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
41
+ await pvar.loadingPromise;
42
+ pvId = pvar()?.activePackageVersionId ?? pvId;
43
+ }
44
+ catch {
45
+ /* use group default */
46
+ }
47
+ if (!pvId)
48
+ return pkg;
49
+ try {
50
+ const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
51
+ if (pv?.appNavs) {
52
+ return { ...pkg, appNavs: pv.appNavs };
53
+ }
54
+ }
55
+ catch {
56
+ /* fall through to pkg.appNavs */
57
+ }
58
+ return pkg;
59
+ }
8
60
  const loadAllRoutes = async () => {
9
61
  if (allRoutesLoaded)
10
62
  return;
11
63
  allRoutesLoaded = true;
12
- // Filter packages that have an active version
13
64
  const packagesWithUI = await (0, peers_sdk_1.Packages)().list({
14
65
  disabled: { $ne: true },
15
66
  activePackageVersionId: { $exists: true },
16
67
  });
17
- (0, exports.allPackages)(packagesWithUI);
18
- await Promise.all(packagesWithUI.map((pkg) => loadRoutesBundle(pkg)));
68
+ const enriched = await Promise.all(packagesWithUI.map(enrichWithPvAppNavs));
69
+ (0, exports.allPackages)(enriched);
70
+ await Promise.all(enriched.map((pkg) => loadRoutesBundle(pkg)));
71
+ for (const pkg of enriched) {
72
+ subscribeToPrefs(pkg.packageId);
73
+ }
19
74
  (0, peers_sdk_1.Packages)().dataChanged.subscribe(async (evt) => {
75
+ const enrichedPkg = await enrichWithPvAppNavs(evt.dataObject);
20
76
  const _allPackages = (0, exports.allPackages)();
21
77
  if (evt.op === "insert") {
22
- _allPackages.push(evt.dataObject);
78
+ _allPackages.push(enrichedPkg);
79
+ subscribeToPrefs(evt.dataObject.packageId);
23
80
  }
24
81
  else if (evt.op === "delete") {
25
- const idx = (0, exports.allPackages)().findIndex((p) => p.packageId === evt.dataObject.packageId);
82
+ const idx = _allPackages.findIndex((p) => p.packageId === evt.dataObject.packageId);
26
83
  if (idx >= 0) {
27
84
  _allPackages.splice(idx, 1);
28
85
  }
29
86
  }
30
87
  else {
31
- const idx = (0, exports.allPackages)().findIndex((p) => p.packageId === evt.dataObject.packageId);
88
+ const idx = _allPackages.findIndex((p) => p.packageId === evt.dataObject.packageId);
32
89
  if (idx >= 0) {
33
- _allPackages[idx] = evt.dataObject;
90
+ _allPackages[idx] = enrichedPkg;
34
91
  }
35
92
  }
36
93
  (0, exports.allPackages)([..._allPackages]);
@@ -59,10 +116,19 @@ function loadRoutesBundle(pkg, forceRefresh) {
59
116
  console.log(`waiting for registerPeersUIRoute to be defined on the window object`);
60
117
  await new Promise((resolve) => setTimeout(resolve, 20));
61
118
  }
119
+ let pvId = pkg.activePackageVersionId;
120
+ try {
121
+ const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
122
+ await pvar.loadingPromise;
123
+ pvId = pvar()?.activePackageVersionId ?? pvId;
124
+ }
125
+ catch {
126
+ /* use group default */
127
+ }
62
128
  let routesBundleFileId;
63
- if (pkg.activePackageVersionId) {
129
+ if (pvId) {
64
130
  try {
65
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pkg.activePackageVersionId);
131
+ const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
66
132
  routesBundleFileId = pv?.routesBundleFileId;
67
133
  }
68
134
  catch {
@@ -190,10 +190,19 @@ const uiLoadingPromises = {};
190
190
  // Check if we're running in a React Native WebView (has injectUIBundle available)
191
191
  const isReactNativeWebView = typeof window.ReactNativeWebView !== "undefined";
192
192
  async function resolveUiBundleFileId(pkg) {
193
- if (!pkg.activePackageVersionId)
193
+ let pvId = pkg.activePackageVersionId;
194
+ try {
195
+ const pvar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
196
+ await pvar.loadingPromise;
197
+ pvId = pvar()?.activePackageVersionId ?? pvId;
198
+ }
199
+ catch {
200
+ /* use group default */
201
+ }
202
+ if (!pvId)
194
203
  return undefined;
195
204
  try {
196
- const pv = await (0, peers_sdk_1.PackageVersions)().get(pkg.activePackageVersionId);
205
+ const pv = await (0, peers_sdk_1.PackageVersions)().get(pvId);
197
206
  return pv?.uiBundleFileId;
198
207
  }
199
208
  catch {
@@ -415,12 +424,31 @@ const reloadPackage = (0, peers_sdk_1.debounceByArgs)(async (packageId) => {
415
424
  (0, globals_1.packageReloaded)(Date.now());
416
425
  }
417
426
  }, 200);
427
+ const uiPrefsSubscribed = new Set();
428
+ function subscribeToPrefs(packageId) {
429
+ if (uiPrefsSubscribed.has(packageId))
430
+ return;
431
+ uiPrefsSubscribed.add(packageId);
432
+ const pvar = (0, peers_sdk_1.packagePrefsVar)(packageId);
433
+ let lastPvId = pvar()?.activePackageVersionId;
434
+ pvar.subscribe((prefs) => {
435
+ const newPvId = prefs?.activePackageVersionId;
436
+ if (newPvId && newPvId !== lastPvId) {
437
+ lastPvId = newPvId;
438
+ reloadPackage(packageId);
439
+ }
440
+ });
441
+ }
418
442
  (0, peers_sdk_1.getUserContext)().then((_userContext) => {
443
+ for (const packageId of Object.keys(uiLoadingPromises)) {
444
+ subscribeToPrefs(packageId);
445
+ }
419
446
  (0, peers_sdk_1.Packages)().dataChanged.subscribe(async (evt) => {
420
447
  const pkg = evt.dataObject;
421
448
  const loadingPromise = uiLoadingPromises[pkg.packageId];
422
449
  if (!loadingPromise)
423
450
  return;
451
+ subscribeToPrefs(pkg.packageId);
424
452
  reloadPackage(pkg.packageId);
425
453
  });
426
454
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.18.5",
3
+ "version": "0.18.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.18.5",
31
+ "@peers-app/peers-sdk": "^0.18.6",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-env": "^7.24.5",
40
40
  "@babel/preset-react": "^7.24.1",
41
41
  "@babel/preset-typescript": "^7.27.1",
42
- "@peers-app/peers-sdk": "0.18.5",
42
+ "@peers-app/peers-sdk": "0.18.6",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  doesTagMatch,
3
+ getEffectivePackagePrefs,
3
4
  groupDeviceVar,
4
5
  type IDoc,
5
6
  type IPackage,
6
7
  type IPackageVersion,
7
8
  PackageVersions,
9
+ packagePrefsVar,
8
10
  packagesRootDir,
9
11
  rpcServerCalls,
12
+ updatePackagePrefs,
10
13
  } from "@peers-app/peers-sdk";
11
14
  import React from "react";
12
15
  import { Input } from "../../components/input";
@@ -14,33 +17,35 @@ import { MarkdownWithMentions } from "../../components/markdown-with-mentions";
14
17
  import { Tooltip } from "../../components/tooltip";
15
18
  import { useObservable, usePromise } from "../../hooks";
16
19
 
17
- const deviceVersionTagVar = groupDeviceVar<string | undefined>("deviceVersionTag");
18
-
19
20
  export const PackageInfo = (props: {
20
21
  pkg: IDoc<IPackage>;
21
22
  saveDeviceTagRef?: React.MutableRefObject<(() => Promise<void>) | null>;
22
23
  }) => {
23
24
  const { pkg } = props;
24
- const [activeVersionId] = useObservable(pkg.qs.activePackageVersionId);
25
- const [versionFollowRange] = useObservable(pkg.qs.versionFollowRange);
26
25
  const [followVersionTags] = useObservable(pkg.qs.followVersionTags);
27
- const [deviceTag] = useObservable(deviceVersionTagVar);
28
26
 
29
27
  const localPathVar = groupDeviceVar<string>(`packageLocalPath_${pkg.packageId}`, {
30
28
  defaultValue: `${packagesRootDir}/${pkg.name}`,
31
29
  });
32
30
  const [localPath, setLocalPath] = useObservable(localPathVar);
33
31
 
34
- const [deviceTagDraft, setDeviceTagDraft] = React.useState(deviceTag || "");
32
+ const [devicePrefs] = useObservable(packagePrefsVar(pkg.packageId));
33
+ const effective = getEffectivePackagePrefs(pkg.toJS(), devicePrefs);
34
+ const activeVersionId = effective.activePackageVersionId;
35
+ const deviceFollowTags = devicePrefs?.followTags;
36
+ const isPinned = effective.isPinned;
37
+ const followRange = effective.followRange;
38
+
39
+ const [deviceTagDraft, setDeviceTagDraft] = React.useState(deviceFollowTags || "");
35
40
  const savingRef = React.useRef(false);
36
41
 
37
42
  React.useEffect(() => {
38
43
  if (!savingRef.current) {
39
- setDeviceTagDraft(deviceTag || "");
44
+ setDeviceTagDraft(deviceFollowTags || "");
40
45
  }
41
- }, [deviceTag]);
46
+ }, [deviceFollowTags]);
42
47
 
43
- const deviceTagDirty = deviceTagDraft.trim() !== (deviceTag || "");
48
+ const deviceTagDirty = deviceTagDraft.trim() !== (deviceFollowTags || "");
44
49
  const prevDirtyRef = React.useRef(false);
45
50
  React.useEffect(() => {
46
51
  if (deviceTagDirty && !prevDirtyRef.current) {
@@ -54,14 +59,12 @@ export const PackageInfo = (props: {
54
59
  if (props.saveDeviceTagRef) {
55
60
  props.saveDeviceTagRef.current = async () => {
56
61
  const val = deviceTagDraft.trim();
57
- if (val !== (deviceTag || "")) {
62
+ if (val !== (deviceFollowTags || "")) {
58
63
  savingRef.current = true;
59
64
  try {
60
- if (val) {
61
- deviceVersionTagVar(val);
62
- } else {
63
- await deviceVersionTagVar.delete();
64
- }
65
+ await updatePackagePrefs(pkg.packageId, {
66
+ followTags: val || undefined,
67
+ });
65
68
  } finally {
66
69
  savingRef.current = false;
67
70
  }
@@ -84,12 +87,14 @@ export const PackageInfo = (props: {
84
87
  const all = await PackageVersions().list({ packageId: pkg.packageId });
85
88
  const active = all.find((v) => v.packageVersionId === activeVersionId);
86
89
  if (!active?.version) return "uptodate";
90
+ const effectiveFollowTags = deviceFollowTags || followVersionTags;
87
91
  const parse = (v: string) => v.split(".").map(Number);
88
92
  const [aMaj, aMin, aPat] = parse(active.version);
89
93
  let highest: "major" | "minor" | "patch" | null = null;
90
94
  for (const v of all) {
91
95
  if (!v.version || v.packageVersionId === activeVersionId) continue;
92
- if (!doesTagMatch(active.versionTag, v.versionTag, followVersionTags, deviceTag)) continue;
96
+ if (!doesTagMatch(active.versionTag, v.versionTag, effectiveFollowTags, undefined))
97
+ continue;
93
98
  const [maj, min, pat] = parse(v.version);
94
99
  if (maj > aMaj) return "major";
95
100
  if (maj === aMaj && min > aMin) {
@@ -113,10 +118,9 @@ export const PackageInfo = (props: {
113
118
  return highest || "uptodate";
114
119
  },
115
120
  undefined,
116
- [activeVersionId, pkg.packageId, followVersionTags, deviceTag],
121
+ [activeVersionId, pkg.packageId, followVersionTags, deviceFollowTags],
117
122
  );
118
123
 
119
- const isPinned = versionFollowRange === "pinned";
120
124
  const remoteRepoUrl = pkg.remoteRepo;
121
125
 
122
126
  return (
@@ -213,15 +217,18 @@ export const PackageInfo = (props: {
213
217
 
214
218
  <div className="mb-3">
215
219
  <small>
216
- Auto-Update Range:
217
- <Tooltip markdownContent="Controls which new versions are auto-activated. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." />
220
+ Auto-Update Range (this device):
221
+ <Tooltip markdownContent="Controls which new versions are auto-activated on this device. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." />
218
222
  </small>
219
223
  <select
220
224
  className="form-select form-select-sm"
221
- value={versionFollowRange || "latest"}
222
- onChange={(e) => {
225
+ value={isPinned ? "pinned" : followRange}
226
+ onChange={async (e) => {
223
227
  const val = e.target.value as "pinned" | "patch" | "minor" | "latest";
224
- pkg.versionFollowRange = val === "latest" ? undefined : val;
228
+ await updatePackagePrefs(pkg.packageId, {
229
+ pinned: val === "pinned",
230
+ followRange: val === "pinned" ? undefined : val,
231
+ });
225
232
  }}
226
233
  >
227
234
  <option value="latest">Latest (auto-update to newest)</option>
@@ -233,29 +240,17 @@ export const PackageInfo = (props: {
233
240
 
234
241
  <div className={`mb-3 ${isPinned ? "opacity-50" : ""}`}>
235
242
  <small>
236
- Follow Version Tags:
237
- <Tooltip markdownContent="Which version tags to auto-activate. Leave empty for **current** (follows the active version's tag). Use `*` for any tag, or a comma-separated list like `stable,prod`." />
238
- </small>
239
- <Input
240
- value={pkg.qs.followVersionTags}
241
- className="form-control form-control-sm p-0 ps-2"
242
- placeholder={`Following "${activeVersion?.versionTag || "stable"}"`}
243
- disabled={isPinned}
244
- />
245
- </div>
246
-
247
- <div className="mb-2">
248
- <small>
249
- Device-Specific Version Tag:
250
- <Tooltip markdownContent="Override the tag policy for this device only. For example, set to `beta` to test pre-release versions on this device without affecting other devices or group members. This is not synced." />
243
+ Follow Version Tags (this device):
244
+ <Tooltip markdownContent="Which version tags to auto-activate on this device. Leave empty to follow the group's tag policy. Use `*` for any tag, or a comma-separated list like `stable,beta`." />
251
245
  </small>
252
246
  <div className="d-flex align-items-center gap-2">
253
247
  <input
254
248
  type="text"
255
249
  className="form-control form-control-sm p-0 ps-2"
256
- placeholder="e.g. beta"
250
+ placeholder={`Following "${activeVersion?.versionTag || "stable"}"`}
257
251
  value={deviceTagDraft}
258
252
  onChange={(e) => setDeviceTagDraft(e.target.value)}
253
+ disabled={isPinned}
259
254
  />
260
255
  {deviceTagDraft && (
261
256
  <button
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  computePackageVersionHash,
3
+ getEffectivePackagePrefs,
3
4
  getMe,
4
5
  type IDoc,
5
6
  type IPackage,
6
7
  type IPackageVersion,
7
8
  Packages,
8
9
  PackageVersions,
10
+ packagePrefsVar,
9
11
  Users,
12
+ updatePackagePrefs,
10
13
  } from "@peers-app/peers-sdk";
11
14
  import { useState } from "react";
12
15
  import { useObservable, useObservableState, usePromise } from "../../hooks";
@@ -39,13 +42,15 @@ function tagBadgeClass(tag?: string): string {
39
42
 
40
43
  export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
41
44
  const { pkg } = props;
42
- const [activeVersionId] = useObservable(pkg.qs.activePackageVersionId);
43
- const [versionFollowRange] = useObservable(pkg.qs.versionFollowRange);
44
45
  const refreshKey = useObservableState(0);
45
46
  const [activating, setActivating] = useState<string | null>(null);
46
47
  const [deleting, setDeleting] = useState<string | null>(null);
47
48
  const [pinning, setPinning] = useState(false);
48
- const isPinned = versionFollowRange === "pinned";
49
+
50
+ const [devicePrefs] = useObservable(packagePrefsVar(pkg.packageId));
51
+ const effective = getEffectivePackagePrefs(pkg.toJS(), devicePrefs);
52
+ const effectiveActiveVersionId = effective.activePackageVersionId;
53
+ const isPinned = effective.isPinned;
49
54
 
50
55
  const versions = usePromise(
51
56
  async () => {
@@ -78,13 +83,21 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
78
83
  async function activateVersion(pv: IPackageVersion) {
79
84
  setActivating(pv.packageVersionId);
80
85
  try {
81
- const current = await Packages().get(pkg.packageId);
82
- if (current) {
83
- current.activePackageVersionId = pv.packageVersionId;
84
- await Packages().signAndSave(current);
85
- await pkg.load();
86
- refreshKey(refreshKey() + 1);
86
+ await updatePackagePrefs(pkg.packageId, {
87
+ activePackageVersionId: pv.packageVersionId,
88
+ });
89
+
90
+ // For non-dev versions, also update the group default so other devices
91
+ // can discover this as the recommended version.
92
+ if (pv.versionTag !== "dev") {
93
+ const current = await Packages().get(pkg.packageId);
94
+ if (current) {
95
+ current.activePackageVersionId = pv.packageVersionId;
96
+ await Packages().signAndSave(current);
97
+ }
87
98
  }
99
+ await pkg.load();
100
+ refreshKey(refreshKey() + 1);
88
101
  } catch (err: unknown) {
89
102
  alert(`Failed to activate version: ${err instanceof Error ? err.message : String(err)}`);
90
103
  } finally {
@@ -151,12 +164,8 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
151
164
  async function pinVersion() {
152
165
  setPinning(true);
153
166
  try {
154
- const current = await Packages().get(pkg.packageId);
155
- if (current) {
156
- current.versionFollowRange = "pinned";
157
- await Packages().signAndSave(current);
158
- await pkg.load();
159
- }
167
+ await updatePackagePrefs(pkg.packageId, { pinned: true });
168
+ await pkg.load();
160
169
  } catch (err: unknown) {
161
170
  alert(`Failed to pin version: ${err instanceof Error ? err.message : String(err)}`);
162
171
  } finally {
@@ -193,7 +202,7 @@ export const PackageVersionsList = (props: { pkg: IDoc<IPackage> }) => {
193
202
  <VersionRow
194
203
  key={pv.packageVersionId}
195
204
  pv={pv}
196
- isActive={pv.packageVersionId === activeVersionId}
205
+ isActive={pv.packageVersionId === effectiveActiveVersionId}
197
206
  isPinned={isPinned}
198
207
  pinning={pinning}
199
208
  activating={activating}
@@ -6,6 +6,7 @@ import {
6
6
  observable,
7
7
  Packages,
8
8
  PackageVersions,
9
+ packagePrefsVar,
9
10
  rpcServerCalls,
10
11
  } from "@peers-app/peers-sdk";
11
12
  import "../ui-defaults";
@@ -13,30 +14,87 @@ import "../ui-defaults";
13
14
  export const allPackages = observable<IPackage[]>([]);
14
15
  let allRoutesLoaded = false;
15
16
 
17
+ const prefsSubscribed = new Set<string>();
18
+
19
+ function subscribeToPrefs(packageId: string) {
20
+ if (prefsSubscribed.has(packageId)) return;
21
+ prefsSubscribed.add(packageId);
22
+ const pvar = packagePrefsVar(packageId);
23
+ let lastPvId = pvar()?.activePackageVersionId;
24
+ pvar.subscribe(async (prefs) => {
25
+ const newPvId = prefs?.activePackageVersionId;
26
+ if (newPvId && newPvId !== lastPvId) {
27
+ lastPvId = newPvId;
28
+ const pkg = await Packages().get(packageId);
29
+ if (pkg) {
30
+ const enrichedPkg = await enrichWithPvAppNavs(pkg);
31
+ const _allPackages = allPackages();
32
+ const idx = _allPackages.findIndex((p) => p.packageId === packageId);
33
+ if (idx >= 0) {
34
+ _allPackages[idx] = enrichedPkg;
35
+ allPackages([..._allPackages]);
36
+ }
37
+ }
38
+ reloadPackage(packageId);
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Enrich an IPackage with appNavs from the device's active PV record.
45
+ * Falls back to `pkg.appNavs` for backward compat with older data.
46
+ */
47
+ async function enrichWithPvAppNavs(pkg: IPackage): Promise<IPackage> {
48
+ let pvId = pkg.activePackageVersionId;
49
+ try {
50
+ const pvar = packagePrefsVar(pkg.packageId);
51
+ await pvar.loadingPromise;
52
+ pvId = pvar()?.activePackageVersionId ?? pvId;
53
+ } catch {
54
+ /* use group default */
55
+ }
56
+ if (!pvId) return pkg;
57
+ try {
58
+ const pv = await PackageVersions().get(pvId);
59
+ if (pv?.appNavs) {
60
+ return { ...pkg, appNavs: pv.appNavs };
61
+ }
62
+ } catch {
63
+ /* fall through to pkg.appNavs */
64
+ }
65
+ return pkg;
66
+ }
67
+
16
68
  export const loadAllRoutes = async () => {
17
69
  if (allRoutesLoaded) return;
18
70
  allRoutesLoaded = true;
19
- // Filter packages that have an active version
20
71
  const packagesWithUI = await Packages().list({
21
72
  disabled: { $ne: true },
22
73
  activePackageVersionId: { $exists: true },
23
74
  });
24
- allPackages(packagesWithUI);
25
- await Promise.all(packagesWithUI.map((pkg) => loadRoutesBundle(pkg)));
75
+ const enriched = await Promise.all(packagesWithUI.map(enrichWithPvAppNavs));
76
+ allPackages(enriched);
77
+ await Promise.all(enriched.map((pkg) => loadRoutesBundle(pkg)));
78
+
79
+ for (const pkg of enriched) {
80
+ subscribeToPrefs(pkg.packageId);
81
+ }
26
82
 
27
83
  Packages().dataChanged.subscribe(async (evt) => {
84
+ const enrichedPkg = await enrichWithPvAppNavs(evt.dataObject);
28
85
  const _allPackages = allPackages();
29
86
  if (evt.op === "insert") {
30
- _allPackages.push(evt.dataObject);
87
+ _allPackages.push(enrichedPkg);
88
+ subscribeToPrefs(evt.dataObject.packageId);
31
89
  } else if (evt.op === "delete") {
32
- const idx = allPackages().findIndex((p) => p.packageId === evt.dataObject.packageId);
90
+ const idx = _allPackages.findIndex((p) => p.packageId === evt.dataObject.packageId);
33
91
  if (idx >= 0) {
34
92
  _allPackages.splice(idx, 1);
35
93
  }
36
94
  } else {
37
- const idx = allPackages().findIndex((p) => p.packageId === evt.dataObject.packageId);
95
+ const idx = _allPackages.findIndex((p) => p.packageId === evt.dataObject.packageId);
38
96
  if (idx >= 0) {
39
- _allPackages[idx] = evt.dataObject;
97
+ _allPackages[idx] = enrichedPkg;
40
98
  }
41
99
  }
42
100
  allPackages([..._allPackages]);
@@ -72,10 +130,19 @@ function loadRoutesBundle(pkg: IPackage, forceRefresh?: boolean): Promise<unknow
72
130
  await new Promise((resolve) => setTimeout(resolve, 20));
73
131
  }
74
132
 
133
+ let pvId = pkg.activePackageVersionId;
134
+ try {
135
+ const pvar = packagePrefsVar(pkg.packageId);
136
+ await pvar.loadingPromise;
137
+ pvId = pvar()?.activePackageVersionId ?? pvId;
138
+ } catch {
139
+ /* use group default */
140
+ }
141
+
75
142
  let routesBundleFileId: string | undefined;
76
- if (pkg.activePackageVersionId) {
143
+ if (pvId) {
77
144
  try {
78
- const pv = await PackageVersions().get(pkg.activePackageVersionId);
145
+ const pv = await PackageVersions().get(pvId);
79
146
  routesBundleFileId = pv?.routesBundleFileId;
80
147
  } catch {
81
148
  /* no version record */
@@ -8,6 +8,7 @@ import {
8
8
  newid,
9
9
  Packages,
10
10
  PackageVersions,
11
+ packagePrefsVar,
11
12
  rpcServerCalls,
12
13
  toJSON,
13
14
  type UIContext,
@@ -283,9 +284,17 @@ const isReactNativeWebView =
283
284
  typeof (window as Window & { ReactNativeWebView?: unknown }).ReactNativeWebView !== "undefined";
284
285
 
285
286
  async function resolveUiBundleFileId(pkg: IPackage): Promise<string | undefined> {
286
- if (!pkg.activePackageVersionId) return undefined;
287
+ let pvId = pkg.activePackageVersionId;
287
288
  try {
288
- const pv = await PackageVersions().get(pkg.activePackageVersionId);
289
+ const pvar = packagePrefsVar(pkg.packageId);
290
+ await pvar.loadingPromise;
291
+ pvId = pvar()?.activePackageVersionId ?? pvId;
292
+ } catch {
293
+ /* use group default */
294
+ }
295
+ if (!pvId) return undefined;
296
+ try {
297
+ const pv = await PackageVersions().get(pvId);
289
298
  return pv?.uiBundleFileId;
290
299
  } catch {
291
300
  return undefined;
@@ -628,11 +637,32 @@ const reloadPackage = debounceByArgs(async (packageId: string) => {
628
637
  }
629
638
  }, 200);
630
639
 
640
+ const uiPrefsSubscribed = new Set<string>();
641
+
642
+ function subscribeToPrefs(packageId: string) {
643
+ if (uiPrefsSubscribed.has(packageId)) return;
644
+ uiPrefsSubscribed.add(packageId);
645
+ const pvar = packagePrefsVar(packageId);
646
+ let lastPvId = pvar()?.activePackageVersionId;
647
+ pvar.subscribe((prefs) => {
648
+ const newPvId = prefs?.activePackageVersionId;
649
+ if (newPvId && newPvId !== lastPvId) {
650
+ lastPvId = newPvId;
651
+ reloadPackage(packageId);
652
+ }
653
+ });
654
+ }
655
+
631
656
  getUserContext().then((_userContext) => {
657
+ for (const packageId of Object.keys(uiLoadingPromises)) {
658
+ subscribeToPrefs(packageId);
659
+ }
660
+
632
661
  Packages().dataChanged.subscribe(async (evt) => {
633
662
  const pkg = evt.dataObject;
634
663
  const loadingPromise = uiLoadingPromises[pkg.packageId];
635
664
  if (!loadingPromise) return;
665
+ subscribeToPrefs(pkg.packageId);
636
666
  reloadPackage(pkg.packageId);
637
667
  });
638
668
  });