Split email cache into full content and stubs.
This also introduces status for in-flight requests to avoid perpetual, unnecessary loops of change-the-re-get-data.
This commit is contained in:
parent
fb53a7506f
commit
4e1922c5fa
|
@ -53,6 +53,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.state,
|
...this.state,
|
||||||
account: this.account(),
|
account: this.account(),
|
||||||
|
email: this.client.email(emailId),
|
||||||
location: {
|
location: {
|
||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
emailId: emailId,
|
emailId: emailId,
|
||||||
|
@ -107,6 +108,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
accounts: this.client.state.session
|
accounts: this.client.state.session
|
||||||
? this.client.state.session.accounts
|
? this.client.state.session.accounts
|
||||||
: {},
|
: {},
|
||||||
|
email: this.client.email(this.state.location.emailId),
|
||||||
mailbox: this.mailbox(),
|
mailbox: this.mailbox(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -130,6 +132,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
accounts={this.state.accounts}
|
accounts={this.state.accounts}
|
||||||
client={this.client}
|
client={this.client}
|
||||||
email={this.state.email}
|
email={this.state.email}
|
||||||
|
emailId={this.state.location.emailId}
|
||||||
mailbox={this.state.mailbox}
|
mailbox={this.state.mailbox}
|
||||||
/>
|
/>
|
||||||
<AuthModal
|
<AuthModal
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Row from "react-bootstrap/Row";
|
||||||
import Col from "react-bootstrap/Col";
|
import Col from "react-bootstrap/Col";
|
||||||
|
|
||||||
import AccountList from "./AccountList";
|
import AccountList from "./AccountList";
|
||||||
import EmailList from "./EmailList";
|
import EmailArea from "./EmailArea";
|
||||||
import MailboxList from "./MailboxList";
|
import MailboxList from "./MailboxList";
|
||||||
import Client from "./client/Client";
|
import Client from "./client/Client";
|
||||||
import { AccountIdMap, IAccount, IEmail, IMailbox } from "./client/types";
|
import { AccountIdMap, IAccount, IEmail, IMailbox } from "./client/types";
|
||||||
|
@ -14,6 +14,7 @@ type TopProps = {
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
client: Client;
|
client: Client;
|
||||||
email: IEmail | null;
|
email: IEmail | null;
|
||||||
|
emailId: string;
|
||||||
mailbox: IMailbox | null;
|
mailbox: IMailbox | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,9 +33,11 @@ const AppLayout: React.FC<TopProps> = (props) => {
|
||||||
<MailboxList account={props.account} client={props.client} />
|
<MailboxList account={props.account} client={props.client} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="11">
|
<Col lg="11">
|
||||||
<EmailList
|
<EmailArea
|
||||||
account={props.account}
|
account={props.account}
|
||||||
client={props.client}
|
client={props.client}
|
||||||
|
email={props.email}
|
||||||
|
emailId={props.emailId}
|
||||||
mailbox={props.mailbox}
|
mailbox={props.mailbox}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from "react";
|
||||||
|
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";
|
||||||
|
|
||||||
|
type EmailAreaProps = {
|
||||||
|
account: IAccount | null;
|
||||||
|
client: Client;
|
||||||
|
email: IEmail | null;
|
||||||
|
emailId: string;
|
||||||
|
mailbox: IMailbox | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmailArea: React.FC<EmailAreaProps> = (props) => {
|
||||||
|
if (props.emailId === "") {
|
||||||
|
return (
|
||||||
|
<EmailList
|
||||||
|
account={props.account}
|
||||||
|
client={props.client}
|
||||||
|
mailbox={props.mailbox}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Stack className="text-start">
|
||||||
|
<EmailList
|
||||||
|
account={props.account}
|
||||||
|
client={props.client}
|
||||||
|
mailbox={props.mailbox}
|
||||||
|
/>
|
||||||
|
<EmailContent
|
||||||
|
account={props.account}
|
||||||
|
client={props.client}
|
||||||
|
email={props.email}
|
||||||
|
emailId={props.emailId}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default EmailArea;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from "react";
|
||||||
|
import Placeholder from "react-bootstrap/Placeholder";
|
||||||
|
|
||||||
|
import Client from "./client/Client";
|
||||||
|
import { IAccount, IEmail } from "./client/types";
|
||||||
|
|
||||||
|
type EmailContentProps = {
|
||||||
|
account: IAccount | null;
|
||||||
|
client: Client | null;
|
||||||
|
email: IEmail | null;
|
||||||
|
emailId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmailContentState = {};
|
||||||
|
|
||||||
|
class EmailContent extends React.Component<
|
||||||
|
EmailContentProps,
|
||||||
|
EmailContentState
|
||||||
|
> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.ensureData();
|
||||||
|
}
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.ensureData();
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureData() {
|
||||||
|
if (this.props.account == null) return;
|
||||||
|
if (this.props.client == null) return;
|
||||||
|
this.props.client.ensureEmailContent(
|
||||||
|
this.props.account.id,
|
||||||
|
this.props.emailId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.props.email == null) {
|
||||||
|
return <Placeholder />;
|
||||||
|
} else {
|
||||||
|
return <p>{this.props.email.preview}</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default EmailContent;
|
|
@ -40,7 +40,7 @@ class EmailList extends React.Component<EmailListProps, EmailListState> {
|
||||||
account={this.props.account}
|
account={this.props.account}
|
||||||
client={this.props.client}
|
client={this.props.client}
|
||||||
emailId={e}
|
emailId={e}
|
||||||
email={this.props.client!.email(e)}
|
emailStub={this.props.client!.emailStub(e)}
|
||||||
key={e}
|
key={e}
|
||||||
mailbox={this.props.mailbox}
|
mailbox={this.props.mailbox}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,13 +2,13 @@ import React from "react";
|
||||||
import Placeholder from "react-bootstrap/Placeholder";
|
import Placeholder from "react-bootstrap/Placeholder";
|
||||||
|
|
||||||
import Client from "./client/Client";
|
import Client from "./client/Client";
|
||||||
import { IAccount, IEmail, IMailbox } from "./client/types";
|
import { IAccount, IEmailStub, IMailbox } from "./client/types";
|
||||||
|
|
||||||
type EmailSummaryProps = {
|
type EmailSummaryProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
client: Client | null;
|
client: Client | null;
|
||||||
email: IEmail | null;
|
|
||||||
emailId: string;
|
emailId: string;
|
||||||
|
emailStub: IEmailStub | null;
|
||||||
mailbox: IMailbox | null;
|
mailbox: IMailbox | null;
|
||||||
};
|
};
|
||||||
type EmailSummaryState = {};
|
type EmailSummaryState = {};
|
||||||
|
@ -27,7 +27,10 @@ class EmailSummary extends React.Component<
|
||||||
ensureData() {
|
ensureData() {
|
||||||
if (this.props.account == null) return;
|
if (this.props.account == null) return;
|
||||||
if (this.props.client == null) return;
|
if (this.props.client == null) return;
|
||||||
this.props.client.ensureEmailGet(this.props.account.id, this.props.emailId);
|
this.props.client.ensureEmailStub(
|
||||||
|
this.props.account.id,
|
||||||
|
this.props.emailId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -48,8 +51,8 @@ class EmailSummary extends React.Component<
|
||||||
return (
|
return (
|
||||||
<div className="p-2 border" key={this.props.emailId}>
|
<div className="p-2 border" key={this.props.emailId}>
|
||||||
<a className="btn" href={href}>
|
<a className="btn" href={href}>
|
||||||
{this.props.email != null
|
{this.props.emailStub != null
|
||||||
? this.props.email.subject
|
? this.props.emailStub.subject
|
||||||
: this.props.emailId}
|
: this.props.emailId}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import * as base64 from "base-64";
|
||||||
import * as jmapclient from "jmap-client-ts";
|
import * as jmapclient from "jmap-client-ts";
|
||||||
import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport";
|
import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport";
|
||||||
|
|
||||||
import { IAccount, IEmail, IMailbox, ISession } from "./types";
|
import { IAccount, IEmail, IEmailStub, IMailbox, ISession } from "./types";
|
||||||
|
|
||||||
type Callback = () => void;
|
type Callback = () => void;
|
||||||
|
|
||||||
|
@ -16,45 +16,31 @@ export interface IAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientState {
|
export interface ClientState {
|
||||||
|
inFlight: {
|
||||||
|
emailContents: Set<string>;
|
||||||
|
emailStubs: Set<string>;
|
||||||
|
mailboxes: Set<string>;
|
||||||
|
};
|
||||||
session: ISession | null;
|
session: ISession | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
// Get the currently active account
|
|
||||||
account(accountId: string): IAccount | null {
|
|
||||||
if (!(this.state.session && this.state.session.accounts)) return null;
|
|
||||||
return this.state.session.accounts[accountId];
|
|
||||||
}
|
|
||||||
// All objects which currently are listening for changes
|
// All objects which currently are listening for changes
|
||||||
callbacks: Array<Callback> = [];
|
callbacks: Array<Callback> = [];
|
||||||
jclient: jmapclient.Client | null = null;
|
jclient: jmapclient.Client | null = null;
|
||||||
state: ClientState = {
|
state: ClientState = {
|
||||||
|
inFlight: {
|
||||||
|
emailContents: new Set(),
|
||||||
|
emailStubs: new Set(),
|
||||||
|
mailboxes: new Set(),
|
||||||
|
},
|
||||||
session: null,
|
session: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange(f: Callback) {
|
// Get the currently active account
|
||||||
this.callbacks.push(f);
|
account(accountId: string): IAccount | null {
|
||||||
}
|
if (!(this.state.session && this.state.session.accounts)) return null;
|
||||||
|
return this.state.session.accounts[accountId];
|
||||||
// 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
|
// Make the request to get system metadata
|
||||||
|
@ -81,36 +67,110 @@ export default class Client {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure we have the full email content
|
||||||
|
ensureEmailContent(accountId: string, emailId: string) {
|
||||||
|
if (this.state.session == null) return;
|
||||||
|
const existing = this.state.session.emails[emailId];
|
||||||
|
if (existing != null) return;
|
||||||
|
this.emailGetContent(accountId, emailId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the email summary
|
||||||
|
ensureEmailStub(accountId: string, emailId: string) {
|
||||||
|
if (this.state.session == null) return;
|
||||||
|
const existing = this.state.session.emailStubs[emailId];
|
||||||
|
if (existing != null) return;
|
||||||
|
this.emailGetStub(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);
|
||||||
|
}
|
||||||
|
|
||||||
email(emailId: string): IEmail | null {
|
email(emailId: string): IEmail | null {
|
||||||
if (this.state.session == null) return null;
|
if (this.state.session == null) return null;
|
||||||
return this.state.session.emails[emailId];
|
return this.state.session.emails[emailId];
|
||||||
}
|
}
|
||||||
|
|
||||||
emailGet(accountId: string, emailId: string) {
|
emailGetContent(accountId: string, emailId: string) {
|
||||||
if (this.jclient == null) return;
|
if (this.jclient == null) return;
|
||||||
|
|
||||||
|
if (this.state.session == null) return;
|
||||||
|
if (this.state.inFlight.emailContents.has(emailId)) return;
|
||||||
|
this.state.inFlight.emailContents.add(emailId);
|
||||||
|
const msg = "Email get content";
|
||||||
this.jclient
|
this.jclient
|
||||||
.email_get({
|
.email_get({
|
||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
ids: [emailId],
|
ids: [emailId],
|
||||||
properties: ["mailboxIds", "subject"],
|
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log("Email response", response);
|
console.log(msg, "response", response);
|
||||||
response.list.forEach((e) => {
|
response.list.forEach((e) => {
|
||||||
if (this.state.session == null) return;
|
const existing = this.state.session!.emails[e.id];
|
||||||
const existing = this.state.session.emails[e.id];
|
|
||||||
if (existing !== undefined && existing.subject !== e.subject) {
|
if (existing !== undefined && existing.subject !== e.subject) {
|
||||||
console.error(
|
console.error(
|
||||||
"Expectations violation: email ID is not unique within the server!",
|
"Expectations violation: email ID is not unique within the server!",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.state.session.emails[e.id] = e;
|
this.state.session!.emails[e.id] = {
|
||||||
this._triggerChange("Email " + e.id);
|
...e,
|
||||||
|
id: e.id,
|
||||||
|
};
|
||||||
|
this._triggerChange(msg + e.id);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((x) => {
|
.catch((x) => {
|
||||||
console.error("Failed to get email", emailId, x);
|
console.error("Failed to get email content", emailId, x);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.state.inFlight.emailContents.delete(emailId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emailGetStub(accountId: string, emailId: string) {
|
||||||
|
if (this.jclient == null) return;
|
||||||
|
if (this.state.session == null) return;
|
||||||
|
if (this.state.inFlight.emailStubs.has(emailId)) return;
|
||||||
|
this.state.inFlight.emailStubs.add(emailId);
|
||||||
|
const msg = "Email get summary";
|
||||||
|
this.jclient
|
||||||
|
.email_get({
|
||||||
|
accountId: accountId,
|
||||||
|
ids: [emailId],
|
||||||
|
properties: ["subject"],
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
console.log(msg, "response", response);
|
||||||
|
response.list.forEach((e) => {
|
||||||
|
if (this.state.session == null) return;
|
||||||
|
const existing = this.state.session.emailStubs[e.id];
|
||||||
|
if (existing !== undefined && existing.subject !== e.subject) {
|
||||||
|
console.error(
|
||||||
|
"Expectations violation: email ID is not unique within the server!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.state.session.emailStubs[e.id] = {
|
||||||
|
id: e.id,
|
||||||
|
subject: e.subject,
|
||||||
|
};
|
||||||
|
this._triggerChange(msg + e.id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((x) => {
|
||||||
|
console.error("Failed to get email stub", emailId, x);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.state.inFlight.emailStubs.delete(emailId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +192,11 @@ export default class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailStub(emailId: string): IEmailStub | null {
|
||||||
|
if (this.state.session == null) return null;
|
||||||
|
return this.state.session.emailStubs[emailId];
|
||||||
|
}
|
||||||
|
|
||||||
mailbox(accountId: string, mailboxId: string): IMailbox | null {
|
mailbox(accountId: string, mailboxId: string): IMailbox | null {
|
||||||
if (this.state.session == null) return null;
|
if (this.state.session == null) return null;
|
||||||
const account = this.state.session.accounts[accountId];
|
const account = this.state.session.accounts[accountId];
|
||||||
|
@ -161,7 +226,6 @@ export default class Client {
|
||||||
mailboxes.push({
|
mailboxes.push({
|
||||||
...m,
|
...m,
|
||||||
emailIds: null,
|
emailIds: null,
|
||||||
emails: null,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
account.mailboxes = mailboxes;
|
account.mailboxes = mailboxes;
|
||||||
|
@ -169,6 +233,10 @@ export default class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChange(f: Callback) {
|
||||||
|
this.callbacks.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
_triggerChange(msg: string) {
|
_triggerChange(msg: string) {
|
||||||
console.log("Client change", msg);
|
console.log("Client change", msg);
|
||||||
this.callbacks.forEach((c) => {
|
this.callbacks.forEach((c) => {
|
||||||
|
@ -191,6 +259,7 @@ export default class Client {
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
emails: {},
|
emails: {},
|
||||||
|
emailStubs: {},
|
||||||
};
|
};
|
||||||
if (!this.state.session) return;
|
if (!this.state.session) return;
|
||||||
this._triggerChange("Session");
|
this._triggerChange("Session");
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import client from "jmap-client-ts/lib/types";
|
import client from "jmap-client-ts/lib/types";
|
||||||
|
|
||||||
|
export interface IEmailStub {
|
||||||
|
id: string;
|
||||||
|
subject: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IMailbox extends client.IMailboxProperties {
|
export interface IMailbox extends client.IMailboxProperties {
|
||||||
emailIds: Array<string> | null;
|
emailIds: Array<string> | null;
|
||||||
emails: Array<client.IEmailProperties> | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEmail extends client.IEmailProperties {}
|
export interface IEmail extends client.IEmailProperties {}
|
||||||
|
@ -13,9 +17,11 @@ export interface IAccount extends client.IAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccountIdMap = { [accountId: string]: IAccount };
|
export type AccountIdMap = { [accountId: string]: IAccount };
|
||||||
|
export type EmailStubIdMap = { [emailId: string]: IEmailStub };
|
||||||
export type EmailIdMap = { [emailId: string]: IEmail };
|
export type EmailIdMap = { [emailId: string]: IEmail };
|
||||||
|
|
||||||
export interface ISession extends client.ISession {
|
export interface ISession extends client.ISession {
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
emails: EmailIdMap;
|
emails: EmailIdMap;
|
||||||
|
emailStubs: EmailStubIdMap;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue