Compare commits
3 Commits
1d9a184140
...
171a8aa1c0
Author | SHA1 | Date |
---|---|---|
|
171a8aa1c0 | |
|
e93cbbaab2 | |
|
c686f5c8d9 |
|
@ -4,7 +4,7 @@ import Stack from "react-bootstrap/Stack";
|
||||||
import { IAccount, IEmail, IMailbox } from "./client/types";
|
import { IAccount, IEmail, IMailbox } from "./client/types";
|
||||||
import Client from "./client/Client";
|
import Client from "./client/Client";
|
||||||
import EmailContent from "./EmailContent";
|
import EmailContent from "./EmailContent";
|
||||||
import EmailList from "./EmailList";
|
import Mailbox from "./Mailbox";
|
||||||
|
|
||||||
type EmailAreaProps = {
|
type EmailAreaProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
|
@ -17,7 +17,7 @@ type EmailAreaProps = {
|
||||||
const EmailArea: React.FC<EmailAreaProps> = (props) => {
|
const EmailArea: React.FC<EmailAreaProps> = (props) => {
|
||||||
if (props.emailId === "") {
|
if (props.emailId === "") {
|
||||||
return (
|
return (
|
||||||
<EmailList
|
<Mailbox
|
||||||
account={props.account}
|
account={props.account}
|
||||||
client={props.client}
|
client={props.client}
|
||||||
mailbox={props.mailbox}
|
mailbox={props.mailbox}
|
||||||
|
@ -26,7 +26,7 @@ const EmailArea: React.FC<EmailAreaProps> = (props) => {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Stack className="text-start">
|
<Stack className="text-start">
|
||||||
<EmailList
|
<Mailbox
|
||||||
account={props.account}
|
account={props.account}
|
||||||
client={props.client}
|
client={props.client}
|
||||||
mailbox={props.mailbox}
|
mailbox={props.mailbox}
|
||||||
|
|
|
@ -1,19 +1,37 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { IAccount, IMailbox } from "./client/types";
|
||||||
|
import Client from "./client/Client";
|
||||||
|
import EmailList from "./EmailList";
|
||||||
|
|
||||||
type MailboxProps = {
|
type MailboxProps = {
|
||||||
accountId: string;
|
account: IAccount | null;
|
||||||
id: string;
|
client: Client;
|
||||||
name: string;
|
mailbox: IMailbox | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Mailbox: React.FC<MailboxProps> = ({ accountId, id, name }) => {
|
type MailboxState = {};
|
||||||
const href = "#" + accountId + "/" + id;
|
|
||||||
|
|
||||||
return (
|
class Mailbox extends React.Component<MailboxProps, MailboxState> {
|
||||||
<a href={href} className="p-2" key={id}>
|
componentDidUpdate() {
|
||||||
{name}
|
if (this.props.account == null) return;
|
||||||
</a>
|
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 (
|
||||||
|
<EmailList
|
||||||
|
account={this.props.account}
|
||||||
|
client={this.props.client}
|
||||||
|
mailbox={this.props.mailbox}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Mailbox;
|
export default Mailbox;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Stack from "react-bootstrap/Stack";
|
||||||
|
|
||||||
import Client from "./client/Client";
|
import Client from "./client/Client";
|
||||||
import { IAccount } from "./client/types";
|
import { IAccount } from "./client/types";
|
||||||
import Mailbox from "./Mailbox";
|
import MailboxSummary from "./MailboxSummary";
|
||||||
|
|
||||||
type MailboxListProps = {
|
type MailboxListProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
|
@ -25,8 +25,8 @@ class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
|
||||||
<Stack />
|
<Stack />
|
||||||
) : (
|
) : (
|
||||||
<Stack>
|
<Stack>
|
||||||
{this.props.account.mailboxes.map((m) => (
|
{Object.values(this.props.account.mailboxes).map((m) => (
|
||||||
<Mailbox
|
<MailboxSummary
|
||||||
accountId={this.props.account!.id}
|
accountId={this.props.account!.id}
|
||||||
id={m.id}
|
id={m.id}
|
||||||
key={m.id}
|
key={m.id}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type MailboxProps = {
|
||||||
|
accountId: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mailbox: React.FC<MailboxProps> = ({ accountId, id, name }) => {
|
||||||
|
const href = "#" + accountId + "/" + id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={href} className="p-2" key={id}>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Mailbox;
|
|
@ -13,6 +13,7 @@ import {
|
||||||
IEmailStub,
|
IEmailStub,
|
||||||
IMailbox,
|
IMailbox,
|
||||||
ISession,
|
ISession,
|
||||||
|
MailboxIdMap,
|
||||||
MailboxRole,
|
MailboxRole,
|
||||||
PushMessage,
|
PushMessage,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
@ -25,6 +26,7 @@ export interface IAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientState {
|
export interface ClientState {
|
||||||
|
emailState: string | null;
|
||||||
inFlight: {
|
inFlight: {
|
||||||
emailContents: Set<string>;
|
emailContents: Set<string>;
|
||||||
emailStubs: Set<string>;
|
emailStubs: Set<string>;
|
||||||
|
@ -33,7 +35,9 @@ export interface ClientState {
|
||||||
mailboxes: {
|
mailboxes: {
|
||||||
trash: IMailbox;
|
trash: IMailbox;
|
||||||
} | null;
|
} | null;
|
||||||
|
mailboxState: string | null;
|
||||||
session: ISession | null;
|
session: ISession | null;
|
||||||
|
threadState: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Account implements IAccount {
|
class Account implements IAccount {
|
||||||
|
@ -42,7 +46,7 @@ class Account implements IAccount {
|
||||||
accountCapabilities: { [key: string]: jmaptypes.IMailCapabilities };
|
accountCapabilities: { [key: string]: jmaptypes.IMailCapabilities };
|
||||||
isPersonal: boolean;
|
isPersonal: boolean;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
mailboxes: Array<IMailbox> | null;
|
mailboxes: MailboxIdMap | null;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
constructor(id: string, props: jmaptypes.IAccount) {
|
constructor(id: string, props: jmaptypes.IAccount) {
|
||||||
|
@ -55,10 +59,9 @@ class Account implements IAccount {
|
||||||
}
|
}
|
||||||
mailboxByRole(role: MailboxRole): IMailbox | null {
|
mailboxByRole(role: MailboxRole): IMailbox | null {
|
||||||
if (this.mailboxes === null) return null;
|
if (this.mailboxes === null) return null;
|
||||||
for (let i = 0; i < this.mailboxes.length; i++) {
|
for (const mailbox of Object.values(this.mailboxes)) {
|
||||||
const m = this.mailboxes[i];
|
if (mailbox.role === role) {
|
||||||
if (m.role === role) {
|
return mailbox;
|
||||||
return m;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -69,13 +72,16 @@ export default class Client {
|
||||||
callbacks: Array<Callback> = [];
|
callbacks: Array<Callback> = [];
|
||||||
jclient: jmapclient.Client | null = null;
|
jclient: jmapclient.Client | null = null;
|
||||||
state: ClientState = {
|
state: ClientState = {
|
||||||
|
emailState: null,
|
||||||
inFlight: {
|
inFlight: {
|
||||||
emailContents: new Set(),
|
emailContents: new Set(),
|
||||||
emailStubs: new Set(),
|
emailStubs: new Set(),
|
||||||
mailboxes: new Set(),
|
mailboxes: new Set(),
|
||||||
},
|
},
|
||||||
mailboxes: null,
|
mailboxes: null,
|
||||||
|
mailboxState: null,
|
||||||
session: null,
|
session: null,
|
||||||
|
threadState: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the currently active account
|
// Get the currently active account
|
||||||
|
@ -174,6 +180,14 @@ export default class Client {
|
||||||
this.doLogin(auth);
|
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 {
|
email(emailId: string): IEmail | null {
|
||||||
if (this.state.session == null) return null;
|
if (this.state.session == null) return null;
|
||||||
const result = this.state.session.emails[emailId];
|
const result = this.state.session.emails[emailId];
|
||||||
|
@ -247,8 +261,10 @@ export default class Client {
|
||||||
receivedAt: e.receivedAt,
|
receivedAt: e.receivedAt,
|
||||||
subject: e.subject,
|
subject: e.subject,
|
||||||
};
|
};
|
||||||
this._triggerChange(msg + e.id);
|
|
||||||
});
|
});
|
||||||
|
this._triggerChange(msg);
|
||||||
|
//this.state.session.state = response.sessionState;
|
||||||
|
this.state.emailState = response.state;
|
||||||
})
|
})
|
||||||
.catch((x) => {
|
.catch((x) => {
|
||||||
console.error("Failed to get email stub", emailId, x);
|
console.error("Failed to get email stub", emailId, x);
|
||||||
|
@ -295,12 +311,39 @@ export default class Client {
|
||||||
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];
|
||||||
if (account.mailboxes == null) return null;
|
if (account.mailboxes == null) return null;
|
||||||
for (let i = 0; i < account.mailboxes.length; i++) {
|
const mailbox = account.mailboxes[mailboxId];
|
||||||
if (account.mailboxes[i].id === mailboxId) {
|
return mailbox;
|
||||||
return account.mailboxes[i];
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
return null;
|
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<string>) {
|
mailboxList(accountId: string, ids: Array<string>) {
|
||||||
if (this.jclient == null) return;
|
if (this.jclient == null) return;
|
||||||
|
@ -315,14 +358,18 @@ export default class Client {
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (this.state.session == null) return;
|
if (this.state.session == null) return;
|
||||||
const account = this.state.session.accounts[response.accountId];
|
const account = this.state.session.accounts[response.accountId];
|
||||||
const mailboxes: Array<IMailbox> = [];
|
const mailboxes: MailboxIdMap = {};
|
||||||
response.list.forEach((m) => {
|
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,
|
...m,
|
||||||
emailIds: null,
|
emailIds: existing === null ? null : existing.emailIds,
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
account.mailboxes = mailboxes;
|
account.mailboxes = mailboxes;
|
||||||
|
this.state.mailboxState = response.state;
|
||||||
this._triggerChange("Mailboxes " + accountId);
|
this._triggerChange("Mailboxes " + accountId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -337,13 +384,70 @@ export default class Client {
|
||||||
c();
|
c();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
_onChangedEmail(accountId: string, state: string) {
|
||||||
|
console.log("Email changed", state);
|
||||||
|
if (this.jclient === null) return;
|
||||||
|
if (this.state.session === null) return;
|
||||||
|
const account = this.account(accountId);
|
||||||
|
if (account === null) return;
|
||||||
|
if (this.state.emailState === null) return;
|
||||||
|
const args = {
|
||||||
|
accountId: accountId,
|
||||||
|
sinceState: this.state.emailState,
|
||||||
|
};
|
||||||
|
this.jclient.email_changes(args).then((r) => {
|
||||||
|
console.log("Handle email changes ", r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_onChangedMailbox(accountId: string, state: string) {
|
||||||
|
console.log("Mailbox changed", state);
|
||||||
|
if (this.jclient === null) return;
|
||||||
|
if (this.state.session === null) return;
|
||||||
|
const account = this.account(accountId);
|
||||||
|
if (account === null) return;
|
||||||
|
if (this.state.mailboxState === null) return;
|
||||||
|
const args = {
|
||||||
|
accountId: accountId,
|
||||||
|
sinceState: this.state.mailboxState,
|
||||||
|
};
|
||||||
|
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) {
|
||||||
|
console.log("Thread changed", state);
|
||||||
|
if (this.jclient === null) return;
|
||||||
|
if (this.state.session === null) return;
|
||||||
|
const account = this.account(accountId);
|
||||||
|
if (account === null) return;
|
||||||
|
if (this.state.threadState === null) return;
|
||||||
|
const args = {
|
||||||
|
accountId: accountId,
|
||||||
|
sinceState: this.state.threadState,
|
||||||
|
};
|
||||||
|
this.jclient.thread_changes(args).then((r) => {
|
||||||
|
console.log("Handle thread changes ", r);
|
||||||
|
});
|
||||||
|
}
|
||||||
_onSession() {
|
_onSession() {
|
||||||
console.log("Session received");
|
|
||||||
|
|
||||||
// For the type checker
|
// For the type checker
|
||||||
if (!this.jclient) return;
|
if (!this.jclient) return;
|
||||||
|
|
||||||
const session = this.jclient.getSession();
|
const session = this.jclient.getSession();
|
||||||
|
console.log("Session received: ", session);
|
||||||
// Subscribe to server-pushed events
|
// Subscribe to server-pushed events
|
||||||
if (session.eventSourceUrl) {
|
if (session.eventSourceUrl) {
|
||||||
this._subscribeToEventSource(session.eventSourceUrl);
|
this._subscribeToEventSource(session.eventSourceUrl);
|
||||||
|
@ -358,6 +462,7 @@ export default class Client {
|
||||||
),
|
),
|
||||||
emails: {},
|
emails: {},
|
||||||
emailStubs: {},
|
emailStubs: {},
|
||||||
|
mailboxes: {},
|
||||||
};
|
};
|
||||||
if (!this.state.session) return;
|
if (!this.state.session) return;
|
||||||
this._triggerChange("Session");
|
this._triggerChange("Session");
|
||||||
|
@ -370,8 +475,32 @@ export default class Client {
|
||||||
.replace("{types}", "*")
|
.replace("{types}", "*")
|
||||||
.replace("{closeafter}", "no")
|
.replace("{closeafter}", "no")
|
||||||
.replace("{ping}", "60");
|
.replace("{ping}", "60");
|
||||||
this.jclient.subscribeToEvents(eventSourceUrl, (e) => {
|
this.jclient.subscribeToEvents(
|
||||||
console.log("Got an event!", e);
|
eventSourceUrl,
|
||||||
});
|
(type: string, message: PushMessage) => {
|
||||||
|
if (type === "ping") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === "state") {
|
||||||
|
console.log("Got an event!", type, message);
|
||||||
|
const stateChange = message as jmaptypes.IStateChange;
|
||||||
|
for (const [accountId, changes] of Object.entries(
|
||||||
|
stateChange.changed,
|
||||||
|
)) {
|
||||||
|
for (const [changeType, state] of Object.entries(changes)) {
|
||||||
|
if (changeType === "Email") {
|
||||||
|
this._onChangedEmail(accountId, state);
|
||||||
|
} else if (changeType === "Mailbox") {
|
||||||
|
this._onChangedMailbox(accountId, state);
|
||||||
|
} else if (changeType === "Thread") {
|
||||||
|
this._onChangedThread(accountId, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Not sure what to do with event", type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2ef5f5b7fa0a22a499bd32831ac24622f17e10e6
|
Subproject commit ee2af71a8e764c1a8153a7dcd71f4085f970243f
|
|
@ -1,10 +1,10 @@
|
||||||
import * as client from "./jmap-client-ts/src/types";
|
import * as client from "./jmap-client-ts/src/types";
|
||||||
|
|
||||||
export type MailboxIdMap = { [mailboxId: string]: boolean };
|
export type MailboxIdMapPresence = { [mailboxId: string]: boolean };
|
||||||
export interface IEmailStub {
|
export interface IEmailStub {
|
||||||
from: Array<client.IEmailAddress>;
|
from: Array<client.IEmailAddress>;
|
||||||
id: string;
|
id: string;
|
||||||
mailboxIds: MailboxIdMap;
|
mailboxIds: MailboxIdMapPresence;
|
||||||
receivedAt: string;
|
receivedAt: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ export interface IEmailStub {
|
||||||
export interface IMailbox extends client.IMailboxProperties {
|
export interface IMailbox extends client.IMailboxProperties {
|
||||||
emailIds: Array<string> | null;
|
emailIds: Array<string> | null;
|
||||||
}
|
}
|
||||||
|
export type MailboxIdMap = { [mailboxId: string]: IMailbox };
|
||||||
|
|
||||||
export interface IEmail extends client.IEmailProperties {
|
export interface IEmail extends client.IEmailProperties {
|
||||||
//sentAt: IutcDate|null,
|
//sentAt: IutcDate|null,
|
||||||
|
@ -19,7 +20,7 @@ export interface IEmail extends client.IEmailProperties {
|
||||||
|
|
||||||
export interface IAccount extends client.IAccount {
|
export interface IAccount extends client.IAccount {
|
||||||
id: string;
|
id: string;
|
||||||
mailboxes: Array<IMailbox> | null;
|
mailboxes: MailboxIdMap | null;
|
||||||
|
|
||||||
mailboxByRole(role: MailboxRole): IMailbox | null;
|
mailboxByRole(role: MailboxRole): IMailbox | null;
|
||||||
}
|
}
|
||||||
|
@ -42,4 +43,5 @@ export interface ISession extends client.ISession {
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
emails: EmailIdMap;
|
emails: EmailIdMap;
|
||||||
emailStubs: EmailStubIdMap;
|
emailStubs: EmailStubIdMap;
|
||||||
|
mailboxes: MailboxIdMap;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue