Commit Graph

4447 Commits

Author SHA1 Message Date
Alex Gleason f4ae344b30 Unify follow list, follow set, and follow pack rendering
Kind 3 (NIP-02 follow list), kind 30000 (NIP-51 follow set), and kind 39089
(follow pack) are all the same semantic thing — an event containing a list of
p-tagged pubkeys — but were being rendered three different ways, with kind 3
having no rendering at all, kind 30000 routing to a bespoke ListDetailPage, and
kind 39089 using its own FollowPackDetailContent.

Merge the three into a single PeopleListContent (feed card) and
PeopleListDetailContent (full detail). The detail component hosts every
feature from the predecessors: Follow All with existing p-tag preservation,
Save-as-copy for non-owners, owner-mode member removal for kind 30000, the
Feed/Members/Comments tabs, sidebar integration, and the share/copy-link
menu. For kind 3 the event has no title of its own, so we fall back to the
author's display name.

Additional refinements bundled in:

- Register kinds 3 and 30000 at every previously-missing rendering point:
  KIND_HEADER_MAP, shellTitleForKind, CommentContext KIND_LABELS/ICONS,
  extraKinds specific labels and icons, and ExternalContentHeader fallbacks.
  Kind 3 and 30000 now share the packs feed toggle via extraFeedKinds.
- Add infinite scroll to the people-list Feed tab via useTabFeed +
  IntersectionObserver sentinel, replacing the useStreamPosts 40-post cap.
- Add Comments tab alongside Feed and Members, powered by useComments
  (NIP-22 kind 1111). Drop the redundant variant badge. Allow kind 39089
  packs to be pinned to the sidebar.
- Trim redundant chrome: drop the member-count pill in the feed card,
  drop the member-count line in the detail header, and stop pulling the
  author's 'about' and 'banner' into kind 3 follow list views.
- Add a dedicated FollowListCommentContext branch so comments on kind 3
  show '@Name's follow list' instead of 'a follow list'.
- Replace the three-dots DropdownMenu on the detail view with the shared
  PostActionBar (reply/repost/react/zap/share/more), matching other
  detail views.
- Promote EmbeddedPost from ReplyComposeModal into a shared component
  that dispatches to EmbeddedNote / EmbeddedNaddr, with a new
  EmbeddedPeopleListCard for kinds 3/30000/39089 so quote posts, reply
  indicators, hover cards, and the More menu all render follow lists
  correctly.
- Link the 'N following' count on profile pages to a naddr of the kind 3
  event (routing to the new detail view) instead of a bespoke modal.
  Delete FollowingListModal. Using naddr rather than nevent ensures the
  link always resolves to the latest replaceable event.

Delete ListDetailPage, FollowPackDetailContent, and FollowingListModal
entirely. All three kinds now route through AddrPostDetailPage →
PeopleListDetailContent.
2026-04-17 18:52:00 -05:00
Mary Kate Fain 61d3c261fe Default tracks and playlists tabs to 'new' sort 2026-04-17 18:15:04 -05:00
Mary Kate Fain ae32b62552 Show 3 playlist columns on desktop instead of 2 2026-04-17 18:03:46 -05:00
Mary Kate Fain 781aa2579b Fix empty results for hot/top sort on music tracks and playlists
When the Ditto relay lacks engagement data for music event kinds
(36787, 34139), sort:hot and sort:top queries return nothing. Add a
chronological fallback in useMusicFeed and the Discover tab's inline
New Tracks query: if the sorted query returns zero events, retry
against the default relay pool without the search param so users
always see content.
2026-04-17 17:55:58 -05:00
Mary Kate Fain 6999da3e45 feat: add Hot/Top/New sort and Global/Following scope filters to music pages
Add sort and scope filter controls to all three music pages:

Discover tab:
- New Tracks section gets sort (Hot/Top/New) and scope (Global/Following)
  controls. Global scope queries curated artists; Following scope queries
  the user's follow list. Hot/Top use Ditto relay NIP-50 search extensions.

Tracks tab:
- Replace useFeed('global') with useMusicFeed hook that supports sort and
  scope. Infinite scroll pagination preserved. Genre chips still work as
  client-side filter on top of the sorted results.

Playlists tab:
- Replace one-shot useMusicPlaylists with useMusicFeed for infinite scroll
  with sort and scope. Album/playlist type toggle preserved.

New shared components:
- MusicSortFilterBar: Pill-style sort (Hot/Top/New with icons) and scope
  (Global/Following) controls. Following only shown when logged in.
