Compare commits

..

No commits in common. "d8ee3d5f0f37c6e182d5f853b1b9a4c0c4759d27" and "5c7e67a2bdf348b203645dc6cba86f4143d6be56" have entirely different histories.

19 changed files with 110 additions and 582 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "src/jmap-client-ts"]
path = src/client/jmap-client-ts
url = git@github.com:EliRibble/jmap-client-ts.git

37
package-lock.json generated
View File

@ -16,14 +16,13 @@
"@types/node": "^16.18.104",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"axios": "^0.21.4",
"base-64": "^1.0.0",
"bootstrap": "^5.3.3",
"jmap-client-ts": "^1.0.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"sass": "^1.77.8",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
@ -5257,14 +5256,6 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@ -9328,11 +9319,6 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -12007,6 +11993,11 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jmap-client-ts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/jmap-client-ts/-/jmap-client-ts-1.0.0.tgz",
"integrity": "sha512-qL31lYUpLAqrVFMaMsogorcAcpZRTSjWPwnxdl61mDNY9DINQvrKIkYCMstva4cxTbL1kQFG4aT8K9wOesrj7g=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -15533,22 +15524,6 @@
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
},
"node_modules/sass": {
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-loader": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",

View File

@ -11,14 +11,13 @@
"@types/node": "^16.18.104",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"axios": "^0.21.4",
"base-64": "^1.0.0",
"bootstrap": "^5.3.3",
"jmap-client-ts": "^1.0.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"sass": "^1.77.8",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},

View File

@ -1,15 +1,14 @@
import "./App.css";
import "./style.scss";
import "bootstrap/dist/css/bootstrap.min.css";
import Client, { IAuth } from "./client/Client";
import { AccountIdMap, IAccount, IEmail, IMailbox } from "./client/types";
import { AccountIdMap, IAccount, IMailbox } from "./client/types";
import AppLayout from "./AppLayout";
import AuthModal from "./AuthModal";
import React from "react";
interface ILocation {
accountId: string;
emailId: string;
mailboxId: string;
}
@ -17,7 +16,6 @@ type AppState = {
auth: IAuth;
account: IAccount | null;
accounts: AccountIdMap;
email: IEmail | null;
location: ILocation;
mailbox: IMailbox | null;
};
@ -39,8 +37,7 @@ class App extends React.Component<AppProps, AppState> {
account: null,
accounts: {},
auth: { email: "", password: "" },
email: null,
location: { accountId: "", emailId: "", mailboxId: "" },
location: { accountId: "", mailboxId: "" },
mailbox: null,
};
@ -49,20 +46,14 @@ class App extends React.Component<AppProps, AppState> {
const parts = hash.split("/");
const accountId = parts[0];
const mailboxId = parts[1];
const emailId = parts[2];
this.setState({
...this.state,
account: this.account(),
email: this.client.email(emailId),
location: {
accountId: accountId,
emailId: emailId,
mailboxId: mailboxId,
},
mailbox: this.client.mailbox(accountId, mailboxId),
});
if (!this.state.account) return;
this.client.ensureEmailList(accountId, mailboxId);
}
// When the user provides credentials
@ -79,6 +70,16 @@ class App extends React.Component<AppProps, AppState> {
this.client.doLogin({ email, password });
}
onMailboxSelect(mailboxId: string) {
if (!this.state.account) return;
this.client.emailList(this.state.account.id, mailboxId, []);
this.setState({
...this.state,
mailbox: this.client.mailbox(this.state.account.id, mailboxId),
});
window.location.hash = this.state.location.accountId + "/" + mailboxId;
}
// Load up auth credentials from the local store
loadAuth() {
const data = localStorage.getItem("auth");
@ -108,7 +109,6 @@ class App extends React.Component<AppProps, AppState> {
accounts: this.client.state.session
? this.client.state.session.accounts
: {},
email: this.client.email(this.state.location.emailId),
mailbox: this.mailbox(),
});
});
@ -131,9 +131,10 @@ class App extends React.Component<AppProps, AppState> {
account={this.state.account}
accounts={this.state.accounts}
client={this.client}
email={this.state.email}
emailId={this.state.location.emailId}
mailbox={this.state.mailbox}
onMailboxSelect={(m) => {
this.onMailboxSelect(m);
}}
/>
<AuthModal
show={this.client.state.session == null}

View File

@ -4,23 +4,22 @@ import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import AccountList from "./AccountList";
import EmailArea from "./EmailArea";
import EmailList from "./EmailList";
import MailboxList from "./MailboxList";
import Client from "./client/Client";
import { AccountIdMap, IAccount, IEmail, IMailbox } from "./client/types";
import { AccountIdMap, IAccount, IMailbox } from "./client/types";
type TopProps = {
account: IAccount | null;
accounts: AccountIdMap;
client: Client;
email: IEmail | null;
emailId: string;
mailbox: IMailbox | null;
onMailboxSelect: (mailboxId: string) => void;
};
const AppLayout: React.FC<TopProps> = (props) => {
return (
<Container fluid>
<Container>
<Row>
<Col>
<AccountList account={props.account} accounts={props.accounts} />
@ -30,14 +29,16 @@ const AppLayout: React.FC<TopProps> = (props) => {
</Row>
<Row>
<Col lg="1">
<MailboxList account={props.account} client={props.client} />
</Col>
<Col lg="11">
<EmailArea
<MailboxList
account={props.account}
client={props.client}
onMailboxSelect={props.onMailboxSelect}
/>
</Col>
<Col lg="11">
<EmailList
account={props.account}
client={props.client}
email={props.email}
emailId={props.emailId}
mailbox={props.mailbox}
/>
</Col>

View File

@ -1,44 +0,0 @@
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;

View File

@ -1,120 +0,0 @@
import Button from "react-bootstrap/Button";
import Col from "react-bootstrap/Col";
import Container from "react-bootstrap/Container";
import Form from "react-bootstrap/Form";
import Placeholder from "react-bootstrap/Placeholder";
import React from "react";
import Row from "react-bootstrap/Row";
import Client from "./client/Client";
import EmailContentHTML from "./EmailContentHTML";
import EmailContentText from "./EmailContentText";
import { IAccount, IEmail } from "./client/types";
type EmailContentProps = {
account: IAccount | null;
client: Client | null;
email: IEmail | null;
emailId: string;
};
type EmailContentState = {
showHTML: boolean;
};
class EmailContent extends React.Component<
EmailContentProps,
EmailContentState
> {
state = {
showHTML: false,
};
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() {
const email = this.props.email;
if (email == null || email.bodyValues == null) {
return <Placeholder />;
}
let content;
if (this.state.showHTML) {
if (email.htmlBody != null) {
content = <EmailContentHTML email={email} />;
} else {
return <p>No HTML content</p>;
}
} else {
if (email.textBody != null) {
content = <EmailContentText email={email} />;
} else {
return <p>No text content/</p>;
}
}
return (
<Container fluid>
<Row>
<Form>
<Form.Group
as={Row}
className="mb-3"
controlId="formHorizontalEmail"
>
<Form.Label column sm={2}>
From
</Form.Label>
<Col sm={10}>
<p>
{email.from == null
? "unknown"
: email.from.map((f) => f.name)}
</p>
</Col>
<Form.Label column sm={2}>
Received
</Form.Label>
<Col sm={10}>{email.receivedAt}</Col>
</Form.Group>
</Form>
</Row>
<Row>
<Button
onClick={() => {
this.switchDisplay();
}}
>
Switch
</Button>
</Row>
<Row>{content}</Row>
</Container>
);
}
switchDisplay() {
if (this.state.showHTML) {
this.setState({
showHTML: false,
});
} else {
this.setState({
showHTML: true,
});
}
}
}
export default EmailContent;

View File

@ -1,33 +0,0 @@
import Placeholder from "react-bootstrap/Placeholder";
import React from "react";
import Stack from "react-bootstrap/Stack";
import { IEmail } from "./client/types";
type EmailContentHTMLProps = {
email: IEmail;
};
const EmailContentHTML: React.FC<EmailContentHTMLProps> = (props) => {
const email = props.email;
if (email.htmlBody == null) {
return <Placeholder />;
}
return (
<Stack>
{email.htmlBody.map((h) =>
h.partId === undefined ? (
<Placeholder />
) : (
<div
key={h.partId}
dangerouslySetInnerHTML={{
__html: email.bodyValues![h.partId].value,
}}
/>
),
)}
</Stack>
);
};
export default EmailContentHTML;

View File

@ -1,28 +0,0 @@
import Placeholder from "react-bootstrap/Placeholder";
import React from "react";
import Stack from "react-bootstrap/Stack";
import { IEmail } from "./client/types";
type EmailContentTextProps = {
email: IEmail;
};
const EmailContentText: React.FC<EmailContentTextProps> = (props) => {
const email = props.email;
if (email.textBody == null) {
return <Placeholder />;
}
return (
<Stack>
{email.textBody.map((t) =>
t.partId === undefined ? (
<pre>undefined partId</pre>
) : (
<pre key={t.partId}>{email.bodyValues![t.partId].value}</pre>
),
)}
</Stack>
);
};
export default EmailContentText;

View File

@ -3,11 +3,10 @@ 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;
client: Client;
client: Client | null;
mailbox: IMailbox | null;
};
@ -18,31 +17,37 @@ class EmailList extends React.Component<EmailListProps, EmailListState> {
if (this.props.account == null) return;
if (this.props.client == null) return;
if (this.props.mailbox == null) return;
this.props.client.ensureEmailList(
this.props.client.emailList(
this.props.account.id,
this.props.mailbox.id,
[],
);
}
render() {
if (
this.props.account === null ||
this.props.mailbox === null ||
this.props.mailbox.emailIds === null
this.props.account == null ||
this.props.mailbox == null ||
this.props.mailbox.emailIds == null
) {
return <Stack />;
} else if (this.props.mailbox.emails == null) {
return (
<Stack>
{this.props.mailbox.emailIds.map((e) => (
<div className="p-2" key={e}>
Email {e}
</div>
))}
</Stack>
);
} else {
return (
<Stack className="text-start">
{this.props.mailbox.emailIds.slice(0, 5).map((e) => (
<EmailSummary
account={this.props.account!}
client={this.props.client}
emailId={e}
emailStub={this.props.client.emailStub(e)!}
key={e}
mailbox={this.props.mailbox!}
/>
<Stack>
{this.props.mailbox.emails.map((m) => (
<div className="p-2" key={m.id}>
{m.subject}
</div>
))}
</Stack>
);

View File

@ -1,62 +0,0 @@
import Placeholder from "react-bootstrap/Placeholder";
import React from "react";
import Client from "./client/Client";
import DateTime from "./components/DateTime";
import { IAccount, IEmailStub, IMailbox } from "./client/types";
type EmailSummaryProps = {
account: IAccount;
client: Client;
emailId: string;
emailStub: IEmailStub | null;
mailbox: IMailbox;
};
type EmailSummaryState = {};
class EmailSummary extends React.Component<
EmailSummaryProps,
EmailSummaryState
> {
componentDidMount() {
this.ensureData();
}
componentDidUpdate() {
this.ensureData();
}
ensureData() {
this.props.client.ensureEmailStub(
this.props.account.id,
this.props.emailId,
);
}
render() {
const href =
"#" +
this.props.account.id +
"/" +
this.props.mailbox.id +
"/" +
this.props.emailId;
const stub = this.props.emailStub;
if (stub === null) {
return <Placeholder />;
}
return (
<div className="p-2 border" key={this.props.emailId}>
<a className="btn" href={href}>
<DateTime d={stub.receivedAt} />
<span>
{" - " +
(stub.from == null ? "?" : stub.from[0].name) +
" - " +
stub.subject}
</span>
</a>
</div>
);
}
}
export default EmailSummary;

View File

@ -1,19 +1,12 @@
import React from "react";
type MailboxProps = {
accountId: string;
id: string;
name: string;
};
const Mailbox: React.FC<MailboxProps> = ({ accountId, id, name }) => {
const href = "#" + accountId + "/" + id;
import ListGroup from "react-bootstrap/ListGroup";
function Mailbox() {
return (
<a href={href} className="p-2" key={id}>
{name}
</a>
<ListGroup>
<ListGroup.Item>Inbox</ListGroup.Item>
<ListGroup.Item>Spam</ListGroup.Item>
</ListGroup>
);
};
}
export default Mailbox;

View File

@ -1,13 +1,14 @@
import React from "react";
import Button from "react-bootstrap/Button";
import Stack from "react-bootstrap/Stack";
import Client from "./client/Client";
import { IAccount } from "./client/types";
import Mailbox from "./Mailbox";
type MailboxListProps = {
account: IAccount | null;
client: Client | null;
onMailboxSelect: (mailboxId: string) => void;
};
type MailboxListState = {};
@ -19,6 +20,10 @@ class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
this.props.client.mailboxList(this.props.account.id, []);
}
onMailboxClick(id: string) {
this.props.onMailboxSelect(id);
}
render() {
return this.props.account == null ||
this.props.account.mailboxes == null ? (
@ -26,12 +31,15 @@ class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
) : (
<Stack>
{this.props.account.mailboxes.map((m) => (
<Mailbox
accountId={this.props.account!.id}
id={m.id}
<Button
className="p-2"
key={m.id}
name={m.name}
/>
onClick={() => {
this.onMailboxClick(m.id);
}}
>
{m.name}
</Button>
))}
</Stack>
);

View File

@ -3,10 +3,10 @@
* None of the dependencies should leak, in types or otherwise
*/
import * as base64 from "base-64";
import * as jmapclient from "./jmap-client-ts/src";
import { FetchTransport } from "./jmap-client-ts/src/utils/fetch-transport";
import * as jmapclient from "jmap-client-ts";
import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport";
import { IAccount, IEmail, IEmailStub, IMailbox, ISession } from "./types";
import { IAccount, IMailbox, ISession } from "./types";
type Callback = () => void;
@ -16,33 +16,31 @@ export interface IAuth {
}
export interface ClientState {
inFlight: {
emailContents: Set<string>;
emailStubs: Set<string>;
mailboxes: Set<string>;
};
session: ISession | null;
}
export default class Client {
// All objects which currently are listening for changes
callbacks: Array<Callback> = [];
jclient: jmapclient.Client | null = null;
state: ClientState = {
inFlight: {
emailContents: new Set(),
emailStubs: new Set(),
mailboxes: new Set(),
},
session: null,
};
// 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
callbacks: Array<Callback> = [];
jclient: jmapclient.Client | null = null;
state: ClientState = {
session: null,
};
onChange(f: Callback) {
this.callbacks.push(f);
}
// 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];
@ -67,149 +65,29 @@ export default class Client {
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 {
if (this.state.session == null) return null;
return this.state.session.emails[emailId];
}
emailGetContent(accountId: string, emailId: string) {
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
.email_get({
accountId: accountId,
ids: [emailId],
fetchAllBodyValues: true,
})
.then((response) => {
console.log(msg, "response", response);
response.list.forEach((e) => {
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,
id: e.id,
};
this._triggerChange(msg + e.id);
});
})
.catch((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: ["from", "receivedAt", "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] = {
from: e.from,
id: e.id,
receivedAt: e.receivedAt,
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);
});
}
emailList(accountId: string, mailboxId: string, ids: Array<string>) {
if (this.jclient == null) return;
/*this.jclient.email_get({
accountId: accountId,
ids: [],
properties: ["threadId"]
});*/
this.jclient
.email_query({
accountId: accountId,
filter: { inMailbox: mailboxId },
sort: [
{
property: "receivedAt",
isAscending: false,
},
],
})
.then((response) => {
const mailbox = this.mailbox(accountId, mailboxId);
if (mailbox == null) return;
mailbox.emailIds = response.ids;
this._triggerChange("Email list " + mailboxId);
this._triggerChange();
})
.catch((e) => {
console.error("Failed to get email list from mailbox", mailboxId, e);
.catch(() => {
console.error("OH NOES");
});
}
emailStub(emailId: string): IEmailStub | null {
if (this.state.session == null) return null;
const result = this.state.session.emailStubs[emailId];
if (result === undefined) {
return null;
}
return result;
}
mailbox(accountId: string, mailboxId: string): IMailbox | null {
if (this.state.session == null) return null;
const account = this.state.session.accounts[accountId];
@ -239,19 +117,15 @@ export default class Client {
mailboxes.push({
...m,
emailIds: null,
emails: null,
});
});
account.mailboxes = mailboxes;
this._triggerChange("Mailboxes " + accountId);
this._triggerChange();
});
}
onChange(f: Callback) {
this.callbacks.push(f);
}
_triggerChange(msg: string) {
console.log("Client change", msg);
_triggerChange() {
this.callbacks.forEach((c) => {
c();
});
@ -271,10 +145,8 @@ export default class Client {
{ ...account, id: key.toString(), mailboxes: null },
]),
),
emails: {},
emailStubs: {},
};
if (!this.state.session) return;
this._triggerChange("Session");
this._triggerChange();
}
}

@ -1 +0,0 @@
Subproject commit fdab37996a4709b13962b67d1aa49e85a8305755

View File

@ -1,18 +1,8 @@
import * as client from "./jmap-client-ts/src/types";
export interface IEmailStub {
from: Array<client.IEmailAddress> | null;
id: string;
receivedAt: string;
subject: string;
}
import client from "jmap-client-ts/lib/types";
export interface IMailbox extends client.IMailboxProperties {
emailIds: Array<string> | null;
}
export interface IEmail extends client.IEmailProperties {
//sentAt: IutcDate|null,
emails: Array<client.IEmailProperties> | null;
}
export interface IAccount extends client.IAccount {
@ -21,11 +11,7 @@ export interface IAccount extends client.IAccount {
}
export type AccountIdMap = { [accountId: string]: IAccount };
export type EmailStubIdMap = { [emailId: string]: IEmailStub };
export type EmailIdMap = { [emailId: string]: IEmail };
export interface ISession extends client.ISession {
accounts: AccountIdMap;
emails: EmailIdMap;
emailStubs: EmailStubIdMap;
}

View File

@ -1,19 +0,0 @@
import React from "react";
type DateTimeProps = {
d: string;
};
const DateTime: React.FC<DateTimeProps> = ({ d }) => {
const datetime = Date.parse(d);
const now = Date.now();
const diff = (now - datetime) / 1000;
if (diff < 30) return <span>moments ago</span>;
if (diff < 60) return <span>{diff}s</span>;
if (diff < 60 * 60) return <span>{Math.round(diff / 60)}m</span>;
if (diff < 60 * 60 * 48) return <span>{Math.round(diff / (60 * 60))}h</span>;
if (diff < 60 * 60 * 24 * 365)
return <span>{Math.round(diff / (60 * 60 * 24))}d</span>;
return <span>{Math.round(diff / (60 * 60 * 24 * 365))}y</span>;
};
export default DateTime;

View File

@ -1 +0,0 @@
@import '~bootstrap/scss/bootstrap';

View File

@ -16,6 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["src/client/jmap-client-ts/tests"]
"include": ["src"]
}