diff --git a/app/.storybook/main.js b/app/.storybook/main.js
index 7312a9811..0fe939139 100644
--- a/app/.storybook/main.js
+++ b/app/.storybook/main.js
@@ -1,5 +1,9 @@
module.exports = {
- stories: ['../src/**/*.stories.(ts|tsx|js|jsx|mdx)'],
+ stories: [
+ // display the Loop page as the first story int he list
+ '../src/__stories__/LoopPage.stories.tsx',
+ '../src/**/*.stories.(ts|tsx|js|jsx|mdx)',
+ ],
addons: [
'@storybook/preset-create-react-app',
'@storybook/addon-actions',
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 980967122..4e316845b 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -2,16 +2,17 @@ import React from 'react';
import './App.scss';
import { createStore, StoreProvider } from 'store';
import { Layout } from 'components/layout';
-import LoopPage from 'components/loop/LoopPage';
+import Pages from 'components/Pages';
import { ThemeProvider } from 'components/theme';
const App = () => {
const store = createStore();
+
return (
-
+
diff --git a/app/src/__stories__/HistoryPage.stories.tsx b/app/src/__stories__/HistoryPage.stories.tsx
new file mode 100644
index 000000000..e386f9bcc
--- /dev/null
+++ b/app/src/__stories__/HistoryPage.stories.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { SwapState, SwapType } from 'types/generated/loop_pb';
+import { useStore } from 'store';
+import HistoryPage from 'components/history/HistoryPage';
+import { Layout } from 'components/layout';
+
+export default {
+ title: 'Pages/History',
+ component: HistoryPage,
+ parameters: { contained: true },
+};
+
+export const Default = () => {
+ const store = useStore();
+ store.swapStore.stopAutoPolling();
+ store.swapStore.sortedSwaps.forEach((s, i) => {
+ if (s.typeName === 'Unknown') s.type = SwapType.LOOP_IN;
+ if (i === 0) s.state = SwapState.INVOICE_SETTLED;
+ });
+ return ;
+};
+
+export const InsideLayout = () => {
+ const store = useStore();
+ store.uiStore.page = 'history';
+ store.swapStore.stopAutoPolling();
+ store.swapStore.sortedSwaps.forEach((s, i) => {
+ if (s.typeName === 'Unknown') s.type = SwapType.LOOP_IN;
+ if (i === 0) s.state = SwapState.INVOICE_SETTLED;
+ });
+ return (
+
+
+
+ );
+};
diff --git a/app/src/__stories__/Layout.stories.tsx b/app/src/__stories__/Layout.stories.tsx
index 5955b0b57..f35ab3cc0 100644
--- a/app/src/__stories__/Layout.stories.tsx
+++ b/app/src/__stories__/Layout.stories.tsx
@@ -4,7 +4,7 @@ import LoopPage from 'components/loop/LoopPage';
import { Layout } from '../components/layout';
export default {
- title: 'Layout',
+ title: 'Components/Layout',
component: Layout,
};
diff --git a/app/src/__stories__/LoopPage.stories.tsx b/app/src/__stories__/LoopPage.stories.tsx
index c903eaa04..e5bc390d5 100644
--- a/app/src/__stories__/LoopPage.stories.tsx
+++ b/app/src/__stories__/LoopPage.stories.tsx
@@ -4,7 +4,7 @@ import { Layout } from 'components/layout';
import LoopPage from 'components/loop/LoopPage';
export default {
- title: 'Loop Page',
+ title: 'Pages/Loop',
component: LoopPage,
parameters: { contained: true },
};
diff --git a/app/src/__tests__/App.spec.tsx b/app/src/__tests__/App.spec.tsx
index fee63a185..deda1586a 100644
--- a/app/src/__tests__/App.spec.tsx
+++ b/app/src/__tests__/App.spec.tsx
@@ -2,8 +2,14 @@ import React from 'react';
import { render } from '@testing-library/react';
import App from '../App';
-it('renders the App', () => {
- const { getByText } = render();
- const linkElement = getByText('Node Status');
- expect(linkElement).toBeInTheDocument();
+describe('App Component', () => {
+ const renderApp = () => {
+ return render();
+ };
+
+ it('should render the App', () => {
+ const { getByText } = renderApp();
+ const linkElement = getByText('Node Status');
+ expect(linkElement).toBeInTheDocument();
+ });
});
diff --git a/app/src/__tests__/Pages.spec.tsx b/app/src/__tests__/Pages.spec.tsx
new file mode 100644
index 000000000..999cb8df0
--- /dev/null
+++ b/app/src/__tests__/Pages.spec.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { renderWithProviders } from 'util/tests';
+import Pages from 'components/Pages';
+
+describe('Pages Component', () => {
+ const render = () => {
+ return renderWithProviders();
+ };
+
+ it('should display the Loop page by default', () => {
+ const { getByText, store } = render();
+ expect(getByText('Lightning Loop')).toBeInTheDocument();
+ expect(store.uiStore.page).toBe('loop');
+ });
+
+ it('should display the History page', () => {
+ const { getByText, store } = render();
+ store.uiStore.goToHistory();
+ expect(getByText('Loop History')).toBeInTheDocument();
+ expect(store.uiStore.page).toBe('history');
+ });
+});
diff --git a/app/src/__tests__/components/history/HistoryPage.spec.tsx b/app/src/__tests__/components/history/HistoryPage.spec.tsx
new file mode 100644
index 000000000..1759e2ed5
--- /dev/null
+++ b/app/src/__tests__/components/history/HistoryPage.spec.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import { renderWithProviders } from 'util/tests';
+import { createStore, Store } from 'store';
+import HistoryPage from 'components/history/HistoryPage';
+
+describe('HistoryPage', () => {
+ let store: Store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.uiStore.goToHistory();
+ });
+
+ const render = () => {
+ return renderWithProviders(, store);
+ };
+
+ it('should display the title', () => {
+ const { getByText } = render();
+ expect(getByText('Loop History')).toBeInTheDocument();
+ });
+
+ it('should display the back link', () => {
+ const { getByText } = render();
+ expect(getByText('Lightning Loop')).toBeInTheDocument();
+ });
+
+ it('should display the back icon', () => {
+ const { getByText } = render();
+ expect(getByText('arrow-left.svg')).toBeInTheDocument();
+ });
+
+ it('should display the export icon', () => {
+ const { getByText } = render();
+ expect(getByText('download.svg')).toBeInTheDocument();
+ });
+
+ it('should display the table headers', () => {
+ const { getByText } = render();
+ expect(getByText('Status')).toBeInTheDocument();
+ expect(getByText('Loop Type')).toBeInTheDocument();
+ expect(getByText('Amount (sats)')).toBeInTheDocument();
+ expect(getByText('Created')).toBeInTheDocument();
+ expect(getByText('Updated')).toBeInTheDocument();
+ });
+
+ it('should navigate back to the Loop Page', () => {
+ const { getByText } = render();
+ fireEvent.click(getByText('arrow-left.svg'));
+ expect(store.uiStore.page).toEqual('loop');
+ });
+});
diff --git a/app/src/__tests__/components/history/HistoryRow.spec.tsx b/app/src/__tests__/components/history/HistoryRow.spec.tsx
new file mode 100644
index 000000000..fafe638bd
--- /dev/null
+++ b/app/src/__tests__/components/history/HistoryRow.spec.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { renderWithProviders } from 'util/tests';
+import { loopListSwaps } from 'util/tests/sampleData';
+import { Swap } from 'store/models';
+import HistoryRow from 'components/history/HistoryRow';
+
+describe('HistoryRow component', () => {
+ let swap: Swap;
+
+ beforeEach(async () => {
+ swap = new Swap(loopListSwaps.swapsList[0]);
+ });
+
+ const render = () => {
+ return renderWithProviders();
+ };
+
+ it('should display the status', () => {
+ const { getByText } = render();
+ expect(getByText(swap.stateLabel)).toBeInTheDocument();
+ });
+
+ it('should display the type', () => {
+ const { getByText } = render();
+ expect(getByText(swap.typeName)).toBeInTheDocument();
+ });
+
+ it('should display the created date', () => {
+ const { getByText } = render();
+ expect(getByText(swap.createdOnLabel)).toBeInTheDocument();
+ });
+
+ it('should display the updated date', () => {
+ const { getByText } = render();
+ expect(getByText(swap.updatedOnLabel)).toBeInTheDocument();
+ });
+});
diff --git a/app/src/__tests__/components/layout/Layout.spec.tsx b/app/src/__tests__/components/layout/Layout.spec.tsx
index d1f7f60b8..a0020577c 100644
--- a/app/src/__tests__/components/layout/Layout.spec.tsx
+++ b/app/src/__tests__/components/layout/Layout.spec.tsx
@@ -21,4 +21,23 @@ describe('Layout component', () => {
fireEvent.click(getByTitle('menu'));
expect(store.settingsStore.sidebarVisible).toBe(true);
});
+
+ it('should navigate to the History page', () => {
+ const { getByText, store } = render();
+ expect(store.uiStore.page).toBe('loop');
+ fireEvent.click(getByText('History'));
+ expect(store.uiStore.page).toBe('history');
+ expect(getByText('History').parentElement).toHaveClass('active');
+ });
+
+ it('should navigate back to the Loop page', () => {
+ const { getByText, store } = render();
+ expect(store.uiStore.page).toBe('loop');
+ fireEvent.click(getByText('History'));
+ expect(store.uiStore.page).toBe('history');
+ expect(getByText('History').parentElement).toHaveClass('active');
+ fireEvent.click(getByText('Lightning Loop'));
+ expect(store.uiStore.page).toBe('loop');
+ expect(getByText('Lightning Loop').parentElement).toHaveClass('active');
+ });
});
diff --git a/app/src/assets/icons/clock.svg b/app/src/assets/icons/clock.svg
new file mode 100644
index 000000000..ea3f5e507
--- /dev/null
+++ b/app/src/assets/icons/clock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/assets/icons/download.svg b/app/src/assets/icons/download.svg
new file mode 100644
index 000000000..76767a924
--- /dev/null
+++ b/app/src/assets/icons/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/components/Pages.tsx b/app/src/components/Pages.tsx
new file mode 100644
index 000000000..b9092b084
--- /dev/null
+++ b/app/src/components/Pages.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+import { useStore } from 'store';
+import HistoryPage from './history/HistoryPage';
+import LoopPage from './loop/LoopPage';
+
+const Pages: React.FC = () => {
+ const { uiStore } = useStore();
+
+ switch (uiStore.page) {
+ case 'history':
+ return ;
+ default:
+ return ;
+ }
+};
+
+export default observer(Pages);
diff --git a/app/src/components/common/PageHeader.tsx b/app/src/components/common/PageHeader.tsx
new file mode 100644
index 000000000..6bb806291
--- /dev/null
+++ b/app/src/components/common/PageHeader.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+import { styled } from 'components/theme';
+import { ArrowLeft, Icon } from './icons';
+import { PageTitle } from './text';
+
+const Styled = {
+ Wrapper: styled.div`
+ display: flex;
+ justify-content: space-between;
+ `,
+ Left: styled.span`
+ flex: 1;
+ text-align: left;
+ `,
+ Center: styled.span`
+ flex: 1;
+ text-align: center;
+ `,
+ Right: styled.span`
+ flex: 1;
+ text-align: right;
+ `,
+ BackLink: styled.a`
+ text-transform: uppercase;
+ font-size: ${props => props.theme.sizes.s};
+ cursor: pointer;
+ color: ${props => props.theme.colors.whitish};
+
+ &:hover {
+ opacity: 80%;
+ }
+ `,
+ BackIcon: styled(ArrowLeft)`
+ margin-right: 5px;
+ `,
+ ActionIcon: styled(Icon)`
+ margin-left: 50px;
+ `,
+};
+
+interface Props {
+ title: string;
+ onBackClick?: () => void;
+ backText?: string;
+ onHistoryClick?: () => void;
+ onExportClick?: () => void;
+}
+
+const PageHeader: React.FC = ({
+ title,
+ onBackClick,
+ backText,
+ onHistoryClick,
+ onExportClick,
+}) => {
+ const { Wrapper, Left, Center, Right, BackLink, BackIcon, ActionIcon } = Styled;
+ return (
+
+
+ {onBackClick && (
+
+
+ {backText}
+
+ )}
+
+
+ {title}
+
+
+ {onHistoryClick && }
+ {onExportClick && }
+
+
+ );
+};
+
+export default observer(PageHeader);
diff --git a/app/src/components/common/base.tsx b/app/src/components/common/base.tsx
index c17faca56..6ff593806 100644
--- a/app/src/components/common/base.tsx
+++ b/app/src/components/common/base.tsx
@@ -53,6 +53,23 @@ export const Button = styled.button`
}
`;
+/**
+ * the react-virtualized list doesn't play nice with the bootstrap row -15px
+ * margin. We need to manually offset the container and remove the
+ * padding from the last column to get the alignment correct
+ */
+export const ListContainer = styled.div`
+ margin-right: -15px;
+
+ .col:last-child {
+ padding-right: 0;
+ }
+
+ *:focus {
+ outline: none;
+ }
+`;
+
/**
* the input[type=range] element. Vendor-specific rules for pseudo
* elements cannot be mixed. As such, there are no shared styles for focus or
diff --git a/app/src/components/common/icons.tsx b/app/src/components/common/icons.tsx
index 78489b00c..11c25dc20 100644
--- a/app/src/components/common/icons.tsx
+++ b/app/src/components/common/icons.tsx
@@ -1,3 +1,8 @@
+import React, { ReactNode } from 'react';
+import { ReactComponent as Clock } from 'assets/icons/clock.svg';
+import { ReactComponent as Download } from 'assets/icons/download.svg';
+import { styled } from 'components/theme';
+
export { ReactComponent as ArrowRight } from 'assets/icons/arrow-right.svg';
export { ReactComponent as ArrowLeft } from 'assets/icons/arrow-left.svg';
export { ReactComponent as Bolt } from 'assets/icons/bolt.svg';
@@ -10,3 +15,46 @@ export { ReactComponent as Menu } from 'assets/icons/menu.svg';
export { ReactComponent as Minimize } from 'assets/icons/minimize.svg';
export { ReactComponent as Maximize } from 'assets/icons/maximize.svg';
export { ReactComponent as Refresh } from 'assets/icons/refresh-cw.svg';
+
+interface IconComponents {
+ clock: ReactNode;
+ download: ReactNode;
+}
+
+const components: IconComponents = {
+ clock: ,
+ download: ,
+};
+
+const Styled = {
+ Wrapper: styled.span`
+ display: inline-block;
+ cursor: pointer;
+ color: ${props => props.theme.colors.whitish};
+
+ &:hover {
+ opacity: 80%;
+ }
+ `,
+ Label: styled.span`
+ margin-left: 5px;
+ `,
+};
+
+interface Props {
+ icon: keyof IconComponents;
+ text?: string;
+ onClick?: () => void;
+ className?: string;
+}
+
+export const Icon: React.FC = ({ icon, text, onClick, className }) => {
+ const { Wrapper, Label } = Styled;
+
+ return (
+
+ {components[icon]}
+ {text && }
+
+ );
+};
diff --git a/app/src/components/history/HistoryList.tsx b/app/src/components/history/HistoryList.tsx
new file mode 100644
index 000000000..d55215a7f
--- /dev/null
+++ b/app/src/components/history/HistoryList.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { AutoSizer, List, WindowScroller } from 'react-virtualized';
+import { observer, Observer } from 'mobx-react-lite';
+import styled from '@emotion/styled';
+import { useStore } from 'store';
+import { ListContainer } from 'components/common/base';
+import HistoryRow, { HistoryRowHeader, ROW_HEIGHT } from './HistoryRow';
+
+const Styled = {
+ Wrapper: styled.div`
+ margin: 100px 0;
+ `,
+};
+
+const HistoryList: React.FC = () => {
+ const { swapStore } = useStore();
+
+ const { Wrapper } = Styled;
+ return (
+
+
+
+
+ {({ width }) => (
+
+ {({ height, isScrolling, onChildScroll, scrollTop }) => (
+
+ {() => (
+ (
+
+ )}
+ scrollTop={scrollTop}
+ width={width}
+ />
+ )}
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default observer(HistoryList);
diff --git a/app/src/components/history/HistoryPage.tsx b/app/src/components/history/HistoryPage.tsx
new file mode 100644
index 000000000..2d3e1489c
--- /dev/null
+++ b/app/src/components/history/HistoryPage.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+import { usePrefixedTranslation } from 'hooks';
+import { useStore } from 'store';
+import PageHeader from 'components/common/PageHeader';
+import { styled } from 'components/theme';
+import HistoryList from './HistoryList';
+
+const Styled = {
+ Wrapper: styled.div`
+ padding: 40px 0;
+ `,
+};
+
+const HistoryPage: React.FC = () => {
+ const { l } = usePrefixedTranslation('cmps.history.HistoryPage');
+ const { uiStore } = useStore();
+
+ const { Wrapper } = Styled;
+ return (
+
+ alert('TODO: Export CSV of Swaps')}
+ />
+
+
+ );
+};
+
+export default observer(HistoryPage);
diff --git a/app/src/components/history/HistoryRow.tsx b/app/src/components/history/HistoryRow.tsx
new file mode 100644
index 000000000..f442fccbc
--- /dev/null
+++ b/app/src/components/history/HistoryRow.tsx
@@ -0,0 +1,85 @@
+import React, { CSSProperties } from 'react';
+import { observer } from 'mobx-react-lite';
+import { usePrefixedTranslation } from 'hooks';
+import { Swap } from 'store/models';
+import { Column, Row } from 'components/common/grid';
+import { Title } from 'components/common/text';
+import SwapDot from 'components/loop/SwapDot';
+import { styled } from 'components/theme';
+
+/**
+ * the virtualized list requires each row to have a specified
+ * height. Defining a const here because it is used in multiple places
+ */
+export const ROW_HEIGHT = 60;
+
+const Styled = {
+ Row: styled(Row)`
+ border-bottom: 0.5px solid ${props => props.theme.colors.darkGray};
+
+ &:last-child {
+ border-bottom-width: 0;
+ }
+ `,
+ Column: styled(Column)`
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: ${ROW_HEIGHT}px;
+ `,
+ StatusTitle: styled(Title)`
+ margin-left: 35px;
+ `,
+ StatusIcon: styled.span`
+ display: inline-block;
+ margin-right: 20px;
+ `,
+};
+
+export const HistoryRowHeader: React.FC = () => {
+ const { l } = usePrefixedTranslation('cmps.history.HistoryRowHeader');
+ const { StatusTitle } = Styled;
+ return (
+
+
+ {l('status')}
+
+
+ {l('type')}
+
+
+ {l('amount')} (sats)
+
+
+ {l('created')}
+
+
+ {l('updated')}
+
+
+ );
+};
+
+interface Props {
+ swap: Swap;
+ style?: CSSProperties;
+}
+
+const HistoryRow: React.FC = ({ swap, style }) => {
+ const { Row, Column, StatusIcon } = Styled;
+ return (
+
+
+
+
+
+ {swap.stateLabel}
+
+ {swap.typeName}
+ {swap.amount.toLocaleString()}
+ {swap.createdOnLabel}
+ {swap.updatedOnLabel}
+
+ );
+};
+
+export default observer(HistoryRow);
diff --git a/app/src/components/layout/NavMenu.tsx b/app/src/components/layout/NavMenu.tsx
index dc7d00d7c..b9d6d8006 100644
--- a/app/src/components/layout/NavMenu.tsx
+++ b/app/src/components/layout/NavMenu.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+import { observer } from 'mobx-react-lite';
import { usePrefixedTranslation } from 'hooks';
+import { useStore } from 'store';
import { Title } from 'components/common/text';
import { styled } from 'components/theme';
@@ -14,14 +16,16 @@ const Styled = {
`,
NavItem: styled.li`
font-size: ${props => props.theme.sizes.s};
+ margin-right: -17px;
- a {
+ span {
display: block;
height: 50px;
line-height: 50px;
padding: 0 12px;
border-left: 3px solid transparent;
color: ${props => props.theme.colors.whitish};
+ cursor: pointer;
&:hover {
text-decoration: none;
@@ -29,10 +33,9 @@ const Styled = {
}
}
- &.active a {
+ &.active span {
border-left: 3px solid ${props => props.theme.colors.whitish};
background-color: ${props => props.theme.colors.blue};
- margin-right: -17px;
&:hover {
border-left: 3px solid ${props => props.theme.colors.pink};
@@ -42,23 +45,26 @@ const Styled = {
};
const NavMenu: React.FC = () => {
- const { NavTitle, Nav, NavItem } = Styled;
-
const { l } = usePrefixedTranslation('cmps.layout.NavMenu');
+ const { uiStore } = useStore();
+ const { NavTitle, Nav, NavItem } = Styled;
return (
<>
{l('menu')}
>
);
};
-export default NavMenu;
+export default observer(NavMenu);
diff --git a/app/src/components/loop/LoopPage.tsx b/app/src/components/loop/LoopPage.tsx
index 1dcfb526c..6310ca077 100644
--- a/app/src/components/loop/LoopPage.tsx
+++ b/app/src/components/loop/LoopPage.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { observer } from 'mobx-react-lite';
import { usePrefixedTranslation } from 'hooks';
import { useStore } from 'store';
-import { PageTitle } from 'components/common/text';
+import PageHeader from 'components/common/PageHeader';
import { styled } from 'components/theme';
import ChannelList from './ChannelList';
import LoopActions from './LoopActions';
@@ -29,7 +29,11 @@ const LoopPage: React.FC = () => {
) : (
<>
- {l('pageTitle')}
+ alert('TODO: Export CSV of Channels')}
+ />
>
diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json
index 161c7d849..fe84bb37b 100644
--- a/app/src/i18n/locales/en-US.json
+++ b/app/src/i18n/locales/en-US.json
@@ -1,4 +1,11 @@
{
+ "cmps.history.HistoryPage.backText": "Lightning Loop",
+ "cmps.history.HistoryPage.pageTitle": "Loop History",
+ "cmps.history.HistoryRowHeader.status": "Status",
+ "cmps.history.HistoryRowHeader.amount": "Amount",
+ "cmps.history.HistoryRowHeader.type": "Loop Type",
+ "cmps.history.HistoryRowHeader.created": "Created",
+ "cmps.history.HistoryRowHeader.updated": "Updated",
"cmps.loop.ChannelRowHeader.canReceive": "Can Receive",
"cmps.loop.ChannelRowHeader.canSend": "Can Send",
"cmps.loop.ChannelRowHeader.upTime": "Up Time %",
@@ -26,6 +33,7 @@
"cmps.loop.swap.SwapReviewStep.total": "Invoice Total",
"cmps.layout.NavMenu.menu": "Menu",
"cmps.layout.NavMenu.loop": "Lightning Loop",
+ "cmps.layout.NavMenu.history": "History",
"cmps.layout.NavMenu.settings": "Settings",
"cmps.NodeStatus.title": "Node Status"
}
diff --git a/app/src/store/models/swap.ts b/app/src/store/models/swap.ts
index 9cef42cfa..e2c68d999 100644
--- a/app/src/store/models/swap.ts
+++ b/app/src/store/models/swap.ts
@@ -80,11 +80,21 @@ export default class Swap {
return new Date(this.initiationTime / 1000 / 1000);
}
+ /** The date this swap was created as formatted string */
+ @computed get createdOnLabel() {
+ return `${this.createdOn.toLocaleDateString()} ${this.createdOn.toLocaleTimeString()}`;
+ }
+
/** The date this swap was last updated as a JS Date object */
@computed get updatedOn() {
return new Date(this.lastUpdateTime / 1000 / 1000);
}
+ /** The date this swap was last updated as formatted string */
+ @computed get updatedOnLabel() {
+ return `${this.updatedOn.toLocaleDateString()} ${this.updatedOn.toLocaleTimeString()}`;
+ }
+
/**
* Updates this swap model using data provided from the Loop GRPC api
* @param loopSwap the swap data
diff --git a/app/src/store/stores/uiStore.ts b/app/src/store/stores/uiStore.ts
index 5699f36c9..3d9be1d91 100644
--- a/app/src/store/stores/uiStore.ts
+++ b/app/src/store/stores/uiStore.ts
@@ -1,15 +1,40 @@
import { action, observable } from 'mobx';
import { Store } from 'store';
+type PageName = 'loop' | 'history';
+
export default class UiStore {
private _store: Store;
+ /** the current page being displayed */
+ @observable page: PageName = 'loop';
+ /** indicates if the Processing Loops section is displayed on the Loop page */
@observable processingSwapsVisible = false;
constructor(store: Store) {
this._store = store;
}
+ /**
+ * Change to the Loop page
+ */
+ @action.bound
+ goToLoop() {
+ this.page = 'loop';
+ this._store.log.info('Go to the Loop page');
+ }
+
+ /**
+ * Change to the History page
+ */
+ @action.bound
+ goToHistory() {
+ this.page = 'history';
+ }
+
+ /**
+ * Toggle displaying of the Processing Loops section
+ */
@action.bound
toggleProcessingSwaps() {
this.processingSwapsVisible = !this.processingSwapsVisible;
diff --git a/app/src/util/tests/sampleData.ts b/app/src/util/tests/sampleData.ts
index 5ac0f1f89..4413c37a3 100644
--- a/app/src/util/tests/sampleData.ts
+++ b/app/src/util/tests/sampleData.ts
@@ -117,7 +117,7 @@ export const loopListSwaps: LOOP.ListSwapsResponse.AsObject = {
type: (i % 3) as LOOP.SwapStatus.AsObject['type'],
state: i % 2 ? LOOP.SwapState.SUCCESS : LOOP.SwapState.FAILED,
initiationTime: 1586390353623905000 + i * 100000000000000,
- lastUpdateTime: 1586398369729857000,
+ lastUpdateTime: 1586398369729857000 + i * 200000000000000,
htlcAddress: 'bcrt1qzu4077erkr78k52yuf2rwkk6ayr6m3wtazdfz2qqmd7taa5vvy9s5d75gd',
costServer: 66,
costOnchain: 6812,