From a34d8f53b33da0bac9c2e17ad864d40bf4d52be9 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 28 Aug 2024 09:21:31 -0700 Subject: [PATCH] Show email subject lines. This includes a bunch of new things. I've introduced "ensureEmail..." to indicate that the UI would like some data to be populated, but if it is already present we don't need to do anything. I've also introduced a cache for emails that is keyed on the email ID. I don't know if email IDs are unique. They look like they should be globally unique within a given server, but I'm not sure and the standard is unclear. It'll need some experimentation. --- src/EmailList.tsx | 27 ++++++++------------ src/EmailSummary.tsx | 40 +++++++++++++++++++++++++++++ src/client/Client.tsx | 59 ++++++++++++++++++++++++++++++++++++++----- src/client/types.tsx | 4 +++ 4 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 src/EmailSummary.tsx diff --git a/src/EmailList.tsx b/src/EmailList.tsx index 34c44be..4f5a20e 100644 --- a/src/EmailList.tsx +++ b/src/EmailList.tsx @@ -3,6 +3,7 @@ import Stack from "react-bootstrap/Stack"; import Client from "./client/Client"; import { IAccount, IMailbox } from "./client/types"; +import EmailSummary from "./EmailSummary"; type EmailListProps = { account: IAccount | null; @@ -17,37 +18,31 @@ class EmailList extends React.Component { if (this.props.account == null) return; if (this.props.client == null) return; if (this.props.mailbox == null) return; - this.props.client.emailList( + this.props.client.ensureEmailList( this.props.account.id, this.props.mailbox.id, - [], ); } render() { if ( this.props.account == null || + this.props.client == null || this.props.mailbox == null || this.props.mailbox.emailIds == null ) { return ; - } else if (this.props.mailbox.emails == null) { - return ( - - {this.props.mailbox.emailIds.map((e) => ( -
- Email {e} -
- ))} -
- ); } else { return ( - {this.props.mailbox.emails.map((m) => ( -
- {m.subject} -
+ {this.props.mailbox.emailIds.slice(0, 5).map((e) => ( + ))}
); diff --git a/src/EmailSummary.tsx b/src/EmailSummary.tsx new file mode 100644 index 0000000..afffa7a --- /dev/null +++ b/src/EmailSummary.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import Client from "./client/Client"; +import { IAccount, IEmail } from "./client/types"; + +type EmailSummaryProps = { + account: IAccount | null; + client: Client | null; + email: IEmail | null; + emailId: string; +}; +type EmailSummaryState = {}; + +class EmailSummary extends React.Component< + EmailSummaryProps, + EmailSummaryState +> { + componentDidMount() { + this.ensureData(); + } + componentDidUpdate() { + this.ensureData(); + } + + ensureData() { + if (this.props.account == null) return; + if (this.props.client == null) return; + this.props.client.ensureEmailGet(this.props.account.id, this.props.emailId); + } + + render() { + return ( +
+ {this.props.email != null + ? this.props.email.subject + : this.props.emailId} +
+ ); + } +} +export default EmailSummary; diff --git a/src/client/Client.tsx b/src/client/Client.tsx index d002d1c..fde5d63 100644 --- a/src/client/Client.tsx +++ b/src/client/Client.tsx @@ -6,7 +6,7 @@ import * as base64 from "base-64"; import * as jmapclient from "jmap-client-ts"; import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport"; -import { IAccount, IMailbox, ISession } from "./types"; +import { IAccount, IEmail, IMailbox, ISession } from "./types"; type Callback = () => void; @@ -36,11 +36,27 @@ export default class Client { this.callbacks.push(f); } + // Ensure we have the fully-fleshed email + ensureEmailGet(accountId: string, emailId: string) { + if (this.state.session == null) return; + const existing = this.state.session.emails[emailId]; + if (existing != null) return; + this.emailGet(accountId, emailId); + } + + // Ensure we have the list of emails for the provided account and mailbox + ensureEmailList(accountId: string, mailboxId: string) { + const mailbox = this.mailbox(accountId, mailboxId); + if (mailbox != null && mailbox.emailIds != null) return; + this.emailList(accountId, mailboxId, []); + } + // Ensure that login happens. If this is called many times, only login once. ensureLogin(auth: IAuth) { if (this.jclient != null) return; this.doLogin(auth); } + // Make the request to get system metadata doLogin(auth: IAuth) { const domain = auth.email.split("@")[1]; @@ -65,13 +81,41 @@ export default class Client { return; } + email(emailId: string): IEmail | null { + if (this.state.session == null) return null; + return this.state.session.emails[emailId]; + } + + emailGet(accountId: string, emailId: string) { + if (this.jclient == null) return; + + this.jclient + .email_get({ + accountId: accountId, + ids: [emailId], + properties: ["mailboxIds", "subject"], + }) + .then((response) => { + console.log("Email response", response); + response.list.forEach((e) => { + if (this.state.session == null) return; + const existing = this.state.session.emails[e.id]; + if (existing !== undefined && existing.subject !== e.subject) { + console.error( + "Expectations violation: email ID is not unique within the server!", + ); + } + this.state.session.emails[e.id] = e; + this._triggerChange(); + }); + }) + .catch((x) => { + console.error("Failed to get email", emailId, x); + }); + } + emailList(accountId: string, mailboxId: string, ids: Array) { if (this.jclient == null) return; - /*this.jclient.email_get({ - accountId: accountId, - ids: [], - properties: ["threadId"] - });*/ this.jclient .email_query({ accountId: accountId, @@ -84,7 +128,7 @@ export default class Client { this._triggerChange(); }) .catch(() => { - console.error("OH NOES"); + console.error("Failed to get email list from mailbox", mailboxId); }); } @@ -145,6 +189,7 @@ export default class Client { { ...account, id: key.toString(), mailboxes: null }, ]), ), + emails: {}, }; if (!this.state.session) return; this._triggerChange(); diff --git a/src/client/types.tsx b/src/client/types.tsx index a4a6b3f..08b9cf9 100644 --- a/src/client/types.tsx +++ b/src/client/types.tsx @@ -5,13 +5,17 @@ export interface IMailbox extends client.IMailboxProperties { emails: Array | null; } +export interface IEmail extends client.IEmailProperties {} + export interface IAccount extends client.IAccount { id: string; mailboxes: Array | null; } export type AccountIdMap = { [accountId: string]: IAccount }; +export type EmailIdMap = { [emailId: string]: IEmail }; export interface ISession extends client.ISession { accounts: AccountIdMap; + emails: EmailIdMap; }