Compare commits
18 Commits
5c7e67a2bd
...
d8ee3d5f0f
Author | SHA1 | Date |
---|---|---|
|
d8ee3d5f0f | |
|
3aebdb471a | |
|
cf20d9aff1 | |
|
895bb8ae47 | |
|
64d4e98996 | |
|
eb09b39de3 | |
|
356173e4a3 | |
|
a65514f707 | |
|
3ef58b2259 | |
|
5c293219f3 | |
|
d71f18cce1 | |
|
4e1922c5fa | |
|
fb53a7506f | |
|
bf1ad0326d | |
|
d6b675f7b6 | |
|
e68a21dcc4 | |
|
0d2e43ae83 | |
|
a34d8f53b3 |
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "src/jmap-client-ts"]
|
||||||
|
path = src/client/jmap-client-ts
|
||||||
|
url = git@github.com:EliRibble/jmap-client-ts.git
|
|
@ -16,13 +16,14 @@
|
||||||
"@types/node": "^16.18.104",
|
"@types/node": "^16.18.104",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"axios": "^0.21.4",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"jmap-client-ts": "^1.0.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.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",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
@ -5256,6 +5257,14 @@
|
||||||
"node": ">=4"
|
"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": {
|
"node_modules/axobject-query": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
|
||||||
|
@ -9319,6 +9328,11 @@
|
||||||
"url": "https://opencollective.com/immer"
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
|
@ -11993,11 +12007,6 @@
|
||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
@ -15524,6 +15533,22 @@
|
||||||
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
|
||||||
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA=="
|
"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": {
|
"node_modules/sass-loader": {
|
||||||
"version": "12.6.0",
|
"version": "12.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||||
|
|
|
@ -11,13 +11,14 @@
|
||||||
"@types/node": "^16.18.104",
|
"@types/node": "^16.18.104",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"axios": "^0.21.4",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"jmap-client-ts": "^1.0.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.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",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|
31
src/App.tsx
31
src/App.tsx
|
@ -1,14 +1,15 @@
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "./style.scss";
|
||||||
|
|
||||||
import Client, { IAuth } from "./client/Client";
|
import Client, { IAuth } from "./client/Client";
|
||||||
import { AccountIdMap, IAccount, IMailbox } from "./client/types";
|
import { AccountIdMap, IAccount, IEmail, IMailbox } from "./client/types";
|
||||||
import AppLayout from "./AppLayout";
|
import AppLayout from "./AppLayout";
|
||||||
import AuthModal from "./AuthModal";
|
import AuthModal from "./AuthModal";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface ILocation {
|
interface ILocation {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
emailId: string;
|
||||||
mailboxId: string;
|
mailboxId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ type AppState = {
|
||||||
auth: IAuth;
|
auth: IAuth;
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
|
email: IEmail | null;
|
||||||
location: ILocation;
|
location: ILocation;
|
||||||
mailbox: IMailbox | null;
|
mailbox: IMailbox | null;
|
||||||
};
|
};
|
||||||
|
@ -37,7 +39,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
account: null,
|
account: null,
|
||||||
accounts: {},
|
accounts: {},
|
||||||
auth: { email: "", password: "" },
|
auth: { email: "", password: "" },
|
||||||
location: { accountId: "", mailboxId: "" },
|
email: null,
|
||||||
|
location: { accountId: "", emailId: "", mailboxId: "" },
|
||||||
mailbox: null,
|
mailbox: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,14 +49,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const parts = hash.split("/");
|
const parts = hash.split("/");
|
||||||
const accountId = parts[0];
|
const accountId = parts[0];
|
||||||
const mailboxId = parts[1];
|
const mailboxId = parts[1];
|
||||||
|
const emailId = parts[2];
|
||||||
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,
|
||||||
mailboxId: mailboxId,
|
mailboxId: mailboxId,
|
||||||
},
|
},
|
||||||
|
mailbox: this.client.mailbox(accountId, mailboxId),
|
||||||
});
|
});
|
||||||
|
if (!this.state.account) return;
|
||||||
|
this.client.ensureEmailList(accountId, mailboxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the user provides credentials
|
// When the user provides credentials
|
||||||
|
@ -70,16 +79,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.client.doLogin({ email, password });
|
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
|
// Load up auth credentials from the local store
|
||||||
loadAuth() {
|
loadAuth() {
|
||||||
const data = localStorage.getItem("auth");
|
const data = localStorage.getItem("auth");
|
||||||
|
@ -109,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(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -131,10 +131,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
account={this.state.account}
|
account={this.state.account}
|
||||||
accounts={this.state.accounts}
|
accounts={this.state.accounts}
|
||||||
client={this.client}
|
client={this.client}
|
||||||
|
email={this.state.email}
|
||||||
|
emailId={this.state.location.emailId}
|
||||||
mailbox={this.state.mailbox}
|
mailbox={this.state.mailbox}
|
||||||
onMailboxSelect={(m) => {
|
|
||||||
this.onMailboxSelect(m);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<AuthModal
|
<AuthModal
|
||||||
show={this.client.state.session == null}
|
show={this.client.state.session == null}
|
||||||
|
|
|
@ -4,22 +4,23 @@ 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, IMailbox } from "./client/types";
|
import { AccountIdMap, IAccount, IEmail, IMailbox } from "./client/types";
|
||||||
|
|
||||||
type TopProps = {
|
type TopProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
client: Client;
|
client: Client;
|
||||||
|
email: IEmail | null;
|
||||||
|
emailId: string;
|
||||||
mailbox: IMailbox | null;
|
mailbox: IMailbox | null;
|
||||||
onMailboxSelect: (mailboxId: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppLayout: React.FC<TopProps> = (props) => {
|
const AppLayout: React.FC<TopProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container fluid>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<AccountList account={props.account} accounts={props.accounts} />
|
<AccountList account={props.account} accounts={props.accounts} />
|
||||||
|
@ -29,16 +30,14 @@ const AppLayout: React.FC<TopProps> = (props) => {
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg="1">
|
<Col lg="1">
|
||||||
<MailboxList
|
<MailboxList account={props.account} client={props.client} />
|
||||||
account={props.account}
|
|
||||||
client={props.client}
|
|
||||||
onMailboxSelect={props.onMailboxSelect}
|
|
||||||
/>
|
|
||||||
</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,120 @@
|
||||||
|
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;
|
|
@ -0,0 +1,33 @@
|
||||||
|
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;
|
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
|
@ -3,10 +3,11 @@ import Stack from "react-bootstrap/Stack";
|
||||||
|
|
||||||
import Client from "./client/Client";
|
import Client from "./client/Client";
|
||||||
import { IAccount, IMailbox } from "./client/types";
|
import { IAccount, IMailbox } from "./client/types";
|
||||||
|
import EmailSummary from "./EmailSummary";
|
||||||
|
|
||||||
type EmailListProps = {
|
type EmailListProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
client: Client | null;
|
client: Client;
|
||||||
mailbox: IMailbox | null;
|
mailbox: IMailbox | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,37 +18,31 @@ class EmailList extends React.Component<EmailListProps, EmailListState> {
|
||||||
if (this.props.account == null) return;
|
if (this.props.account == null) return;
|
||||||
if (this.props.client == null) return;
|
if (this.props.client == null) return;
|
||||||
if (this.props.mailbox == null) return;
|
if (this.props.mailbox == null) return;
|
||||||
this.props.client.emailList(
|
this.props.client.ensureEmailList(
|
||||||
this.props.account.id,
|
this.props.account.id,
|
||||||
this.props.mailbox.id,
|
this.props.mailbox.id,
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (
|
if (
|
||||||
this.props.account == null ||
|
this.props.account === null ||
|
||||||
this.props.mailbox == null ||
|
this.props.mailbox === null ||
|
||||||
this.props.mailbox.emailIds == null
|
this.props.mailbox.emailIds === null
|
||||||
) {
|
) {
|
||||||
return <Stack />;
|
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 {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack className="text-start">
|
||||||
{this.props.mailbox.emails.map((m) => (
|
{this.props.mailbox.emailIds.slice(0, 5).map((e) => (
|
||||||
<div className="p-2" key={m.id}>
|
<EmailSummary
|
||||||
{m.subject}
|
account={this.props.account!}
|
||||||
</div>
|
client={this.props.client}
|
||||||
|
emailId={e}
|
||||||
|
emailStub={this.props.client.emailStub(e)!}
|
||||||
|
key={e}
|
||||||
|
mailbox={this.props.mailbox!}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
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;
|
|
@ -1,12 +1,19 @@
|
||||||
import ListGroup from "react-bootstrap/ListGroup";
|
import React from "react";
|
||||||
|
|
||||||
|
type MailboxProps = {
|
||||||
|
accountId: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Mailbox: React.FC<MailboxProps> = ({ accountId, id, name }) => {
|
||||||
|
const href = "#" + accountId + "/" + id;
|
||||||
|
|
||||||
function Mailbox() {
|
|
||||||
return (
|
return (
|
||||||
<ListGroup>
|
<a href={href} className="p-2" key={id}>
|
||||||
<ListGroup.Item>Inbox</ListGroup.Item>
|
{name}
|
||||||
<ListGroup.Item>Spam</ListGroup.Item>
|
</a>
|
||||||
</ListGroup>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Mailbox;
|
export default Mailbox;
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Button from "react-bootstrap/Button";
|
|
||||||
import Stack from "react-bootstrap/Stack";
|
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";
|
||||||
|
|
||||||
type MailboxListProps = {
|
type MailboxListProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
client: Client | null;
|
client: Client | null;
|
||||||
onMailboxSelect: (mailboxId: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type MailboxListState = {};
|
type MailboxListState = {};
|
||||||
|
@ -20,10 +19,6 @@ class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
|
||||||
this.props.client.mailboxList(this.props.account.id, []);
|
this.props.client.mailboxList(this.props.account.id, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMailboxClick(id: string) {
|
|
||||||
this.props.onMailboxSelect(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.props.account == null ||
|
return this.props.account == null ||
|
||||||
this.props.account.mailboxes == null ? (
|
this.props.account.mailboxes == null ? (
|
||||||
|
@ -31,15 +26,12 @@ class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
|
||||||
) : (
|
) : (
|
||||||
<Stack>
|
<Stack>
|
||||||
{this.props.account.mailboxes.map((m) => (
|
{this.props.account.mailboxes.map((m) => (
|
||||||
<Button
|
<Mailbox
|
||||||
className="p-2"
|
accountId={this.props.account!.id}
|
||||||
|
id={m.id}
|
||||||
key={m.id}
|
key={m.id}
|
||||||
onClick={() => {
|
name={m.name}
|
||||||
this.onMailboxClick(m.id);
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{m.name}
|
|
||||||
</Button>
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
* 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 jmapclient from "jmap-client-ts";
|
import * as jmapclient from "./jmap-client-ts/src";
|
||||||
import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport";
|
import { FetchTransport } from "./jmap-client-ts/src/utils/fetch-transport";
|
||||||
|
|
||||||
import { IAccount, IMailbox, ISession } from "./types";
|
import { IAccount, IEmail, IEmailStub, IMailbox, ISession } from "./types";
|
||||||
|
|
||||||
type Callback = () => void;
|
type Callback = () => void;
|
||||||
|
|
||||||
|
@ -16,31 +16,33 @@ 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 {
|
||||||
|
// 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
|
// Get the currently active account
|
||||||
account(accountId: string): IAccount | null {
|
account(accountId: string): IAccount | null {
|
||||||
if (!(this.state.session && this.state.session.accounts)) return null;
|
if (!(this.state.session && this.state.session.accounts)) return null;
|
||||||
return this.state.session.accounts[accountId];
|
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
|
// Make the request to get system metadata
|
||||||
doLogin(auth: IAuth) {
|
doLogin(auth: IAuth) {
|
||||||
const domain = auth.email.split("@")[1];
|
const domain = auth.email.split("@")[1];
|
||||||
|
@ -65,29 +67,149 @@ 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 {
|
||||||
|
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>) {
|
emailList(accountId: string, mailboxId: string, ids: Array<string>) {
|
||||||
if (this.jclient == null) return;
|
if (this.jclient == null) return;
|
||||||
/*this.jclient.email_get({
|
|
||||||
accountId: accountId,
|
|
||||||
ids: [],
|
|
||||||
properties: ["threadId"]
|
|
||||||
});*/
|
|
||||||
this.jclient
|
this.jclient
|
||||||
.email_query({
|
.email_query({
|
||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
filter: { inMailbox: mailboxId },
|
filter: { inMailbox: mailboxId },
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
property: "receivedAt",
|
||||||
|
isAscending: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const mailbox = this.mailbox(accountId, mailboxId);
|
const mailbox = this.mailbox(accountId, mailboxId);
|
||||||
if (mailbox == null) return;
|
if (mailbox == null) return;
|
||||||
mailbox.emailIds = response.ids;
|
mailbox.emailIds = response.ids;
|
||||||
this._triggerChange();
|
this._triggerChange("Email list " + mailboxId);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
console.error("OH NOES");
|
console.error("Failed to get email list from mailbox", mailboxId, e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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];
|
||||||
|
@ -117,15 +239,19 @@ export default class Client {
|
||||||
mailboxes.push({
|
mailboxes.push({
|
||||||
...m,
|
...m,
|
||||||
emailIds: null,
|
emailIds: null,
|
||||||
emails: null,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
account.mailboxes = mailboxes;
|
account.mailboxes = mailboxes;
|
||||||
this._triggerChange();
|
this._triggerChange("Mailboxes " + accountId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_triggerChange() {
|
onChange(f: Callback) {
|
||||||
|
this.callbacks.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerChange(msg: string) {
|
||||||
|
console.log("Client change", msg);
|
||||||
this.callbacks.forEach((c) => {
|
this.callbacks.forEach((c) => {
|
||||||
c();
|
c();
|
||||||
});
|
});
|
||||||
|
@ -145,8 +271,10 @@ export default class Client {
|
||||||
{ ...account, id: key.toString(), mailboxes: null },
|
{ ...account, id: key.toString(), mailboxes: null },
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
emails: {},
|
||||||
|
emailStubs: {},
|
||||||
};
|
};
|
||||||
if (!this.state.session) return;
|
if (!this.state.session) return;
|
||||||
this._triggerChange();
|
this._triggerChange("Session");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit fdab37996a4709b13962b67d1aa49e85a8305755
|
|
@ -1,8 +1,18 @@
|
||||||
import client from "jmap-client-ts/lib/types";
|
import * as client from "./jmap-client-ts/src/types";
|
||||||
|
|
||||||
|
export interface IEmailStub {
|
||||||
|
from: Array<client.IEmailAddress> | null;
|
||||||
|
id: string;
|
||||||
|
receivedAt: 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 {
|
||||||
|
//sentAt: IutcDate|null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccount extends client.IAccount {
|
export interface IAccount extends client.IAccount {
|
||||||
|
@ -11,7 +21,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 interface ISession extends client.ISession {
|
export interface ISession extends client.ISession {
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
|
emails: EmailIdMap;
|
||||||
|
emailStubs: EmailStubIdMap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
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;
|
|
@ -0,0 +1 @@
|
||||||
|
@import '~bootstrap/scss/bootstrap';
|
|
@ -16,5 +16,6 @@
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"],
|
||||||
|
"exclude": ["src/client/jmap-client-ts/tests"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue