diff --git a/packages/iios-messaging-ui/package.json b/packages/iios-messaging-ui/package.json index 6a9d110..69ac4b7 100644 --- a/packages/iios-messaging-ui/package.json +++ b/packages/iios-messaging-ui/package.json @@ -1,6 +1,6 @@ { "name": "@insignia/iios-messaging-ui", - "version": "0.1.0", + "version": "0.1.1", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/packages/iios-messaging-ui/src/adapters/mock-inbox.ts b/packages/iios-messaging-ui/src/adapters/mock-inbox.ts index c2603e4..dfa03a5 100644 --- a/packages/iios-messaging-ui/src/adapters/mock-inbox.ts +++ b/packages/iios-messaging-ui/src/adapters/mock-inbox.ts @@ -88,6 +88,11 @@ export class MockInboxAdapter implements InboxAdapter { return { contentRef: `mock/${this.seq++}`, mimeType: file.type || 'application/octet-stream', sizeBytes: file.size, filename: file.name }; } + async downloadAttachment(attachment: MailAttachment): Promise { + // Demo: no real bytes — hand back a data URL so the click resolves without a network call. + return `data:${attachment.mimeType};base64,`; + } + async directory(): Promise { return [...PEOPLE]; } diff --git a/packages/iios-messaging-ui/src/inbox/adapter.ts b/packages/iios-messaging-ui/src/inbox/adapter.ts index dec547e..df26cb3 100644 --- a/packages/iios-messaging-ui/src/inbox/adapter.ts +++ b/packages/iios-messaging-ui/src/inbox/adapter.ts @@ -22,6 +22,10 @@ export interface InboxAdapter { /** Upload a file to storage, returning a reference to send with a reply/compose. */ uploadAttachment?(file: File): Promise; + /** Resolve a short-lived URL to view/download an attachment. Absent => attachment chips are + * shown but not clickable. */ + downloadAttachment?(attachment: MailAttachment): Promise; + // ── Compose (optional) — absent hides the "New message" affordance ── /** People you can compose an in-app message to. */ directory?(): Promise; diff --git a/packages/iios-messaging-ui/src/inbox/hooks.ts b/packages/iios-messaging-ui/src/inbox/hooks.ts index b9ab6a2..323b68a 100644 --- a/packages/iios-messaging-ui/src/inbox/hooks.ts +++ b/packages/iios-messaging-ui/src/inbox/hooks.ts @@ -60,6 +60,8 @@ export interface MailThreadState { reply: (content: string, attachment?: MailAttachment) => Promise; canAttach: boolean; upload: (file: File) => Promise; + canDownload: boolean; + download: (attachment: MailAttachment) => Promise; refetch: () => void; } @@ -113,8 +115,25 @@ export function useMailThread(threadId: string | null): MailThreadState { }, [adapter], ); + const download = useCallback( + async (attachment: MailAttachment) => { + if (!adapter.downloadAttachment) throw new Error('attachment download is not supported by this adapter'); + return adapter.downloadAttachment(attachment); + }, + [adapter], + ); - return { messages, loading, error, reply, canAttach: typeof adapter.uploadAttachment === 'function', upload, refetch }; + return { + messages, + loading, + error, + reply, + canAttach: typeof adapter.uploadAttachment === 'function', + upload, + canDownload: typeof adapter.downloadAttachment === 'function', + download, + refetch, + }; } export interface ComposeState { diff --git a/packages/iios-messaging-ui/src/inbox/inbox.tsx b/packages/iios-messaging-ui/src/inbox/inbox.tsx index 4cf235a..3587b6c 100644 --- a/packages/iios-messaging-ui/src/inbox/inbox.tsx +++ b/packages/iios-messaging-ui/src/inbox/inbox.tsx @@ -117,7 +117,7 @@ function Detail({ item, onTransition }: { item: InboxItem; onTransition: (state: /** Read a mail thread (HTML in a sandboxed iframe) + reply. */ export function MailReader({ threadId, subject }: { threadId: string; subject: string }) { - const { messages, loading, error, reply, canAttach, upload } = useMailThread(threadId); + const { messages, loading, error, reply, canAttach, upload, canDownload, download } = useMailThread(threadId); const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); const [pending, setPending] = useState(null); @@ -125,6 +125,15 @@ export function MailReader({ threadId, subject }: { threadId: string; subject: s const [attachErr, setAttachErr] = useState(null); const fileRef = useRef(null); + async function openAttachment(att: MailAttachment): Promise { + try { + const url = await download(att); + window.open(url, '_blank', 'noopener,noreferrer'); + } catch (err) { + setAttachErr(err instanceof Error ? err.message : String(err)); + } + } + async function pick(e: React.ChangeEvent): Promise { const file = e.target.files?.[0]; e.target.value = ''; @@ -176,7 +185,13 @@ export function MailReader({ threadId, subject }: { threadId: string; subject: s
{m.text}
) : null} {m.attachment ? ( - 📎 {m.attachment.filename ?? 'attachment'}{m.attachment.sizeBytes ? ` · ${fmtBytes(m.attachment.sizeBytes)}` : ''} + canDownload ? ( + + ) : ( + 📎 {m.attachment.filename ?? 'attachment'}{m.attachment.sizeBytes ? ` · ${fmtBytes(m.attachment.sizeBytes)}` : ''} + ) ) : null} ))} diff --git a/packages/iios-messaging-ui/src/styles.css b/packages/iios-messaging-ui/src/styles.css index c7b01c2..68f0d5f 100644 --- a/packages/iios-messaging-ui/src/styles.css +++ b/packages/iios-messaging-ui/src/styles.css @@ -353,7 +353,15 @@ white-space: nowrap; } .miu-mail-msg .miu-attach-chip { - margin-top: 6px; + margin: 8px 12px 12px; +} +button.miu-attach-dl { + cursor: pointer; + color: var(--miu-text); +} +button.miu-attach-dl:hover { + border-color: var(--miu-accent); + color: var(--miu-accent); } .miu-attach-pending { display: inline-flex; @@ -763,7 +771,7 @@ .miu-detail { display: flex; flex-direction: column; - height: 100%; + flex: 1; min-height: 0; } .miu-detail-actions { @@ -790,7 +798,7 @@ .miu-mail { display: flex; flex-direction: column; - height: 100%; + flex: 1; min-height: 0; } .miu-mail-head {