Commit Graph

34 Commits

Author SHA1 Message Date
shakespeare.diy 5babdf6fa0 Defer sidebar queries until feed loads and sidebar is visible
- Add `enabled` parameter to `useTrendingTags` and `useLatestAccounts` hooks
- RightSidebar now detects xl breakpoint visibility with matchMedia
- Sidebar queries only fire after the feed finishes its initial fetch
- On smaller screens where the sidebar is hidden, queries are skipped entirely
- Gives the feed full relay bandwidth priority on page load

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 03:16:04 -06:00
shakespeare.diy d5f61e1ff8 Switch feed to infinite scroll with 30 events per page
- Rewrite useFeed to use useInfiniteQuery with timestamp-based pagination
- Reduce page size from 60/40 to 30 for both follows and global tabs
- Add intersection observer sentinel in Feed component for auto-loading
- Deduplicate feed items across pages
- Show spinner while fetching next page

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 03:05:55 -06:00
shakespeare.diy 3ea786f3e8 Default to follows tab when logged in, filter out future-dated posts
- Feed.tsx: initialize activeTab to 'follows' when user is logged in, 'global' otherwise
- useFeed.ts: filter out events with created_at > now in both follows and global feeds, including embedded repost content and fetched originals

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 02:51:03 -06:00
shakespeare.diy b81aa6f2c8 Render naddr1 URLs as embedded Nostr events instead of broken link previews
- Added useAddrEvent hook for fetching addressable events by kind+pubkey+d-tag
- Created EmbeddedNaddr component that displays title, description, image, and author
  from addressable events (extracting metadata from tags and JSON content)
- Updated NoteContent to detect naddr1 identifiers in regular URLs (e.g.
  bitpopart.com/art/naddr1...) and render them as Nostr embeds
- Added naddr1 to the nostr: URI regex so nostr:naddr1... is also handled
- Added naddr-embed token type with proper whitespace collapsing

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 02:30:17 -06:00
shakespeare.diy a3033b6b90 Include kind 6 reposts in the follows feed
- Updated useFeed hook to query kinds [1, 6] for the follows tab
- Reposts are resolved from embedded content JSON or fetched by event ID
- Added FeedItem type with repostedBy and sortTimestamp fields
- Deduplication prefers direct posts over reposts of the same event
- NoteCard now shows a "reposted by" header with the reposter's name
- Feed renders repost items with unique keys

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 02:22:46 -06:00
shakespeare.diy 4f117403b0 Add Vines support: kind 34236 feed page, VineCard, search filter
- Created VinesPage with streaming feed of kind 34236 events
- Created VineCard component: renders video with thumbnail, play overlay,
  author header, title, and hashtag links
- Updated useStreamPosts: 'vines' filter switches to kind 34236 query
  (restarts stream since it's a different kind)
- Search page renders VineCard for kind 34236 events, NoteCard for kind 1
- Updated AppRouter: /vines route now uses VinesPage instead of placeholder
- Created useStreamVines hook for dedicated Vines page streaming
- Validates vine events: requires d tag, imeta with url

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:16:56 -06:00
shakespeare.diy 5b08d2d305 Fix filters: apply client-side without restarting the stream
- Stream effect only depends on [nostr, query] — filter toggles don't
  restart the stream or re-fetch from the relay
- Store all raw events in allEvents state (unfiltered)
- Apply includeReplies/mediaType filters via useMemo on the raw events
- Toggling a filter instantly shows/hides posts from the existing set
  instead of fetching a new batch and getting "no posts"

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:09:31 -06:00
shakespeare.diy 0b1abc2ca0 Make filter changes restart the stream with new results
- Added options.includeReplies and options.mediaType to effect deps
- Changing a radio button or toggle now re-fetches and re-streams
- Removed refs that were preventing filter changes from taking effect

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:07:25 -06:00
shakespeare.diy 63461b84a7 Use wss://relay.ditto.pub for all streaming (both default and search)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:05:17 -06:00
shakespeare.diy 97a5c9669e Use nostr.relay() directly for streaming instead of pool with relays option
- Call nostr.relay('wss://relay.damus.io').req() directly on a single relay
  instead of passing relays option to pool.req()
- Use refs for filter options to avoid restarting the stream on filter changes
- Minimal effect deps: only [nostr, query] trigger stream restart
- Filter changes apply via optionsRef without killing the subscription

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:04:06 -06:00
shakespeare.diy 9d95b10a48 Clean up debug logs from stream hook
The debug logs confirmed the stream IS working correctly:
- req() subscription opens successfully
- EOSE is received and subscription stays open
- New events arrive after EOSE
- Cleanup only happens when the preview environment reloads (replaceDocument)

Removed eoseTimeout from NPool in previous commit which was the
main fix for subscription lifecycle issues.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:02:21 -06:00
shakespeare.diy 67d523b260 Match bitmap/clawchat streaming pattern exactly, remove eoseTimeout
- Rewrote useStreamPosts to exactly mirror bitmap's useChatSession pattern
- Removed eoseTimeout: 200 from NPool config (not a valid NPoolOpts field,
  was potentially causing issues with subscription lifecycle)
- Uses AbortController directly (not useRef)
- fetchInitialMessages + subscribeToMessages run in parallel
- 30s timeout on initial fetch (matching bitmap)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 01:00:01 -06:00
shakespeare.diy d9d6e19ca0 Fix streaming: pass relays option directly to req() like clawchat does
- Pass `relays` array directly to nostr.req() and nostr.query()
  to bypass pool's reqRouter/eoseTimeout that was killing streams
- This is the pattern clawchat uses successfully for live subscriptions
- Default stream uses multiple relays, NIP-50 search uses relay.ditto.pub only
- Filter out future-dated events (created_at > now)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:57:41 -06:00
shakespeare.diy 99f922ea15 Fix streaming: use single relay instead of pool, filter future events
- Always use relay.ditto.pub directly for req() streaming
  (NPool's eoseTimeout: 200 was killing subscriptions prematurely)
- Filter out events with created_at in the future
- Matches clawchat pattern: fetchInitial + streamNew in parallel
- Both query() and req() go to same single relay

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:55:48 -06:00
shakespeare.diy 738b29f075 Fix streaming: run fetch + stream in parallel, use limit: 100 for req
- Match clawchat's proven streaming pattern
- fetchInitial() and streamNew() run in parallel, not sequentially
- req() uses limit: 100 (not limit: 0 which was wrong)
- Added isSubscribed guard for safe state updates
- Proper AbortError handling on both fetch and stream paths

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:53:27 -06:00
shakespeare.diy 87e1580c5a Fix streaming: query initial batch then req for live updates
- Step 1: query() with limit: 40 to fetch initial posts immediately
- Step 2: req() with limit: 0 + since: now to stream new posts live
- Fixes blank page issue (limit: 0 alone returns no stored events)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:52:09 -06:00
shakespeare.diy dcf0650eeb Fix streaming: use AsyncIterable API with AbortController, stream by default
- Rewrote useStreamPosts to use Nostrify's correct req() API which
  returns AsyncIterable, not a subscription object with .close()
- Uses AbortController to cancel streams on cleanup (fixes sub.close error)
- Streams posts by default with no search query (global kind:1 feed)
- When search query is provided, uses NIP-50 search on relay.ditto.pub
- Properly handles EOSE, CLOSED, and EVENT messages from the stream

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:51:12 -06:00
shakespeare.diy 9d3d62f7e6 Set stream subscription limit to 0 for proper real-time streaming
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:49:27 -06:00
shakespeare.diy 9bd46e93a8 Fix search page layout - move filters to collapsible section above content
- Removed broken sidebar-inside-main-column layout that squished content
- Filters now collapse/expand above the post results (matching Ditto's pattern)
- Smooth animated expand/collapse with chevron indicator
- Simplified empty states (plain text, no cards)
- Cleaned up unused useSearchPosts.ts (replaced by useStreamPosts)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:46:37 -06:00
shakespeare.diy 151746e7b2 Implement Ditto-style search page with live streaming posts
- Added Posts, Trends, and Accounts tabs
- Implemented sidebar search filters for Posts tab (replies, media type, language)
- Created useStreamPosts hook for real-time streaming subscriptions
- Search uses relay.ditto.pub for NIP-50 search functionality
- Posts stream in real-time with REQ subscription instead of static queries
- Layout matches Ditto design with filters in left sidebar on desktop
- All tabs functional with proper empty states

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-17 00:44:37 -06:00
shakespeare.diy 9b2d26b6a0 Fix link preview spacing, bot headers, and add YouTube embeds
Spacing:
- Reduce link preview margins from my-2 to mt-1.5 mb-0.5
- Collapse whitespace on text tokens adjacent to block-level tokens
  (link-preview, youtube-embed) so newlines from the original content
  don't stack with the card's own spacing
- Filter out empty text tokens after trimming

Bot headers:
- Send proper User-Agent header ("Mew/1.0 Link Preview Bot") when
  fetching OG data so dynamic sites (SPAs, Twitter, etc.) return
  server-rendered HTML with OG meta tags instead of a JS shell
- Accept application/xhtml+xml in addition to text/html

YouTube embeds:
- Detect YouTube URLs (youtube.com/watch, youtu.be, /embed/, /shorts/)
  and extract video ID
- New YouTubeEmbed component renders a privacy-enhanced iframe player
  (youtube-nocookie.com) with proper 16:9 aspect ratio
- YouTube links render as playable inline embeds instead of OG cards

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 23:03:09 -06:00
shakespeare.diy dd02f2ff27 Render link previews inline where URLs appear in post content
Move LinkPreview rendering from a fixed position below the post into
NoteContent itself, so previews appear exactly where the URL was in
the original text.

- Rewrite NoteContent to use a token-based architecture: useMemo
  produces typed tokens (text, link-preview, mention, nostr-link,
  hashtag), then the render phase maps tokens to components
- Non-media URLs produce a `link-preview` token that renders an
  inline <LinkPreview> card at that position in the content flow
- Remove separate LinkPreview blocks from NoteCard and PostDetailPage
  since previews are now handled by NoteContent
- Remove unused extractPreviewUrl helper from useLinkPreview hook

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 23:00:58 -06:00
shakespeare.diy 00d5024218 Replace non-media link URLs with rich preview cards
- Add useLinkPreview hook that fetches Open Graph metadata (title,
  description, image, favicon, site name) via CORS proxy with 1hr
  stale time caching
- Add extractPreviewUrl helper to find the first non-media URL in
  note content
- Add LinkPreview component with a polished card UI showing OG image,
  title, description, domain favicon, and external link indicator
- Strip all URLs (both media and non-media) from NoteContent inline
  text — media URLs are rendered as image embeds, non-media URLs are
  rendered as LinkPreview cards
- Integrate LinkPreview into NoteCard and PostDetailPage between
  image attachments and action buttons

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 22:58:19 -06:00
shakespeare.diy 5264a7004c Add profile search autocomplete using relay.ditto.pub NIP-50 search
- Create useSearchProfiles hook that queries wss://relay.ditto.pub
  with NIP-50 search filter for kind 0 (profile) events
- Create ProfileSearchDropdown component with debounced input,
  keyboard navigation, and animated dropdown of matching profiles
- Update SearchPage to use the new profile autocomplete
- Update LeftSidebar search bar to use the profile autocomplete
- Selecting a profile navigates to their npub profile page

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 22:46:02 -06:00
shakespeare.diy 86c9627cd0 Limit latest accounts to 5 with client-side slice
The query already had limit: 5 at the relay level, but relays may
return more results than requested. Added .slice(0, 5) after sorting
to guarantee only the 5 most recent accounts are shown.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 22:33:25 -06:00
shakespeare.diy c6317c3893 Add interactions modal for reposts, reactions, and zaps on post details
- Created useEventInteractions hook that fetches kind 6 (reposts),
  kind 7 (reactions), and kind 9735 (zap receipts) for a given event ID
- Created InteractionsModal component with three tabs:
  - Reposts: shows who reposted with avatar, name, and timestamp
  - Reactions: groups reactions by emoji, showing each user under the
    emoji header with counts
  - Zaps: shows who zapped with amount badges (sorted by largest first),
    zap messages, and a total sats summary at top
- Stats row text (e.g. "2 Reposts", "1 👍", "500 sats") is now clickable
  and opens the modal to the relevant tab
- Action buttons (Repost, Reactions, Zaps) in the post detail view also
  open the modal to their respective tabs
- All user rows link to the user's profile page

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 22:14:40 -06:00
shakespeare.diy cd194aa639 Fix emoji reactions display and remove stats border
- Map "+" and empty reactions to 👍 emoji so they appear in the stats row
- Remove border-top from stats row and date-only row to match Ditto

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 21:30:31 -06:00
shakespeare.diy 29a143337b Match PostDetailPage to Ditto's exact layout
- Remove reply composer (Ditto shows replies directly below action bar)
- Change action buttons from justify-around to justify-between (matching Ditto)
- Show reaction emojis in stats row (e.g. "32 👍😂👆") instead of "32 Likes"
  when emoji reactions are available, matching Ditto's display
- Extract unique reaction emojis from kind:7 events in useEventStats hook
- Clean up unused imports (Button, Loader2, useRef, useCurrentUser, etc.)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 21:29:14 -06:00
shakespeare.diy 3fa066bd46 Add post detail page with replies and reply composer
- Create PostDetailPage with expanded post view showing author info,
  full content, images, stats (reposts, likes, zap amount), full date,
  action buttons, and inline reply composer
- Create useEvent hook for fetching single events by hex ID
- Create useReplies hook for fetching kind:1 replies to an event
- Update NIP19Page to render PostDetailPage for note1/nevent1 identifiers
- Make NoteCard clickable to navigate to post detail via nevent1 URL
- Fix NoteMoreMenu to use React Router navigate instead of window.location.href

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 21:24:01 -06:00
shakespeare.diy faf382cdcc Fix zap amount not showing on notes
- Fixed early return using old `zaps` key instead of `zapAmount`
- Added robust amount extraction with 3 fallback strategies:
  1. Top-level `amount` tag on the zap receipt
  2. `amount` tag inside the embedded zap request (description tag)
  3. Parsing the bolt11 invoice string
- This ensures zap amounts display even when relays omit the amount tag

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 17:54:53 -06:00
shakespeare.diy f181f58425 Show total zap amount (sats) instead of zap count on notes
- useEventStats now sums the `amount` tag (millisatoshis) from kind 9735
  zap receipts and returns `zapAmount` in sats instead of a zap count
- NoteCard displays the total with compact formatting (e.g. 1.2K, 3.5M)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 17:53:22 -06:00
shakespeare.diy 589a60ec89 Replace "More" with Bookmarks page
- Created useBookmarks hook for NIP-51 kind 10003 bookmark list management
  (query, add, remove bookmarks)
- Created BookmarksPage with login prompt, loading skeletons, and empty state
- Replaced "More" nav item with "Bookmarks" in LeftSidebar
- Added bookmark toggle button to NoteCard action bar (replaces three-dot menu)
- Removed /more route from AppRouter
- Bookmarks are stored per NIP-51 as replaceable kind 10003 events

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 17:46:37 -06:00
shakespeare.diy 4f1b4800d0 Build Mew: a full-featured kind 1 Nostr client
- Dark theme with purple accent colors matching the reference design
- Three-column layout: left sidebar, main feed, right sidebar
- Left sidebar: Mew logo, search, navigation (Home, Notifications, Search, Vines, Profile, Wallet, Settings, More), compose button, user profile/login
- Main feed: compose box, Follows/Global tabs, live note cards with avatars, author info, NIP-05 verification, relative timestamps, image attachments, and action buttons (reply, repost, like, zap, more)
- Right sidebar: trending hashtags with sparklines, latest accounts, footer
- Full routing: home, profile, notifications, search, hashtags, settings (profile/relays), placeholder pages
- NIP-19 routing with automatic profile redirect
- Custom hooks: useFeed, useTrendingTags, useLatestAccounts, useEventStats
- NoteContent renders mentions, links, and hashtags as clickable elements
- Inter Variable font, skeleton loading states, responsive layout

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 17:05:32 -06:00
shakespeare.diy 98ff1e09e0 New project created with Shakespeare
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 16:54:01 -06:00