- useMusicFeed: Infinite scroll hook that maps sort modes to Ditto relay
  NIP-50 search extensions and restricts authors for Following scope.
2026-04-17 17:31:35 -05:00
Mary Kate Fain 2c5528774f revert: remove New Tracks artist deduplication
Not enough curated artists yet to justify one-per-artist filtering,
which leaves the section too sparse. Restore chronological ordering
so the feed fills up. The genre chips below the header change is kept.
2026-04-17 17:21:21 -05:00
Mary Kate Fain 78db2568e0 fix: move genre chips below New Tracks header and deduplicate by artist
Genre filter chips now appear below the 'New Tracks' section title
instead of above it, making it clear they filter that section.

New Tracks section now shows at most one track per artist (most
recent from each), preventing a prolific artist from dominating
the entire section. This applies to both the 'All' default state
and genre-filtered states.
2026-04-17 17:16:50 -05:00
Mary Kate Fain 64db8b2ce0 feat: collapse long playlist descriptions with show more/less toggle
Playlist detail page now clamps the description to 3 lines with a
'Show more' button that expands to show the full text. Uses CSS
line-clamp with a scrollHeight check to only show the toggle when
the text actually overflows.
2026-04-17 16:53:55 -05:00
Alex Gleason 3b052d3eb6 Fix naddr lookup for legacy replaceable kinds (0, 3, etc.)
useAddrEvent only treated kinds in 10000-19999 as replaceable, so any
naddr with a kind outside that range got a '#d' filter applied. For
legacy replaceable kinds like 0 and 3, real events don't carry a 'd'
tag, so the query matched nothing even when the relay had the event.

Invert the check to only apply the '#d' filter for true addressable
events (30000-39999). Legacy replaceable kinds and 10000-19999 are
now queried by kind+author alone.

Regression-of: 9b5df28b
2026-04-17 16:47:09 -05:00
Mary Kate Fain ecb61d44a5 feat: fall back to first track's artwork for playlist covers
When a playlist has no artwork or its image fails to load, resolve
the first track from the playlist's a-tag refs and use its artwork
as a fallback cover image.

- Add usePlaylistCoverArt hook: lightweight single-track query that
  only fires when the playlist's own artwork is missing. Returns the
  playlist art if present, or the first track's artwork otherwise.
- MusicPlaylistCard: uses the hook for cover art with per-URL error
  tracking so a broken playlist image triggers the fallback without
  breaking a working track image.
- PlaylistDetail: derives cover art from already-resolved trackEvents
  (no extra query needed since tracks are already loaded).
2026-04-17 16:44:08 -05:00
Mary Kate Fain 5789b34b5a fix: add onError fallback for broken images in music components
When an image URL from a Nostr event returns an error (404, stale
URL, server down), the img element now falls back to the existing
gradient/icon placeholder instead of showing a broken image.

Applied to MusicPlaylistCard, MusicTrackCard, MusicHeroCard,
MusicTrackRow, and MusicDetailContent (track hero, playlist hero,
and PlaylistTrackRow). Each uses a local imgError state that flips
on the img onError event to swap in the fallback.
2026-04-17 16:39:21 -05:00
Alex Gleason 5810c86e07 Reword Blobbi state action header to 'cared for their Blobbi'
The previous 'updated their Blobbi' wording felt mechanical for what is
really a care interaction (feeding, cleaning, playing, etc.). 'Cared for'
better reflects the user's intent.
2026-04-17 16:30:05 -05:00
Mary Kate Fain 3312621f1d fix: remove loading=lazy from horizontal scroll cards
Images inside overflow-x-auto scroll containers don't reliably
trigger the browser's lazy load IntersectionObserver, causing
playlist and track card artwork to not load in horizontal scroll
sections. Remove loading=lazy from MusicPlaylistCard and
MusicTrackCard since they render a small fixed number of cards
that should load immediately. MusicTrackRow (vertical list)
keeps lazy loading since it works correctly there.
2026-04-17 16:26:57 -05:00
Mary Kate Fain ccf1e0f137 feat: reorder Discover sections and sort playlists by hot
Move Artists and Playlists above Genre chips + New Tracks on the
Discover page. Playlists now query the Ditto relay with sort:hot
so the most engaging playlists surface first.

