Skip to content

feat(ui): add custom feed icons and rework feed tabs - #2353

Merged
infin1t3 merged 29 commits into
devfrom
bug-ui/1898-custom-feeds-hover-interaction
Aug 26, 2026
Merged

feat(ui): add custom feed icons and rework feed tabs#2353
infin1t3 merged 29 commits into
devfrom
bug-ui/1898-custom-feeds-hover-interaction

Conversation

@infin1t3

@infin1t3 infin1t3 commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Feed tabs work on mobile too. The tab bar sticks under the header: the selected tab shows icon + name, the rest collapse to icon-only, and the last one creates a feed. Feed switching no longer lives in the mobile right drawer, which now shows Who to follow / Hot tags / Feedback like the desktop one.
  • The first tab follows the selected reach — All, My network, Following, Friends, Me, Tagged as — with its matching icon, instead of a fixed "Home".
  • Custom feeds can have their own icon. Create/Edit Feed has a Feed Icon field that opens a searchable icon picker. Search matches icon names, their older names, and synonyms, so typing "delete" finds the trash icons. Feeds without an icon keep a default one.
  • Edit pencil: appears on hover on desktop, and stays visible on the selected tab on mobile (touch devices never fire hover, so hiding it there would make editing unreachable).
  • Editing or deleting a feed you are not currently reading no longer moves you. You are redirected only when the feed you are on changes id or is deleted.

Review feedback

  • Mobile feed menu looked broken → feed switching moved into the tab bar, drawer restored to its normal content.
  • Feed name was off-center → all tabs use symmetric padding.
  • Pencil was easy to miss → bigger tap target.
  • Missing Linux VRT baselines → regenerated for darwin and linux through the VRT Update Baselines workflow.

Notes

  • Bumps pubky-app-specs to 0.7.0 for the icon field. The icon is stored next to the feed name on the homeserver and is not part of the feed id hash, so changing an icon does not change the feed URL. Feeds saved by an older client keep their icon.
  • The Create/Edit Feed dialog now runs on react-hook-form + zod (useCustomFeedForm), which is where the redirect rule lives.
  • The icon catalog and its search data are loaded on demand, so they stay out of the initial bundle of /home and /feed.

Testing

  • npm test — 12429 passed
  • npm run typecheck, npm run lint, npm run test:vrt:check-baselines
  • VRT baselines regenerated on CI (darwin + linux)

Closes #1898
Closes #1897

Screen.Recording.2026-08-25.at.11.24.24.mov

Show a muted pencil on the right of editable feed tabs so users can
open Edit without navigating. Clicking the name still opens the feed.

Refs: #1898
Co-authored-by: Cursor <cursoragent@cursor.com>
@infin1t3 infin1t3 self-assigned this Aug 16, 2026
@infin1t3 infin1t3 added the 🎨 ui Interface components and styling label Aug 16, 2026
@infin1t3 infin1t3 added this to the v1.8.0 milestone Aug 16, 2026
@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR updates custom-feed navigation so each tab exposes a separate pencil control for editing while preserving feed navigation across the full tab area.

  • Shows edit controls for every custom feed, with responsive hover visibility.
  • Passes the selected feed directly into the edit dialog.
  • Renames the create-tab label from “Create Feed” to “Feed” and updates tests and visual baselines.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/components/organisms/FeedNavigation/FeedNavigation.tsx The full-tab navigation fix now uses an inset Link beneath a higher-stacking edit trigger, resolving the previously reported hit-area issue.
src/components/organisms/CustomFeedDialog/CustomFeedDialog.tsx The dialog can receive an explicit feed for editing while retaining route-derived fallback behavior.
src/components/organisms/FeedNavigation/FeedNavigation.test.tsx Tests cover the revised tab structure, styling, edit controls, and create-tab label.
src/components/organisms/CustomFeedDialog/CustomFeedDialog.test.tsx The added test verifies that an explicitly supplied edit feed takes precedence over the route feed.

Reviews (3): Last reviewed commit: "fix(ui): stretch custom feed link across..." | Re-trigger Greptile

