Rowstr Changelog
Latest updates and improvements from Rowstr.
0.9.1
Chat rework — grouped messages, threads, pins, performance
ChatThreadsVoiceMediaPerformance
One message, multiple attachments
- Single-message multi-attach: photos, videos, voice notes and text sent together now go out as one message instead of one-per-piece. Order is always media → voice → text inside the bubble.
- Attachment queue in the composer: pick several images at once with the paperclip, stack voice notes, write a caption, then hit Send. Each tile has a hover-only X in the top-right that disappears during upload.
- Drag-to-reorder the queue — no grip handle, just drag the tile; a primary-colored vertical bar shows the drop target.
- Images lightbox: click any image in the thread (chat or thread panel) to open it full-screen, object-contained, with
cursor-zoom-inaffordance. - Image grid layout inside a bubble: 1 → full, 2 → 2-col, 3+ → 3-col
aspect-squaretiles. - Voice-note grid inside a bubble: multiple voice bubbles wrap side-by-side up to ~3 per row.
Voice recording — Slack-style
- Popover recording UI above the mic button: live timer, animated waveform, and an X to cancel.
- Mic button becomes the validate button while recording — the icon swaps from 🎤 to ✓, click to confirm, click ✕ (or press
Escape) to discard. - Enter confirms from the keyboard;
Escapecancels. - Preview before sending: recorded voices land in the attachment queue as a full
VoiceMessageBubble— play / pause, scrub the waveform, remove from the queue, stack another. - "Microphone blocked" popover is now dismissable: click the mic again (or outside) to close, state resets cleanly.
- MIME handling:
audio/webm;codecs=opusnow correctly passes server validation — the codec suffix is stripped before the allowed-types check.
Rich link cards (IG / TikTok reels)
- Preferred data source is
SavedReel: when a reel link is posted in chat and the URL matches a reel already in one of the user's orgs, the preview card uses its stored metadata (handle, caption, likes, comments,videoUrl) instead of scraping the public HTML. More reliable, and the video actually plays. - Inline playback: hover to prefetch + autoplay muted after 400ms; click to zoom in a small dialog with native controls.
- Meta layout overhaul:
@handle+ posted date on one row, ❤ likes / 💬 comments on the next, caption underneath. No more "ugly pink" Instagram pill — just a neutral dark badge. - og:description parser for Instagram: splits the
"13K likes, 76 comments - keo on October 26, 2025: "Nutted""format into structured fields server-side. - Save + Recreate CTAs removed from the card for now.
Threads vs replies — properly separated
- A new
isThreadReplyflag on every message distinguishes:- Reply (from the main composer): appears in the main conversation with the quoted parent above it, does not open or belong to a thread.
- Reply in thread (from the thread panel): lives only inside the thread, never shown in the main conversation.
getMessagesfilters out thread replies,getThreadonly returns thread replies.- Thread reply counter on the root message (
N replieswith an icon chip) — filtered to thread replies only. Click to open theThreadPanel.
Pinned messages
- Pin / Unpin any message via a new Pin action next to reply / thread / reactions. Works on your own messages and on others'. Optimistic update — the pin flips instantly with a rollback on error.
- "Pinned by you" / "Pinned by [sender name]" indicator above the bubble with a height-animated reveal.
- Pinned banner at the top of the conversation: collapsed row showing
{count} pinned+ preview of the most recent pin; Show all expands to a list with jump-to + hover Unpin. - Jump-to-message flash: clicking a pinned item or a reply quote scrolls the target into view and runs a subtle
bg-primary/10pulse (@keyframes chat-flash, 1.2s) — no more hard ring.
Message actions — polish
- One set of actions per burst: when you send a photo + voice + text together, only the last message in the burst shows the action row — not three redundant copies.
- Hover-only with pointer-events handling: actions fade in on message hover and fade fully out after, no sticky hover-state bug.
- Tooltips on every action (Reply, Reply in thread, Pin / Unpin, Delete) via a local
ActionTooltipwrapping each button in its ownTooltipProvider— no more "Tooltip must be used within TooltipProvider" runtime error. - Destructive hover (Delete) properly overrides the icon variant's primary hover — red wash on hover instead of green.
- Unified icon-button sizing:
Buttonnow has a propericonvariant (!h-6 !w-6 !p-0, rounded, muted → primary hover). All chat icon buttons — search toggle, emoji picker, attach, reel picker, mic, send, message actions — share it. Fixes a long-standing bug where default size classes were adding padding that broke hit-boxes and made hover states leak.
Scroll + loading
- Pinned-to-bottom on load: a
ResizeObserveron the messages container keeps the thread pinned to the bottom while images/attachments finish laying out, instead of leaving a half-screen gap on refresh. - Jump-to-latest floating button (bottom-right chevron) appears the moment you scroll up — click to smooth-scroll back to the end. Fades in/out via framer-motion.
- Paginated 20 at a time (was 30): the initial query is lighter and the IntersectionObserver on the top sentinel fetches older pages as you scroll up.
- Dropped eager reply-preview:
getMessagesused to embed up to 3 sender previews per message for the old "avatar stack" on thread pills. That's ~3×N extra joins per page — gone. TheThreadInlineSummarynow shows a single icon +N repliestext; the full thread loads lazily only when you click.
Message-level tweaks
- Burst grouping: same-sender messages within a 5s window stack tighter (
mb-pxbetween them) — gives a visual cluster without a DB change. - Fix: when a message has a reply quote, the previous message now correctly closes its group with a
mb-3gap so the reply card doesn't feel cramped. - Thread panel: input and header heights now match the main conversation (
h-14, icon-only Send button). - Voice message preview in the queue uses the real
VoiceMessageBubblecomponent so you can actually play it before sending — not a placeholder. - Unused descriptive comments removed from recent edits per project convention.
Schema
Two new migrations applied:
20260420103000_add_message_attachments_and_pin— addsMessage.attachments Json?,Message.pinnedAt DateTime?,Message.pinnedById String? → User, with an index on(conversationId, pinnedAt).20260420120000_add_is_thread_reply— addsMessage.isThreadReply Boolean @default(false)to separate thread replies from inline replies.
API
- New routes:
chat.togglePinMessage,chat.listPinnedMessages. chat.sendMessageacceptsattachments: [{ mediaKey, mediaType, duration? }]andisThreadReply?: boolean.chat.getLinkPreviewreaches intoSavedReelbyshortcode/mediaPkbefore falling back to scraping, and its cache guards against pre-change shape./api/upload(category: voice) now normalizes MIME (audio/webm;codecs=opus→audio/webm) before the allowed-types check.
0.9
Chat v2, comments, notifications, and a single todo sheet
ChatTodoNotificationsActivitySearchDashboardBilling
Chat — reactions, voice, threads, search
- Emoji reactions on every message with a quick-pick popover (12 common emojis) and live SSE sync — pill toggles optimistically, aggregates by emoji with a count and a "you reacted" highlight. Own reactions read as a subtle tinted pill, sized to fit small messages. Spring animation on add / remove.
- Voice notes end-to-end: record from the composer (WebM/Opus with a Safari fallback to
audio/mp4), 5-minute cap, cancel or send. Playback bubble with a deterministic waveform, scrubbable timeline, and proper HTTP Range streaming from R2 for instant seeking. - Slack-style thread mode: every message with replies gets an inline summary next to its timestamp — stacked avatars of the 3 most recent repliers, a clickable "N Replies" pill, and the time. Opening a thread swaps the right sidebar for the
ThreadPanel(shareable via the?thread=URL param). The main conversation's inline reply preview is preserved. - In-conversation search (
⌘F): overlay search with debounced results, keyboard navigation (↑ / ↓ / Enter / Esc), and flash-highlight scroll when clicking a result. Doesn't hijack global⌘Foutside an active conversation. - Rich link cards for Instagram and TikTok URLs in messages — detected automatically and rendered as a preview card with thumbnail, author, caption snippet, and two CTAs: Save to feed (writes to
SavedReelwhen Instagram) and Create recreate todo (creates a todo withtype: "recreate"pre-filled). - Search button in the conversation header for discoverability, in addition to the keyboard shortcut.
Chat — composer
- Media attachments (images + videos): attach via a paperclip, preview above the input (thumbnail + filename + size + remove), write a caption, then send. The file is only uploaded to R2 when you hit Send — removing the attachment is a zero-cost operation.
- Emoji picker in the composer: a 96-emoji grid next to the mic / reel / attachment buttons. Works in the main input and in the thread reply composer.
- Reel picker — the composer's "film" button opens the same
FeedPickerlibrary used elsewhere. Multi-select reels and insert their URLs into the message; the rich link card renders them inline. - Voice mic permission UX: the button is always clickable. Denied or non-HTTPS origins show a help popover with instructions ("click the lock icon → allow microphone → reload") and a Try again action. An insecure origin now reports HTTPS required instead of silently failing.
- Thread reply input matches the main composer — same height,
@mentions with a popover, emoji picker, Enter to send (Shift+Enter for newline). - Unified action button styling: hover-only tint (transparent green wash on hover, red for destructive), consistent across reply / thread-open / trash / emoji picker — replaces the mixed green-on-green pill look.
Chat — model context sidebar
- Upcoming shoots, Recent media, and Saved reels sections on the right-hand sidebar (admin view on a model DM), backed by a single consolidated
chat.getModelContextprocedure — no waterfall. - Platform icons (TikTok, Instagram, OnlyFans) replace text labels on shoot cards.
- Broken-thumbnail fallback: remote IG / TikTok CDNs frequently 404 once a post is taken down. Images and videos now swap to an icon fallback on load error so the grid stays clean.
- Error + retry state when the context query fails, with a one-click retry.
- "View all" on Saved reels opens the saved-reel library directly (see below) instead of navigating away.
Chat — layout
- Bigger thread panel: the right column expands to
384pxwhen a thread is open (vs.288pxfor model / DM info), so longer replies don't feel cramped. - Unified header heights: main conversation header and thread-panel header both locked to
h-14for pixel-perfect alignment. - Mobile back buttons and responsive collapse rules (single-column focus on narrow screens, three-column on desktop).
Saved reels — library management
- "Manage my saved reels" button on the Feed page and on the chat sidebar's Saved reels section — both open the same
FeedPickerin manage mode. - Multi-select with a floating
SelectionDock(same pattern as Drive): pick reels, preview the first three as rotated thumbnails, see a live count, and Remove them in bulk with a single action. Per-tile delete is gone in favor of multi-select for consistency. - Sort: a new combobox next to the search input — Recently added, Oldest first, Most viewed, Most liked, By handle.
- Lists: create, rename (double-click or pencil), and delete reel lists from the sidebar. Inline inputs with
⏎ save/⏎ addhints inside the field. Deletion is confirmed through a realAlertDialog(no more nativeconfirm) and explains that reels become Unsorted rather than being deleted.
Todos — single sheet, everything inline
The edit sheet is gone. Viewing, editing, and creating a todo all happen in the same Notion-style sheet.
- Inline Notion-style fields: Title, Status, Dates, Platforms, Drive folder, and Brief are clickable directly in the header. Clicks open a small popover (status picker, date + time picker with full-day toggle, platform checkboxes, drive folder tree) — no more second sheet on top of the first.
- Read-only by default, Edit to unlock the content block: the footer shows Close and Edit. Edit switches the content section to the full editor (drag-to-reorder, duplicate, upload from device, add from a saved reel, status cycle, brief inline). Close exits edit mode — or closes the sheet when you're already in view mode.
- Blank-then-open create flow: "+ New todo" creates a minimal blank todo on the server and opens the sheet directly in edit mode, so you fill everything in place. "Todo from media" in Drive does the same, with the selected assets pre-attached as content items.
- Smoother view ↔ edit transition: no more close/reopen flash. The sheet stays mounted and its content swaps in place.
- No more edit sheet (
TodoSheet.tsx): all callers (calendar, kanban, table, drive) now go through the single view sheet.
Todos — content editor
- Card-list content editor with drag-and-drop reordering (
@dnd-kit) in both create and edit modes. - Unified thumbnail dropdown on every card — one entry point for Preview, Upload from device, Pick a saved reel. Empty original cards render as a dashed upload drop-zone so the expected action is obvious.
- "Add content item" dropdown gets three options: Blank item, Upload from device (with a spinner while the file uploads), From a saved reel.
- Duplicate a content item with a small Copy button next to the trash — clones the source item (type, source reel, brief) directly below it.
- Subtle entrance animation on new cards (fade + slide-in from top).
- Visible brief textarea on each card so it's clear where to type the brief for that specific content item.
- Optimistic cache updates on add / update / delete / reorder — no more waiting for the round-trip before the UI reflects the change.
Todos — comments and activity
- Comment threads on every todo with
@mentionautocomplete — scoped to the organization. Comments live in the view sheet as a dedicated tab. - Agency-only by default: models can't see comments unless the org opts in through a new setting. The UI gates the tab accordingly.
- Activity timeline as its own tab (agency-only) — renders status changes, content additions, uploads, and comment events on a vertical rail with platform icons where relevant.
- Airtable-style tabs at the top of the content section: Details, Comments, Activity.
Todos — view sheet polish
- Bigger brief textarea in the view (resizable,
min-h-[88px], borderless) and moved to the last position in the header rows so it gets space to breathe. - Progress row realigned with the other rows — icon + label + fixed-width bar + ratio, matching Deadline / Platforms / Brief layout instead of stretching full-width.
- Drive folder helper: a small
?next to the "Drive" label explains that model uploads on the todo automatically land in the linked folder. - Model popover scroll wheel fixed — the wheel now scrolls the list instead of the sheet behind it.
- Pinned footer: the footer stays at the bottom of the sheet regardless of content length; only the content section scrolls.
Notifications — new
- In-app notification center in the sidebar rail with a bell and an unread badge.
- Deadline alerts fire automatically when a todo's due date is approaching or overdue.
- Type-based tabs (All / Mentions / Deadlines / Members) with a fluid tab-switch animation and a properly-measured popover resize.
- Per-row actions — mark as read, delete — with tooltips and muted / red hover chips. No more "Clear all" (accidental-taps risk); bulk actions moved to a safer flow.
- Skeleton loading state while the list loads, matching the rest of the app.
Activity log
- Org-wide audit trail of who did what and when — surfaced on the dashboard as an activity feed card, and under each todo as the Activity tab.
Global search (⌘K)
- Command-menu search across models, todos, media, drive folders, and chats. Keyboard-first, scoped per organization.
Dashboard
- Role-specific home for models with upload shortcuts styled like drive folders and a format that mirrors the agency dashboard.
- Fixed model route: models can now access
/dashboardwithout being redirected away.
Billing
- Stripe integration for subscription and plan management (agency billing page).
Removed
- Tags on todos — shipped earlier in the cycle, then pulled. The feature has been fully removed from UI, router, and schema; run
pnpm prisma:migrateto drop thetag/todo_tagtables. - Todo templates and recurring todos — shipped as an experiment, not enough signal to keep; removed ahead of release.
- Demo notifications — the dev-only
seedMocksmutation and the "Seed demo" buttons in the notification bell are gone.
Under the hood
- New shared
todo/sheet/module:StatusPopover,DatesPopover,PlatformsPopover,DatePickerField,EditableContentCard,ContentCardList,AddMediaButton, plus shared constants. These are now the single source of truth for todo popovers and content cards across the app. - Stricter deps on a few
useCallbackhooks so the React Compiler can preserve memoization.
0.8
Snappier chat, drive, and todos
PerformanceChatDriveTodo
Chat
- Cleaner conversation switching: switching between threads now fully resets the composer, reply draft, and mention state — no more leftover drafts or stale typing indicators from the previous conversation.
- Correct thread height: the chat card now fills the available space properly on every screen size.
- Smoother typing: key presses in the message composer no longer trigger unnecessary re-renders across the thread.
Todos
- URL-driven todo sheet: opening a todo updates the URL so you can share a link, bookmark it, or reopen the same todo after a reload. Closing the sheet clears the param.
- Stable pagination: the todo table no longer jumps back to page 1 when filters rerun in the background.
- Feed picker: the "pick a reel" dialog now opens from a clean state every time and doesn't carry over search or selection from the previous open.
Drive
- Faster dialogs: the folder customization and upload dialogs mount only when you open them, so the Drive page itself loads slightly faster and each dialog starts fresh.
- Media details fix: opening a second media item no longer briefly shows the previous one's dimensions or duration before updating.
- Video reset: the video player in the media detail sheet cleanly resets when you switch files.
Blog
- Tag filter in the URL: picking a category is now reflected in the URL without a full reload, and back/forward navigation keeps your filter.
Agency & settings
- No more state flashes: settings, the agency general tab, and the members page no longer briefly show stale org data when navigating into them.
Sign-up
- Server-side redirect: signed-in users hitting
/sign-upare now redirected to the dashboard on the server — no more flash of the sign-up form.
Under the hood
- A large sweep removed a class of unnecessary re-renders across chat, drive, settings, todos, and agency pages so the app feels more responsive.
- The React Compiler can now optimize more components than before, which means fewer wasted renders even on pages we didn't touch directly.
0.7
Auth, invites, and the inbox
AuthEmail
Sign-in
- Magic-link sign-in: anyone without a Google account can now sign in by typing their email — we send them a one-click link, no password, no extra account setup. Google sign-in is still the primary path.
- Same account across methods: signing in via magic link with an email that's already tied to a Google account logs you into the same account — no duplicates.
Invitations
- Role assignments actually work: picking a role when inviting someone now sticks. Previously everyone ended up as "Member" regardless of what was selected.
- Revoked invitations stay visible: revoking an invitation no longer hides it — it moves to a "Revoked" section as a history log so you always know who was invited and when.
- Timestamps on every invitation: each row now shows when it was sent and, for revoked ones, when it was revoked.
- Revoked links can't be claimed: clicking a cancelled or expired invitation now shows a clear "Invitation unavailable" screen instead of silently letting the recipient join.
Emails
- Branded email templates: invitations, welcome, and magic-link emails all use a new layout with the Rowstr logo, brand colors, and bullet-proof call-to-action buttons that render consistently across Gmail, Outlook, and Apple Mail.
- Welcome email timing fix: welcome emails now arrive only once the recipient actually verifies their account — no more receiving one seconds before you've even clicked your magic link.
- Roomier uploads: avatar and logo uploads now allow up to 5 MB (was 2 MB) so common PNG exports don't get rejected.
0.6
Drive, top to bottom
DriveMediaPerformance
Folders
- Customizable folders: pick a color, an icon, and optionally link a folder to a model — your drive finally looks like yours.
- Linked-model badge: folders tied to a model show a compact pill with the model's avatar and name, tinted to match the folder color.
- Persistent empty folders: create a folder without dropping files into it — it sticks across navigation and reloads.
- Smart import target: importing from inside a folder now lands the files in that folder by default.
- Instant feedback: creating a folder and editing its color, icon, or linked model updates the UI immediately.
- UX rework: folders now sit in a compact row above the media grid with a kebab menu for actions, media tiles no longer slide in when you switch folders, and the breadcrumb only appears once you're inside a folder.
Uploads
- Uploads routed through our CDN: video traffic now goes through
cdn.rowstr.com, a Cloudflare Worker sitting right next to R2. No more round-trips through the app server — your file takes the shortest path to storage. - Parallel multipart for large videos: anything over 100 MB is split into chunks and pushed in parallel so very big clips land much faster than they used to.
- Cancel any upload, any time: hover a file in the upload widget and click the X — it actually stops, no more "ghost" uploads finishing in the background after you changed your mind.
- Real progress, not a fake bar: the progress indicator now reflects bytes actually moving on the wire, not a canned animation.
- Stronger video compression: re-encodes now use a slower, higher-quality preset that trims roughly 30% more off each video with no visible quality drop.
- Tighter size estimates: the upload dialog shows a realistic post-compression size upfront, so you know before you hit "upload" whether you're about to bust your quota.
- Drive sheet fix: reopening a media item in the drive no longer gets stuck on an infinite spinner.
0.5
Org timezones
Calendar
- Timezone per org: every organization can now pick its own timezone. The content calendar, todo scheduling, and the "today" line all follow it instead of UTC — so a shoot scheduled for 8am in Paris shows as 8am in Paris, not 10am or 6am depending on where you happen to be logged in.
- Inactive orgs are archived automatically: if an organization goes quiet, Rowstr now archives it in the background so your workspace stays tidy.
0.4
Faster video uploads & mobile dashboard
MediaMobile
- Instant video thumbnails: the thumbnail appears in the drive the moment you drop a clip, instead of waiting for the server to process it.
- Real upload progress: the progress bar now reflects the actual upload, not a fake animation — especially visible on large files going straight to the CDN.
- No more waiting on transcodes: video processing moved to a dedicated worker in the background. You can keep working while clips transcode, and larger files no longer time out mid-upload.
- Mobile & tablet: full pass on the dashboard and model pages so they're finally usable on phones and tablets without pinching.
0.3
Member roles & bigger storage
PermissionsStorage
- Roles in the UI: member and admin roles are now enforced everywhere. Non-admins no longer see buttons and settings they can't actually use.
- More storage by default: every organization gets a larger default storage quota — plenty of room before you need to think about it.
- Faster chat: unread counts in the sidebar no longer slow down on busy orgs with lots of messages.