Commit Graph

1948 Commits

Author SHA1 Message Date
Alex Gleason a6bfd2cb68 Add Birdex chorus button
New BirdexChorusButton plays every species' Wikipedia recording at once
— a dawn chorus for the whole life list. Hides itself when no species
has usable audio, and the feed-card variant swallows clicks so toggling
playback doesn't navigate away from the NoteCard.
2026-05-05 17:24:57 -05:00
Alex Gleason df38cfdbca Remove share button from PostActionBar
Share action is still available from the NoteMoreMenu. The inline button
in the action bar is redundant and adds visual noise to the row.
2026-05-05 17:24:49 -05:00
Alex Gleason 234d3a21a3 Support NIP-84 Highlight events (kind 9802)
Render highlight excerpts as pull-quotes with source attribution across
feed cards, detail pages, and quote embeds. Without this, kind 9802
events fell through to UnknownKindContent in cards and — worse — had
their quoted prose fed through the kind-1 tokenizer in embeds,
auto-linkifying URLs and hashtags that were part of the original source,
not the highlight author's post.

Integration points:
- HighlightContent renders the excerpt with an accent blockquote, wraps
  the highlighted span in <mark> when a context tag is present, and
  attributes the source via EmbeddedNaddr (a-tag), EmbeddedNote
  (e-tag), or a sanitized URL chip (r-tag).
- EmbeddedHighlightCard gives quoted highlights a dedicated compact
  card instead of the generic-embed fallback.
- Added 9802 to NoteCard + PostDetailPage dispatch, KIND_HEADER_MAP,
  CommentContext labels/icons, NOTIFICATION_KIND_NOUNS, and the
  EmbeddedNote dispatcher.
- Registered an EXTRA_KINDS entry with feedIncludeHighlights (off by
  default) and showHighlights (on), plus a /highlights route backed by
  KindFeedPage.
- Added a highlights notification type with its own subscription
  template, preference toggle, grouped notification row, and
  author-ownership filter so users are only notified when their own
  content is highlighted.
- feedUtils hides empty highlights with no source reference.
2026-05-05 17:24:42 -05:00
Alex Gleason 64a4643503 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-05-05 16:11:00 -05:00
Alex Gleason c69aee40a2 Add bottom border to ComposeBox by default
Every context except the Feed composer follows the compose box with
content that needs visual separation (comment lists, wall posts,
threaded replies), so add a default border-b and let Feed opt out with
a new hideBorder prop — it sits directly above SubHeaderBar's arc
background, which already provides its own separator.

Modal usage (forceExpanded) stays borderless since it lives inside
a dialog container.
2026-05-05 15:49:55 -05:00
Alex Gleason 908c5b248c Enable commenting on NIP-73 identifier /i/ pages
ExternalContentPage already supported fetching comments for non-URL
external roots (bitcoin:tx:..., isbn:..., iso3166:..., etc.), but the
inline ComposeBox and the FAB's ReplyComposeModal were both gated on
the URL-only commentRootUrl — leaving those pages with no way to post.

Widen ComposeBox's replyTo and ReplyComposeModal's event to accept
`#${string}` NIP-73 identifiers alongside URLs and events, route them
through the existing NIP-22 publish path (usePostComment already
handled string roots), and wire the page up to the combined
commentRoot.
2026-05-05 15:46:30 -05:00
Alex Gleason 546b1aff9b Let shield icon inherit hover color from button
text-primary was applied directly to the Shield icon, overriding the
ghost button's hover:text-accent-foreground. On hover the background
turned into accent while the icon stayed primary, producing a
low-contrast pairing. Move text-primary up to the Button so the
ghost variant's hover rule can take over, and let the icon inherit
currentColor.
2026-05-05 15:01:13 -05:00
Chad Curtis 9aecefff40 Merge branch 'feat/blobbi-1124-interactions' into 'main'
Add Blobbi social interactions (kind 1124)

Closes #265

See merge request soapbox-pub/ditto!211
2026-05-05 19:11:09 +00:00
Alex Gleason d34a155922 Simplify permission manager popover
- Remove the app-name subtitle under "Permissions"; the site is already
  identified by the nav bar directly above the popover.
- Drop the trash icon from the "Revoke all" button so the destructive
  action reads as plain text like the row items.
- Remove the per-row allow/deny toggle. Stored permissions are always
  "allowed" in practice (a denied prompt doesn't surface a row the user
  would want to keep around), so the toggle added noise without a
  realistic use case. Users who change their mind can remove the row and
  re-prompt.
- Drop the status check/X icon on the left of each row now that the
  toggle is gone and every listed permission is implicitly allowed.
- Show the remove button always (was opacity-0 until hover) and switch
  it from a trash icon to an X, matching the close-affordance idiom used
  elsewhere in the app.
- Drop the siteName prop from NsitePermissionManager; nothing uses it
  anymore.
2026-05-05 14:00:28 -05:00
Alex Gleason c2c5b5c3be Drop target-pubkey from nsite encryption/decryption prompts
The target pubkey was rendered as a truncated hex string in the prompt,
which is noise to the user: they can't verify it, it doesn't scope the
stored permission (which is global-to-the-app), and showing a pubkey
next to "Allow" misleadingly suggests the decision applies only to that
peer. Drop the field from NsitePromptState and stop threading it through
the four encrypt/decrypt RPC branches.
2026-05-05 13:57:15 -05:00
Alex Gleason 5e729f74cd Grant broad permissions-policy and fix nav-bar safe-area spacing on sandbox frames
Sandbox iframes live on a cross-origin subdomain, so most capability APIs
are blocked unless the parent delegates them with allow=. Add a permissive
policy covering media (camera, microphone, display-capture, encrypted-media,
picture-in-picture, autoplay, speaker-selection), sensors (accelerometer,
gyroscope, magnetometer, ambient-light-sensor, compute-pressure, battery),
input (gamepad, midi, keyboard-map, xr-spatial-tracking), and UX features
(fullscreen, geolocation, idle-detection, screen-wake-lock, clipboard-write,
web-share, window-management, storage-access) so nsites and webxdc apps can
use anything a regular web app would.

Deliberately omitted for security: payment, publickey-credentials-*,
otp-credentials, identity-credentials-get (all phishing/account-takeover
vectors), local-fonts (fingerprinting), bluetooth/hid/serial/usb/
clipboard-read (raw device access left off for now).

Drop WebxdcIframe's narrow allow= override so webxdc apps get the same
broad policy instead of downgrading to just autoplay/fullscreen/gamepad.

Also split safe-area-top from content padding in the NsitePreviewDialog
and WebxdcEmbed nav bars: the outer element reserves space for the
notch inset, the inner row keeps a fixed px-3 py-2 flex layout so
content stays vertically centered inside the intended 44px bar height.
2026-05-05 12:52:49 -05:00
Alex Gleason 9e6ed02ce1 Add nsite:// sidebar pinning with auto-launch, favicon, and highlight
Introduces a new sidebar item type for nsites that auto-opens the nsite
preview when clicked, using React Router state to prevent external URLs
from triggering auto-launch.

- Add isNsiteUri/nsiteUriToSubdomain helpers and parseNsiteSubdomain
- Create NsiteSidebarItem with site favicon and link preview title label
- Wire nsite:// dispatch in SidebarNavList, useFeedSettings, SidebarMoreMenu
- NoteMoreMenu pins named nsite events as nsite:// URIs instead of nostr:
- NsiteCard gains a Pin/Unpin button and an autoPlayKey prop that re-opens
  the player each time the sidebar item is clicked
- NsitePlayerContext tracks the active subdomain for sidebar highlighting,
  provided in MainLayout so sidebar and pages share state
- PostDetailContent consumes nsiteAutoPlay router state and clears it
  after consumption so a page refresh doesn't re-trigger auto-play
2026-05-05 12:38:26 -05:00
Alex Gleason 973acd7e9b Inject NIP-07 signer into nsites and drop native sandbox path
When a logged-in user opens an nsite preview, a window.nostr provider is
injected into the sandboxed iframe. The provider proxies signEvent, nip04,
and nip44 calls to the parent signer over the existing JSON-RPC bridge.

A permission system gates each operation:
- getPublicKey is auto-allowed (clicking Run implies consent)
- signEvent prompts are granular per event kind (like Amber)
- encrypt/decrypt prompts are per operation type
- Users can check 'Remember for this site' to persist decisions
- Permissions are scoped to (userPubkey, siteId) in localStorage

The nsite preview nav bar gains a shield icon that opens a popover for
managing stored permissions.

Kind labels for the signer nudge, the permission prompt, and the post-
detail loading title now route through a central KIND_LABELS registry
(src/lib/kindLabels.ts) instead of three divergent inline maps.