Comment thread src/components/organisms/FeedNavigation/FeedNavigation.tsx
infin1t3 and others added 2 commits August 16, 2026 14:12
Keep name clicks and empty tab space navigating to the feed while the
pencil stays a separate edit control.

Co-authored-by: Cursor <cursoragent@cursor.com>
h-full does not fill a min-height flex wrapper, so clicks on the
tab padding still missed the link. Pin the link to inset-0 instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

URL: https://pubky-app-pr-2353-fzxmjul7ya-oa.a.run.app

pubky-app-pr-2353b7534d7

@secondl1ght secondl1ght left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The mobile feed menu looks bad now, I think this was an unintentional change because it is not mentioned in the PR description. I don't see where this menu is in the Figma, but this doesn't look right.

Before:
Image

After:
Image

Another UI one--having only pr-6 makes the feed label off-center, we can use px-6 to keep it centered while still padding the pencil icon:
Image

Also we can now edit feeds while not viewing them, but then on success we are navigated either to the feed on edit or back to home in the delete case. Is this expected behavior?

And I think the VRTs are QA/CI responsibility right? Or did @catch-21 say he would like us to include these? Only partial coverage was generated here anyway (Linux missing).

Last thing, small nit - the pencil icon is pretty tiny so it could be easy to miss especially on mobile. Consider giving this button some padding.

@infin1t3

Copy link
Copy Markdown
Collaborator Author

The mobile feed menu looks bad now, I think this was an unintentional change because it is not mentioned in the PR description. I don't see where this menu is in the Figma, but this doesn't look right.

Before: Image

After: Image

Another UI one--having only pr-6 makes the feed label off-center, we can use px-6 to keep it centered while still padding the pencil icon: Image

Also we can now edit feeds while not viewing them, but then on success we are navigated either to the feed on edit or back to home in the delete case. Is this expected behavior?

And I think the VRTs are QA/CI responsibility right? Or did @catch-21 say he would like us to include these? Only partial coverage was generated here anyway (Linux missing).

Last thing, small nit - the pencil icon is pretty tiny so it could be easy to miss especially on mobile. Consider giving this button some padding.

You are right, this was an unintentional change.

@aldertnl Looking at the v25 prototype - https://www.figma.com/design/01ZvjSPZnKTNmaEWz0yJsq/Pubky-SHADCN?node-id=35688-283878&t=AGgGqjTMDG5oJVff-4 - I see that the right header button in mobile is not triggering feeds menu anymore - instead it shows Who to follow, Hot tags and Feedback sections - is that intentional?

As for VRT, checked with @catch-21 and he is ok with us including VRT. Waiting for @aldertnl feedback before i add Linux as well.

@aldertnl

aldertnl commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

In prototype v26 feed selection on mobile now behaves the same as on desktop (tabbar), but with only the label showing for the selected tab, icon only on other tabs. This has been done to make mobile more consistent with the desktop experience. As you are working on this feed tab feature now, it makes sense to include this v26 change in this PR. Figma component

Another thing: 'Home' tab should reflected selected reach (so not 'Home', but 'My network', or 'All', etc, with correct icon).

image

infin1t3 and others added 10 commits August 20, 2026 09:23
- Feed navigation is now a tab bar on mobile too (v26): the selected tab
  shows icon + label, other tabs collapse to icon-only, Create is a plus
  icon; the strip sticks under the compact mobile header (Hot pattern)
  and the mobile right drawer falls back to Who to follow / Hot tags.
- The first tab mirrors the selected reach (My network, All, Tagged as,
  ...) with the matching icon, via a shared REACH_FILTER_META map.
- Custom feeds get a selectable Lucide icon (icon picker dialog, dynamic
  icon atom, spec 0.7.0 with the icon field synced to the homeserver),
  ported from PR #2284 and de-i18n-ized; feeds without an icon fall back
  to Activity.
- CustomFeedDialog moves to a useCustomFeedForm hook (react-hook-form +
  zod); edit/delete only navigate away when you are viewing the edited
  feed, and config edits use replace since the old hash id is dead.
- Pencil affordance: hover-revealed on desktop, always visible on the
  selected custom feed tab on mobile, with a larger hit area.
- Pencil: on hover-capable lg+ pointers it hides (and ignores events)
  until hover/focus-within; hover-less devices (iPad, touch laptops)
  keep it visible since :hover can never fire there.
- Feed tab bar: sticky chrome and the gradient fade move to a
  non-scrolling wrapper so the fade paints below the bar instead of
  being clipped into the scrollport with phantom vertical scroll.
- Icon picker: search also matches deprecated Lucide aliases mapped to
  their canonical glyph (querying 'home' finds 'house'); the aliases
  file now stores the alias->canonical map.
- Lucide catalog (~116KB dynamic-import map) is lazily chunked instead
  of shipping in the /home bundle; name checks are shape-based and the
  loader validates against the real catalog.
- One controlled edit CustomFeedDialog per strip instead of a mounted
  react-hook-form instance per feed tab; dialog supports controlled
  open/onOpenChange without a trigger.
- useCustomFeedForm guards submit/delete re-entrancy with a ref so a
  queued second click cannot start a second commit.
- Shared useSelectedReachFilter hook replaces the copied reach
  derivation in HomeFeedSidebar and FeedNavigation; the tab falls back
  to All for an out-of-enum persisted reach instead of crashing.
- CustomFeedDialog derives reach options from REACH_FILTER_META; tab
  width/padding constants unified (active pill pads px-8 everywhere).
- Cleanups: sanitizeIcon at the FeedNormalizer chokepoint, stale JSDoc
  and duplicate import removed in feed.validators, dead
  HomeFeedRightDrawerMobile removed.
- Icon state moves into a subscribable module store read through
  useSyncExternalStore: no impure map read during render (React
  Compiler-safe), hydration correct by construction, one shared state
  per icon, and a failed chunk heals every mounted instance when any
  retry succeeds. Unknown names are answered from the resident catalog
  instead of stored, so peer-synced garbage cannot grow memory, and
  'unknown' is distinguished from 'error'.
- Loaded dynamic icons carry the same lucide-<name> class as static
  imports so CSS rules and test selectors match both.
- Icon picker drops the hand-rolled scroll virtualizer (per-scroll-event
  renders, Tailwind-coupled constants, Tab-unreachable cells) for
  progressive append via the existing useInfiniteScroll hook, drops the
  open-animation gate that blanked the grid on every open, and defers
  the search query with useDeferredValue so superseded keystrokes abandon
  their work instead of mounting throwaway cells.
- Search now also matches lucide's synonym tags ('delete' finds the
  trash icons) via a generated lucideIcons.tags map loaded lazily with
  the catalog; name matches rank first. Result count is announced via
  role=status.
- Verified on the production build: the catalog (57KB) and tags (148KB
  raw) chunks stay out of the /home initial scripts.
- Run VRT serialized (--no-file-parallelism): parallel page startup
  intermittently kills the whole run before any test executes with an
  unhandled 'route.fulfill: target closed' rejection from
  @vitest/browser-playwright (reproduced locally on two consecutive
  commits; serialized runs are reliably stable). Documented in
  docs/visual-regression-testing.md.
- Pre-bundle lucide-react/dynamic.js in the vrt project so the lazy
  catalog import cannot trigger a mid-run dependency re-optimization
  and page reload under in-flight requests.
- renderForVRT now waits for dynamic Lucide icons to finish loading
  (no empty svg.lucide in the capture root), so a baseline can never
  nondeterministically bake in a blank icon box; a hung chunk fails
  the test, matching the existing image policy.
… exports

- Replace plain div/p/span in IconPickerDialog and the FeedNavigation
  custom-feed tab wrapper with Container/Typography (overrideDefaults),
  matching house style; sentinel follows the WhoToFollow precedent.
- Un-export IconPickerDialogProps, DynamicLucideIconProps and
  LucideIconState — no external consumers.
…ds-hover-interaction

# Conflicts:
#	src/components/molecules/Filters/FilterReach/FilterReach.tsx

@aldertnl aldertnl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. Can we swap Save and Delete button positions and use a trash icon for Delete and check icon for Save?
image
  1. Please increase space between name and edit icon on mobile
image
  1. The feed tab bar does not span full width on mobile currently, please fix.
image

Otherwise, LGTM

@aldertnl
aldertnl self-requested a review August 24, 2026 15:32
aldertnl
aldertnl previously approved these changes Aug 24, 2026
…ds-hover-interaction

# Conflicts:
#	package.json
@infin1t3 infin1t3 changed the title fix(ui): reveal custom feed tab pencil on hover feat(ui): add custom feed icons and rework feed tabs Aug 25, 2026
…sm note

Pre-bundling lucide-react/dynamic.js does not prevent the browser-mode
teardown crash — measured with and without it, parallel runs fail either
way. Keep only the serialization flag, which does prevent it, and record
what was actually measured.
Drops the serialization flag: the browser-mode teardown race it worked
around is intermittent, and a rerun is preferable to slowing every VRT
run. Leaves the VRT scripts, config and docs identical to dev.
…dings

- Move the lucide store, alias map, tag map and their tests from
  libs/utils into a dedicated libs/lucide folder.
- Normalize icon-name case in one place: toLucideIconName (UI) and
  FeedValidators.sanitizeIcon (persistence) now agree, so an icon another
  client stored as 'Activity' renders its real glyph instead of silently
  falling back.
- Cancel the VRT dynamic-icon poll on timeout so it stops burning frames
  for the rest of the page.
- Drop the unused reset from useCustomFeedForm.
- Use the shared useControlledState in IconPickerDialog instead of its
  own controlled/uncontrolled handling.
- Close the feed edit dialog when the session ends.
- Drop reach options with no home-store equivalent instead of defaulting
  them onto another option's label and icon.
- Update the docs that referenced the old path and helper.
…omponent

TAGGED_AS_FILTER_KEY moves to config/feed and the ReachFilterValue type
next to ReachType in the home store types, so hooks and core-adjacent
code no longer import a constant from a filter component. FilterReach
keeps only the presentational REACH_FILTER_META.
@infin1t3
infin1t3 requested a review from Taewa August 25, 2026 09:17
infin1t3 and others added 3 commits August 25, 2026 16:06
…failures

- Look up the Lucide catalog with Object.hasOwn: 'constructor' is an
  inherited key that resolves to Object, whose module has no icon node,
  so a synced feed carrying that name crashed the feed pages. Guard the
  resolved node too.
- Handle a rejected catalog load in the icon picker: the loader's own
  catch does not cover the caller's derived promise, so a failed chunk
  became an unhandled rejection and left the grid loading forever.
- Store lucide tags hyphenated so a multi-word query ('air conditioner')
  matches the 227 multi-word synonyms, not just single words.
- Validate stored icons against the same name pattern the UI resolves
  with, now shared from config, so a name cannot be persisted and then
  permanently render as the fallback.
- Retry icons that failed to load when the browser comes back online,
  which is what the component's self-healing promise claimed.
- Cap the active feed tab width below lg so one long feed name truncates
  instead of pushing every other tab off-screen.
- Drop the edit dialog's feed snapshot when a background sync removes
  that feed, and show a stored reach or content this dialog cannot offer
  as a disabled option instead of an apparently empty select.
@Taewa

Taewa commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

I tested with 1 desktop and 1 mobile with the same pubky.
Basically, newly created feed tab is not added on other device unless logout->login

Scenario

  1. On desktop, creates a new tab
  2. On mobile, already logged in and refresh the app
  3. The new tab that was added on the desktop does not appear unless user logs out then logs in themselves

Since it's not involved with Nexus, waiting/refresh doesn't help.
I think we can improve with SSE in other GH ticket.

WDYT @infin1t3 ?

@Taewa

Taewa commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

There some dead-code. Found by AI.

  1. src/components/organisms/ContentLayout/ContentLayout.types.ts:11 — the rightDrawerContentMobile?: React.ReactNode prop declaration.
  2. src/components/organisms/ContentLayout/ContentLayout.tsx:60, 164, 166 — its destructuring and the isMobile && rightDrawerContentMobile ? … : … branch. No production caller passes it; configs.test.ts:36,46,57 in fact assert it is undefined.
  3. src/components/molecules/FeedSection/FeedSection.tsx — the content of the deleted mobile drawer. Zero production imports; only its own test renders it.
  4. src/components/molecules/FeedSection/FeedSection.test.tsx and FeedSection.test.tsx.snap — the test and snapshot for (3).
  5. src/components/organisms/FeedRightSidebar/FeedRightSidebar.test.tsx:6-8 — vi.mock('@/molecules/FeedSection/FeedSection'), still stubbing a component FeedRightSidebar no longer renders.

The drawer itself (HomeFeedRightDrawerMobile) was properly deleted. Removing 1–2 makes 3–5 fall out with it.

Comment thread src/libs/lucide/lucideIcons.ts
@infin1t3

infin1t3 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@Taewa good catch.

I agree this is not caused by this PR. The feed list is read from the local database, and the homeserver is only checked when the app boots — there is no live subscription to the feeds folder, so a second device only learns about a new feed on the next login. The same applies to edits and deletions.

Opened a follow-up for it: #2416. It also proposes reusing the pattern we already have for the mute list, which keeps itself in sync through a homeserver event stream.

…wer slot

- The icon picker renders ~1700 cells and lucide ships one chunk per
  icon, so scrolling the catalog cost ~1700 requests and ~920KB. Add a
  generated bundle of every canonical icon node and load it once when
  the picker opens: one request, ~109KB, and every cell paints straight
  away. Feed tabs keep the per-icon path — they render a handful of
  icons and must not pull the whole set.
- Stop notifying subscribers when an icon enters the loading state: it
  renders exactly like a name that was never requested, so no output can
  change. Halves the notifications a picker session produces.
- Skip writing an icon node that the bundle already resolved while a
  per-icon request was in flight.
- Remove the mobile right-drawer slot from ContentLayout, the FeedSection
  molecule it used to hold, and the stale mock left behind: nothing has
  passed that slot since feed selection moved into the tab bar.
- Document the three generated lucide files and when to regenerate them.
@infin1t3

infin1t3 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@Taewa both points were right, thanks — fixed in 23bbeb4.

Dead code: removed all of it. The mobile right-drawer slot on ContentLayout, the FeedSection component it used to hold, its test and snapshot, and the leftover mock in FeedRightSidebar's test. Nothing passed that slot once feed selection moved into the tab bar.

Icon picker cost: we knew about this one and had measured it while building the picker, so let me share the numbers.

The part you describe as the listener storm is real in count but cheap in practice: we measured the ~3M callbacks at about 2.2ms in total, spread across a whole scroll session, where a single frame is 16.7ms. So on its own that was not a reason to change anything.

What did convince us is the other half — the requests. Every cell fetched its own file, so scrolling the full catalog meant ~1700 requests and ~920KB, which is a real cost on a phone on a first visit. Loading everything in one file is ~109KB, so the break-even is around 200 icons, and the picker already shows 150 in its first batch. Below that the old approach was cheaper, which is why we started there; anything past the first screen tips the other way.

It also removes the waiting: cells now paint the moment they appear, instead of showing an empty square until their file arrives.

The feed tabs keep loading icons one by one, since they only ever show a few and should not pull the whole set.

I also took your point about the loading state: it looks identical on screen to an icon that was never requested, so telling every subscriber about it was pure waste. That notification is gone, which halves the callbacks a picker session produces.

One note on lucide-static: I went with a generated file built from lucide-react instead. lucide-static is a 48MB package that ships image files rather than the icon data our renderer uses, so it would have meant giving up the shared icon styling the rest of the app relies on.

@infin1t3
infin1t3 enabled auto-merge August 26, 2026 06:12
@infin1t3
infin1t3 requested a review from Taewa August 26, 2026 06:13

@Taewa Taewa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@infin1t3
infin1t3 added this pull request to the merge queue Aug 26, 2026
Merged via the queue into dev with commit e6e83bf Aug 26, 2026
14 checks passed
@infin1t3
infin1t3 deleted the bug-ui/1898-custom-feeds-hover-interaction branch August 26, 2026 08:46
@aldertnl

aldertnl commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

potential bug: cannot create or save feeds in latest dev code (with this latest PR merged in)

Fixed locally, was a dependency issue

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

Labels

🎨 ui Interface components and styling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[UI]: Custom feeds hover interaction (v23) [UI]: Custom feed edit icons (v23)

4 participants