diff --git a/src/EmailArea.tsx b/src/EmailArea.tsx index 3d3c049..b9c6254 100644 --- a/src/EmailArea.tsx +++ b/src/EmailArea.tsx @@ -4,7 +4,7 @@ import Stack from "react-bootstrap/Stack"; import { IAccount, IEmail, IMailbox } from "./client/types"; import Client from "./client/Client"; import EmailContent from "./EmailContent"; -import EmailList from "./EmailList"; +import Mailbox from "./Mailbox"; type EmailAreaProps = { account: IAccount | null; @@ -17,7 +17,7 @@ type EmailAreaProps = { const EmailArea: React.FC = (props) => { if (props.emailId === "") { return ( - = (props) => { } else { return ( - = ({ accountId, id, name }) => { - const href = "#" + accountId + "/" + id; +type MailboxState = {}; - return ( - - {name} - - ); -}; +class Mailbox extends React.Component { + componentDidUpdate() { + if (this.props.account == null) return; + if (this.props.client == null) return; + if (this.props.mailbox == null) return; + this.props.client.ensureMailbox( + this.props.account.id, + this.props.mailbox.id, + ); + } + + render() { + return ( + + ); + } +} export default Mailbox; diff --git a/src/MailboxList.tsx b/src/MailboxList.tsx index 3bbe6a6..8d8e847 100644 --- a/src/MailboxList.tsx +++ b/src/MailboxList.tsx @@ -3,7 +3,7 @@ import Stack from "react-bootstrap/Stack"; import Client from "./client/Client"; import { IAccount } from "./client/types"; -import Mailbox from "./Mailbox"; +import MailboxSummary from "./MailboxSummary"; type MailboxListProps = { account: IAccount | null; @@ -25,8 +25,8 @@ class MailboxList extends React.Component { ) : ( - {this.props.account.mailboxes.map((m) => ( - ( + = ({ accountId, id, name }) => { + const href = "#" + accountId + "/" + id; + + return ( + + {name} + + ); +}; + +export default Mailbox; diff --git a/src/client/Client.tsx b/src/client/Client.tsx index 2adc0a5..9f35f35 100644 --- a/src/client/Client.tsx +++ b/src/client/Client.tsx @@ -13,6 +13,7 @@ import { IEmailStub, IMailbox, ISession, + MailboxIdMap, MailboxRole, PushMessage, } from "./types"; @@ -45,7 +46,7 @@ class Account implements IAccount { accountCapabilities: { [key: string]: jmaptypes.IMailCapabilities }; isPersonal: boolean; isReadOnly: boolean; - mailboxes: Array | null; + mailboxes: MailboxIdMap | null; name: string; constructor(id: string, props: jmaptypes.IAccount) { @@ -58,10 +59,9 @@ class Account implements IAccount { } mailboxByRole(role: MailboxRole): IMailbox | null { if (this.mailboxes === null) return null; - for (let i = 0; i < this.mailboxes.length; i++) { - const m = this.mailboxes[i]; - if (m.role === role) { - return m; + for (const mailbox of Object.values(this.mailboxes)) { + if (mailbox.role === role) { + return mailbox; } } return null; @@ -180,6 +180,14 @@ export default class Client { this.doLogin(auth); } + // Ensure we have the full email content + ensureMailbox(accountId: string, mailboxId: string) { + if (this.state.session == null) return; + const existing = this.state.session.mailboxes[mailboxId]; + if (existing != null) return; + this.mailboxList(accountId, [mailboxId]); + } + email(emailId: string): IEmail | null { if (this.state.session == null) return null; const result = this.state.session.emails[emailId]; @@ -303,12 +311,39 @@ export default class Client { if (this.state.session == null) return null; const account = this.state.session.accounts[accountId]; if (account.mailboxes == null) return null; - for (let i = 0; i < account.mailboxes.length; i++) { - if (account.mailboxes[i].id === mailboxId) { - return account.mailboxes[i]; - } - } - return null; + const mailbox = account.mailboxes[mailboxId]; + return mailbox; + } + mailboxGet(accountId: string, id: string) { + if (this.jclient == null) return; + if (this.state.session == null) return; + // TODO: do this in a single request with 2 queries when the client can handle it. + this.jclient + .mailbox_get({ + accountId: accountId, + ids: [id], + }) + .then((response) => { + if (this.state.session == null) return; + const account = this.state.session.accounts[response.accountId]; + if (response.list.length !== 1) { + console.error( + "Should only get 1 mailbox in mailboxGet. Got ", + response.list, + ); + return; + } + const m = response.list[0]; + if (account.mailboxes === null) { + account.mailboxes = {}; + } + account.mailboxes[m.id] = { + ...m, + emailIds: null, + }; + this.state.mailboxState = response.state; + this.emailList(accountId, id, []); + }); } mailboxList(accountId: string, ids: Array) { if (this.jclient == null) return; @@ -323,14 +358,18 @@ export default class Client { .then((response) => { if (this.state.session == null) return; const account = this.state.session.accounts[response.accountId]; - const mailboxes: Array = []; + const mailboxes: MailboxIdMap = {}; response.list.forEach((m) => { - mailboxes.push({ + // If we already have a mailbox with emails then don't throw that data away + const existing = + account.mailboxes === null ? null : account.mailboxes[m.id]; + mailboxes[m.id] = { ...m, - emailIds: null, - }); + emailIds: existing === null ? null : existing.emailIds, + }; }); account.mailboxes = mailboxes; + this.state.mailboxState = response.state; this._triggerChange("Mailboxes " + accountId); }); } @@ -369,10 +408,23 @@ export default class Client { if (this.state.mailboxState === null) return; const args = { accountId: accountId, - sinceState: this.state.session.state, + sinceState: this.state.mailboxState, }; - this.jclient.mailbox_changes(args).then((r) => { - console.log("Handle mailbox changes ", r); + this.jclient.mailbox_changes(args).then((response) => { + console.log("Handle mailbox changes ", response); + for (let i = 0; i < response.created.length; i++) { + const mailboxId = response.created[i]; + this.mailboxGet(accountId, mailboxId); + } + for (let i = 0; i < response.updated.length; i++) { + const mailboxId = response.updated[i]; + this.mailboxGet(accountId, mailboxId); + } + if (this.state.session === null) return; + for (let i = 0; i < response.destroyed.length; i++) { + const mailboxId = response.destroyed[i]; + delete this.state.session.mailboxes[mailboxId]; + } }); } _onChangedThread(accountId: string, state: string) { @@ -384,7 +436,7 @@ export default class Client { if (this.state.threadState === null) return; const args = { accountId: accountId, - sinceState: this.state.session.state, + sinceState: this.state.threadState, }; this.jclient.thread_changes(args).then((r) => { console.log("Handle thread changes ", r); @@ -410,6 +462,7 @@ export default class Client { ), emails: {}, emailStubs: {}, + mailboxes: {}, }; if (!this.state.session) return; this._triggerChange("Session"); diff --git a/src/client/types.tsx b/src/client/types.tsx index a251cd0..1b0e56f 100644 --- a/src/client/types.tsx +++ b/src/client/types.tsx @@ -1,10 +1,10 @@ import * as client from "./jmap-client-ts/src/types"; -export type MailboxIdMap = { [mailboxId: string]: boolean }; +export type MailboxIdMapPresence = { [mailboxId: string]: boolean }; export interface IEmailStub { from: Array; id: string; - mailboxIds: MailboxIdMap; + mailboxIds: MailboxIdMapPresence; receivedAt: string; subject: string; } @@ -12,6 +12,7 @@ export interface IEmailStub { export interface IMailbox extends client.IMailboxProperties { emailIds: Array | null; } +export type MailboxIdMap = { [mailboxId: string]: IMailbox }; export interface IEmail extends client.IEmailProperties { //sentAt: IutcDate|null, @@ -19,7 +20,7 @@ export interface IEmail extends client.IEmailProperties { export interface IAccount extends client.IAccount { id: string; - mailboxes: Array | null; + mailboxes: MailboxIdMap | null; mailboxByRole(role: MailboxRole): IMailbox | null; } @@ -42,4 +43,5 @@ export interface ISession extends client.ISession { accounts: AccountIdMap; emails: EmailIdMap; emailStubs: EmailStubIdMap; + mailboxes: MailboxIdMap; }