diff --git a/src/components/AddColumnModal/column-types.tsx b/src/components/AddColumnModal/column-types.tsx
index 5003cbe..9fb0793 100644
--- a/src/components/AddColumnModal/column-types.tsx
+++ b/src/components/AddColumnModal/column-types.tsx
@@ -99,7 +99,7 @@ const HOME_DESCRIPTOR: ColumnTypeDescriptor = {
}),
isReadyToPreview: (draft) => !!draft.viewContext,
supportsViewAs: true,
- PreviewBody: () => ,
+ PreviewBody: ({ column }) => ,
previewHint: 'Pick an account to preview'
}
diff --git a/src/components/Column/HomeColumnBody.tsx b/src/components/Column/HomeColumnBody.tsx
index a2ae07c..2dcc8e8 100644
--- a/src/components/Column/HomeColumnBody.tsx
+++ b/src/components/Column/HomeColumnBody.tsx
@@ -1,6 +1,9 @@
// src/components/Column/HomeColumnBody.tsx
import FollowingFeed from '@/components/FollowingFeed'
import { useAccountScope } from '@/providers/AccountScope'
+import { useColumns } from '@/providers/ColumnsProvider'
+import { TColumn } from '@/types/column'
+import { useCallback } from 'react'
/**
* Body of a Home column. Subscribes to the following feed of the column's
@@ -8,10 +11,31 @@ import { useAccountScope } from '@/providers/AccountScope'
* foreign — via the enclosing . Passes that pubkey explicitly to
* so it ignores the global useNostr() pubkey.
*
+ * Also owns the per-column "Notes vs Notes-and-replies" tab preference: seeds
+ * the feed from `column.config.feedTab` and persists the user's choice back via
+ * updateColumnConfig (localStorage immediately; rides the deck's NIP-78 sync on
+ * the next deck save). Absent config → 'posts' (Notes), the app default.
+ *
* Lives as its own file because Phase 2 will customize per-column toolbars and
* Phase 3 may swap the underlying feed component for a column-specific variant.
*/
-export default function HomeColumnBody() {
+export default function HomeColumnBody({ column }: { column: TColumn }) {
const { viewContext } = useAccountScope()
- return
+ const { updateColumnConfig } = useColumns()
+ const handleTabChange = useCallback(
+ (tabId: string) => {
+ // Only the two known feed tabs reach here; cast narrows string → the
+ // config union. An unknown value would be ignored by resolveInitialTabId
+ // on next mount anyway.
+ updateColumnConfig(column.id, { feedTab: tabId as 'posts' | 'postsAndReplies' })
+ },
+ [column.id, updateColumnConfig]
+ )
+ return (
+
+ )
}
diff --git a/src/components/Column/index.tsx b/src/components/Column/index.tsx
index 16e6afc..c7f06e6 100644
--- a/src/components/Column/index.tsx
+++ b/src/components/Column/index.tsx
@@ -388,7 +388,7 @@ function ScopedSecondaryPage({
export function dispatchBody(column: TColumn): React.ReactNode {
switch (column.type) {
case 'home':
- return
+ return
case 'notifications':
return
case 'articles':
diff --git a/src/components/FollowingFeed/index.tsx b/src/components/FollowingFeed/index.tsx
index 6f6eb53..64b123f 100644
--- a/src/components/FollowingFeed/index.tsx
+++ b/src/components/FollowingFeed/index.tsx
@@ -15,9 +15,17 @@ import { useTranslation } from 'react-i18next'
type Props = {
/** Override pubkey for column-scoped use. If absent, falls back to the global active account. */
pubkey?: string
+ /** Persisted feed tab to open on (forwarded to ). */
+ initialTabId?: string
+ /** Called when the user switches feed tab, so the column can persist it. */
+ onTabChange?: (tabId: string) => void
}
-export default function FollowingFeed({ pubkey: pubkeyProp }: Props = {}) {
+export default function FollowingFeed({
+ pubkey: pubkeyProp,
+ initialTabId,
+ onTabChange
+}: Props = {}) {
const { t } = useTranslation()
const { pubkey: activePubkey } = useNostr()
const pubkey = pubkeyProp ?? activePubkey
@@ -93,6 +101,8 @@ export default function FollowingFeed({ pubkey: pubkeyProp }: Props = {}) {
setRefreshCount((count) => count + 1)
}}
isPubkeyFeed
+ initialTabId={initialTabId}
+ onTabChange={onTabChange}
/>
)
}
diff --git a/src/components/NormalFeed/__tests__/initial-tab.spec.ts b/src/components/NormalFeed/__tests__/initial-tab.spec.ts
new file mode 100644
index 0000000..2469db8
--- /dev/null
+++ b/src/components/NormalFeed/__tests__/initial-tab.spec.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+import { resolveInitialTabId } from '../initial-tab'
+
+const TABS = [{ id: 'posts' }, { id: 'postsAndReplies' }]
+
+describe('resolveInitialTabId', () => {
+ it('falls back to the first tab when no preference is persisted', () => {
+ expect(resolveInitialTabId(undefined, TABS)).toBe('posts')
+ })
+
+ it('honors a persisted tab that matches a visible tab', () => {
+ expect(resolveInitialTabId('postsAndReplies', TABS)).toBe('postsAndReplies')
+ })
+
+ it('ignores a stale/unknown persisted tab and falls back to the first', () => {
+ expect(resolveInitialTabId('media', TABS)).toBe('posts')
+ })
+
+ it('returns empty string when there are no tabs (defensive)', () => {
+ expect(resolveInitialTabId('posts', [])).toBe('')
+ })
+})
diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx
index 4628836..de08a2a 100644
--- a/src/components/NormalFeed/index.tsx
+++ b/src/components/NormalFeed/index.tsx
@@ -7,6 +7,7 @@ import { TFeedSubRequest, TFeedTabConfig } from '@/types'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import KindFilter from '../KindFilter'
import { RefreshButton } from '../RefreshButton'
+import { resolveInitialTabId } from './initial-tab'
export default function NormalFeed({
feedId,
@@ -14,7 +15,9 @@ export default function NormalFeed({
areAlgoRelays = false,
showRelayCloseReason = false,
onRefresh,
- isPubkeyFeed = false
+ isPubkeyFeed = false,
+ initialTabId,
+ onTabChange
}: {
feedId: string
subRequests: TFeedSubRequest[]
@@ -22,17 +25,26 @@ export default function NormalFeed({
showRelayCloseReason?: boolean
onRefresh?: () => void
isPubkeyFeed?: boolean
+ /** Tab to open on (a DEFAULT_FEED_TABS id). Read once at mount. Absent → the
+ * first visible tab ('Notes'). The Home column passes its persisted
+ * column.config.feedTab here. */
+ initialTabId?: string
+ /** Called with the new tab id when the user switches tabs, so the caller can
+ * persist the choice (e.g. into column.config.feedTab). */
+ onTabChange?: (tabId: string) => void
}) {
const { getShowKinds } = useKindFilter()
const feedShowKinds = useMemo(() => getShowKinds(feedId), [getShowKinds, feedId])
const [temporaryShowKinds, setTemporaryShowKinds] = useState(feedShowKinds)
- const visibleTabs = useMemo(
- () => DEFAULT_FEED_TABS.filter((tab) => !tab.hidden),
- []
- )
+ const visibleTabs = useMemo(() => DEFAULT_FEED_TABS.filter((tab) => !tab.hidden), [])
- const [selectedTabId, setSelectedTabId] = useState()
+ // Seed from the caller's persisted tab once at mount; an absent/stale value
+ // falls back to the first visible tab. After mount this component owns the
+ // selection (re-seeding on every render would fight the user's clicks).
+ const [selectedTabId, setSelectedTabId] = useState(() =>
+ resolveInitialTabId(initialTabId, visibleTabs)
+ )
const selectedTab: TFeedTabConfig = selectedTabId
? (visibleTabs.find((tab) => tab.id === selectedTabId) ?? visibleTabs[0])
: visibleTabs[0]
@@ -57,10 +69,14 @@ export default function NormalFeed({
setTemporaryShowKinds(feedShowKinds)
}, [feedShowKinds])
- const handleListModeChange = useCallback((mode: string) => {
- setSelectedTabId(mode)
- topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
- }, [])
+ const handleListModeChange = useCallback(
+ (mode: string) => {
+ setSelectedTabId(mode)
+ onTabChange?.(mode)
+ topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ },
+ [onTabChange]
+ )
const handleShowKindsChange = useCallback((newShowKinds: number[]) => {
setTemporaryShowKinds(newShowKinds)
diff --git a/src/components/NormalFeed/initial-tab.ts b/src/components/NormalFeed/initial-tab.ts
new file mode 100644
index 0000000..c122204
--- /dev/null
+++ b/src/components/NormalFeed/initial-tab.ts
@@ -0,0 +1,15 @@
+type TabLike = { id: string }
+
+/**
+ * Resolve the initial selected feed tab for a .
+ *
+ * Uses the column's persisted `configTab` (e.g. `column.config.feedTab`) when it
+ * matches a currently-visible tab; otherwise falls back to the first visible tab
+ * — today's "Notes" (replies-hidden) default. Tolerates an unknown/stale
+ * persisted value (e.g. a tab that was later removed or renamed) by falling
+ * back rather than selecting nothing.
+ */
+export function resolveInitialTabId(configTab: string | undefined, tabs: TabLike[]): string {
+ if (configTab && tabs.some((tab) => tab.id === configTab)) return configTab
+ return tabs[0]?.id ?? ''
+}
diff --git a/src/services/__tests__/deck-sync-codec.spec.ts b/src/services/__tests__/deck-sync-codec.spec.ts
index 342a7dd..83a3278 100644
--- a/src/services/__tests__/deck-sync-codec.spec.ts
+++ b/src/services/__tests__/deck-sync-codec.spec.ts
@@ -80,6 +80,16 @@ describe('deck-sync-codec', () => {
expect(decodeWorkspace(bad)).toEqual({ ok: false, reason: 'shape' })
})
+ it('round-trips a persisted feedTab on a home column config', () => {
+ const s = workspace({
+ decks: [deck({ savedColumns: [col({ config: { feedTab: 'postsAndReplies' } })] })]
+ })
+ const decoded = decodeWorkspace(encodeWorkspace(s))
+ expect(decoded.ok).toBe(true)
+ if (!decoded.ok) return
+ expect(decoded.workspace.decks[0].columns[0].config?.feedTab).toBe('postsAndReplies')
+ })
+
it('decode isolates columns and savedColumns config objects', () => {
const s = workspace({
decks: [deck({ savedColumns: [col({ id: 'a', type: 'hashtag', config: { hashtags: ['x'] } })] })]
diff --git a/src/types/column.d.ts b/src/types/column.d.ts
index a136359..03ccf9b 100644
--- a/src/types/column.d.ts
+++ b/src/types/column.d.ts
@@ -81,6 +81,12 @@ export type TColumnConfig = {
* 2-hop WoT (your follows + their follows). Per-column binary filter
* replacing the prior trustScoreThreshold int. Default false. */
wotOnly?: boolean
+ /** 'home': which tab this column opens on, persisted per-column
+ * so the choice survives reloads and rides the deck's NIP-78 sync. Matches a
+ * DEFAULT_FEED_TABS id. Absent → opens on 'posts' (Notes, replies hidden),
+ * today's default. An unknown/stale value falls back to 'posts'
+ * (see resolveInitialTabId). Only the Home column persists this today. */
+ feedTab?: 'posts' | 'postsAndReplies'
}
export type TColumn = {