Compare commits

...

4 Commits

Author SHA1 Message Date
Eli Ribble 1e9dae15f1 Add button to move an email to trash.
This is the first time I'm modifying data instead of just displaying it.
And this commit is a mess, it's all over the place with duplicating
types and breaking my class layers.

But it works, technically, so, whatever, checkpoint!

I need to totally start reworking the base client library as I'm not
happy with it at all. Also, it turns out that we have very little type
protection on the "set" methods. I had a totally improper signature for
about an hour that led to useless debugging. Reading the standard, which
is excellent, helped me get sorted out, but they type checker should be
helping me.

Additionally, I should be creating this Account class type within the
client.
2024-09-03 12:03:04 -07:00
Eli Ribble dfca32eb36 Initial integration of event stream.
This doesn't do much yet but log the events I'm getting until I
understand them better.
2024-09-03 10:11:02 -07:00
Eli Ribble 5a36e46da5 Add support for react-bootstrap-icons. 2024-09-03 10:09:44 -07:00
Eli Ribble a412b6a0a4 Display the 'sentAt' information. 2024-09-03 08:36:28 -07:00
7 changed files with 153 additions and 6 deletions

12
package-lock.json generated
View File

@ -21,6 +21,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sass": "^1.77.8", "sass": "^1.77.8",
@ -14746,6 +14747,17 @@
} }
} }
}, },
"node_modules/react-bootstrap-icons": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz",
"integrity": "sha512-lnkOpNEZ/Zr7mNxvjA9efuarCPSgtOuGA55XiRj7ASJnBjb1wEAdtJOd2Aiv9t07r7FLI1IgyZPg9P6jqWD/IA==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">=16.8.6"
}
},
"node_modules/react-dev-utils": { "node_modules/react-dev-utils": {
"version": "12.0.1", "version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",

View File

@ -16,6 +16,7 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sass": "^1.77.8", "sass": "^1.77.8",

View File

@ -88,6 +88,10 @@ class EmailContent extends React.Component<
Received Received
</Form.Label> </Form.Label>
<Col sm={10}>{email.receivedAt}</Col> <Col sm={10}>{email.receivedAt}</Col>
<Form.Label column sm={2}>
Sent
</Form.Label>
<Col sm={10}>{email.sentAt}</Col>
</Form.Group> </Form.Group>
</Form> </Form>
</Row> </Row>

View File

@ -1,3 +1,7 @@
import { Trash } from "react-bootstrap-icons";
import Button from "react-bootstrap/Button";
import ButtonGroup from "react-bootstrap/ButtonGroup";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Placeholder from "react-bootstrap/Placeholder"; import Placeholder from "react-bootstrap/Placeholder";
import React from "react"; import React from "react";
@ -55,6 +59,21 @@ class EmailSummary extends React.Component<
stub.subject} stub.subject}
</span> </span>
</a> </a>
<ButtonToolbar>
<ButtonGroup className="me-2">
<Button
onClick={() => {
this.props.client.emailMoveTrash(
this.props.account,
this.props.emailId,
);
}}
>
<Trash />
</Button>
<Button>2</Button>
</ButtonGroup>
</ButtonToolbar>
</div> </div>
); );
} }

View File

