diff --git a/package.json b/package.json
index c5456f0..7c753ff 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "jank",
- "version": "26.13.0",
+ "version": "26.13.1",
"description": "A TweetDeck-style multi-column deck for Nostr — just another nostr klient",
"type": "module",
"author": "DocNR",
diff --git a/src/components/Column/index.tsx b/src/components/Column/index.tsx
index a2a27b5..85618b0 100644
--- a/src/components/Column/index.tsx
+++ b/src/components/Column/index.tsx
@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
import { useColumns } from '@/providers/ColumnsProvider'
import { AccountScope } from '@/providers/AccountScope'
import { ScrollContainerProvider } from '@/providers/ScrollContainerProvider'
+import { MutedThreadRevealProvider } from '@/providers/MutedThreadRevealProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useNostr } from '@/providers/NostrProvider'
@@ -332,7 +333,9 @@ export function Column({ column, dragHandleProps, style }: Props) {
viewContext={column.viewContext}
signingIdentity={column.signingIdentity}
>
- }>{dispatchBody(column)}
+
+ }>{dispatchBody(column)}
+
diff --git a/src/components/Note/MutedNote.tsx b/src/components/Note/MutedNote.tsx
index 4a04e5a..499fef5 100644
--- a/src/components/Note/MutedNote.tsx
+++ b/src/components/Note/MutedNote.tsx
@@ -26,7 +26,7 @@ export default function MutedNote({
variant="outline"
>
- {t('Temporarily display this note')}
+ {reason === 'thread' ? t('Reveal muted thread') : t('Temporarily display this note')}
)
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index 312ea4c..5289ce5 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -1,13 +1,14 @@
import { useSecondaryPage } from '@/DeckManager'
import { ExtendedKind, NSFW_DISPLAY_POLICY, SUPPORTED_KINDS } from '@/constants'
import { cn } from '@/lib/utils'
-import { getParentStuff, isInMutedThread, isNsfwEvent } from '@/lib/event'
+import { getParentStuff, getThreadRootId, isInMutedThread, isNsfwEvent } from '@/lib/event'
import { toExternalContent, toNote } from '@/lib/link'
import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/UserListsProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
+import { useMutedThreadReveal } from '@/providers/MutedThreadRevealProvider'
import { Event, kinds } from 'nostr-tools'
import { ReactNode, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -79,6 +80,10 @@ export default function Note({
const [showNsfw, setShowNsfw] = useState(false)
const { mutePubkeySet, muteEventIdSet } = useMuteList()
const [showMuted, setShowMuted] = useState(false)
+ // Thread reveal is per-column (resets when the column closes) and keyed by
+ // thread root, so revealing the root note also un-hides this thread's replies
+ // — see useFilteredReplies.
+ const { revealed: revealedMutedThreads, reveal: revealMutedThread } = useMutedThreadReveal()
const isNsfw = useMemo(
() => (nsfwDisplayPolicy === NSFW_DISPLAY_POLICY.SHOW ? false : isNsfwEvent(event)),
[event, nsfwDisplayPolicy]
@@ -104,8 +109,11 @@ export default function Note({
content =
} else if (mutePubkeySet.has(event.pubkey) && !showMuted) {
content = setShowMuted(true)} />
- } else if (isInMutedThread(event, muteEventIdSet) && !showMuted) {
- content = setShowMuted(true)} />
+ } else if (
+ isInMutedThread(event, muteEventIdSet) &&
+ !revealedMutedThreads.has(getThreadRootId(event))
+ ) {
+ content = revealMutedThread(getThreadRootId(event))} />
} else if (isNsfw && !showNsfw) {
content = setShowNsfw(true)} />
} else if (event.kind === kinds.Highlights) {
diff --git a/src/hooks/useFilteredReplies.tsx b/src/hooks/useFilteredReplies.tsx
index bf6b910..82888e0 100644
--- a/src/hooks/useFilteredReplies.tsx
+++ b/src/hooks/useFilteredReplies.tsx
@@ -1,6 +1,7 @@
-import { getEventKey, isInMutedThread, isMentioningMutedUsers } from '@/lib/event'
+import { getEventKey, getThreadRootId, isInMutedThread, isMentioningMutedUsers } from '@/lib/event'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/UserListsProvider'
+import { useMutedThreadReveal } from '@/providers/MutedThreadRevealProvider'
import { useNostr } from '@/providers/NostrProvider'
import { NostrEvent } from 'nostr-tools'
import { useEffect, useState } from 'react'
@@ -10,6 +11,7 @@ export function useFilteredReplies(stuffKey: string) {
const { pubkey } = useNostr()
const { mutePubkeySet, muteEventIdSet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
+ const { revealed: revealedMutedThreads } = useMutedThreadReveal()
const allThreads = useAllDescendantThreads(stuffKey)
const [replies, setReplies] = useState([])
const [hasReplied, setHasReplied] = useState(false)
@@ -25,7 +27,8 @@ export function useFilteredReplies(stuffKey: string) {
replyKeySet.add(key)
if (mutePubkeySet.has(evt.pubkey)) continue
- if (isInMutedThread(evt, muteEventIdSet)) continue
+ if (isInMutedThread(evt, muteEventIdSet) && !revealedMutedThreads.has(getThreadRootId(evt)))
+ continue
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) continue
filtered.push(evt)
@@ -33,7 +36,14 @@ export function useFilteredReplies(stuffKey: string) {
filtered.sort((a, b) => b.created_at - a.created_at)
setReplies(filtered)
- }, [stuffKey, allThreads, mutePubkeySet, muteEventIdSet, hideContentMentioningMutedUsers])
+ }, [
+ stuffKey,
+ allThreads,
+ mutePubkeySet,
+ muteEventIdSet,
+ revealedMutedThreads,
+ hideContentMentioningMutedUsers
+ ])
useEffect(() => {
let replied = false
@@ -54,6 +64,7 @@ export function useFilteredAllReplies(stuffKey: string) {
const allThreads = useAllDescendantThreads(stuffKey)
const { mutePubkeySet, muteEventIdSet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
+ const { revealed: revealedMutedThreads } = useMutedThreadReveal()
const [replies, setReplies] = useState([])
const [hasReplied, setHasReplied] = useState(false)
@@ -70,6 +81,8 @@ export function useFilteredAllReplies(stuffKey: string) {
replyKeySet.add(key)
if (mutePubkeySet.has(evt.pubkey)) continue
+ if (isInMutedThread(evt, muteEventIdSet) && !revealedMutedThreads.has(getThreadRootId(evt)))
+ continue
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) continue
replyEvents.push(evt)
@@ -77,7 +90,14 @@ export function useFilteredAllReplies(stuffKey: string) {
parentKeys = events.map((evt) => getEventKey(evt))
}
setReplies(replyEvents.sort((a, b) => a.created_at - b.created_at))
- }, [stuffKey, allThreads, mutePubkeySet, muteEventIdSet, hideContentMentioningMutedUsers])
+ }, [
+ stuffKey,
+ allThreads,
+ mutePubkeySet,
+ muteEventIdSet,
+ revealedMutedThreads,
+ hideContentMentioningMutedUsers
+ ])
useEffect(() => {
let replied = false
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 2cbf94f..828a1c9 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -1088,6 +1088,7 @@ export default {
Words: 'Words',
'No muted users': 'No muted users',
'No muted threads': 'No muted threads',
- 'Muted words apply to all your accounts.': 'Muted words apply to all your accounts.'
+ 'Muted words apply to all your accounts.': 'Muted words apply to all your accounts.',
+ 'Reveal muted thread': 'Reveal muted thread'
}
}
diff --git a/src/providers/MutedThreadRevealProvider.tsx b/src/providers/MutedThreadRevealProvider.tsx
new file mode 100644
index 0000000..ea89c8a
--- /dev/null
+++ b/src/providers/MutedThreadRevealProvider.tsx
@@ -0,0 +1,53 @@
+import { createContext, ReactNode, useContext, useMemo, useState } from 'react'
+
+type TMutedThreadReveal = {
+ /** Thread ROOT ids revealed within this column. */
+ revealed: Set
+ /** Reveal a muted thread (by its root id) for this column. */
+ reveal: (rootId: string) => void
+}
+
+const MutedThreadRevealContext = createContext(null)
+
+/**
+ * Per-column reveal state for muted threads. Mounted once inside every
+ * , so the reveal is scoped to that column's lifetime:
+ *
+ * - A muted thread opens COLLAPSED ("Reveal muted thread").
+ * - Clicking reveal adds the thread root here; the `Note` collapse (root +
+ * parent notes) and the reply filters (useFilteredReplies /
+ * useFilteredAllReplies) both read it, so one click reveals the whole
+ * conversation — note AND replies — together.
+ * - CLOSING the column drops this state. Reopening the thread starts collapsed
+ * again, because the thread is still muted. This keeps "reveal" (a temporary
+ * peek) visually distinct from "unmute" (permanent).
+ *
+ * Not persisted; not shared across columns. Threads stay muted in feeds /
+ * notifications regardless.
+ */
+export function MutedThreadRevealProvider({ children }: { children: ReactNode }) {
+ const [revealed, setRevealed] = useState>(() => new Set())
+ const value = useMemo(
+ () => ({
+ revealed,
+ reveal: (rootId: string) =>
+ setRevealed((prev) => {
+ if (prev.has(rootId)) return prev
+ const next = new Set(prev)
+ next.add(rootId)
+ return next
+ })
+ }),
+ [revealed]
+ )
+ return (
+ {children}
+ )
+}
+
+// Stable empty fallback for notes rendered outside any column (e.g. tests).
+const EMPTY: TMutedThreadReveal = { revealed: new Set(), reveal: () => {} }
+
+export function useMutedThreadReveal(): TMutedThreadReveal {
+ return useContext(MutedThreadRevealContext) ?? EMPTY
+}
diff --git a/src/release-notes.ts b/src/release-notes.ts
index ac18182..aeae8ce 100644
--- a/src/release-notes.ts
+++ b/src/release-notes.ts
@@ -14,6 +14,13 @@ export type ReleaseNote = {
}
export const RELEASE_NOTES: ReleaseNote[] = [
+ {
+ version: '26.13.1',
+ date: '2026-06-14',
+ highlights: [
+ 'Opening a muted thread now lets you read the whole conversation. Click "Reveal muted thread" and the note and all of its replies appear together, instead of showing only the one note. The thread stays muted in your feeds and notifications.'
+ ]
+ },
{
version: '26.13.0',
date: '2026-06-13',