open-output-in-channel.ts4.6 KBView on GitHub 'use client';
/**
* "Open this draft where it is going" — the click behind an output cell's channel pill.
*
* It opens the chat the way the unibox opens it: `/mail/inbox?slack=<containerKey>`, or
* `?linkedin=` / `?whatsapp=` with the Unipile chat id. All three are the deep-link shape
* `LayoutUrlSync` projects and restores (`useChannelArtifactParam`), and `mail.tsx` adopts the
* matching feed filter on the way in. So you land in the same full `ChannelThreadView` you
* would have reached by clicking that row — history, mention menu, Send and all — rather than
* in a second, lesser copy of it.
*
* That is also why there is no conversation lookup here. The unibox addresses a chat by its OWN
* key; only the per-deal composer needs a `conversationId`, and requiring one would make the
* pill dead on every chat that is not linked to a deal. That per-deal path is the sibling
* definition, `conversations/.../composer/open-slack-draft-in-channel.ts` — use it when you
* have a conversation, and this one when you only have a chat.
*
* ── The draft ──
*
* `ChannelThreadView` clears its composer on every row change, so the message is handed over
* through `seedChannelDraft` and adopted there on mount — keyed by the same container key the
* URL carries, on every channel. Raw text, `<@U…>` tokens included on Slack: see
* `pending-channel-draft.ts`.
*
* A LinkedIn or WhatsApp composer ALSO persists what it holds (`inbox.saveChannelDraft` writes
* `linkedin_chats.draft`), so a handed-over draft that the user walks away from mid-edit
* survives on those two where Slack's would not. The seed is still what carries it across the
* navigation: the persisted draft is written by the composer, not read into it.
*
* ── A sent cell opens the chat, not the draft ──
*
* `sentAt` means this message is already out. Seeding it back into the composer would put a
* fresh copy in the box, one Enter away from being sent twice.
*/
import { useCallback } from 'react';
import { useNavigate } from 'react-router';
import { TABLE_OUTPUT_KIND } from '@zero/server/table';
import { seedChannelDraft } from '@/modules/inbox/store/pending-channel-draft';
import { isOutputSent, type CellOutput } from './cell-output';
/**
* Where a chat opens. The `inbox` folder of the mail chrome — the query param is what names the
* chat, and `mail.tsx` switches the feed to that channel when it sees one.
*/
const CHANNEL_ROUTE = '/mail/inbox';
/** The URL param each channel is addressed by. Keyed by output kind so a new kind is a type error. */
const CHANNEL_PARAM = {
[TABLE_OUTPUT_KIND.SLACK]: 'slack',
[TABLE_OUTPUT_KIND.LINKEDIN]: 'linkedin',
[TABLE_OUTPUT_KIND.WHATSAPP]: 'whatsapp',
} as const;
/** An output cell that names a chat this app can open, resolved to its address. */
export interface OpenableOutput {
param: (typeof CHANNEL_PARAM)[keyof typeof CHANNEL_PARAM];
/** The provider's container id — a Slack channel id, or a Unipile chat id. */
containerKey=[redacted];
message: string;
sent: boolean;
}
/**
* The chat this cell would open, or null.
*
* Null for an email or a file (nowhere to go), and — importantly — for a LinkedIn or WhatsApp
* draft addressed only by `attendeeProviderId` / `phoneE164`. Those can be SENT, because the
* provider will open the conversation on the way, but there is no chat to open yet, so the
* pill stays a plain one rather than navigating to a chat id that does not exist.
*/
export function openableOutput(output: CellOutput): OpenableOutput | null {
if (output.kind === TABLE_OUTPUT_KIND.SLACK) {
if (!output.channelId) return null;
return {
param: CHANNEL_PARAM[TABLE_OUTPUT_KIND.SLACK],
containerKey=[redacted],
message: output.message ?? '',
sent: isOutputSent(output),
};
}
if (output.kind === TABLE_OUTPUT_KIND.LINKEDIN || output.kind === TABLE_OUTPUT_KIND.WHATSAPP) {
if (!output.chatId) return null;
return {
param: CHANNEL_PARAM[output.kind],
containerKey=[redacted],
message: output.message ?? '',
sent: isOutputSent(output),
};
}
return null;
}
export interface OpenOutputInChannel {
/** Hands the draft to the chat's composer and lands there. */
open: (target: OpenableOutput) => void;
}
export function useOpenOutputInChannel(): OpenOutputInChannel {
const navigate = useNavigate();
const open = useCallback(
(target: OpenableOutput) => {
if (!target.sent) seedChannelDraft(target.containerKey, target.message);
void navigate(
`${CHANNEL_ROUTE}?${target.param}=${encodeURIComponent(target.containerKey)}`,
);
},
[navigate],
);
return { open };
}