The native SandboxPlugin (iOS WKWebView / Android WebView overlay) is
removed; SandboxFrame now always uses iframe.diy, so native behavior
matches web. This drops ~1100 lines of native code, the Android-only
blob prefetch workaround in NsitePreviewDialog, and the createPluginCall
registration in MainActivity and capacitor.config.json.
2026-05-05 12:15:31 -05:00
Alex Gleason 0497aa33c9 Show live progress during the nostrconnect login handshake
When a user tapped "Open Signer App", the dialog previously stayed
frozen on the same screen — same button, same copy-URI fallback, no
feedback — until the login either succeeded (and the dialog dismissed)
or timed out after two minutes. With slow or flaky signers (Amber's
current listening-REQ bug being the immediate trigger, but any NIP-46
signer that takes more than a second or two to respond hits the same
hole) this looked indistinguishable from a hang. Users retapped the
button, closed the dialog, gave up.

Now the dialog swaps the QR / Open Signer App area for a centered
spinner with a live status line as the handshake advances:

- "Waiting for signer connection…" while the signer app has the user
  and we're listening on kind 24133 for the connect-ack.
- "Getting public key…" once the connect-ack arrives and we're
  issuing the NIP-46 get_public_key RPC.

On mobile the swap happens synchronously when the user taps "Open
Signer App" so they see the progress state the moment they return
from the signer — this is the most important window, since that's
exactly when the original UI left them staring at a button they
were worried they needed to re-tap. On desktop the QR stays visible
through the awaiting-connect phase (they may still be scanning with
a different device) and only swaps in once the signer has
acknowledged.

The progress view includes a Cancel link (primary color, matches the
"Create account" affordance) that aborts the in-flight subscription
and regenerates fresh connect params — equivalent to the existing
Retry path, but reachable while the handshake is live instead of
only after a failure.

The handshake phases are surfaced via the new `onStatus` callback
on `NLogin.fromNostrConnect` in @nostrify/react 0.6.0. Bumps
@nostrify/react to ^0.6.0 and @nostrify/nostrify / @nostrify/types
to their matching versions (^0.52.0 / ^0.37.0) to avoid duplicate
nested package copies that would otherwise split type identity.

Incidental cleanup while editing the dialog: the Copy URI button and
the "Tap to open your signer app" / "Scan with your signer app"
status lines are removed. The primary Open Signer App button is
self-explanatory on mobile, and the QR on desktop doesn't need a
caption.
2026-05-04 21:42:45 -05:00
Alex Gleason 0d1fe7bbca Fix nostrconnect listen effect losing successful logins on re-render
The nostrconnect listening effect depended on `login`, `onLogin`,
`onClose`, and `isWaitingForConnect`. `login` is a fresh object from
useLoginActions on every render, and every call site passes inline
arrow functions for onLogin/onClose, so the effect re-ran on every
parent render. Each re-run fired the cleanup and flipped a local
`cancelled = true` flag, and when the signer's NIP-46 response
eventually arrived, the success branch saw `cancelled === true` and
silently skipped `onLogin()` / `onClose()` — the user was logged in on
the backend but the dialog never closed.

Stabilize onLogin/onClose/login via latest-value refs, narrow the
effect deps to `[nostrConnectParams]` only, and gate the success
branch on `controller.signal.aborted` (which is only true when the
dialog was explicitly closed or handleRetry fired). Drop the unused
`isWaitingForConnect` state. Also abort the in-flight controller from
handleRetry before regenerating params, so the prior subscription
doesn't linger.

The bug was masked by most signers responding fast enough (<1s) that
parent re-renders didn't happen during the wait. It surfaced during
an upstream Amber bug that delays its listening REQ by ~8+ seconds,
giving render cycles time to fire (https://github.com/greenart7c3/Amber/pull/420).
The Amber bug is getting fixed separately; this Ditto fix stands on
its own — any signer that takes a few seconds to respond could trip
the same race.
2026-05-04 21:16:38 -05:00
Alex Gleason b5675802f4 Replace feed-style page-load skeleton with a neutral spinner 2026-05-04 19:12:31 -05:00
Alex Gleason 14e5a82b1e Play shortform videos unmuted when autoplay is off
VineMedia (kind 22 / 34236 in regular feeds) always started muted on
click-to-play, even when the "autoplay videos" setting was disabled.
That made click-to-play behave differently from the regular VideoPlayer,
where a user-gesture click plays with sound.

Now VineMedia only starts muted when autoplayVideos is on (browsers
require autoplay to be muted). When the setting is off, a click is an
explicit user gesture and plays with audio. The <video> muted attribute
is also bound to state so it tracks the mute toggle correctly. If play
is rejected unmuted, we retry muted as a fallback.

Regression-of: 23e845eb
2026-05-04 18:13:04 -05:00
filemon 20d7aa199d Merge branch 'main' into feat/blobbi-1124-interactions 2026-05-04 13:29:31 -03:00
Alex Gleason 64ca61bb32 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-05-04 09:52:00 -05:00
root 46cdbe08eb Linkify nostr URIs, URLs, hashtags, and emoji in article bodies
Long-form articles (kind 30023) previously ran their content through
react-markdown with no Nostr awareness, so `nostr:` URIs, bare URLs,
hashtags, and custom emoji all surfaced as literal text. This composes
NoteContent into the markdown renderer instead, so mentions get hover
cards, quoted notes render as embedded cards, URLs get link previews,
and custom emoji resolve — matching regular note behavior exactly, with
zero duplicated parsing.

NoteContent gains an `as: 'div' | 'span'` prop so it can be embedded
inline inside markdown elements without invalid-HTML nesting. Markdown
paragraphs render as `<div>` (with reproduced prose paragraph spacing)
so block-level quote cards and images are legal children. Headings
suppress block embeds to avoid quote cards inside an `<h1>`.

Also strips Typography's default always-on link underline in favor of
hover-only, matching the rest of the app.
2026-05-04 04:28:41 +00:00
Alex Gleason 1597e7540b Disable user relays by default
User relays are no longer used until the user explicitly opts in via
Settings > Network. Adds a useUserRelays toggle alongside the existing
useAppRelays toggle in RelayListManager, defaulting to false. Fresh
installs and new accounts will only query app-default relays until the
user enables their personal NIP-65 list.

The user's relay list (kind 10002) is still synced from Nostr and
managed in the UI when logged in — the toggle only controls whether it
is included in the effective relay set used by NostrProvider's pool
and useNativeNotifications. The setting is persisted to AppConfig and
synced cross-device via NIP-78 encrypted settings.

getEffectiveRelays now takes both flags and short-circuits accordingly,
producing an empty list when both are off (instead of the previous
behavior of always returning user relays).
2026-05-03 09:00:43 -05:00
Alex Gleason 74a2522af1 Show right widget sidebar at iPad-landscape width
The right sidebar previously required xl (1280px) to appear, so horizontal
iPad (1024px) viewports saw only the left sidebar + main column. Use the
existing lg breakpoint (1024px) to control right-sidebar visibility, and
let the sidebars scale fluidly with the viewport by setting them to w-1/4
max-w-[300px] instead of fixed pixel widths. The center column (flex-1)
absorbs whatever space remains, so the layout fills the viewport smoothly
from 1024px up through the 1200px wrapper cap instead of leaving dead
space at intermediate widths. Below the lg breakpoint, the left sidebar
keeps its fixed 300px width.
2026-05-01 22:10:11 -05:00
Chad Curtis 5d99337cd2 Allow hyphens inside hashtags
Tags like #70-706 and #bitcoin-conference were split at the hyphen,
since the regex only matched letters, numbers, and underscores. Allow
internal hyphens (but not leading or trailing ones, so #nostr- still
captures just #nostr) and share the pattern across NoteContent,
BioContent, and the compose boxes so posted t-tags match what renders.
2026-04-30 11:22:07 -05:00
Alex Gleason 9a34fa0102 Play bird-song recordings on Wikipedia species pages
Wikipedia/Wikimedia Commons hosts editorially-curated, mostly Xeno-
Canto-sourced recordings on bird species articles. Surface them on
Ditto's /i/ page whenever the article exposes one: an emerald play
button sits inline with the article title, looping the song on
click and swapping its play triangle for an animated equaliser to
indicate active playback. When a recording is present, the footer
row gains a second link crediting the recordist and license and
pointing at the Commons file-description page for verification.

Adapted from Birdstar's BirdInfoDialog / useBirdSound / useWikipedia
Sound hooks; the iNaturalist fallback from the original is dropped
per request — Ditto only uses Wikipedia/Commons.
2026-04-30 01:57:51 -05:00
Alex Gleason 7eb70f3a61 Prefer the n tag for scientific name on Bird Detection cards
Birdstar kind 2473 events now carry the species' scientific binomial
name in an authoritative 'n' tag, added to the NIP so clients can
label a detection without round-tripping Wikidata. Ditto was still
scraping the name out of the 'alt' tag's parenthetical, which loses
the label when the publisher emits a bare alt like "Bird detection".

Prefer the 'n' tag and fall back to 'alt' parsing only for older
events authored before the tag was part of the NIP. Also show the
scientific name as a persistent italic sub-label on Birdex tiles,
matching how detection cards stack the two labels.

Regression-of: b2634d2f
2026-04-30 01:52:01 -05:00
Alex Gleason b2634d2fcb Render Birdstar Birdex events (kind 12473) as tiled life lists
A Birdex is a replaceable per-author index of every bird species the
author has ever confirmed via kind 2473, stored as positional i/n tag
pairs in chronological first-seen order. In feeds, show a compact
tile strip of the most recently-added species with a "+N" capstone
when the list overflows — mirroring how kind 3 follow lists preview
members as an avatar stack plus "+N more". On the post-detail page,
render every species as a responsive grid so visitors can browse the
author's whole life list.

Each tile resolves the species' Wikidata entity through English
Wikipedia to pull a thumbnail and common-name label, reusing the
same fetch path as kind 2473 detection cards. The Wikidata URL is
sanitized before being routed, and the paired n tag provides a
scientific-name fallback while the remote lookup is in flight.
2026-04-30 01:44:15 -05:00
Alex Gleason 981e4f0726 Prefer metadata.name, fall back to metadata.display_name everywhere
Previously getDisplayName() and ~50 inline sites only consulted
metadata.name, ignoring display_name. A handful of other sites used the
opposite priority (display_name || name), so the same user could render
under different names across the UI.

Standardize on `name || display_name || genUserName(pubkey)` in the
helper and at every call site, and widen two local inline metadata types
in RightSidebar and SearchPage that did not declare display_name.
2026-04-30 01:30:07 -05:00
Alex Gleason 476e99ab81 Allow clicking the Bitcoin tab when the signer is unsupported
When the Bitcoin tab only had a dead-end error panel, ZapDialog auto-
switched to Lightning whenever capability was unsupported and the author
had a Lightning address. Now that the Bitcoin tab renders a QR fallback
the user can actually use, that forced redirect prevented them from
clicking into it: every render while activeTab === 'onchain' flipped it
back to 'lightning'.

Drop the mid-session auto-flip effect. The initial tab choice still
biases toward Lightning (via the useState initializer and the open-reset
effect), but manual navigation into Bitcoin is respected.

Regression-of: bf540fb5
2026-04-29 23:33:46 -05:00
filemon 9483fbc99a Merge branch 'main' into feat/blobbi-1124-interactions 2026-04-30 00:56:01 -03:00
Alex Gleason bf540fb5c1 Offer QR-code fallback for unsupported signers in onchain zap dialog
When the user's signer can't sign PSBTs (extension without signPsbt, or a
bunker that rejected sign_psbt), the onchain zap tab previously showed a
dead-end 'Bitcoin zaps aren't available' panel. Replace it with an
amount selector + BIP-21 QR code the user can scan from any external
Bitcoin wallet, plus copy buttons for the address and payment link.

Because Ditto never sees the resulting transaction, no kind 8333 is
published in this flow. A warning explains that the zap won't show up
as theirs on Nostr even though the recipient still gets the Bitcoin.

Also gate the mempool.space UTXO and fee-rate queries on the capability
being non-unsupported to avoid pointless network calls in this branch.
2026-04-29 22:39:27 -05:00
Alex Gleason c5d5165f84 Merge remote-tracking branch 'origin/main' into wallet
# Conflicts:
#	package-lock.json
#	src/components/CommentContext.tsx
#	src/components/ExternalContentHeader.tsx
#	src/components/NoteCard.tsx
#	src/pages/VinesFeedPage.tsx
2026-04-29 21:44:07 -05:00
filemon 222f641123 Merge branch 'main' into feat/blobbi-1124-interactions 2026-04-29 14:58:01 -03:00
Chad Curtis 43b2ac91b6 Merge branch 'fix/blobbi-daily-missions' into 'main'
Fix daily bounty hydration and persistence

Closes #268

See merge request soapbox-pub/ditto!212
2026-04-29 11:08:48 +00:00
filemon 98644047eb Fix daily bounty hydration and persistence
- Add usePersistDailyProgress hook: debounced persistence of daily mission
  progress to kind 11125 content, with flush on visibility-hidden and unmount
- Fix hydration race: deterministic merge strategy prefers persisted missions
  when they carry real progress; keeps local if persisted has zero progress
- Fix content preservation: all kind 11125 write paths now preserve existing
  content instead of publishing content: '' which wiped daily mission data
- Fix stale-read-then-write: useBlobbiPurchaseItem, profile normalization,
  and onboarding auto-fix now use fetchFreshEvent before publishing
- Add isLoading state to useDailyMissions for proper loading/empty exclusion
- Convert take_photo missions from event to tally tracking
- Extract trackInventoryDailyActions for DRY daily mission tracking
- Handle legacy EventMission → TallyMission conversion in trackTally
- Add dev-only Reset Daily Missions button in BlobbiDevEditor

The content: '' pattern originated in 251ea43e and several subsequent commits
that created kind 11125 write paths. It only became a data-loss bug when daily
mission persistence was introduced. No single regression-of commit is
identifiable — it is a systemic gap across multiple files.
2026-04-28 23:17:38 -03:00
Alex Gleason 423d53ea58 Send 'Discover people' link to the Global tab on /packs
When the home-feed Follows tab is empty, the 'Discover people to follow'
button links to /packs. That page is also a Follows/Global tabbed feed,
and it likewise defaulted to Follows — so a user who follows nobody
landed on another empty view.

Pre-seed the /packs feed tab to 'global' in sessionStorage before
navigating, so the link lands on a populated view.

Regression-of: 399df4da
2026-04-28 20:45:12 -05:00
Alex Gleason cf2f466772 Restrict unknown-kind previews to the NIP-31 alt tag
The alt-tag fallback shipped in 9813a226 let several display paths fall
through to other tags (title, name, summary, description, d) or to the
raw content when alt was absent. For an unknown kind like attestr.xyz
(31871), that surfaced the opaque d-tag identifier
'e5272de9:289bce03a0b7:1777206698' as the preview and leaked raw content
into the hover-card reply indicator — both worse than the 'This event
kind is not supported' tombstone the feature was meant to produce.

Tighten the fallback everywhere an unknown kind might render:

- getEventFallbackText: only the NIP-31 alt tag; no title/name/d.
- CommentContext.getEventDisplayName: known kinds keep title/name/d,
  unknown kinds consider only alt. getKindLabel returns 'an unsupported
  event' instead of 'a post', so 'Commenting on ...' never implies the
  root is a text note.
- EmbeddedNote tagMeta: alt only, no title/name/description fallback.
- ExternalContentHeader.AddressableEventPreview: drop d-tag fallback.
- EmbeddedNaddr: gate rich title/description/content rendering behind
  a known-kind check; unknown kinds render UnknownKindContent instead
  of extractMetadata, which was leaking plaintext content as the
  description when the body wasn't JSON.

Regression-of: 9813a226
2026-04-28 14:44:48 -05:00
Alex Gleason 9813a226ec Render unknown event kinds with a NIP-31 alt-tag fallback
Previously, any kind not explicitly handled by NoteCard or
PostDetailContent fell through to the kind-1 text-note renderer, which
ran the URL/hashtag/nostr: tokenizer over arbitrary content — broken
for events whose content is JSON or empty.

Add an UnknownKindContent component that displays the NIP-31 'alt' tag
(falling back to title/name/summary/d) in a rounded card, or a dashed
'This event kind is not supported' tombstone when the event carries no
fallback text. Route to it from both dispatchers when the kind isn't
1, 11, or 1111.

Extend the same handling to embedded quote previews (EmbeddedNote,
EmbeddedNaddr, AddressableEventPreview) so reply-context hover cards,
compose previews, more-menu previews, notification references, and
inline nostr: mentions all display unknown kinds consistently instead
of feeding JSON or arbitrary content to the kind-1 tokenizer.
2026-04-28 14:05:51 -05:00
Alex Gleason fcfcb381a8 Add detail-page header and a Discuss button for bird detections
Detail page: render the kind 2473 / 30621 action header ("Alex Gleason
heard a bird", "… drew a constellation") alongside the existing
per-kind headers, using the same phrasing as KIND_HEADER_MAP.

Bird-detection card: add a Discuss link next to the Wikipedia link,
routing to /i/<wikidata-url> so comments on the species' NIP-73
identifier aggregate across detections.

Regression-of: c957041c
2026-04-28 03:23:48 -05:00
Alex Gleason 3f3d99e25a Swap constellation edge count for a Birdstar deep-link
Surface a "View on Birdstar" external action that opens the constellation
on birdstar.app via its naddr1, which is more useful than the edge count
— users can actually interact with the figure there.

Regression-of: c957041c
2026-04-28 03:14:23 -05:00
Alex Gleason c957041cf3 Render Birdstar bird detections and custom constellations
Adds feed support for kind 2473 (bird-by-ear detections) and kind 30621
(user-drawn star figures) from Birdstar. Detections render as species
cards using the existing Wikidata + Wikipedia summary hooks; constellations
render as gnomonically-projected SVG star-maps backed by the Hipparcos
catalog from d3-celestial. The 1.1 MB catalog is code-split via lazy() so
it only loads when a constellation event is actually viewed.
2026-04-28 03:12:24 -05:00
filemon 6684efd146 Add Blobbi social interactions (kind 1124)
Introduce external social interactions for Blobbi companions. Other users
can feed, play, clean, and medicate a Blobbi they don't own via kind 1124
events. The owner's canonical 31124 state absorbs these interactions
through a checkpoint-based consolidation system with auto-sync on page load.

Key additions:
- Kind 1124 event schema with validation, parsing, and deterministic sort
- Social projection pipeline (read-only stat overlay from pending interactions)
- Owner-side consolidation into canonical state with checkpoint advancement
- Auto-sync hook (useCanonicalSync) triggered when owner opens /blobbi
- Social permission toggle (open/closed tag on 31124)
- Interaction UI: popover with item carousel on feed cards, detail pages,
  and the owner dashboard action bar
- Interaction reactions: facial expressions, sparkles, bubbles, floating
  hearts with phase-based animation system
- Activity tab showing interaction history with caretaker attribution
- BlobbiSocialActions component with egg gating and cooldown logic
- NIP.md documentation for the new kind
2026-04-27 23:02:19 -03:00
Alex Gleason 480e0aa97f Render follow lists with avatar stacks in profile recovery
The Follows tab of the profile recovery dialog previously showed only
a generic Users icon, a follow count, and a timestamp — making every
historical snapshot look identical and giving users no way to
distinguish one version from another. Now each snapshot renders an
avatar stack of the followed profiles, which makes differences between
snapshots visible at a glance.

Changes along the way:

- Extract the avatar-stack block from PeopleListContent into a shared
  PeopleAvatarStack component (sm/md/lg sizes, hover-to-pop-forward
  with display-name tooltip, "+N more" overflow).
- Reverse `p` tags in kind 3 displays so the newest follows surface
  first. Kind 3 grows by appending, so the natural order is
  oldest-first — the same early follows would otherwise dominate every
  preview. Implemented as a getDisplayPubkeys(event, pubkeys) helper
  used at display sites only; mutations and filters keep the original
  array.
- Compact the snapshot cards in ProfileRecoveryDialog by moving the
  Restore button into the top-right slot that already hosted the
  Current badge, and dropping the redundant "<icon> N follows" title
  row from the Follows card since the avatars communicate the same
  thing more clearly.
2026-04-27 15:58:07 -05:00
Alex Gleason ee5d3415ac Drop hardcoded feedSettings block from onboarding
The questionnaire that used to drive this block was simplified away in
e88f9e5f, but the save handler kept writing a full hardcoded feedSettings
object. That clobbered App.tsx's defaultConfig with a narrower preset and,
on the phase === 'not-found' path, could overwrite a returning user's
tuned settings if the encrypted-settings fetch returned empty. It also
drifted every time a new setting was added to App.tsx.

Trust defaultConfig for fresh users and encrypted-settings sync for
returning users. The follow-list check and step routing (outro vs follows)
are what the callback is actually for.
2026-04-27 14:19:11 -05:00
Alex Gleason e9def50a85 Render webxdc embeds as a tilted, color-tinted cartridge
Replace the plain launch card with a Game Boy-style cartridge (using
public/cartridge.png) whose label region centers the app icon. The whole
cartridge scales as one image and reacts to the pointer with the
existing useCardTilt 3D effect.

The cartridge is tinted by the icon's dominant color: a new
useDominantColor hook samples the icon in an off-screen canvas, picks
the heaviest hue bucket, and exposes it as HSL. A mask-image layer
masked to the cartridge silhouette blends the color over the grayscale
PNG with mix-blend-mode: color, preserving the shading. Grayscale or
CORS-blocked icons fall back to the original gray cartridge.

The app name moves out from under the cartridge and into the description
card in FileMetadataContent — rendered larger and bolder above the
note's content — while WebxdcEmbed still renders its own name card when
a parent isn't providing one (e.g. the kind 1 imeta path).
2026-04-27 01:15:54 -05:00
Alex Gleason e883309791 Remove direct messaging feature and skill
Deletes the DM implementation (DMProvider, DMContext, useDMContext,
useConversationMessages, DMChatArea, DMConversationList,
DMMessagingInterface, DMStatusInfo, dmMessageStore, dmUtils,
dmConstants, the orphaned pages/Messages.tsx, and the
nostr-direct-messages skill) and removes the corresponding wrapper
from the provider tree in App.tsx.

The feature was already disabled (dmConfig.enabled = false), so this
removes no user-visible functionality -- only ~1,600 lines in
DMProvider and the associated UI/context/hooks. The nip44/nip04 signer
paths used by drafts, letters, mute lists, and encrypted settings are
unrelated and remain. Kind 1222 voice messages are a public-feed
feature and stay.

Documentation cleanup: strip the three DM mentions from AGENTS.md
(Project Structure, App.tsx provider list, Specialized Workflows skill
pointer) and the Private Messaging bullet from README.md's feature
list. Historical CHANGELOG entries are preserved.
2026-04-26 23:21:54 -05:00
Alex Gleason 9d914a430c Add touch support to Magic card 3D tilt
Wire up pointerdown/up and the glare overlay so press-and-drag drives
the tilt on touch, matching the BadgeDetailContent behaviour. A quick
tap still opens the lightbox via the inner button. touch-action: pan-y
keeps vertical page scrolling working.
2026-04-25 21:09:06 -05:00
Alex Gleason aa8541298e Render Gatherer card URLs as Magic cards via Scryfall
gatherer.wizards.com URLs (e.g. /BNG/en-us/156/xenagos-god-of-revels or
the legacy ?multiverseid=...) are now resolved through the Scryfall API
and rendered as actual Magic: The Gathering cards throughout the app:

- /i/<gatherer-url>: GathererCardHeader shows the card art at 280px max
  width with properly rounded corners, a mouse-driven 3D tilt + specular
  glare matching the badge showcase, a click-to-open lightbox, and a
  face toggle for DFC/MDFC/split cards.
- Page <title> on /i/<gatherer-url> uses the real card name.
- 'Commenting on …' breadcrumbs under kind 1111 comments show the card
  name with the CardsIcon and a hover-card preview.
- Parent context on PostDetailPage (e.g. /nevent1… for a comment rooted
  on a Gatherer URL) shows a compact preview row matching the ISBN and
  country patterns: small card art, 'Magic Card' label, card name, set.

Scryfall integration is centralised in src/lib/scryfall.ts (image URLs
and typed JSON fetching) and src/hooks/useScryfallCard.ts. MagicDeckContent
has been refactored to use the shared image helper. All four call sites
share a single TanStack Query cache keyed on the lookup, so one card
triggers one network request.
2026-04-25 20:46:52 -05:00
Alex Gleason 4fdbb4d960 Render Wikidata URLs as their Wikipedia article on /i/
Adds special handling for Wikidata entity URLs (https://www.wikidata.org/entity/ID
and https://www.wikidata.org/wiki/ID) on the /i/ external content page.

When a Wikidata URL is used, the entity's enwiki sitelink is resolved via the
Wikidata Action API and the page renders the same rich Wikipedia embed that
would appear for the Wikipedia URL directly. Falls back to a generic link
preview when the entity has no English Wikipedia article.
2026-04-25 19:34:33 -05:00
Chad Curtis f4f8e49627 Merge branch 'feat/update-blobbi-decay' into 'main'
Add pure segment-display helper for stage-based Blobbi status UI

Closes #255

See merge request soapbox-pub/ditto!204
2026-04-25 22:29:04 +00:00
Chad Curtis a55233fdb1 Fix compose textarea losing its expanded height when toggling preview
The auto-resize effect only depended on content, so switching back
from preview mode remounted the textarea without recalculating its
height. Adding previewMode to the dependency array ensures the
textarea is resized immediately after remounting.
2026-04-24 17:36:03 -05:00