Everything Moves: The Richness Sprint
A funny thing happens when you're shipping features at pace and the beta testers are hunting inconsistencies. Suddenly you're not just fixing bugs, you're discovering that you've accidentally built something worth defending. This sprint felt like that turning point.
Let's start with the visible stuff, because there's a lot of it.
Your status updates are now genuinely social. Posts can hold up to four photos instead of just one, displayed as a 2x2 grid with full lightbox navigation (arrow keys, click buttons, all of it). You upload once, all photos go directly into your gallery automatically if you ask them to, no round-tripping to add them individually. We learned the hard way that when you soft-delete a conversation and then message that person again on the Initiate flow, the conversation needs to un-hide for BOTH of you, not just one. That's fixed now. A quirk from the old testing sessions left some phantom conversations floating in the database; we cleaned those up too. Feels like starting fresh.
Friends online just got real. The Presence Drawer now actually tells you who's online without requiring a page reload. We had three layered bugs in the realtime polling (React StrictMode was silently ghosting remounts, the channel-not-ready race was returning false negatives, and 3-second polling was absurdly aggressive). All three are fixed. Keyboard arrows work on the notification lightbox, click buttons do too, we'd accidentally buried them under a pointer-events hierarchy that made them un-clickable, and when Lilith and Ace caught it, we realized the same issue existed in three different lightbox components. They're now standardized to match the gallery lightbox, which is the one that was already working perfectly.
Conversations now let you hide them from your list instead of deleting them forever. It's a soft-delete on your end only (the other person keeps their copy), and if you message that person again, the conversation resurfaces with full history intact. The implementation is surgical: a `deleted_at` column on `dm_participants`, filtered server-side on every read, and cleared on new messages. This was a harder problem than it looked because the Initiate route (first message to a new person) had its own message-sending path that wasn't mirroring the logic from the regular send route. Ace's insight about needing per-participant soft-delete instead of whole-conversation hard-delete turned out to be exactly right, harassment reports need the history to exist somewhere for moderation, but users need control over their own list.
YouTube Shorts are now supported. We realized we were only extracting `youtube.com/watch?v=`, `youtu.be/`, and the embed formats, but missing `/shorts/`. One regex fix and a comment update later, you can paste a Shorts link into a status update or video highlight and it embeds like any other YouTube video. Ace had apparently forgotten this existed; someone asked and we delivered in about 10 seconds flat.
There were also eight profile themes that came to life. Dreamy (amber, default), Nocturne (rose-red), Neon (cyan glow), Frost (glassy white), Nightshade (synthwave purple), Sunset (warm orange), Midnight Velvet (slate-silver), and Matcha Tea (sage green). Each one adapts to light and dark mode automatically, so your visitor's preference doesn't break your aesthetic. You pick it from a palette icon next to your name, it changes in real time, and every card on your profile, from your info card to your journal posts to your gallery, respects the theme. The whole system is a neat example of how CSS custom properties plus a single `data-profile-theme` wrapper can cascade beauty across a whole lot of surface area.
Then we had to rebuild the notification routing. When you tapped "so-and-so replied to your comment," the URL would change but the page wouldn't scroll to that exact post. It looked broken. Turns out the `FeedPostHighlighter` had a 150ms timeout that fired before the feed re-rendered, so the element it was looking for didn't exist yet. We gave it a retry loop now (tries every 100ms for up to 1.5 seconds) so it always finds the post, scrolls to it, and glows it. Same fix logic we'd used for the presence polling.
Finally, the kind of thing that nobody sees but makes the whole ship run cleaner: we migrated multi-photo posts to `media_urls jsonb` with backwards compatibility for existing single-photo `media_url` rows, standardized lightbox navigation across three different components, cleared up the soft-delete messaging system, fixed the presence polling race conditions, and added comprehensive action logging to track whether posts get added to the gallery. Zenna caught the lightbox arrow inconsistency (one post had them outside, one had them inside, the gallery had them distant). Instead of shipping inconsistency, we aligned everything to what was already working.
There's a pattern here. The bugs we're finding now are subtle. Pointer-events hierarchies, stale closures in effect dependencies, remount guards that survive the first mount but get nuked on the second. The code is clean enough now that the problems aren't "how does this feature exist" but "does this feature behave the same everywhere." We're not adding features anymore, we're defending them. That feels like graduation day.
Morley Ashland
Words & Research


