diff --git a/src/AccountList.tsx b/src/AccountList.tsx index 611fbb0..f47d0fb 100644 --- a/src/AccountList.tsx +++ b/src/AccountList.tsx @@ -1,7 +1,7 @@ import React from "react"; import Dropdown from "react-bootstrap/Dropdown"; -import { IAccount, AccountIdMap } from "./types"; +import { AccountIdMap, IAccount } from "./client/types"; export type AccountListProps = { account: IAccount | null; @@ -17,7 +17,9 @@ const AccountList: React.FC = ({ account, accounts }) => { {Object.keys(accounts).map((key: keyof AccountIdMap) => ( - {accounts[key].name} + + {accounts[key].name} + ))} diff --git a/src/App.tsx b/src/App.tsx index e9c5895..648ca80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,92 +1,47 @@ import "./App.css"; import "bootstrap/dist/css/bootstrap.min.css"; -import * as base64 from "base-64"; -import { Client } from "jmap-client-ts"; -import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport"; -import { IAccount, ISession } from "./types"; +import Client, { IAuth } from "./client/Client"; +import { AccountIdMap, IAccount } from "./client/types"; import AppLayout from "./AppLayout"; import AuthModal from "./AuthModal"; import React from "react"; -interface IAuth { - email: string; - password: string; -} - interface ILocation { accountId: string; } type AppState = { auth: IAuth; + account: IAccount | null; + accounts: AccountIdMap; location: ILocation; - session: ISession | null; }; type AppProps = {}; class App extends React.Component { account(): IAccount | null { - if (!(this.state.session && this.state.session.accounts)) return null; - return this.state.session.accounts[this.state.location.accountId]; + return this.client.account(this.state.location.accountId); } - client: Client | null = null; + client: Client = new Client(); state: AppState = { + account: null, + accounts: {}, auth: { email: "", password: "" }, location: { accountId: "" }, - session: null, }; - // Make the request to get system metadata - doLogin(auth: IAuth) { - const domain = auth.email.split("@")[1]; - const well_known_url = "https://" + domain + "/.well-known/jmap"; - const basic_auth = - "Basic " + base64.encode(auth.email + ":" + auth.password); - - this.client = new Client({ - accessToken: "fake token", - httpHeaders: { Authorization: basic_auth }, - sessionUrl: well_known_url, - transport: new FetchTransport(fetch.bind(window)), - }); - - this.client - .fetchSession() - .then(() => { - console.log("Session received"); - - // For the type checker - if (!this.client) return; - - const session = this.client.getSession(); - this.setState({ - ...this.state, - session: { - ...session, - accounts: Object.fromEntries( - Object.entries(session.accounts).map(([key, account]) => [ - key, - { ...account, id: key.toString(), mailboxes: [] }, - ]), - ), - }, - }); - }) - .catch((error) => console.error(error)); - - return; - } - onHashChange() { console.log(window.location.hash); const hash = window.location.hash.substring(1); this.setState({ ...this.state, + account: this.account(), location: { accountId: hash }, }); } + // When the user provides credentials onLogin(email: string, password: string) { // Store the provided credentials for now @@ -98,9 +53,10 @@ class App extends React.Component { }, }); localStorage.setItem("auth", JSON.stringify(this.state.auth)); - this.doLogin({ email, password }); + this.client.doLogin({ email, password }); } + // Load up auth credentials from the local store loadAuth() { const data = localStorage.getItem("auth"); if (!data) return; @@ -109,10 +65,7 @@ class App extends React.Component { ...this.state, auth: auth, }); - if (this.client == null) { - this.doLogin(auth); - return; - } + this.client.ensureLogin(auth); } componentDidMount() { @@ -125,6 +78,15 @@ class App extends React.Component { ); this.loadAuth(); this.onHashChange(); + this.client.onChange(() => { + this.setState({ + ...this.state, + account: this.account(), + accounts: this.client.state.session + ? this.client.state.session.accounts + : {}, + }); + }); } componentWillUnmount() { @@ -140,15 +102,15 @@ class App extends React.Component { render() { return (
- {this.state && this.state.session ? ( - - ) : ( - - )} + +
); } diff --git a/src/AppLayout.tsx b/src/AppLayout.tsx index 99b88ad..d4fedd9 100644 --- a/src/AppLayout.tsx +++ b/src/AppLayout.tsx @@ -3,16 +3,23 @@ import Container from "react-bootstrap/Container"; import Row from "react-bootstrap/Row"; import Col from "react-bootstrap/Col"; -import AccountList, { AccountListProps } from "./AccountList"; +import AccountList from "./AccountList"; import MailboxList from "./MailboxList"; -import { IAccount, TopProps } from "./types"; +import Client from "./client/Client"; +import { AccountIdMap, IAccount } from "./client/types"; + +type TopProps = { + account: IAccount | null; + accounts: AccountIdMap; + client: Client; +}; const AppLayout: React.FC = (props) => { return ( - + diff --git a/src/AuthModal.tsx b/src/AuthModal.tsx index 4e13da7..907cd09 100644 --- a/src/AuthModal.tsx +++ b/src/AuthModal.tsx @@ -6,58 +6,54 @@ import React, { useState } from "react"; type AuthProps = { onLogin: (email: string, password: string) => void; + show: boolean; }; -const AuthModal: React.FC = ({ onLogin }) => { +const AuthModal: React.FC = ({ onLogin, show }) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); return ( -
- - - Modal title - + + + Modal title + - -
{ - e.preventDefault(); - onLogin(email, password); - }} - > - - Email address - setEmail(e.target.value)} - /> - - We'll never share your email with anyone else. - - + + { + e.preventDefault(); + onLogin(email, password); + }} + > + + Email address + setEmail(e.target.value)} + /> + + We'll never share your email with anyone else. + + - - Password - setPassword(e.target.value)} - /> - - - -
+ + Password + setPassword(e.target.value)} + /> + + + +
- -
-
+ + ); }; export default AuthModal; diff --git a/src/MailboxList.tsx b/src/MailboxList.tsx index 51b342e..eb045f5 100644 --- a/src/MailboxList.tsx +++ b/src/MailboxList.tsx @@ -1,27 +1,17 @@ import React from "react"; import Stack from "react-bootstrap/Stack"; -import { Client, IAccount } from "./types"; +import Client from "./client/Client"; +import { IAccount } from "./client/types"; type MailboxListProps = { account: IAccount | null; client: Client | null }; type MailboxListState = {}; class MailboxList extends React.Component { - componentDidMount() { + componentDidUpdate() { if (this.props.account == null) return; if (this.props.client == null) return; - const args = { - accountId: this.props.account.id, - ids: [], - }; - this.props.client - .mailbox_get(args) - .then(() => { - console.log("got mailboxen"); - }) - .catch(() => { - console.error("Failed to get mailboxes"); - }); + this.props.client.mailboxList(this.props.account.id, []); } render() { diff --git a/src/client/Client.tsx b/src/client/Client.tsx new file mode 100644 index 0000000..ebb3e95 --- /dev/null +++ b/src/client/Client.tsx @@ -0,0 +1,107 @@ +/* + * Contains all of the logic for interacting with JMAP + * None of the dependencies should leak, in types or otherwise + */ +import * as base64 from "base-64"; +import * as jmapclient from "jmap-client-ts"; +import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport"; + +import { IAccount, ISession } from "./types"; + +type Callback = () => void; + +export interface IAuth { + email: string; + password: string; +} + +export interface ClientState { + session: ISession | null; +} + +export default class Client { + // Get the currently active account + account(accountId: string): IAccount | null { + if (!(this.state.session && this.state.session.accounts)) return null; + return this.state.session.accounts[accountId]; + } + // All objects which currently are listening for changes + callbacks: Array = []; + 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]; + const well_known_url = "https://" + domain + "/.well-known/jmap"; + const basic_auth = + "Basic " + base64.encode(auth.email + ":" + auth.password); + + this.jclient = new jmapclient.Client({ + accessToken: "fake token", + httpHeaders: { Authorization: basic_auth }, + sessionUrl: well_known_url, + transport: new FetchTransport(fetch.bind(window)), + }); + + this.jclient + .fetchSession() + .then(() => { + this._onSession(); + }) + .catch((error) => console.error(error)); + + return; + } + + mailboxList(accountId: string, ids: Array) { + if (this.jclient == null) return; + this.jclient + .mailbox_get({ + accountId: accountId, + ids: null, + }) + .then((response) => { + if (this.state.session == null) return; + const account = this.state.session.accounts[response.accountId]; + account.mailboxes = response.list; + this._triggerChange(); + }); + } + + _triggerChange() { + this.callbacks.forEach((c) => { + c(); + }); + } + _onSession() { + console.log("Session received"); + + // For the type checker + if (!this.jclient) return; + + const session = this.jclient.getSession(); + this.state.session = { + ...session, + accounts: Object.fromEntries( + Object.entries(session.accounts).map(([key, account]) => [ + key, + { ...account, id: key.toString(), mailboxes: [] }, + ]), + ), + }; + if (!this.state.session) return; + this._triggerChange(); + } +} diff --git a/src/types.tsx b/src/client/types.tsx similarity index 60% rename from src/types.tsx rename to src/client/types.tsx index f0585a5..7bec110 100644 --- a/src/types.tsx +++ b/src/client/types.tsx @@ -1,8 +1,4 @@ import client from "jmap-client-ts/lib/types"; -import { Client } from "jmap-client-ts"; - -export type { Client } from "jmap-client-ts"; - export interface IAccount extends client.IAccount { id: string; mailboxes: Array; @@ -13,9 +9,3 @@ export type AccountIdMap = { [accountId: string]: IAccount }; export interface ISession extends client.ISession { accounts: AccountIdMap; } - -export type TopProps = { - account: IAccount | null; - accounts: AccountIdMap; - client: Client | null; -};