feat(messaging-ui): attachments in inbox mail — reply + compose upload
InboxAdapter gains uploadAttachment + attachment params on mailReply/ composeInternal/composeExternal. MailReader reply and ComposeModal grow an attach affordance (paperclip + pending chips); mail history renders sent attachments. MockInboxAdapter implements upload. 67 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useInboxAdapter } from './provider';
|
||||
import type { InboxItem, InboxState, MailMessage, MailPerson } from './types';
|
||||
import type { InboxItem, InboxState, MailAttachment, MailMessage, MailPerson } from './types';
|
||||
|
||||
export interface InboxData {
|
||||
items: InboxItem[];
|
||||
@@ -57,7 +57,9 @@ export interface MailThreadState {
|
||||
messages: MailMessage[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
reply: (content: string) => Promise<void>;
|
||||
reply: (content: string, attachment?: MailAttachment) => Promise<void>;
|
||||
canAttach: boolean;
|
||||
upload: (file: File) => Promise<MailAttachment>;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
@@ -97,22 +99,31 @@ export function useMailThread(threadId: string | null): MailThreadState {
|
||||
|
||||
const refetch = useCallback(() => setNonce((n) => n + 1), []);
|
||||
const reply = useCallback(
|
||||
async (content: string) => {
|
||||
async (content: string, attachment?: MailAttachment) => {
|
||||
if (!threadId) return;
|
||||
await adapter.mailReply(threadId, content);
|
||||
await adapter.mailReply(threadId, content, attachment);
|
||||
setNonce((n) => n + 1);
|
||||
},
|
||||
[adapter, threadId],
|
||||
);
|
||||
const upload = useCallback(
|
||||
async (file: File) => {
|
||||
if (!adapter.uploadAttachment) throw new Error('attachments are not supported by this adapter');
|
||||
return adapter.uploadAttachment(file);
|
||||
},
|
||||
[adapter],
|
||||
);
|
||||
|
||||
return { messages, loading, error, reply, refetch };
|
||||
return { messages, loading, error, reply, canAttach: typeof adapter.uploadAttachment === 'function', upload, refetch };
|
||||
}
|
||||
|
||||
export interface ComposeState {
|
||||
supported: boolean;
|
||||
directory: MailPerson[];
|
||||
sendInternal: (recipientUserId: string, subject: string, text: string) => Promise<void>;
|
||||
sendExternal: (target: string, subject: string, text: string) => Promise<void>;
|
||||
sendInternal: (recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]) => Promise<void>;
|
||||
sendExternal: (target: string, subject: string, text: string, attachments?: MailAttachment[]) => Promise<void>;
|
||||
canAttach: boolean;
|
||||
upload: (file: File) => Promise<MailAttachment>;
|
||||
}
|
||||
|
||||
/** Compose a new message — in-app (to a person) or external (to an email). */
|
||||
@@ -138,19 +149,26 @@ export function useCompose(): ComposeState {
|
||||
}, [adapter]);
|
||||
|
||||
const sendInternal = useCallback(
|
||||
async (recipientUserId: string, subject: string, text: string) => {
|
||||
async (recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]) => {
|
||||
if (!adapter.composeInternal) throw new Error('compose is not supported by this adapter');
|
||||
await adapter.composeInternal(recipientUserId, subject, text);
|
||||
await adapter.composeInternal(recipientUserId, subject, text, attachments);
|
||||
},
|
||||
[adapter],
|
||||
);
|
||||
const sendExternal = useCallback(
|
||||
async (target: string, subject: string, text: string) => {
|
||||
async (target: string, subject: string, text: string, attachments?: MailAttachment[]) => {
|
||||
if (!adapter.composeExternal) throw new Error('compose is not supported by this adapter');
|
||||
await adapter.composeExternal(target, subject, text);
|
||||
await adapter.composeExternal(target, subject, text, attachments);
|
||||
},
|
||||
[adapter],
|
||||
);
|
||||
const upload = useCallback(
|
||||
async (file: File) => {
|
||||
if (!adapter.uploadAttachment) throw new Error('attachments are not supported by this adapter');
|
||||
return adapter.uploadAttachment(file);
|
||||
},
|
||||
[adapter],
|
||||
);
|
||||
|
||||
return { supported, directory, sendInternal, sendExternal };
|
||||
return { supported, directory, sendInternal, sendExternal, canAttach: typeof adapter.uploadAttachment === 'function', upload };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user