Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jank",
"version": "26.14.2",
"version": "26.15.0",
"description": "A TweetDeck-style multi-column deck for Nostr — just another nostr klient",
"type": "module",
"author": "DocNR",
Expand Down
36 changes: 34 additions & 2 deletions src/DeckManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@
* here.
*/
import { activeColumnIdAtom } from '@/atoms/active-column'
import { mobileNavStackAtom } from '@/atoms/mobile-nav-stack'
import AgentDrawer from '@/components/AgentDrawer'
import CommandDispatcher from '@/components/CommandDispatcher'
import CommandPalette from '@/components/CommandPalette'
import StarterCommands from '@/components/CommandPalette/StarterCommands'
import MobileNavStack from '@/components/MobileNavStack'
import Shell from '@/components/Shell'
import TopBar from '@/components/TopBar'
import { opensAsColumnOnMobile } from '@/lib/link'
import DeckHomePage from '@/pages/primary/DeckHomePage'
import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider'
import { useStore } from 'jotai'
import { useSetAtom, useStore } from 'jotai'
import {
createContext,
useCallback,
Expand Down Expand Up @@ -71,6 +74,7 @@ export function DeckManager() {
const { isSmallScreen } = useScreenSize()
const { addTransientColumn, columns, removeColumn } = useColumns()
const { account } = useNostr()
const setMobileStack = useSetAtom(mobileNavStackAtom)
const ignorePopStateRef = useRef(false)
// Read the active column id imperatively (not via useAtomValue) so this
// value can be sampled inside the pushSecondaryPage callback without
Expand Down Expand Up @@ -136,6 +140,19 @@ export function DeckManager() {
return
}

// Mobile push-stack: back pops one screen. Re-arm a history entry so the
// NEXT back also fires popstate (instead of exiting the SPA).
const navStack = jotaiStore.get(mobileNavStackAtom)
if (navStack.length > 0) {
setMobileStack(navStack.slice(0, -1))
try {
window.history.pushState(null, '', '/')
} catch {
/* non-browser env — skip */
}
return
}

// Back-button closes the focused transient column. There is no
// history entry per transient column, so one back-click closes one
// transient — no stack to navigate through. Pinned columns are
Expand Down Expand Up @@ -182,6 +199,20 @@ export function DeckManager() {
url: string,
opts?: { sourceViewContext?: string; sourceSigningIdentity?: string | null }
) => {
// Mobile: in-feed drill-downs (note threads, profiles, settings, ...)
// become native pushed screens. Only feed-shaped standing surfaces
// (hashtag/relay/search/notifications/bookmarks/mutes) still spawn deck
// columns. Desktop is unchanged — everything spawns a transient column.
if (isSmallScreen && !opensAsColumnOnMobile(url)) {
setMobileStack((prev) => [...prev, { id: crypto.randomUUID(), url }])
// Arm a history entry so hardware/gesture back pops the screen.
try {
window.history.pushState(null, '', '/')
} catch {
/* non-browser env — skip */
}
return
}
const parentColumnId = jotaiStore.get(activeColumnIdAtom) ?? undefined
const source = opts?.sourceViewContext
? {
Expand All @@ -191,7 +222,7 @@ export function DeckManager() {
: null
addTransientColumn(url, source, parentColumnId)
},
[addTransientColumn, jotaiStore]
[isSmallScreen, setMobileStack, addTransientColumn, jotaiStore]
)

// popSecondaryPage outside a DetailColumnBody = "close the focused
Expand Down Expand Up @@ -224,6 +255,7 @@ export function DeckManager() {
content={<DeckHomePage />}
bottomBar={isSmallScreen ? <BottomNavigationBar /> : null}
/>
{isSmallScreen && <MobileNavStack />}
<TooManyRelaysAlertDialog />
<AgentDrawer />
<BackgroundAudio className="fixed end-0 bottom-20 z-50 w-80 overflow-hidden rounded-s-full rounded-e-none border shadow-lg" />
Expand Down
7 changes: 7 additions & 0 deletions src/atoms/active-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export const focusedColumnRequestAtom = atom<string | null>(null)
*/
export const focusBeamActiveAtom = atom<boolean>(false)

/**
* Open/closed state of the column overview ("exploding tabs") overlay — a
* full-screen grid of every open column for jump / close / add. Opened from the
* mobile page-dot pill's grid button and a desktop TopBar button. Ephemeral.
*/
export const columnOverviewOpenAtom = atom<boolean>(false)

/**
* Open/closed state of the Track B agent chat drawer. Lifted into Jotai so the
* TopBar toggle button and the AgentDrawer shell (mounted in DeckManager) share
Expand Down
17 changes: 17 additions & 0 deletions src/atoms/mobile-nav-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { atom } from 'jotai'

/**
* Mobile native push/pop navigation stack. Each in-feed drill-down (note
* thread, profile, settings, ...) on mobile appends an entry here and renders
* as a full-screen pushed screen instead of spawning a deck column. Back chevron
* / hardware back / Overview pop or clear it. Desktop never uses this — it keeps
* spawning transient columns. Ephemeral (not persisted).
*/
export type TMobileNavEntry = {
/** Stable key for the layer (also the React key + slide-in animation reset). */
id: string
/** The pushed URL, dispatched against SECONDARY_ROUTES. */
url: string
}

export const mobileNavStackAtom = atom<TMobileNavEntry[]>([])
2 changes: 1 addition & 1 deletion src/components/AddColumnModal/PickerGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function PickerGrid({ onSelect }: Props) {
)}
>
<Icon className="size-7 opacity-80" />
<span className="w-full px-1 text-center text-xs leading-tight font-medium">
<span className="w-full px-1 text-center text-xs leading-tight font-medium break-words hyphens-auto">
{t(desc.label)}
</span>
{/* Keyboard-shortcut hint in the bottom-end corner. Muted +
Expand Down
27 changes: 0 additions & 27 deletions src/components/BottomNavigationBar/HomeButton.tsx

This file was deleted.

16 changes: 16 additions & 0 deletions src/components/BottomNavigationBar/NewColumnButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { addColumnDialogOpenAtom } from '@/atoms/active-column'
import { Plus } from '@phosphor-icons/react'
import { useSetAtom } from 'jotai'
import BottomNavigationBarItem from './BottomNavigationBarItem'

// Opens the column picker (AddColumnModal, mounted in DeckArea and driven by
// addColumnDialogOpenAtom). The bottom-bar "+" reads as "new column" — compose
// has its own pencil button now.
export default function NewColumnButton() {
const setAddOpen = useSetAtom(addColumnDialogOpenAtom)
return (
<BottomNavigationBarItem onClick={() => setAddOpen(true)}>
<Plus weight="bold" />
</BottomNavigationBarItem>
)
}
27 changes: 27 additions & 0 deletions src/components/BottomNavigationBar/OverviewButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { columnOverviewOpenAtom } from '@/atoms/active-column'
import { mobileNavStackAtom } from '@/atoms/mobile-nav-stack'
import { SquaresFour } from '@phosphor-icons/react'
import { useSetAtom } from 'jotai'
import BottomNavigationBarItem from './BottomNavigationBarItem'

// "Home base" for the deck on mobile: opens the column overview (the exploding
// grid of all open columns). Replaces the old scroll-to-first-column home
// button — the brand tap in the TopBar still does that, and the overview
// reaches any column including the first.
//
// Also the one-tap escape from a deep push-stack: clears any open drill-down
// screens so you land back on the deck overview from any depth.
export default function OverviewButton() {
const setOverviewOpen = useSetAtom(columnOverviewOpenAtom)
const setMobileStack = useSetAtom(mobileNavStackAtom)
return (
<BottomNavigationBarItem
onClick={() => {
setMobileStack([])
setOverviewOpen(true)
}}
>
<SquaresFour weight="fill" />
</BottomNavigationBarItem>
)
}
6 changes: 4 additions & 2 deletions src/components/BottomNavigationBar/PostButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import PostEditor from '@/components/PostEditor/LazyPostEditor'
import { useNostr } from '@/providers/NostrProvider'
import { PlusCircleIcon } from '@phosphor-icons/react'
import { NotePencil } from '@phosphor-icons/react'
import { useState } from 'react'
import BottomNavigationBarItem from './BottomNavigationBarItem'

// Compose a note. A pencil now, not a plus — the bottom-bar "+" means "new
// column". Kept as PostButton (it drives the PostEditor) to avoid churn.
export default function PostButton() {
const { checkLogin } = useNostr()
const [open, setOpen] = useState(false)
Expand All @@ -17,7 +19,7 @@ export default function PostButton() {
})
}}
>
<PlusCircleIcon weight="bold" className="size-7!" />
<NotePencil weight="fill" />
</BottomNavigationBarItem>
<PostEditor open={open} setOpen={setOpen} />
</>
Expand Down
13 changes: 7 additions & 6 deletions src/components/BottomNavigationBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { cn } from '@/lib/utils'
import HomeButton from './HomeButton'
import NewColumnButton from './NewColumnButton'
import OverviewButton from './OverviewButton'
import PostButton from './PostButton'

// WS2 shrunk the bar to Home + Post. Search/Messages/Notifications are deck
// columns now, not primary pages, so the icons no longer have a meaningful
// destination. The middle is reserved for the WS3 swipe page-dot indicator.
// BackgroundAudio moved to the unified shell's floating widget mount.
// Overview (all columns) · New column · Compose. The middle is reserved for the
// WS3 swipe page-dot indicator. Search/Messages/Notifications are deck columns
// now, so they don't get bottom-bar icons.
export default function BottomNavigationBar() {
return (
<div
Expand All @@ -15,7 +15,8 @@ export default function BottomNavigationBar() {
}}
>
<div className="flex w-full items-center justify-around [&_svg]:size-4 [&_svg]:shrink-0">
<HomeButton />
<OverviewButton />
<NewColumnButton />
<PostButton />
</div>
</div>
Expand Down
12 changes: 10 additions & 2 deletions src/components/Column/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@ export function Column({ column, dragHandleProps, style }: Props) {
if (!isActive) setActiveColumnId(column.id)
}}
className={cn(
'border-border bg-card flex h-full shrink-0 flex-col overflow-hidden rounded-lg border shadow-md transition-[box-shadow,border-color] duration-200',
// Mobile: edge-to-edge, no card frame (one column fills the screen, so
// the border/rounding/shadow that separate side-by-side columns are
// wasted chrome). Desktop keeps the floating-card frame.
'border-border bg-card flex h-full shrink-0 flex-col overflow-hidden transition-[box-shadow,border-color] duration-200 md:rounded-lg md:border md:shadow-md',
// Sticky-selection affordance: recolor the existing 1px border to
// the account hue (so the top stripe + side border read as one
// continuous frame, no second outline) and pump the layered
Expand Down Expand Up @@ -321,7 +324,12 @@ export function Column({ column, dragHandleProps, style }: Props) {
<div
ref={bodyRef}
data-column-body
className="min-h-0 flex-1 overflow-y-auto [&_.sticky]:!top-0"
// Mobile bottom padding so a column's last content clears the fixed
// bottom bar + page-dot pager (which overlay the deck). Applies to
// every column body — finite content (settings, mute, profile) was
// getting cut off; feeds just gain a little scroll-past room. Desktop
// has no bottom chrome, so it's mobile-only.
className="min-h-0 flex-1 overflow-y-auto max-md:pb-[calc(6rem+env(safe-area-inset-bottom))] [&_.sticky]:!top-0"
>
<ScrollContainerProvider scrollRef={bodyRef}>
<AccountScope
Expand Down
Loading
Loading