add new history view and some global style changes

This commit is contained in:
Marcos Gutierrez 2020-01-27 10:11:39 -03:00
parent b961ca2c39
commit d6d5647946
No known key found for this signature in database
GPG Key ID: 3C721C882729F8AB
2 changed files with 459 additions and 104 deletions

View File

@ -12,21 +12,24 @@ body {
padding: 0; padding: 0;
max-height: 800px; max-height: 800px;
overflow: hidden; overflow: hidden;
background-color: #f8faf7;
font-family: Arial, Helvetica, sans-serif;
} }
.nav { .nav {
background-color: #ddd; background-color: #033;
padding: 0.5em 0; padding: 0.5em 0;
} }
.nav a { .nav a {
color: black; color: #f8faf7;
padding: 0.5em; padding: 0.7em 1.4em;
text-decoration: none; text-decoration: none;
} }
.nav a.active { .nav a.active {
background-color: white; background-color: #f8faf7;
color: #000;
font-weight: bold; font-weight: bold;
} }
@ -71,14 +74,114 @@ body {
} }
.historyItem { .historyItem {
border: 1px solid black; min-width: 380px;
border-radius: 10px; display: flex;
padding-left: 0.5em; flex-direction: row;
margin: 0.5em; border-bottom: 1px solid #d9dbd8;
} padding: 0.5em;
align-items: center;
.historyDate { }
font-size: 90%;
.historyItem .amount {
font-size: 110%;
font-weight: bold;
text-align: right;
}
.historyDate,
.historyTitle,
.historyText,
.historySmall {
margin: 0.3em; margin: 0.3em;
}
.historyDate {
font-size: 90%;
color: slategray; color: slategray;
} text-align: right;
}
.historyLeft {
display: flex;
flex-direction: column;
text-align: right;
}
.historyContent {
flex: 1;
}
.historyTitle {
font-weight: 400;
}
.historyText {
font-size: 90%;
}
.historySmall {
font-size: 70%;
text-transform: uppercase;
}
.historyAmount {
flex-grow: 1;
}
.historyAmount .primary {
font-size: 100%;
}
.historyAmount .secondary {
font-size: 80%;
}
.historyAmount .positive {
color: #088;
}
.historyAmount .positive:before {
content: "+";
}
.historyAmount .negative {
color: #800
}
.historyAmount .negative:before {
color: #800;
content: "-";
}
.icon {
margin: 0 10px;
text-align: center;
width: 35px;
font-size: 20px;
border-radius: 50%;
background: #ccc;
color: #fff;
padding-top: 4px;
height: 30px;
}
.option {
text-transform: uppercase;
text-align: right;
padding: 0.4em;
font-size: 0.9em;
}
input[type=checkbox], input[type=radio] {
vertical-align: middle;
position: relative;
bottom: 1px;
}
input[type=radio] {
bottom: 2px;
}
.balance {
text-align: center;
padding-top: 2em;
}

View File

@ -42,9 +42,12 @@ import {
} from "../renderHtml"; } from "../renderHtml";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
import * as React from "react"; import React, { Fragment } from "react";
import { HistoryEvent } from "../../types/history"; import { HistoryEvent } from "../../types/history";
import moment from "moment";
import { Timestamp } from "../../util/time";
function onUpdateNotification(f: () => void): () => void { function onUpdateNotification(f: () => void): () => void {
const port = chrome.runtime.connect({ name: "notifications" }); const port = chrome.runtime.connect({ name: "notifications" });
const listener = () => { const listener = () => {
@ -185,7 +188,7 @@ function bigAmount(amount: AmountJson): JSX.Element {
const v = amount.value + amount.fraction / Amounts.fractionalBase; const v = amount.value + amount.fraction / Amounts.fractionalBase;
return ( return (
<span> <span>
<span style={{ fontSize: "300%" }}>{v}</span>{" "} <span style={{ fontSize: "5em", display: 'block'}}>{v}</span>{" "}
<span>{amount.currency}</span> <span>{amount.currency}</span>
</span> </span>
); );
@ -193,12 +196,10 @@ function bigAmount(amount: AmountJson): JSX.Element {
function EmptyBalanceView() { function EmptyBalanceView() {
return ( return (
<div> <i18n.Translate wrap="p">
<i18n.Translate wrap="p"> You have no balance to show. Need some{" "}
You have no balance to show. Need some{" "} <PageLink pageName="welcome.html">help</PageLink> getting started?
<PageLink pageName="welcome.html">help</PageLink> getting started? </i18n.Translate>
</i18n.Translate>
</div>
); );
} }
@ -299,7 +300,7 @@ class WalletBalanceView extends React.Component<any, any> {
const wallet = this.balance; const wallet = this.balance;
if (this.gotError) { if (this.gotError) {
return ( return (
<div> <div className='balance'>
<p>{i18n.str`Error: could not retrieve balance information.`}</p> <p>{i18n.str`Error: could not retrieve balance information.`}</p>
<p> <p>
Click <PageLink pageName="welcome.html">here</PageLink> for help and Click <PageLink pageName="welcome.html">here</PageLink> for help and
@ -320,114 +321,354 @@ class WalletBalanceView extends React.Component<any, any> {
</p> </p>
); );
}); });
return <div>{listing.length > 0 ? listing : <EmptyBalanceView />}</div>; return listing.length > 0 ? <div className='balance'>{listing}</div> : <EmptyBalanceView />;
} }
} }
function Icon({ l }: { l: string }) {
return <div className={"icon"}>{l}</div>;
}
function formatAndCapitalize(text: string) {
text = text.replace("-", " ");
text = text.replace(/^./, text[0].toUpperCase());
return text;
}
type HistoryItemProps = {
title?: string | JSX.Element;
text?: string | JSX.Element;
small?: string | JSX.Element;
amount?: string | AmountJson;
fees?: string | AmountJson;
invalid?: string | AmountJson;
icon?: string;
timestamp: Timestamp;
negative?: boolean;
};
function HistoryItem({
title,
text,
small,
amount,
fees,
invalid,
timestamp,
icon,
negative = false
}: HistoryItemProps) {
function formatDate(timestamp: number | "never") {
if (timestamp !== "never") {
const itemDate = moment(timestamp);
if (itemDate.isBetween(moment().subtract(2, "days"), moment())) {
return itemDate.fromNow();
}
return itemDate.format("lll");
}
return null;
}
let invalidElement, amountElement, feesElement;
if (amount) {
amountElement = renderAmount(amount);
}
if (fees) {
fees = typeof fees === "string" ? Amounts.parse(fees) : fees;
if (fees && Amounts.isNonZero(fees)) {
feesElement = renderAmount(fees);
}
}
if (invalid) {
invalid = typeof invalid === "string" ? Amounts.parse(invalid) : invalid;
if (invalid && Amounts.isNonZero(invalid)) {
invalidElement = renderAmount(invalid);
}
}
return (
<div className="historyItem">
{icon ? <Icon l={icon} /> : null}
<div className="historyContent">
{title ? <div className={"historyTitle"}>{title}</div> : null}
{text ? <div className={"historyText"}>{text}</div> : null}
{small ? <div className={"historySmall"}>{small}</div> : null}
</div>
<div className={"historyLeft"}>
<div className={"historyAmount"}>
{amountElement ? (
<div className={`${negative ? "negative" : "positive"}`}>
{amountElement}
</div>
) : null}
{invalidElement ? (
<div className={"secondary"}>
{i18n.str`Invalid `}{" "}
<span className={"negative"}>{invalidElement}</span>
</div>
) : null}
{feesElement ? (
<div className={"secondary"}>
{i18n.str`Fees `}{" "}
<span className={"negative"}>{feesElement}</span>
</div>
) : null}
</div>
<div className="historyDate">{formatDate(timestamp.t_ms)}</div>
</div>
</div>
);
}
function amountDiff(
total: string | Amounts.AmountJson,
partial: string | Amounts.AmountJson
): Amounts.AmountJson | string {
let a = typeof total === "string" ? Amounts.parse(total) : total;
let b = typeof partial === "string" ? Amounts.parse(partial) : partial;
if (a && b) {
return Amounts.sub(a, b).amount;
} else {
return total;
}
}
function parseSummary(summary: string) {
let parsed = summary.split(/: (.+)/);
return {
merchant: parsed[0],
item: parsed[1]
};
}
function formatHistoryItem(historyItem: HistoryEvent) { function formatHistoryItem(historyItem: HistoryEvent) {
const d = historyItem;
console.log("hist item", historyItem);
switch (historyItem.type) { switch (historyItem.type) {
/* case "refreshed": {
case "create-reserve":
return ( return (
<i18n.Translate wrap="p"> <HistoryItem
Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for{" "} timestamp={historyItem.timestamp}
<span>{renderAmount(d.requestedAmount)}</span>. small={i18n.str`Refresh sessions has completed`}
</i18n.Translate> fees={amountDiff(
); historyItem.amountRefreshedRaw,
case "confirm-reserve": { historyItem.amountRefreshedEffective
const exchange = new URL(d.exchangeBaseUrl).host; )}
const pub = abbrev(d.reservePub); />
return (
<i18n.Translate wrap="p">
Started to withdraw
<span>{renderAmount(d.requestedAmount)}</span>
from <span>{exchange}</span> (<span>{pub}</span>).
</i18n.Translate>
); );
} }
case "offer-contract": {
case "order-refused": {
const { merchant, item } = parseSummary(
historyItem.orderShortInfo.summary
);
return ( return (
<i18n.Translate wrap="p"> <HistoryItem
Merchant <em>{abbrev(d.merchantName, 15)}</em> offered contract{" "} icon={"X"}
<span>{abbrev(d.contractTermsHash)}</span>. timestamp={historyItem.timestamp}
</i18n.Translate> small={i18n.str`Order Refused`}
title={merchant}
text={abbrev(item, 30)}
/>
); );
} }
case "depleted-reserve": {
const exchange = d.exchangeBaseUrl case "order-redirected": {
? new URL(d.exchangeBaseUrl).host const { merchant, item } = parseSummary(
: "??"; historyItem.newOrderShortInfo.summary
const amount = renderAmount(d.requestedAmount); );
const pub = abbrev(d.reservePub);
return ( return (
<i18n.Translate wrap="p"> <HistoryItem
Withdrew <span>{amount}</span> from <span>{exchange}</span> ( icon={"⟲"}
<span>{pub}</span>). small={i18n.str`Order redirected`}
</i18n.Translate> text={abbrev(item, 40)}
timestamp={historyItem.timestamp}
title={merchant}
/>
); );
} }
case "pay": {
const url = d.fulfillmentUrl; case "payment-aborted": {
const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>; const { merchant, item } = parseSummary(
historyItem.orderShortInfo.summary
);
return (
<HistoryItem
amount={historyItem.orderShortInfo.amount}
fees={historyItem.amountLost}
icon={"P"}
small={i18n.str`Payment aborted`}
text={abbrev(item, 40)}
timestamp={historyItem.timestamp}
title={merchant}
/>
);
}
case "payment-sent": {
const url = historyItem.orderShortInfo.merchantBaseUrl;
const { merchant, item } = parseSummary(
historyItem.orderShortInfo.summary
);
const fees = amountDiff(
historyItem.amountPaidWithFees,
historyItem.orderShortInfo.amount
);
const fulfillmentLinkElem = ( const fulfillmentLinkElem = (
<a href={url} onClick={openTab(url)}> <Fragment>
view product <a
</a> href={historyItem.orderShortInfo.merchantBaseUrl}
onClick={openTab(url)}
>
{item ? abbrev(item, 30) : null}
</a>
</Fragment>
); );
return ( return (
<i18n.Translate wrap="p"> <HistoryItem
Paid <span>{renderAmount(d.amount)}</span> to merchant{" "} amount={historyItem.orderShortInfo.amount}
<span>{merchantElem}</span>.<span> </span>( fees={fees}
<span>{fulfillmentLinkElem}</span>) icon={"P"}
</i18n.Translate> negative={true}
small={i18n.str`Payment Sent`}
text={fulfillmentLinkElem}
timestamp={historyItem.timestamp}
title={merchant}
/>
);
}
case "order-accepted": {
const url = historyItem.orderShortInfo.merchantBaseUrl;
const { merchant, item } = parseSummary(
historyItem.orderShortInfo.summary
);
const fulfillmentLinkElem = (
<Fragment>
<a
href={historyItem.orderShortInfo.merchantBaseUrl}
onClick={openTab(url)}
>
{item ? abbrev(item, 40) : null}
</a>
</Fragment>
);
return (
<HistoryItem
negative={true}
amount={historyItem.orderShortInfo.amount}
icon={"P"}
small={i18n.str`Order accepted`}
text={fulfillmentLinkElem}
timestamp={historyItem.timestamp}
title={merchant}
/>
);
}
case "reserve-balance-updated": {
return (
<HistoryItem
timestamp={historyItem.timestamp}
small={i18n.str`Reserve balance updated`}
fees={amountDiff(
historyItem.amountExpected,
historyItem.amountReserveBalance
)}
/>
); );
} }
case "refund": { case "refund": {
const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>; const merchantElem = (
<em>{abbrev(historyItem.orderShortInfo.summary, 25)}</em>
);
return ( return (
<i18n.Translate wrap="p"> <HistoryItem
Merchant <span>{merchantElem}</span> gave a refund over{" "} icon={"R"}
<span>{renderAmount(d.refundAmount)}</span>. timestamp={historyItem.timestamp}
</i18n.Translate> small={i18n.str`Payment refund`}
text={merchantElem}
amount={historyItem.amountRefundedRaw}
invalid={historyItem.amountRefundedInvalid}
fees={amountDiff(
amountDiff(
historyItem.amountRefundedRaw,
historyItem.amountRefundedInvalid
),
historyItem.amountRefundedEffective
)}
/>
); );
} }
case "tip": { case "withdrawn": {
const tipPageUrl = new URL(chrome.extension.getURL("/src/webex/pages/tip.html")); const exchange = new URL(historyItem.exchangeBaseUrl).host;
tipPageUrl.searchParams.set("tip_id", d.tipId); const fees = amountDiff(
tipPageUrl.searchParams.set("merchant_domain", d.merchantDomain); historyItem.amountWithdrawnRaw,
const url = tipPageUrl.href; historyItem.amountWithdrawnEffective
const tipLink = <a href={url} onClick={openTab(url)}>{i18n.str`tip`}</a>; );
// i18n: Tip
return ( return (
<> <HistoryItem
<i18n.Translate wrap="p"> amount={historyItem.amountWithdrawnRaw}
Merchant <span>{d.merchantDomain}</span> gave a{" "} fees={fees}
<span>{tipLink}</span> of <span>{renderAmount(d.amount)}</span>. icon={"w"}
</i18n.Translate> small={i18n.str`Withdrawn`}
<span> title={exchange}
{" "} timestamp={historyItem.timestamp}
{d.accepted ? null : ( />
<i18n.Translate>You did not accept the tip yet.</i18n.Translate> );
)} }
</span> case "tip-accepted": {
</> return (
<HistoryItem
icon={"T"}
negative={true}
timestamp={historyItem.timestamp}
title={<i18n.Translate wrap={Fragment}>Tip Accepted</i18n.Translate>}
amount={historyItem.tipAmountRaw}
/>
);
}
case "tip-declined": {
return (
<HistoryItem
icon={"T"}
timestamp={historyItem.timestamp}
title={<i18n.Translate wrap={Fragment}>Tip Declined</i18n.Translate>}
amount={historyItem.tipAmountRaw}
/>
); );
} }
*/
default: default:
return <p>{i18n.str`Unknown event (${historyItem.type})`}</p>; return (
<HistoryItem
timestamp={historyItem.timestamp}
small={i18n.str`${formatAndCapitalize(historyItem.type)}`}
/>
);
} }
} }
const HistoryComponent = (props: any) => {
const record = props.record;
return formatHistoryItem(record);
};
class WalletHistory extends React.Component<any, any> { class WalletHistory extends React.Component<any, any> {
private myHistory: any[]; private myHistory: any[];
private gotError = false; private gotError = false;
private unmounted = false; private unmounted = false;
private hidenTypes: string[] = [
"order-accepted",
"order-redirected",
"refreshed",
"reserve-balance-updated",
"exchange-updated",
"exchange-added"
];
componentWillMount() { componentWillMount() {
this.update(); this.update();
this.setState({ filter: true });
onUpdateNotification(() => this.update()); onUpdateNotification(() => this.update());
} }
@ -468,21 +709,32 @@ class WalletHistory extends React.Component<any, any> {
} }
const listing: any[] = []; const listing: any[] = [];
for (const record of history.reverse()) { const messages = history
const item = ( .reverse()
<div className="historyItem"> .filter(hEvent => {
<div className="historyDate"> if (!this.state.filter) return true;
{new Date(record.timestamp.t_ms).toString()} return this.hidenTypes.indexOf(hEvent.type) === -1;
</div> });
{formatHistoryItem(record)}
</div>
);
for (const record of messages) {
const item = (<HistoryComponent key={record.eventId} record={record} />);
listing.push(item); listing.push(item);
} }
if (listing.length > 0) { if (listing.length > 0) {
return <div className="container">{listing}</div>; return (
<div>
<div className="container">{listing}</div>
<div className="option">
Filtered list{" "}
<input
type="checkbox"
checked={this.state.filter}
onChange={() => this.setState({ filter: !this.state.filter })}
/>
</div>
</div>
);
} }
return <p>{i18n.str`Your wallet has no events recorded.`}</p>; return <p>{i18n.str`Your wallet has no events recorded.`}</p>;
} }