@ -3,10 +3,19 @@
* None of the dependencies should leak, in types or otherwise * None of the dependencies should leak, in types or otherwise
*/ */
import * as base64 from "base-64"; import * as base64 from "base-64";
import * as jmaptypes from "./jmap-client-ts/src/types";
import * as jmapclient from "./jmap-client-ts/src"; import * as jmapclient from "./jmap-client-ts/src";
import { FetchTransport } from "./jmap-client-ts/src/utils/fetch-transport"; import { FetchTransport } from "./jmap-client-ts/src/utils/fetch-transport";
import { IAccount, IEmail, IEmailStub, IMailbox, ISession } from "./types"; import {
IAccount,
IEmail,
IEmailStub,
IMailbox,
ISession,
MailboxRole,
PushMessage,
} from "./types";
type Callback = () => void; type Callback = () => void;
@ -21,9 +30,40 @@ export interface ClientState {
emailStubs: Set<string>; emailStubs: Set<string>;
mailboxes: Set<string>; mailboxes: Set<string>;
}; };
mailboxes: {
trash: IMailbox;
} | null;
session: ISession | null; session: ISession | null;
} }
class Account implements IAccount {
id: string;
accountCapabilities: { [key: string]: jmaptypes.IMailCapabilities };
isPersonal: boolean;
isReadOnly: boolean;
mailboxes: Array<IMailbox> | null;
name: string;
constructor(id: string, props: jmaptypes.IAccount) {
this.id = id;
this.accountCapabilities = props.accountCapabilities;
this.isPersonal = props.isPersonal;
this.isReadOnly = props.isReadOnly;
this.mailboxes = null;
this.name = props.name;
}
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;
}
}
return null;
}
}
export default class Client { export default class Client {
// All objects which currently are listening for changes // All objects which currently are listening for changes
callbacks: Array<Callback> = []; callbacks: Array<Callback> = [];
@ -34,6 +74,7 @@ export default class Client {
emailStubs: new Set(), emailStubs: new Set(),
mailboxes: new Set(), mailboxes: new Set(),
}, },
mailboxes: null,
session: null, session: null,
}; };
@ -67,6 +108,43 @@ export default class Client {
return; return;
} }
emailMoveTrash(account: IAccount, emailId: string) {
if (this.jclient === null) return;
console.log("Trashing", emailId);
const email = this.emailStub(emailId);
if (email === null) return;
const trashMailbox = account.mailboxByRole("trash");
if (trashMailbox === null) {
console.error(
"Cannot trash ",
emailId,
" because ",
account.id,
" does not have a 'trash' mailbox",
);
return;
}
let mailboxIds = Object.keys(email.mailboxIds).reduce(
(acc, key) => {
acc[key as keyof typeof email.mailboxIds] = false;
return acc;
},
{} as Record<keyof typeof email.mailboxIds, boolean>,
);
mailboxIds[trashMailbox.id] = true;
const props = {
accountId: account.id,
update: {
[email.id]: {
mailboxIds: mailboxIds,
},
},
};
this.jclient.email_set(props).then((response) => {
console.log("Trashed", emailId);
});
}
// Ensure we have the full email content // Ensure we have the full email content
ensureEmailContent(accountId: string, emailId: string) { ensureEmailContent(accountId: string, emailId: string) {
if (this.state.session == null) return; if (this.state.session == null) return;
@ -98,7 +176,9 @@ export default class Client {
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]; const result = this.state.session.emails[emailId];
if (result === undefined) return null;
return result;
} }
emailGetContent(accountId: string, emailId: string) { emailGetContent(accountId: string, emailId: string) {
@ -148,7 +228,7 @@ export default class Client {
.email_get({ .email_get({
accountId: accountId, accountId: accountId,
ids: [emailId], ids: [emailId],
properties: ["from", "receivedAt", "subject"], properties: ["from", "mailboxIds", "receivedAt", "subject"],
}) })
.then((response) => { .then((response) => {
console.log(msg, "response", response); console.log(msg, "response", response);
@ -163,6 +243,7 @@ export default class Client {
this.state.session.emailStubs[e.id] = { this.state.session.emailStubs[e.id] = {
from: e.from, from: e.from,
id: e.id, id: e.id,
mailboxIds: e.mailboxIds,
receivedAt: e.receivedAt, receivedAt: e.receivedAt,
subject: e.subject, subject: e.subject,
}; };
@ -263,12 +344,16 @@ export default class Client {
if (!this.jclient) return; if (!this.jclient) return;
const session = this.jclient.getSession(); const session = this.jclient.getSession();
// Subscribe to server-pushed events
if (session.eventSourceUrl) {
this._subscribeToEventSource(session.eventSourceUrl);
}
this.state.session = { this.state.session = {
...session, ...session,
accounts: Object.fromEntries( accounts: Object.fromEntries(
Object.entries(session.accounts).map(([key, account]) => [ Object.entries(session.accounts).map(([key, account]) => [
key, key,
{ ...account, id: key.toString(), mailboxes: null }, new Account(key.toString(), account),
]), ]),
), ),
emails: {}, emails: {},
@ -277,4 +362,16 @@ export default class Client {
if (!this.state.session) return; if (!this.state.session) return;
this._triggerChange("Session"); this._triggerChange("Session");
} }
_subscribeToEventSource(url: string) {
// For typechecker
if (this.jclient === null) return;
const eventSourceUrl = url
.replace("{types}", "*")
.replace("{closeafter}", "no")
.replace("{ping}", "60");
this.jclient.subscribeToEvents(eventSourceUrl, (e) => {
console.log("Got an event!", e);
});
}
} }

@ -1 +1 @@
Subproject commit fdab37996a4709b13962b67d1aa49e85a8305755 Subproject commit 2ef5f5b7fa0a22a499bd32831ac24622f17e10e6

View File

@ -1,8 +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 interface IEmailStub { export interface IEmailStub {
from: Array<client.IEmailAddress> | null; from: Array<client.IEmailAddress> | null;
id: string; id: string;
mailboxIds: MailboxIdMap;
receivedAt: string; receivedAt: string;
subject: string; subject: string;
} }
@ -18,8 +20,20 @@ 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: Array<IMailbox> | null;
}
mailboxByRole(role: MailboxRole): IMailbox | null;
}
export type MailboxRole =
| "all"
| "archive"
| "drafts"
| "flagged"
| "important"
| "junk"
| "sent"
| "subscribed"
| "trash";
export type PushMessage = client.PushMessage;
export type AccountIdMap = { [accountId: string]: IAccount }; export type AccountIdMap = { [accountId: string]: IAccount };
export type EmailStubIdMap = { [emailId: string]: IEmailStub }; export type EmailStubIdMap = { [emailId: string]: IEmailStub };
export type EmailIdMap = { [emailId: string]: IEmail }; export type EmailIdMap = { [emailId: string]: IEmail };