New section order: Hero > Featured > Artists > Playlists > Genre
chips > New Tracks > CTA.
2026-04-17 16:23:40 -05:00
Mary Kate Fain 2ba987f532 fix: backfill featured tracks with recent when hot results are sparse
When sort:hot distinct:author returns fewer than 5 tracks (common
with limited engagement data), issue a second query for recent tracks
with distinct:author and merge them in, skipping authors already
present. Ensures the Featured section always has enough variety.
2026-04-17 16:20:12 -05:00
Mary Kate Fain f677c131c0 feat: curator-gated Discover page with hot featured tracks
Rework the Music Discover tab so every section is gated through the
curator's lists, ensuring only high-quality content appears.

- Add useFeaturedMusicTracks hook: queries Ditto relay with sort:hot
  and distinct:author for curated artists. The #1 hot track becomes
  the hero; the rest populate the Featured horizontal scroll with no
  artist repeats.
- Add useMusicCuratorFollows hook: fetches Heather's kind 3 follow
  list to filter playlists to people she follows.
- Add authors param to useMusicPlaylists and useMusicTracksByGenre
  so both can be restricted to specific pubkeys.
- Rewire MusicDiscoverTab: New Tracks and genre filtering now use
  curated artists only; playlists use curator follows; section renamed
  from 'Recently Added' to 'New Tracks'.
2026-04-17 16:16:59 -05:00
Mary Kate Fain bf59fd6dc2 fix: point curated music artists to Listr-maintained list
Update useCuratedMusicArtists to fetch the externally maintained
kind 30000 follow set from npub1nl8r463... instead of looking for
a d:music-artists list from the app curator pubkey. The list is
maintained on Listr and contains 56 curated music artist pubkeys.

Fallback pubkeys updated to a subset of the same list.
2026-04-17 16:00:04 -05:00
Mary Kate Fain 72a25d09aa feat: show tracks on playlist detail page with full playback and album support
- Add usePlaylistTracks hook to resolve a-tag refs into ordered track events
- Render track list in PlaylistDetail with Play All button and per-track
  playlist playback via playPlaylist()
- Support albums as playlists tagged with t:album, showing release date,
  label, and Disc3 icon
- Add All/Playlists/Albums filter toggle to MusicPlaylistsTab
- Show Album badge on playlist cards in grid views
- Add KIND_HEADER_MAP entries for music tracks (36787) and playlists (34139)
- Fix shellTitleForKind to return 'Playlist Details' for kind 34139
- Document music kinds and album convention in NIP.md
2026-04-17 15:25:06 -05:00
Mary Kate Fain bb2bd15a71 Merge remote-tracking branch 'origin/main' into ditto-music-feed 2026-04-17 15:04:44 -05:00
Mary Kate Fain baf77c95aa Fix missing background on expanded emoji picker in feeds
The full emoji picker in QuickReactMenu had no background because
EmojiPicker sets its shadow DOM background to transparent, and the
wrapper div only had rounded-xl/shadow-xl without a background class.
Added bg-popover and border-border so the picker matches the quick-react
pill bar styling.

Closes #235
2026-04-17 14:55:57 -05:00
shakespeare.diy de8a39f78a feat: add ZapTrax and Sunami to music external app links
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-04-17 14:26:20 -05:00
shakespeare.diy 4f6f6beff3 feat: redesign Music page with curated discovery experience
Replace the generic KindFeedPage-based music feed with a dedicated
music discovery page featuring:

- **Discover tab** (default): Hero card with featured track, horizontal
  scroll of featured tracks from curated artists, genre chip filters,
  recently added track rows, playlists section, artist showcase, and
  "Share Your Music on Nostr" CTA card
- **Tracks tab**: Infinite-scroll list of all music tracks with genre
  filtering, using useFeed for standard pagination
- **Playlists tab**: 2-column grid of kind 34139 playlist cards
- **Artists tab**: 3-column grid of artist profile cards, curated
  artists shown first

Architecture decisions:
- Shared discovery components in src/components/discovery/ (SectionHeader,
  TagChips, HorizontalScroll, ProfileCard, ContentCTACard) designed for
  reuse by podcasts and other future content-type discovery pages
- Music-specific components in src/components/music/ (MusicHeroCard,
  MusicTrackRow, MusicTrackCard, MusicPlaylistCard)
- Single base query (useMusicData) fetches kind 36787 tracks and derives
  genres + artists client-side to avoid redundant relay requests
- Curated artist list via kind 30000 follow set (d:music-artists) from
  curator pubkey, with hardcoded fallback of 9 verified Wavlake artists
