Skip to content

Conversation

@j-paterson
Copy link
Collaborator

@j-paterson j-paterson commented Oct 16, 2025

Summary by CodeRabbit

  • New Features

    • Global store-driven tab state with a configurable default tab (exposed as currentTabName); TabBar accepts a defaultTab and follows the global tab.
  • Bug Fixes

    • Safer create/rename/delete flows with optimistic updates, better duplicate/name validation, and improved local-vs-remote reconciliation using timestamps and robust error handling.
  • Refactor

    • Tab orchestration moved to centralized store for simpler loading, lifecycle, and rollback behavior; UI simplified to rely on store state.
  • Documentation

    • Added Tab Management System guide.
  • Chores

    • DB index updated to enforce uniqueness for non-empty channel IDs.

@vercel
Copy link

vercel bot commented Oct 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
nounspace-ts Ready Ready Preview Comment Oct 17, 2025 5:02pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

Move tab/space UI to a store-driven flow using currentSpaceId/currentTabName; add centralized tab validation and withOptimisticUpdate; implement optimistic create/rename/delete in stores; update TabBar API (defaultTab added, inHome/switchTabTo removed); enforce default-first tab ordering; replace DB UNIQUE constraint with a partial unique index.

Changes

Cohort / File(s) Summary
Public Space UI
src/app/(spaces)/PublicSpace.tsx
Replace local orchestration and per-tab lifecycle with store-driven currentSpaceId/currentTabName; sync from spacePageData/props; derive isEditable from store; route tab actions through store; simplify loading, init and error paths.
Private / Homebase UI
src/app/(spaces)/homebase/PrivateSpace.tsx, src/app/home/[tabname]/page.tsx
Adopt store currentTabName with HOMEBASE_DEFAULT_TAB fallback; remove local tabName state; simplify init effects and config loading; update TabBar usage (add defaultTab, stable keys).
TabBar component
src/common/components/organisms/TabBar.tsx
Prop API change: add defaultTab, remove inHome and switchTabTo; internal switchTabTo updates store+URL; use validateTabName/isEditableTab; implement optimistic create/rename/delete flows and remove local operating flags.
Tab utilities
src/common/utils/tabUtils.ts
New helpers: validateTabName, isDuplicateTabName, withOptimisticUpdate — validation, duplicate detection, and optimistic-update wrapper with rollback.
Homebase tabs store
src/common/data/stores/app/homebase/homebaseTabsStore.ts
Add HOMEBASE_DEFAULT_TAB = "Feed"; enforce default-first ordering; adopt optimistic update patterns and timestamp-aware load/merge; optimistic rename with rollback and duplicate checks; preserve local edits when reconciling remote.
Space store (app)
src/common/data/stores/app/space/spaceStore.ts
Add renameSpaceTab(...) with optimistic commit/rollback; remove fid from loadSpaceTab; simplify saveLocalSpaceTab signature; validate tab names on create; ensure localSpaces populated with metadata during load.
SpacePage small cleanup
src/app/(spaces)/SpacePage.tsx
Remove redundant React fragment wrapper and return Space component directly.
DB migration
supabase/migrations/20250929120000_add_channel_spaces.sql.sql
Replace UNIQUE constraint with a partial unique index on spaceRegistrations.channelId WHERE NOT NULL (allow NULLs; enforce uniqueness for non-NULLs).
Docs
docs/TAB_MANAGEMENT_SYSTEM.md
Add documentation describing tab management architecture, optimistic-update patterns, component/store contracts, migration guidance, and troubleshooting.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant UI as TabBar / SpacePage
    participant Store
    participant API

    Note over UI,Store: User triggers tab operation (create / rename / delete / switch)
    User->>UI: click create/rename/delete/switch
    UI->>Store: dispatch action (createTab / renameSpaceTab / deleteTab / setCurrentTabName)
    Store->>Store: withOptimisticUpdate → updateFn (apply optimistic/local change)
    Store->>UI: optimistic state rendered
    Store->>API: commitFn (persist change)
    alt commit success
        API-->>Store: success
        Store->>UI: committed state rendered
    else commit failure
        API-->>Store: error
        Store->>Store: rollbackFn (restore previous state)
        Store->>UI: rolled-back state + error surfaced
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • sktbrd
  • willyogo
  • Jhonattan2121

Poem

🐇
I hop through tabs and check each name,
I nudge the store and call its game.
Optimistic hops, then commit with cheer,
If things go wrong, I roll back near.
🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title "Tab bar fix" does reference a real component involved in the changeset (TabBar), however it significantly mischaracterizes the scope and nature of the changes. The changeset is a comprehensive refactoring that transitions the entire tab management system from local component state to centralized store-driven state management, affecting multiple core components (PublicSpace, PrivateSpace, TabBar), introducing new utilities and stores, and adding extensive documentation. The term "fix" implies a bug correction or minor adjustment, when this is actually a major architectural refactoring. A developer scanning the git history would be misled about what this PR accomplishes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tabBarFix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/(spaces)/PublicSpace.tsx (1)

248-265: Don’t read stale localSpaces when loading post-registration tabs

Inside the registration effect, tabOrder is pulled from the localSpaces snapshot that was captured when the effect first ran. Because you mutate the store via loadSpaceTabOrder, loadEditableSpaces, and loadSpaceTab, the localSpaces reference inside this closure never sees those updates, so tabOrder resolves to []. That means the “load remaining tabs” loop is a no-op, and additional tabs for a freshly registered space never hydrate locally.

Re-fetch the latest state from the store before iterating. For example:

-            const tabOrder = localSpaces[newSpaceId]?.order || [];
+            const latestLocalSpaces = get().space.localSpaces;
+            const tabOrder = latestLocalSpaces[newSpaceId]?.order || [];
             for (const tabName of tabOrder) {
               if (tabName !== spacePageData.defaultTab) {
                 await loadSpaceTab(newSpaceId, tabName);
               }
             }

This ensures you’re working with the up-to-date tab order populated by the prior async calls.

src/common/components/organisms/TabBar.tsx (1)

153-191: Restore URL when an optimistic rename fails

debouncedRenameTab switches the URL to the tentative uniqueName before awaiting renameTab. If renameTab throws (network error, validation, etc.), withOptimisticUpdate rolls the store back to the old tab name, but the router stays on the new path (router.replace isn’t reversed). The user ends up on a URL that no longer matches the active tab, and navigation/bookmarking breaks until they manually click away.

Wrap the await renamePromise in a try/catch and, on failure, swap the router back to the original tab:

-        await renamePromise;
-        commitTab(uniqueName);
-        commitTabOrder();
+        try {
+          await renamePromise;
+          commitTab(uniqueName);
+          commitTabOrder();
+        } catch (error) {
+          updateUrl(tabName);
+          setCurrentTabName(tabName);
+          throw error;
+        }

This keeps the URL in sync with the rolled-back store state.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efa1df7 and ef1b961.

📒 Files selected for processing (7)
  • src/app/(spaces)/PublicSpace.tsx (11 hunks)
  • src/app/(spaces)/homebase/PrivateSpace.tsx (5 hunks)
  • src/common/components/organisms/TabBar.tsx (6 hunks)
  • src/common/data/stores/app/homebase/homebaseTabsStore.ts (6 hunks)
  • src/common/data/stores/app/space/spaceStore.ts (7 hunks)
  • src/common/utils/tabUtils.ts (1 hunks)
  • supabase/migrations/20250929120000_add_channel_spaces.sql.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/common/data/stores/app/space/spaceStore.ts (2)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/common/utils/tabUtils.ts (3)
  • validateTabName (11-16)
  • isDuplicateTabName (25-34)
  • withOptimisticUpdate (46-74)
src/app/(spaces)/homebase/PrivateSpace.tsx (1)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (4)
src/common/utils/tabUtils.ts (3)
  • validateTabName (11-16)
  • isDuplicateTabName (25-34)
  • withOptimisticUpdate (46-74)
src/pages/api/space/homebase/tabs/index.ts (1)
  • UnsignedManageHomebaseTabsRequest (24-29)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/common/components/organisms/TabBar.tsx (4)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/common/data/stores/app/currentSpace/index.ts (1)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (3)
  • createTab (229-309)
  • deleteTab (310-347)
  • renameTab (348-449)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/app/(spaces)/PublicSpace.tsx (5)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-78)
🔇 Additional comments (1)
supabase/migrations/20250929120000_add_channel_spaces.sql.sql (1)

2-18: Well-designed migration using partial unique index pattern.

The migration correctly implements channel space support:

  • Column addition (lines 2–3) is safely guarded with IF NOT EXISTS and nullable by default.
  • Constraint update (lines 6–12) idempotently replaces the old validation with one that includes the new 'channel' spaceType; existing data remains valid.
  • Partial unique index (lines 15–18) is the correct pattern here: it allows multiple NULL values (for non-channel spaces) while enforcing uniqueness only on non-NULL channelId values. The comment clearly explains this design choice.

All operations are properly guarded to ensure idempotency and backward compatibility.

Verify this migration in context of the broader PR changes.

Since the PR summary mentions refactoring tab/space management and introducing optimistic updates, please ensure:

  • The migration is tested alongside the tab/space management code changes (e.g., creation, rename, delete operations for channel spaces).
  • Any application code that inserts into spaceRegistrations correctly populates or leaves channelId NULL as appropriate.
  • The optimistic update patterns in the application layer handle the case where an operation might roll back after the index constraint is checked.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(spaces)/PublicSpace.tsx (1)

325-346: Fix the dependency array for saveConfig.

Line 345 includes config.fidgetInstanceDatums in the dependency array, but config is a derived object created at lines 182-187. This will cause saveConfig to be recreated on every render where the config object identity changes, even if the underlying data hasn't changed. This can lead to performance issues and unnecessary re-renders of child components.

Apply this diff to fix the dependency:

   const saveConfig = useCallback(
     async (spaceConfig: SpaceConfigSaveDetails) => {
       if (isNil(currentSpaceId)) {
         throw new Error("Cannot save config until space is registered");
       }
       const saveableConfig = {
         ...spaceConfig,
         fidgetInstanceDatums: spaceConfig.fidgetInstanceDatums
           ? mapValues(spaceConfig.fidgetInstanceDatums, (datum) => ({
             ...datum,
             config: {
               settings: datum.config.settings,
               editable: datum.config.editable,
             },
           }))
           : undefined,
         isPrivate: false,
       };
       return saveLocalSpaceTab(currentSpaceId, currentTabName, saveableConfig);
     },
-    [currentSpaceId, currentTabName, saveLocalSpaceTab, config.fidgetInstanceDatums]
+    [currentSpaceId, currentTabName, saveLocalSpaceTab]
   );
🧹 Nitpick comments (1)
src/app/(spaces)/PublicSpace.tsx (1)

156-169: Consider adding cleanup for async operations.

The effect contains async operations without cleanup, which could cause issues if the component unmounts or dependencies change while the promises are still pending. This could lead to state updates on unmounted components or stale data being loaded.

Apply this diff to add proper cleanup:

   useEffect(() => {
     if (!currentSpaceId || !currentTabName) return;

+    let cancelled = false;
+
     const loadSpace = async () => {
       try {
         await loadSpaceTabOrder(currentSpaceId);
+        if (cancelled) return;
         await loadSpaceTab(currentSpaceId, currentTabName);
       } catch (error) {
         console.error("Error loading space:", error);
       }
     };

     loadSpace();
+
+    return () => {
+      cancelled = true;
+    };
   }, [currentSpaceId, currentTabName, loadSpaceTabOrder, loadSpaceTab]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef1b961 and cccc425.

📒 Files selected for processing (4)
  • src/app/(spaces)/PublicSpace.tsx (11 hunks)
  • src/app/(spaces)/homebase/PrivateSpace.tsx (5 hunks)
  • src/common/components/organisms/TabBar.tsx (6 hunks)
  • src/common/data/stores/app/space/spaceStore.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/common/components/organisms/TabBar.tsx (3)
src/common/data/stores/app/currentSpace/index.ts (1)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (3)
  • createTab (229-309)
  • deleteTab (310-347)
  • renameTab (348-449)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/common/data/stores/app/space/spaceStore.ts (2)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/common/utils/tabUtils.ts (3)
  • validateTabName (11-16)
  • isDuplicateTabName (25-34)
  • withOptimisticUpdate (46-74)
src/app/(spaces)/homebase/PrivateSpace.tsx (1)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/PublicSpace.tsx (5)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-78)
🔇 Additional comments (10)
src/app/(spaces)/homebase/PrivateSpace.tsx (2)

154-170: LGTM!

The TabBar memoization correctly includes currentTabName and tabName in the dependency array, ensuring proper re-renders when the active tab changes. The defaultTab="Feed" prop and currentTab={currentTabName || tabName} provide appropriate fallback behavior.


92-102: Verify the intended behavior of Feed config loading.

The review comment correctly identifies that the effect will re-run when homebaseConfig changes. However, the underlying cause differs from the stated reasoning: the functions from useAppStore are stable (bound to the store instance), but the effect logic itself has a conditional flow issue.

Specifically:

  • When the component first renders with tabName="Feed" and !homebaseConfig, the effect calls loadFeedConfig()
  • When homebaseConfig becomes defined asynchronously, the effect re-runs (due to the dependency)
  • At that point, the condition tabName === "Feed" && !homebaseConfig becomes false, so loadTab(tabName) executes instead

Whether this is intentional or a bug depends on your Feed loading flow. If loadFeedConfig() should be the final action without a subsequent loadTab() call, then splitting the effects as suggested would prevent this behavior. However, if calling loadTab() after config loads is correct, the current code may be fine.

Manually verify that the effect behavior matches your intended data-loading sequence for the Feed tab.

src/app/(spaces)/PublicSpace.tsx (1)

96-102: LGTM!

The effect properly sets the current space and tab name when the space data changes, with appropriate dependencies to trigger re-runs when spacePageData.spaceId, providedTabName, or spacePageData.defaultTab change.

src/common/components/organisms/TabBar.tsx (3)

67-71: LGTM!

The switchTabTo function provides a clean, centralized way to handle tab navigation by updating both the store state and the URL. The shouldSave parameter provides flexibility for different use cases.


186-209: LGTM!

The tab name generation logic is well-designed:

  • generateNewTabName starts counting from tabList.length + 1 and has a reasonable fallback using Date.now()
  • generateUniqueTabName validates the input, handles duplicates with numeric suffixes, and has a safety limit to prevent infinite loops

82-110: Error handling needed for tab switching and order commit; verify if commitTabOrder() is redundant.

The create tab flow correctly handles errors from createTab (line 105), but if switchTabTo (line 100) or commitTabOrder (line 103) fail, there's no error handling.

Regarding the potential redundancy of commitTabOrder(): A TODO comment in PrivateSpace indicates that component has not yet been updated to use optimistic updates like PublicSpace. This suggests that in at least some code paths, createTab may only perform local state updates without persisting to the backend, making commitTabOrder() necessary rather than redundant. Verify whether this applies to your use case before removing the call.

src/common/data/stores/app/space/spaceStore.ts (4)

239-276: LGTM!

The refactored saveLocalSpaceTab correctly:

  • Removes the rename logic (now in a separate renameSpaceTab function)
  • Handles both new and existing tabs
  • Updates timestamps appropriately
  • Uses proper deep cloning and merging

364-386: Verify currentTabName update logic during rename.

Lines 382-384 update draft.currentSpace.currentTabName when renaming a tab, but only if the current tab matches the old tab name. This logic is correct. However, the same update appears in the rollback at lines 415-419, which is also correct.

The optimistic update flow looks solid:

  • Updates tab config, changedNames mapping, order, and timestamps
  • Conditionally updates currentTabName only if renaming the current tab
  • Rollback restores all state

359-395: LGTM!

The network resolution logic (lines 359-361) properly falls back through multiple sources:

  1. Provided network parameter
  2. Local space's network property
  3. Remote space's network property

The commitFn (lines 387-395) correctly handles both sync and async return values from commitSpaceTabToDatabase, awaiting only if a promise is returned.


1328-1348: LGTM!

The enhanced loadEditableSpaces correctly populates missing metadata in localSpaces:

  • Only updates entries that don't exist or are missing contract metadata
  • Preserves existing tabs, order, and changedNames
  • Adds contractAddress, network, fid, proposalId, and channelId from remote data

This ensures the local cache has complete metadata for all editable spaces.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/app/(spaces)/PublicSpace.tsx (1)

418-430: Critical: Remove invalid await on config.

Line 420 attempts to await config, but config is a plain object (defined at lines 185-193), not a Promise. This will cause resolvedConfig to simply be the config object itself, which works but is misleading and incorrect usage of await.

Apply this diff:

       renameTab={async (oldName, newName) => {
         if (currentSpaceId) {
-          const resolvedConfig = await config;
           return renameSpaceTab(
             currentSpaceId,
             oldName,
             newName,
-            resolvedConfig,
+            config,
             isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
           );
         }
         return undefined;
       }}
🧹 Nitpick comments (2)
src/app/(spaces)/PublicSpace.tsx (2)

185-193: Complex conditional logic for config resolution.

The config logic has nested ternaries that determine whether to return a loaded tab config, a default config, or undefined (to trigger Suspense). While logically correct, this could benefit from clarifying comments explaining each branch condition.

Consider adding inline comments to clarify the branches:

+  // Config resolution:
+  // 1. If tab exists in store, use it with computed isEditable
+  // 2. If viewing unregistered space (no currentSpaceId), use default config  
+  // 3. Otherwise, return undefined to trigger Suspense during load
   const config = currentTabName ? (
     currentConfig?.tabs?.[currentTabName] ? {

331-352: Unnecessary dependency in saveConfig callback.

Line 351 includes config?.fidgetInstanceDatums in the dependency array, but the callback only references currentSpaceId, currentTabName, and saveLocalSpaceTab. The spaceConfig parameter comes from the function arguments, not the outer config variable.

Apply this diff to remove the unnecessary dependency:

-    [currentSpaceId, currentTabName, saveLocalSpaceTab, config?.fidgetInstanceDatums]
+    [currentSpaceId, currentTabName, saveLocalSpaceTab]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cccc425 and 9a1e030.

📒 Files selected for processing (2)
  • src/app/(spaces)/PublicSpace.tsx (11 hunks)
  • src/app/(spaces)/SpacePage.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/(spaces)/SpacePage.tsx (1)
src/app/(spaces)/Space.tsx (1)
  • Space (112-360)
src/app/(spaces)/PublicSpace.tsx (6)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfigSaveDetails (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-76)
🔇 Additional comments (4)
src/app/(spaces)/SpacePage.tsx (1)

62-74: LGTM! Fragment wrapper removed.

Removing the unnecessary React fragment wrapper simplifies the component structure without affecting functionality.

src/app/(spaces)/PublicSpace.tsx (3)

95-102: LGTM! Store synchronization effect.

Properly synchronizes route params with store state, ensuring currentSpaceId and currentTabName reflect the latest space data.


146-169: LGTM! Data loading effects with error handling.

The two effects properly handle:

  1. Loading editable spaces when the user signs in
  2. Loading space data when IDs are set

Both include appropriate error handling and correct dependencies.


467-477: LGTM! Direct SpacePage rendering.

Returns SpacePage directly with the necessary props, consistent with the removal of the fragment wrapper in SpacePage.tsx.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/app/(spaces)/PublicSpace.tsx (2)

104-109: Remove unnecessary dependencies from useCallback; consider if getConfig wrapper is needed at all.

The getCurrentSpaceConfig() already reads currentSpaceId and currentTabName fresh from the store at call time via get(), making these dependencies redundant. Additionally, the callback wrapper serves no purpose since it's called immediately on line 109 rather than being passed as a memoized dependency to child components.

Consider calling getCurrentSpaceConfig() directly:

-  // Get the current config using the store's getter
-  const getConfig = useCallback(() => {
-    return getCurrentSpaceConfig();
-  }, [getCurrentSpaceConfig, currentSpaceId, currentTabName]);
-
-  const currentConfig = getConfig();
+  // Get the current config using the store's getter
+  const currentConfig = getCurrentSpaceConfig();

Based on past review comments.


418-430: Critical: Remove invalid await on config.

Line 420 attempts to await config, but config is a plain object (or undefined), not a Promise. This will cause resolvedConfig to simply be the config object itself, which works but is misleading and incorrect usage of await.

Apply this diff:

       renameTab={async (oldName, newName) => {
         if (currentSpaceId) {
-          const resolvedConfig = await config;
           return renameSpaceTab(
             currentSpaceId,
             oldName,
             newName,
-            resolvedConfig,
+            config,
             isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
           );
         }
         return undefined;
       }}

Based on past review comments.

🧹 Nitpick comments (4)
src/app/(spaces)/PublicSpace.tsx (4)

14-14: Remove unused imports.

The imports debounce (line 14) and withOptimisticUpdate (line 28) are not used anywhere in the visible code.

Apply this diff:

-import { indexOf, isNil, mapValues, noop, debounce } from "lodash";
+import { indexOf, isNil, mapValues, noop } from "lodash";
-import { withOptimisticUpdate } from "@/common/utils/tabUtils";

Also applies to: 28-28


172-179: Remove unnecessary dependency from isEditable useMemo.

The dependency isSignedIntoFarcaster on line 179 is not used in the useMemo computation (lines 173-178), making it unnecessary and causing redundant recomputations.

Apply this diff:

   const isEditable = useMemo(() => {
     const result = spacePageData.isEditable(
       currentUserFid || undefined, 
       wallets.map((w) => ({ address: w.address as Address }))
     );
     
     return result;
-  }, [spacePageData, currentUserFid, wallets, isSignedIntoFarcaster]);
+  }, [spacePageData, currentUserFid, wallets]);

185-193: Consider extracting config logic to useMemo for clarity.

The config determination logic is complex with nested ternaries. While correct, wrapping it in a useMemo would improve readability and make the dependencies explicit.

-  const config = currentTabName ? (
-    currentConfig?.tabs?.[currentTabName] ? {
-      ...currentConfig.tabs[currentTabName],
-      isEditable,
-    } : !currentSpaceId ? {
-      ...spacePageData.config,
-      isEditable,
-    } : undefined
-  ) : undefined;
+  const config = useMemo(() => {
+    if (!currentTabName) return undefined;
+    
+    if (currentConfig?.tabs?.[currentTabName]) {
+      return {
+        ...currentConfig.tabs[currentTabName],
+        isEditable,
+      };
+    }
+    
+    if (!currentSpaceId) {
+      return {
+        ...spacePageData.config,
+        isEditable,
+      };
+    }
+    
+    return undefined;
+  }, [currentTabName, currentConfig, currentSpaceId, spacePageData.config, isEditable]);

331-352: Remove incorrect dependency from saveConfig callback.

Line 351 includes config?.fidgetInstanceDatums in the dependency array, but this value is never used in the saveConfig function. The function only uses the passed-in spaceConfig parameter. This unnecessary dependency causes redundant recreations of the callback.

Apply this diff:

   const saveConfig = useCallback(
     async (spaceConfig: SpaceConfigSaveDetails) => {
       if (isNil(currentSpaceId) || isNil(currentTabName)) {
         throw new Error("Cannot save config until space and tab are initialized");
       }
       const saveableConfig = {
         ...spaceConfig,
         fidgetInstanceDatums: spaceConfig.fidgetInstanceDatums
           ? mapValues(spaceConfig.fidgetInstanceDatums, (datum) => ({
             ...datum,
             config: {
               settings: datum.config.settings,
               editable: datum.config.editable,
             },
           }))
           : undefined,
         isPrivate: false,
       };
       return saveLocalSpaceTab(currentSpaceId, currentTabName, saveableConfig);
     },
-    [currentSpaceId, currentTabName, saveLocalSpaceTab, config?.fidgetInstanceDatums]
+    [currentSpaceId, currentTabName, saveLocalSpaceTab]
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccd8d22 and bd592b5.

📒 Files selected for processing (2)
  • src/app/(spaces)/PublicSpace.tsx (11 hunks)
  • src/app/home/[tabname]/page.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(spaces)/PublicSpace.tsx (5)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfigSaveDetails (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-76)
🔇 Additional comments (9)
src/app/home/[tabname]/page.tsx (2)

83-83: LGTM: Default tab alignment.

The defaultTab prop correctly aligns with the static tab configuration and provides a consistent fallback for the home page.


88-88: LGTM: No-op rename callback for static tabs.

The no-op renameTab implementation is appropriate since home page tabs are static and not renameable.

src/app/(spaces)/PublicSpace.tsx (7)

96-102: LGTM: Effect synchronizes store state with space data.

The effect correctly initializes currentSpaceId and currentTabName from the space page data with proper dependencies.


147-153: LGTM: Effect loads editable spaces when user authenticates.

The effect correctly triggers space loading when the user signs in, with proper error handling.


156-169: LGTM: Effect loads space data when IDs are set.

The effect correctly loads space tab order and tab data when both currentSpaceId and currentTabName are available, with proper error handling.


196-329: LGTM: Space registration effect with proper orchestration.

The effect correctly handles space registration for different space types (token, proposal, profile, channel) with proper early returns, error handling, and post-registration data loading.


354-358: LGTM: commitConfig callback.

The callback correctly commits the space tab with proper dependencies.


360-377: LGTM: resetConfig callback.

The callback correctly resets the config to either the default or remote config with proper dependencies.


467-477: LGTM: SpacePage rendering.

The component correctly returns SpacePage with all required props including the computed config.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/app/(spaces)/PublicSpace.tsx (2)

104-109: Remove useCallback wrapper or fix dependencies (regression from previous review).

This was previously flagged and marked as addressed, but the issue remains. The callback includes currentSpaceId and currentTabName in dependencies, yet getCurrentSpaceConfig() reads these values fresh from the store via get(), making them redundant. More critically, the callback is called immediately on line 109 rather than being passed as a dependency, so the memoization serves no purpose.

Either remove the wrapper entirely:

-  const getConfig = useCallback(() => {
-    return getCurrentSpaceConfig();
-  }, [getCurrentSpaceConfig, currentSpaceId, currentTabName]);
-
-  const currentConfig = getConfig();
+  const currentConfig = getCurrentSpaceConfig();

Or if memoization is needed for other reasons, reduce the deps:

-  }, [getCurrentSpaceConfig, currentSpaceId, currentTabName]);
+  }, [getCurrentSpaceConfig]);

416-428: Critical: Remove invalid await on config (still unfixed from previous review).

Line 418 attempts to await config, but config (defined at lines 185-191) is a plain object or undefined, not a Promise. Awaiting it is incorrect and misleading, though it won't cause a runtime error. This critical issue was previously flagged but remains unfixed.

Apply this diff:

       renameTab={async (oldName, newName) => {
         if (currentSpaceId) {
-          const resolvedConfig = await config;
           return renameSpaceTab(
             currentSpaceId,
             oldName,
             newName,
-            resolvedConfig,
+            config,
             isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
           );
         }
         return undefined;
       }}
🧹 Nitpick comments (2)
src/app/(spaces)/PublicSpace.tsx (2)

329-350: Remove unnecessary dependency from saveConfig.

The dependency config?.fidgetInstanceDatums on line 349 is not referenced in the callback body. The callback uses spaceConfig.fidgetInstanceDatums from the parameter instead. This unnecessary dependency causes the callback to be recreated whenever config.fidgetInstanceDatums changes, which is inefficient.

Apply this diff:

-  }, [currentSpaceId, currentTabName, saveLocalSpaceTab, config?.fidgetInstanceDatums]);
+  }, [currentSpaceId, currentTabName, saveLocalSpaceTab]);

181-191: Consider extracting config logic for clarity.

The nested ternary on lines 185-191 determines which config to use based on multiple conditions. While functionally correct, extracting this into a helper function or using if-else statements would improve readability.

Example refactor:

