AttachToConversation.tsxrefactor(mail): stop announcing success, and fix the qualified-date parser
Toasts now only fire when something went wrong. 416 success confirmations
across 151 files told the user that the thing they had just watched happen had
happened — the row was already gone, the draft already sent, the text already in
the clipboard. Removing them took the variables and callbacks that existed only
to build those messages with them.
Thirteen survive, minus the success styling: the ones carrying an Undo button,
where the toast is the affordance rather than a notification. Those now use the
neutral toast() so they read as "here is your undo", not congratulation.
toast.info / toast.warning are untouched — those report neutral or unwanted
outcomes, which is not the same thing as celebrating one.
Also fixes "mid august" resolving to August 1st in every date picker. chrono has
no notion of early/mid/late, so it finds the month, discards the qualifier and
answers the 1st — and near a month boundary forwardDate then pushed that past
date into NEXT YEAR, so scheduling a send three days out landed eleven months
away. resolveQualifiedPeriod runs ahead of chrono and takes the words it cannot
express: named months, relative months, weeks and years, each with early / mid /
late, rolling forward only when the resolved date has actually passed.
And the scheduled email in a thread now renders as an ordinary message rather
than a bespoke card: same MailDisplay, a Scheduled badge in the tracking-badge
slot, and Edit / Delete replacing reply / reply-all / forward — there is nothing
to reply to on mail that has not gone out yet. Edit unschedules before reopening,
because the send payload is a frozen snapshot and would otherwise fire the old
text alongside the edited one.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 12, 2026, 10:13 PMchannel-icons.tsxfeat: inbox row/view polish — colourful logos, date split, tab selector, channel view (design: omni-channel-inbox)
- Full-colour brand logos (LinkedIn/WhatsApp/Slack) + Gmail for email; rendered
in the subject position (after the unread dot), not next to the name.
- InboxList gets the same Today/Yesterday/Last 7 days/Older date split as MailList.
- ChannelSelector restyled to look exactly like a folder tab ("All Channels v").
- ChannelThreadView reworked: mail-thread-like top bar (back · counterpart ·
star/snooze/done commands), real Slack message rendering (from the conversation
timeline) + reply for all three channels.
Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 27, 2026, 12:27 PMChannelConnectPrompt.tsxfeat: inbox — connect prompt for LinkedIn/WhatsApp when the account isn't linked
Selecting the LinkedIn or WhatsApp filter with no connected account now shows a
circular brand-mark connect prompt (mirroring the empty-email state) that kicks
off the same Unipile hosted-auth flow as settings → connections, instead of a
dead empty feed. Extracts the connect+poll flow into shared hooks so the settings
cards and the inbox prompt share one implementation.
Co-Authored-By: Claude Opus 4.8 (1M context) <<email>>Jul 28, 2026, 11:49 PMChannelMessageList.tsxfix(slack): grow the reply box, colour the mentions, and render the real emoji
The unibox's Slack/LinkedIn surface, held next to Slack itself.
The reply box never grew. A textarea does not size to its content, so `rows={1}`
with a `max-h` was a one-line box that scrolled internally the moment you wrote a
second sentence — the `max-h` it was written against could never be reached.
`SlackMentionTextarea` now measures and writes back its height on every edit
(opt-in, because the table's output-cell editor is sized in `rows` and has a
resize handle the user drags). Both composers cap at ~3x their resting height,
and a ResizeObserver re-measures on WIDTH changes only — reacting to the height
it just wrote would loop.
Mentions rendered grey on grey: `bg-primary/10 text-primary` inherits a primary
that is near-black in this theme, so a ping read as nothing at all. They are now
blue on a lighter wash of the same blue, and the composer's TipTap chip uses the
same pair, so a mention does not change colour the moment it is sent.
Reactions showed the wrong glyph, in three different ways:
- The dataset stores the BARE codepoint for ~170 legacy symbols (`❤` U+2764,
`✔` U+2714, `⚠` U+26A0), and a bare codepoint renders in text presentation —
a thin monochrome glyph, not the coloured emoji Slack shows for the very same
reaction. U+FE0F is appended per codepoint, so `🤷♂` gets the selector after
the ♂ and nowhere else, with a range table of the BMP codepoints that are
already emoji by default keeping `✅` free of a pointless invisible character.
- Slack writes some names with hyphens (`star-struck`, `man-shrugging`) where the
dataset uses underscores. Unresolved, those chips rendered their own raw name.
- A `::skin-tone-N` suffix resolved to nothing. The modifier now goes in after the
base codepoint, before any ZWJ tail, which is where Unicode wants it.
The chip consults Slack's key first — it is what Slack renders from and it carries
the tone — with the stored `emojiUnicode` behind it, itself normalized, since it
came from the same bare dataset. `SlackBodyRenderer` had a second, weaker copy of
the shortcode map for `:heart:` in message text; it is gone, and body text now
resolves through the same function, so a heart in the words and a heart on the
reaction row are the same heart.
Two smaller things from the same comparison: the day divider's rules run into the
badge instead of stopping short of it, one hairline interrupted by the date; and
the reply count sits at `text-xs`, below the message text rather than level with it.
Co-Authored-By: Claude Opus 5 <<email>>Aug 26, 2026, 7:33 PMChannelSelector.tsxfeat(inbox,two-factor,marketing): channel filter on query tabs, 2FA policy, host routing
Three in-flight threads plus supporting docs, committed together.
- inbox: the channel badge no longer goes empty on a Gmail-query tab —
participatingChannels stops dropping chat channels when the tab rule is
mailOnly, so an explicit single-channel badge asks the sources it names.
- two-factor: policy module with its own tests, wired through session
verification and auth, with the env and runtime-contract keys it needs.
- marketing: host routing for the signed-out root, with a redirect test.
- docs: inbox channel-filter bug writeup, daily agenda templates,
open-tracking v2 notes, and the 2026-08-30 customer feedback audit.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:53 AMChannelThreadView.tsxfix(mail): a link in a chat thread is a link
LinkedIn and WhatsApp send their words as plain text, so a pasted URL
rendered as characters. Slack linkified only its own <url|label> tokens —
which its composer emits and a bot, an unfurl field or pasted text do not.
Bare URLs are now linkified in both, before the mrkdwn formatting pass so
the parser cannot read a URL's underscores as italics. The splitter is
lifted out of the calendar's LinkifiedEventText into lib/linkify.
Links are painted with the action ramp, not primary: primary is near-black
on light and near-white on dark, the colour of the words around them.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 18, 2026, 9:38 AM1 defectInboxList.tsxfix(inbox): key the head-change latch to the query it describes
Greptile, on the previous commit.
The hook instance outlives the question it is asking: switching folder, inbox or
the unread lens swaps the cache entry underneath it while the ref persists. A
bare boolean carried the previous query's `true` into the next one, read "no
transition", and skipped a reset that query genuinely needed — leaving its later
pages on cursors minted against a boundary that had since moved. Id-dedup cannot
clean that up, since stale rows need not collide by id. The latch now remembers
which query it was describing.
Also fixes the degraded-channel notice, which claimed more than it knew. A
channel that had already loaded a page keeps showing it when a later refetch
fails — react-query retains the last good data, and dropping rows we did fetch
would be a worse answer than holding them. So the notice cannot say the channel
is missing; what is missing is anything newer than what is on screen, which is
equally true of a channel that never loaded at all.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 12:51 AMInboxRow.tsxfeat(inbox): unibox keyboard parity, undo-send that puts the draft back, sharper chat context
Unibox now navigates like the email list — j/k, Enter, Escape all walk whichever
list is on screen via a new `activeListSource` on the thread slice, and Escape
clears the full selection (focused row, bulk checkboxes, ambient conversation)
instead of only the keyboard cursor.
Undo send stops round-tripping through localStorage and a URL rewrite: the send
records where the draft came from and undo reopens that surface, restoring the
un-quoted body onto the draft row (recreating it when the send removed it).
Chat context: an auto-committed primary conversation is provisional while the
chat is empty and drops when you move on, the context cards follow the deal
you're actually on, threads with no conversation get a link prompt, and the
conversation surface shows a keyboard legend rather than restating cards already
on screen.
Also: agenda Trash removes the row outright, Next steps renders as one card with
its booked calls beneath, and channel chats focus the composer on open.Jul 30, 2026, 11:54 PMSlackThreadPanel.tsxfix(ui): a composer that grows to half the screen, not five lines
The Slack/LinkedIn/WhatsApp reply box capped at 112px, so anything past
about five wrapped lines scrolled inside a box too small to reread what
you had just written. Cap it at half the viewport instead — the thread
above keeps the other half — and give the email reply body the same 50vh
ceiling, dropping its 820px cap while keeping the 250px floor.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 2, 2026, 3:18 PM