- Global by default (no follows/global tab split for discovery)
- Full now-playing state indicators across all components via useAudioPlayer
- Complete loading skeletons and empty states for every section

New files:
- src/components/discovery/{SectionHeader,TagChips,HorizontalScroll,
  ProfileCard,ContentCTACard}.tsx
- src/components/music/{MusicHeroCard,MusicTrackRow,MusicTrackCard,
  MusicPlaylistCard,MusicDiscoverTab,MusicTracksTab,MusicPlaylistsTab,
  MusicArtistsTab}.tsx
- src/hooks/{useCuratedMusicArtists,useMusicData,useMusicPlaylists}.ts
- src/pages/MusicPage.tsx

Modified: src/AppRouter.tsx (route update)
Orphaned: src/pages/MusicFeedPage.tsx (no longer imported)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-04-17 14:26:13 -05:00
Alex Gleason d224035d28 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-17 13:30:22 -05:00
Alex Gleason e914109b4b Show full interactions on reaction/repost/zap/poll-vote detail views
These activity-style detail pages previously rendered only a slim action
row, missing the stats summary (Reposts / Quotes / Likes / Zaps), the
client + full-date row, and the InteractionsModal affordance. Users had
no way to see or browse the interactions the event itself had received.

Extract the stats + date row into a shared JSX block and replace the
four inline action-button grids with PostActionBar, bringing these
detail views in line with the standard post layout while keeping their
compact emoji/icon headers.
2026-04-17 13:29:27 -05:00
Chad Curtis 1e694a6cf8 Remove blobbi post requirement from evolution hatch missions
Add migration logic so users with stale persisted evolution missions
(e.g. containing the removed create_post mission) get their mission
list rebuilt to match current definitions while preserving progress.
2026-04-17 12:30:57 -05:00
Alex Gleason 8e6bd29be0 Include kind 8 badge awards in home, profile, and Badges feeds
Declares kind 8 as a third sub-kind under the existing Badges
ExtraKindDef with its own 'showBadgeAwards' / 'feedIncludeBadgeAwards'
toggles. The home feed and profile feed both derive their kinds list
from getEnabledFeedKinds, so both pick up badge awards automatically.
The Badges page's follows feed is a hardcoded list, so kind 8 is added
there explicitly.

Defaults match existing badge settings: enabled in hardcodedConfig (new
users see them), conservative in InitialSyncGate and TestApp. The
ContentSettings UI auto-generates a new Badge Awards toggle row.

Removes the now-redundant KIND_SPECIFIC_LABELS/ICONS entries for kind 8
since the sub-kind carries that metadata.
2026-04-17 11:39:54 -05:00
Alex Gleason ab1f95f2df Render NIP-58 badge award events (kind 8) in feeds
Badge awards previously only appeared as notifications when you were the
recipient. Now they render as full feed cards — showcase image, badge
metadata, recipient row, and an Accept button for logged-in recipients —
so issuers can share awards and feeds can surface community recognition.

Extracts parseBadgeATag, unslugify, and AcceptBadgeButton out of
NotificationsPage.tsx into shared modules, adds a compact embedded card
for kind 8 nevent references, and wires the kind through NoteCard,
PostDetailPage, CommentContext, and extraKinds registries.
2026-04-17 11:31:57 -05:00
Alex Gleason fe11513a6f Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-17 11:06:33 -05:00
Alex Gleason 52e42fcd6e Replace hardcoded 'Ditto' with appConfig.appName and appConfig.appId
User-facing display strings now read from config.appName so forks can
rebrand without code changes, and localStorage keys are namespaced by
config.appId so forks running on the same origin don't clobber each
other's preferences. Module-level cache-key constants that previously
hardcoded 'ditto:' have been refactored into hook-scoped reads from
config.appId (via a new getStorageKey() helper). The helpContent FAQ
template now uses {appName} placeholders substituted at read-time
through getFAQCategories(appName)/getFAQItem(appName, id).
2026-04-17 11:01:04 -05:00
sam 945ae3b126 ported country-scoped feed model from Pathos 2026-04-17 17:22:59 +05:45
sam 2ee979afc0 spark wallet+ 2026-04-17 16:22:33 +05:45
sam ba996d9878 drop wikipedia 2026-04-17 14:39:18 +05:45
sam e0e2300521 reconsidered sidebar items 2026-04-17 14:17:07 +05:45
sam a56860a6ce logo/copy changes 2026-04-17 12:15:51 +05:45
sam 9550094ffb wip mega dump/migration from ditto 2026-04-17 12:10:11 +05:45
Chad Curtis 3aa08ba93e Overhaul compose box UX: inline picker, draft autosave, prevent accidental dismiss
- Move emoji/GIF/sticker picker from popover to inline panel in all contexts
- Add pill-style tab highlights for inline picker (emoji, GIF, stickers)
- Auto-save compose drafts to localStorage with debounce, keyed by context
- Prevent accidental modal dismissal (backdrop click blocked)
- Theme emoji picker to match Ditto design system (search, nav, scrollbar)
- Fix emoji picker width (dynamicWidth + shadow DOM overrides)
- Create StickerPicker component with search bar
- Restructure modal layout: scrollable content, sticky toolbar, fixed picker
- Mobile keyboard handling (blur/refocus textarea around picker)
- Move Tenor attribution into GIF search bar as inline hint
- Bump picker height to 280px
- Auto-focus textarea on modal open (iOS keyboard fix)
- Remove rounded corners from feed compose box
2026-04-17 00:01:38 -05:00
Alex Gleason 9837c23a96 Always add NIP-89 client tag, including on localhost
The HTTPS check was a leftover from when the client name was derived
from the hostname. Now that it comes from appConfig, the tag should
be added unconditionally.
2026-04-16 23:48:07 -05:00
Alex Gleason 99fefdda67 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-16 17:17:47 -05:00
Alex Gleason dabe3c1687 Fix avatar shape not saving during signup
The signup and onboarding profile steps rendered ProfileCard without
passing onAvatarShape, so emoji shape selections were silent no-ops and
never made it into the published kind 0 event.
2026-04-16 17:06:27 -05:00
Chad Curtis c3f0e9d3fa Merge branch 'main' of gitlab.com:soapbox-pub/ditto into ai-chat-429
# Conflicts:
#	package-lock.json
2026-04-16 16:59:54 -05:00
Chad Curtis 377b536456 Merge remote-tracking branch 'origin/main' into feat/evolution-missions-to-kind-11125
# Conflicts:
#	package-lock.json
2026-04-16 16:35:38 -05:00
Chad Curtis bf0fde9d06 Fix interaction tally not incrementing: ensure evolution missions exist in session store
The interactions tally mission was silently dropped because
trackEvolutionTally maps over the evolution[] array — if it's empty,
nothing gets incremented. This happened when evolution missions
weren't persisted to kind 11125 or weren't hydrated on page load.

Both useHatchTasks and useEvolveTasks now have a safety-net effect:
if the companion is in an active task process (incubating/evolving)
but evolution[] is empty, they re-populate from the static mission
definitions. This ensures tally tracking works immediately regardless
of hydration timing.
2026-04-16 16:33:45 -05:00
Alex Gleason fb5278b891 Add nsec backup to Profile settings
Lets users with a local-nsec login reveal, copy, and back up their secret
key from /settings/profile. Uses saveNsec() so iOS gets iCloud Keychain,
Android gets Credential Manager with a file fallback, and web gets a
.nsec.txt download plus an opportunistic PasswordCredential save.

Renders an explanatory message for NIP-07 extension and NIP-46 bunker
logins, where the key is not accessible from the app.
2026-04-16 16:24:07 -05:00
Chad Curtis a27ee3af86 Fix self-review findings: remove dead code, fix task progress display, fix hydration race
- Remove dead code: useSyncTaskCompletions, incrementInteractionTaskTags,
  getInteractionCount, getEvolveInteractionCount, unused lookup maps
- Fix task progress showing 0/N on load: compute event-based task counts
  directly from Nostr query results (authoritative) instead of relying
  solely on the evolution mission store which may not be hydrated yet.
  Use max(queryCount, missionCount) so progress displays immediately.
- Fix hydration race: useDailyMissions raw memo now waits for hydration
  before creating fresh missions, preventing overwrite of persisted
  evolution[] with empty array. Also preserve evolution missions across
  daily resets during hydration.
- Fix session store miss: use ensureSessionStore in incubation/evolution
  start so evolution missions are always populated even if the store
  hasn't been hydrated yet.
- Extract duplicate findMission to shared findEvolutionMission in
  evolution-missions.ts
- Document evolution[] field on kind 11125 in NIP.md
2026-04-16 16:17:57 -05:00
Alex Gleason 2dfb880566 Improve signup save-key step
Addresses confusion on the key-save step during signup:

- Rename the primary button from 'Continue' to 'Save Key' with a
  Download icon, so the label matches the action it performs.