const config = useMemo(() => {
  // Tab loaded in store
  if (currentTabName && currentConfig?.tabs?.[currentTabName]) {
    return {
      ...currentConfig.tabs[currentTabName],
      isEditable,
    };
  }
  
  // Viewing someone else's space (no currentSpaceId)
  if (!currentSpaceId) {
    return {
      ...spacePageData.config,
      isEditable,
    };
  }
  
  // Loading state - trigger Suspense
  return undefined;
}, [currentTabName, currentConfig, currentSpaceId, spacePageData.config, isEditable]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec3d4dc and 849154b.

📒 Files selected for processing (2)
  • src/app/(spaces)/PublicSpace.tsx (11 hunks)
  • src/app/home/[tabname]/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/home/[tabname]/page.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(spaces)/PublicSpace.tsx (5)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfigSaveDetails (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-76)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/app/(spaces)/PublicSpace.tsx (2)

416-428: Critical: Remove invalid await on config.

Line 418 attempts to await config, but config is a plain object (defined at lines 185-191), not a Promise. This will cause resolvedConfig to simply be the config object itself, which works but is misleading and incorrect usage of await.

Note: This issue was flagged in a previous review and marked as "Addressed in commit 849154b", but the fix is not present in the current code.

Apply this diff:

       renameTab={async (oldName, newName) => {
         if (currentSpaceId) {
-          const resolvedConfig = await config;
           return renameSpaceTab(
             currentSpaceId,
             oldName,
             newName,
-            resolvedConfig,
+            config,
             isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
           );
         }
         return undefined;
       }}

104-109: Remove unnecessary useCallback wrapper.

The getConfig callback is invoked immediately on line 109 rather than being passed as a memoized dependency, making the wrapper pointless. Additionally, getCurrentSpaceConfig() already reads currentSpaceId and currentTabName from the store at call-time, so including them in the dependency array forces unnecessary recalculations.

Apply this diff to remove the wrapper:

-  // Get the current config using the store's getter
-  const getConfig = useCallback(() => {
-    return getCurrentSpaceConfig();
-  }, [getCurrentSpaceConfig, currentSpaceId, currentTabName]);
-
-  const currentConfig = getConfig();
+  // Get the current config using the store's getter
+  const currentConfig = getCurrentSpaceConfig();
🧹 Nitpick comments (1)
src/app/(spaces)/PublicSpace.tsx (1)

185-191: Memoize config to prevent unnecessary re-renders.

The config object is created on every render because it's not memoized. This creates a new object reference each time, potentially causing unnecessary re-renders of child components that receive config as a prop.

Apply this diff to memoize the config:

+  const config = useMemo(() => {
+    const baseConfig = currentTabName && currentConfig?.tabs?.[currentTabName] 
+      ? currentConfig.tabs[currentTabName]
+      : spacePageData.config;
+    
+    return {
+      ...baseConfig,
+      isEditable,
+    };
+  }, [currentTabName, currentConfig, spacePageData.config, isEditable]);
-  const config = currentTabName && currentConfig?.tabs?.[currentTabName] ? {
-    ...currentConfig.tabs[currentTabName],
-    isEditable,
-  } : {
-    ...spacePageData.config,
-    isEditable,
-  };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 849154b and 90e81d2.

📒 Files selected for processing (1)
  • src/app/(spaces)/PublicSpace.tsx (11 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(spaces)/PublicSpace.tsx (6)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfigSaveDetails (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-76)
🔇 Additional comments (3)
src/app/(spaces)/PublicSpace.tsx (3)

95-102: LGTM!

The effect correctly synchronizes spacePageData and providedTabName with the store-backed state.


146-153: LGTM!

The effect correctly loads editable spaces when the user signs in, with proper error handling.


155-169: LGTM!

The effect correctly loads space data when IDs are set, with proper error handling.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/common/components/organisms/TabBar.tsx (1)

174-182: Fix URL encoding in switchTabTo call.

Line 178 calls switchTabTo(encodeURIComponent(uniqueName), false), but switchTabTo expects a tab name, not a URL-encoded string. The getSpacePageUrl function inside switchTabTo should handle URL encoding if needed. Passing an already-encoded name could result in double-encoding or incorrect tab state.

Apply this diff:

         // Fire off rename (optimistic update happens immediately and handles order update)
         const renamePromise = renameTab(tabName, uniqueName);
         
         // Update current tab name in store and URL
-        switchTabTo(encodeURIComponent(uniqueName), false);
+        switchTabTo(uniqueName, false);
         
         // Wait for rename to complete, then commit
         await renamePromise;
🧹 Nitpick comments (3)
src/app/home/[tabname]/page.tsx (1)

69-73: Remove unused function.

The switchTabTo function is defined but never called in this file. Since the TabBar component now handles tab switching internally via its own switchTabTo implementation (using history.replaceState), this local function appears to be dead code.

Apply this diff to remove the unused function:

-  function switchTabTo(newTabName: string) {
-    // Update the store immediately for better responsiveness
-    setCurrentTabName(newTabName);
-    router.push(`/home/${newTabName}`);
-  }
-
   const tabBar = (
src/common/components/organisms/TabBar.tsx (2)

67-71: Remove unused parameter.

The shouldSave parameter is declared but never used in the function body. Either implement the intended behavior or remove the parameter.

Apply this diff to remove the unused parameter:

-  function switchTabTo(tabName: string, shouldSave: boolean = true) {
+  function switchTabTo(tabName: string) {
     setCurrentTabName(tabName);
     const url = getSpacePageUrl(tabName);
     window.history.replaceState(null, '', url);

Then update all call sites to remove the second argument:

-        switchTabTo(cleanTabName, true);
+        switchTabTo(cleanTabName);
-        switchTabTo(cleanTabName, false);
+        switchTabTo(cleanTabName);
-          switchTabTo(finalTabName, false);
+          switchTabTo(finalTabName);
-          switchTabTo(nextTab, true);
+          switchTabTo(nextTab);
-        switchTabTo(encodeURIComponent(uniqueName), false);
+        switchTabTo(encodeURIComponent(uniqueName));
-    switchTabTo(tabName, true);
+    switchTabTo(tabName);

82-116: Consider implementing rollback for failed tab creation.

The TODO comment at line 112 indicates that rollback logic for failed tab creation is not yet implemented. If tab creation fails after the optimistic UI update, the user will see the new tab in the UI but it won't exist on the server.

Do you want me to generate a rollback implementation that removes the tab from the UI if creation fails?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90e81d2 and a6cb439.

📒 Files selected for processing (2)
  • src/app/home/[tabname]/page.tsx (4 hunks)
  • src/common/components/organisms/TabBar.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/common/components/organisms/TabBar.tsx (3)
src/common/data/stores/app/currentSpace/index.ts (1)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (3)
  • createTab (229-309)
  • deleteTab (310-347)
  • renameTab (348-449)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/app/home/[tabname]/page.tsx (3)
src/common/data/stores/app/currentSpace/index.ts (1)
  • setCurrentTabName (51-55)
src/app/(spaces)/SpacePage.tsx (2)
  • SpacePageArgs (5-15)
  • SpacePage (17-76)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/common/components/organisms/TabBar.tsx (1)

266-284: Don’t allow the default tab to be draggable

Default tab is rendered in the list but values excludes it. With draggable={inEditMode}, it can become a Reorder.Item and desync the list.

-                      draggable={inEditMode}
+                      draggable={inEditMode && isEditableTab(tabName, defaultTab)}

Optionally ensure the Tab component does not render a Reorder.Item for non-draggable items.

♻️ Duplicate comments (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)

409-411: LGTM: active tab follows rename

Updating draft.currentSpace.currentTabName during optimistic rename fixes the prior selection loss.

🧹 Nitpick comments (7)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (3)

25-25: Replace Moment with native Date to reduce bundle size

Use Date.parse and new Date().toISOString() instead of moment. Same behavior, fewer deps.

Based on learnings

-import moment from "moment";
+// import moment from "moment"; // not needed; use native Date
-      if (existingTab?.config?.timestamp && 
-          remoteConfig.timestamp &&
-          moment(existingTab.config.timestamp).isAfter(moment(remoteConfig.timestamp))) {
+      if (
+        existingTab?.config?.timestamp &&
+        remoteConfig.timestamp &&
+        Date.parse(existingTab.config.timestamp) > Date.parse(remoteConfig.timestamp)
+      ) {
-    // Update timestamp to mark this as a local change
-    localCopy.timestamp = moment().toISOString();
+    // Update timestamp to mark this as a local change
+    localCopy.timestamp = new Date().toISOString();

Also applies to: 493-496, 571-572


470-477: Avoid stale tab config by cache-busting the GET

Add a ?t= param like you do for tab ordering to defeat CDN/browser caches.

-      const { data } = await axios.get<Blob>(publicUrl, {
+      const t = Date.now();
+      const urlWithParam = `${publicUrl}?t=${t}`;
+      const { data } = await axios.get<Blob>(urlWithParam, {
         responseType: "blob",
         headers: {
           "Cache-Control": "no-cache",
           Pragma: "no-cache",
           Expires: "0",
         },
       });

334-345: Optional: keep currentTabName valid when deleting current tab

Store method doesn’t adjust currentTabName if the active tab is deleted (UI handles it in TabBar). Consider a defensive update here to avoid stale state when deleteTab is called from other flows.

         // Commit the updated tab order to the database
         await get().homebase.commitTabOrderingToDatabase();
+
+        // Optional guard: if the deleted tab was active, switch to a safe fallback
+        set((draft) => {
+          if (draft.currentSpace.currentTabName === tabName) {
+            const next = draft.homebase.tabOrdering.local[0] ?? HOMEBASE_DEFAULT_TAB;
+            draft.currentSpace.currentTabName = next;
+          }
+        }, "deleteHomebaseTab:ensureCurrentTab");
src/common/components/organisms/TabBar.tsx (2)

68-72: Remove unused shouldSave parameter from switchTabTo

Param isn’t used. Simplify signature and calls.

-  function switchTabTo(tabName: string, shouldSave: boolean = true) {
+  const switchTabTo = React.useCallback((tabName: string) => {
     setCurrentTabName(tabName);
     const url = getSpacePageUrl(tabName);
     window.history.replaceState(null, '', url);
-  }
+  }, [getSpacePageUrl, setCurrentTabName]);
-        switchTabTo(cleanTabName, true);
+        switchTabTo(cleanTabName);
-      switchTabTo(cleanTabName, false);
+      switchTabTo(cleanTabName);
-        if (finalTabName !== cleanTabName) {
-          switchTabTo(finalTabName, false);
-        }
+        if (finalTabName !== cleanTabName) {
+          switchTabTo(finalTabName);
+        }
-        if (shouldSwitch && nextTab) {
-          switchTabTo(nextTab, true);
-        }
+        if (shouldSwitch && nextTab) {
+          switchTabTo(nextTab);
+        }
-    switchTabTo(tabName, true);
+    switchTabTo(tabName);

Also applies to: 91-93, 96-97, 104-107, 139-142, 226-227


259-266: Verify Reorder values vs rendered items

values={tabList} while rendering [HOMEBASE_DEFAULT_TAB, ...tabList] for homebase. If Tab renders a Reorder.Item for the default tab, this mismatch can break ordering. Either ensure the default tab is not a Reorder.Item (non-draggable) or include it in values and filter it before persisting.

If needed:

-                values={tabList}
+                values={inHomebase ? [HOMEBASE_DEFAULT_TAB, ...tabList] : tabList}
                 onReorder={debouncedReorder}

And filter before commit:

-    debounce((newOrder) => {
-      updateTabOrder(newOrder);
+    debounce((newOrder) => {
+      const filtered = newOrder.filter((n: string) => isEditableTab(n, defaultTab));
+      updateTabOrder(filtered);
       commitTabOrder();
     }, 300)

Also applies to: 266-268, 238-243

src/app/(spaces)/homebase/PrivateSpace.tsx (1)

176-205: Optional: allow Suspense instead of default config fallback

Returning a default config prevents SpacePage’s Suspense from showing a loading state and can cause a brief “flash” of default layout. Consider returning undefined until the active config is available.

docs/TAB_MANAGEMENT_SYSTEM.md (1)

40-44: Specify language for fenced code block (markdownlint MD040)

Add a language (e.g., text) to the ASCII diagram block.

-```
+```text
 URL Params → Store Sync → Tab Loading → Config Resolution → UI Render
      ↓            ↓            ↓              ↓              ↓
   tabName → currentTabName → loadTab → getConfig → SpacePage

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a6cb4396a94c1070c50d475b528ab01844fea74e and 8278d61ac5254d4ad44eeea35d24a617bb6f2223.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `docs/TAB_MANAGEMENT_SYSTEM.md` (1 hunks)
* `src/app/(spaces)/homebase/PrivateSpace.tsx` (9 hunks)
* `src/common/components/organisms/TabBar.tsx` (8 hunks)
* `src/common/data/stores/app/homebase/homebaseTabsStore.ts` (7 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (3)</summary>

<details>
<summary>src/common/data/stores/app/homebase/homebaseTabsStore.ts (4)</summary><blockquote>

<details>
<summary>src/common/utils/tabUtils.ts (3)</summary>

* `validateTabName` (11-16)
* `isDuplicateTabName` (25-34)
* `withOptimisticUpdate` (46-74)

</details>
<details>
<summary>src/pages/api/space/homebase/tabs/index.ts (1)</summary>

* `UnsignedManageHomebaseTabsRequest` (24-29)

</details>
<details>
<summary>src/common/lib/signedFiles.ts (1)</summary>

* `signSignable` (54-62)

</details>
<details>
<summary>src/app/(spaces)/Space.tsx (1)</summary>

* `SpaceConfig` (37-49)

</details>

</blockquote></details>
<details>
<summary>src/common/components/organisms/TabBar.tsx (3)</summary><blockquote>

<details>
<summary>src/common/data/stores/app/currentSpace/index.ts (1)</summary>

* `setCurrentTabName` (51-55)

</details>
<details>
<summary>src/common/data/stores/app/homebase/homebaseTabsStore.ts (4)</summary>

* `createTab` (232-312)
* `deleteTab` (313-350)
* `renameTab` (351-456)
* `HOMEBASE_DEFAULT_TAB` (35-35)

</details>
<details>
<summary>src/common/utils/tabUtils.ts (1)</summary>

* `validateTabName` (11-16)

</details>

</blockquote></details>
<details>
<summary>src/app/(spaces)/homebase/PrivateSpace.tsx (4)</summary><blockquote>

<details>
<summary>src/common/data/stores/app/currentSpace/index.ts (3)</summary>

* `setCurrentSpaceId` (43-47)
* `HOMEBASE_ID` (25-25)
* `setCurrentTabName` (51-55)

</details>
<details>
<summary>src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)</summary>

* `HOMEBASE_DEFAULT_TAB` (35-35)

</details>
<details>
<summary>src/app/(spaces)/SpacePage.tsx (2)</summary>

* `SpacePageArgs` (5-15)
* `SpacePage` (17-76)

</details>
<details>
<summary>src/constants/initialSpaceConfig.ts (1)</summary>

* `INITIAL_SPACE_CONFIG_EMPTY` (8-20)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/TAB_MANAGEMENT_SYSTEM.md</summary>

[grammar] ~9-~9: There might be a mistake here.
Context: ...able of Contents  - [System Architecture](#system-architecture) - [Store Comparison](#store-comparison) - [...

(QB_NEW_EN)

---

[grammar] ~10-~10: There might be a mistake here.
Context: ...system-architecture) - [Store Comparison](#store-comparison) - [State Management Patterns](#state-manage...

(QB_NEW_EN)

---

[grammar] ~11-~11: There might be a mistake here.
Context: ...comparison) - [State Management Patterns](#state-management-patterns) - [Tab Operations](#tab-operations) - [Opti...

(QB_NEW_EN)

---

[grammar] ~12-~12: There might be a mistake here.
Context: ...e-management-patterns) - [Tab Operations](#tab-operations) - [Optimistic Updates](#optimistic-updates)...

(QB_NEW_EN)

---

[grammar] ~13-~13: There might be a mistake here.
Context: ...](#tab-operations) - [Optimistic Updates](#optimistic-updates) - [Component Integration](#component-integr...

(QB_NEW_EN)

---

[grammar] ~14-~14: There might be a mistake here.
Context: ...mistic-updates) - [Component Integration](#component-integration) - [Performance Considerations](#performance...

(QB_NEW_EN)

---

[grammar] ~15-~15: There might be a mistake here.
Context: ...tegration) - [Performance Considerations](#performance-considerations) - [Security Model](#security-model) - [Migr...

(QB_NEW_EN)

---

[grammar] ~16-~16: There might be a mistake here.
Context: ...rmance-considerations) - [Security Model](#security-model) - [Migration Guide](#migration-guide)  ## S...

(QB_NEW_EN)

---

[grammar] ~34-~34: There might be a mistake here.
Context: ...tils.ts` - Shared utilities and patterns    - Validation functions    - Optimistic upd...

(QB_NEW_EN)

---

[grammar] ~35-~35: There might be a mistake here.
Context: ...s and patterns    - Validation functions    - Optimistic update helpers  ### State Flo...

(QB_NEW_EN)

---

[grammar] ~70-~70: There might be a mistake here.
Context: ...rder   } } ```  **Key Characteristics**: - ✅ Encrypted storage (Supabase Private) -...

(QB_NEW_EN)

---

[grammar] ~71-~71: There might be a mistake here.
Context: ...- ✅ Encrypted storage (Supabase Private) - ✅ Single user only - ✅ Simple local/remo...

(QB_NEW_EN)

---

[grammar] ~72-~72: There might be a mistake here.
Context: ... (Supabase Private) - ✅ Single user only - ✅ Simple local/remote state pattern - ✅ ...

(QB_NEW_EN)

---

[grammar] ~73-~73: There might be a mistake here.
Context: ...ly - ✅ Simple local/remote state pattern - ✅ Direct API endpoints - ✅ Timestamp-bas...

(QB_NEW_EN)

---

[grammar] ~74-~74: There might be a mistake here.
Context: ...e state pattern - ✅ Direct API endpoints - ✅ Timestamp-based conflict resolution  #...

(QB_NEW_EN)

---

[grammar] ~101-~101: There might be a mistake here.
Context: ...r fields } ```  **Key Characteristics**: - ❌ Unencrypted storage (Supabase Public) ...

(QB_NEW_EN)

---

[grammar] ~102-~102: There might be a mistake here.
Context: ... ❌ Unencrypted storage (Supabase Public) - ✅ Multi-user support - ✅ Complex state m...

(QB_NEW_EN)

---

[grammar] ~103-~103: There might be a mistake here.
Context: ...(Supabase Public) - ✅ Multi-user support - ✅ Complex state management - ✅ Registry ...

(QB_NEW_EN)

---

[grammar] ~104-~104: There might be a mistake here.
Context: ...ser support - ✅ Complex state management - ✅ Registry API system - ✅ Advanced confl...

(QB_NEW_EN)

---

[grammar] ~105-~105: There might be a mistake here.
Context: ...state management - ✅ Registry API system - ✅ Advanced conflict resolution  ## State...

(QB_NEW_EN)

---

[grammar] ~448-~448: There might be a mistake here.
Context: ... All data encrypted with user's root key - **Storage**: Supabase Private Storage (enc...

(QB_NEW_EN)

---

[grammar] ~449-~449: There might be a mistake here.
Context: ...base Private Storage (encrypted at rest) - **Access**: Single user only - **Authentic...

(QB_NEW_EN)

---

[grammar] ~450-~450: There might be a mistake here.
Context: ... at rest) - **Access**: Single user only - **Authentication**: Cryptographic signatur...

(QB_NEW_EN)

---

[grammar] ~455-~455: There might be a mistake here.
Context: ...es  - **Encryption**: None (public data) - **Storage**: Supabase Public Storage - **A...

(QB_NEW_EN)

---

[grammar] ~456-~456: There might be a mistake here.
Context: ...) - **Storage**: Supabase Public Storage - **Access**: Multi-user, public - **Authent...

(QB_NEW_EN)

---

[grammar] ~457-~457: There might be a mistake here.
Context: ...Storage - **Access**: Multi-user, public - **Authentication**: API-based permissions ...

(QB_NEW_EN)

---

[grammar] ~563-~563: There might be a mistake here.
Context: ...window.history.replaceState` instead of `router.push` 4. **Flash of Default Config**: Return `undef...

(QB_NEW_EN)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

<details>
<summary>docs/TAB_MANAGEMENT_SYSTEM.md</summary>

40-40: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)</summary><blockquote>

`153-153`: **Backend route verification complete—no issues found.**

The route file `src/pages/api/space/homebase/tabOrder.ts` exists and is properly configured. It exports a default handler with POST support via `requestHandler({ post: updateTabOrder })`, which will correctly handle the frontend call to `/api/space/homebase/tabOrder`. No 404 risk.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines 19 to 35
interface TabBarProps {
inHome?: boolean;
inHomebase: boolean;
inEditMode: boolean;
currentTab: string;
tabList: string[];
defaultTab: string;
updateTabOrder: (newOrder: string[]) => void;
commitTabOrder: () => void;
switchTabTo: (tabName: string, shouldSave?: boolean) => void;
deleteTab: (tabName: string) => void;
createTab: (tabName: string) => Promise<{ tabName: string } | undefined>;
renameTab: (tabName: string, newName: string) => void;
renameTab: (tabName: string, newName: string) => Promise<void> | void;
commitTab: (tabName: string) => void;
getSpacePageUrl: (tabName: string) => string;
isTokenPage?: boolean;
contractAddress?: Address;
isEditable?: boolean;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix TabBar prop types to match usage

deleteTab is awaited; commitTab may be async. Update types.

-  deleteTab: (tabName: string) => void;
+  deleteTab: (tabName: string) => Promise<void>;
   createTab: (tabName: string) => Promise<{ tabName: string } | undefined>;
-  renameTab: (tabName: string, newName: string) => Promise<void> | void;
-  commitTab: (tabName: string) => void;
+  renameTab: (tabName: string, newName: string) => Promise<void> | void;
+  commitTab: (tabName: string) => Promise<void> | void;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface TabBarProps {
inHome?: boolean;
inHomebase: boolean;
inEditMode: boolean;
currentTab: string;
tabList: string[];
defaultTab: string;
updateTabOrder: (newOrder: string[]) => void;
commitTabOrder: () => void;
switchTabTo: (tabName: string, shouldSave?: boolean) => void;
deleteTab: (tabName: string) => void;
createTab: (tabName: string) => Promise<{ tabName: string } | undefined>;
renameTab: (tabName: string, newName: string) => void;
renameTab: (tabName: string, newName: string) => Promise<void> | void;
commitTab: (tabName: string) => void;
getSpacePageUrl: (tabName: string) => string;
isTokenPage?: boolean;
contractAddress?: Address;
isEditable?: boolean;
}
interface TabBarProps {
inHomebase: boolean;
inEditMode: boolean;
currentTab: string;
tabList: string[];
defaultTab: string;
updateTabOrder: (newOrder: string[]) => void;
commitTabOrder: () => void;
deleteTab: (tabName: string) => Promise<void>;
createTab: (tabName: string) => Promise<{ tabName: string } | undefined>;
renameTab: (tabName: string, newName: string) => Promise<void> | void;
commitTab: (tabName: string) => Promise<void> | void;
getSpacePageUrl: (tabName: string) => string;
isTokenPage?: boolean;
contractAddress?: Address;
isEditable?: boolean;
}
🤖 Prompt for AI Agents
In src/common/components/organisms/TabBar.tsx around lines 19 to 35, the
TabBarProps types don't match usage: deleteTab is awaited and commitTab may be
async. Change deleteTab to return a Promise (e.g., Promise<void>) and make
commitTab async by returning a Promise (e.g., Promise<void>) as well (or union
Promise<void> | void if still used synchronously elsewhere) so the prop
signatures reflect their actual asynchronous usage.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)

338-370: Guard against deleting the default tab

Defensive check in the store prevents accidental/indirect deletion even if UI guards fail.

   async deleteTab(tabName) {
     // console.log('Deleting tab:', { tabName });
     const publicKey = get().account.currentSpaceIdentityPublicKey;
     if (!publicKey) return;
+    if (tabName === HOMEBASE_DEFAULT_TAB) {
+      console.warn("Cannot delete the default tab.");
+      return;
+    }
♻️ Duplicate comments (1)
src/common/components/organisms/TabBar.tsx (1)

18-34: Align TabBar prop types with async usage

deleteTab and commitTab are awaited/async. Update types to avoid confusion. This mirrors a prior comment.

 interface TabBarProps {
   inHomebase: boolean;
   inEditMode: boolean;
   currentTab: string;
   tabList: string[];
   defaultTab: string;
   updateTabOrder: (newOrder: string[]) => void;
   commitTabOrder: () => void;
-  deleteTab: (tabName: string) => void;
+  deleteTab: (tabName: string) => Promise<void>;
   createTab: (tabName: string) => Promise<{ tabName: string } | undefined>;
-  renameTab: (tabName: string, newName: string) => Promise<void> | void;
-  commitTab: (tabName: string) => void;
+  renameTab: (tabName: string, newName: string) => Promise<void> | void;
+  commitTab: (tabName: string) => Promise<void> | void;
   getSpacePageUrl: (tabName: string) => string;
   isTokenPage?: boolean;
   contractAddress?: Address;
   isEditable?: boolean;
 }
🧹 Nitpick comments (8)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (2)

148-176: Pin default tab at commit-time to prevent drift

Users can reorder the default away from index 0; this persists to remote. Re-pin before commit so the invariant holds consistently.

-  const localCopy = cloneDeep(
-    get().homebase.tabOrdering.local.filter((name, i, arr) =>
-      arr.indexOf(name) === i,
-    ),
-  );
+  const deduped = get().homebase.tabOrdering.local.filter((name, i, arr) => arr.indexOf(name) === i);
+  let localCopy = cloneDeep(deduped);
+  if (!localCopy.includes(HOMEBASE_DEFAULT_TAB)) {
+    localCopy.unshift(HOMEBASE_DEFAULT_TAB);
+  } else if (localCopy[0] !== HOMEBASE_DEFAULT_TAB) {
+    localCopy = [HOMEBASE_DEFAULT_TAB, ...localCopy.filter((n) => n !== HOMEBASE_DEFAULT_TAB)];
+  }

595-597: Prefer native Date for ISO timestamps (drop moment dependency here)

Simplify and avoid extra dependency for trivial ISO stamps.

-    localCopy.timestamp = moment().toISOString();
+    localCopy.timestamp = new Date().toISOString();
src/common/data/stores/app/homebase/homebaseStore.ts (1)

73-79: Preserve remote snapshot when local wins

Keep remoteHomebaseConfig updated even if you return the local config, so reset operations have the latest remote.

-      if (currentHomebase && 
-          currentHomebase.timestamp && 
-          spaceConfig && 
-          (!spaceConfig.timestamp || moment(currentHomebase.timestamp).isAfter(moment(spaceConfig.timestamp)))) {
-        return cloneDeep(currentHomebase);
-      }
+      if (
+        currentHomebase &&
+        currentHomebase.timestamp &&
+        spaceConfig &&
+        (!spaceConfig.timestamp ||
+          moment(currentHomebase.timestamp).isAfter(moment(spaceConfig.timestamp)))
+      ) {
+        set((draft) => {
+          draft.homebase.remoteHomebaseConfig = cloneDeep(spaceConfig);
+        }, "loadHomebase-remote-only");
+        return cloneDeep(currentHomebase);
+      }
src/app/(spaces)/homebase/PrivateSpace.tsx (2)

143-149: Type the save handler parameter

Use SpaceConfigSaveDetails for better type-safety.

-  const saveConfigHandler = async (configToSave) => {
+  const saveConfigHandler = async (configToSave: SpaceConfigSaveDetails) => {

Add to imports:

-import SpacePage, { SpacePageArgs } from "@/app/(spaces)/Space";
+import SpacePage, { SpacePageArgs, SpaceConfigSaveDetails } from "@/app/(spaces)/Space";

102-111: Skip redundant load for default tab

loadHomebaseTab no-ops for the default; just load the feed config.

-    if (!isNil(currentTabName)) {
-      if (currentTabName === HOMEBASE_DEFAULT_TAB && !homebaseConfig) {
-        loadFeedConfig().then(() => loadTab(currentTabName));
-      } else {
-        loadTab(currentTabName);
-      }
-    }
+    if (!isNil(currentTabName)) {
+      if (currentTabName === HOMEBASE_DEFAULT_TAB) {
+        if (!homebaseConfig) void loadFeedConfig();
+      } else {
+        loadTab(currentTabName);
+      }
+    }
src/common/components/organisms/TabBar.tsx (3)

236-242: Keep default tab pinned at index 0 on reorder

Prevents “Feed” drifting via drag-and-drop.

-  const debouncedReorder = React.useCallback(
-    debounce((newOrder) => {
-      updateTabOrder(newOrder);
-      commitTabOrder();
-    }, 300),
-    [updateTabOrder, commitTabOrder]
-  );
+  const debouncedReorder = React.useCallback(
+    debounce((newOrder: string[]) => {
+      const normalized = [defaultTab, ...newOrder.filter((n) => n !== defaultTab)];
+      updateTabOrder(normalized);
+      commitTabOrder();
+    }, 300),
+    [updateTabOrder, commitTabOrder, defaultTab]
+  );

266-281: Disable dragging for the default tab

UI affordance to match the pinned-first invariant.

-                      draggable={inEditMode}
+                      draggable={inEditMode && isEditableTab(tabName, defaultTab)}

97-114: Rollback active tab on create failures

If create fails after optimistic switch, revert to previous tab to avoid a stuck URL/selection.

-      } catch (error) {
-        console.error("Error creating tab:", error);
-        toast.error("Failed to create tab. Please try again.");
-        // TODO: Rollback optimistic changes if needed
-      }
+      } catch (error) {
+        console.error("Error creating tab:", error);
+        toast.error("Failed to create tab. Please try again.");
+        // Roll back optimistic switch
+        switchTabTo(currentTab);
+      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8278d61 and 63fd5fd.

📒 Files selected for processing (4)
  • src/app/(spaces)/homebase/PrivateSpace.tsx (8 hunks)
  • src/common/components/organisms/TabBar.tsx (7 hunks)
  • src/common/data/stores/app/homebase/homebaseStore.ts (1 hunks)
  • src/common/data/stores/app/homebase/homebaseTabsStore.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (4)
src/common/utils/tabUtils.ts (3)
  • validateTabName (11-16)
  • isDuplicateTabName (25-34)
  • withOptimisticUpdate (46-74)
src/pages/api/space/homebase/tabs/index.ts (1)
  • UnsignedManageHomebaseTabsRequest (24-29)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/common/components/organisms/TabBar.tsx (3)
src/common/data/stores/app/currentSpace/index.ts (1)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (3)
  • createTab (257-337)
  • deleteTab (338-375)
  • renameTab (376-481)
src/common/utils/tabUtils.ts (1)
  • validateTabName (11-16)
src/app/(spaces)/homebase/PrivateSpace.tsx (3)
src/common/data/stores/app/currentSpace/index.ts (3)
  • setCurrentSpaceId (43-47)
  • HOMEBASE_ID (25-25)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (2)
  • loadTabNames (176-256)
  • HOMEBASE_DEFAULT_TAB (35-35)
src/app/(spaces)/SpacePage.tsx (2)
  • SpacePageArgs (5-15)
  • SpacePage (17-76)

Comment on lines +380 to +399
const sanitizedNewName = newName.trim();
if (!sanitizedNewName || sanitizedNewName === tabName) {
return;
}

// Validate tab name using shared utility
const validationError = validateTabName(sanitizedNewName);
if (validationError) {
const error = new Error(validationError);
(error as any).status = 400;
throw error;
}

const req: UnsignedManageHomebaseTabsRequest = {
publicKey,
type: "rename",
tabName,
newName,
};
const signedReq = await signSignable(
req,
get().account.getCurrentIdentity()!.rootKeys.privateKey,
// Check for duplicates - should not happen since UI ensures uniqueness
// but log a warning just in case
const existingTabs = get().homebase.tabOrdering.local;
if (isDuplicateTabName(sanitizedNewName, existingTabs, tabName)) {
console.warn(`Tab name "${sanitizedNewName}" already exists - UI should have ensured uniqueness`);
return; // Exit gracefully instead of throwing
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Disallow renaming the default tab

Renaming the canonical default breaks assumptions across ordering and routing. Block it early.

   const sanitizedNewName = newName.trim();
   if (!sanitizedNewName || sanitizedNewName === tabName) {
     return;
   }
+  if (tabName === HOMEBASE_DEFAULT_TAB) {
+    console.warn("Cannot rename the default tab.");
+    return;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sanitizedNewName = newName.trim();
if (!sanitizedNewName || sanitizedNewName === tabName) {
return;
}
// Validate tab name using shared utility
const validationError = validateTabName(sanitizedNewName);
if (validationError) {
const error = new Error(validationError);
(error as any).status = 400;
throw error;
}
const req: UnsignedManageHomebaseTabsRequest = {
publicKey,
type: "rename",
tabName,
newName,
};
const signedReq = await signSignable(
req,
get().account.getCurrentIdentity()!.rootKeys.privateKey,
// Check for duplicates - should not happen since UI ensures uniqueness
// but log a warning just in case
const existingTabs = get().homebase.tabOrdering.local;
if (isDuplicateTabName(sanitizedNewName, existingTabs, tabName)) {
console.warn(`Tab name "${sanitizedNewName}" already exists - UI should have ensured uniqueness`);
return; // Exit gracefully instead of throwing
}
const sanitizedNewName = newName.trim();
if (!sanitizedNewName || sanitizedNewName === tabName) {
return;
}
if (tabName === HOMEBASE_DEFAULT_TAB) {
console.warn("Cannot rename the default tab.");
return;
}
// Validate tab name using shared utility
const validationError = validateTabName(sanitizedNewName);
if (validationError) {
const error = new Error(validationError);
(error as any).status = 400;
throw error;
}
// Check for duplicates - should not happen since UI ensures uniqueness
// but log a warning just in case
const existingTabs = get().homebase.tabOrdering.local;
if (isDuplicateTabName(sanitizedNewName, existingTabs, tabName)) {
console.warn(`Tab name "${sanitizedNewName}" already exists - UI should have ensured uniqueness`);
return; // Exit gracefully instead of throwing
}
🤖 Prompt for AI Agents
In src/common/data/stores/app/homebase/homebaseTabsStore.ts around lines 380 to
399, add an early guard that prevents renaming the canonical default tab: detect
when the current tabName matches the app's DEFAULT_TAB constant (import it if
not present) and immediately throw a 400 error (new Error with status = 400)
with a clear message like "Renaming the default tab is not allowed" instead of
proceeding to validation or duplicate checks; ensure the check occurs before
trimming/validating the new name so the default tab cannot be changed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (3)

198-240: Keep state consistent when remote tabs are removed; also ensure default placeholder

  • If a tab is removed remotely and was active, currentTabName remains pointing to a deleted tab.
  • Also ensure tabs entry for the default exists when enforcing it to be first.

Apply within this set block:

           currentTabNames.forEach(tabName => {
             if (!validTabNames.includes(tabName)) {
               delete draft.homebase.tabs[tabName];
             }
           });
@@
           draft.homebase.tabOrdering.local = draft.homebase.tabOrdering.local.filter(
             tabName => validTabNames.includes(tabName)
           );
 
+          // If current selection no longer exists, switch to a safe default
+          if (!draft.homebase.tabOrdering.local.includes(draft.currentSpace.currentTabName)) {
+            draft.currentSpace.currentTabName =
+              draft.homebase.tabOrdering.local[0] ?? HOMEBASE_DEFAULT_TAB;
+          }
@@
           if (!draft.homebase.tabOrdering.local.includes(HOMEBASE_DEFAULT_TAB)) {
             draft.homebase.tabOrdering.local.unshift(HOMEBASE_DEFAULT_TAB);
           } else {
             // If default tab exists but isn't first, move it to the front
             const defaultTabIndex = draft.homebase.tabOrdering.local.indexOf(HOMEBASE_DEFAULT_TAB);
             if (defaultTabIndex > 0) {
               draft.homebase.tabOrdering.local.splice(defaultTabIndex, 1);
               draft.homebase.tabOrdering.local.unshift(HOMEBASE_DEFAULT_TAB);
             }
           }
+          // Ensure a placeholder exists for default so the order never references a missing tab
+          if (!draft.homebase.tabs[HOMEBASE_DEFAULT_TAB]) {
+            draft.homebase.tabs[HOMEBASE_DEFAULT_TAB] = {};
+          }

359-366: Handle active-tab deletion (and consider disallowing default deletion)

If you delete the active tab, selection becomes invalid. Switch to default or the first tab.

Apply within this set block:

           draft.homebase.tabOrdering.local = [...draft.homebase.tabOrdering.local]
             .filter(name => name !== tabName);
+          // If we deleted the active tab, switch to a safe fallback
+          if (draft.currentSpace.currentTabName === tabName) {
+            const next = draft.homebase.tabOrdering.local[0] ?? HOMEBASE_DEFAULT_TAB;
+            draft.currentSpace.currentTabName = next;
+          }

Additionally, add an early guard near the start of deleteTab to block deleting the default:

if (tabName === HOMEBASE_DEFAULT_TAB) {
  console.warn("Cannot delete the default tab.");
  return;
}

262-267: Create-tab duplicate check should be case-insensitive

Current check is key-existence only. You can create “feed” alongside “Feed”. Align with rename’s duplicate semantics and treat it as “open existing”.

Apply:

-    // Check if tab already exists
-    if (get().homebase.tabs[tabName]) {
+    // Check if tab already exists (case-insensitive) and treat as existing
+    const existingNames = Object.keys(get().homebase.tabs);
+    const canonical = existingNames.find((n) => n.toLowerCase() === tabName.toLowerCase());
+    if (canonical) {
       // console.log('Tab already exists:', { tabName });
       // If tab exists but doesn't have remote state, load it
-      if (!get().homebase.tabs[tabName]?.remoteConfig) {
-        await get().homebase.loadHomebaseTab(tabName);
+      if (!get().homebase.tabs[canonical]?.remoteConfig) {
+        await get().homebase.loadHomebaseTab(canonical);
       }
 
       // Add it back to the tab order if it's not already there
       set((draft) => {
-        if (!draft.homebase.tabOrdering.local.includes(tabName)) {
-          draft.homebase.tabOrdering.local.push(tabName);
+        if (!draft.homebase.tabOrdering.local.includes(canonical)) {
+          draft.homebase.tabOrdering.local.push(canonical);
         }
       }, "addExistingTabToOrder");
       return get().homebase.commitTabOrderingToDatabase();
     }

Also applies to: 269-283

♻️ Duplicate comments (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)

380-400: Block renaming the default tab; it breaks the default-in-first-order invariant

Your ordering logic always re-inserts HOMEBASE_DEFAULT_TAB = "Feed". Renaming the default will desync ordering vs tabs.

Apply an early guard:

   const sanitizedNewName = newName.trim();
   if (!sanitizedNewName || sanitizedNewName === tabName) {
     return;
   }
+  if (tabName === HOMEBASE_DEFAULT_TAB) {
+    const error = new Error("Renaming the default tab is not allowed");
+    (error as any).status = 400;
+    throw error;
+  }
🧹 Nitpick comments (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)

25-25: Optional: avoid Moment for simple ISO timestamps

Native Date suffices (smaller bundle, fewer deps). Consider replacing Moment usage here.

Based on learnings

Suggested changes:

  • Replace comparison:
    • new Date(existingTab.config.timestamp!).getTime() > new Date(remoteConfig.timestamp!).getTime()
  • Replace timestamp generation:
    • new Date().toISOString()
  • Remove import moment from "moment";

Also applies to: 529-535, 614-616

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63fd5fd and 13ff1cb.

📒 Files selected for processing (1)
  • src/common/data/stores/app/homebase/homebaseTabsStore.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (4)
src/common/utils/tabUtils.ts (3)
  • validateTabName (11-16)
  • isDuplicateTabName (25-34)
  • withOptimisticUpdate (46-74)
src/pages/api/space/homebase/tabs/index.ts (1)
  • UnsignedManageHomebaseTabsRequest (24-29)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
🔇 Additional comments (2)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (2)

434-436: Good fix: active tab follows rename

Updating currentSpace.currentTabName ensures UI stays consistent during rename.


166-166: API route change verified as valid and safe

The endpoint /api/space/homebase/tabOrder is properly implemented at src/pages/api/space/homebase/tabOrder.ts with a POST handler that validates and processes signed file uploads. The client call at line 166 in homebaseTabsStore.ts correctly invokes this endpoint, and no lingering references to old paths exist in the codebase.

Comment on lines +124 to +136
// Ensure default tab is always first in the loaded order
const orderedTabs = clone(tabOrder);
if (!orderedTabs.includes(HOMEBASE_DEFAULT_TAB)) {
orderedTabs.unshift(HOMEBASE_DEFAULT_TAB);
} else {
// If default tab exists but isn't first, move it to the front
const defaultTabIndex = orderedTabs.indexOf(HOMEBASE_DEFAULT_TAB);
if (defaultTabIndex > 0) {
orderedTabs.splice(defaultTabIndex, 1);
orderedTabs.unshift(HOMEBASE_DEFAULT_TAB);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid order referencing a non-existent default tab

You force-insert the default into ordering but don’t ensure a tabs entry exists. This can break lookups and lazy loads. Create a placeholder when adding the default.

Apply inside this set block:

         const orderedTabs = clone(tabOrder);
         if (!orderedTabs.includes(HOMEBASE_DEFAULT_TAB)) {
           orderedTabs.unshift(HOMEBASE_DEFAULT_TAB);
         } else {
           // If default tab exists but isn't first, move it to the front
           const defaultTabIndex = orderedTabs.indexOf(HOMEBASE_DEFAULT_TAB);
           if (defaultTabIndex > 0) {
             orderedTabs.splice(defaultTabIndex, 1);
             orderedTabs.unshift(HOMEBASE_DEFAULT_TAB);
           }
         }
+        // Ensure a placeholder exists for default so order never points to a missing tab
+        if (!draft.homebase.tabs[HOMEBASE_DEFAULT_TAB]) {
+          draft.homebase.tabs[HOMEBASE_DEFAULT_TAB] = {};
+        }

Optionally trigger a lazy load after the set:

// After set(...)
void get().homebase.loadHomebaseTab(HOMEBASE_DEFAULT_TAB);
🤖 Prompt for AI Agents
In src/common/data/stores/app/homebase/homebaseTabsStore.ts around lines 124 to
136, when you force-insert HOMEBASE_DEFAULT_TAB into orderedTabs you must also
ensure a corresponding tabs entry exists to avoid lookup/lazy-load failures; if
the default tab is added and no tabs[HOMEBASE_DEFAULT_TAB] exists, create a
minimal placeholder tab object in the store before calling set so subsequent
lookups succeed, and optionally trigger the lazy loader after set by calling
get().homebase.loadHomebaseTab(HOMEBASE_DEFAULT_TAB).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/app/(spaces)/PublicSpace.tsx (2)

7-7: Clean up unused imports

TabBarSkeleton, debounce, and withOptimisticUpdate aren’t used here.

-import TabBarSkeleton from "@/common/components/organisms/TabBarSkeleton";
+// import removed

-import { indexOf, isNil, mapValues, noop, debounce } from "lodash";
+import { indexOf, isNil, mapValues, noop } from "lodash";

-import { withOptimisticUpdate } from "@/common/utils/tabUtils";
+// import removed

Also applies to: 14-14, 28-28


417-426: Critical: Remove invalid await on config (duplicate of prior review)

config is a plain object, not a Promise. await config is misleading and unnecessary.

-          const resolvedConfig = await config;
-          return renameSpaceTab(
-            currentSpaceId,
-            oldName,
-            newName,
-            resolvedConfig,
-            isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
-          );
+          return renameSpaceTab(
+            currentSpaceId,
+            oldName,
+            newName,
+            config,
+            isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
+          );
src/common/data/stores/app/homebase/homebaseTabsStore.ts (2)

124-136: Ensure order doesn’t reference a missing default tab (placeholder entry)

You force-insert HOMEBASE_DEFAULT_TAB into ordering but don’t ensure a tabs entry exists, which can break lookups/lazy loads when only ordering is fetched first.

       set((draft) => {
         // Ensure default tab is always first in the loaded order
         const orderedTabs = clone(tabOrder);
         if (!orderedTabs.includes(HOMEBASE_DEFAULT_TAB)) {
           orderedTabs.unshift(HOMEBASE_DEFAULT_TAB);
         } else {
           // If default tab exists but isn't first, move it to the front
           const defaultTabIndex = orderedTabs.indexOf(HOMEBASE_DEFAULT_TAB);
           if (defaultTabIndex > 0) {
             orderedTabs.splice(defaultTabIndex, 1);
             orderedTabs.unshift(HOMEBASE_DEFAULT_TAB);
           }
         }
+        // Ensure a placeholder exists for default so order never points to a missing tab
+        if (!draft.homebase.tabs[HOMEBASE_DEFAULT_TAB]) {
+          draft.homebase.tabs[HOMEBASE_DEFAULT_TAB] = {};
+        }
         
         draft.homebase.tabOrdering = {
           local: orderedTabs,
           remote: clone(tabOrder),
         };
       }, `loadHomebaseTabOrdering`);

380-399: Disallow renaming the default tab

Renaming the canonical default breaks assumptions across ordering/routing.

   const sanitizedNewName = newName.trim();
   if (!sanitizedNewName || sanitizedNewName === tabName) {
     return;
   }
+  if (tabName === HOMEBASE_DEFAULT_TAB) {
+    const err: any = new Error("Renaming the default tab is not allowed");
+    err.status = 400;
+    throw err;
+  }
🧹 Nitpick comments (3)
src/app/(spaces)/PublicSpace.tsx (1)

104-109: Remove unnecessary useCallback wrapper for getConfig

The callback is called immediately and adds unrelated deps. Use the getter directly.

-  const getConfig = useCallback(() => {
-    return getCurrentSpaceConfig();
-  }, [getCurrentSpaceConfig, currentSpaceId, currentTabName]);
-
-  const currentConfig = getConfig();
+  const currentConfig = getCurrentSpaceConfig();
src/common/data/stores/app/homebase/homebaseTabsStore.ts (1)

521-548: Optional: avoid Moment for simple timestamp comparisons

Moment is heavy and in maintenance. Native Date or a lighter lib (e.g., date-fns) could suffice for ISO comparisons here.

Example with Date:

const isLocalNewer =
  existingTab?.config?.timestamp &&
  (!remoteConfig.timestamp ||
    new Date(existingTab.config.timestamp).getTime() >
      new Date(remoteConfig.timestamp).getTime());

Based on learnings.

src/app/(spaces)/homebase/PrivateSpace.tsx (1)

3-3: Remove unused imports

useTransition and useRouter aren’t used.

-import React, { useEffect, useMemo, useCallback, lazy, useTransition } from "react";
+import React, { useEffect, useMemo, useCallback, lazy } from "react";
...
-import { useRouter } from "next/navigation";
+// import removed

Also applies to: 9-9

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13ff1cb and 606c57f.

📒 Files selected for processing (3)
  • src/app/(spaces)/PublicSpace.tsx (12 hunks)
  • src/app/(spaces)/homebase/PrivateSpace.tsx (8 hunks)
  • src/common/data/stores/app/homebase/homebaseTabsStore.ts (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (4)
src/common/utils/tabUtils.ts (3)
  • validateTabName (11-16)
  • isDuplicateTabName (25-34)
  • withOptimisticUpdate (46-74)
src/pages/api/space/homebase/tabs/index.ts (2)
  • UnsignedManageHomebaseTabsRequest (24-29)
  • ManageHomebaseTabsResponse (21-22)
src/common/lib/signedFiles.ts (1)
  • signSignable (54-62)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfig (37-49)
src/app/(spaces)/homebase/PrivateSpace.tsx (3)
src/common/data/stores/app/currentSpace/index.ts (3)
  • setCurrentSpaceId (43-47)
  • HOMEBASE_ID (25-25)
  • setCurrentTabName (51-55)
src/common/data/stores/app/homebase/homebaseTabsStore.ts (2)
  • loadTabNames (176-256)
  • HOMEBASE_DEFAULT_TAB (35-35)
src/app/(spaces)/SpacePage.tsx (2)
  • SpacePageArgs (5-15)
  • SpacePage (17-76)
src/app/(spaces)/PublicSpace.tsx (6)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfigSaveDetails (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-76)
🪛 Biome (2.1.2)
src/app/(spaces)/PublicSpace.tsx

[error] 291-291: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

Comment on lines 291 to 293
const freshLocalSpaces = useAppStore((state) => state.space.localSpaces);
const tabOrder = freshLocalSpaces[newSpaceId]?.order || [];
for (const tabName of tabOrder) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix Rules of Hooks violation: don’t call store hook inside async effect

useAppStore(...) is used inside an async function within useEffect. Hooks must run unconditionally at the top level. Use the store’s getState() instead.

Based on static analysis hints.

-            // Load remaining tabs - get fresh reference after loading operations
-            const freshLocalSpaces = useAppStore((state) => state.space.localSpaces);
-            const tabOrder = freshLocalSpaces[newSpaceId]?.order || [];
+            // Load remaining tabs - read from store without using the hook
+            const tabOrder =
+              useAppStore.getState().space.localSpaces[newSpaceId]?.order || [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const freshLocalSpaces = useAppStore((state) => state.space.localSpaces);
const tabOrder = freshLocalSpaces[newSpaceId]?.order || [];
for (const tabName of tabOrder) {
// Load remaining tabs - read from store without using the hook
const tabOrder =
useAppStore.getState().space.localSpaces[newSpaceId]?.order || [];
for (const tabName of tabOrder) {
🧰 Tools
🪛 Biome (2.1.2)

[error] 291-291: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In src/app/(spaces)/PublicSpace.tsx around lines 291 to 293, the code calls
useAppStore(...) inside an async effect which violates the Rules of Hooks;
replace that in-effect hook call with the store's synchronous getter: use the
store's getState() to read localSpaces (e.g. const freshLocalSpaces =
useAppStore.getState().space.localSpaces) before or inside the effect without
invoking the hook, then compute tabOrder from
freshLocalSpaces[newSpaceId]?.order || [] and iterate over it; ensure no Hook
calls occur conditionally or inside async functions.

@j-paterson j-paterson merged commit 26246a6 into canary Oct 17, 2025
3 of 4 checks passed
@j-paterson j-paterson deleted the tabBarFix branch October 17, 2025 17:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(spaces)/PublicSpace.tsx (1)

350-366: Critical: guard against undefined remote tab in resetConfig.

remoteSpaces[currentSpaceId].tabs[currentTabName] can be undefined; spreading it throws. Add a safe fallback.

   } else {
-      const remoteConfig = remoteSpaces[currentSpaceId].tabs[currentTabName];
-      configToSave = {
-        ...remoteConfig,
-      };
+      const remoteConfig =
+        remoteSpaces[currentSpaceId]?.tabs?.[currentTabName];
+      configToSave = remoteConfig
+        ? { ...remoteConfig }
+        : { ...spacePageData.config, isPrivate: false };
   }
 
-    saveLocalSpaceTab(currentSpaceId, currentTabName, configToSave);
+    saveLocalSpaceTab(currentSpaceId, currentTabName, configToSave);
♻️ Duplicate comments (2)
src/app/(spaces)/PublicSpace.tsx (2)

26-26: Remove unused import (duplicate).

withOptimisticUpdate is not used.

-import { withOptimisticUpdate } from "@/common/utils/tabUtils";

407-416: Critical: remove invalid await on config (duplicate).

config is a plain object; await config is misleading and unnecessary. Pass config directly.

       renameTab={async (oldName, newName) => {
         if (currentSpaceId) {
-          const resolvedConfig = await config;
           return renameSpaceTab(
             currentSpaceId,
             oldName,
             newName,
-            resolvedConfig,
+            config,
             isTokenSpace(spacePageData) ? spacePageData.tokenData?.network as EtherScanChainName : undefined,
           );
         }
         return undefined;
       }}
🧹 Nitpick comments (8)
src/app/(spaces)/PublicSpace.tsx (8)

102-108: Drop unnecessary useCallback wrapper for getConfig.

It’s called immediately and doesn’t use currentSpaceId/currentTabName; read from the store directly.

-  // Get the current config using the store's getter
-  const getConfig = useCallback(() => {
-    return getCurrentSpaceConfig();
-  }, [getCurrentSpaceConfig, currentSpaceId, currentTabName]);
-
-  const currentConfig = getConfig();
+  // Read current config once per render
+  const currentConfig = getCurrentSpaceConfig();

340-341: Trim saveConfig deps to avoid unnecessary re-creations.

config?.fidgetInstanceDatums isn’t used in the callback body.

-    [currentSpaceId, currentTabName, saveLocalSpaceTab, config?.fidgetInstanceDatums]
+    [currentSpaceId, currentTabName, saveLocalSpaceTab]

169-177: Remove unused dep in isEditable memo.

isSignedIntoFarcaster is not referenced in the memo body.

-  }, [spacePageData, currentUserFid, wallets, isSignedIntoFarcaster]);
+  }, [spacePageData, currentUserFid, wallets]);

179-189: Align comment with behavior or implement Suspense path.

Comment says “return undefined to trigger Suspense,” but code always falls back to spacePageData.config. Either update the comment or return undefined when a user space is loading to leverage SpacePage’s Suspense.

-  // - Otherwise, return undefined to trigger Suspense while loading
-  const config = currentTabName && currentConfig?.tabs?.[currentTabName] ? {
-    ...currentConfig.tabs[currentTabName],
-    isEditable,
-  } : {
-    ...spacePageData.config,
-    isEditable,
-  };
+  // - Otherwise, fall back to default config (or return undefined to trigger Suspense)
+  const config =
+    currentTabName && currentConfig?.tabs?.[currentTabName]
+      ? { ...currentConfig.tabs[currentTabName], isEditable }
+      : isNil(currentSpaceId)
+      ? { ...spacePageData.config, isEditable }
+      : undefined;

93-101: Avoid redundant store writes when props don’t change.

No-op if values are already equal; reduces unnecessary store updates/renders.

   useEffect(() => {
     const newSpaceId = spacePageData.spaceId ?? null;
     const newTabName = providedTabName || spacePageData.defaultTab;
-    
-    setCurrentSpaceId(newSpaceId);
-    setCurrentTabName(newTabName);
-  }, [spacePageData.spaceId, providedTabName, spacePageData.defaultTab, setCurrentSpaceId, setCurrentTabName]);
+    if (currentSpaceId !== newSpaceId) setCurrentSpaceId(newSpaceId);
+    if (currentTabName !== newTabName) setCurrentTabName(newTabName);
+  }, [
+    spacePageData.spaceId,
+    providedTabName,
+    spacePageData.defaultTab,
+    currentSpaceId,
+    currentTabName,
+    setCurrentSpaceId,
+    setCurrentTabName,
+  ]);

153-167: Optional: guard against stale async loads.

If IDs change quickly, earlier loads may complete late. Add a simple staleness check.

   useEffect(() => {
     if (!currentSpaceId || !currentTabName) return;
 
-    const loadSpace = async () => {
+    const spaceId = currentSpaceId;
+    const tabName = currentTabName;
+    let cancelled = false;
+    const loadSpace = async () => {
       try {
-        await loadSpaceTabOrder(currentSpaceId);
-        await loadSpaceTab(currentSpaceId, currentTabName);
+        await loadSpaceTabOrder(spaceId);
+        if (cancelled) return;
+        await loadSpaceTab(spaceId, tabName);
       } catch (error) {
         console.error("Error loading space:", error);
       }
     };
 
-    loadSpace();
+    loadSpace();
+    return () => {
+      cancelled = true;
+    };
   }, [currentSpaceId, currentTabName, loadSpaceTabOrder, loadSpaceTab]);

64-89: Don’t select unused fields from the store.

clearLocalSpaces, getCurrentSpaceId, and getCurrentTabName aren’t used; removing them avoids unnecessary re-renders when they change.

-  } = useAppStore((state) => ({
-    clearLocalSpaces: state.clearLocalSpaces,
-    getCurrentSpaceId: state.currentSpace.getCurrentSpaceId,
+  } = useAppStore((state) => ({
     setCurrentSpaceId: state.currentSpace.setCurrentSpaceId,
-    getCurrentTabName: state.currentSpace.getCurrentTabName,
     setCurrentTabName: state.currentSpace.setCurrentTabName,

420-427: Consistency: cast network like other handlers.

Minor consistency with other TabBar handlers using as EtherScanChainName.

-              isTokenSpace(spacePageData) ? spacePageData.tokenData?.network : undefined
+              isTokenSpace(spacePageData)
+                ? (spacePageData.tokenData?.network as EtherScanChainName)
+                : undefined
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 606c57f and 2b0d20e.

📒 Files selected for processing (1)
  • src/app/(spaces)/PublicSpace.tsx (12 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(spaces)/PublicSpace.tsx (6)
src/common/data/stores/app/currentSpace/index.ts (2)
  • setCurrentSpaceId (43-47)
  • setCurrentTabName (51-55)
src/app/(spaces)/Space.tsx (1)
  • SpaceConfigSaveDetails (51-55)
src/common/types/spaceData.ts (1)
  • isTokenSpace (85-87)
src/common/components/organisms/Sidebar.tsx (1)
  • useSidebarContext (50-52)
src/constants/etherscanChainIds.ts (1)
  • EtherScanChainName (1-1)
src/app/(spaces)/SpacePage.tsx (1)
  • SpacePage (17-76)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants