Rip apart client, build a wrapper.
The goal here is to be able to augment the client with additional data as we get it. At this point I'm now augmenting with the mailbox data that the MailboxList is requesting and showing that. That's progress. There may be significant issues with making multiple requests in a single round-trip because my client library appears to do things like hard-coding the position of specific requests. I may have to work around this.
This commit is contained in:
parent
c17e8b9ad0
commit
bab5d421d4
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Dropdown from "react-bootstrap/Dropdown";
|
import Dropdown from "react-bootstrap/Dropdown";
|
||||||
|
|
||||||
import { IAccount, AccountIdMap } from "./types";
|
import { AccountIdMap, IAccount } from "./client/types";
|
||||||
|
|
||||||
export type AccountListProps = {
|
export type AccountListProps = {
|
||||||
account: IAccount | null;
|
account: IAccount | null;
|
||||||
|
@ -17,7 +17,9 @@ const AccountList: React.FC<AccountListProps> = ({ account, accounts }) => {
|
||||||
|
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
{Object.keys(accounts).map((key: keyof AccountIdMap) => (
|
{Object.keys(accounts).map((key: keyof AccountIdMap) => (
|
||||||
<Dropdown.Item href={"#" + key}>{accounts[key].name}</Dropdown.Item>
|
<Dropdown.Item href={"#" + key} key={key}>
|
||||||
|
{accounts[key].name}
|
||||||
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
94
src/App.tsx
94
src/App.tsx
|
@ -1,92 +1,47 @@
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import "bootstrap/dist/css/bootstrap.min.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 AppLayout from "./AppLayout";
|
||||||
import AuthModal from "./AuthModal";
|
import AuthModal from "./AuthModal";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface IAuth {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILocation {
|
interface ILocation {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppState = {
|
type AppState = {
|
||||||
auth: IAuth;
|
auth: IAuth;
|
||||||
|
account: IAccount | null;
|
||||||
|
accounts: AccountIdMap;
|
||||||
location: ILocation;
|
location: ILocation;
|
||||||
session: ISession | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppProps = {};
|
type AppProps = {};
|
||||||
|
|
||||||
class App extends React.Component<AppProps, AppState> {
|
class App extends React.Component<AppProps, AppState> {
|
||||||
account(): IAccount | null {
|
account(): IAccount | null {
|
||||||
if (!(this.state.session && this.state.session.accounts)) return null;
|
return this.client.account(this.state.location.accountId);
|
||||||
return this.state.session.accounts[this.state.location.accountId];
|
|
||||||
}
|
}
|
||||||
client: Client | null = null;
|
client: Client = new Client();
|
||||||
state: AppState = {
|
state: AppState = {
|
||||||
|
account: null,
|
||||||
|
accounts: {},
|
||||||
auth: { email: "", password: "" },
|
auth: { email: "", password: "" },
|
||||||
location: { accountId: "" },
|
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() {
|
onHashChange() {
|
||||||
console.log(window.location.hash);
|
console.log(window.location.hash);
|
||||||
const hash = window.location.hash.substring(1);
|
const hash = window.location.hash.substring(1);
|
||||||
this.setState({
|
this.setState({
|
||||||
...this.state,
|
...this.state,
|
||||||
|
account: this.account(),
|
||||||
location: { accountId: hash },
|
location: { accountId: hash },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the user provides credentials
|
// When the user provides credentials
|
||||||
onLogin(email: string, password: string) {
|
onLogin(email: string, password: string) {
|
||||||
// Store the provided credentials for now
|
// Store the provided credentials for now
|
||||||
|
@ -98,9 +53,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
localStorage.setItem("auth", JSON.stringify(this.state.auth));
|
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() {
|
loadAuth() {
|
||||||
const data = localStorage.getItem("auth");
|
const data = localStorage.getItem("auth");
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
@ -109,10 +65,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
...this.state,
|
...this.state,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
});
|
});
|
||||||
if (this.client == null) {
|
this.client.ensureLogin(auth);
|
||||||
this.doLogin(auth);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -125,6 +78,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
);
|
);
|
||||||
this.loadAuth();
|
this.loadAuth();
|
||||||
this.onHashChange();
|
this.onHashChange();
|
||||||
|
this.client.onChange(() => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
account: this.account(),
|
||||||
|
accounts: this.client.state.session
|
||||||
|
? this.client.state.session.accounts
|
||||||
|
: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -140,15 +102,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
{this.state && this.state.session ? (
|
|
||||||
<AppLayout
|
<AppLayout
|
||||||
account={this.account()}
|
account={this.state.account}
|
||||||
accounts={this.state.session.accounts}
|
accounts={this.state.accounts}
|
||||||
client={this.client}
|
client={this.client}
|
||||||
/>
|
/>
|
||||||
) : (
|
<AuthModal
|
||||||
<AuthModal onLogin={this.onLogin}></AuthModal>
|
show={this.client.state.session == null}
|
||||||
)}
|
onLogin={this.onLogin}
|
||||||
|
></AuthModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,23 @@ import Container from "react-bootstrap/Container";
|
||||||
import Row from "react-bootstrap/Row";
|
import Row from "react-bootstrap/Row";
|
||||||
import Col from "react-bootstrap/Col";
|
import Col from "react-bootstrap/Col";
|
||||||
|
|
||||||
import AccountList, { AccountListProps } from "./AccountList";
|
import AccountList from "./AccountList";
|
||||||
import MailboxList from "./MailboxList";
|
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<TopProps> = (props) => {
|
const AppLayout: React.FC<TopProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
<AccountList {...props} />
|
<AccountList account={props.account} accounts={props.accounts} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col></Col>
|
<Col></Col>
|
||||||
<Col></Col>
|
<Col></Col>
|
||||||
|
|
|
@ -6,18 +6,15 @@ import React, { useState } from "react";
|
||||||
|
|
||||||
type AuthProps = {
|
type AuthProps = {
|
||||||
onLogin: (email: string, password: string) => void;
|
onLogin: (email: string, password: string) => void;
|
||||||
|
show: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthModal: React.FC<AuthProps> = ({ onLogin }) => {
|
const AuthModal: React.FC<AuthProps> = ({ onLogin, show }) => {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal show={show}>
|
||||||
className="modal show"
|
|
||||||
style={{ display: "block", position: "initial" }}
|
|
||||||
>
|
|
||||||
<Modal.Dialog>
|
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Modal title</Modal.Title>
|
<Modal.Title>Modal title</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
@ -56,8 +53,7 @@ const AuthModal: React.FC<AuthProps> = ({ onLogin }) => {
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
||||||
<Modal.Footer></Modal.Footer>
|
<Modal.Footer></Modal.Footer>
|
||||||
</Modal.Dialog>
|
</Modal>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default AuthModal;
|
export default AuthModal;
|
||||||
|
|
|
@ -1,27 +1,17 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Stack from "react-bootstrap/Stack";
|
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 MailboxListProps = { account: IAccount | null; client: Client | null };
|
||||||
type MailboxListState = {};
|
type MailboxListState = {};
|
||||||
|
|
||||||
class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
|
class MailboxList extends React.Component<MailboxListProps, MailboxListState> {
|
||||||
componentDidMount() {
|
componentDidUpdate() {
|
||||||
if (this.props.account == null) return;
|
if (this.props.account == null) return;
|
||||||
if (this.props.client == null) return;
|
if (this.props.client == null) return;
|
||||||
const args = {
|
this.props.client.mailboxList(this.props.account.id, []);
|
||||||
accountId: this.props.account.id,
|
|
||||||
ids: [],
|
|
||||||
};
|
|
||||||
this.props.client
|
|
||||||
.mailbox_get(args)
|
|
||||||
.then(() => {
|
|
||||||
console.log("got mailboxen");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.error("Failed to get mailboxes");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -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<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];
|
||||||
|
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<string>) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,4 @@
|
||||||
import client from "jmap-client-ts/lib/types";
|
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 {
|
export interface IAccount extends client.IAccount {
|
||||||
id: string;
|
id: string;
|
||||||
mailboxes: Array<client.IMailboxProperties>;
|
mailboxes: Array<client.IMailboxProperties>;
|
||||||
|
@ -13,9 +9,3 @@ export type AccountIdMap = { [accountId: string]: IAccount };
|
||||||
export interface ISession extends client.ISession {
|
export interface ISession extends client.ISession {
|
||||||
accounts: AccountIdMap;
|
accounts: AccountIdMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TopProps = {
|
|
||||||
account: IAccount | null;
|
|
||||||
accounts: AccountIdMap;
|
|
||||||
client: Client | null;
|
|
||||||
};
|
|
Loading…
Reference in New Issue