diff --git a/package.json b/package.json
index 375e726c..160ae86b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "impro",
- "version": "0.19.7",
+ "version": "0.19.8",
"type": "module",
"scripts": {
"start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve",
diff --git a/src/css/style.css b/src/css/style.css
index 91d0ff94..d56d80b0 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -469,6 +469,112 @@ trending-pane {
display: block;
}
+pinned-feeds-pane {
+ display: none;
+}
+
+@media (min-width: 1100px) {
+ pinned-feeds-pane {
+ display: block;
+ }
+}
+
+.pinned-feeds-pane {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2px;
+ margin: 16px;
+}
+
+.pinned-feeds-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ max-width: 100%;
+ box-sizing: border-box;
+ padding: 6px 8px;
+ border: none;
+ border-radius: 8px;
+ background: none;
+ color: var(--text-color-muted);
+ font-family: inherit;
+ font-size: 15px;
+ text-align: left;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.pinned-feeds-item .home-icon {
+ width: 15px;
+}
+
+.pinned-feeds-item .plus-icon {
+ width: 15px;
+}
+
+@media (hover: hover) {
+ .pinned-feeds-item:hover {
+ color: var(--text-color);
+ }
+}
+
+.pinned-feeds-item.active {
+ background: var(--highlight-color-tint);
+ color: var(--text-color);
+ font-weight: 600;
+}
+
+.pinned-feeds-item-label {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.pinned-feeds-item-avatar {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ border-radius: var(--feed-avatar-border-radius);
+ object-fit: cover;
+}
+
+.pinned-feeds-item-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ border-radius: var(--feed-avatar-border-radius);
+}
+
+.pinned-feeds-item-icon svg {
+ width: 14px;
+ height: 14px;
+ display: block;
+}
+
+.pinned-feeds-timeline-icon {
+ background: var(--highlight-color);
+ color: var(--white);
+}
+
+.pinned-feeds-more-icon {
+ background: var(--post-hover-color);
+}
+
+.pinned-feeds-item-skeleton {
+ align-self: stretch;
+ max-width: none;
+}
+
+.pinned-feeds-skeleton-bar {
+ display: inline-block;
+ height: 14px;
+ border-radius: 4px;
+}
+
.trending-list {
display: flex;
flex-direction: column;
diff --git a/src/js/components/pinned-feeds-pane.js b/src/js/components/pinned-feeds-pane.js
new file mode 100644
index 00000000..dcf363a9
--- /dev/null
+++ b/src/js/components/pinned-feeds-pane.js
@@ -0,0 +1,165 @@
+import { html, render } from "/js/lib/lit-html.js";
+import { Component } from "/js/components/component.js";
+import { Signal, effect } from "/js/signals.js";
+import { classnames } from "/js/utils.js";
+import { cdnImageUrl } from "/js/dataHelpers.js";
+import { homeIconTemplate } from "/js/templates/icons/homeIcon.template.js";
+import { plusIconTemplate } from "/js/templates/icons/plusIcon.template.js";
+
+const SKELETON_COUNT = 5;
+
+function pinnedItemAvatarTemplate({ item }) {
+ if (item.type === "timeline") {
+ return html`${homeIconTemplate({ filled: true })}`;
+ }
+ const fallback =
+ item.type === "list"
+ ? "/img/list-avatar-fallback.svg"
+ : "/img/feed-avatar-fallback.svg";
+ return html`
`;
+}
+
+function pinnedItemTemplate({ item, isCurrent, onSelect }) {
+ return html``;
+}
+
+function pinnedFeedsSkeletonTemplate() {
+ return html`${Array.from(
+ { length: SKELETON_COUNT },
+ (_, index) =>
+ html`
+
+
`,
+ )}`;
+}
+
+function pinnedFeedsPaneTemplate({
+ pinnedItems,
+ isLoading,
+ currentFeedUri,
+ moreFeedsActive,
+ onSelect,
+}) {
+ return html``;
+}
+
+class PinnedFeedsPane extends Component {
+ static get observedAttributes() {
+ return ["show-selected", "more-feeds-active"];
+ }
+
+ attributeChangedCallback() {
+ if (this.initialized) {
+ this.render();
+ }
+ }
+
+ connectedCallback() {
+ if (this.initialized) return;
+ this.initialized = true;
+ if (!this.dataLayer) {
+ throw new Error("pinned-feeds-pane requires a dataLayer property");
+ }
+ this.$failed = new Signal.State(false);
+ this._disposers = [effect(() => this.render())];
+ this.load();
+ }
+
+ disconnectedCallback() {
+ if (!this.initialized) return;
+ this._disposers?.forEach((dispose) => dispose());
+ this._disposers = null;
+ this.initialized = false;
+ }
+
+ render() {
+ const pinnedItems = this.dataLayer.derived.$hydratedPinnedItems.get();
+ const failed = this.$failed.get();
+ const selectedFeedUri = this.dataLayer.derived.$selectedFeedUri.get();
+ const currentFeedUri = this.hasAttribute("show-selected")
+ ? selectedFeedUri
+ : null;
+ if (failed) {
+ render(html``, this);
+ return;
+ }
+ render(
+ pinnedFeedsPaneTemplate({
+ pinnedItems: pinnedItems ?? [],
+ isLoading: pinnedItems === null,
+ currentFeedUri,
+ moreFeedsActive: this.hasAttribute("more-feeds-active"),
+ onSelect: (feedUri) => this.handleSelect(feedUri),
+ }),
+ this,
+ );
+ }
+
+ handleSelect(feedUri) {
+ // On the home page, a "home-feed-select" event lets the view run its
+ // animated tab switch. From elsewhere, set the selection first so the home
+ // view's first paint already shows the right feed, then navigate; the view
+ // picks up the changed selection on page-show.
+ if (window.location.pathname === "/") {
+ window.dispatchEvent(
+ new CustomEvent("home-feed-select", { detail: feedUri }),
+ );
+ return;
+ }
+ this.dataLayer.mutations.setSelectedFeedUri(feedUri);
+ window.router.go("/");
+ }
+
+ async load() {
+ try {
+ await this.dataLayer.declarative.ensurePinnedItems();
+ } catch (error) {
+ console.warn("Could not load pinned feeds", error);
+ this.$failed.set(true);
+ }
+ }
+}
+
+PinnedFeedsPane.register();
diff --git a/src/js/dataLayer/dataLayer.js b/src/js/dataLayer/dataLayer.js
index e9ea2fc9..72d2b458 100644
--- a/src/js/dataLayer/dataLayer.js
+++ b/src/js/dataLayer/dataLayer.js
@@ -1,4 +1,5 @@
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
import { PatchStore } from "/js/dataLayer/patchStore.js";
import { Mutations } from "/js/dataLayer/mutations.js";
import { Requests } from "/js/dataLayer/requests.js";
@@ -20,7 +21,8 @@ export class DataLayer extends EventEmitter {
this.identityResolver = identityResolver;
this.draftMediaStore = draftMediaStore;
this.isAuthenticated = api.isAuthenticated;
- this.dataStore = new DataStore();
+ this.sessionState = createSessionState(api.session ?? null);
+ this.dataStore = new DataStore(this.sessionState);
this.patchStore = new PatchStore(this.dataStore);
this.preferencesProvider = preferencesProvider;
this.hiddenFeedItemsStore = hiddenFeedItemsStore;
diff --git a/src/js/dataLayer/dataStore.js b/src/js/dataLayer/dataStore.js
index 92c2faf8..6eef7f54 100644
--- a/src/js/dataLayer/dataStore.js
+++ b/src/js/dataLayer/dataStore.js
@@ -3,8 +3,9 @@ import { getQuotedPost, embedViewRecordToPostView } from "/js/dataHelpers.js";
// The store saves canonical data from the server. Patches are layered on top of this.
export class DataStore extends ReactiveStore {
- constructor() {
+ constructor(sessionState) {
super("dataStore");
+ this.sessionState = sessionState;
// Single-value signals
this.$currentUser = new Signal.State(null);
this.$profileSearchResults = new Signal.State(null);
@@ -16,6 +17,7 @@ export class DataStore extends ReactiveStore {
this.$mentionNotifications = new Signal.State(null);
this.$notificationsLastSeenAt = new Signal.State(null);
this.$pinnedItems = new Signal.State(null);
+ this.$selectedFeedUri = this.sessionState.$selectedFeedUri;
this.$bookmarks = new Signal.State(null);
this.$drafts = new Signal.State(null);
this.$convoList = new Signal.State(null);
@@ -137,4 +139,20 @@ export class DataStore extends ReactiveStore {
}
}
}
+
+ // All pinned item writes go through here so the selected feed can't dangle:
+ // a selection that's no longer pinned falls back to the first pinned item.
+ setPinnedItems(pinnedItems) {
+ this.$pinnedItems.set(pinnedItems);
+ const selectedFeedUri = this.$selectedFeedUri.get();
+ if (!selectedFeedUri) {
+ return;
+ }
+ const isPinned = pinnedItems.some(
+ (item) => item.data.uri === selectedFeedUri,
+ );
+ if (!isPinned) {
+ this.$selectedFeedUri.set(pinnedItems[0]?.data.uri ?? null);
+ }
+ }
}
diff --git a/src/js/dataLayer/derived.js b/src/js/dataLayer/derived.js
index ebeaa5cf..5b3ec695 100644
--- a/src/js/dataLayer/derived.js
+++ b/src/js/dataLayer/derived.js
@@ -342,6 +342,9 @@ export class Derived extends ReactiveStore {
() => this.dataStore.$feedSearchResults.get()?.cursor ?? null,
);
this.$trends = new Signal.Computed(() => this.dataStore.$trends.get());
+ this.$selectedFeedUri = new Signal.Computed(() =>
+ this.dataStore.$selectedFeedUri.get(),
+ );
this.$postSearchResultsTop = new Signal.Computed(() =>
this.hydratePostSearchResults(this.dataStore.$postSearchResultsTop),
);
diff --git a/src/js/dataLayer/mutations.js b/src/js/dataLayer/mutations.js
index 0348fe71..30baae20 100644
--- a/src/js/dataLayer/mutations.js
+++ b/src/js/dataLayer/mutations.js
@@ -497,10 +497,14 @@ export class Mutations {
pinnedItems.map((item) => [valueForPinnedItem(item), item]),
);
const next = values.map((value) => byValue.get(value)).filter(Boolean);
- this.dataStore.$pinnedItems.set(next);
+ this.dataStore.setPinnedItems(next);
}
}
+ setSelectedFeedUri(feedUri) {
+ this.dataStore.$selectedFeedUri.set(feedUri);
+ }
+
async hidePost(post) {
const patchId = this.patchStore.addPostPatch(post.uri, {
type: "hidePost",
@@ -1165,7 +1169,7 @@ export class Mutations {
}
const pinnedItems = untrack(() => this.dataStore.$pinnedItems.get());
if (pinnedItems?.some((item) => item.data?.uri === list.uri)) {
- this.dataStore.$pinnedItems.set(
+ this.dataStore.setPinnedItems(
pinnedItems.filter((item) => item.data?.uri !== list.uri),
);
}
diff --git a/src/js/dataLayer/requests.js b/src/js/dataLayer/requests.js
index e3ef5c26..4d2263b9 100644
--- a/src/js/dataLayer/requests.js
+++ b/src/js/dataLayer/requests.js
@@ -1285,7 +1285,7 @@ export class Requests {
}
}
- this.dataStore.$pinnedItems.set(orderedItems);
+ this.dataStore.setPinnedItems(orderedItems);
}
async loadActorFeeds(did, { reload = false, limit = 50 } = {}) {
diff --git a/src/js/dataLayer/sessionState.js b/src/js/dataLayer/sessionState.js
new file mode 100644
index 00000000..df11475c
--- /dev/null
+++ b/src/js/dataLayer/sessionState.js
@@ -0,0 +1,42 @@
+import { Signal, ReactiveStore, PersistedReactiveStore } from "/js/signals.js";
+
+const LEGACY_SELECTED_FEED_URI_STORAGE_KEY = "home-view-currentFeedUri";
+
+function migrateLegacySelectedFeedUri(storageKey) {
+ if (localStorage.getItem(storageKey) !== null) {
+ return;
+ }
+ const legacyStored = localStorage.getItem(
+ LEGACY_SELECTED_FEED_URI_STORAGE_KEY,
+ );
+ localStorage.removeItem(LEGACY_SELECTED_FEED_URI_STORAGE_KEY);
+ if (legacyStored === null) {
+ return;
+ }
+ let selectedFeedUri = null;
+ try {
+ selectedFeedUri = JSON.parse(legacyStored);
+ } catch {
+ return;
+ }
+ if (!selectedFeedUri) {
+ return;
+ }
+ localStorage.setItem(storageKey, JSON.stringify({ selectedFeedUri }));
+}
+
+// Local client state, persisted per account. Logged-out session
+// state (a null session) doesn't persist.
+export function createSessionState(session) {
+ let sessionState;
+ if (session) {
+ const storageKey = `session-state:${session.did}`;
+ migrateLegacySelectedFeedUri(storageKey);
+ sessionState = new PersistedReactiveStore(storageKey);
+ } else {
+ sessionState = new ReactiveStore("sessionState");
+ }
+ // The home view's selected feed, shared with the pinned feeds pane
+ sessionState.$selectedFeedUri = new Signal.State(null);
+ return sessionState;
+}
diff --git a/src/js/displayPreferences.js b/src/js/displayPreferences.js
index a7b6724e..60ff2cbe 100644
--- a/src/js/displayPreferences.js
+++ b/src/js/displayPreferences.js
@@ -1,85 +1,4 @@
-import { Signal, effect, untrack } from "/js/signals.js";
-
-function readPersisted(storageKey) {
- let parsed = null;
- try {
- parsed = JSON.parse(localStorage.getItem(storageKey));
- } catch {
- return {};
- }
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
- return {};
- }
- return parsed;
-}
-
-// A store whose `$signals` are backed by one JSON object in localStorage.
-export class PersistedReactiveStore {
- #storageKey;
- #stored;
- #defaults = new Map();
- #signals = new Map();
-
- constructor(storageKey) {
- this.#storageKey = storageKey;
- this.#stored = readPersisted(storageKey);
- return new Proxy(this, {
- set(target, prop, value) {
- if (prop.startsWith("$")) {
- target.#register(prop, value);
- } else {
- target[prop] = value;
- }
- return true;
- },
- });
- }
-
- #register(prop, $signal) {
- $signal.__debugName = `${this.#storageKey}.${prop}`;
- const key = prop.slice(1);
- const defaultValue = untrack(() => $signal.get());
- this.#defaults.set(key, defaultValue);
- this.#signals.set(key, $signal);
- if (typeof this.#stored[key] === typeof defaultValue) {
- $signal.set(this.#stored[key]);
- }
- // `this` is the target rather than the proxy, so this won't re-enter the trap
- this[prop] = $signal;
- // effect() runs on creation; skip that so declaring a signal doesn't
- // immediately write its default back to storage
- let isFirstRun = true;
- effect(() => {
- $signal.get();
- if (isFirstRun) {
- isFirstRun = false;
- return;
- }
- this.#save();
- });
- }
-
- // untracked so one signal's save effect doesn't subscribe to all the others
- #save() {
- const data = { ...this.#stored };
- untrack(() => {
- for (const [key, $signal] of this.#signals) {
- const value = $signal.get();
- if (value === this.#defaults.get(key)) {
- delete data[key];
- } else {
- data[key] = value;
- }
- }
- });
- this.#stored = data;
- if (Object.keys(data).length === 0) {
- localStorage.removeItem(this.#storageKey);
- } else {
- localStorage.setItem(this.#storageKey, JSON.stringify(data));
- }
- }
-}
+import { Signal, PersistedReactiveStore } from "/js/signals.js";
const displayPreferences = new PersistedReactiveStore("display-preferences");
diff --git a/src/js/mainLayout.js b/src/js/mainLayout.js
index 5a9a288b..607c11f0 100644
--- a/src/js/mainLayout.js
+++ b/src/js/mainLayout.js
@@ -53,6 +53,7 @@ import { Layout } from "/js/router.js";
import "/js/components/animated-sidebar.js";
import "/js/components/trending-pane.js";
import "/js/components/sidebar-search.js";
+import "/js/components/pinned-feeds-pane.js";
export function mainLayoutTemplate({
isAuthenticated = true,
@@ -104,6 +105,13 @@ export function mainLayoutTemplate({
.dataLayer=${dataLayer}
.isAuthenticated=${isAuthenticated}
>`}
+ ${isAuthenticated
+ ? html``
+ : ""}
diff --git a/src/js/signals.js b/src/js/signals.js
index 26da5d66..cf04db87 100644
--- a/src/js/signals.js
+++ b/src/js/signals.js
@@ -437,3 +437,92 @@ export class ReactiveStore extends EventEmitter {
});
}
}
+
+function readPersisted(storageKey) {
+ let parsed = null;
+ try {
+ parsed = JSON.parse(localStorage.getItem(storageKey));
+ } catch {
+ return {};
+ }
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+ return {};
+ }
+ return parsed;
+}
+
+// A store whose `$signals` are backed by one JSON object in localStorage.
+export class PersistedReactiveStore {
+ #storageKey;
+ #stored;
+ #defaults = new Map();
+ #signals = new Map();
+
+ constructor(storageKey) {
+ if (!storageKey) {
+ throw new Error("PersistedReactiveStore requires a storage key");
+ }
+ this.#storageKey = storageKey;
+ this.#stored = readPersisted(storageKey);
+ return new Proxy(this, {
+ set(target, prop, value) {
+ if (prop.startsWith("$")) {
+ target.#register(prop, value);
+ } else {
+ target[prop] = value;
+ }
+ return true;
+ },
+ });
+ }
+
+ #register(prop, $signal) {
+ $signal.__debugName = `${this.#storageKey}.${prop}`;
+ const key = prop.slice(1);
+ const defaultValue = untrack(() => $signal.get());
+ this.#defaults.set(key, defaultValue);
+ this.#signals.set(key, $signal);
+ const storedValue = this.#stored[key];
+ const restores =
+ defaultValue === null
+ ? storedValue !== undefined
+ : typeof storedValue === typeof defaultValue;
+ if (restores) {
+ $signal.set(storedValue);
+ }
+ // `this` is the target rather than the proxy, so this won't re-enter the trap
+ this[prop] = $signal;
+ // effect() runs on creation; skip that so declaring a signal doesn't
+ // immediately write its default back to storage
+ let isFirstRun = true;
+ effect(() => {
+ $signal.get();
+ if (isFirstRun) {
+ isFirstRun = false;
+ return;
+ }
+ this.#save();
+ });
+ }
+
+ // untracked so one signal's save effect doesn't subscribe to all the others
+ #save() {
+ const data = { ...this.#stored };
+ untrack(() => {
+ for (const [key, $signal] of this.#signals) {
+ const value = $signal.get();
+ if (value === this.#defaults.get(key)) {
+ delete data[key];
+ } else {
+ data[key] = value;
+ }
+ }
+ });
+ this.#stored = data;
+ if (Object.keys(data).length === 0) {
+ localStorage.removeItem(this.#storageKey);
+ } else {
+ localStorage.setItem(this.#storageKey, JSON.stringify(data));
+ }
+ }
+}
diff --git a/src/js/views/home.view.js b/src/js/views/home.view.js
index c9724958..5a14fac7 100644
--- a/src/js/views/home.view.js
+++ b/src/js/views/home.view.js
@@ -42,54 +42,18 @@ export default async function homeView({
interactionHandlers,
},
}) {
- const CURRENT_FEED_URI_STORAGE_KEY = "home-view-currentFeedUri";
const WELCOME_MODAL_SEEN_STORAGE_KEY = "welcome-modal-seen";
- // Keyed per account
- const currentFeedUriStorageKey = isAuthenticated
- ? `${CURRENT_FEED_URI_STORAGE_KEY}:${api.session.did}`
- : null;
-
- function readStoredFeedUri() {
- if (!currentFeedUriStorageKey) {
- return null;
- }
- const stored = localStorage.getItem(currentFeedUriStorageKey);
- if (stored !== null) {
- return stored;
- }
- // Carry over the selection saved before the key was per-account
- const legacyStored = localStorage.getItem(CURRENT_FEED_URI_STORAGE_KEY);
- localStorage.removeItem(CURRENT_FEED_URI_STORAGE_KEY);
- return legacyStored;
- }
-
- function saveFeedUri(feedUri) {
- if (!currentFeedUriStorageKey) {
- return;
- }
- localStorage.setItem(currentFeedUriStorageKey, JSON.stringify(feedUri));
- }
-
- const storedFeedUri = readStoredFeedUri();
-
const state = new ReactiveStore("homeView");
- state.$currentFeedUri = new Signal.State(
- storedFeedUri ? JSON.parse(storedFeedUri) : null,
- );
state.$isReloadingFeed = new Signal.State(false);
state.$materializedFeedUris = new SignalSet();
- function resetToDefaultFeed() {
- state.$currentFeedUri.set(
+ if (!dataLayer.derived.$selectedFeedUri.get()) {
+ dataLayer.mutations.setSelectedFeedUri(
isAuthenticated ? FOLLOWING_FEED_URI : LOGGED_OUT_FEED_URI,
);
}
- if (!state.$currentFeedUri.get()) {
- resetToDefaultFeed();
- }
-
if (
!isAuthenticated &&
!pluginService.isPreviewMode &&
@@ -185,7 +149,7 @@ export default async function homeView({
}
async function handleTabClick(feedUri) {
- let currentFeedUri = state.$currentFeedUri.get();
+ let currentFeedUri = dataLayer.derived.$selectedFeedUri.get();
if (feedUri === currentFeedUri) {
scrollAndReloadFeed();
return;
@@ -194,8 +158,7 @@ export default async function homeView({
feedScrollState.set(currentFeedUri, window.scrollY);
state.$materializedFeedUris.add(currentFeedUri);
// Switch feed
- state.$currentFeedUri.set(feedUri);
- saveFeedUri(feedUri);
+ dataLayer.mutations.setSelectedFeedUri(feedUri);
// Scroll to saved position for new feed
const savedScrollY = feedScrollState.get(feedUri) ?? 0;
requestAnimationFrame(() => {
@@ -233,9 +196,15 @@ export default async function homeView({
scrollAndReloadFeed();
});
+ // Dispatched by the pinned feeds pane in the right column
+ bindToPage(root, window, "home-feed-select", (event) => {
+ const feedUri = event.detail;
+ handleTabClick(feedUri);
+ });
+
const $currentPinnedItem = new Signal.Computed(() => {
const pinnedItems = dataLayer.derived.$hydratedPinnedItems.get() ?? [];
- const currentFeedUri = state.$currentFeedUri.get();
+ const currentFeedUri = dataLayer.derived.$selectedFeedUri.get();
return pinnedItems.find((item) => item.uri === currentFeedUri);
});
@@ -306,7 +275,7 @@ export default async function homeView({
pageEffect(root, () => {
const currentUser = dataLayer.derived.$currentUser.get();
const pinnedItems = dataLayer.derived.$hydratedPinnedItems.get() ?? [];
- const currentFeedUri = state.$currentFeedUri.get();
+ const currentFeedUri = dataLayer.derived.$selectedFeedUri.get();
const currentFeedRequestStatus =
dataLayer.requests.statusStore.$statuses.get(
"loadNextFeedPage-" + currentFeedUri,
@@ -390,7 +359,7 @@ export default async function homeView({
}
async function loadCurrentFeed({ reload = false } = {}) {
- const currentFeedUri = state.$currentFeedUri.get();
+ const currentFeedUri = dataLayer.derived.$selectedFeedUri.get();
await dataLayer.requests.loadNextFeedPage(
getFeedRequestDescriptor(currentFeedUri),
{ reload, limit: FEED_PAGE_SIZE + 1 },
@@ -398,10 +367,11 @@ export default async function homeView({
}
async function preloadHiddenFeeds(pinnedItems) {
- const currentFeedUri = state.$currentFeedUri.get();
+ const currentFeedUri = dataLayer.derived.$selectedFeedUri.get();
const itemsToPreload = pinnedItems
.filter((item) => item.uri !== currentFeedUri)
- .slice(0, 5);
+ .slice(0, 5)
+ .filter((item) => !dataLayer.hasCachedFeed(item.uri));
for (const item of itemsToPreload) {
await dataLayer.requests.loadNextFeedPage(item, {
limit: FEED_PAGE_SIZE + 1,
@@ -419,23 +389,20 @@ export default async function homeView({
}
async function loadPageData() {
- const currentFeedUri = state.$currentFeedUri.get();
const pinnedItems = await dataLayer.declarative.ensurePinnedItems();
- if (!pinnedItems.some((item) => item.uri === currentFeedUri)) {
- resetToDefaultFeed();
- }
preloadHiddenFeeds(pinnedItems);
// Ensure current user before loading the feed to prevent a flash of
// unfiltered posts
if (isAuthenticated) {
await dataLayer.declarative.ensureCurrentUser();
}
- await loadCurrentFeed({ reload: true });
+ if (!dataLayer.hasCachedFeed(dataLayer.derived.$selectedFeedUri.get())) {
+ await loadCurrentFeed();
+ }
}
- loadPageData().catch((error) => console.error(error));
-
onPageShow(root, () => {
+ loadPageData().catch((error) => console.error(error));
for (const observer of postSeenObservers.values()) {
observer.connect();
}
diff --git a/tests/e2e/specs/concerns/pinnedFeedsPane.test.js b/tests/e2e/specs/concerns/pinnedFeedsPane.test.js
new file mode 100644
index 00000000..7dd886bd
--- /dev/null
+++ b/tests/e2e/specs/concerns/pinnedFeedsPane.test.js
@@ -0,0 +1,98 @@
+import { test, expect } from "../../base.js";
+import { login } from "../../helpers.js";
+import { MockServer } from "../../mockServer.js";
+import { createFeedGenerator, createPost } from "../../../shared/factories.js";
+
+test.describe("Pinned feeds pane", () => {
+ let mockServer;
+ const feed = createFeedGenerator({
+ uri: "at://did:plc:creator1/app.bsky.feed.generator/trending",
+ displayName: "Trending",
+ creatorHandle: "creator1.bsky.social",
+ });
+
+ test.beforeEach(async ({ page }) => {
+ mockServer = new MockServer();
+ mockServer.addFeedGenerators([feed]);
+ mockServer.setPinnedFeeds([feed.uri]);
+ mockServer.addFeedItems(feed.uri, [
+ createPost({
+ uri: "at://did:plc:author1/app.bsky.feed.post/feedpost1",
+ text: "Post from the pinned feed",
+ authorHandle: "author1.bsky.social",
+ }),
+ ]);
+ await mockServer.setup(page);
+ await login(page);
+ });
+
+ test("lists pinned feeds with a More feeds link in the right column", async ({
+ page,
+ }) => {
+ await page.goto("/");
+ await expect(page.locator("#home-view")).toBeVisible({ timeout: 10000 });
+
+ const pane = page.locator('[data-testid="pinned-feeds-pane"]');
+ await expect(pane).toBeVisible();
+ const items = pane.locator('[data-testid="pinned-feeds-item"]');
+ await expect(items).toHaveCount(2, { timeout: 10000 });
+ await expect(items.nth(0)).toContainText("Following");
+ await expect(items.nth(1)).toContainText("Trending");
+ await expect(
+ pane.locator('[data-testid="pinned-feeds-more"]'),
+ ).toBeVisible();
+ });
+
+ test("switches the home feed when a pinned feed is clicked", async ({
+ page,
+ }) => {
+ await page.goto("/");
+ const view = page.locator("#home-view");
+ await expect(view.locator(".tab-bar-button.active")).toContainText(
+ "Following",
+ { timeout: 10000 },
+ );
+
+ const items = page.locator('[data-testid="pinned-feeds-item"]');
+ await expect(items).toHaveCount(2, { timeout: 10000 });
+ await items.nth(1).click();
+
+ await expect(view.locator(".tab-bar-button.active")).toContainText(
+ "Trending",
+ );
+ await expect(items.nth(1)).toHaveClass(/active/);
+ await expect(view).toContainText("Post from the pinned feed", {
+ timeout: 10000,
+ });
+ });
+
+ test("navigates home and selects the feed when clicked from another page", async ({
+ page,
+ }) => {
+ await page.goto("/notifications");
+ await expect(page.locator("#notifications-view")).toBeVisible({
+ timeout: 10000,
+ });
+
+ const items = page.locator('[data-testid="pinned-feeds-item"]');
+ await expect(items).toHaveCount(2, { timeout: 10000 });
+ await items.nth(1).click();
+
+ await expect(page).toHaveURL("/");
+ await expect(
+ page.locator("#home-view .tab-bar-button.active"),
+ ).toContainText("Trending", { timeout: 10000 });
+ });
+
+ test("navigates to the feeds view from the More feeds link", async ({
+ page,
+ }) => {
+ await page.goto("/");
+ await expect(page.locator("#home-view")).toBeVisible({ timeout: 10000 });
+
+ await page.locator('[data-testid="pinned-feeds-more"]').click();
+
+ await expect(page).toHaveURL("/feeds");
+ await expect(page.locator("#feeds-view")).toBeVisible({ timeout: 10000 });
+ });
+});
diff --git a/tests/e2e/specs/flows/pinFeed.test.js b/tests/e2e/specs/flows/pinFeed.test.js
index 8bc98168..1e2acd41 100644
--- a/tests/e2e/specs/flows/pinFeed.test.js
+++ b/tests/e2e/specs/flows/pinFeed.test.js
@@ -1,9 +1,66 @@
import { test, expect } from "../../base.js";
import { login } from "../../helpers.js";
import { MockServer } from "../../mockServer.js";
-import { createFeedGenerator } from "../../../shared/factories.js";
+import { createFeedGenerator, createPost } from "../../../shared/factories.js";
test.describe("Pin feed flow", () => {
+ test("should fall back to Following on home after unpinning the selected feed", async ({
+ page,
+ }) => {
+ const mockServer = new MockServer();
+ const feed = createFeedGenerator({
+ uri: "at://did:plc:creator1/app.bsky.feed.generator/trending",
+ displayName: "Trending",
+ creatorHandle: "creator1.bsky.social",
+ });
+ mockServer.addFeedGenerators([feed]);
+ mockServer.setPinnedFeeds([feed.uri]);
+ mockServer.addFeedItems(feed.uri, [
+ createPost({
+ uri: "at://did:plc:author1/app.bsky.feed.post/trendingpost",
+ text: "Post from the trending feed",
+ authorHandle: "author1.bsky.social",
+ }),
+ ]);
+ await mockServer.setup(page);
+ await login(page);
+
+ // Select the pinned feed on home
+ await page.goto("/");
+ const homeView = page.locator("#home-view");
+ const tabs = homeView.locator(".tab-bar-button");
+ await expect(tabs).toHaveCount(2, { timeout: 10000 });
+ await tabs.nth(1).click();
+ await expect(homeView.locator(".tab-bar-button.active")).toContainText(
+ "Trending",
+ );
+
+ // Unpin it from the feeds view (in-app navigation keeps home cached)
+ await page.locator('[data-testid="pinned-feeds-more"]').click();
+ const feedsView = page.locator("#feeds-view");
+ await expect(feedsView.locator(".feeds-list-item")).toHaveCount(2, {
+ timeout: 10000,
+ });
+ await feedsView.locator('[data-testid="feeds-edit-button"]').click();
+ await feedsView
+ .locator(".feeds-list-item", { hasText: "Trending" })
+ .locator('[data-testid="feeds-list-item-unpin-button"]')
+ .click();
+ await feedsView.locator('[data-testid="feeds-save-button"]').click();
+ await expect(feedsView.locator(".feeds-list-item")).toHaveCount(1, {
+ timeout: 10000,
+ });
+
+ // Back on home, the selection falls back to Following
+ await page.locator('[data-testid="sidebar-nav-home"]').click();
+ await expect(homeView.locator(".tab-bar-button")).toHaveCount(1, {
+ timeout: 10000,
+ });
+ await expect(homeView.locator(".tab-bar-button.active")).toContainText(
+ "Following",
+ );
+ });
+
test("should show feed in feeds index after pinning from feed detail", async ({
page,
}) => {
diff --git a/tests/unit/specs/components/pinned-feeds-pane.test.js b/tests/unit/specs/components/pinned-feeds-pane.test.js
new file mode 100644
index 00000000..694733cf
--- /dev/null
+++ b/tests/unit/specs/components/pinned-feeds-pane.test.js
@@ -0,0 +1,234 @@
+import { describe, it, beforeEach, afterEach, mock } from "node:test";
+import assert from "node:assert/strict";
+import "/js/components/pinned-feeds-pane.js";
+import { makeTestDataLayer } from "../../testHelpers.js";
+import { createFeedGenerator, createList } from "../../../shared/factories.js";
+
+describe("pinned-feeds-pane", () => {
+ const feedGenerator = createFeedGenerator({
+ uri: "at://did:plc:creator1/app.bsky.feed.generator/trending",
+ displayName: "Trending",
+ creatorHandle: "creator1.bsky.social",
+ });
+ const list = createList({
+ uri: "at://did:plc:creator1/app.bsky.graph.list/mylist",
+ name: "My Curated List",
+ creatorHandle: "creator1.bsky.social",
+ });
+ const pinnedItems = [
+ {
+ type: "timeline",
+ data: { uri: "following", displayName: "Following" },
+ },
+ { type: "feed", data: feedGenerator },
+ { type: "list", data: list },
+ ];
+
+ let dataLayer;
+ let element;
+ let routerGo;
+ let previousRouter;
+
+ function mount({ showSelected = true, moreFeedsActive = false } = {}) {
+ element = document.createElement("pinned-feeds-pane");
+ element.dataLayer = dataLayer;
+ if (showSelected) {
+ element.setAttribute("show-selected", "");
+ }
+ if (moreFeedsActive) {
+ element.setAttribute("more-feeds-active", "");
+ }
+ document.body.appendChild(element);
+ return element;
+ }
+
+ async function flushMicrotasks() {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+
+ function itemLabels() {
+ return [
+ ...element.querySelectorAll("[data-testid='pinned-feeds-item']"),
+ ].map((item) => item.querySelector(".pinned-feeds-item-label").textContent);
+ }
+
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ dataLayer = makeTestDataLayer();
+ routerGo = mock.fn(async () => {});
+ previousRouter = window.router;
+ window.router = { go: routerGo };
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = "";
+ window.router = previousRouter;
+ window.history.replaceState(null, "", "/");
+ });
+
+ it("renders skeleton rows while pinned items are loading", () => {
+ mount();
+ assert.deepEqual(
+ element.querySelectorAll("[data-testid='pinned-feeds-skeleton']").length,
+ 5,
+ );
+ });
+
+ it("renders a row per pinned item plus the More feeds link", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ mount();
+ await flushMicrotasks();
+
+ assert.deepEqual(itemLabels(), [
+ "Following",
+ "Trending",
+ "My Curated List",
+ ]);
+ const moreLink = element.querySelector("[data-testid='pinned-feeds-more']");
+ assert.deepEqual(moreLink.getAttribute("href"), "/feeds");
+ });
+
+ it("renders an icon for the timeline item and avatars for feeds and lists", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ mount();
+ await flushMicrotasks();
+
+ const items = element.querySelectorAll("[data-testid='pinned-feeds-item']");
+ assert(items[0].querySelector(".pinned-feeds-timeline-icon") !== null);
+ assert.deepEqual(
+ items[1].querySelector(".pinned-feeds-item-avatar").getAttribute("src"),
+ "/img/feed-avatar-fallback.svg",
+ );
+ assert.deepEqual(
+ items[2].querySelector(".pinned-feeds-item-avatar").getAttribute("src"),
+ "/img/list-avatar-fallback.svg",
+ );
+ });
+
+ it("highlights the selected feed while on the home route", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ dataLayer.dataStore.$selectedFeedUri.set(feedGenerator.uri);
+ mount();
+ await flushMicrotasks();
+
+ const active = element.querySelectorAll(".pinned-feeds-item.active");
+ assert.deepEqual(active.length, 1);
+ assert.deepEqual(
+ active[0].querySelector(".pinned-feeds-item-label").textContent,
+ "Trending",
+ );
+ });
+
+ it("moves the highlight when the selected feed changes", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ dataLayer.dataStore.$selectedFeedUri.set(feedGenerator.uri);
+ mount();
+ await flushMicrotasks();
+
+ dataLayer.dataStore.$selectedFeedUri.set(list.uri);
+ await flushMicrotasks();
+
+ const active = element.querySelectorAll(".pinned-feeds-item.active");
+ assert.deepEqual(active.length, 1);
+ assert.deepEqual(
+ active[0].querySelector(".pinned-feeds-item-label").textContent,
+ "My Curated List",
+ );
+ });
+
+ it("does not highlight any feed when show-selected is absent", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ dataLayer.dataStore.$selectedFeedUri.set(feedGenerator.uri);
+ mount({ showSelected: false });
+ await flushMicrotasks();
+
+ assert.deepEqual(
+ element.querySelectorAll(".pinned-feeds-item.active").length,
+ 0,
+ );
+ });
+
+ it("re-renders the highlight when the show-selected attribute changes", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ dataLayer.dataStore.$selectedFeedUri.set(feedGenerator.uri);
+ mount();
+ await flushMicrotasks();
+ assert.deepEqual(
+ element.querySelectorAll(".pinned-feeds-item.active").length,
+ 1,
+ );
+
+ element.removeAttribute("show-selected");
+ assert.deepEqual(
+ element.querySelectorAll(".pinned-feeds-item.active").length,
+ 0,
+ );
+
+ element.setAttribute("show-selected", "");
+ assert.deepEqual(
+ element.querySelectorAll(".pinned-feeds-item.active").length,
+ 1,
+ );
+ });
+
+ it("highlights the More feeds link when more-feeds-active is set", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ mount({ showSelected: false, moreFeedsActive: true });
+ await flushMicrotasks();
+
+ const moreLink = element.querySelector("[data-testid='pinned-feeds-more']");
+ assert(moreLink.classList.contains("active"));
+ });
+
+ it("dispatches home-feed-select without navigating when already on home", async () => {
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ mount();
+ await flushMicrotasks();
+ const selected = [];
+ const listener = (event) => selected.push(event.detail);
+ window.addEventListener("home-feed-select", listener);
+
+ element.querySelectorAll("[data-testid='pinned-feeds-item']")[1].click();
+ await flushMicrotasks();
+ window.removeEventListener("home-feed-select", listener);
+
+ assert.deepEqual(routerGo.mock.callCount(), 0);
+ assert.deepEqual(selected, [feedGenerator.uri]);
+ });
+
+ it("sets the selection before navigating home from another route", async () => {
+ window.history.replaceState(null, "", "/notifications");
+ dataLayer.dataStore.$pinnedItems.set(pinnedItems);
+ mount({ showSelected: false });
+ await flushMicrotasks();
+ const selected = [];
+ const listener = (event) => selected.push(event.detail);
+ window.addEventListener("home-feed-select", listener);
+ let selectedFeedUriAtNavigation = null;
+ routerGo.mock.mockImplementation(async () => {
+ selectedFeedUriAtNavigation = dataLayer.dataStore.$selectedFeedUri.get();
+ });
+
+ element.querySelectorAll("[data-testid='pinned-feeds-item']")[0].click();
+ await flushMicrotasks();
+ window.removeEventListener("home-feed-select", listener);
+
+ assert.deepEqual(routerGo.mock.callCount(), 1);
+ assert.deepEqual(routerGo.mock.calls[0].arguments, ["/"]);
+ assert.deepEqual(selectedFeedUriAtNavigation, "following");
+ // The home view applies the pre-set selection itself; no event is needed
+ assert.deepEqual(selected, []);
+ });
+
+ it("renders nothing when loading pinned items fails", async () => {
+ // makeTestDataLayer has no loaded preferences, so ensurePinnedItems rejects
+ mount();
+ await flushMicrotasks();
+
+ assert.deepEqual(
+ element.querySelector("[data-testid='pinned-feeds-pane']"),
+ null,
+ );
+ });
+});
diff --git a/tests/unit/specs/dataLayer/dataLayer.test.js b/tests/unit/specs/dataLayer/dataLayer.test.js
index 7d6ca1ce..af741901 100644
--- a/tests/unit/specs/dataLayer/dataLayer.test.js
+++ b/tests/unit/specs/dataLayer/dataLayer.test.js
@@ -1,4 +1,4 @@
-import { describe, it } from "node:test";
+import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { DataLayer } from "/js/dataLayer/dataLayer.js";
import { DraftMediaStore } from "/js/drafts.js";
@@ -7,9 +7,11 @@ import { HiddenFeedItemsStore } from "/js/dataLayer/hiddenFeedItemsStore.js";
import { Constellation } from "/js/constellation.js";
function createMockApi(options = {}) {
+ const isAuthenticated = options.isAuthenticated ?? false;
return {
getProfile: async (did) => options.profiles?.[did] ?? null,
- isAuthenticated: options.isAuthenticated ?? false,
+ isAuthenticated,
+ session: isAuthenticated ? { did: "did:plc:testuser" } : null,
getPreferences: async () => options.preferences ?? [],
getLabelers: async () => options.labelers ?? [],
updatePreferences: async () => {},
@@ -206,3 +208,87 @@ describe("component integration", () => {
assert(profile !== null);
});
});
+
+describe("selected feed persistence", () => {
+ // createMockApi's authenticated session did
+ const sessionStateKey = "session-state:did:plc:testuser";
+ const legacyKey = "home-view-currentFeedUri";
+
+ // PersistedReactiveStore saves via an effect, which flushes on a double
+ // requestAnimationFrame
+ function flushEffects() {
+ return new Promise((resolve) =>
+ requestAnimationFrame(() => requestAnimationFrame(resolve)),
+ );
+ }
+
+ function cleanup() {
+ localStorage.removeItem(sessionStateKey);
+ localStorage.removeItem(legacyKey);
+ }
+
+ function createAuthedDataLayer() {
+ return createDataLayer(createMockApi({ isAuthenticated: true }));
+ }
+
+ beforeEach(cleanup);
+ afterEach(cleanup);
+
+ it("restores the stored selection for the account", () => {
+ localStorage.setItem(
+ sessionStateKey,
+ JSON.stringify({ selectedFeedUri: "following" }),
+ );
+ const dataLayer = createAuthedDataLayer();
+ assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), "following");
+ });
+
+ it("migrates a selection stored under the legacy key", () => {
+ localStorage.setItem(legacyKey, JSON.stringify("following"));
+ const dataLayer = createAuthedDataLayer();
+ assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), "following");
+ assert.deepEqual(localStorage.getItem(legacyKey), null);
+ });
+
+ it("prefers the session-state key over the legacy key", () => {
+ localStorage.setItem(
+ sessionStateKey,
+ JSON.stringify({ selectedFeedUri: "following" }),
+ );
+ localStorage.setItem(legacyKey, JSON.stringify("stale"));
+ const dataLayer = createAuthedDataLayer();
+ assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), "following");
+ });
+
+ it("persists selection changes", async () => {
+ const dataLayer = createAuthedDataLayer();
+ dataLayer.mutations.setSelectedFeedUri("following");
+ await flushEffects();
+ assert.deepEqual(JSON.parse(localStorage.getItem(sessionStateKey)), {
+ selectedFeedUri: "following",
+ });
+ });
+
+ it("persists a fallback applied by setPinnedItems", async () => {
+ const dataLayer = createAuthedDataLayer();
+ dataLayer.mutations.setSelectedFeedUri(
+ "at://did:test/app.bsky.feed.generator/gone",
+ );
+ dataLayer.dataStore.setPinnedItems([
+ { type: "timeline", data: { uri: "following" } },
+ ]);
+ await flushEffects();
+ assert.deepEqual(JSON.parse(localStorage.getItem(sessionStateKey)), {
+ selectedFeedUri: "following",
+ });
+ });
+
+ it("neither restores nor persists without a session", async () => {
+ localStorage.setItem(legacyKey, JSON.stringify("following"));
+ const dataLayer = createDataLayer(createMockApi());
+ assert.deepEqual(dataLayer.dataStore.$selectedFeedUri.get(), null);
+ dataLayer.mutations.setSelectedFeedUri("following");
+ await flushEffects();
+ assert.deepEqual(localStorage.getItem(sessionStateKey), null);
+ });
+});
diff --git a/tests/unit/specs/dataLayer/dataStore.test.js b/tests/unit/specs/dataLayer/dataStore.test.js
index bd30ab2b..f11ddb92 100644
--- a/tests/unit/specs/dataLayer/dataStore.test.js
+++ b/tests/unit/specs/dataLayer/dataStore.test.js
@@ -1,10 +1,11 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
describe("setPosts", () => {
it("should insert multiple posts", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const posts = [
{ uri: "at://did:test/app.bsky.feed.post/1", record: { text: "one" } },
{ uri: "at://did:test/app.bsky.feed.post/2", record: { text: "two" } },
@@ -17,8 +18,8 @@ describe("setPosts", () => {
});
it("should match $posts.set behavior when given a single post", () => {
- const dataStoreA = new DataStore();
- const dataStoreB = new DataStore();
+ const dataStoreA = new DataStore(createSessionState(null));
+ const dataStoreB = new DataStore(createSessionState(null));
const post = {
uri: "at://did:test/app.bsky.feed.post/solo",
record: { text: "solo" },
@@ -32,7 +33,7 @@ describe("setPosts", () => {
});
it("should store nested quoted posts as embedded previews", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const nestedQuotedPost = {
$type: "app.bsky.embed.record#viewRecord",
uri: "at://did:test/app.bsky.feed.post/nested",
@@ -97,7 +98,7 @@ describe("setPosts", () => {
});
it("should replace an embedded preview with a full post", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const quotedUri = "at://did:test/app.bsky.feed.post/quoted";
const quotedPost = {
$type: "app.bsky.embed.record#viewRecord",
@@ -135,7 +136,7 @@ describe("setPosts", () => {
describe("setConvo", () => {
it("should save the convo and prepend it to the loaded convo list", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [{ id: "c1" }], cursor: "page2" });
const convo = { id: "c2", status: "accepted" };
@@ -150,7 +151,7 @@ describe("setConvo", () => {
});
it("should replace an existing list entry instead of duplicating it", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({
convos: [{ id: "c1", unreadCount: 0 }],
cursor: null,
@@ -168,7 +169,7 @@ describe("setConvo", () => {
});
it("should leave unloaded lists null", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.setConvo({ id: "c1", status: "accepted" });
@@ -178,7 +179,7 @@ describe("setConvo", () => {
});
it("should route request convos to the request list", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [], cursor: null });
dataStore.$convoRequestList.set({ convos: [], cursor: null });
@@ -192,7 +193,7 @@ describe("setConvo", () => {
});
it("should move an accepted convo out of the request list", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [], cursor: null });
dataStore.$convoRequestList.set({
convos: [{ id: "c1", status: "request" }],
@@ -208,3 +209,47 @@ describe("setConvo", () => {
assert.deepEqual(dataStore.$convoRequestList.get().convos, []);
});
});
+
+describe("setPinnedItems", () => {
+ const followingItem = {
+ type: "timeline",
+ data: { uri: "following", displayName: "Following" },
+ };
+ const feedItem = {
+ type: "feed",
+ data: { uri: "at://did:test/app.bsky.feed.generator/cats" },
+ };
+
+ it("should save the pinned items", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ dataStore.setPinnedItems([followingItem, feedItem]);
+ assert.deepEqual(dataStore.$pinnedItems.get(), [followingItem, feedItem]);
+ });
+
+ it("should keep a selected feed that is still pinned", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ dataStore.$selectedFeedUri.set(feedItem.data.uri);
+ dataStore.setPinnedItems([followingItem, feedItem]);
+ assert.deepEqual(dataStore.$selectedFeedUri.get(), feedItem.data.uri);
+ });
+
+ it("should reset a no-longer-pinned selected feed to the first pinned item", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ dataStore.$selectedFeedUri.set(feedItem.data.uri);
+ dataStore.setPinnedItems([followingItem]);
+ assert.deepEqual(dataStore.$selectedFeedUri.get(), "following");
+ });
+
+ it("should leave a null selected feed alone", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ dataStore.setPinnedItems([followingItem]);
+ assert.deepEqual(dataStore.$selectedFeedUri.get(), null);
+ });
+
+ it("should clear the selected feed when nothing is pinned", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ dataStore.$selectedFeedUri.set(feedItem.data.uri);
+ dataStore.setPinnedItems([]);
+ assert.deepEqual(dataStore.$selectedFeedUri.get(), null);
+ });
+});
diff --git a/tests/unit/specs/dataLayer/derived.test.js b/tests/unit/specs/dataLayer/derived.test.js
index 35ff50aa..bdc8f936 100644
--- a/tests/unit/specs/dataLayer/derived.test.js
+++ b/tests/unit/specs/dataLayer/derived.test.js
@@ -2,6 +2,7 @@ import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { Derived } from "/js/dataLayer/derived.js";
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
import { DraftMediaStore, getDraftDeviceId } from "/js/drafts.js";
import { PatchStore } from "/js/dataLayer/patchStore.js";
import { Preferences } from "/js/preferences.js";
@@ -57,13 +58,13 @@ describe("$hydratedFeeds", () => {
const feedURI = "at://did:test/app.bsky.feed.generator/test";
it("should return null when feed does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedFeeds.get(feedURI), null);
});
it("should hydrate and return a feed with posts", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const rawFeed = {
@@ -88,7 +89,7 @@ describe("$hydratedFeeds", () => {
});
it("should apply patches to posts in feed", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const rawFeed = {
@@ -114,7 +115,7 @@ describe("$hydratedFeeds", () => {
describe("$hydratedEmbeddedPosts", () => {
it("hydrates embedded posts without exposing them as full posts", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const post = {
uri: "at://did:test/app.bsky.feed.post/quoted",
@@ -135,13 +136,13 @@ describe("$hydratedHashtagFeeds", () => {
const hashtagKey = "javascript-top";
it("should return null when feed does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedHashtagFeeds.get(hashtagKey), null);
});
it("should hydrate and return a feed with posts", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const rawFeed = {
@@ -163,7 +164,7 @@ describe("$hydratedHashtagFeeds", () => {
});
it("should attach parentAuthor when post is a reply and parent is loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const parentAuthor = { did: "did:parent", handle: "parent.test" };
@@ -195,7 +196,7 @@ describe("$hydratedHashtagFeeds", () => {
});
it("should apply patches to posts in feed", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const rawFeed = {
@@ -222,13 +223,13 @@ describe("$hydratedProfiles", () => {
const did = "did:plc:user";
it("should return null when profile does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedProfiles.get(did), null);
});
it("should return the profile when it exists", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const profile = { did, handle: "user.test", followersCount: 10 };
dataStore.$profiles.set(did, profile);
@@ -239,7 +240,7 @@ describe("$hydratedProfiles", () => {
});
it("should apply profile patches", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const profile = {
did,
@@ -269,13 +270,13 @@ describe("$convoProfiles", () => {
}
it("should return an empty list for an unknown convo", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$convoProfiles.get(convoId), []);
});
it("should return the members when no other profiles are referenced", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
setupConvo(dataStore);
const { derived } = makeDerived(dataStore);
const profiles = derived.$convoProfiles.get(convoId);
@@ -284,7 +285,7 @@ describe("$convoProfiles", () => {
});
it("should append hydrated profiles referenced by the last interaction", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
setupConvo(dataStore, {
lastMessage: { id: "msg1", sender: { did: referencedDid } },
});
@@ -298,7 +299,7 @@ describe("$convoProfiles", () => {
});
it("should append hydrated profiles referenced by loaded messages", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
setupConvo(dataStore);
dataStore.$convoMessages.set(convoId, {
messages: [
@@ -319,7 +320,7 @@ describe("$convoProfiles", () => {
});
it("should not duplicate referenced profiles that are also members", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
setupConvo(dataStore, {
lastMessage: { id: "msg1", sender: { did: memberDid } },
});
@@ -330,7 +331,7 @@ describe("$convoProfiles", () => {
});
it("should skip referenced dids whose profiles are not hydrated", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
setupConvo(dataStore, {
lastMessage: { id: "msg1", sender: { did: referencedDid } },
});
@@ -346,13 +347,13 @@ describe("$hydratedAuthorFeeds", () => {
const feedURI = `${did}-posts`;
it("should return null when author feed does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedAuthorFeeds.get(feedURI), null);
});
it("should hydrate and return an author feed", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const post1 = { uri: "post1", likeCount: 1 };
const post2 = { uri: "post2", likeCount: 2 };
@@ -370,7 +371,7 @@ describe("$hydratedAuthorFeeds", () => {
});
it("should filter to replies-only for replies feed type", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const repliesFeedURI = `${did}-replies`;
const post1 = { uri: "post1" };
@@ -394,7 +395,7 @@ describe("$hydratedAuthorFeeds", () => {
});
it("should pass through tombstone reply roots and parents unchanged", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const repliesFeedURI = `${did}-replies`;
const notFoundRoot = {
@@ -424,7 +425,7 @@ describe("$hydratedAuthorFeeds", () => {
});
it("should hydrate postView reply roots and parents from the post store", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const repliesFeedURI = `${did}-replies`;
const rootPost = { uri: "root", likeCount: 3 };
@@ -448,7 +449,7 @@ describe("$hydratedAuthorFeeds", () => {
});
it("should apply author feed patches", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const pinnedPost = { uri: "pinned", likeCount: 0 };
const otherPost = { uri: "other", likeCount: 0 };
@@ -471,13 +472,13 @@ describe("$actorFeeds", () => {
const did = "did:plc:author";
it("should return null when actor feeds do not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$actorFeeds.get(did), null);
});
it("should return the stored actor feeds", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const actorFeeds = { feeds: [{ uri: "feed-1" }], cursor: "c" };
dataStore.$actorFeeds.set(did, actorFeeds);
@@ -489,13 +490,13 @@ describe("$profileChatStatus", () => {
const did = "did:plc:user";
it("should return null when chat status does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$profileChatStatus.get(did), null);
});
it("should return the stored chat status", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const status = { canChat: true, convo: null };
dataStore.$profileChatStatus.set(did, status);
@@ -507,13 +508,13 @@ describe("$labelerInfo", () => {
const did = "did:plc:labeler";
it("should return null when labeler info does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$labelerInfo.get(did), null);
});
it("should return the stored labeler info", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const info = { policies: { labelValues: ["spam"] } };
dataStore.$labelerInfo.set(did, info);
@@ -523,7 +524,7 @@ describe("$labelerInfo", () => {
describe("$labelerSettings", () => {
it("should return labeler settings from preferences", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const labelerDid = "did:plc:labeler";
const result = derived.$labelerSettings.get(labelerDid);
@@ -534,13 +535,13 @@ describe("$labelerSettings", () => {
describe("$hydratedBookmarks", () => {
it("should return null when bookmarks do not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedBookmarks.get(), null);
});
it("should hydrate and return bookmarks", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const post1 = { uri: "post1", likeCount: 5 };
const post2 = { uri: "post2", likeCount: 10 };
@@ -558,7 +559,7 @@ describe("$hydratedBookmarks", () => {
});
it("should attach parentAuthor when bookmarked post is a reply", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const parentAuthor = { did: "did:parent", handle: "parent.test" };
dataStore.$posts.set("post-parent", {
@@ -586,13 +587,13 @@ describe("$hydratedBookmarks", () => {
describe("$hydratedPinnedItems", () => {
it("should return null when pinned items are not set", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedPinnedItems.get(), null);
});
it("should hydrate pinned feed generators from the store", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const fg1 = { uri: "feed-1", displayName: "Feed One" };
const fg2 = { uri: "feed-2", displayName: "Feed Two" };
@@ -612,7 +613,7 @@ describe("$hydratedPinnedItems", () => {
});
it("should hydrate list and timeline entries", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const list = { uri: "list-1", name: "My List" };
dataStore.$pinnedItems.set([
@@ -632,13 +633,13 @@ describe("$hydratedPosts (post hydration)", () => {
const postURI = "at://did:test/app.bsky.feed.post/x";
it("should return null when the post does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedPosts.get(postURI), null);
});
it("should mark the post when it contains a muted word", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({ postHasMutedWord: () => true }),
});
@@ -648,7 +649,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should not mark the post when there is no muted word match", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -658,7 +659,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should mark the post hidden when preferences say so", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({ isPostHidden: () => true }),
});
@@ -668,7 +669,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should attach badge, content, and media labels from preferences", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
getBadgeLabelsForPost: () => ["badge"],
@@ -684,7 +685,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should leave the post untouched when no labels apply", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -696,7 +697,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should compose muted/hidden/label marks on a single post", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
postHasMutedWord: () => true,
@@ -712,7 +713,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should synthesize a #blockedPost when the viewer blocks the author", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -737,7 +738,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should not re-synthesize a post that is already a #blockedPost", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -770,7 +771,7 @@ describe("$hydratedPosts (post hydration)", () => {
}
it("should keep a viewer-blocked quote as blocked when the quoted post is not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -786,7 +787,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should mark a viewer-blocked quote as deleted when the post is confirmed unavailable", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -807,7 +808,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should mark a blocked-by quote as deleted when the post is confirmed unavailable", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -825,7 +826,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should keep a viewer-blocked quote blocked even when the quoted post is loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -848,7 +849,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should resolve a third-party-blocked quote when the quoted post is loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -869,7 +870,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should keep the quote blocked when the quoted author blocks the viewer", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -889,7 +890,7 @@ describe("$hydratedPosts (post hydration)", () => {
});
it("should return the post unchanged when there is no blocked quote to resolve", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -907,7 +908,7 @@ describe("$convoForProfile", () => {
const members = [{ did: "did:plc:me" }, { did: profileDid }];
it("should return the direct convo containing the profile", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convos.set("direct1", {
id: "direct1",
members,
@@ -919,7 +920,7 @@ describe("$convoForProfile", () => {
});
it("should ignore group convos even with two members", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convos.set("group1", {
id: "group1",
members,
@@ -942,7 +943,7 @@ describe("$hydratedConvoMessages", () => {
const convoId = "convo-1";
it("should return null when the convo has no messages", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedConvoMessages.get(convoId), null);
});
@@ -958,7 +959,7 @@ describe("$hydratedConvoMessages", () => {
}
it("should pass through raw messages and cursor", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedMessages(
dataStore,
[
@@ -976,7 +977,7 @@ describe("$hydratedConvoMessages", () => {
});
it("should preserve replyTo when present on a message", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const replyTo = {
$type: "chat.bsky.convo.defs#messageView",
id: "m1",
@@ -1009,7 +1010,7 @@ describe("$hydratedConvoMessages", () => {
});
it("should drop reactions from senders the viewer is blocking or blocked by", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedConvoMembers(dataStore, [
{
did: "did:plc:blocked",
@@ -1037,7 +1038,7 @@ describe("$hydratedConvoMessages", () => {
});
it("should keep reactions from senders who are not convo members", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedConvoMembers(dataStore, [{ did: "did:plc:alice", viewer: {} }]);
seedMessages(dataStore, [
{
@@ -1053,7 +1054,7 @@ describe("$hydratedConvoMessages", () => {
});
it("should leave messages without reactions untouched", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedMessages(dataStore, [
{ id: "m1", sender: { did: "did:plc:alice" }, text: "hello" },
]);
@@ -1063,7 +1064,7 @@ describe("$hydratedConvoMessages", () => {
});
it("should not recompute when the convo changes but members stay the same", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const members = [{ did: "did:plc:alice", viewer: {} }];
dataStore.$convos.set(convoId, { id: convoId, members, unreadCount: 3 });
seedMessages(dataStore, [
@@ -1115,7 +1116,7 @@ describe("$hydratedDrafts", () => {
// `localMedia` seeds the store's $media: path -> { url } | null
function hydrateDraftPosts(draftViews, { localMedia = {} } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const draftMediaStore = new DraftMediaStore("test-media");
draftMediaStore.$media.set(localMedia);
const { derived } = makeDerived(dataStore, { draftMediaStore });
@@ -1124,7 +1125,7 @@ describe("$hydratedDrafts", () => {
}
it("is null before loading and carries the view fields and cursor through", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedDrafts.get(), null);
dataStore.$drafts.set({ drafts: [makeDraftView()], cursor: "next" });
@@ -1281,13 +1282,13 @@ describe("$groupConvoMemberList", () => {
const convoId = "convo1";
it("should return null when nothing is loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$groupConvoMemberList.get(convoId), null);
});
it("should pass through the stored members and cursor", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoMemberLists.set(convoId, {
members: [{ did: "did:plc:alice" }, { did: "did:plc:bob" }],
cursor: "2",
@@ -1323,7 +1324,7 @@ describe("$hydratedPosts (nested quotes and author labels)", () => {
}
it("should mark a quoted post that contains a muted word", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
quotedPostHasMutedWord: (quotedPost) => quotedPost.uri === quotedUri,
@@ -1336,7 +1337,7 @@ describe("$hydratedPosts (nested quotes and author labels)", () => {
});
it("should mark a quoted post that is hidden", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
isPostHidden: (uri) => uri === quotedUri,
@@ -1349,7 +1350,7 @@ describe("$hydratedPosts (nested quotes and author labels)", () => {
});
it("should attach a blur label to the post author", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
getProfileBlurLabel: (author) =>
@@ -1366,7 +1367,7 @@ describe("$hydratedPosts (nested quotes and author labels)", () => {
});
it("should keep a third-party-blocked quote blocked when the quoted post is not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -1406,7 +1407,7 @@ describe("$hydratedPosts (join link previews)", () => {
}
it("should attach a loaded join link preview to an invite link embed", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const preview = {
$type: "chat.bsky.group.defs#joinLinkPreviewView",
@@ -1421,7 +1422,7 @@ describe("$hydratedPosts (join link previews)", () => {
});
it("should leave the embed unchanged when no preview is loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set(postURI, makeInviteLinkPost());
const result = derived.$hydratedPosts.get(postURI);
@@ -1433,7 +1434,7 @@ describe("$hydratedFeeds (reason, replies, following feed)", () => {
const feedURI = "at://did:test/app.bsky.feed.generator/test";
it("should preserve the feed item reason", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const reason = {
$type: "app.bsky.feed.defs#reasonRepost",
@@ -1449,7 +1450,7 @@ describe("$hydratedFeeds (reason, replies, following feed)", () => {
});
it("should hydrate reply root and parent post views and pass through other reply nodes", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const rootPost = { uri: "root1", record: { text: "r" }, likeCount: 7 };
const parentPost = { uri: "parent1", record: { text: "p" }, likeCount: 3 };
@@ -1482,7 +1483,7 @@ describe("$hydratedFeeds (reason, replies, following feed)", () => {
});
it("should run the following feed through the following filter pipeline", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const post = createPost({
uri: "at://did:plc:author/app.bsky.feed.post/1",
@@ -1512,13 +1513,13 @@ describe("$notifications", () => {
}
it("should return null when notifications are not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$notifications.get(), null);
});
it("should attach the liked post as subject when it is loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set("post1", { uri: "post1", record: { text: "hi" } });
seedNotifications(dataStore, [
@@ -1530,7 +1531,7 @@ describe("$notifications", () => {
});
it("should attach an unavailable subject when the liked post is missing", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
seedNotifications(dataStore, [
createNotification({ reason: "repost", author, reasonSubject: "gone" }),
@@ -1540,7 +1541,7 @@ describe("$notifications", () => {
});
it("should resolve via-repost notifications from the record subject", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set("post1", { uri: "post1", record: { text: "hi" } });
seedNotifications(dataStore, [
@@ -1555,7 +1556,7 @@ describe("$notifications", () => {
});
it("should attach post and parent for reply notifications", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set("reply1", { uri: "reply1", record: { text: "r" } });
dataStore.$posts.set("parent1", { uri: "parent1", record: { text: "p" } });
@@ -1573,7 +1574,7 @@ describe("$notifications", () => {
});
it("should leave parentPost null for mentions without a reply parent", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set("m1", { uri: "m1", record: { text: "m" } });
seedNotifications(dataStore, [
@@ -1585,7 +1586,7 @@ describe("$notifications", () => {
});
it("should attach the post as reasonSubject for subscribed-post notifications", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set("s1", { uri: "s1", record: { text: "s" } });
seedNotifications(dataStore, [
@@ -1596,7 +1597,7 @@ describe("$notifications", () => {
});
it("should pass through notifications with other reasons unchanged", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const notification = createNotification({ reason: "follow", author });
seedNotifications(dataStore, [notification]);
@@ -1605,7 +1606,7 @@ describe("$notifications", () => {
});
it("should hydrate the author from the profile store", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore, {
preferences: fakePreferences({
getBadgeLabelsForProfile: () => ["verified"],
@@ -1621,7 +1622,7 @@ describe("$notifications", () => {
});
it("should override isRead using the captured seenAt", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
seedNotifications(dataStore, [
createNotification({
@@ -1646,7 +1647,7 @@ describe("$notifications", () => {
});
it("should treat a notification indexed exactly at seenAt as read", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
seedNotifications(dataStore, [
createNotification({
@@ -1661,7 +1662,7 @@ describe("$notifications", () => {
});
it("should trust the server isRead when seenAt is null", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
seedNotifications(dataStore, [
createNotification({ reason: "follow", author, uri: "r", isRead: true }),
@@ -1673,7 +1674,7 @@ describe("$notifications", () => {
});
it("should filter out notifications from authors the viewer is blocking", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const blocked = createProfile({
did: "did:plc:blocked",
@@ -1690,7 +1691,7 @@ describe("$notifications", () => {
});
it("should filter out notifications from muted authors the viewer does not follow", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const muted = createProfile({
did: "did:plc:muted",
@@ -1707,7 +1708,7 @@ describe("$notifications", () => {
});
it("should keep notifications from muted authors when the viewer follows them", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const mutedFollow = createProfile({
did: "did:plc:mutedfollow",
@@ -1726,7 +1727,7 @@ describe("$notifications", () => {
});
it("should recompute isRead when seenAt is captured later", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
seedNotifications(dataStore, [
createNotification({
@@ -1744,13 +1745,13 @@ describe("$notifications", () => {
describe("$mentionNotifications", () => {
it("should return null when mention notifications are not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$mentionNotifications.get(), null);
});
it("should hydrate mention notifications", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const author = createProfile({ did: "did:plc:a", handle: "a.test" });
dataStore.setProfiles([author]);
@@ -1772,7 +1773,7 @@ describe("$mentionNotifications", () => {
});
it("should override isRead using the shared seenAt from the main feed", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const author = createProfile({ did: "did:plc:a", handle: "a.test" });
dataStore.setProfiles([author]);
@@ -1796,7 +1797,7 @@ describe("$hydratedPostThreads", () => {
const threadUri = "post1";
it("should return null when the thread or its hidden replies are not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedPostThreads.get(threadUri), null);
dataStore.$postThreads.set(threadUri, {
@@ -1808,7 +1809,7 @@ describe("$hydratedPostThreads", () => {
});
it("should pass through an empty (not found) thread", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const notFound = {
$type: "app.bsky.feed.defs#notFoundPost",
@@ -1820,7 +1821,7 @@ describe("$hydratedPostThreads", () => {
});
it("should return null when the thread root post is not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$postThreads.set(threadUri, {
$type: "app.bsky.feed.defs#threadViewPost",
@@ -1831,7 +1832,7 @@ describe("$hydratedPostThreads", () => {
});
it("should hydrate replies, marking hidden ones and passing through non-post replies", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const blockedReply = {
$type: "app.bsky.feed.defs#blockedPost",
@@ -1876,7 +1877,7 @@ describe("$hydratedPostThreads", () => {
}
it("should hydrate a parent chain recursively", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const grandparent = { $type: "app.bsky.feed.defs#notFoundPost", uri: "gp" };
dataStore.$posts.set("pp", { uri: "pp", record: { text: "parent" } });
@@ -1892,7 +1893,7 @@ describe("$hydratedPostThreads", () => {
});
it("should replace a confirmed-unavailable parent with an unavailable post", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$unavailablePosts.set("pp", createUnavailablePost("pp"));
seedThreadWithParent(dataStore, {
@@ -1906,7 +1907,7 @@ describe("$hydratedPostThreads", () => {
});
it("should keep a blocked parent as-is when the parent author blocks the viewer", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const blockedParent = {
$type: "app.bsky.feed.defs#blockedPost",
@@ -1928,7 +1929,7 @@ describe("actor search results", () => {
}
it("$profileSearchResults should be null before a search and hydrate after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$profileSearchResults.get(), null);
assert.deepEqual(derived.$profileSearchCursor.get(), null);
@@ -1944,7 +1945,7 @@ describe("actor search results", () => {
});
it("$chatRecipientSearchResults should be null before a search and hydrate after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$chatRecipientSearchResults.get(), null);
seedProfile(dataStore);
@@ -1954,7 +1955,7 @@ describe("actor search results", () => {
});
it("$searchTypeaheadResults should be null before a search and hydrate after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$searchTypeaheadResults.get(), null);
seedProfile(dataStore);
@@ -1966,7 +1967,7 @@ describe("actor search results", () => {
describe("$feedSearchResults", () => {
it("should be null before a search and pass feeds through after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$feedSearchResults.get(), null);
assert.deepEqual(derived.$feedSearchCursor.get(), null);
@@ -1979,7 +1980,7 @@ describe("$feedSearchResults", () => {
describe("post search results", () => {
it("should be null before a search and hydrate loaded posts after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$postSearchResultsTop.get(), null);
assert.deepEqual(derived.$postSearchResultsLatest.get(), null);
@@ -2008,13 +2009,13 @@ describe("$hydratedPostQuotes", () => {
const postUri = "post1";
it("should return null when quotes are not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedPostQuotes.get(postUri), null);
});
it("should hydrate loaded quote posts and skip missing ones", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
dataStore.$posts.set("quote1", { uri: "quote1", record: { text: "q" } });
dataStore.$postQuotes.set(postUri, {
@@ -2032,13 +2033,13 @@ describe("$listMembers", () => {
const listUri = "at://did:plc:owner/app.bsky.graph.list/1";
it("should return null when list members are not loaded", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$listMembers.get(listUri), null);
});
it("should hydrate member profiles and carry the cursor", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
const did = "did:plc:member";
dataStore.setProfiles([createProfile({ did, handle: "member.test" })]);
@@ -2057,7 +2058,7 @@ describe("$hydratedProfiles (labels)", () => {
const did = "did:plc:user";
it("should attach a blur label from preferences", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({ getProfileBlurLabel: () => "adult" }),
});
@@ -2067,7 +2068,7 @@ describe("$hydratedProfiles (labels)", () => {
});
it("should attach badge labels from preferences", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
getBadgeLabelsForProfile: () => ["verified"],
@@ -2079,7 +2080,7 @@ describe("$hydratedProfiles (labels)", () => {
});
it("should return the profile unchanged when no labels apply", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -2100,7 +2101,7 @@ describe("$mutedProfiles and $blockedProfiles", () => {
}
it("should attach badge labels to muted profiles", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
getBadgeLabelsForProfile: () => ["verified"],
@@ -2113,7 +2114,7 @@ describe("$mutedProfiles and $blockedProfiles", () => {
});
it("should attach badge labels to blocked profiles", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
getBadgeLabelsForProfile: () => ["verified"],
@@ -2127,7 +2128,7 @@ describe("$mutedProfiles and $blockedProfiles", () => {
});
it("should return the lists unchanged when no labels apply", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -2138,7 +2139,7 @@ describe("$mutedProfiles and $blockedProfiles", () => {
});
it("should reflect profile patches in the list", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -2153,13 +2154,13 @@ describe("$hydratedDetailedProfiles", () => {
const did = "did:plc:user";
it("should return null when the detailed profile does not exist", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$hydratedDetailedProfiles.get(did), null);
});
it("should return the profile unchanged when no labels apply", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
});
@@ -2174,7 +2175,7 @@ describe("$hydratedDetailedProfiles", () => {
});
it("should attach blur and badge labels from preferences", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
getProfileBlurLabel: () => "adult",
@@ -2216,14 +2217,14 @@ describe("$convoList and $convoRequestList", () => {
}
it("$convoList should be null before loading", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$convoList.get(), null);
assert.deepEqual(derived.$convoListCursor.get(), null);
});
it("$convoList should sort convos by last interaction, newest first", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const older = makeConvoWithMessage("older", "2026-01-01T00:00:00.000Z");
const newer = makeConvoWithMessage("newer", "2026-01-05T00:00:00.000Z");
seedConvos(dataStore, [older, newer]);
@@ -2241,14 +2242,14 @@ describe("$convoList and $convoRequestList", () => {
});
it("$convoRequestList should be null before loading", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$convoRequestList.get(), null);
assert.deepEqual(derived.$convoRequestListCursor.get(), null);
});
it("$convoRequestList should drop unknown convos and sort the rest", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const older = makeConvoWithMessage(
"older",
"2026-01-01T00:00:00.000Z",
@@ -2283,7 +2284,7 @@ describe("$convoProfiles (labels)", () => {
}
it("should attach badge labels to convo members", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedConvo(dataStore);
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({
@@ -2295,7 +2296,7 @@ describe("$convoProfiles (labels)", () => {
});
it("should attach a blur label to convo members", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedConvo(dataStore);
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences({ getProfileBlurLabel: () => "adult" }),
@@ -2305,7 +2306,7 @@ describe("$convoProfiles (labels)", () => {
});
it("should return members unchanged when no labels apply", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
seedConvo(dataStore);
const { derived } = makeDerived(dataStore, {
preferences: fakePreferences(),
@@ -2326,7 +2327,7 @@ describe("interaction and graph list hydration", () => {
}
it("$postLikes should be null before loading and hydrate actors after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$postLikes.get("post1"), null);
seedActor(dataStore);
@@ -2341,7 +2342,7 @@ describe("interaction and graph list hydration", () => {
});
it("$postReposts should be null before loading and hydrate actors after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$postReposts.get("post1"), null);
seedActor(dataStore);
@@ -2355,7 +2356,7 @@ describe("interaction and graph list hydration", () => {
});
it("$profileFollows should be null before loading and hydrate actors after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$profileFollows.get("did:plc:subject"), null);
seedActor(dataStore);
@@ -2369,7 +2370,7 @@ describe("interaction and graph list hydration", () => {
});
it("$profileFollowers should be null before loading and hydrate actors after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$profileFollowers.get("did:plc:subject"), null);
seedActor(dataStore);
@@ -2383,7 +2384,7 @@ describe("interaction and graph list hydration", () => {
});
it("$knownFollowers should be null before loading and hydrate actors after", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$knownFollowers.get("did:plc:subject"), null);
seedActor(dataStore);
@@ -2401,13 +2402,13 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => {
const did = "did:test:pending";
it("$isFollowPending is false when no follow/unfollow patch is pending", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived } = makeDerived(dataStore);
assert.deepEqual(derived.$isFollowPending.get(did), false);
});
it("$isFollowPending flips true while a followProfile patch is pending", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const patchId = patchStore.addProfilePatch(did, { type: "followProfile" });
assert.deepEqual(derived.$isFollowPending.get(did), true);
@@ -2416,14 +2417,14 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => {
});
it("$isFollowPending flips true while an unfollowProfile patch is pending", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
patchStore.addProfilePatch(did, { type: "unfollowProfile" });
assert.deepEqual(derived.$isFollowPending.get(did), true);
});
it("$isFollowPending ignores unrelated profile patches", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
patchStore.addProfilePatch(did, { type: "muteProfile" });
patchStore.addProfilePatch(did, { type: "blockProfile" });
@@ -2431,7 +2432,7 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => {
});
it("$isBlockPending covers blockProfile and unblockProfile", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const blockId = patchStore.addProfilePatch(did, { type: "blockProfile" });
assert.deepEqual(derived.$isBlockPending.get(did), true);
@@ -2441,7 +2442,7 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => {
});
it("$isMutePending covers muteProfile and unmuteProfile", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
const muteId = patchStore.addProfilePatch(did, { type: "muteProfile" });
assert.deepEqual(derived.$isMutePending.get(did), true);
@@ -2451,7 +2452,7 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => {
});
it("pending signals are keyed per profile", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
patchStore.addProfilePatch(did, { type: "followProfile" });
assert.deepEqual(derived.$isFollowPending.get(did), true);
diff --git a/tests/unit/specs/dataLayer/mutations.test.js b/tests/unit/specs/dataLayer/mutations.test.js
index 1d2be3a8..532cdd8f 100644
--- a/tests/unit/specs/dataLayer/mutations.test.js
+++ b/tests/unit/specs/dataLayer/mutations.test.js
@@ -2,6 +2,7 @@ import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { Mutations } from "/js/dataLayer/mutations.js";
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
import { DraftMediaStore } from "/js/drafts.js";
import { PatchStore } from "/js/dataLayer/patchStore.js";
import { Derived } from "/js/dataLayer/derived.js";
@@ -75,7 +76,7 @@ describe("addLike", () => {
const mockApi = {
createLikeRecord: async () => ({ uri: "like-uri" }),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -99,7 +100,7 @@ describe("addLike", () => {
const mockApi = {
createLikeRecord: async () => mockLike,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -128,7 +129,7 @@ describe("addLike", () => {
setTimeout(() => resolve({ uri: "like-uri" }), 50),
),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -152,7 +153,7 @@ describe("addLike", () => {
});
it("should not double-count when a refresh lands while the like is in flight", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockApi = {
createLikeRecord: async () => {
@@ -199,7 +200,7 @@ describe("removeLike", () => {
setTimeout(resolve, 100);
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -222,7 +223,7 @@ describe("removeLike", () => {
const mockApi = {
deleteLikeRecord: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -261,7 +262,7 @@ describe("followProfile", () => {
setTimeout(() => resolve({ uri: "follow-uri" }), 100);
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -285,7 +286,7 @@ describe("followProfile", () => {
const mockApi = {
createFollowRecord: async () => mockFollow,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$detailedProfiles.set(testProfile.did, testProfile);
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
@@ -328,7 +329,7 @@ describe("unfollowProfile", () => {
setTimeout(resolve, 100);
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -351,7 +352,7 @@ describe("unfollowProfile", () => {
const mockApi = {
deleteFollowRecord: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$detailedProfiles.set(testProfile.did, testProfile);
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
@@ -397,7 +398,7 @@ describe("subscribeLabeler", () => {
await new Promise((resolve) => setTimeout(resolve, 100));
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -421,7 +422,7 @@ describe("subscribeLabeler", () => {
}),
updatePreferences: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -445,7 +446,7 @@ describe("subscribeLabeler", () => {
throw new Error("API error");
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -482,7 +483,7 @@ describe("unsubscribeLabeler", () => {
await new Promise((resolve) => setTimeout(resolve, 100));
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -506,7 +507,7 @@ describe("unsubscribeLabeler", () => {
}),
updatePreferences: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -530,7 +531,7 @@ describe("unsubscribeLabeler", () => {
throw new Error("API error");
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -566,7 +567,7 @@ describe("updateLabelerSetting", () => {
await new Promise((resolve) => setTimeout(resolve, 100));
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -592,7 +593,7 @@ describe("updateLabelerSetting", () => {
}),
updatePreferences: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -616,7 +617,7 @@ describe("updateLabelerSetting", () => {
throw new Error("API error");
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -648,7 +649,7 @@ describe("updateLabelerSetting", () => {
}),
updatePreferences: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -681,7 +682,7 @@ describe("Error Handling and Edge Cases", () => {
deleteLikeRecord: async () =>
new Promise((resolve) => setTimeout(resolve, 75)),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -714,7 +715,7 @@ describe("Error Handling and Edge Cases", () => {
const mockApi = {
deleteLikeRecord: async () => undefined,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -742,7 +743,7 @@ describe("addMutedWord", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -772,7 +773,7 @@ describe("addMutedWord", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -826,7 +827,7 @@ describe("removeMutedWord", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -854,7 +855,7 @@ describe("updateProfile", () => {
};
function createMutationsWithMockApi(mockApi) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1084,7 +1085,7 @@ describe("pinPost", () => {
};
function setup(mockApi, { pinnedPost = null, authorFeed = null } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1258,7 +1259,7 @@ describe("unpinPost", () => {
};
function setup(mockApi, { pinnedPost, authorFeed = null } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1339,7 +1340,7 @@ describe("muteProfile", () => {
};
function setup(mockApi = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1400,7 +1401,7 @@ describe("unmuteProfile", () => {
};
function setup(mockApi = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1456,7 +1457,7 @@ describe("blockProfile", () => {
const blockUri = "at://did:plc:me/app.bsky.graph.block/123";
function setup(mockApi = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1548,7 +1549,7 @@ describe("unblockProfile", () => {
};
function setup(mockApi = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1628,7 +1629,7 @@ describe("addBookmark", () => {
};
function setup(mockApi = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1692,7 +1693,7 @@ describe("removeBookmark", () => {
};
function setup(mockApi = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1759,7 +1760,7 @@ describe("createRepost", () => {
};
function setup(mockApi = {}, { authorFeed } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1865,7 +1866,7 @@ describe("deleteRepost", () => {
};
function setup(mockApi = {}, { authorFeed } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -1941,7 +1942,7 @@ describe("pinFeed", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2005,7 +2006,7 @@ describe("pinFeed", () => {
requirePreferences: () => preferences,
updatePreferences: () => updatePromise,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2046,7 +2047,7 @@ describe("unpinFeed", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2078,7 +2079,7 @@ describe("unpinFeed", () => {
requirePreferences: () => preferences,
updatePreferences: () => updatePromise,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2136,7 +2137,7 @@ describe("setPinnedItems", () => {
await updatePromise;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
if (preloadPinnedItems) {
dataStore.$pinnedItems.set([
{ type: "timeline", data: { uri: "following" } },
@@ -2210,7 +2211,7 @@ describe("setPinnedItems", () => {
throw error;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$pinnedItems.set([
{ type: "feed", data: { uri: feedA } },
{ type: "list", data: { uri: listA } },
@@ -2249,7 +2250,7 @@ describe("hidePost", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2273,7 +2274,7 @@ describe("hidePost", () => {
requirePreferences: () => preferences,
updatePreferences: () => updatePromise,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2321,7 +2322,7 @@ describe("updateMutedWord", () => {
updatedPreferences = prefs;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -2352,7 +2353,7 @@ describe("updatePostNotificationSubscription", () => {
it("should set viewer.activitySubscription on the profile", async () => {
const subscription = { post: true, reply: false };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2380,7 +2381,7 @@ describe("updatePostNotificationSubscription", () => {
});
it("should remove the patch on failure and rethrow", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2414,7 +2415,7 @@ describe("createThread", () => {
const newPostUri = `at://${currentUserDid}/app.bsky.feed.post/new`;
function setup({ replyPostThread, authorFeed, replyAuthorFeed } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2537,7 +2538,7 @@ describe("deletePost", () => {
cid: "cid-abc",
};
let apiCalledWith = null;
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2572,7 +2573,7 @@ describe("createMessage", () => {
};
function setup({ convoMessages, convo } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2626,7 +2627,7 @@ describe("createMessage", () => {
it("should pass replyTo to the api", async () => {
let apiCalledWith = null;
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2651,7 +2652,7 @@ describe("createMessage", () => {
it("should pass embed to the api", async () => {
let apiCalledWith = null;
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2676,7 +2677,7 @@ describe("createMessage", () => {
});
it("should propagate the raw error on send failure", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2705,7 +2706,7 @@ describe("acceptConvo", () => {
const convo = { id: "convo-1", status: "request" };
function setup({ convoList, convoRequestList } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2793,7 +2794,7 @@ describe("rejectConvo", () => {
it("should clear the convo, call api.leaveConvo, and remove it from the request list only", async () => {
const otherAccepted = { id: "convo-2", status: "accepted" };
const otherRequest = { id: "convo-3", status: "request" };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2841,7 +2842,7 @@ describe("leaveConvo", () => {
it("should clear the convo, call api.leaveConvo, and remove it from the accepted list only", async () => {
const otherAccepted = { id: "convo-2", status: "accepted" };
const otherRequest = { id: "convo-3", status: "request" };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2884,7 +2885,7 @@ describe("leaveConvo", () => {
it("should leave the store unchanged when api.leaveConvo throws", async () => {
const otherAccepted = { id: "convo-2", status: "accepted" };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2918,7 +2919,7 @@ describe("setConvoMuted", () => {
const convo = { id: "convo-1", muted: false };
it("should optimistically patch, then write to dataStore and clear the patch on success", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2950,7 +2951,7 @@ describe("setConvoMuted", () => {
it("should call api.unmuteConvo when muted=false", async () => {
const mutedConvo = { id: "convo-1", muted: true };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -2975,7 +2976,7 @@ describe("setConvoMuted", () => {
});
it("should revert the optimistic patch and leave the dataStore unchanged when the api throws", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3000,7 +3001,7 @@ describe("setConvoMuted", () => {
});
it("should not write to dataStore if the underlying convo was cleared during the api call", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3027,7 +3028,7 @@ describe("setConvoMuted", () => {
describe("markConvoAsRead", () => {
it("should call api.markConvoAsRead and zero the unread count", async () => {
const convoId = "convo-1";
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3052,7 +3053,7 @@ describe("markConvoAsRead", () => {
});
it("should not throw when the convo is not cached", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3070,7 +3071,7 @@ describe("markConvoAsRead", () => {
it("should not call the api when the convo has no unread messages", async () => {
const convoId = "convo-read";
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3105,7 +3106,7 @@ describe("addMessageReaction", () => {
};
function setup({ convo } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3172,7 +3173,7 @@ describe("removeMessageReaction", () => {
const updatedMessage = { id: messageId, reactions: [] };
function setup({ convo } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3235,7 +3236,7 @@ describe("sendShowLessInteraction", () => {
const feedProxyUrl = "https://feed.example/xrpc";
it("should append the interaction to the dataStore (empty list branch)", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3270,7 +3271,7 @@ describe("sendShowLessInteraction", () => {
});
it("should append to an existing list (non-empty branch)", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3298,7 +3299,7 @@ describe("sendShowLessInteraction", () => {
});
it("should key stored interactions by feed", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3323,7 +3324,7 @@ describe("sendShowLessInteraction", () => {
});
it("should omit feedContext when null but keep an empty string", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3359,7 +3360,7 @@ describe("sendShowLessInteraction", () => {
});
it("should store but not send when there is no feed proxy url", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3390,7 +3391,7 @@ describe("sendShowMoreInteraction", () => {
const feedProxyUrl = "https://feed.example/xrpc";
it("should append the interaction to the dataStore (empty list branch)", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3424,7 +3425,7 @@ describe("sendShowMoreInteraction", () => {
});
it("should append to an existing list (non-empty branch)", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3476,7 +3477,7 @@ describe("pinList", () => {
updatePreferences: async () =>
new Promise((resolve) => setTimeout(resolve, 100)),
});
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations({}, dataStore, patchStore, provider);
@@ -3491,7 +3492,7 @@ describe("pinList", () => {
it("should call preferences.pinFeed with type 'list'", async () => {
const { provider, pinFeedCalls } = makeMockProvider();
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations({}, dataStore, patchStore, provider);
@@ -3504,7 +3505,7 @@ describe("pinList", () => {
it("should remove patch after successful update", async () => {
const { provider } = makeMockProvider();
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations({}, dataStore, patchStore, provider);
@@ -3519,7 +3520,7 @@ describe("pinList", () => {
throw new Error("API error");
},
});
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations({}, dataStore, patchStore, provider);
@@ -3547,7 +3548,7 @@ describe("pinFeed entryType", () => {
updatePreferences: () =>
new Promise((resolve) => setTimeout(resolve, 100)),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations(
{},
@@ -3578,7 +3579,7 @@ describe("unpinList", () => {
}),
updatePreferences: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations({}, dataStore, patchStore, provider);
@@ -3599,7 +3600,7 @@ describe("unpinList", () => {
}),
updatePreferences: () => updatePromise,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mutations = makeMutations({}, dataStore, patchStore, provider);
@@ -3629,7 +3630,7 @@ describe("addProfileToList", () => {
const mockApi = {
createListItemRecord: async () => ({ uri: "listitem-real-uri" }),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3664,7 +3665,7 @@ describe("addProfileToList", () => {
const mockApi = {
createListItemRecord: async () => ({ uri: "listitem-real-uri" }),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3688,7 +3689,7 @@ describe("addProfileToList", () => {
const mockApi = {
createListItemRecord: async () => ({ uri: "listitem-real-uri" }),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listMembers.set(testList.uri, {
items: [
{
@@ -3723,7 +3724,7 @@ describe("addProfileToList", () => {
throw new Error("nope");
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3764,7 +3765,7 @@ describe("removeProfileFromList", () => {
const mockApi = {
deleteListItemRecord: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listsWithMembershipByActor.set(testProfile.did, {
listsWithMembership: [
{
@@ -3795,7 +3796,7 @@ describe("removeProfileFromList", () => {
const mockApi = {
deleteListItemRecord: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3819,7 +3820,7 @@ describe("removeProfileFromList", () => {
const mockApi = {
deleteListItemRecord: async () => {},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listsWithMembershipByActor.set(testProfile.did, {
listsWithMembership: [
{
@@ -3866,7 +3867,7 @@ describe("removeProfileFromList", () => {
throw new Error("boom");
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const initialListItem = { uri: membershipUri, subject: testProfile.did };
dataStore.$listsWithMembershipByActor.set(testProfile.did, {
listsWithMembership: [{ list: testList, listItem: initialListItem }],
@@ -3916,7 +3917,7 @@ describe("$detailedProfiles mirroring", () => {
};
function setup(mockApi, { seedDetailed = true } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const preferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -3981,7 +3982,7 @@ describe("$detailedProfiles mirroring", () => {
it("unfollowProfile mirrors viewer.following=null and decremented count", async () => {
const seedFollowed = { ...detailedSeed, viewer: { following: "at://x" } };
const mockApi = { deleteFollowRecord: async () => {} };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const preferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -4017,7 +4018,7 @@ describe("$detailedProfiles mirroring", () => {
});
it("unmuteProfile mirrors viewer.muted=false into $detailedProfiles", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const preferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -4058,7 +4059,7 @@ describe("$detailedProfiles mirroring", () => {
});
it("unblockProfile mirrors viewer.blocking=null into $detailedProfiles", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const preferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -4116,7 +4117,7 @@ describe("updateList", () => {
};
function setup(overrides = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const preferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -4344,7 +4345,7 @@ describe("deleteList", () => {
};
function setup({ listItems = [], overrides = {} } = {}) {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const preferences = Preferences.createLoggedOutPreferences();
const preferencesProvider = {
@@ -4531,7 +4532,7 @@ describe("deleteList", () => {
});
it("unpins the list if it was pinned", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
let preferences = Preferences.createLoggedOutPreferences().pinFeed(
listUri,
@@ -4564,3 +4565,14 @@ describe("deleteList", () => {
assert.equal(preferences.isFeedPinned(listUri), false);
});
});
+
+describe("setSelectedFeedUri", () => {
+ it("should set the selection", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ const mutations = makeMutations({}, dataStore, new PatchStore(), {});
+ mutations.setSelectedFeedUri("following");
+ assert.deepEqual(dataStore.$selectedFeedUri.get(), "following");
+ mutations.setSelectedFeedUri(null);
+ assert.deepEqual(dataStore.$selectedFeedUri.get(), null);
+ });
+});
diff --git a/tests/unit/specs/dataLayer/patchStore.test.js b/tests/unit/specs/dataLayer/patchStore.test.js
index 435efaa2..ab8ce895 100644
--- a/tests/unit/specs/dataLayer/patchStore.test.js
+++ b/tests/unit/specs/dataLayer/patchStore.test.js
@@ -2,6 +2,7 @@ import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { PatchStore } from "/js/dataLayer/patchStore.js";
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
// applyPostPatches now requires the patches array explicitly. This helper
// fetches the current patches for a post URI and applies them.
@@ -1055,7 +1056,7 @@ describe("Message Patches", () => {
});
it("should expose the overlay via $patchedMessages", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
dataStore.$messages.set(messageId, baseMessage);
@@ -1073,7 +1074,7 @@ describe("Message Patches", () => {
});
it("should return null from $patchedMessages when the underlying message is absent", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
assert.deepEqual(patchStore.$patchedMessages.get("missing"), null);
});
@@ -1130,7 +1131,7 @@ describe("Author Feed Patches - pinPost apply", () => {
describe("Post Patches - $patchedPosts overlay", () => {
it("should overlay post + author profile patches", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const postURI = "at://did:plc:author/app.bsky.feed.post/1";
const post = {
@@ -1159,7 +1160,7 @@ describe("Post Patches - $patchedPosts overlay", () => {
});
it("should return null when the underlying post is absent", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
assert.deepEqual(patchStore.$patchedPosts.get("missing"), null);
});
@@ -1169,7 +1170,7 @@ describe("Profile Patches - $patchedProfiles / $patchedDetailedProfiles overlays
const did = "did:plc:x";
it("should overlay profile patches on $profiles reads", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
dataStore.$profiles.set(did, {
did,
@@ -1185,7 +1186,7 @@ describe("Profile Patches - $patchedProfiles / $patchedDetailedProfiles overlays
});
it("should overlay profile patches on $detailedProfiles reads", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
dataStore.$detailedProfiles.set(did, {
did,
@@ -1200,7 +1201,7 @@ describe("Profile Patches - $patchedProfiles / $patchedDetailedProfiles overlays
});
it("should return null when the underlying profile is absent", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
assert.deepEqual(patchStore.$patchedProfiles.get("missing"), null);
assert.deepEqual(patchStore.$patchedDetailedProfiles.get("missing"), null);
@@ -1276,7 +1277,7 @@ describe("Post Patches - convergence", () => {
});
it("should not double-apply when a canonical refresh lands while the patch is installed", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
dataStore.$posts.set(postURI, {
uri: postURI,
@@ -1387,7 +1388,7 @@ describe("Message Patches - convergence", () => {
describe("Convo Patches - $patchedConvos", () => {
it("should overlay the patch on top of the dataStore convo", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
const convo = { id: "convo-1", muted: false, rev: "rev-1" };
dataStore.$convos.set("convo-1", convo);
@@ -1406,13 +1407,13 @@ describe("Convo Patches - $patchedConvos", () => {
});
it("should return null when the underlying convo is absent", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
assert.deepEqual(patchStore.$patchedConvos.get("nope"), null);
});
it("should isolate patches between different convos", () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const patchStore = new PatchStore(dataStore);
dataStore.$convos.set("convo-1", { id: "convo-1", muted: false });
dataStore.$convos.set("convo-2", { id: "convo-2", muted: false });
diff --git a/tests/unit/specs/dataLayer/requests.test.js b/tests/unit/specs/dataLayer/requests.test.js
index 524153e6..bf67e4a0 100644
--- a/tests/unit/specs/dataLayer/requests.test.js
+++ b/tests/unit/specs/dataLayer/requests.test.js
@@ -2,6 +2,7 @@ import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { Requests } from "/js/dataLayer/requests.js";
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
import { DraftMediaStore } from "/js/drafts.js";
import { Preferences } from "/js/preferences.js";
import { ApiError } from "/js/api.js";
@@ -46,7 +47,7 @@ describe("loadPostThread", () => {
getPostThreadOther: async () => mockPostThreadOther,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -85,7 +86,7 @@ describe("loadPostThread", () => {
getPostThreadOther: async () => [],
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -118,7 +119,7 @@ describe("loadNextFeedPage", () => {
getFeed: async () => mockFeed,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -139,7 +140,7 @@ describe("loadNextFeedPage", () => {
});
it("should append to existing feed", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
// Set up existing feed
const existingFeed = {
@@ -185,7 +186,7 @@ describe("loadNextFeedPage", () => {
});
it("should discard a stale page when a reload lands mid-flight", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$feeds.set(feedURI, {
feed: [{ post: { uri: "post1" } }],
cursor: "cursor1",
@@ -218,7 +219,7 @@ describe("loadNextFeedPage", () => {
});
it("should emit feedLoaded with the reload flag", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$feeds.set(feedURI, {
feed: [{ post: { uri: "post1" } }],
cursor: "cursor1",
@@ -256,7 +257,7 @@ describe("loadNextFeedPage", () => {
getFeed: async () => emptyFeed,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -295,7 +296,7 @@ describe("loadNextFeedPage", () => {
getFeed: async () => feedWithReplies,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -332,7 +333,7 @@ describe("loadDetailedProfile", () => {
getProfile: async () => mockProfile,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -350,7 +351,7 @@ describe("loadDetailedProfile", () => {
});
it("should handle profile updates", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
// Load initial profile
const initialProfile = {
@@ -404,7 +405,7 @@ describe("loadPosts", () => {
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -430,7 +431,7 @@ describe("loadPosts", () => {
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -464,7 +465,7 @@ describe("loadLabelerInfo", () => {
getLabeler: async () => mockLabelerInfo,
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -488,7 +489,7 @@ describe("loadLabelerInfo", () => {
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -515,7 +516,7 @@ describe("loadLabelerInfo", () => {
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -547,7 +548,7 @@ describe("loadMutedProfiles", () => {
cursor: "next",
};
const mockApi = { getMutes: async () => res };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -569,7 +570,7 @@ describe("loadMutedProfiles", () => {
});
it("should append paginated muted profiles when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$mutedProfiles.set({
mutes: [{ did: "did:plc:a" }],
cursor: "page2",
@@ -606,7 +607,7 @@ describe("loadMutedProfiles", () => {
return { mutes: [], cursor: undefined };
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$mutedProfiles.set({ mutes: [], cursor: "abc" });
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
@@ -622,7 +623,7 @@ describe("loadMutedProfiles", () => {
});
it("should discard the response when the cursor no longer matches", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const existing = {
mutes: [{ did: "did:plc:a" }],
cursor: "page3",
@@ -650,7 +651,11 @@ describe("loadMutedProfiles", () => {
});
});
-function makeRequests(api, dataStore = new DataStore(), preferences) {
+function makeRequests(
+ api,
+ dataStore = new DataStore(createSessionState(null)),
+ preferences,
+) {
const provider = {
requirePreferences: () =>
preferences ?? Preferences.createLoggedOutPreferences(),
@@ -665,7 +670,7 @@ describe("loadBlockedProfiles", () => {
cursor: "next",
};
const mockApi = { getBlocks: async () => res };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadBlockedProfiles();
@@ -680,7 +685,7 @@ describe("loadBlockedProfiles", () => {
});
it("should append paginated blocked profiles when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$blockedProfiles.set({
blocks: [{ did: "did:plc:a" }],
cursor: "page2",
@@ -710,7 +715,7 @@ describe("loadBlockedProfiles", () => {
return { blocks: [], cursor: undefined };
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$blockedProfiles.set({ blocks: [], cursor: "abc" });
const requests = makeRequests(mockApi, dataStore);
@@ -730,7 +735,7 @@ describe("loadNextAuthorFeedPage", () => {
return { feed: [{ post: { uri: "p1" } }], cursor: "c1" };
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadNextAuthorFeedPage(did, "posts");
@@ -797,7 +802,7 @@ describe("loadNextAuthorFeedPage", () => {
it("should append to existing feed", async () => {
const feedURI = `${did}-posts`;
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$authorFeeds.set(feedURI, {
feed: [{ post: { uri: "old1" } }],
cursor: "c1",
@@ -822,7 +827,7 @@ describe("loadNextAuthorFeedPage", () => {
it("should reset cursor and replace feed on reload", async () => {
const feedURI = `${did}-posts`;
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$authorFeeds.set(feedURI, {
feed: [{ post: { uri: "old1" } }],
cursor: "c1",
@@ -861,7 +866,7 @@ describe("loadNextAuthorFeedPage", () => {
describe("loadPostSearchTop / loadPostSearchLatest", () => {
it("should clear results for both sorts when query is empty", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$postSearchResultsTop.set({
posts: [{ uri: "p1" }],
cursor: "c1",
@@ -887,7 +892,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
cursor: "next",
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadPostSearchTop("hello");
@@ -904,7 +909,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
cursor: null,
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadPostSearchTop("hello");
@@ -921,7 +926,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
});
it("should not discard an in-flight sort when the other sort loads", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveTop;
const topPromise = new Promise((resolve) => {
resolveTop = resolve;
@@ -954,7 +959,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
});
it("should discard stale responses based on requestTime guard", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveFirst;
const firstPromise = new Promise((resolve) => {
resolveFirst = resolve;
@@ -984,7 +989,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
});
it("should discard a stale cursored response that finishes dependency loading after a re-search", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const replyPost = (uri) => ({
uri,
record: { reply: { parent: { uri: `${uri}-parent` } } },
@@ -1028,7 +1033,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
});
it("should append when cursor is provided and existing results present", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$postSearchResultsTop.set({
posts: [{ uri: "p1", record: {} }],
cursor: "c1",
@@ -1052,7 +1057,7 @@ describe("loadPostSearchTop / loadPostSearchLatest", () => {
describe("loadProfileSearch", () => {
it("should clear results when query is empty", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$profileSearchResults.set({
actors: [{ did: "x" }],
cursor: "c",
@@ -1074,7 +1079,7 @@ describe("loadProfileSearch", () => {
cursor: "next",
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadProfileSearch("alice");
@@ -1085,7 +1090,7 @@ describe("loadProfileSearch", () => {
});
it("should discard stale responses", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveFirst;
const firstPromise = new Promise((resolve) => {
resolveFirst = resolve;
@@ -1114,7 +1119,7 @@ describe("loadProfileSearch", () => {
});
it("should append when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$profileSearchResults.set({
actors: [{ did: "did:plc:a" }],
cursor: "c1",
@@ -1135,7 +1140,7 @@ describe("loadProfileSearch", () => {
});
it("should discard in-flight responses after the query is cleared", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveSearch;
const searchPromise = new Promise((resolve) => {
resolveSearch = resolve;
@@ -1159,7 +1164,7 @@ describe("loadProfileSearch", () => {
describe("loadChatRecipientSearch", () => {
it("should store the search results", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchProfilesTypeahead: async () => ({
actors: [{ did: "did:plc:a" }],
@@ -1175,7 +1180,7 @@ describe("loadChatRecipientSearch", () => {
});
it("should clear results when query is empty", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$chatRecipientSearchResults.set({ actors: [{ did: "x" }] });
const mockApi = {
searchProfilesTypeahead: async () => ({ actors: [] }),
@@ -1188,7 +1193,7 @@ describe("loadChatRecipientSearch", () => {
});
it("should discard in-flight responses after the query is cleared", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveSearch;
const searchPromise = new Promise((resolve) => {
resolveSearch = resolve;
@@ -1212,7 +1217,7 @@ describe("loadChatRecipientSearch", () => {
describe("loadSearchTypeahead", () => {
it("should store the search results and hydrate profiles", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchProfilesTypeahead: async () => ({
actors: [{ did: "did:plc:a" }],
@@ -1231,7 +1236,7 @@ describe("loadSearchTypeahead", () => {
});
it("should clear results when query is empty", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$searchTypeaheadResults.set({ actors: [{ did: "x" }] });
const mockApi = {
searchProfilesTypeahead: async () => ({ actors: [] }),
@@ -1244,7 +1249,7 @@ describe("loadSearchTypeahead", () => {
});
it("should discard in-flight responses after the query is cleared", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveSearch;
const searchPromise = new Promise((resolve) => {
resolveSearch = resolve;
@@ -1268,7 +1273,7 @@ describe("loadSearchTypeahead", () => {
describe("loadSidebarSearchTypeahead", () => {
it("should store the search results and hydrate profiles", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchProfilesTypeahead: async () => ({
actors: [{ did: "did:plc:a" }],
@@ -1287,7 +1292,7 @@ describe("loadSidebarSearchTypeahead", () => {
});
it("should clear results when query is empty", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$sidebarSearchTypeaheadResults.set({ actors: [{ did: "x" }] });
const mockApi = {
searchProfilesTypeahead: async () => ({ actors: [] }),
@@ -1300,7 +1305,7 @@ describe("loadSidebarSearchTypeahead", () => {
});
it("should discard in-flight responses after the query is cleared", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveSearch;
const searchPromise = new Promise((resolve) => {
resolveSearch = resolve;
@@ -1322,7 +1327,7 @@ describe("loadSidebarSearchTypeahead", () => {
});
it("should not disturb the search view's typeahead results", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchProfilesTypeahead: async () => ({
actors: [{ did: "did:plc:sidebar" }],
@@ -1344,7 +1349,7 @@ describe("loadSidebarSearchTypeahead", () => {
describe("loadFeedSearch", () => {
it("should clear results when query is empty", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$feedSearchResults.set({ feeds: [{ uri: "f1" }], cursor: "c" });
const mockApi = {
searchFeedGenerators: async () => ({ feeds: [], cursor: null }),
@@ -1357,7 +1362,7 @@ describe("loadFeedSearch", () => {
});
it("should store feeds and cache feed generators", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchFeedGenerators: async () => ({
feeds: [{ uri: "f1", displayName: "Feed One" }],
@@ -1377,7 +1382,7 @@ describe("loadFeedSearch", () => {
});
it("should discard stale responses", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let resolveFirst;
const firstPromise = new Promise((resolve) => {
resolveFirst = resolve;
@@ -1406,7 +1411,7 @@ describe("loadFeedSearch", () => {
});
it("should append when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$feedSearchResults.set({
feeds: [{ uri: "f1" }],
cursor: "c1",
@@ -1429,7 +1434,7 @@ describe("loadFeedSearch", () => {
describe("loadNotifications", () => {
it("should set notifications and cursor on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getNotifications: async () => ({
notifications: [
@@ -1451,7 +1456,7 @@ describe("loadNotifications", () => {
});
it("should append when cursor matches previous", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notifications.set({
notifications: [{ reason: "like", uri: "n1" }],
cursor: "page2",
@@ -1480,7 +1485,7 @@ describe("loadNotifications", () => {
});
it("should reset on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notifications.set({
notifications: [{ reason: "like", uri: "n1" }],
cursor: "page2",
@@ -1511,7 +1516,7 @@ describe("loadNotifications", () => {
});
it("should capture seenAt on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getNotifications: async () => ({
notifications: [
@@ -1533,7 +1538,7 @@ describe("loadNotifications", () => {
});
it("should overwrite the captured seenAt on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notifications.set({
notifications: [{ reason: "like", uri: "n1" }],
cursor: "page2",
@@ -1561,7 +1566,7 @@ describe("loadNotifications", () => {
});
it("should not capture seenAt on subsequent pages", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notifications.set({
notifications: [{ reason: "like", uri: "n1" }],
cursor: "page2",
@@ -1589,7 +1594,7 @@ describe("loadNotifications", () => {
});
it("should set seenAt to null when the response omits it", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notificationsLastSeenAt.set("2025-01-14T10:00:00.000Z");
const mockApi = {
getNotifications: async () => ({
@@ -1608,7 +1613,7 @@ describe("loadNotifications", () => {
});
it("should discard a stale response when a reload lands mid-flight", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notifications.set({
notifications: [{ uri: "n1", reason: "follow" }],
cursor: "c1",
@@ -1638,7 +1643,7 @@ describe("loadNotifications", () => {
});
it("should discard a stale page when the list reaches its end mid-flight", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$notifications.set({
notifications: [{ uri: "n1", reason: "follow" }],
cursor: "c1",
@@ -1673,7 +1678,7 @@ describe("loadNotifications", () => {
describe("loadMentionNotifications", () => {
it("should request only mention reasons and store results", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let capturedReasons;
const mockApi = {
getNotifications: async ({ reasons }) => {
@@ -1700,7 +1705,7 @@ describe("loadMentionNotifications", () => {
});
it("should not capture seenAt", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getNotifications: async () => ({
notifications: [
@@ -1719,7 +1724,7 @@ describe("loadMentionNotifications", () => {
});
it("should append when cursor matches previous", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$mentionNotifications.set({
notifications: [{ reason: "mention", uri: "n1" }],
cursor: "page2",
@@ -1746,7 +1751,7 @@ describe("loadMentionNotifications", () => {
});
it("should reset on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$mentionNotifications.set({
notifications: [{ reason: "mention", uri: "n1" }],
cursor: "page2",
@@ -1773,7 +1778,7 @@ describe("loadMentionNotifications", () => {
describe("loadBookmarks", () => {
it("should set bookmarks on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getBookmarks: async () => ({
bookmarks: [{ item: { uri: "post1", record: {} } }],
@@ -1792,7 +1797,7 @@ describe("loadBookmarks", () => {
});
it("should append on subsequent loads", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$bookmarks.set({
bookmarks: [{ item: { uri: "post1" } }],
cursor: "c1",
@@ -1815,7 +1820,7 @@ describe("loadBookmarks", () => {
});
it("should reset on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$bookmarks.set({
bookmarks: [{ item: { uri: "post1" } }],
cursor: "c1",
@@ -1847,7 +1852,7 @@ describe("loadProfileFollowers", () => {
const profileDid = "did:plc:profile";
it("should set followers on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const res = {
followers: [{ did: "did:plc:a" }],
cursor: "next",
@@ -1861,7 +1866,7 @@ describe("loadProfileFollowers", () => {
});
it("should append followers when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$profileFollowers.set(profileDid, {
followers: [{ did: "did:plc:a" }],
cursor: "c1",
@@ -1886,7 +1891,7 @@ describe("loadProfileFollows", () => {
const profileDid = "did:plc:profile";
it("should set follows on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const res = { follows: [{ did: "did:plc:a" }], cursor: "next" };
const mockApi = { getFollows: async () => res };
const requests = makeRequests(mockApi, dataStore);
@@ -1897,7 +1902,7 @@ describe("loadProfileFollows", () => {
});
it("should append follows when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$profileFollows.set(profileDid, {
follows: [{ did: "did:plc:a" }],
cursor: "c1",
@@ -1920,7 +1925,7 @@ describe("loadProfileFollows", () => {
describe("loadConvoList", () => {
it("should set convo list and cache individual convos on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
listConvos: async () => ({
convos: [
@@ -1941,7 +1946,7 @@ describe("loadConvoList", () => {
});
it("should append when previous cursor matches", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [{ id: "c1" }], cursor: "page2" });
const mockApi = {
@@ -1959,7 +1964,7 @@ describe("loadConvoList", () => {
});
it("should drop convos already in the list when appending a page", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({
convos: [{ id: "c2", unreadCount: 1 }, { id: "c1" }],
cursor: "page2",
@@ -1984,7 +1989,7 @@ describe("loadConvoList", () => {
});
it("should reset cursor and replace on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [{ id: "c1" }], cursor: "page2" });
let capturedCursor;
@@ -2007,7 +2012,7 @@ describe("loadConvoList", () => {
describe("loadConvoRequestList", () => {
it("should request only request convos and cache them on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let capturedStatus;
const mockApi = {
listConvos: async ({ status }) => {
@@ -2033,7 +2038,7 @@ describe("loadConvoRequestList", () => {
});
it("should append when previous cursor matches", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoRequestList.set({
convos: [{ id: "r1" }],
cursor: "page2",
@@ -2054,7 +2059,7 @@ describe("loadConvoRequestList", () => {
});
it("should drop convos already in the list when appending a page", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoRequestList.set({
convos: [{ id: "r2" }, { id: "r1" }],
cursor: "page2",
@@ -2077,7 +2082,7 @@ describe("loadConvoRequestList", () => {
});
it("should reset cursor and replace on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoRequestList.set({
convos: [{ id: "r1" }],
cursor: "page2",
@@ -2105,7 +2110,7 @@ describe("loadConvo", () => {
const convoId = "convo1";
it("should store the convo and track status under a namespaced key", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getConvo: async () => ({ convo: { id: convoId } }),
};
@@ -2120,7 +2125,7 @@ describe("loadConvo", () => {
});
it("should add the convo to the loaded convo list", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [{ id: "other" }], cursor: null });
const mockApi = {
getConvo: async () => ({ convo: { id: convoId, status: "accepted" } }),
@@ -2143,7 +2148,7 @@ describe("loadConvo", () => {
headers: {},
url: "/x",
});
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getConvo: async () => {
throw apiError;
@@ -2165,7 +2170,7 @@ describe("loadConvo", () => {
describe("loadConvoForProfile", () => {
it("should store the convo and add it to the loaded convo list", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoList.set({ convos: [{ id: "other" }], cursor: null });
const mockApi = {
getConvoForMembers: async () => ({
@@ -2188,7 +2193,7 @@ describe("loadConvoMembers", () => {
const convoId = "convo1";
it("should store the first page with its cursor", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getConvoMembers: async () => ({
members: [{ did: "did:plc:alice" }, { did: "did:plc:bob" }],
@@ -2208,7 +2213,7 @@ describe("loadConvoMembers", () => {
});
it("should append the next page using the stored cursor", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const capturedCursors = [];
const pages = [
{ members: [{ did: "did:plc:alice" }], cursor: "1" },
@@ -2235,7 +2240,7 @@ describe("loadConvoMembers", () => {
});
it("should overwrite the stored list on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoMemberLists.set(convoId, {
members: [{ did: "did:plc:stale" }],
cursor: "5",
@@ -2267,7 +2272,7 @@ describe("loadConvoMembers", () => {
headers: {},
url: "/x",
});
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getConvoMembers: async () => {
throw apiError;
@@ -2291,7 +2296,7 @@ describe("loadConvoMessages", () => {
const convoId = "convo1";
it("should set messages on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getMessages: async () => ({
messages: [{ id: "m1" }, { id: "m2" }],
@@ -2308,7 +2313,7 @@ describe("loadConvoMessages", () => {
});
it("should append messages when prior cursor exists", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoMessages.set(convoId, {
messages: [{ id: "m1" }],
cursor: "page2",
@@ -2335,7 +2340,7 @@ describe("loadConvoMessages", () => {
});
it("should reset on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$convoMessages.set(convoId, {
messages: [{ id: "old" }],
cursor: "page2",
@@ -2359,7 +2364,7 @@ describe("loadConvoMessages", () => {
});
it("should store related profiles", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getMessages: async () => ({
messages: [{ id: "m1" }],
@@ -2398,7 +2403,7 @@ describe("pollConvoMessages", () => {
let dataStore;
beforeEach(() => {
- dataStore = new DataStore();
+ dataStore = new DataStore(createSessionState(null));
dataStore.$currentUser.set({ did: currentUserDid });
dataStore.$convos.set(convoId, {
id: convoId,
@@ -2691,7 +2696,7 @@ describe("loadPostLikes", () => {
const postUri = "at://did/post/1";
it("should set likes on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const res = { likes: [{ actor: { did: "did:plc:a" } }], cursor: "next" };
const mockApi = { getLikes: async () => res };
const requests = makeRequests(mockApi, dataStore);
@@ -2702,7 +2707,7 @@ describe("loadPostLikes", () => {
});
it("should append likes when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$postLikes.set(postUri, {
likes: [{ actor: { did: "did:plc:a" } }],
cursor: "c1",
@@ -2727,7 +2732,7 @@ describe("loadPostQuotes", () => {
const postUri = "at://did/post/1";
it("should set quotes on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getQuotes: async () => ({
posts: [{ uri: "q1", record: {} }],
@@ -2745,7 +2750,7 @@ describe("loadPostQuotes", () => {
});
it("should append quotes when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$postQuotes.set(postUri, {
posts: [{ uri: "q1", record: {} }],
cursor: "c1",
@@ -2771,7 +2776,7 @@ describe("loadPostReposts", () => {
const postUri = "at://did/post/1";
it("should set reposts on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getRepostedBy: async () => ({
repostedBy: [{ did: "did:plc:a" }],
@@ -2788,7 +2793,7 @@ describe("loadPostReposts", () => {
});
it("should append reposts when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$postReposts.set(postUri, {
repostedBy: [{ did: "did:plc:a" }],
cursor: "c1",
@@ -2813,7 +2818,7 @@ describe("loadActorFeeds", () => {
const did = "did:plc:author";
it("should set actor feeds and cache feed generators on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getActorFeeds: async () => ({
feeds: [{ uri: "f1", displayName: "F1" }],
@@ -2831,7 +2836,7 @@ describe("loadActorFeeds", () => {
});
it("should append on subsequent calls when cursor remains", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$actorFeeds.set(did, {
feeds: [{ uri: "f1" }],
cursor: "c1",
@@ -2852,7 +2857,7 @@ describe("loadActorFeeds", () => {
});
it("should short-circuit when there is no remaining cursor", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$actorFeeds.set(did, {
feeds: [{ uri: "f1" }],
cursor: null,
@@ -2872,7 +2877,7 @@ describe("loadActorFeeds", () => {
});
it("should reset on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$actorFeeds.set(did, {
feeds: [{ uri: "f1" }],
cursor: null,
@@ -2902,7 +2907,7 @@ describe("loadListsWithMembershipForActor", () => {
const list2 = { uri: "at://owner/app.bsky.graph.list/2", name: "L2" };
it("should store the first page keyed by actor", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getListsWithMembership: async () => ({
listsWithMembership: [
@@ -2923,7 +2928,7 @@ describe("loadListsWithMembershipForActor", () => {
});
it("should append the next page when called again with a cached cursor", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listsWithMembershipByActor.set(actorDid, {
listsWithMembership: [{ list: list1 }],
cursor: "c1",
@@ -2949,7 +2954,7 @@ describe("loadListsWithMembershipForActor", () => {
});
it("should short-circuit when fully loaded", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listsWithMembershipByActor.set(actorDid, {
listsWithMembership: [{ list: list1 }],
cursor: null,
@@ -2969,7 +2974,7 @@ describe("loadListsWithMembershipForActor", () => {
});
it("should refetch from scratch on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listsWithMembershipByActor.set(actorDid, {
listsWithMembership: [{ list: list1 }],
cursor: "c1",
@@ -2997,7 +3002,7 @@ describe("loadListsWithMembershipForActor", () => {
describe("loadHashtagFeed", () => {
it("should store hashtag feed posts on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchPosts: async () => ({
posts: [{ uri: "p1", record: {} }],
@@ -3016,7 +3021,7 @@ describe("loadHashtagFeed", () => {
});
it("should append on subsequent loads", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$hashtagFeeds.set("foo-top", {
posts: [{ uri: "p1" }],
cursor: "c1",
@@ -3038,7 +3043,7 @@ describe("loadHashtagFeed", () => {
});
it("should store an empty page when the response has no posts array", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
searchPosts: async () => ({ cursor: null }),
getPosts: async () => [],
@@ -3053,7 +3058,7 @@ describe("loadHashtagFeed", () => {
});
it("should reset on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$hashtagFeeds.set("foo-top", {
posts: [{ uri: "p1" }],
cursor: "c1",
@@ -3101,7 +3106,7 @@ describe("loadPinnedItems", () => {
return { list: { uri, name: `list-${uri}` }, items: [], cursor: "" };
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const provider = { requirePreferences: () => preferences };
const requests = createRequests(mockApi, dataStore, provider);
@@ -3139,7 +3144,7 @@ describe("loadPinnedItems", () => {
return null;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const provider = { requirePreferences: () => preferences };
const requests = createRequests(mockApi, dataStore, provider);
@@ -3156,7 +3161,7 @@ describe("loadPinnedItems", () => {
describe("enableStatus / getStatus", () => {
it("should track loading start, end, and clear errors on success", async () => {
const mockApi = { getMutes: async () => ({ mutes: [], cursor: null }) };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
const initialStatus = requests.getStatus("loadMutedProfiles");
@@ -3185,7 +3190,7 @@ describe("enableStatus / getStatus", () => {
throw apiError;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadMutedProfiles();
@@ -3205,7 +3210,7 @@ describe("enableStatus / getStatus", () => {
throw otherError;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
let caught = null;
@@ -3233,7 +3238,7 @@ describe("enableStatus / getStatus", () => {
return { mutes: [], cursor: null };
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadMutedProfiles().catch(() => {});
@@ -3245,7 +3250,7 @@ describe("enableStatus / getStatus", () => {
});
it("should namespace status by request id derived from arguments", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getProfile: async (did) => ({ did, handle: "x" }),
};
@@ -3275,7 +3280,7 @@ describe("_loadBlockedPosts", () => {
function setup({ getPosts = async () => [], getRecord }) {
const mockApi = { getPosts, getRecord };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
@@ -3359,7 +3364,10 @@ describe("statusStore.$statuses", () => {
throw apiError;
},
};
- const requests = makeRequests(mockApi, new DataStore());
+ const requests = makeRequests(
+ mockApi,
+ new DataStore(createSessionState(null)),
+ );
assert.deepEqual(requests.statusStore.$statuses.get("loadMutedProfiles"), {
loading: false,
@@ -3390,7 +3398,7 @@ describe("loadCurrentUser", () => {
return profile;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadCurrentUser();
@@ -3410,7 +3418,7 @@ describe("loadCurrentUser", () => {
value: { displayName: "Me" },
}),
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadCurrentUser();
@@ -3436,7 +3444,7 @@ describe("loadCurrentUser", () => {
});
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadCurrentUser();
@@ -3458,7 +3466,7 @@ describe("loadCurrentUser", () => {
throw recordError;
},
};
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await assert.rejects(() => requests.loadCurrentUser(), recordError);
@@ -3470,7 +3478,7 @@ describe("loadPost", () => {
it("should load and store a single post", async () => {
const post = { uri: "at://did:plc:a/app.bsky.feed.post/1", record: {} };
const mockApi = { getPost: async () => post };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadPost(post.uri);
@@ -3532,7 +3540,7 @@ describe("loadPostThread with a blocked parent", () => {
}
it("should rebuild the parent chain from backlinks across blocked authors", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => [
{
@@ -3579,7 +3587,7 @@ describe("loadPostThread with a blocked parent", () => {
[grandparentUri, grandparentPost],
[rootUri, rootPost],
]);
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => [
{
@@ -3619,7 +3627,7 @@ describe("loadPostThread with a blocked parent", () => {
});
it("should rethrow non-abort backlink failures", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => {
throw new TypeError("network down");
@@ -3643,7 +3651,7 @@ describe("loadPostThread with a blocked parent", () => {
});
it("should fall back to loading the blocked parent thread when the viewer is involved in the block", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let constellationCalled = false;
const constellation = {
getLinks: async () => {
@@ -3680,7 +3688,7 @@ describe("loadPostThread with a blocked parent", () => {
});
it("should fall back to loading the blocked parent thread when backlinks time out", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => {
throw Object.assign(new Error("timed out"), { name: "AbortError" });
@@ -3714,7 +3722,7 @@ describe("loadPostThread with a blocked parent", () => {
});
it("should fall back to loading the blocked parent thread when backlinks yield no posts", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
// No backlinks by the blocked author — only the root gets appended
const constellation = { getLinks: async () => [] };
const parentThread = {
@@ -3751,7 +3759,7 @@ describe("_loadBlockedReplies", () => {
const blockedReplyUri = "at://did:plc:blocker/app.bsky.feed.post/r2";
it("should return an empty list when the thread has no post", async () => {
- const requests = makeRequests({}, new DataStore());
+ const requests = makeRequests({}, new DataStore(createSessionState(null)));
const replies = await requests._loadBlockedReplies({});
@@ -3759,7 +3767,7 @@ describe("_loadBlockedReplies", () => {
});
it("should keep the loaded replies when backlinks time out", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => {
throw Object.assign(new Error("timed out"), { name: "AbortError" });
@@ -3784,7 +3792,7 @@ describe("_loadBlockedReplies", () => {
});
it("should rethrow non-abort backlink failures", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => {
throw new TypeError("network down");
@@ -3807,7 +3815,7 @@ describe("_loadBlockedReplies", () => {
});
it("should load missing replies from backlinks and mark them as blocked replies", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const constellation = {
getLinks: async () => [
{
@@ -3864,7 +3872,7 @@ describe("_loadBlockedReplies", () => {
describe("loadNextFeedPage feed types", () => {
it("should use getFollowingFeed for the timeline type", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let called = false;
const mockApi = {
getFollowingFeed: async () => {
@@ -3881,7 +3889,7 @@ describe("loadNextFeedPage feed types", () => {
});
it("should use getListFeed for the list type", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const listUri = "at://did/app.bsky.graph.list/1";
let requestedUri = null;
const mockApi = {
@@ -3899,7 +3907,7 @@ describe("loadNextFeedPage feed types", () => {
});
it("should reject on an unknown feed type", async () => {
- const requests = makeRequests({}, new DataStore());
+ const requests = makeRequests({}, new DataStore(createSessionState(null)));
await assert.rejects(
requests.loadNextFeedPage({ type: "bogus", uri: "x" }),
@@ -3915,7 +3923,7 @@ describe("loadDetailedProfiles", () => {
{ did: "did:plc:b", handle: "b.test" },
];
const mockApi = { getProfiles: async () => profiles };
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const requests = makeRequests(mockApi, dataStore);
await requests.loadDetailedProfiles(["did:plc:a", "did:plc:b"]);
@@ -3932,7 +3940,10 @@ describe("loadDetailedProfiles", () => {
return [];
},
};
- const requests = makeRequests(mockApi, new DataStore());
+ const requests = makeRequests(
+ mockApi,
+ new DataStore(createSessionState(null)),
+ );
await requests.loadDetailedProfiles([]);
@@ -3942,7 +3953,7 @@ describe("loadDetailedProfiles", () => {
describe("_loadJoinLinkPreviews", () => {
it("should fetch distinct codes and store previews by code", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let requestedCodes = null;
const mockApi = {
isAuthenticated: true,
@@ -3977,7 +3988,10 @@ describe("_loadJoinLinkPreviews", () => {
return { joinLinkPreviews: [] };
},
};
- const requests = makeRequests(mockApi, new DataStore());
+ const requests = makeRequests(
+ mockApi,
+ new DataStore(createSessionState(null)),
+ );
await requests._loadJoinLinkPreviews(["abc"]);
assert.deepEqual(called, false);
@@ -3994,7 +4008,10 @@ describe("_loadJoinLinkPreviews", () => {
throw new Error("boom");
},
};
- const requests = makeRequests(mockApi, new DataStore());
+ const requests = makeRequests(
+ mockApi,
+ new DataStore(createSessionState(null)),
+ );
await requests._loadJoinLinkPreviews(["abc"]);
});
@@ -4007,7 +4024,10 @@ describe("_loadPostDependencies", () => {
throw new Error("network down");
},
};
- const requests = makeRequests(mockApi, new DataStore());
+ const requests = makeRequests(
+ mockApi,
+ new DataStore(createSessionState(null)),
+ );
const blockedPost = {
$type: "app.bsky.feed.defs#blockedPost",
uri: "at://did:plc:x/app.bsky.feed.post/1",
@@ -4019,7 +4039,7 @@ describe("_loadPostDependencies", () => {
describe("loadFeedGenerator / loadList / loadStarterPack", () => {
it("should store the feed generator by uri", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const feedGenerator = { uri: "at://did/feed/1", displayName: "Feed" };
const mockApi = { getFeedGenerator: async () => feedGenerator };
const requests = makeRequests(mockApi, dataStore);
@@ -4033,7 +4053,7 @@ describe("loadFeedGenerator / loadList / loadStarterPack", () => {
});
it("should store the list view by uri", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const listUri = "at://did/app.bsky.graph.list/1";
let capturedOptions = null;
const mockApi = {
@@ -4051,7 +4071,7 @@ describe("loadFeedGenerator / loadList / loadStarterPack", () => {
});
it("should store the starter pack by uri", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const starterPack = { uri: "at://did/starterpack/1", record: {} };
const mockApi = { getStarterPack: async () => starterPack };
const requests = makeRequests(mockApi, dataStore);
@@ -4066,7 +4086,7 @@ describe("loadListMembers", () => {
const listUri = "at://did/app.bsky.graph.list/1";
it("should store the first page and hydrate member profiles", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getList: async () => ({
list: { uri: listUri },
@@ -4085,7 +4105,7 @@ describe("loadListMembers", () => {
});
it("should short-circuit when fully loaded", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listMembers.set(listUri, { items: [], cursor: null });
let called = false;
const mockApi = {
@@ -4102,7 +4122,7 @@ describe("loadListMembers", () => {
});
it("should refetch from scratch on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$listMembers.set(listUri, {
items: [{ uri: "li1", subject: { did: "did:plc:a" } }],
cursor: null,
@@ -4132,7 +4152,7 @@ describe("loadActorLists", () => {
const did = "did:plc:author";
it("should store actor lists and cache each list view", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getActorLists: async () => ({
lists: [{ uri: "at://did/app.bsky.graph.list/1", name: "L1" }],
@@ -4153,7 +4173,7 @@ describe("loadActorLists", () => {
});
it("should short-circuit when there is no remaining cursor", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$actorLists.set(did, { lists: [], cursor: null });
let called = false;
const mockApi = {
@@ -4170,7 +4190,7 @@ describe("loadActorLists", () => {
});
it("should refetch from scratch on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$actorLists.set(did, {
lists: [{ uri: "old" }],
cursor: null,
@@ -4202,7 +4222,10 @@ describe("loadCurrentUserLists", () => {
return { lists: [], cursor: null };
},
};
- const requests = makeRequests(mockApi, new DataStore());
+ const requests = makeRequests(
+ mockApi,
+ new DataStore(createSessionState(null)),
+ );
await requests.loadCurrentUserLists();
@@ -4210,7 +4233,7 @@ describe("loadCurrentUserLists", () => {
});
it("should load the current user's lists", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$currentUser.set({ did: "did:plc:me" });
let requestedDid = null;
const mockApi = {
@@ -4241,7 +4264,7 @@ describe("loadDrafts", () => {
}
it("should store the drafts page and load local media refs", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const draftView = {
draft: {
posts: [
@@ -4276,7 +4299,7 @@ describe("loadDrafts", () => {
});
it("should refetch from scratch on reload", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$drafts.set({ drafts: [{ draft: {} }], cursor: "c1" });
let capturedCursor;
const mockApi = {
@@ -4302,7 +4325,7 @@ describe("loadKnownFollowers", () => {
const profileDid = "did:plc:target";
it("should store known followers and hydrate profiles on first load", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const mockApi = {
getKnownFollowers: async () => ({
followers: [{ did: "did:plc:a", handle: "a" }],
@@ -4320,7 +4343,7 @@ describe("loadKnownFollowers", () => {
});
it("should append when cursor is provided", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
dataStore.$knownFollowers.set(profileDid, {
followers: [{ did: "did:plc:a" }],
cursor: "c1",
@@ -4343,7 +4366,7 @@ describe("loadKnownFollowers", () => {
describe("loadProfileChatStatus", () => {
it("should store the availability response keyed by profile did", async () => {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
let requestedDids = null;
const availability = { canChat: true };
const mockApi = {
diff --git a/tests/unit/specs/identityPrecaching.test.js b/tests/unit/specs/identityPrecaching.test.js
index 2b163938..d47f1879 100644
--- a/tests/unit/specs/identityPrecaching.test.js
+++ b/tests/unit/specs/identityPrecaching.test.js
@@ -2,9 +2,10 @@ import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { setUpIdentityPrecaching } from "/js/identityPrecaching.js";
import { DataStore } from "/js/dataLayer/dataStore.js";
+import { createSessionState } from "/js/dataLayer/sessionState.js";
function setup() {
- const dataStore = new DataStore();
+ const dataStore = new DataStore(createSessionState(null));
const dataLayer = {
dataStore,
preferencesProvider: { $preferences: { get: () => null } },
diff --git a/tests/unit/specs/mainLayout.test.js b/tests/unit/specs/mainLayout.test.js
index 3027fa76..f9b9b7b8 100644
--- a/tests/unit/specs/mainLayout.test.js
+++ b/tests/unit/specs/mainLayout.test.js
@@ -32,11 +32,20 @@ describe("MainLayout", () => {
const composePost = mock.fn();
const $trends = new Signal.State(null);
const loadTrends = mock.fn(async () => {});
+ const $hydratedPinnedItems = new Signal.State(null);
+ const $selectedFeedUri = new Signal.State(null);
+ const ensurePinnedItems = mock.fn(async () => []);
const context = {
isAuthenticated: true,
dataLayer: {
- derived: { $currentUser, $trends },
+ derived: {
+ $currentUser,
+ $trends,
+ $hydratedPinnedItems,
+ $selectedFeedUri,
+ },
requests: { loadTrends },
+ declarative: { ensurePinnedItems },
},
notificationService: { $numNotifications },
chatNotificationService: { $numNotifications: $numChatNotifications },
@@ -133,7 +142,16 @@ describe("MainLayout", () => {
const elements = [...rightColumn.children].map((child) =>
child.tagName.toLowerCase(),
);
- assert.deepEqual(elements, ["sidebar-search", "trending-pane"]);
+ assert.deepEqual(elements, [
+ "sidebar-search",
+ "pinned-feeds-pane",
+ "trending-pane",
+ ]);
+ assert(
+ rightColumn
+ .querySelector("pinned-feeds-pane")
+ .hasAttribute("show-selected"),
+ );
setRoute({ layoutOptions: { activeNavItem: "search" } });
await flushRender();
@@ -289,6 +307,37 @@ describe("mainLayoutTemplate", () => {
});
});
+describe("mainLayoutTemplate - pinned feeds pane", () => {
+ it("renders the pinned feeds pane only when authenticated", () => {
+ const container = document.createElement("div");
+ render(
+ mainLayoutTemplate({
+ pluginService: mockPluginService,
+ isAuthenticated: true,
+ currentUser: mockUser,
+ children: html`Content
`,
+ }),
+ container,
+ );
+ assert(container.querySelector("pinned-feeds-pane") !== null);
+
+ const loggedOutContainer = document.createElement("div");
+ render(
+ mainLayoutTemplate({
+ pluginService: mockPluginService,
+ isAuthenticated: false,
+ currentUser: null,
+ children: html`Content
`,
+ }),
+ loggedOutContainer,
+ );
+ assert.deepEqual(
+ loggedOutContainer.querySelector("pinned-feeds-pane"),
+ null,
+ );
+ });
+});
+
describe("mainLayoutTemplate - footer", () => {
it("should render footer", () => {
const result = mainLayoutTemplate({
diff --git a/tests/unit/specs/signals.test.js b/tests/unit/specs/signals.test.js
index 87eec186..170ffe88 100644
--- a/tests/unit/specs/signals.test.js
+++ b/tests/unit/specs/signals.test.js
@@ -1,4 +1,4 @@
-import { describe, it } from "node:test";
+import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import {
Signal,
@@ -6,6 +6,7 @@ import {
SignalMap,
SignalArray,
effect,
+ PersistedReactiveStore,
} from "/js/signals.js";
// effect() batches reactions via a double requestAnimationFrame (see
@@ -609,3 +610,67 @@ describe("effect", () => {
assert.deepEqual(seen, [0]);
});
});
+
+describe("PersistedReactiveStore", () => {
+ const storageKey = "persisted-store-test";
+
+ beforeEach(() => localStorage.removeItem(storageKey));
+ afterEach(() => localStorage.removeItem(storageKey));
+
+ it("restores a stored value matching the default's type", () => {
+ localStorage.setItem(storageKey, JSON.stringify({ hidden: true }));
+ const store = new PersistedReactiveStore(storageKey);
+ store.$hidden = new Signal.State(false);
+ assert.deepEqual(store.$hidden.get(), true);
+ });
+
+ it("ignores a stored value whose type differs from the default", () => {
+ localStorage.setItem(storageKey, JSON.stringify({ hidden: "yes" }));
+ const store = new PersistedReactiveStore(storageKey);
+ store.$hidden = new Signal.State(false);
+ assert.deepEqual(store.$hidden.get(), false);
+ });
+
+ it("restores any stored value when the default is null", () => {
+ localStorage.setItem(storageKey, JSON.stringify({ selected: "following" }));
+ const store = new PersistedReactiveStore(storageKey);
+ store.$selected = new Signal.State(null);
+ assert.deepEqual(store.$selected.get(), "following");
+ });
+
+ it("keeps a null default when nothing is stored", () => {
+ const store = new PersistedReactiveStore(storageKey);
+ store.$selected = new Signal.State(null);
+ assert.deepEqual(store.$selected.get(), null);
+ });
+
+ it("saves changed values and drops values back at their default", async () => {
+ const store = new PersistedReactiveStore(storageKey);
+ store.$hidden = new Signal.State(false);
+ store.$selected = new Signal.State(null);
+
+ store.$hidden.set(true);
+ store.$selected.set("following");
+ await flushEffects();
+ assert.deepEqual(JSON.parse(localStorage.getItem(storageKey)), {
+ hidden: true,
+ selected: "following",
+ });
+
+ store.$hidden.set(false);
+ store.$selected.set(null);
+ await flushEffects();
+ assert.deepEqual(localStorage.getItem(storageKey), null);
+ });
+
+ it("does not write the defaults back on registration", async () => {
+ const store = new PersistedReactiveStore(storageKey);
+ store.$hidden = new Signal.State(false);
+ await flushEffects();
+ assert.deepEqual(localStorage.getItem(storageKey), null);
+ });
+
+ it("throws without a storage key", () => {
+ assert.throws(() => new PersistedReactiveStore(), /storage key/);
+ });
+});