- Change saveNsec() to return 'saved' | 'saved-to-file' | 'dismissed'
  instead of throwing on native dismissal. Dismissing the iCloud
  Keychain prompt is a legitimate user choice so the handler now
  proceeds silently rather than blocking with a 'Save failed' toast.
- Add an in-flight guard on the Save Key button with a spinner and
  'Saving…' label. The finally block guarantees the disabled state is
  cleared, so users can never get stuck on an unresponsive button —
  fixing the 'button became disabled after I dismissed the prompt'
  complaint by construction.
- On de-Googled Android builds (GrapheneOS, /e/OS, etc.) the AndroidX
  Credential Manager has no provider to delegate to, so the keychain
  save fails immediately. Fall back to writing the key to the app's
  Documents directory so the user always has a persistent backup, and
  surface a toast telling them where the file is.
- iOS keeps its original behaviour: dismissing the iCloud Keychain
  sheet is a deliberate user choice, no automatic fallback. The
  Documents folder on iOS is accessible via the Files app without
  authentication, so silently dropping a plaintext nsec there would
  violate user intent.
- Use the app name (from config.appName) as the filename slug for any
  .nsec.txt file written to disk. On Capacitor location.hostname is
  always 'localhost', so passing the app name is the only way to get
  a meaningful filename. Drop the redundant 'nostr-' prefix since the
  '.nsec.txt' extension already identifies the file.
- Rewrite the description and title on the save step: 'Your secret
  key' + a single paragraph explaining what the key is and why it
  matters.
- When the user reveals the key via the eye toggle, show an amber
  callout with sharing/screenshotting warnings and a 'Learn more' link
  to the Managing Nostr keys blog post. The warning appears at the
  moment risk is highest.
- Auto-select the full nsec on focus/click so users copying into a
  password manager don't have to fight mobile selection handles.
- Use openUrl() for the external 'Learn more' link so it works
  correctly inside Capacitor's WKWebView.
- Singularise the keygen step copy ('cryptographic key' / 'Generate
  my key') to stay consistent with the save step which presents a
  single secret key.
2026-04-16 15:50:59 -05:00
Chad Curtis 13d4f667b6 Restore AI chat widget and extract shared credits hook
- Restore full interactive chat widget with ScrollArea, streaming messages,
  input area, and conversation cache that was regressed in ec9b6c43
- Extract useShakespeareCredits hook so credits gating is DRY between the
  widget and the full AI chat page
- Show Dork ASCII mascot consistently across all empty/logged-out states
  instead of the generic Bot icon
2026-04-16 15:47:33 -05:00
Chad Curtis d73460a617 Fix self-review findings: invalid HTML nesting, credits error handling, swallowed 400 errors 2026-04-16 15:36:09 -05:00
Chad Curtis ec9b6c43be Overhaul AI chat: handle 429 rate limiting, require Shakespeare credits
- Add RateLimitError class with Retry-After header parsing
- Distinguish insufficient_quota 429 from rate-limit 429
- Friendly Dork-themed error banners for rate limiting and out-of-credits
- Clean no-credits empty state with directive CTA and Get Credits button
- Hide model selector, trash, and input when user has no credits
- Hide page title on mobile, align model selector right
- Simplify sidebar widget to Shakespeare CTA
2026-04-16 15:28:09 -05:00
Alex Gleason 0d3b8ed23d Harden CSS/URL handling, NWC storage, and Android backup
- Sanitize event-sourced URLs before CSS url() interpolation in
  ProfileCard banner and letter stationery background (closes H-1, H-2)
- Sanitize event-sourced font families at the parse layer and in letter
  card/detail consumers that bypass resolveStationery (closes M-6)
- Export sanitizeCssString for broader reuse
- Route NWC wallet connection URIs and active pointer through a new
  useSecureLocalStorage hook, storing in iOS Keychain / Android KeyStore
  on native (closes M-1)
- Add removeItem to secureStorage
- Add Android backup/data-extraction rules that exclude WebView storage
  and Capacitor secure-storage SharedPreferences so wallet credentials
  don't leak via Google Auto Backup (closes M-5)
- Document that GOOGLE_PLAY_SERVICE_ACCOUNT_JSON must be base64-encoded
  to match what the CI job expects (closes M-2)
2026-04-16 14:20:26 -05:00
Alex Gleason a61925b821 Merge branch 'main' of gitlab.com:soapbox-pub/ditto 2026-04-16 13:50:42 -05:00