feat(plugins) : add onRecipientChipsChange hook
This commit is contained in:
committed by
Linus Rath
parent
0a30b2fb3a
commit
2cb5c739b4
@@ -49,6 +49,8 @@ import {
|
|||||||
waitForPendingUploads,
|
waitForPendingUploads,
|
||||||
extractUserAuthoredText,
|
extractUserAuthoredText,
|
||||||
type Recipient,
|
type Recipient,
|
||||||
|
enrichChipsWithColorsAndIcons,
|
||||||
|
ICON_MAP,
|
||||||
} from "@/lib/email-composer-utils";
|
} from "@/lib/email-composer-utils";
|
||||||
import { isValidEmail } from "@/lib/validation";
|
import { isValidEmail } from "@/lib/validation";
|
||||||
import { RichTextEditor } from "@/components/email/rich-text-editor";
|
import { RichTextEditor } from "@/components/email/rich-text-editor";
|
||||||
@@ -826,6 +828,26 @@ export function EmailComposer({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [composerClient, plainTextMode, mode]);
|
}, [composerClient, plainTextMode, mode]);
|
||||||
|
|
||||||
|
const processEnrichment = async (
|
||||||
|
recipients: Recipient[],
|
||||||
|
setRecipients: (items: Recipient[]) => void
|
||||||
|
) => {
|
||||||
|
const hasUnenriched = recipients.some((r) => !r.extra?.enriched);
|
||||||
|
if (!hasUnenriched) return;
|
||||||
|
|
||||||
|
const newChips = await enrichChipsWithColorsAndIcons(recipients);
|
||||||
|
const fullyEnriched = newChips.map((chip) => ({
|
||||||
|
...chip,
|
||||||
|
extra: { ...chip.extra, enriched: true },
|
||||||
|
}));
|
||||||
|
setRecipients(fullyEnriched);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => { processEnrichment(to, setTo); }, [to]);
|
||||||
|
useEffect(() => { processEnrichment(cc, setCc); }, [cc]);
|
||||||
|
useEffect(() => { processEnrichment(bcc, setBcc); }, [bcc]);
|
||||||
|
|
||||||
const composerSignatureHtml = signatureIdentity?.htmlSignature
|
const composerSignatureHtml = signatureIdentity?.htmlSignature
|
||||||
? `<div>${sanitizeSignatureHtmlForDisplay(signatureIdentity.htmlSignature)}</div>`
|
? `<div>${sanitizeSignatureHtmlForDisplay(signatureIdentity.htmlSignature)}</div>`
|
||||||
: signatureIdentity?.textSignature
|
: signatureIdentity?.textSignature
|
||||||
@@ -3169,6 +3191,11 @@ function RecipientChipInput({
|
|||||||
const handleContainerDrop = (e: React.DragEvent) => {
|
const handleContainerDrop = (e: React.DragEvent) => {
|
||||||
performDrop(e, dropIndex ?? chips.length);
|
performDrop(e, dropIndex ?? chips.length);
|
||||||
};
|
};
|
||||||
|
const colorStyles: Record<'success' | 'destructive' | 'warning', string> = {
|
||||||
|
success: "bg-success/15 text-secondary-foreground hover:bg-success/30 !border-success",
|
||||||
|
destructive: "bg-destructive/15 text-secondary-foreground hover:bg-destructive/30 !border-destructive",
|
||||||
|
warning: "bg-warning/15 text-secondary-foreground hover:bg-warning/30 !border-warning",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 relative min-w-0">
|
<div className="flex-1 relative min-w-0">
|
||||||
@@ -3186,6 +3213,12 @@ function RecipientChipInput({
|
|||||||
{chips.map((chip, i) => {
|
{chips.map((chip, i) => {
|
||||||
const isEditing = editingChip?.index === i;
|
const isEditing = editingChip?.index === i;
|
||||||
const chipDisplay = formatChipDisplay(chip);
|
const chipDisplay = formatChipDisplay(chip);
|
||||||
|
let IconComponent = null;
|
||||||
|
if(chip.extra?.icon){
|
||||||
|
IconComponent = ICON_MAP[chip.extra?.icon];
|
||||||
|
}
|
||||||
|
const customColor = chip.extra?.color;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`${chip.email}-${i}`}>
|
<React.Fragment key={`${chip.email}-${i}`}>
|
||||||
{dropIndex === i && (
|
{dropIndex === i && (
|
||||||
@@ -3214,11 +3247,17 @@ function RecipientChipInput({
|
|||||||
"inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-sm border border-border transition-colors",
|
"inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-sm border border-border transition-colors",
|
||||||
isEditing
|
isEditing
|
||||||
? "bg-background ring-1 ring-ring"
|
? "bg-background ring-1 ring-ring"
|
||||||
: "bg-secondary text-secondary-foreground hover:bg-accent cursor-grab active:cursor-grabbing",
|
: ( customColor && colorStyles[customColor]
|
||||||
|
? `${colorStyles[customColor]} cursor-grab active:cursor-grabbing`
|
||||||
|
: "bg-secondary text-secondary-foreground hover:bg-accent cursor-grab active:cursor-grabbing"),
|
||||||
!isEditing && draggingIndex === i && "opacity-50"
|
!isEditing && draggingIndex === i && "opacity-50"
|
||||||
)}
|
)}
|
||||||
onContextMenu={isEditing ? undefined : (e) => handleContextMenu(e, i, chip)}
|
onContextMenu={isEditing ? undefined : (e) => handleContextMenu(e, i, chip)}
|
||||||
>
|
>
|
||||||
|
{IconComponent ? (
|
||||||
|
<IconComponent className={`w-4 h-4 text-${customColor}`} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
ref={editInputRef}
|
ref={editInputRef}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { isValidEmail } from "@/lib/validation";
|
import { isValidEmail } from "@/lib/validation";
|
||||||
import { htmlToPlainText } from "@/lib/html-to-text";
|
import { htmlToPlainText } from "@/lib/html-to-text";
|
||||||
|
import { emailHooks } from "./plugin-hooks";
|
||||||
|
import { Ellipsis, Lock, TriangleAlert } from "lucide-react";
|
||||||
|
|
||||||
const HTML_ESCAPE_MAP = {
|
const HTML_ESCAPE_MAP = {
|
||||||
"&": "&",
|
"&": "&",
|
||||||
@@ -109,6 +111,17 @@ export function extractUserAuthoredText(
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for hook to let plugins enrich recipient chips with colors and icons.
|
||||||
|
* The icon is a key into ICON_MAP, which maps to a lucide-react component.
|
||||||
|
*/
|
||||||
|
export const ICON_MAP = {
|
||||||
|
'lock': Lock,
|
||||||
|
'triangle-alert': TriangleAlert,
|
||||||
|
'ellipsis': Ellipsis,
|
||||||
|
};
|
||||||
|
type IconName = keyof typeof ICON_MAP;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composer recipient. Display name is optional; email is required - except
|
* A composer recipient. Display name is optional; email is required - except
|
||||||
* for contact-group chips, which carry their already-resolved members and an
|
* for contact-group chips, which carry their already-resolved members and an
|
||||||
@@ -119,6 +132,16 @@ export type Recipient = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
email: string;
|
email: string;
|
||||||
group?: { members: Array<{ name?: string; email: string }> };
|
group?: { members: Array<{ name?: string; email: string }> };
|
||||||
|
extra?: {
|
||||||
|
color?: "success" | "destructive" | "warning"; // optional color for display purposes. May be populated by plugins via the onRecipientChipsChange hook.
|
||||||
|
icon?: IconName; // optional icon for display purposes. May be populated by plugins via the onRecipientChipsChange hook.
|
||||||
|
enriched?: boolean; // optional flag to indicate if the recipient has been enriched by plugins via the onRecipientChipsChange hook.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Enriches recipient chips with colors and icons. */
|
||||||
|
export async function enrichChipsWithColorsAndIcons(chips: Recipient[]): Promise<Recipient[]> {
|
||||||
|
return await emailHooks.onRecipientChipsChange.transform(chips);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -276,6 +276,10 @@ export const emailHooks = {
|
|||||||
// lets plugin edit emails before they are shown in row. Used to populate preview
|
// lets plugin edit emails before they are shown in row. Used to populate preview
|
||||||
// field for encryption plugins.
|
// field for encryption plugins.
|
||||||
onEmailsFetched: new HookBus(),
|
onEmailsFetched: new HookBus(),
|
||||||
|
// Transform hook - lets plugins edit the recipient chips in composer fields.
|
||||||
|
// They can add, remove, or modify chips (e.g. rewrite addresses, add colors/icons).
|
||||||
|
// Take Recipient[] as argument.
|
||||||
|
onRecipientChipsChange: new HookBus(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// §7.2 Calendar Hooks
|
// §7.2 Calendar Hooks
|
||||||
|
|||||||
Reference in New Issue
Block a user