From adeb1e62356e2f2bb42e88cb45f1258c53d1471b Mon Sep 17 00:00:00 2001 From: Nikola Glumac Date: Fri, 27 Sep 2019 12:42:43 +0200 Subject: [PATCH] [DDW-901] Newsfeed (#1570) * [DDW-901] Adds more content fields to component and story, aligns exit btn * [DDW-901] Fixes ESLint warnings * [DDW-901] Introduce news feed store observers * [DDW-901] Flow fixes * [DDW-901] Fixes file names * [DDW-901] Passes data from newsfeed store to container to component * [DDW-901] News feed - creating News feed component - adding News Feed - no news feed items * [DDW-901] News feed - creating News feed component - adding News Feed container * [DDW-901] Adjusts rendering of props * [DDW-901] News feed - creating News feed component - adding News Feed container * [DDW-901] basic setup for patching news feed api in tests * [DDW-901] NewsFeed store improvement * [DDW-901] News feed - creating News feed component - adding toggle action on app store * [DDW-901] News feed - creating News feed component - adding toggle button on top bar * [DDW-901] News feed - creating News feed component - adding toggle button on top bar * [DDW-901] News feed - creating News feed component - small fix * [DDW-901] Flow improvements * [DDW-901] News feed - creating News feed component - adding toggle button on loading screen * [DDW-901] News feed - creating News feed component - small hover fix * [DDW-901] add read news local storage api * [DDW-901] remove old comments * [DDW-901] implement news feed test setup steps * [DDW-901] News feed - creating News feed component - adding News Feed open/close animation * [DDW-901] News feed - creating News feed component - adding toggle button on loading screen * [DDW-901] Alerts and Incidents * [DDW-901] refactor news feed test steps * [DDW-901] News feed - creating News feed component - adding toggle button animation * [DDW-901] News feed - creating News feed component - adding toggle button on loading screen * [DDW-901] News feed - creating News feed component - fixing storybook * [DDW-901] News feed - creating News feed component - renaming storybook domain name for news * [DDW-901] Updates styles for incident and alert overlay * [DDW-901] Handle Markdown in News Item content * [DDW-901] Fix syncing tooltip * [DDW-901] News feed - creating News feed component - adding News Item component * [DDW-901] News feed - creating News feed component - adding News Item component * [DDW-901] News feed - creating News feed component - adding News Item component * [DDW-901] refactor and implement more acceptance tests * [DDW-901] News feed - creating News feed component - fixing News Item component in storybook * [DDW-901] News feed - creating News feed component - fixing storybook * [DDW-901] Introduce WalletsCollection class with helper methods * [DDW-901] Update markdown content * [DDW-901] News feed - creating News feed component - News Item expanding/collapsing * [DDW-901] News Store hotfix * [DDW-901] Continues adjusting styles of AlertOverlay * [DDW-901] Update markdown * [DDW-901] Adds content box scrolling, and scrollbar thumb styles * [DDW-901] News feed sidebar animation update * [DDW-901] News feed - creating News feed component - News Item date format * [DDW-901] Update news fetching state * [DDW-901] Introduce markAsRead @action * [DDW-901] Fix environment label * Adds counter to AlertsOverlay, adds styles to IncidentOverlay * [DDW-901] News feed - creating News feed component - News Feed scroll * [DDW-901] Adds onMarkNewsAsRead handler to onClose func * [DDW-901] Introduce poller interval set/reset on initial fetching error * [DDW-901] Improve mobX handlers to prevent set issues * [DDW-901] News feed - creating News feed component - News Item * [DDW-901] Corrects colors for IncidentOverlay and formats markdown for Alerts and Incidents * [DDW-901] Removes alert icon from AlertsOverlay and IncidentOverlay * [DDW-901] Changes import order of assets in overlays * [DDW-901] Bell icon logic * [DDW-901] fix some minor issues * [DDW-901] merging wip stash * [DDW-901] Use correct news data * [DDW-901] Fix observable issue * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] fix broken news collection * [DDW-901] show news item badge only for unread items * [DDW-901] Introduce targetVersion news check * [DDW-901] Remove unnecessary log * Updates content 2 Expands dummy data for content 2 * Changes item 9 to an incident * [DDW-901] add advanced incident content * [DDW-901] only show news icon dot for unread announcements * [DDW-901] fix wrong content assignment in news feed store * [DDW-901] update some tests to new requirements * [DDW-901] Fixes fixed positioning of overlays, content min-width, and styling of ordered lists in markdown * [DDW-901] News feed - creating News feed component - News Item updates for content styling * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] Flow improvements * [DDW-901] Remwove unnecessary log * [DDW-901] Update bell highlighted state * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] Safe length check * [DDW-901] NewsFeed story data improvement * [DDW-901] Merge and fix conflicts * [DDW-901] Adds CHANGELOG entry * [DDW-901] Fixes styling in overlays * [DDW-901] Fixes ESLint errors in overlays * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] Misha DAVAY! * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] Changes alert counter to --font-medium, and Misha Davay * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] News feed - creating News feed component - News Item updates * [DDW-901] News feed - creating News feed component - News Item spinner update * [DDW-901] News feed - creating News feed component - News Item spinner update * [DDW-901] Changes news feed to newsfeed throughout app * [DDW-901] Fixes ESLint error * [DDW-901] Updates defaultMessages.json * [DDW-901] implement 80% of tests * [DDW-901] fix newsfeed state logic when news feed couldnt be fetched * [DDW-901] Change dummy JSN file location and fetch from daedaluswallet.io * [DDW-901] fix broken newsfeed fetching state * [DDW-901] News feed - creating News feed component - News Item height update, expanded/collapsed state update * [DDW-901] Improve fetching spinner apeareance logic and new storybook story * [DDW-901] Fix white theme * [DDW-901] Fix news item spacing * [DDW-901] News feed - creating News feed component - News Item height update, expanded/collapsed state update * [DDW-901] News feed - creating News feed component - News Item height update, expanded/collapsed state update * [DDW-901] News feed - creating News feed component - News Item height update, expanded/collapsed state update * [DDW-901] News feed - creating News feed component - News Item color update * Dummy News JSON update * [DDW-901] Adds action for opening an alert * Dummy News JSON update * [DDW-901] News feed - creating News feed component - News Item color update * [DDW-901] Update animation * [DDW-901] News feed - creating News feed component - News Item color update * [DDW-901] Prevent multiple markingAsRead if already marked * [DDW-901] optimize newsfeed tests for speed * [DDW-901] News feed - creating News feed component - News Item color update * [DDW-901] Fix line breaks and scroll issues, Add markdown styling * [DDW-901] News feed - creating News feed component - News Item color update * [DDW-901] Introduce news content link click handler * [DDW-901] News feed - creating News feed component - News Item color update * [DDW-901] News feed - creating News feed component - News Item color update * [DDW-901] Click handlers * [DDW-901] Fixes open and close action logic for AlertsOverlay * [DDW-901] Scss rules alphabetic order * [DDW-901] Fixes flow error in NewsFeedStore.js * [DDW-901] Fixes flow error in AlertOverlay.stories.js * [DDW-901] add smooth news item expanding animation * [DDW-901] Introduce internal github request to JSON news files * [DDW-901] Freeze package.json * [DDW-901] disallow unused markdown types in newsfeed items * [DDW-901] Removes min-height from content of AlertsOverlay.scss * [DDW-901] Inject Japanese translations * [DDW-901] Adds external link icon to action buttons in overlays * [DDW-901] Drop shadow when scrolling * [DDW-901] Introduce incident interval * [DDW-901] Fetch news from daedalus.io * [DDW-901] Reposition Alert overlay counter * [DDW-901] Match multiple target platforms from single item * [DDW-901] Disable incident/alert overlays in specific cases and overlays opacity improvement * [DDW-901] Improve getNews api response type * [DDW-901] Add news item route action handler * [DDW-901] Alerts overlay counter and internal actions button improvements * [DDW-901] Introduce news feed title generator and dummy JSON file with no internal route actions * [DDW-901] Rename newsfeed files * [DDW-901] Restore failed test scenario screenshots * [DDW-901] Fix Bell icon position on connecting screen * [DDW-901] Fetch news over HTTPS * [DDW-901] Fix E2E tests * [DDW-901] Remove unnecessary logs * [DDW-901] Improve eslint config and fix feature/test eslint issue * [DDW-901] Improve news unread dot margin * [DDW-901] Center newsfeed header badge counter * [DDW-901] Collapse all expanded items on newsfeed sidebar close * [DDW-901] Remove unnecessary @watch flags * [DDW-901] Fix theming issues on profile screens * [DDW-901] Small code cleanup * [DDW-901] Code style improvements * [DDW-901] Add news bell icon to storybook layouts * [DDW-901] Verify newsfeed using hashes --- .eslintignore | 2 +- CHANGELOG.md | 3 +- features/data-layer-migration.feature | 2 +- features/news-feed.feature | 81 ------- features/newsfeed.feature | 87 +++++++ features/tests/e2e/documents/dummy-news.json | 213 ++++++++++++++++ .../paper-wallet-certificate.pdf | Bin 1714657 -> 1714525 bytes .../e2e/helpers/language-selection-helpers.js | 1 - .../tests/e2e/helpers/terms-of-use-helpers.js | 1 - features/tests/e2e/setup/electron.js | 21 +- features/tests/e2e/steps/app-steps.js | 24 ++ features/tests/e2e/steps/helper-steps.js | 10 +- features/tests/e2e/steps/newsfeed-steps.js | 227 ++++++++++++++++++ .../steps/node-update-notification-steps.js | 4 +- ...llet-recovery-phrase-verification-steps.js | 16 +- .../tests/e2e/steps/wallets-utxos-steps.js | 1 - features/tests/unit/setup/utxo-helpers.js | 2 +- .../generate-filename-with-timestamp-steps.js | 2 +- features/tests/unit/steps/mnemonics-steps.js | 9 +- .../spending-password-validation-steps.js | 3 +- .../tests/unit/steps/utxos-chart-steps.js | 12 +- ...tings-recovery-phrase-verification.feature | 3 +- gulpfile.js | 6 +- package.json | 3 + source/renderer/app/App.js | 32 ++- source/renderer/app/actions/app-actions.js | 1 + source/renderer/app/api/api.js | 48 ++++ .../renderer/app/api/news/requests/getNews.js | 19 ++ .../app/api/news/requests/getNewsHash.js | 18 ++ source/renderer/app/api/news/types.js | 41 ++++ .../renderer/app/api/utils/externalRequest.js | 8 +- source/renderer/app/api/utils/hashing.js | 9 + source/renderer/app/api/utils/localStorage.js | 45 ++++ source/renderer/app/api/utils/patchAdaApi.js | 26 ++ .../images/top-bar/news-feed-icon.inline.svg | 5 + .../syncing-connecting/SyncingConnecting.js | 19 ++ .../system-time-error/SystemTimeError.scss | 2 +- .../app/components/news/AlertsOverlay.js | 118 +++++++++ .../app/components/news/AlertsOverlay.scss | 187 +++++++++++++++ .../app/components/news/IncidentOverlay.js | 68 ++++++ .../app/components/news/IncidentOverlay.scss | 143 +++++++++++ .../renderer/app/components/news/NewsFeed.js | 176 ++++++++++++++ .../app/components/news/NewsFeed.scss | 133 ++++++++++ .../renderer/app/components/news/NewsItem.js | 171 +++++++++++++ .../app/components/news/NewsItem.scss | 159 ++++++++++++ .../wallet/layouts/WalletWithNavigation.scss | 2 +- .../app/components/widgets/LoadingSpinner.js | 8 +- .../components/widgets/LoadingSpinner.scss | 10 + .../app/components/widgets/NewsFeedIcon.js | 28 +++ .../app/components/widgets/NewsFeedIcon.scss | 57 +++++ .../components/widgets/NodeSyncStatusIcon.js | 4 +- .../widgets/NodeSyncStatusIcon.scss | 18 +- .../widgets/WalletTestEnvironmentLabel.scss | 21 +- source/renderer/app/config/news.dummy.json | 161 +++++++++++++ source/renderer/app/config/timingConfig.js | 3 + source/renderer/app/config/urlsConfig.js | 18 ++ .../app/containers/TopBarContainer.js | 13 +- .../loading/SyncingConnectingPage.js | 6 + .../app/containers/news/NewsFeedContainer.js | 43 ++++ .../containers/news/NewsOverlayContainer.js | 62 +++++ source/renderer/app/domains/News.js | 150 ++++++++++++ source/renderer/app/i18n/locales/de-DE.json | 3 + .../app/i18n/locales/defaultMessages.json | 47 ++++ source/renderer/app/i18n/locales/en-US.json | 3 + source/renderer/app/i18n/locales/hr-HR.json | 3 + source/renderer/app/i18n/locales/ja-JP.json | 3 + source/renderer/app/i18n/locales/ko-KR.json | 3 + source/renderer/app/i18n/locales/zh-CN.json | 3 + source/renderer/app/stores/AppStore.js | 8 + source/renderer/app/stores/NewsFeedStore.js | 193 +++++++++++++++ source/renderer/app/stores/index.js | 4 + .../renderer/app/themes/daedalus/cardano.js | 40 ++- .../renderer/app/themes/daedalus/dark-blue.js | 32 +++ .../app/themes/daedalus/dark-cardano.js | 34 ++- .../app/themes/daedalus/light-blue.js | 36 ++- source/renderer/app/themes/daedalus/white.js | 36 ++- source/renderer/app/themes/daedalus/yellow.js | 34 ++- .../renderer/app/themes/utils/createTheme.js | 33 +++ source/renderer/app/utils/network.js | 44 ++++ .../Loading-SyncingConnecting.stories.js | 15 ++ storybook/stories/TopBar.stories.js | 10 + storybook/stories/index.js | 5 + .../stories/news/AlertsOverlay.stories.js | 59 +++++ .../stories/news/IncidentOverlay.stories.js | 29 +++ storybook/stories/news/NewsFeed.stories.js | 184 ++++++++++++++ storybook/stories/support/StoryLayout.js | 5 + yarn.lock | 15 +- 87 files changed, 3455 insertions(+), 191 deletions(-) delete mode 100644 features/news-feed.feature create mode 100644 features/newsfeed.feature create mode 100644 features/tests/e2e/documents/dummy-news.json create mode 100644 features/tests/e2e/steps/newsfeed-steps.js create mode 100644 source/renderer/app/api/news/requests/getNews.js create mode 100644 source/renderer/app/api/news/requests/getNewsHash.js create mode 100644 source/renderer/app/api/news/types.js create mode 100644 source/renderer/app/api/utils/hashing.js create mode 100644 source/renderer/app/assets/images/top-bar/news-feed-icon.inline.svg create mode 100644 source/renderer/app/components/news/AlertsOverlay.js create mode 100644 source/renderer/app/components/news/AlertsOverlay.scss create mode 100644 source/renderer/app/components/news/IncidentOverlay.js create mode 100644 source/renderer/app/components/news/IncidentOverlay.scss create mode 100644 source/renderer/app/components/news/NewsFeed.js create mode 100644 source/renderer/app/components/news/NewsFeed.scss create mode 100644 source/renderer/app/components/news/NewsItem.js create mode 100644 source/renderer/app/components/news/NewsItem.scss create mode 100644 source/renderer/app/components/widgets/NewsFeedIcon.js create mode 100644 source/renderer/app/components/widgets/NewsFeedIcon.scss create mode 100644 source/renderer/app/config/news.dummy.json create mode 100644 source/renderer/app/containers/news/NewsFeedContainer.js create mode 100644 source/renderer/app/containers/news/NewsOverlayContainer.js create mode 100644 source/renderer/app/domains/News.js create mode 100644 source/renderer/app/stores/NewsFeedStore.js create mode 100644 storybook/stories/news/AlertsOverlay.stories.js create mode 100644 storybook/stories/news/IncidentOverlay.stories.js create mode 100644 storybook/stories/news/NewsFeed.stories.js diff --git a/.eslintignore b/.eslintignore index c7a5f674a7..23fdd0c543 100755 --- a/.eslintignore +++ b/.eslintignore @@ -5,7 +5,7 @@ main.js logs node_modules translations -tests +./tests features/tests/e2e/documents/* mainnet-genesis-dryrun-with-stakeholders.json source/renderer/app/i18n/locales diff --git a/CHANGELOG.md b/CHANGELOG.md index eb74603c1f..2b5ed93062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Changelog ### Features +- Implemented "Newsfeed" feature ([PR 1570](https://github.com/input-output-hk/daedalus/pull/1570)) - Implemented wallet recovery phrase verification ([PR 1565](https://github.com/input-output-hk/daedalus/pull/1565)) - Removed select dropdown arrow ([PR 1550](https://github.com/input-output-hk/daedalus/pull/1550)) - Implemented automated and manual update flows unification ([PR 1491](https://github.com/input-output-hk/daedalus/pull/1491)) @@ -44,7 +45,7 @@ Changelog ### Specifications -- News feed ([PR 1569](https://github.com/input-output-hk/daedalus/pull/1569)) +- Newsfeed ([PR 1569](https://github.com/input-output-hk/daedalus/pull/1569)) ## 0.14.0 diff --git a/features/data-layer-migration.feature b/features/data-layer-migration.feature index 71b8491821..08d9f44a4f 100644 --- a/features/data-layer-migration.feature +++ b/features/data-layer-migration.feature @@ -15,6 +15,6 @@ Feature: Data Layer Migration | Wallet | | Then I should see the Data Layer Migration screen When I click the migration button - Then I should see the initial screen + Then I should see the main ui When I refresh the main window Then I should not see the Data Layer Migration screen diff --git a/features/news-feed.feature b/features/news-feed.feature deleted file mode 100644 index 92776ee5c8..0000000000 --- a/features/news-feed.feature +++ /dev/null @@ -1,81 +0,0 @@ -@wip -Feature: News feed - - News feed delivers important information to Daedalus users, like information about network failures, bugs and other - issues, information about upcoming and recent releases and so on. News are categorised in 4 types: - incident, alert, announcement and info. Incidents and alerts cover the whole user interface, - announcements and info are delivered in the news feed part of the user interface. - - Scenario: Bell icon is highlighted when there are unread news - - Given I have Daedalus running - And there are unread news - Then the bell icon is highlighted - - Scenario: Bell icon is not highlighted when there are no unread news - - Given I have Daedalus running - And there are no unread news - Then the bell icon is not highlighted - - Scenario: No news available - - Given I have Daedalus running - And there is no news - Then the bell icon is not highlighted - When I click on the bell icon - Then the news feed is opened - And the news feed contains "no news available" message - - Scenario: Only read news available - - Given I have Daedalus running - And there are 5 read news - When I click on the bell icon - Then the news feed is opened - And the news feed contains 5 read news - - Scenario: Displaying an incident - - Given I have Daedalus running - And there is an incident - Then the incident will cover the screen - - Scenario: Dismissing an alert - - Given I have Daedalus running - And there are unread alerts - Then the latest alert will cover the screen - When I dismiss the alert - Then the alert I have dismissed becomes read - - Scenario: Reading an announcement - - Given I have Daedalus running - And there is 1 unread announcement - When I click on the bell icon - Then the news feed is opened - And the news feed contains 1 unread announcement - When I click on the unread announcement to expand it - Then the announcement content is shown - And the announcement is marked as read - - Scenario: Reading an info - - Given I have Daedalus running - And there is 1 unread info - When I click on the bell icon - Then the news feed is opened - And the news feed contains 1 unread info - When I click on the unread info to expand it - Then the info content is shown - And the info is marked as read - And the bell icon is not highlighted - - Scenario: News unavailable - - Given I have Daedalus running - And news feed server is unreachable - When I click on the bell icon - Then the news feed is opened - And the news feed contains "news unavailable message" diff --git a/features/newsfeed.feature b/features/newsfeed.feature new file mode 100644 index 0000000000..9a110492ed --- /dev/null +++ b/features/newsfeed.feature @@ -0,0 +1,87 @@ +@e2e @newsfeed @noReload +Feature: Newsfeed + + Newsfeed delivers important information to Daedalus users, like information about network failures, bugs and other + issues, information about upcoming and recent releases and so on. News are categorised in 4 types: + incident, alert, announcement and info. Incidents and alerts cover the whole user interface, + announcements and info are delivered in the newsfeed part of the user interface. + + Background: + Given I have completed the basic setup + + @reconnectApp + Scenario: Newsfeed icon is visible on the connecting screen + Given im on the connecting screen + Then i should see the newsfeed icon + + @reconnectApp + Scenario: Newsfeed icon is visible on the syncing screen + Given im on the syncing screen + Then i should see the newsfeed icon + + Scenario: Newsfeed icon is visible in the main ui + Given I should see the main ui + Then i should see the newsfeed icon + + Scenario: Newsfeed icon is highlighted when there are unread infos + Given there are unread infos + Then the newsfeed icon is highlighted + + Scenario: Newsfeed icon is highlighted when there are unread announcements + Given there are unread announcements + Then the newsfeed icon is highlighted + + Scenario: Newsfeed icon is not highlighted when all infos have been read + Given there are read infos + Then the newsfeed icon is not highlighted + + Scenario: Open the newsfeed by clicking the newsfeed icon + Given I click on the newsfeed icon + Then the newsfeed is open + + Scenario: No news available in the feed + Given there is no news + When I open the newsfeed + Then the newsfeed is empty + + Scenario: Only read infos available in the feed + Given there are 2 read infos + When I open the newsfeed + Then the newsfeed contains 2 read infos + + Scenario: Displaying an incident + Given there is an incident + Then the incident will cover the screen + + Scenario: Dismissing an alert + Given there is 1 unread alert + When I dismiss the alert + Then the alert disappears + + Scenario: Opening and dismissing a read alert + Given there is 1 read alert + When I open the newsfeed + When I click on the alert in the newsfeed + Then the alert overlay opens + When I dismiss the alert + Then the alert disappears + + Scenario: Reading an announcement + Given there is 1 unread announcement + When I open the newsfeed + And I click on the unread announcement to expand it + Then the announcement content is shown + And the announcement is marked as read + + Scenario: Reading an info + Given there is 1 unread info + When I open the newsfeed + And I click on the unread info to expand it + Then the info content is shown + And the info is marked as read + And the newsfeed icon is not highlighted + + Scenario: News unavailable + Given the newsfeed server is unreachable + When I open the newsfeed + Then no news are available diff --git a/features/tests/e2e/documents/dummy-news.json b/features/tests/e2e/documents/dummy-news.json new file mode 100644 index 0000000000..3d0ca9df4d --- /dev/null +++ b/features/tests/e2e/documents/dummy-news.json @@ -0,0 +1,213 @@ +{ + "updatedAt": 1569324863299, + "items": [ + { + "title": { + "en-US": "Incident 1 in English", + "ja-JP": "Incident 1 in Japanese" + }, + "content": { + "en-US": "# h1 English incident content 1\nUt consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu.\n\n## h2 Heading\n\nConsequat mauris nunc congue nisi vitae suscipit. Dictum non consectetur a erat nam. Laoreet non curabitur gravida arcu ac tortor dignissim. Eu augue ut lectus arcu bibendum at. Facilisis gravida neque convallis a cras semper. Ut consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu. Nam aliquam sem et tortor consequat id porta nibh venenatis.\n\nViverra nam libero justo laoreet sit amet. Pharetra diam sit amet nisl. Quam viverra orci sagittis eu. Rhoncus dolor purus non enim. Posuere urna nec tincidunt praesent semper feugiat. Suspendisse in est ante in nibh mauris cursus. Sit amet consectetur adipiscing elit duis. Tortor id aliquet lectus proin nibh nisl condimentum id. At in tellus integer feugiat scelerisque. Maecenas sed enim ut sem viverra aliquet. Pellentesque pulvinar pellentesque habitant morbi. Ultrices neque ornare aenean euismod elementum nisi quis eleifend. Praesent tristique magna sit amet purus gravida. Diam volutpat commodo sed egestas egestas. Ut placerat orci nulla pellentesque dignissim enim. Ultrices in iaculis nunc sed augue lacus viverra. Etiam sit amet nisl purus.\n\n## Typographic replacements\n\nEnable typographer option to see result.\n\n(c) (C) (r) (R) (tm) (TM) (p) (P) +-\n\ntest.. test... test..... test?..... test!....\n\n!!!!!! ???? ,, -- ---\n\n\"Smartypants, double quotes\" and 'single quotes'\n\n\n## Emphasis\n\n**This is bold text**\n\n__This is bold text__\n\n*This is italic text*\n\n_This is italic text_\n\n## Lists\n\nUnordered\n\n+ Create a list by starting a line with `+`, `-`, or `*`\n+ Sub-lists are made by indenting 2 spaces:\n+ Very easy!\n\nOrdered\n\n1. Lorem ipsum dolor sit amet\n2. Consectetur adipiscing elit\n3. Integer molestie lorem at massa\n\n\n1. You can use sequential numbers...\n1. ...or keep all the numbers as `1.`\n\n## Links\n\n[link text](http://dev.nodeca.com)\n\n[link with title](http://nodeca.github.io/pica/demo/ \"title text!\")\n\nAutoconverted link https://github.com/nodeca/pica (enable linkify to see)\n\n### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)\n\n- 19^th^\n- H~2~O", + "ja-JP": "# h1 Japanese incident content 1\nUt consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu.\n\n## h2 Heading\n\nConsequat mauris nunc congue nisi vitae suscipit. Dictum non consectetur a erat nam. Laoreet non curabitur gravida arcu ac tortor dignissim. Eu augue ut lectus arcu bibendum at. Facilisis gravida neque convallis a cras semper. Ut consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu. Nam aliquam sem et tortor consequat id porta nibh venenatis.\n\nViverra nam libero justo laoreet sit amet. Pharetra diam sit amet nisl. Quam viverra orci sagittis eu. Rhoncus dolor purus non enim. Posuere urna nec tincidunt praesent semper feugiat. Suspendisse in est ante in nibh mauris cursus. Sit amet consectetur adipiscing elit duis. Tortor id aliquet lectus proin nibh nisl condimentum id. At in tellus integer feugiat scelerisque. Maecenas sed enim ut sem viverra aliquet. Pellentesque pulvinar pellentesque habitant morbi. Ultrices neque ornare aenean euismod elementum nisi quis eleifend. Praesent tristique magna sit amet purus gravida. Diam volutpat commodo sed egestas egestas. Ut placerat orci nulla pellentesque dignissim enim. Ultrices in iaculis nunc sed augue lacus viverra. Etiam sit amet nisl purus.\n\n## Typographic replacements\n\nEnable typographer option to see result.\n\n(c) (C) (r) (R) (tm) (TM) (p) (P) +-\n\ntest.. test... test..... test?..... test!....\n\n!!!!!! ???? ,, -- ---\n\n\"Smartypants, double quotes\" and 'single quotes'\n\n\n## Emphasis\n\n**This is bold text**\n\n__This is bold text__\n\n*This is italic text*\n\n_This is italic text_\n\n## Lists\n\nUnordered\n\n+ Create a list by starting a line with `+`, `-`, or `*`\n+ Sub-lists are made by indenting 2 spaces:\n+ Very easy!\n\nOrdered\n\n1. Lorem ipsum dolor sit amet\n2. Consectetur adipiscing elit\n3. Integer molestie lorem at massa\n\n\n1. You can use sequential numbers...\n1. ...or keep all the numbers as `1.`\n\n## Links\n\n[link text](http://dev.nodeca.com)\n\n[link with title](http://nodeca.github.io/pica/demo/ \"title text!\")\n\nAutoconverted link https://github.com/nodeca/pica (enable linkify to see)\n\n### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)\n\n- 19^th^\n- H~2~O" + }, + "target": { + "daedalusVersion": null, + "platforms":["darwin","win32","linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569325866799, + "type": "incident" + }, + { + "title": { + "en-US": "Alert 1 in English", + "ja-JP": "Alert 1 in Japanese" + }, + "content": { + "en-US": "# h1 English alert content 1", + "ja-JP": "# h1 Japanese alert content 1" + }, + "target": { + "daedalusVersion": null, + "platforms":["darwin","win32","linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569152525812, + "type": "alert" + }, + { + "title": { + "en-US": "Info 1 in English", + "ja-JP": "Info 1 in Japanese" + }, + "content": { + "en-US": "# h1 English info content 1\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese info content 1\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569324863299, + "type": "info" + }, + { + "title": { + "en-US": "Info 2 in English - a little bit longer title", + "ja-JP": "Info 2 in Japanese" + }, + "content": { + "en-US": "# h1 English info content 2\n", + "ja-JP": "# h1 Japanese info content 2\n." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1568979341589, + "type": "info" + }, + { + "title": { + "en-US": "Linux news 1 in English", + "ja-JP": "Linux news 1 in Japanese" + }, + "content": { + "en-US": "# h1 English info content 3\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese info content 3\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569152115002, + "type": "info" + }, + { + "title": { + "en-US": "Announcement 1 in English", + "ja-JP": "Announcement 1 in Japanese" + }, + "content": { + "en-US": "# h1 English announcement content 1\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese announcement content 1\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569065729169, + "type": "announcement" + }, + { + "title": { + "en-US": "Announcement 2 in English - a little bit longer title", + "ja-JP": "Announcement 2 in Japanese" + }, + "content": { + "en-US": "# h1 English announcement content 2\n", + "ja-JP": "# h1 Japanese announcement content 2\n." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569238489658, + "type": "announcement" + }, + { + "title": { + "en-US": "Windows announcement 1 in English", + "ja-JP": "Windows announcement 1 in Japanese" + }, + "content": { + "en-US": "# h1 English announcement content 3\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese announcement content 3\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1568892951736, + "type": "announcement" + } + ] +} diff --git a/features/tests/e2e/documents/paper_wallet_certificates/paper-wallet-certificate.pdf b/features/tests/e2e/documents/paper_wallet_certificates/paper-wallet-certificate.pdf index c53d116d878bb68c16eb962cc1600ea8c09f9251..77b10ac59743d5ad64df83615b55df26a605433f 100644 GIT binary patch delta 22672 zcmZ6x18^Wsw?7=)#%5#Nw(VqNn;UzQjcwc57@Ligjcwbu{q1w#d;j-7U)5Ak&%v*c zx@PK3Z_iq;&sh2gfd+vEfd@eZK?XquK?lJE!3Mzv!3QA(AqF7@AqSxZp$4HfqW@3_ zn7G*B1O(umU7XB}?BG0B&Xkd9M4SLt${)he_fk_V5O%V%; zLy?2U`8tvRXaWDI{ln(=EEQ6$rhv`)AHshDetDjS`*;X;DY%@t_&38-VI$^8q9G^s z%If%!7|j1R;6DrH%I8ECDf#URt{AM3t67Y-SWN|bsij65oU=U@EzD-Ha|@PG86 z6Wk+S6%1FyqY?``Zuh-bLH=bvp`w7Gm^^gbXe z^nan9_GEV!6&i@bqj-P1GC=&xsQ69(E2I&Qv9}!=n}53+u$NuS3HzN__+i6&OcCS! z>%a5}6*s8$7T3L0fCKj>o<((;e@FLk9;GW=tH-)tIyg>ZaQ~Bv{{irEU0R*!Ts`+U zZSrb{{2%InMP4w>N1&LM)QXL)|9unrUjdNxYgV#H9|u-fEyM6A{vRUn&&-eQdBcU* zXcgbtz|LIv1My#&@r=}atMdL6A3R2$<*fey$?$&{4WU8~L|b;%MAuAsMWUBGSB;Ry%C0FawE?6LcAo9L3?vWwL@Su&-PR`HrglaBj~l>$ zWg0r+lFBVHvh8L6ubayEB9{Nc1^7InOB#9}TF==J6cPS~8-Lh;>+|!p-~>&w`1m{b z>Hn?vzk=d7uK8CnUOb)(|1aSGHFkKyk%Wfk`EV{#RfKh6=d-Ge~IJ7!@Jv8rVp|gX*a`wEkiK*W*9tz;Uv$Ffem)a&vH}3K{+d_$z}2_rEr# z+Ulc|rurJ9tN*tri-zdMU|<}~seUHtOh8{d9xSo?aj+<%^@{2|_>ydpT9nyPKv&JR zZDNI#Y2!xvGV#XKH^GIHT=BOP3D*KGmyYR}lItCe@RW&;@CTJ6{c+u319mo!h>oX5 zIab7`V+rMyc$Di*2gL@Rz60S{`t>RcMuo)FaV=Ycm{Lcl)^D%!;OSat1r=x+Jb=UF z+|r>r5i0cYuZ7Od$3V| z3GyasoJZ*?ZdWA7$ZJ4uPp)T7=S~NObC+>P*mhvnDCH`YKxq5wO3nl9k3}9v9+_@W zu@KqtLfl~<+0p)!sFNCOg0xry3>EVHqE~-cYRkq$Yx<(R3ija}TJSyMYQu%R!%^$D zTjNifcES3#5xG=wGjwurR&K6;B25A$tBxvwGb6Mg(%yg<)!n9`gjV=(L9O8LD7m$v zK?|vqcM|gcdI1p&uCQv4F`1Ne>+yQg19_i^saEIhOs|rr=dP^R6m^PIr5h3tg{IM6 z3()gTgKqd{Ctxr*zI#3s!@ElW1p}#S`&Ij)nBr7#i|B&0ER%O!sG;rpU2qmqE^&X+ z3(nz70qdgHMaqDV2Ib8 zlsOQZI1o4zA}XRxW+ECAqEi?euma;FF<7F=p^`9KxQjNf7_O+@GRB07$DgYNFsz-E zx1Yc46|!4hPd6%fTo)=%nC>U6Ou_=j$g`Gg2G;m8$OKM7$l0l+^t<3K0r%A+9i18g zGOO$T0N0(2;doN2-oC@eT4N`J8D8{!QQ^=42IpkzEyLx+VNbB>^F13Dkm* zu;vc2Fwi9Cc7?F-_!3YjX@xkLwfB@Fwji%2L0rG`1~7t?t_sFIop=cGZ~<)t1dL_|C~*ZG6?5Xi}}?{BUExz6Oc2QZL`rqAFw zBuW@q(#>lk;%o;naplr2Wf3?*k|J4Nk%+4tGMJdjx=2d;AH!Nw_XgvO7(*(&B zG*+WN1(7|Ls6F^7O{?PM`bda4`vj}i*dghp#;ndpK7CSIQ?F`4*s$5YKl)bB*c-;g zpUr|VWN%#`bR?JnS+SXytm>${DJiZv9X)50t>rT|{~r>&pW)QTa4qF#q@K}dq|j36 zR%ANyXoey=#Ia4of-oC7F>UxTPyTeInT_6~Age>O;my;iE_}XO3I(E8&%m9e)8ls^ zo1tX3C(8HrAD2*0oKDCIo~Brx4pShw;g@LjvFpO-P8|z?#tv)ce)^tH4LNKJXjj}Y zz3f!!?jE8KYDPZ;_d5^j2nVFq1y;+g~(v|?IEclZ@t87S+PL3Ga- z=DUb^4{106cfCFaqp4lGdZBalIy&G-!h_-tZ|K-I{x|a6-WSfUNoOWOQJp(u0*~bQ z!JX_OE|z2j%x3h$iK^A58yg>tuIvzIY)0$swIG3PVpXP(c~iueK)y;nf-a_oQRP;s z<8My*xI=oT@b5~r28h=0g6~gKaV{jNENN>Z<_VHOw_U2ASKP-Ql+s(dpr;$XCv3w9 z+V2zTsS{?O&$nCh=P16R!GxYSzGDeSaQDN8DwnTi-}x_PFY+>nh)C{_$M5WK;O`Td zJZt3fWruB@+(%RC1?xKc!Y?zoio*9$^3E*?yt<)-O2)>zqVqL`RQlL*I(5s_J4XMnU zhfE=~%gSRDd`_VZC6UvTNw_@JCMMqjyq5SiaL*+wiMVWA2rZ#^U?IekB=CvA1^ZL& zi^Y87F_3?h>zCwP)FnjNGerZ2fk>*6o* zfGRkBhktA!q4$>e*WHT{2OERNFD~0ATQLD~`H=qKhoXhvW8TP*Y(ykys5}B1-{xMD z8hxMmgmcXWSc2`RE8TaT2^&Y1oDf^e<#QQR?nA}iK5w5L@ZLO^xuj0c_Ocgl1ZjcS z&lhLp&O|BlA|sRoZ;X{pWG@7<;o{s1!Q*itWoXipk2eRQ#H4!_J-npJmGL+Qy?LU1 zP2|kDJ{N68bQ54<2=QTt7L+~{&Aq7|*uG$MpkteIx1?|d@Nh;WvH)T7A*58o3`e2XCE#({SAkQGwhIOOnyvCH}n}PXEBK${Cw;(HmqBp<}=KR!UzyR(SK01 z(J*VZ?&ifO<+MztA|)t#3Cn5^kLRsud!;`GN#XloYazGDN+}vVtEqUU1ZLnHZRUtZ z+i2k}BT;99mg@vF3(9erh-uS$yc!Jm?y&9@A(DmC6@9Ho(RL+%rc7U)kwU6?-lJ`Q zXJr8~dsHFqq@L26udT=Ra00mQ&JxpVtm3*tg{r)VWrm)Zrk<3AEE;{u-x}`o`Y0N` zwD3=X?ZB_Lq4GFza0apuLO{EOTbzw2t7Q}FnsN;vgH6*LNKGLU{YIF|anD!*oWu(e zt*P(yW``-!PeazJRN_iBeh<}{6Jz`e_T?G8v1d8U1I${6*aR-B79c?*QjwVy0z)9w zyGVQhYLUx02fJot39}p#&xBpm+G%{MxUZ4KL#5la*fjTPnYk}alKI_DA{{oy-J|IhQ zmGM`8QcuhX$J!M6`4*=^EY=#%Do>OzpCI_K)KOBk<*v@2o5{ylg34MlR?##w?d7c@p#rzY6Usa9s_BpPZEg!(SdpbiWH-J=M*$Z$wXvYUcUM$r= zTJmS>AyJiKUEv*zahOqRXC@BlBO68zLR){%=-v2~j|;zPD40@ugD=YI6FP+HV#RZ% zU`8b2FNdD74=KlGxB3f4h1XgkzsAXdFg>&`tXx{q-uwa z=E!lk!SvRE;u>27PV3PLy1M)cXJAC8XvhML21NIb=tIFaPc6F$L&_Zp?1gl4*Eq1k z&_VvC6^okYXs3JuO-Fg$ecYW5Vq``S0oYM%L2?E0NT4ZeYz@bZi0)bK3)fN>aBZ!3 z4}$RQ*Nk+eom(cdaGL)TE;*oiw8ZdLOV3FgOJ{B-+&@jy!lNA5FgJn`9YOqEVv0I5 zzmiR7rXhML!hPC+N7RVt)=FxmrVAc!5J(f5giCa>?!gR?+)t)3IPYT?;6zvg01+&h z#35Rq-?R$&ei+fKVNoL+?jT0wt`!!=LxXHm>kZX_1iSRA3wGmmvzA>aXk-)@aTXPc zC*8!zK)oy|N=vSYUgw312T_wp=->-ClU=~kE&pN)=cW8kX(*S1rm%2JKDr)(iE*rH zuf7t03MzM8_N^&{z~2m!(i){~3^3M}T^VvgoT&mSfk?t2#}s9wq3B0Y=n`R0*aj)t zqiT`)3YA@MN-d0_9flu{?}%{4ymjd>2*PtdfdXQ)%xG`x_(J;N6zhKgG6q=*2~$sa zyy|ZNqPs>E(ko>avHjMc#_p`(8BsL&1S0TI{jH3&z9YaJEcflENd zLEO4Zsu6mpl`^W|Q|Q+1CiO@3_wok~Y>D85H#ANBjkm)R2cVtCSThCDx3PLC6SJZ;6J4ggjI%pqoB`HnikOUSM%-SzgFiK4AGvE3RNW8_U zKOu4?OQmJezN6!l&)MckGe5F!tTK0EXqTX4e6ZlO&*!UI_0mAvOZC|qObFFCq zzM4HMQA}z2-mnrFLE!ErLA})cc_Guzl?x&{R!I|o<2yn*E|M`Q?2t^8r^GKeuLYkH zgDfXA;B0P+dlyj@mHRXKi=Mk#GJPXKAHj}{F{SViu+A3BRoFw=LiPvga{Cj>WAr7P zsh2ZCmj$%DyZ3o(8}QkCiQscWaRei3GXF~DTepYb%?OhH3(pFg=}H2{On5JZC|Q#d zWD^8Jdq@Oc>qs2y#MH_;JD}OzpOEG116IlcECoaW>J3z>5iQ+h4-b`u6x4}s*+b`- z1{QfD$Z0T}B}WmYza+>%sTY7g60$DW%1NdUOI$FN4cFBNa>&890x{Yd{PRmdRHpM# z^y3EL!*3Fz|3FV%1(4~x48v=CZ%b|vugzeiRzn4H*JG`n|H;(bGQfVBiUCAh!EjKe z)6&m@wrwa*oeOTkp`oGeb%gddx*c$^@ID8D$nv9+MV7_2#O5?An-vxId36# z=Ylmue}F9k2yf&}OCdmVH$7D>w{JZE8~W9bA;#IE14SlR7}|0lvEg!`(U!;-sq`D( zmHF0UfHw#sc#3=nC|xEG5?!Vj`r7$0+xY-_*>G2?2I)`O;3<1vPw?0PMZ5jwH&5f; zRZwQWTR!TkysxzcDByYyP$p{#fvNrYu=|Wxc745o$?sEQinY=r#-lx`W=@fZso_2(L#md-p6zI{Ov}r(N$5*6LHby6FEZBi-eEZ+^7Nj) zVY!-bHTuH{Z|7nqWnAgUYH?%T^fIQp8)79rhEW(7g?iE$OgPcy7H*u=vf z@DMdP9&c(Ne;IfKMYyyJv2qcAsC9BnzY9=8iQeC{axY+Z@G62WQ`D~wzqbY%Nx+Hl z;`Ew@kLIY)*(37L_nJ&wtl?i&Wf}Oo5ST17*GuWjWL5#*R~kx@!MGvhmd{ni)k@!Mrp?NTMijO`bQt z#h(#`UhU+DuIS#s;qTqoCs9qwkSw1VREn9G2X^!6M7wkLzhv13Bo#zpdaP$#n zXnNsi4YnK-uhuOgL@HEkga6_J7c!cCrhR-)k_}?`o4PQO_>6}I^NP;D;isuO%1s4S zCM!$X+F*485`uo4p+bK?`%YXT_8ol?iNe0 z(TBI)pm}Un@&%V(yHuG2G>VHGTRTWqh?KQ-&6IWYTK4-$Rp)3}7KdySR!{>3ZePEh z#0n-nec}DN&Gj1U!}`;L(Q=Wo)(#Kd&ScVNak9aJ{q0LF-DXDLB-u6AgDv=r%m{sD z$`YuSzlhM^EuUxNia08kmc+Z{GXqc5Qk%gCwuHP@8RRihn?2K zxrN+9-=w!+>@)uLn_t<_yHeE%;^PgCSs^jBxZ4SG3)N8T+RudoR$xP zxtcgBwfhJ%DIg7u&?DjiDkj*5OT)R}z*=fFW@#k406^`J4hr|Gqkqn^V=q2Tgs8w`C^vWppTne>n1mcrg^I{)#!ykDKVl zGBhS$5=gyvR2}^YDJ~ooXX9(938-n+-yx74tqG&kGf#3YI8Sk<4X4u)?nSOq`g5v5 zyWQ0N6OI=Ef2O>!QX+ThiD}Ft;Q#t!q+CCn5}yIlki!dTqBn*e6J zZ@f&<1R?3LJ#yuC5T)A%v_8K-1kG`J?4mo}BfbDpE?yMpCWXUjLj3-{^#C6HzU*X> z-<094*vfe!VH~l)14ve9uT!4fN^)u47b$0?JzmMgXSWRX@esk}s`OntE*>o>TsSM< zZeI`uL33fd_;^1O0u^+=$;YDQ5@2xH5`>g+mNTyqCOT0iLHbhx@aG!yP5%H1y@USa zs5P2Lw9kG{rV@9vC5uO7iIcsZ|46`Dl=Tp@q(q9&G1d4MX>nn>E}#TEOJk^n zJ5r_rn~HyIaMUytTEuUKi%Z{2vO?;%Z&+3QweCh>ST2=>UKOkDq}8-!5;I&*G?XF! zNI&Fvkojy}39Tq%(~PZZ7lJ<Ff9qkAV3w4A5Br-_F-_xHuM6<&;?=7Err}Z(reHz(6g(!Hatj{e-z@nY>|!jSbX{4kZ`d+tXDO&XVAm$T^yA8Jmh#v#}R zu3s2O2r#7m-f2yQSe(S-)j3vE1{r!_HUyl7(3(eT7CBTH?2B+1-CHSlU z7I8Lzi|4`{pH66PlgUyFLH#fIWfqnyt#po(H1`Z-TjRoq$y!+&s;XR_i z(Sl3WK?OC|`3}zox_n{8YgC2KpviM{LNp{ZBotDjZL-pfd#s!W3t31-@khr1RCqIz z-`zMO&5 zdM&n5UAcCgM)n3~cq}yCi*F0xzw~fw*q7dIZ&OuSz-JPL-WwaCgF8L6Y!8f3;KeoD zsBROes0doBos0H1v(B{TFjOJ~dYaVAcZ|vRTJV`?rc(QE65gc?k)a{Tg!%?NmX5`h z+a;dUU85HP`aKIv;=S(8!_}|o`*Jb$Y7Q6LWE)&-QTtbjha$0siy-;D z`p@!k9?JvYDqHIt0$&={QD;x@oT~JyH?oOc?u_RV`JF6NPpa@$^6yOH4GCO{77nF$ ztsL!5fZIl0ex7XK1_Y3`&Je(i5pLoY$gwl0SEQglS>DTti<3>MM}bfl>vQ#y&D7!~ zwBqqH*OwM&gfZ>2P!WXLRVw)hLhzAH`8D--$>VoR~$C zZgFfWx4=~Z=Be`c2`j9tJ=jjcng;H?t?mWT2Z#Pgbf%juGD51KKs{1<$Q^* zIVbRG;)lYOM7n1(F8XWJT0NFsB(kCt5fjy-C?jhr=)V;ey_S}1Np>n%MNSfzU*^kA1e;JzAo3}_tSu~{LZz~CiU>KJjayAt(>$I z2pL*O<>Ml@$YjB&Y6APGY`%Op2YuEYed>+MPK2clm0aeNvP%RZ;(dpjgWjLoI}vXj zezm*lFq%Vr_k_H}U0UmB%CHzjN?5sG%Zz>fbM4 zqr0@+Ab}!b-#gjam9l_`1cM<@dPiT<*#pg@qZrsJuAPR`+RiAd$G=mZhbJO7dVBZh?&nfi>rds-9Km45z9hzqukJ(R9=H`+*~y)8sgFO=YD1QuLxnx8%DG=_nQp zk`C2lH<9*-^Y>wbRq4+h`JJDBMbbX&H5(Y!oIA>@Wt_+JqlCXwr`zCKUtJHp2xR7! zF1H;$_5(0mHhTh83MrD|WH9Ve#2h})xIW^4?Ce4gwqZaK2umWNMT{IC(Ci_H59_w5 z(*v<~KWCHm$f8#YNui!Xo^jMW1RToU)7)D%nNQb~Tpg!XSoi&cRWh)oG9}CK6sesU%*11Mc9?Lv3o0rUj?cPibU9xQ z^(;OCQkMD(S>pa)(w`C_2y9}`w<0u3b?|K07^z7lhj*ZDbH~@KZvBtYYSRLKcYxvB zMX!M?3v2vkVJG2cCl48)o&s&##632Ai2KIFpoH1tbB=ci@AHPubLn{bH6f7hm*haI zWT)Ajg(du*8#aOInXL6C1Su(ZEPvgZ#)-z6OJ_gdCGyxCJ=4BuJsNXB#w78&O!@ls z#&z8lvsETrC4+qY@ec?2~sqw}@c^Xm>ojR4I2q6M|CiNY&L7k^nsZ!2C5n zJu2R2oy7$G#-c_W3#^faa!-D%@5)f}rS^=MCFoz9n}3&yK|B_v>HD~k2CSDhNBv$~ z+W&=Lx&V1HJK5P*li#sBC3>9^KHoGL@lkI-3jWBSiB;?QIa6A&J7v{b%Av5~csZF{ zT@*PAFO#az?9{P$Auh!L?GE(TN)kk=QkF-nRLsqtKoXEj?lVfYoFIgN62o#w9PdX* zmyHh@j>*E@?ogDQdfufh+9fH5wjZCXIh1&5=TBXHzzM1bD}F(x3}&jE!<=_%MNlXo z?)qb-nTtj%$Ivl&IsAAB-8@ zp5XW({a~!fO82^{%PtAIcIMDNYQAr?&plejDCE8tw0x}l{Q9_;Q01DZUbb(MQd&u@ zf0JriM-`N}GLP9iHV+&eAMi9V=|pkVyfPoutE05=6*q8M-&3fkmagXSik05Je$mz( zMdD0d7Odit^I|8wPKIyAiCowbiqYBrWEFI+-*8Y*Wn^9+=|Y#r}16gDw2 zsa4eXHt({($Ua->R|C9#u6GX10$-1clMrmV$GZmFm!x^L&;WvM*+SdM(&65+Y3Ua0 zhxFUAtgp({tZ*Zog`x;fX@_}dalto*Q7gauoBx(4w*k6-~0m6LLMo(*9Ad2Dm_J!caM+Y3V*zZv6S5}-f4WLq%7TB-I4|! z%Nux8(;SwG+zH@)?k+AqZqVTEc3iGJEd?Sow44OAA~vqpOqCL(u$;DNB7NbxT2JM@ zSpA;3w+xAMLwuDJt=pXVO}jI!6(I#}evehSK)xdRCV8Q(seIpdE;~cZxC!63B9SY~ zx=O%$F|ZWf$7L!8_h_D0uJLDY8h%u4|Qv|A$*(OCL!%3R{{C z%l)y05p2RrdwUWjmV4D`@lQmFRh@ylH$$Cu$o}BIfGKdZ460{u;kT}MnlbG7Kl404 z)m;}Rws~mf{)7cdIjZ&MBInMuDWXsuXa!L}5Yi0mO2yi2nb~j^a636+eoYBk(Ld?$yRdHd z#9jfjx)3R-_WD@t-1iDy4Ku@ta>jGV){GI_#6o;;Qq~bp34GB%`wYg~}Ad zY5Eqe7!**v^&^;cBU@j2trTSg#rbh1hD3X!?{v>Uu(1##x=;<4d>%l~nXUI%>O6O` zCh*Y#(dA^G>SIEkUB*F4``g&h=b=7dpab~B@Z84#CFM~Z>3bZkGVfu;ykV8e+IXeg z0AhWnQA025Z>_A6_e%7MNRPcR@_ac@QxVtDrl4SuU6ufuA3NdtTZDmN=as+guE#lYX$ZZ z3?x3OGKd?rdS$dLR#y}d zhnDmb=&=)QoC~@<{XI|FaBm2_Z3=e1zu@ebdt7qII%IL$ymtp*w-B0wCK^IPSh7>9M7M3nKd=t>Tv1ETOt6+XYjAC2o2gz zX8I2C@eeO*{GMeVp5C5+BC~f@o>_l*9>>|)Ob_PIysB};vpET5IL!#xxJP|w;uur0 zGwOAj=h2|XjW3bzT34TI+Jk!Z;5@Z2WsSNE^Z!XYT3Ybd_{;Vz5d7q}lpo`&4IC5@ z^pFdBR($S!e81Qgm}~_I9CLAIOREr%#>`i(J5D!2M--qGpmCA|z*zU5W#7Tydf!A& z=*-Pu)T~L%C&VLSA!dR$+lMg5mCWm0X2;+Yy_Ymi1zB|_YO;2M9tQXVqZ9p9Z?`K* zz6l&Zgx)utL#w|FTXpy1ZH5-Jq>=~;Gwgg%{ChML{sq5L?N%3v(_VS}kaSb>ej8+T z>jS=boq5~t6jZHZ@f$R`!tyS!^-*qOI0)P;zg0EIXkjyY+3aU5iP}Hha-f(J5cB-Z z0xHBz__G=_!>ifsIL?^}n8Qio_-V=QG=hK4 zA%(wT;50(CtMW1bRPAVdeiTTuX`#Iu%aI?`3mQ(LfRzJrb3f*2MXB)Sv@*rsLhPj6w@d6|YJ3l0D2 zrYRFqc>j8Y9Jm?huvHQBvX!K9T5_7FKow@1??)(4gWeGjRkq&zW4Vfhlr3VzOj1wk!8y38z4RtHk|Ta7 z2pm>vZoV5W=5rQFPzQ6mB_2R3hP7(^IeAU?fg}Nfg+)Tjlrtt2I4r^uv26WYRn{wW z)G*sB@KV30x*{oG&WqpkoDj8^^=lM38yA>86^=VQMX>WS6;x<`t4Kx-=Pq`mx1HT9 zpyYcp_L~ibMj}`lpxAIbrMd)t@q05c@Ow%BoR*XvYY+bveQ@TpU6!piJ2q8yQ7Ql4 z8m|sS9y3|?-pzY)fRscj&mZ0*-hS^`cc^J}#v_VX*W>g8CD;nd<=)Ah^L}A5R=2OKhS2Q}V+;rn| z!IPpx3El4hbP{8dJ^66!mv#=NZB```_}&c|DCtLh@^_Mch`cRW9AOH+OZ4hBHwj+T zovlp5Q|UdgZ$BBjtzKUqX?wk|Hoi=S61sW%@cZOAL+>8iXsW!zCy+!I(}Nto3^g7l zGkPI;;&}V3`i*>aWY>N1=JXci)m;1P9+T;e8I@Lx`sPm1zsFNHUR3G0GR|TAL#r0R zv5}t!8T+y663^I^Qj}MjI_EW99=MhOuaeG&q>Sf2D`I04gf_mSU4%yom@}Fv(9p+Q`eG=DsDzIwRefsw zD`jt4Ccoi^V4c8FB6L%EcdK_rm4gWod#?RbF?nU-GPz|w?)%6*?##%vDzirdW%6}L z+jmB`Ltifjujs&#$;370gjL0uO%SxZ^;N>DTrxs)ejP*{lr>MN2W@hfW4* zsHBF~FAmfnHXI?Art>nAGTWuKzh?aQ^o<2&*af*C|A>Sx?rOC7ZSOpl4kBeevXwBT zyIsPc9=Z|imE)+)x*rd%Vsqn*el9icZq{52jd(Z24Cr*OV19cUdT3$Ib>X+C$7MZw zz(pQIyjXob4%y|Ds&0k7y&>HL=tAG~7AGP#sJL}_Mx|e~^_kty%&r|DTd`hQy7FLhAFt2gGTtLzslRgJ8-6jb zTk$2wz~(ErAD!RYl zLssF1;@Q38nxNT0A1w3&umC)sdnf$Sh`P5BqcL|P@**|Sou(F%M1(3#Pm_lTNz0Ho zH{O8?tbh@xj8Y@F&W>qy<+0wQ(5H;DCys&&PKRX1RnL}pl*uy3$XhZM!H~D)A)YCA z&G>+IOxR!LDz;dE?c$=0N;1hI2QdqxT3eBL75%cIeU|ysuK}>=#XK3M zwDVRux613(*|n@cg-dC~ELhBz2}(8!;d>j~dNnJLJ1S@Z;m4gH7c+9$qSPtN!qhNM zxRqbtEP}#j1nd6bCfW38he&anJK+q}BS18!CQBP%6Qz!J0(6{=MXD_-&)3f5r#XBR z5t~{YOWg9(v`bQt1KxidA%Y5m>F%pH-n*LJqUHW#A^C^$Q!<5x?`Sp_?K4`tS=)OP zn0@y8pk@d8^Et;kn|xo$;=gWOav&WBPi$e}$xqtB|F#Gq#@pIW+hlr}S<=<=;38|e`tbs!*{^vVC8Bn6nkN_~lzx9IfwiJgJED?@pT)z2^7z-!Nq6%| z?2UNax@K?$DdNy5+C~MQ1Xg|w$zH+7RerRpMwDs#my4{tr&KL}K-P2NS-&Na*VM;L z$7@!Eui!9isbaMhUX=Cf?M&$P)ni%Nn7;n~$JN%i@;zQaduYP~@&z6H$ny$Jk8uV3 zc*Lqr62L1@%v~ss3*sck#*+HODf3NpyenAR-yRLglB@+sM`eZnZ_$BA=PsJ-uS}pW zs%pL+q7(LgHaT5TZg#a_UQ%x2HCSG!oxBp_uv(nLKQb&mAt0*_bgiM6?j`p-c#Cg3 z2W$2P1Kek&I`3BRgkQYlW=`Mlpk6&* z`Q0w+rkGQ#uzvlystWktk$RGi5o1AcFhn?l%xvJL&tnp*Ql?9*2wQcvtZMh`4il-g z6_{=mDJpO2Tx-|mTGT4Rmdf{~-S5p+Im#phKk40BR zpH>Mg5~IW5cj$IY?yJgGQQQ1U@W=aEk<+I+|N0DmpzYGb(eI^kNe6gyI@xdQEAhBa zT3T3k&h;=K*36w;xl%caBX+mSjV5QnU<0JLo%#=>>oh&H?cy!0WkL z%#Lg?9govZHtA*i?F2(`cvzHWn`Zy8lx(@BfaCAyBVHFbE-DtD+0eZWjDnwuCfM?A zl8vd;^JOWaNe0{UTvB2BSRJJk!r10UA9{|Pwiq#-ce5fBZ{{a1t!GGW!|{C?^3OC04-N?XQ6mHI*jMQOJ= zLz-a?AK5}b^nHqeZ{y=-Nh_egg=bU0#%F3W;HIj#w&w8cW_UL%7=hfw`~uGhIT@f+{E-7uBLZY532!)cghWDx9?)u~X?A~00J8p@vj zKnkziOo$LV45-;OxWi2LZ5E?wxIRGcMlcnyLsX9_?@MSJ-^-O$aF*`?&2#h(88 zwqkOcAP>DWxXXG(w*o!ecr>eZO_&?``Q9+Ov#I)N(Tv>YX(}ApR_${VeTVCXNg$OQ z85`yhDd^_(^z+mT_I_$~2e=#%j0DgD(wfr8ZR6+FZ!IRtYouBP1T~6VnS)bc%!%HL#fa_ywe!Bvvy&2 zVBbkU3m@{r$GoiXf!23Ds=4-+tkrDx92ZPqnRD3i5GC5CfwF|YU(cvc30V{s;V6+g zfnUt`|0pP@zcU?FmgT*M(bx%gRKQ&`zY)_<9jm8Y<@oe>G}^@uubjkK-skMi@QCHf z8Kf+6<|3T)apM$JNJt%9e0*5>yk`wawBCn4d8Bx{zd`+$^FI3Z9~y~`!VB&*@Ss-FMO@ScR%k@@~tRs z^1UFf$@hNn`ry{1A>BjDmCc=}Mic~mo`|2$TI+!G03WwNHqk;W$uUvYp;V(J(LQ+e zfU0hCK}*hgOgYyc?KWfeW-NVyOSu&yJbT%+T^@~>3}B%Oa7AqSp%qgdrv4+g;Q{`E z&r|O$_6hr192#k=?HG}hE)CnD@=T`1_i@SO(|!ZWeMwgfJLwK*%}KkVhuYNMWyyt7 z&uYU+!1#c+$ILjNi@XzCq{g7k;srG6;MuY>P&?PLl4(UX=dNn2`Yq{#TJA;B)82ks zmkM9D93WrNj`R*-$S}tpRB<)=bcQw*I~BO)gq7|;s@VHMkX(Yfpcbiv?hVq~_uxbQ zN$qlRJ$sny$7Zs-snT4$!Ee0ZIv*=n<@_jt4mi7Zm3iQX;oK@_FgJn14xluy@pD}|3(43xq5v-nsv|yQt9(LM zDo)R}?Do#BS>K@$4f=p8m5`0bUqKJaD6*3 zTxit(A;oeO3Wh6e)s$^SBywE`%xe0y5X&a~>vjs3jr1PKf?Gd>m;vnJl?cMY(=E`5mX4jZlIH-?P523evQ8np`S5z+#thJ%wUN^P{_UFuK&LnFRQn{h;rh zW&dq`e))}!ck*EX`ftZm6tL{XbMd|Tv4M|qZn%{+sfuW?&Q4?_)+dr(0oN;w=o~(B z_>l_5r)nywT10Il|A*8y8~tZfPamEgGeDnWeI><;E+49;dxqis@wYc_l#@oveUCMi zs<<4~5?nLn5^+I1vX;aWd`$HPx4^(=3bxeXyqY0s=v;{kKTnO87IUJFk|X*VZ*GAU zMw(7lO(i>Ep(p#tOW!HV+O_y}UyPD_(5={3vN=m+`rV2)5?uoatqNRIe;}&59q|8^ zag{+)Ky6n+KpLcxknUXAAVg^a$)!t>Zjg{%kroA!js=NjVZlY3r4%HjySux)Kfd|? ze6R1FIdf;8d(S=d+<$lG+@nt>hRrVEla6@AxWK~Y{wi=ml1e6JG;i=XO;78!0>_Ek zySwucvF;cxPFPxeR4L}AKHK6Q-9VAP0gu6v1^iQinQ6Yi%Gd*OYd!KYbgryhOSyWE z{)FYb%FnZhm908_xZ~u;}n0|2ycga>KV}#<$`f+!bt-_R3r#TX`?Ey91 z7*!VCqJ%#!vc4b)q2@bi4JimXR#w?X+P+6~@R*St_AO?-#au$>tMsUpS+6qL?UiAi zTmY2W)FWNCql9S?0?9yI3r zA+Dz!Gh#0+ov#|-1mk)7CfB#Br;4#E9w;KG>D}->-~m$t#h-kNG>zJLfxM^;Rsz8J zGUSP6SlG5Tdt>$eiYMH0Ii!#}kB<|_X!@rq# z-@bUWwg=?vr^1GRM+rRHtSZy=wjCvwXb-fgG{wi^ygsT7vEBPC_4R5Q(D!`oL>Cxz zc>C51aM}ooAbmdPHrKe~!)S!OdEr6zn8Sy``6n}rsEnU9>(WNP#E{C6{g5kX*n+mk zE4x%_hyJOBX9#}4{;;(l@_D?`O083-buFT|Y%u5E zqF1zs+TW(>*!LMbwCf3B*ltA{Zst*3bI~&GJ&dQ_85gTPo#;Jgrjvv&$;Ht$NYIqKgiIPEN^fE5jL^>Q)}-c@)$)RJL_7YjBvgnevwXm)SXB6VBB6!6 z_@SqSXD`Wx4h?4b)JFHnUAuoP1hp!c1}f00tK<_{%`4sIGm`L4OM&>DS5amCuCtq< zWzK=~mOtU*nc{(MG4}`?sBG_NI-?#Apud1U^I$*+V4b7GiQrLdyp8KK&mnN*n7c2( z$^ff@{38#XA+2eF(3+FO3VQ(FF)5Yx+4|)8I4xbZ$ESCRCz}y1#3lB1FMfzscJ6_vn0Ye#73%i%s=hKJ6y~XHJNyM_Ob$o##!l4eqExR*xgLC9b*7B4pqyzv-BPm zr|h6I!JcMO8tml3PER*;etycfGlS5;yW|dN3SYI}_72jW7GKYFOue3fG6sBi&2E>n zf*y-1B@Jlj*wgRG=MpY_ZZgQ3EQE{rM#Feqa(mG5%^b9mC0blQFfZJf`0XUa18@Oc zTOMTW(;aUMYwpN3J~T1XUS4M3cg@n=^>rQ0H2YHMV+N=yb)ShI5qjRt$gkml<>TLC_^lXB%cu>R@;cVNWN242!;#0}+ zZl`{y)uuD1;iY6tA5i8d^$v4d;);r8{^h!J zX;_}+AKn@sgA6Bqo-=zMd_Hr)b3i8qG}IWvLXqy6l^u^%Ckf2XcJ>LqZJhCZaojrh z#S@W-%aI{KJY|W#;GO=7| zkM7oXEy>uGW3^mG@OpoH+J(jWrgj)%D4l%hQEzcZZLo7@7PB`QSUZymSMBs%%=+oW zTt3ma(f5nPqZcG#A(^fbkle9SQl*ovukqGQZTU|!@30DH0yZ#wXd_l#;E&HIJ7|?7 zIZvQ@DWrn9e=RBY0gRe>$>UIEn*}5a9N{_2hEXOfsJfa?U7ro!OB0>Fn@lqblAe~^piY|CG48gG`Cu7Rlnx&>)76@-;Hs-Sf45d z7x{KK_ZZrGyN^=nEc(MAOA)kpL2#wk<}hACs`IHf>@X);{S1BVa{AF^R(sFY%w^vxNy=~g~+8lj2~x3_ZEs7p#v*~%Q><+n!) z1OXIaNZhc4fafb>pDY~3LgS>=luNY)Lb@yJE;2eYPfN!aTD3*{ta%j znnWu~*t96g4m13aPV?f8?%5x7nDR7+e{qG0fKpR6GSSExVk+D=39F&8a+%I>uVc;C zBTZ!~Q+s5wq}Y`fTVlH^bOU(BQ!+5*(DCQ;vzJGSC2dG&o0tJy{mh|g)0}an?zr%F z_!=rt7*#3$R41f@55LpAHauGO<7CP5yF-Y^ zvDM#0$`Jjic;YC;Yv=sQUd^XCu!D9YHX(r5-8%bXQd^2%Y6pvjjP z{UTk6-iSrAdVx-7a z8()J))9TTa1y{!L_G$%61@J;|PYVS}R3{9vgPLOLkBd=DTxPq{{go%i466V!Un)pq z6~Wy$IZTg5G0*s`!}I3I?p=FRX?A#gPgL1rs@C=@LPqv|@`%1W1tW6X!%AMF?N z(PvRsHvpWiPfhovZp8cp=4*cO&kfgqYGtCWTUc5pRp>ojL!rD|m*>*TA*r6g5!ghIO;?w^4bIz-+wrHth>DH|tbMd-1{-T1wK2y>*At+Rc2Tm>xs|GkXO*M~^Py=~r7o^NIq``mlbE1&Lw5voisNVfg z_-EfUQav-@X#>Bj_13laL-A-NQKrxxd$?4@d`Ku=esO?iMXYi($UG!{v zx{(G>+4S}^3=KM++IYWSy_X~?WbU>#sN~MrnHNu5|?52@LG1AD=mJ)aW;dDfAyqJU!4@ zq%v<`VTKC7@Kv$?WVWv_uv)4UgBn=%-ltCbwf!m zy}~?xa4j#G0Uyz@?O#9wea*{=_4e;Xnic^SZp=VT@dI2r#hWKt1-x}`lVFSoXEsLs zgYxR#(f;@87$Lr^y#!p}zZ?&Xd1<^%T1~LVlkwbzxLrOmKdnWH2HCBn7K};F>F%Pa zp=eqa|7R24*97NzvF)U0FWEfp1Gr~+Eg%ifkQr*&FTLhNFnY18i4D7n4TnC~go(KL zBa3P9v&m)9N_uVjBWy|JMx=v_!-cei*q;Y$f0|6X345WhabW>z^Q5m&YU|baL+Dkq zXSRb>U~&Ut_5!8q^^i3V|LHeg5+e9@65kklHo`4EIasx@?$Y|v$C__y?e5Z zSN6jS7X0k~Yj~(r!rf8yd76`aqv9yu-%B|!;|_`W)*5qE^w$HUAOg%wIu#e$R9(8s zC+%Kg#e@df>#_be!;BnraREMzvorWBv1~H5d*wrSQwV*{fvPz-rDx%O?2_L(nEl0UO7Y681BURX)=m&s@wA48ws-dJ*~9 zWo0F&4+$nPc6W9wYpA})KM!={HBpb4v8haXy^qm_vw}_9VusE4_H-s(0|LMI6P0)ndxe3+?G5?XI6h-)X#c$os-;s5Xg-sr9B!K*tRDc-|e7Yul z0d?g7*9d6tYdmM8UGI2!=O-6m_B3n<;w9N*fBc!#S&*;nNzBb?*wI_1@MA5!Sigi7 z&pTo7#GAVEW{UIvwV=;?ZYZz)ZS3YFZdg2ILT=p@IlLWD+jiYsVs`SKQX16so^~j? z%l=yc_us6r8l&Ro>97t8)hPwfK2qfh0`dmd(pE)RzmF|UGiPMs#2v*PwN{n0-$vLS zw?wdle6YQ*a?gT^LNDaQE+B`g$T7M=oz1xrGx{aCQijyoifIA6V$DjxUd#t z<$XUrg8@(yxr5gE_ZR7xloylBkL7~6hO_d?pdaKP+MBVRdx_QIYClVf0zZ8&Nyfo%;+$1+xu zKw9{fj;;SbKrh)|N@j+=57=Z*Cv|sn_`rB&_!WxkoKKjceHFs)-u3BE!l=Z#ho<(? zCr3sH->NIVpRM;UP%IMc_KZx2pz9OsDNLvoEqYk*DX`XmBLPCsP}uaced z5tr&Zz@pFjZK>D^e~!0$t>%jrKIt7H^&3b{j4@*87g-;p7=x1HwDfAtDanL%W5oCk zJMP&pkd!#~ZVS1d*SYs_y!!hfCPH4ik~{@1dWJ8Kr>>mP7jiUp%fI! zF69(d{}(9+*XxQI7ACvTd%MQAx>`V1EdDfl;fB2u4(_({W+$n8{G;XUtt7m~f=;Pa zvE!Ud-v?1^2_=K=3D4HIJ%2C$+~=1hg_=fC+L4oAg`%WN^r*VY`!mC$cJrArLD#Qm zaa?0PyG&WP*9=*Wfqt4xj(E}{Wo)_FZ+_0#YxTc_Ijmc=Vx3Hp6K#40LmLzWLBWmoV6+`Wl@Gc zN~GN#Mi%|m!N7@tKhs0S9&z2o+$vfX9#-{R<(WD-pQpX$%0h7INLNrU*cX1je9R;? z2vRzzK(9W=$xMImwD>Jhmu*`wL!m^AnZc$$e7a1Kcy4)+27@z@S1;)%P+R{cYz_7^ zPjHQ1I>UH$)mf@dAj^ROO=i({i4)@+YD(FvALUa&vSqz)WC0NySVFxOSt zd2&@Zn^W&m!FzXrcc15i33l5Yse34$9R@T9ofv(VK>U@;0w;0Dwef6DH)8G*N1as) zlCO%qx(9vAA}*V;)9O7V`kLQ)xTrE#&0hr0WXuH#t(Idk-JIqiW*x#PU0ycxlbR+7 zv1&wO*nbocw7Sb%g;}bmv&rUc)ElYrbVvaSVz%39nuN=I-pe_zET7Q~^H8E91}iD< zL3t0Vdr;ql<{q@wgOzmH9@@g<&xHSXJac(Qa6gHNf`o;|f&bP-gvG`FsR6-&d)9w) zfnX7ca55J+y#y~1@}Fw}Fc>8APfZjEf!rr!Msw4HVZspbzp6m6xY&O&z+!-Z_5p<< z!os5esU`{p1A+hS69wHTYW{Z@U{UZr_x1n30w6>fBJ$6HAYq{BzwJUqfFl2hh(Sca XqM+nh9(pB000ami;NVcuP$c*t@T3G| delta 22858 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 client.waitForVisible(LANGUAGE_SELECTION_FORM, null, isHidden), ensureLanguageIsSelected: async (client, { language } = {}) => { - await languageSelection.waitForVisible(client); await i18n.setActiveLanguage(client, { language }); await languageSelection.waitForVisible(client, { isHidden: true }); }, diff --git a/features/tests/e2e/helpers/terms-of-use-helpers.js b/features/tests/e2e/helpers/terms-of-use-helpers.js index 966184861b..e387b7c923 100644 --- a/features/tests/e2e/helpers/terms-of-use-helpers.js +++ b/features/tests/e2e/helpers/terms-of-use-helpers.js @@ -4,7 +4,6 @@ const termsOfUse = { waitForVisible: async (client, { isHidden } = {}) => client.waitForVisible(TERMS_OF_USE_FORM, null, isHidden), acceptTerms: async client => { - await termsOfUse.waitForVisible(client); await client.execute(() => { daedalus.actions.profile.acceptTermsOfUse.trigger(); }); diff --git a/features/tests/e2e/setup/electron.js b/features/tests/e2e/setup/electron.js index 22c95c5898..70e53d00f4 100644 --- a/features/tests/e2e/setup/electron.js +++ b/features/tests/e2e/setup/electron.js @@ -69,13 +69,18 @@ const startApp = async () => { // and helpful than "this step timed out after 5 seconds" messages setDefaultTimeout(DEFAULT_TIMEOUT + 1000); +function getTagNames(testCase) { + return testCase.pickle.tags.map(t => t.name); +} + // Boot up the electron app before all features BeforeAll({ timeout: 5 * 60 * 1000 }, async () => { context.app = await startApp(); }); // Make the electron app accessible in each scenario context -Before({ timeout: DEFAULT_TIMEOUT * 2 }, async function() { +Before({ timeout: DEFAULT_TIMEOUT * 2 }, async function(testCase) { + const tags = getTagNames(testCase); this.app = context.app; this.client = context.app.client; this.browserWindow = context.app.browserWindow; @@ -108,7 +113,9 @@ Before({ timeout: DEFAULT_TIMEOUT * 2 }, async function() { }); // Load fresh root url with test environment for each test case - await refreshClient(this.client); + if (!tags.includes('@noReload')) { + await refreshClient(this.client); + } // Ensure that frontend is synced and ready before test case await this.client.executeAsync(done => { @@ -135,9 +142,9 @@ After({ tags: '@restartApp' }, async function() { // eslint-disable-next-line prefer-arrow-callback After({ tags: '@reconnectApp' }, async function() { await this.client.executeAsync(done => { - daedalus.api.ada - .setSubscriptionStatus(null) - .then(() => daedalus.stores.networkStatus._updateNetworkStatus()) + daedalus.api.ada.resetTestOverrides(); + daedalus.stores.networkStatus + ._updateNetworkStatus() .then(done) .catch(error => done(error)); }); @@ -162,10 +169,6 @@ AfterAll(async function() { await printMainProcessLogs(); } if (process.env.KEEP_APP_AFTER_TESTS === 'true') { - // eslint-disable-next-line no-console - console.log( - 'Keeping the app running since KEEP_APP_AFTER_TESTS env var is true' - ); return; } return context.app.stop(); diff --git a/features/tests/e2e/steps/app-steps.js b/features/tests/e2e/steps/app-steps.js index d43c718022..8015c1246d 100644 --- a/features/tests/e2e/steps/app-steps.js +++ b/features/tests/e2e/steps/app-steps.js @@ -12,6 +12,26 @@ Given(/^Daedalus is running$/, function() { expect(this.app.isRunning()).to.equal(true); }); +Given('im on the syncing screen', async function() { + this.client.executeAsync(done => { + // Simulate that syncing is necessary + const adaApi = daedalus.api.ada; + adaApi.setNetworkBlockHeight(10); + adaApi.setLocalBlockHeight(1); + daedalus.stores.networkStatus._updateNetworkStatus().then(done); + }); + await this.client.waitForVisible('.SyncingConnecting_is-syncing'); +}); + +Given('im on the connecting screen', async function() { + this.client.executeAsync(done => { + // Simulate that there is no connection to cardano node + daedalus.api.ada.setSubscriptionStatus({}); + daedalus.stores.networkStatus._updateNetworkStatus().then(done); + }); + await this.client.waitForVisible('.SyncingConnecting_is-connecting'); +}); + When(/^I refresh the main window$/, async function() { await refreshClient(this.client); }); @@ -37,3 +57,7 @@ Then(/^I should see the loading screen with "([^"]*)"$/, async function( text: message, }); }); + +Then(/^I should see the main ui/, function() { + return this.client.waitForVisible('.SidebarLayout_component'); +}); diff --git a/features/tests/e2e/steps/helper-steps.js b/features/tests/e2e/steps/helper-steps.js index 6d33b2cf53..d0141ace1a 100644 --- a/features/tests/e2e/steps/helper-steps.js +++ b/features/tests/e2e/steps/helper-steps.js @@ -1,4 +1,4 @@ -import { When, Then } from 'cucumber'; +import { When } from 'cucumber'; import { generateScreenshotFilePath, saveScreenshot, @@ -10,10 +10,6 @@ When(/^I freeze$/, { timeout: oneHour }, callback => { setTimeout(callback, oneHour); }); -Then(/^I should see the initial screen$/, function() { - return this.client.waitForVisible('.SidebarLayout_component'); -}); - When(/^I take a screenshot named "([^"]*)"$/, async function(testName) { const file = generateScreenshotFilePath(testName); await saveScreenshot(this, file); @@ -44,9 +40,9 @@ When(/^I trigger the apply-update endpoint$/, async function() { When(/^I set next update version to "([^"]*)"$/, async function( applicationVersion ) { - await this.client.executeAsync((applicationVersion, done) => { + await this.client.executeAsync((version, done) => { daedalus.api.ada - .setNextUpdate(parseInt(applicationVersion)) + .setNextUpdate(parseInt(version, 10)) .then(done) .catch(e => { throw e; diff --git a/features/tests/e2e/steps/newsfeed-steps.js b/features/tests/e2e/steps/newsfeed-steps.js new file mode 100644 index 0000000000..6385d25d58 --- /dev/null +++ b/features/tests/e2e/steps/newsfeed-steps.js @@ -0,0 +1,227 @@ +import { expect } from 'chai'; +import { get } from 'lodash'; +import { Before, Given, When, Then } from 'cucumber'; +import moment from 'moment'; + +import newsDummyJson from '../documents/dummy-news.json'; +import { + expectTextInSelector, + getVisibleElementsCountForSelector, +} from '../helpers/shared-helpers'; + +async function prepareFakeNews(context, fakeNews, preparation, ...args) { + // Run custom preparation logic + await context.client.executeAsync(preparation, fakeNews, ...args); + // Extract the computed newsfeed data from the store + const newsData = await context.client.executeAsync(done => { + const { newsFeed } = daedalus.stores; + // Refresh the newsfeed request & store + newsFeed.getNews().then(() => { + const d = newsFeed.newsFeedData; + done({ + all: d.all, + read: d.read, + unread: d.unread, + alerts: d.alerts, + infos: d.infos, + announcements: d.announcements, + incident: d.incident, + }); + }); + }); + if (newsData.value) { + // Provide the newsfeed data from the store to the other steps + context.news = newsData.value; + } +} + +async function prepareNewsOfType( + context, + type, + count = null, + markAsRead = false +) { + const items = newsDummyJson.items + .filter(i => i.type === type) + .slice(0, count); + const newsFeed = { + updatedAt: Date.now(), + items, + }; + await prepareFakeNews( + context, + newsFeed, + (news, isRead, done) => { + const { api } = daedalus; + api.ada.setFakeNewsFeedJsonForTesting(news); + if (isRead) { + api.localStorage.markNewsAsRead(news.items.map(i => i.date)).then(done); + } else { + done(); + } + }, + markAsRead + ); +} + +function setNewsFeedIsOpen(client, flag) { + return client.execute(desiredState => { + if (daedalus.stores.app.newsFeedIsOpen !== desiredState) { + daedalus.actions.app.toggleNewsFeed.trigger(); + } + }, flag); +} + +// Reset the fake news +function resetTestNews(client) { + return client.executeAsync(done => { + daedalus.api.ada.setFakeNewsFeedJsonForTesting({ + updatedAt: Date.now(), + items: [], + }); + daedalus.stores.newsFeed.getNews().then(done); + }); +} + +// SCENARIO HOOKS + +Before({ tags: '@newsfeed' }, async function() { + setNewsFeedIsOpen(this.client, false); + resetTestNews(this.client); +}); + +// GIVEN STEPS + +Given(/^there (?:are|is)\s?(\d+)? (read|unread) (\w+?)s?$/, async function( + count, + read, + newsType +) { + await prepareNewsOfType( + this, + newsType, + parseInt(count || 2, 10), + read === 'read' + ); +}); + +Given('there is no news', async function() { + await prepareFakeNews(this, newsDummyJson, (news, done) => { + daedalus.api.ada.setFakeNewsFeedJsonForTesting({ + updatedAt: Date.now(), + items: [], + }); + done(); + }); +}); + +Given('there is an incident', async function() { + await prepareFakeNews(this, newsDummyJson, (news, done) => { + const incident = news.items.find(i => i.type === 'incident'); + daedalus.api.ada.setFakeNewsFeedJsonForTesting({ + updatedAt: Date.now(), + items: [incident], + }); + done(); + }); +}); + +Given('the newsfeed server is unreachable', async function() { + this.client.executeAsync(done => { + daedalus.api.ada.setFakeNewsFeedJsonForTesting(null); + daedalus.stores.newsFeed.getNews().then(done); + }); + this.news = []; +}); + +Given('the latest alert will cover the screen', async function() { + const latestAlert = this.news.alerts.unread[0]; + await expectTextInSelector(this.client, { + selector: '.AlertsOverlay_date', + text: moment(latestAlert.date).format('YYYY-MM-DD'), + }); +}); + +When('I click on the newsfeed icon', async function() { + await this.waitAndClick('.NewsFeedIcon_component'); +}); + +When('I open the newsfeed', async function() { + await setNewsFeedIsOpen(this.client, true); +}); + +When('I dismiss the alert', async function() { + this.dismissedAlert = get(this.news, ['alerts', 'unread', 0]); + await this.waitAndClick('.AlertsOverlay_closeButton'); +}); + +When(/^I click on the unread (\w+?) to expand it$/, async function(type) { + setNewsFeedIsOpen(this.client, true); + await this.waitAndClick(`.NewsItem_${type}`); +}); + +When('I click on the alert in the newsfeed', async function() { + await this.waitAndClick('.NewsItem_alert.NewsItem_isRead'); +}); + +Then('i should see the newsfeed icon', async function() { + await this.client.waitForVisible('.NewsFeedIcon_component'); +}); + +Then('the newsfeed icon is highlighted', async function() { + await this.client.waitForVisible('.NewsFeedIcon_withDot'); +}); + +Then('the newsfeed icon is not highlighted', async function() { + await this.client.waitForVisible('.NewsFeedIcon_withDot', null, true); +}); + +Then('the newsfeed is open', async function() { + await this.client.waitForVisible('.NewsFeed_component'); +}); + +Then('the newsfeed is empty', async function() { + setNewsFeedIsOpen(this.client, true); + await this.client.waitForVisible('.NewsFeed_newsFeedEmpty'); +}); + +Then('no news are available', async function() { + setNewsFeedIsOpen(this.client, true); + await this.client.waitForVisible('.NewsFeed_newsFeedNoFetch'); +}); + +Then('the incident will cover the screen', async function() { + await this.client.waitForVisible('.IncidentOverlay_component'); +}); + +Then('the alert disappears', async function() { + await this.client.waitForVisible('.AlertsOverlay_component', null, true); +}); + +Then('the alert overlay opens', async function() { + await this.client.waitForVisible('.AlertsOverlay_component'); +}); + +Then(/^the newsfeed contains (\d+) read (\w+?)s$/, async function( + expectedReadNewsCount, + newsType +) { + setNewsFeedIsOpen(this.client, true); + const readNewsCount = await getVisibleElementsCountForSelector( + this.client, + `.NewsItem_${newsType}.NewsItem_isRead` + ); + expect(readNewsCount).to.equal(expectedReadNewsCount); +}); + +Then(/^the (\w+?) content is shown$/, async function(type) { + setNewsFeedIsOpen(this.client, true); + await this.client.waitForVisible( + `.NewsItem_${type} .NewsItem_newsItemContentContainer` + ); +}); + +Then(/^the (\w+?) is marked as read$/, async function(type) { + setNewsFeedIsOpen(this.client, true); + await this.client.waitForVisible(`.NewsItem_${type}.NewsItem_isRead`); +}); diff --git a/features/tests/e2e/steps/node-update-notification-steps.js b/features/tests/e2e/steps/node-update-notification-steps.js index 46872cc3f8..45fb725191 100644 --- a/features/tests/e2e/steps/node-update-notification-steps.js +++ b/features/tests/e2e/steps/node-update-notification-steps.js @@ -1,4 +1,4 @@ -import { Given, When, Then } from 'cucumber'; +import { When, Then } from 'cucumber'; import { expect } from 'chai'; import { environment } from '../../../../source/main/environment'; import { getVisibleTextsForSelector } from '../helpers/shared-helpers'; @@ -38,7 +38,7 @@ When(/^I set next application version to "([^"]*)"$/, async function( applicationVersion ) { await this.client.execute(version => { - daedalus.api.ada.setApplicationVersion(parseInt(version)); + daedalus.api.ada.setApplicationVersion(parseInt(version, 10)); }, applicationVersion); }); diff --git a/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js b/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js index b155263adb..df2fb49dd6 100644 --- a/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js +++ b/features/tests/e2e/steps/wallet-recovery-phrase-verification-steps.js @@ -1,10 +1,4 @@ import { Given, When, Then } from 'cucumber'; -import { expect } from 'chai'; -import { navigateTo } from '../helpers/route-helpers'; -import { - waitUntilWaletNamesEqual, - getNameOfActiveWalletInSidebar, -} from '../helpers/wallets-helpers'; const SETTINGS_PAGE_STATUS_SELECTOR = '.WalletRecoveryPhrase_validationStatus'; const SETTINGS_PAGE_BUTTON_SELECTOR = `${SETTINGS_PAGE_STATUS_SELECTOR} .WalletRecoveryPhrase_validationStatusButton`; @@ -39,7 +33,7 @@ Then( 'I should see a {string} recovery phrase veryfication feature', async function(status) { const statusClassname = `${SETTINGS_PAGE_STATUS_SELECTOR}${status}`; - return await this.client.waitForVisible(statusClassname); + return this.client.waitForVisible(statusClassname); } ); @@ -54,11 +48,11 @@ When(/^I click the checkbox and Continue button$/, function() { When(/^I enter the recovery phrase mnemonics correctly$/, async function() { const recoveryPhrase = this.mnemonics[walletName].slice(); - await this.client.executeAsync((recoveryPhrase, done) => { + await this.client.executeAsync((phrase, done) => { const { checkRecoveryPhrase } = daedalus.actions.walletBackup; checkRecoveryPhrase.once(done); checkRecoveryPhrase.trigger({ - recoveryPhrase, + recoveryPhrase: phrase, }); }, recoveryPhrase); }); @@ -66,11 +60,11 @@ When(/^I enter the recovery phrase mnemonics correctly$/, async function() { When(/^I enter the recovery phrase mnemonics incorrectly$/, async function() { const incorrectRecoveryPhrase = [...this.mnemonics[walletName]]; incorrectRecoveryPhrase[0] = 'wrong'; - await this.client.executeAsync((recoveryPhrase, done) => { + await this.client.executeAsync((phrase, done) => { const { checkRecoveryPhrase } = daedalus.actions.walletBackup; checkRecoveryPhrase.once(done); checkRecoveryPhrase.trigger({ - recoveryPhrase, + recoveryPhrase: phrase, }); }, incorrectRecoveryPhrase); }); diff --git a/features/tests/e2e/steps/wallets-utxos-steps.js b/features/tests/e2e/steps/wallets-utxos-steps.js index e43bc45188..e72488dd9a 100644 --- a/features/tests/e2e/steps/wallets-utxos-steps.js +++ b/features/tests/e2e/steps/wallets-utxos-steps.js @@ -1,6 +1,5 @@ import { Then } from 'cucumber'; import { expect } from 'chai'; -import { BigNumber } from 'bignumber.js'; import { getVisibleTextsForSelector } from '../helpers/shared-helpers'; import { getWalletUtxosTotalAmount } from '../../../../source/renderer/app/utils/utxoUtils'; diff --git a/features/tests/unit/setup/utxo-helpers.js b/features/tests/unit/setup/utxo-helpers.js index 72ceaa1d67..477ef2f6a0 100644 --- a/features/tests/unit/setup/utxo-helpers.js +++ b/features/tests/unit/setup/utxo-helpers.js @@ -1,5 +1,5 @@ export const getHistogramFromTable = data => { - let histogram = {}; + const histogram = {}; data.hashes().forEach(({ walletAmount, walletUtxosAmount }) => { histogram[walletAmount] = walletUtxosAmount; }); diff --git a/features/tests/unit/steps/generate-filename-with-timestamp-steps.js b/features/tests/unit/steps/generate-filename-with-timestamp-steps.js index 2c047c14cb..b1ba200d24 100644 --- a/features/tests/unit/steps/generate-filename-with-timestamp-steps.js +++ b/features/tests/unit/steps/generate-filename-with-timestamp-steps.js @@ -1,4 +1,4 @@ -import { Given, When, Then } from 'cucumber'; +import { Given, Then } from 'cucumber'; import { expect } from 'chai'; import { pickBy, identity } from 'lodash'; import { diff --git a/features/tests/unit/steps/mnemonics-steps.js b/features/tests/unit/steps/mnemonics-steps.js index 7b9ce92033..e81c01c2dd 100644 --- a/features/tests/unit/steps/mnemonics-steps.js +++ b/features/tests/unit/steps/mnemonics-steps.js @@ -1,5 +1,4 @@ -import { Given, When, Then } from 'cucumber'; -import { expect } from 'chai'; +import { Given, Then } from 'cucumber'; import { range } from 'lodash'; import { generateAccountMnemonics } from '../../../../source/renderer/app/api/utils/mnemonics'; import { isValidMnemonic } from '../../../../source/common/crypto/decrypt'; @@ -28,15 +27,17 @@ Given( 'I generate and validate an unbound number of wallet recovery mnemonics', function() { let numberOfTestsExecuted = 0; - while (true) { + let generated = true; + while (generated) { const mnemonic = generateAccountMnemonics().join(' '); if (!isValidWalletRecoveryPhrase(mnemonic)) { + generated = false; throw new Error(`"${mnemonic}" is not valid`); } numberOfTestsExecuted++; process.stdout.clearLine(); process.stdout.cursorTo(0); - process.stdout.write(numberOfTestsExecuted + ' mnemonics validated.'); + process.stdout.write(`${numberOfTestsExecuted} mnemonics validated.`); } } ); diff --git a/features/tests/unit/steps/spending-password-validation-steps.js b/features/tests/unit/steps/spending-password-validation-steps.js index 2a5bbf32e2..e34bc6a772 100644 --- a/features/tests/unit/steps/spending-password-validation-steps.js +++ b/features/tests/unit/steps/spending-password-validation-steps.js @@ -1,7 +1,8 @@ -import { Given, When, Then } from 'cucumber'; +import { Given, Then } from 'cucumber'; import { expect } from 'chai'; import { isValidSpendingPassword } from '../../../../source/renderer/app/utils/validations'; +/* eslint-disable no-unused-expressions */ Given('I use the spending password {string}', function(password) { this.context.spendingPassword = password; }); diff --git a/features/tests/unit/steps/utxos-chart-steps.js b/features/tests/unit/steps/utxos-chart-steps.js index 240f2d93a9..a7a93147ef 100644 --- a/features/tests/unit/steps/utxos-chart-steps.js +++ b/features/tests/unit/steps/utxos-chart-steps.js @@ -1,6 +1,5 @@ -import { Given, When, Then } from 'cucumber'; +import { Given, Then } from 'cucumber'; import { expect } from 'chai'; -import { isValidSpendingPassword } from '../../../../source/renderer/app/utils/validations'; import { formattedAmountToLovelace, formattedLovelaceToAmount, @@ -12,6 +11,7 @@ import { } from '../../../../source/renderer/app/utils/utxoUtils'; import { getHistogramFromTable } from '../setup/utxo-helpers'; +/* eslint-disable no-unused-expressions */ Given('the `getUtxoChartData` function receives the following props:', function( data ) { @@ -55,10 +55,10 @@ Then( const walletAmountThreshold = formattedAmountToLovelace( String(walletAmount) ); - const { histogram, utxoChartData, sortedHistogram } = this.context; + const { utxoChartData, sortedHistogram } = this.context; const expectedAggregatedUtxosAmount = sortedHistogram.reduce( - (sum, [walletAmount, walletUtxosAmount]) => { - if (walletAmount >= walletAmountThreshold) + (sum, [amount, walletUtxosAmount]) => { + if (amount >= walletAmountThreshold) sum += parseInt(walletUtxosAmount, 10); return sum; }, @@ -94,7 +94,7 @@ Then('the response should have type {string}', function(type) { if (type === 'array') { return expect(Array.isArray(response)).to.be.true; } - expect(typeof response).to.equal(type); + return expect(typeof response).to.equal(type); }); Then('wallet amounts less than {int} should not be modified', function(amount) { diff --git a/features/wallet-settings-recovery-phrase-verification.feature b/features/wallet-settings-recovery-phrase-verification.feature index 5d1b7f9816..e327ad01bf 100644 --- a/features/wallet-settings-recovery-phrase-verification.feature +++ b/features/wallet-settings-recovery-phrase-verification.feature @@ -1,4 +1,4 @@ -@e2e +@e2e @watch Feature: Wallet Settings - Recovery Phrase Verification Background: @@ -29,4 +29,3 @@ Feature: Wallet Settings - Recovery Phrase Verification Then I should see the error dialog When I click the close button Then I should not see any dialog - diff --git a/gulpfile.js b/gulpfile.js index e7a7537808..3ceec80513 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -63,8 +63,10 @@ const buildRendererWatch = () => done => rendererInputSource() .pipe( webpackStream(rendererWebpackWatchConfig, webpack, () => { - // Reload app everytime after renderer script has been re-compiled - electronServer.reload(); + if (electronServer) { + // Reload app everytime after renderer script has been re-compiled + electronServer.reload(); + } done(); }) ) diff --git a/package.json b/package.json index 0ba4e664be..5e9ffa4c4b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:unit:unbound": "yarn cucumber --require 'features/tests/unit/**/*.js' --tags '@unbound and not @skip and not @wip'", "test:e2e": "yarn cucumber --require 'features/tests/e2e/**/*.js' --tags '@e2e and not @skip and not @wip'", "test:e2e:watch": "gulp test:e2e:watch", + "test:e2e:watch:once": "KEEP_APP_AFTER_TESTS=true yarn test:e2e --tags '@e2e and @watch'", "cucumber": "cross-env NODE_ENV=test cucumber-js --require-module @babel/register -f node_modules/cucumber-pretty --format-options '{\"snippetInterface\": \"async-await\"}'", "debug": "gulp debug", "package": "gulp build && cross-env NODE_ENV=production node -r @babel/register -r @babel/polyfill scripts/package.js", @@ -169,6 +170,7 @@ "qr-image": "3.2.0", "qrcode.react": "0.8.0", "react": "16.6.3", + "react-animate-height": "2.0.15", "react-copy-to-clipboard": "5.0.1", "react-custom-scrollbars": "4.2.1", "react-dom": "16.6.3", @@ -185,6 +187,7 @@ "route-parser": "0.0.5", "rust-cardano-crypto": "0.2.0", "safe-buffer": "5.1.1", + "semver": "6.3.0", "source-map-support": "0.5.9", "spectron-fake-dialog": "0.0.1", "split-file": "2.1.0", diff --git a/source/renderer/app/App.js b/source/renderer/app/App.js index 924a5b4260..a463279881 100755 --- a/source/renderer/app/App.js +++ b/source/renderer/app/App.js @@ -15,9 +15,11 @@ import DaedalusDiagnosticsDialog from './containers/status/DaedalusDiagnosticsDi import BlockConsolidationStatusDialog from './containers/status/BlockConsolidationStatusDialog'; import GenericNotificationContainer from './containers/notifications/GenericNotificationContainer'; import AutomaticUpdateNotificationDialog from './containers/notifications/AutomaticUpdateNotificationDialog'; +import NewsOverlayContainer from './containers/news/NewsOverlayContainer'; import { DIALOGS } from '../../common/ipc/constants'; import type { StoresMap } from './stores/index'; import type { ActionsMap } from './actions/index'; +import NewsFeedContainer from './containers/news/NewsFeedContainer'; @observer export default class App extends Component<{ @@ -31,15 +33,35 @@ export default class App extends Component<{ } render() { const { stores, actions, history } = this.props; - const { app, nodeUpdate } = stores; - const { showNextUpdate } = nodeUpdate; - const { isActiveDialog } = app; + const { app, nodeUpdate, networkStatus } = stores; + const { + showNextUpdate, + isNewAppVersionAvailable, + isUpdatePostponed, + isUpdateAvailable, + } = nodeUpdate; + const { isActiveDialog, isSetupPage } = app; + const { isNodeStopping, isNodeStopped } = networkStatus; const locale = stores.profile.currentLocale; const mobxDevTools = global.environment.mobxDevTools ? : null; const { currentTheme } = stores.profile; const themeVars = require(`./themes/daedalus/${currentTheme}.js`).default; const { ABOUT, BLOCK_CONSOLIDATION, DAEDALUS_DIAGNOSTICS } = DIALOGS; + const isManualUpdateAvailable = + isNewAppVersionAvailable && + !isNodeStopping && + !isNodeStopped && + !isUpdatePostponed && + !isUpdateAvailable; + + const canShowNews = + !isSetupPage && // Active page is not "Language Selection" or "Terms of Use" + !showNextUpdate && // Autmatic update not available + !isManualUpdateAvailable && // Manual update not available + !isNodeStopping && // Daedalus is not shutting down + !isNodeStopped; // Daedalus is not shutting down + return ( @@ -65,6 +87,10 @@ export default class App extends Component<{ , ] )} + {canShowNews && [ + , + , + ]} diff --git a/source/renderer/app/actions/app-actions.js b/source/renderer/app/actions/app-actions.js index 73d2f29179..91b6258084 100644 --- a/source/renderer/app/actions/app-actions.js +++ b/source/renderer/app/actions/app-actions.js @@ -8,6 +8,7 @@ export default class AppActions { getGpuStatus: Action = new Action(); initAppEnvironment: Action = new Action(); setNotificationVisibility: Action = new Action(); + toggleNewsFeed: Action = new Action(); // About dialog actions closeAboutDialog: Action = new Action(); diff --git a/source/renderer/app/api/api.js b/source/renderer/app/api/api.js index 06d34de5ba..e9b9ca2fb1 100644 --- a/source/renderer/app/api/api.js +++ b/source/renderer/app/api/api.js @@ -47,6 +47,9 @@ import { updateWallet } from './wallets/requests/updateWallet'; import { getWalletUtxos } from './wallets/requests/getWalletUtxos'; import { getWalletIdAndBalance } from './wallets/requests/getWalletIdAndBalance'; +// News requests +import { getNews } from './news/requests/getNews'; + // utility functions import { awaitUpdateChannel, @@ -135,6 +138,9 @@ import type { GetWalletIdAndBalanceResponse, } from './wallets/types'; +// News Types +import type { GetNewsResponse } from './news/types'; + // Common errors import { GenericApiError, @@ -161,6 +167,8 @@ import { } from './transactions/errors'; import type { FaultInjectionIpcRequest } from '../../../common/types/cardano-node.types'; import { TlsCertificateNotValidError } from './nodes/errors'; +import { getSHA256HexForString } from './utils/hashing'; +import { getNewsHash } from './news/requests/getNewsHash'; export default class AdaApi { config: RequestConfig; @@ -1040,6 +1048,42 @@ export default class AdaApi { } }; + getNews = async (): Promise => { + Logger.debug('AdaApi::getNews called'); + + // Fetch news json + let rawNews: string; + let news: GetNewsResponse; + try { + rawNews = await getNews(); + news = JSON.parse(rawNews); + } catch (error) { + Logger.error('AdaApi::getNews error', { error }); + throw new Error('Unable to fetch news'); + } + + // Fetch news verification hash + let newsHash: string; + let expectedNewsHash: string; + try { + newsHash = await getSHA256HexForString(rawNews); + expectedNewsHash = await getNewsHash(news.updatedAt); + } catch (error) { + Logger.error('AdaApi::getNews (hash) error', { error }); + throw new Error('Unable to fetch news hash'); + } + + if (newsHash !== expectedNewsHash) { + throw new Error('Newsfeed could not be verified'); + } + + Logger.debug('AdaApi::getNews success', { + updatedAt: news.updatedAt, + items: news.items.length, + }); + return news; + }; + setCardanoNodeFault = async (fault: FaultInjectionIpcRequest) => { await cardanoFaultInjectionChannel.send(fault); }; @@ -1054,6 +1098,10 @@ export default class AdaApi { setLatestAppVersion: Function; setApplicationVersion: Function; setFaultyNodeSettingsApi: boolean; + resetTestOverrides: Function; + + // Newsfeed testing utility + setFakeNewsFeedJsonForTesting: (fakeNewsfeedJson: GetNewsResponse) => void; } // ========== TRANSFORM SERVER DATA INTO FRONTEND MODELS ========= diff --git a/source/renderer/app/api/news/requests/getNews.js b/source/renderer/app/api/news/requests/getNews.js new file mode 100644 index 0000000000..444a5259f8 --- /dev/null +++ b/source/renderer/app/api/news/requests/getNews.js @@ -0,0 +1,19 @@ +// @flow +import { externalRequest } from '../../utils/externalRequest'; +import { getNewsURL } from '../../../utils/network'; + +const { network } = global.environment; +const hostname = getNewsURL(network); +const path = '/newsfeed'; +const filename = `newsfeed_${network}.json`; + +export const getNews = (): Promise => + externalRequest( + { + hostname, + path: `${path}/${filename}`, + method: 'GET', + protocol: 'https', + }, + true + ); diff --git a/source/renderer/app/api/news/requests/getNewsHash.js b/source/renderer/app/api/news/requests/getNewsHash.js new file mode 100644 index 0000000000..522b3b4c4f --- /dev/null +++ b/source/renderer/app/api/news/requests/getNewsHash.js @@ -0,0 +1,18 @@ +// @flow +import { externalRequest } from '../../utils/externalRequest'; +import { getNewsHashURL } from '../../../utils/network'; + +const { network } = global.environment; +const hostname = getNewsHashURL(network); +const path = `/newsfeed-verification/${network}`; + +export const getNewsHash = (timestamp: number): Promise => + externalRequest( + { + hostname, + path: `${path}/${timestamp}.txt`, + method: 'GET', + protocol: 'https', + }, + true + ); diff --git a/source/renderer/app/api/news/types.js b/source/renderer/app/api/news/types.js new file mode 100644 index 0000000000..da878a5f55 --- /dev/null +++ b/source/renderer/app/api/news/types.js @@ -0,0 +1,41 @@ +// @flow + +export type NewsTranslations = { + 'en-US': string, + 'ja-JP': string, +}; + +export type NewsAction = { + label: NewsTranslations, + url?: NewsTranslations, + route?: string, +}; + +export type NewsTarget = { + daedalusVersion: ?string, + platform: string, +}; + +export type NewsType = 'incident' | 'alert' | 'announcement' | 'info'; + +export type NewsTimestamp = number; + +export type NewsItem = { + title: NewsTranslations, + content: NewsTranslations, + target: NewsTarget, + action: NewsAction, + date: NewsTimestamp, + type: NewsType, +}; + +export type GetNewsResponse = { + updatedAt: number, + items: Array, +}; + +export type GetReadNewsResponse = { + readNewsItems: NewsTimestamp[], +}; + +export type MarkNewsAsReadResponse = Array; diff --git a/source/renderer/app/api/utils/externalRequest.js b/source/renderer/app/api/utils/externalRequest.js index 63d9562d79..700214c29f 100644 --- a/source/renderer/app/api/utils/externalRequest.js +++ b/source/renderer/app/api/utils/externalRequest.js @@ -14,7 +14,10 @@ export type HttpOptions = { }, }; -export const externalRequest = (httpOptions: HttpOptions): Promise => +export const externalRequest = ( + httpOptions: HttpOptions, + raw: boolean = false +): Promise => new Promise((resolve, reject) => { if (!ALLOWED_EXTERNAL_HOSTNAMES.includes(httpOptions.hostname)) { return reject(new Error('Hostname not allowed')); @@ -33,8 +36,7 @@ export const externalRequest = (httpOptions: HttpOptions): Promise => response.on('error', error => reject(error)); response.on('end', () => { try { - const parsedBody = JSON.parse(body); - resolve(parsedBody); + resolve(raw ? body : JSON.parse(body)); } catch (error) { // Handle internal server errors (e.g. HTTP 500 - 'Something went wrong') reject(new Error(error)); diff --git a/source/renderer/app/api/utils/hashing.js b/source/renderer/app/api/utils/hashing.js new file mode 100644 index 0000000000..0d3f3e914d --- /dev/null +++ b/source/renderer/app/api/utils/hashing.js @@ -0,0 +1,9 @@ +// @flow + +// From: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest +export const getSHA256HexForString = async (str: string): Promise => { + const data = new TextEncoder().encode(str); + const hashBuffer = await window.crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +}; diff --git a/source/renderer/app/api/utils/localStorage.js b/source/renderer/app/api/utils/localStorage.js index 7687d22ed7..107122df1c 100644 --- a/source/renderer/app/api/utils/localStorage.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -2,6 +2,9 @@ /* eslint-disable consistent-return */ +import { includes } from 'lodash'; +import type { NewsTimestamp } from '../news/types'; + const store = global.electronStore; export type WalletLocalData = { @@ -19,6 +22,7 @@ type StorageKeys = { TERMS_OF_USE_ACCEPTANCE: string, THEME: string, DATA_LAYER_MIGRATION_ACCEPTANCE: string, + READ_NEWS: string, WALLETS: string, }; @@ -36,6 +40,7 @@ export default class LocalStorageApi { TERMS_OF_USE_ACCEPTANCE: `${NETWORK}-TERMS-OF-USE-ACCEPTANCE`, THEME: `${NETWORK}-THEME`, DATA_LAYER_MIGRATION_ACCEPTANCE: `${NETWORK}-DATA-LAYER-MIGRATION-ACCEPTANCE`, + READ_NEWS: `${NETWORK}-READ_NEWS`, WALLETS: `${NETWORK}-WALLETS`, }; } @@ -222,10 +227,50 @@ export default class LocalStorageApi { } }); + getReadNews = (): Promise => + new Promise((resolve, reject) => { + try { + const readNews = store.get(this.storageKeys.READ_NEWS); + if (!readNews) return resolve([]); + resolve(readNews); + } catch (error) { + return reject(error); + } + }); + + markNewsAsRead = ( + newsTimestamps: NewsTimestamp[] + ): Promise => + new Promise((resolve, reject) => { + try { + const readNews = store.get(this.storageKeys.READ_NEWS) || []; + + if (!includes(readNews, newsTimestamps[0])) { + store.set( + this.storageKeys.READ_NEWS, + readNews.concat(newsTimestamps) + ); + } + + resolve(readNews); + } catch (error) { + return reject(error); + } + }); + + unsetReadNews = (): Promise => + new Promise(resolve => { + try { + store.delete(this.storageKeys.READ_NEWS); + resolve(); + } catch (error) {} // eslint-disable-line + }); + reset = async () => { await this.unsetUserLocale(); await this.unsetTermsOfUseAcceptance(); await this.unsetUserTheme(); await this.unsetDataLayerMigrationAcceptance(); + await this.unsetReadNews(); }; } diff --git a/source/renderer/app/api/utils/patchAdaApi.js b/source/renderer/app/api/utils/patchAdaApi.js index 16cf9f5dcc..c5f6e77e3d 100644 --- a/source/renderer/app/api/utils/patchAdaApi.js +++ b/source/renderer/app/api/utils/patchAdaApi.js @@ -15,6 +15,7 @@ import type { GetNodeSettingsResponse, GetLatestAppVersionResponse, } from '../nodes/types'; +import type { GetNewsResponse } from '../news/types'; let LATEST_APP_VERSION = null; let LOCAL_TIME_DIFFERENCE = 0; @@ -23,6 +24,7 @@ let NETWORK_BLOCK_HEIGHT = null; let NEXT_ADA_UPDATE = null; let SUBSCRIPTION_STATUS = null; let APPLICATION_VERSION = null; +let FAKE_NEWSFEED_JSON: ?GetNewsResponse; export default (api: AdaApi) => { api.getLocalTimeDifference = async () => @@ -185,4 +187,28 @@ export default (api: AdaApi) => { api.setNetworkBlockHeight = async (height: number) => { NETWORK_BLOCK_HEIGHT = height; }; + + api.setFakeNewsFeedJsonForTesting = (fakeNewsfeedJson: ?GetNewsResponse) => { + FAKE_NEWSFEED_JSON = fakeNewsfeedJson; + }; + + api.getNews = (): Promise => { + return new Promise((resolve, reject) => { + if (!FAKE_NEWSFEED_JSON) { + reject(new Error('Unable to fetch news')); + } else { + resolve(FAKE_NEWSFEED_JSON); + } + }); + }; + + api.resetTestOverrides = () => { + LATEST_APP_VERSION = null; + LOCAL_TIME_DIFFERENCE = 0; + LOCAL_BLOCK_HEIGHT = null; + NETWORK_BLOCK_HEIGHT = null; + NEXT_ADA_UPDATE = null; + SUBSCRIPTION_STATUS = null; + APPLICATION_VERSION = null; + }; }; diff --git a/source/renderer/app/assets/images/top-bar/news-feed-icon.inline.svg b/source/renderer/app/assets/images/top-bar/news-feed-icon.inline.svg new file mode 100644 index 0000000000..de6253833d --- /dev/null +++ b/source/renderer/app/assets/images/top-bar/news-feed-icon.inline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/source/renderer/app/components/loading/syncing-connecting/SyncingConnecting.js b/source/renderer/app/components/loading/syncing-connecting/SyncingConnecting.js index 5eda8cbecc..a4d70b4791 100644 --- a/source/renderer/app/components/loading/syncing-connecting/SyncingConnecting.js +++ b/source/renderer/app/components/loading/syncing-connecting/SyncingConnecting.js @@ -10,6 +10,7 @@ import { CardanoNodeStates } from '../../../../../common/types/cardano-node.type import styles from './SyncingConnecting.scss'; import type { CardanoNodeState } from '../../../../../common/types/cardano-node.types'; import { REPORT_ISSUE_TIME_TRIGGER } from '../../../config/timingConfig'; +import NewsFeedIcon from '../../widgets/NewsFeedIcon'; let connectingInterval = null; let syncingInterval = null; @@ -35,6 +36,7 @@ type Props = { syncPercentage: number, hasLoadedCurrentLocale: boolean, hasLoadedCurrentTheme: boolean, + hasUnreadNews: boolean, isCheckingSystemTime: boolean, isNodeResponding: boolean, isNodeSubscribed: boolean, @@ -44,10 +46,12 @@ type Props = { isNewAppVersionLoading: boolean, isNewAppVersionLoaded: boolean, disableDownloadLogs: boolean, + showNewsFeedIcon: boolean, onIssueClick: Function, onDownloadLogs: Function, onGetAvailableVersions: Function, onStatusIconClick: Function, + onToggleNewsFeedIconClick: Function, }; @observer @@ -196,6 +200,7 @@ export default class SyncingConnecting extends Component { isSyncing, hasLoadedCurrentLocale, hasLoadedCurrentTheme, + hasUnreadNews, onIssueClick, onDownloadLogs, disableDownloadLogs, @@ -210,6 +215,8 @@ export default class SyncingConnecting extends Component { isNodeStopped, syncPercentage, onStatusIconClick, + onToggleNewsFeedIconClick, + showNewsFeedIcon, } = this.props; const componentStyles = classNames([ @@ -219,6 +226,11 @@ export default class SyncingConnecting extends Component { isSyncing ? styles['is-syncing'] : null, ]); + const newsFeedIconStyles = classNames([ + isConnecting ? 'connectingScreen' : null, + isSyncing || isSynced ? 'syncingScreen' : null, + ]); + return (
{this.showReportIssue && ( @@ -231,6 +243,13 @@ export default class SyncingConnecting extends Component { isSyncing={isSyncing} /> )} + {showNewsFeedIcon && ( + + )} , + onCloseOpenAlert: Function, + onMarkNewsAsRead: Function, + onOpenExternalLink: Function, + allAlertsCount: number, + hideCounter?: boolean, +}; + +@observer +export default class AlertsOverlay extends Component { + constructor(props: Props) { + super(props); + this.state = { + showOverlay: true, + }; + } + + localizedDateFormat: 'MM/DD/YYYY'; + + componentWillMount() { + this.localizedDateFormat = moment.localeData().longDateFormat('L'); + } + + contentClickHandler(event: SyntheticMouseEvent) { + const linkUrl = get(event, ['target', 'href']); + if (linkUrl) { + event.preventDefault(); + this.props.onOpenExternalLink(linkUrl); + } + } + + onClose = () => { + const { alerts } = this.props; + if (alerts.length <= 1) { + this.props.onMarkNewsAsRead(alerts[0].date); + this.props.onCloseOpenAlert(); + this.setState({ showOverlay: false }); + return; + } + this.props.onMarkNewsAsRead(alerts[0].date); + }; + + renderAction = (action: Object) => { + if (action && action.url) { + return ( + + ); + } + return null; + }; + + renderCounter = (alerts: Array) => { + const { allAlertsCount, hideCounter } = this.props; + if (!hideCounter) { + return ( + + {allAlertsCount - alerts.length + 1} / {allAlertsCount} + + ); + } + return null; + }; + + render() { + const { showOverlay } = this.state; + const { alerts } = this.props; + const [alert] = alerts; + const { content, date, action, title } = alert; + return ( + showOverlay && ( +
+ + {this.renderCounter(alerts)} +

{title}

+ + {moment(date).format(this.localizedDateFormat)} + +
+ +
+ {this.renderAction(action)} +
+ ) + ); + } +} diff --git a/source/renderer/app/components/news/AlertsOverlay.scss b/source/renderer/app/components/news/AlertsOverlay.scss new file mode 100644 index 0000000000..f7aa09469e --- /dev/null +++ b/source/renderer/app/components/news/AlertsOverlay.scss @@ -0,0 +1,187 @@ +.actionBtn { + background-color: rgba(45, 45, 45, 0.1); + border: 1px solid #2d2d2d; + border-radius: 5px; + box-sizing: border-box; + color: #2d2d2d; + font-family: var(--font-medium); + font-size: 14px; + font-weight: 500; + line-height: 1.36; + margin-bottom: 22.5px; + min-height: 50px; + width: 360px; + svg { + height: 13px; + margin: -1px 5px; + width: 13px; + + g { + path { + stroke: #2d2d2d; + } + } + } + &:hover { + background-color: #2d2d2d; + color: #fff; + cursor: pointer; + svg { + g { + path { + stroke: #fff; + } + } + } + } +} + +.closeButton { + cursor: pointer; + display: flex; + justify-content: flex-end; + position: fixed; + right: 10px; + top: 10px; + z-index: 4; + + span { + border-radius: 50%; + height: 44px; + width: 44px; + + &:hover { + background-color: rgba(0, 0, 0, 0.16); + border-radius: 50%; + } + } + + svg { + height: 12px; + margin-top: 16px; + width: 12px; + + polygon { + fill: #2d2d2d; + } + path { + stroke: #2d2d2d; + } + } +} + +.component { + align-items: center; + background-color: rgba(255, 185, 35, 0.98); + color: #2d2d2d; + display: flex; + flex-direction: column; + font-family: var(--font-regular); + height: 100vh; + justify-content: center; + position: fixed; + top: 0px; + width: 100vw; + z-index: 20; +} + +.content { + background-color: rgba(45, 45, 45, 0.1); + font-size: 16px; + line-height: 1.2; + margin-bottom: 30px; + max-height: 464px; + max-width: 600px; + min-width: 600px; + opacity: 0.8; + overflow-y: scroll; + padding: 12px 20px; + word-break: break-word; + &::-webkit-scrollbar-thumb { + background-color: rgba(45, 45, 45, 0.3); + min-height: 60px; + outline: none; + width: 4px; + + &:hover { + background-color: rgba(45, 45, 45, 0.5); + } + } + + h1, + h2 { + font-family: var(--font-medium); + margin-bottom: 6px; + } + + h1 { + font-size: 18px; + } + + h2 { + font-size: 16px; + } + + * + h2 { + margin-top: 16px; + } + + ol, + ul { + list-style: disc; + margin-left: 20px; + } + + ol { + list-style-type: decimal; + } + + p, + li { + color: rgba(45, 45, 45, 0.7); + font-family: var(--font-regular); + font-size: 14px; + line-height: 1.5; + margin-bottom: 6px; + + strong { + color: rgba(45, 45, 45, 1); + font-weight: 500; + } + } + + a { + border-bottom: 1px solid #2d2d2d; + color: #2d2d2d; + text-decoration: none; + } + + em { + font-style: italic; + } +} + +.counter { + color: #2d2d2d; + font-family: var(--font-medium); + font-size: 14px; + font-weight: 500; + position: fixed; + right: 74px; + top: 24.5px; +} + +.date { + font-family: var(--font-medium); + font-size: 14px; + line-height: 1.43; + margin-bottom: 16px; + opacity: 0.5; +} + +.title { + font-size: 20px; + line-height: 1.2; + margin-bottom: 0; + margin-top: 22.5px; +} diff --git a/source/renderer/app/components/news/IncidentOverlay.js b/source/renderer/app/components/news/IncidentOverlay.js new file mode 100644 index 0000000000..a4e997b2b4 --- /dev/null +++ b/source/renderer/app/components/news/IncidentOverlay.js @@ -0,0 +1,68 @@ +// @flow +import React, { Component } from 'react'; +import moment from 'moment'; +import { observer } from 'mobx-react'; +import { get } from 'lodash'; +import ReactMarkdown from 'react-markdown'; +import SVGInline from 'react-svg-inline'; +import News from '../../domains/News'; +import externalLinkIcon from '../../assets/images/link-ic.inline.svg'; +import styles from './IncidentOverlay.scss'; + +type Props = { + incident: News.News, + onOpenExternalLink: Function, +}; + +@observer +export default class IncidentOverlay extends Component { + localizedDateFormat: 'MM/DD/YYYY'; + + componentWillMount() { + this.localizedDateFormat = moment.localeData().longDateFormat('L'); + } + + contentClickHandler(event: SyntheticMouseEvent) { + const linkUrl = get(event, ['target', 'href']); + if (linkUrl) { + event.preventDefault(); + this.props.onOpenExternalLink(linkUrl); + } + } + + renderAction = (action: Object) => { + if (action && action.url) { + return ( + + ); + } + return null; + }; + + render() { + const { incident } = this.props; + const { content, date, action, title } = incident; + return ( +
+

{title}

+ + {moment(date).format(this.localizedDateFormat)} + +
+ +
+ {this.renderAction(action)} +
+ ); + } +} diff --git a/source/renderer/app/components/news/IncidentOverlay.scss b/source/renderer/app/components/news/IncidentOverlay.scss new file mode 100644 index 0000000000..8493e12b62 --- /dev/null +++ b/source/renderer/app/components/news/IncidentOverlay.scss @@ -0,0 +1,143 @@ +.actionBtn { + background-color: rgba(0, 0, 0, 0.1); + border: 1px solid #fafbfc; + border-radius: 5px; + box-sizing: border-box; + color: #fafbfc; + font-family: var(--font-medium); + font-size: 14px; + font-weight: 500; + line-height: 1.36; + margin-bottom: 22.5px; + min-height: 50px; + width: 360px; + svg { + height: 13px; + margin: -1px 5px; + width: 13px; + + g { + path { + stroke: #fafbfc; + } + } + } + + &:hover { + background-color: #fafbfc; + color: #ab1700; + cursor: pointer; + svg { + g { + path { + stroke: #ab1700; + } + } + } + } +} + +.component { + align-items: center; + background-color: rgba(171, 23, 0, 0.98); + color: #fafbfc; + display: flex; + flex-direction: column; + font-family: var(--font-regular); + height: 100vh; + justify-content: center; + position: fixed; + top: 0px; + width: 100vw; + z-index: 20; +} + +.content { + background-color: rgba(0, 0, 0, 0.1); + font-size: 16px; + line-height: 1.2; + margin-bottom: 30px; + max-height: 464px; + max-width: 600px; + min-width: 600px; + opacity: 0.8; + overflow-y: scroll; + padding: 12px 20px; + word-break: break-word; + &::-webkit-scrollbar-thumb { + background-color: rgba(250, 251, 252, 0.3); + outline: none; + width: 4px; + + &:hover { + background-color: rgba(250, 251, 252, 0.5); + } + } + + h1, + h2 { + font-family: var(--font-medium); + margin-bottom: 6px; + } + + h1 { + font-size: 18px; + } + + h2 { + font-size: 16px; + } + + * + h2 { + margin-top: 16px; + } + + ol, + ul { + list-style: disc; + margin-left: 20px; + } + + ol { + list-style-type: decimal; + } + + p, + li { + color: rgba(250, 251, 252, 0.7); + font-family: var(--font-regular); + font-size: 14px; + line-height: 1.5; + margin-bottom: 6px; + + strong { + color: rgba(250, 251, 252, 1); + font-weight: 500; + } + } + + a { + border-bottom: 1px solid #fafbfc; + color: #fafbfc; + text-decoration: none; + } + + em { + font-style: italic; + } +} + +.date { + font-family: var(--font-medium); + font-size: 14px; + line-height: 1.43; + margin-bottom: 16px; + opacity: 0.5; +} + +.title { + font-size: 20px; + line-height: 1.2; + margin-bottom: 0; + margin-top: 22.5px; +} diff --git a/source/renderer/app/components/news/NewsFeed.js b/source/renderer/app/components/news/NewsFeed.js new file mode 100644 index 0000000000..91e28877f1 --- /dev/null +++ b/source/renderer/app/components/news/NewsFeed.js @@ -0,0 +1,176 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import classNames from 'classnames'; +import SVGInline from 'react-svg-inline'; +import { get } from 'lodash'; +import closeCrossThin from '../../assets/images/close-cross-thin.inline.svg'; +import styles from './NewsFeed.scss'; +import News from '../../domains/News'; +import NewsItem from './NewsItem'; +import LoadingSpinner from '../widgets/LoadingSpinner'; + +const messages = defineMessages({ + newsFeedEmpty: { + id: 'news.newsfeed.empty', + defaultMessage: 'Newsfeed is empty', + description: 'Newsfeed is empty', + }, + newsFeedNoFetch: { + id: 'news.newsfeed.noFetch', + defaultMessage: 'Trying to fetch the newsfeed...', + description: 'Trying to fetch the newsfeed...', + }, + newsFeedTitle: { + id: 'news.newsfeed.title', + defaultMessage: 'Newsfeed', + description: 'Newsfeed', + }, +}); + +type Props = { + onClose: Function, + onOpenAlert?: Function, + news?: News.NewsCollection, + isNewsFeedOpen: boolean, + onMarkNewsAsRead: Function, + openWithoutTransition?: boolean, + isLoadingNews: boolean, + onOpenExternalLink: Function, + onGoToRoute: Function, +}; + +type State = { + hasShadow: boolean, +}; + +const SCROLLABLE_DOM_ELEMENT_SELECTOR = '.NewsFeed_newsFeedList'; + +@observer +export default class NewsFeed extends Component { + static defaultProps = { + onClose: null, + openWithoutTransition: false, + }; + + static contextTypes = { + intl: intlShape.isRequired, + }; + + state = { + hasShadow: false, + }; + + scrollableDomElement: ?HTMLElement = null; + + componentDidMount() { + this.scrollableDomElement = document.querySelector( + SCROLLABLE_DOM_ELEMENT_SELECTOR + ); + if (!(this.scrollableDomElement instanceof HTMLElement)) return; + this.scrollableDomElement.addEventListener('scroll', this.handleOnScroll); + } + + componentWillUnmount() { + if (this.scrollableDomElement) { + this.scrollableDomElement.removeEventListener( + 'scroll', + this.handleOnScroll + ); + } + } + + handleOnScroll = () => { + const { hasShadow: currentHasShadow } = this.state; + + if (this.scrollableDomElement) { + const { scrollTop } = this.scrollableDomElement; + const hasShadow = scrollTop > 3; + if (currentHasShadow !== hasShadow) { + this.setState({ + hasShadow, + }); + } + } + }; + + render() { + const { intl } = this.context; + const { + news, + isNewsFeedOpen, + isLoadingNews, + onClose, + onOpenAlert, + onMarkNewsAsRead, + onOpenExternalLink, + openWithoutTransition, + onGoToRoute, + } = this.props; + const { hasShadow } = this.state; + + const totalNewsItems = get(news, 'all', []).length; + const totalUnreadNewsItems = get(news, 'unread', []).length; + const componentClasses = classNames([ + styles.component, + isNewsFeedOpen ? styles.show : null, + openWithoutTransition ? styles.noTransition : null, + ]); + + const newsFeedHeaderStyles = classNames([ + styles.newsFeedHeader, + hasShadow ? styles.hasShadow : null, + ]); + + return ( +
+
+

+ {intl.formatMessage(messages.newsFeedTitle)} + {totalUnreadNewsItems > 0 && ( + + {totalUnreadNewsItems} + + )} +

+ +
+
+ {news && totalNewsItems > 0 && ( +
+ {news.all.map(newsItem => ( + + ))} +
+ )} + {news && totalNewsItems === 0 && !isLoadingNews && ( +
+

+ {intl.formatMessage(messages.newsFeedEmpty)} +

+
+ )} + {(!news || totalNewsItems === 0) && isLoadingNews && ( +
+

+ {intl.formatMessage(messages.newsFeedNoFetch)} +

+ +
+ )} +
+
+ ); + } +} diff --git a/source/renderer/app/components/news/NewsFeed.scss b/source/renderer/app/components/news/NewsFeed.scss new file mode 100644 index 0000000000..df96521366 --- /dev/null +++ b/source/renderer/app/components/news/NewsFeed.scss @@ -0,0 +1,133 @@ +.component { + background: var(--theme-news-feed-background-color); + box-shadow: var(--theme-news-feed-box-shadow-color); + display: flex; + flex-direction: column; + height: 100%; + margin-right: -480px; + overflow: hidden; + position: fixed; + right: 0; + top: 0; + transition: margin-right 400ms ease; + width: 460px; + z-index: 20; + + &.noTransition { + transition: none; + } + + .newsFeedHeader { + align-items: center; + background: var(--theme-news-feed-header-background-color); + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2); + display: flex; + flex-shrink: 0; + height: 84px; + justify-content: space-between; + padding: 30px 30px 30px 40px; + position: relative; + transition: box-shadow 0.15s ease-in; + width: 100%; + z-index: 1; + + &.hasShadow { + box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.2); + } + + .newsFeedTitle { + color: var(--theme-news-feed-header-title-color); + display: inline-block; + font-family: var(--font-regular); + font-size: 18px; + line-height: 1.33; + margin-top: 3px; + position: relative; + } + + .newsFeedBadge { + background-color: var(--theme-news-feed-badge-background-color); + border-radius: 10px; + color: var(--theme-news-feed-badge-text-color); + display: inline-block; + font-size: 11px; + font-weight: bold; + left: 100%; + line-height: 16px; + margin: 3px 8px; + min-width: 17px; + padding: 0 5px 1px; + position: absolute; + text-align: center; + top: 0; + } + + .newsFeedCloseBtn { + cursor: pointer; + + span { + border-radius: 50%; + padding: 15.5px 16px; + + &:hover { + background-color: var( + --theme-news-feed-icon-close-hover-background-color + ); + } + } + + svg { + height: 12px; + position: relative; + top: 2px; + width: 12px; + + & > g { + stroke: var(--theme-news-feed-icon-close-button-color); + } + } + } + } + + .newsFeedList { + background: var(--theme-news-feed-background-color); + height: calc(100% - 84px); + overflow-x: hidden; + overflow-y: overlay; + padding: 20px; + + &::-webkit-scrollbar-track { + margin: 13px 0; + } + + .newsFeedEmptyContainer, + .newsFeedNoFetchContainer { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + + .newsFeedEmpty, + .newsFeedNoFetch { + color: var(--theme-news-feed-no-fetch-color); + font-family: var(--font-regular); + font-size: 14px; + font-weight: 300; + line-height: 1.36; + margin: 20px 0; + opacity: 0.7; + text-align: center; + } + } + + .newsFeedItemsContainer { + font-family: var(--font-regular); + overflow: hidden; + } + } + + &.show { + margin-right: 0; + } +} diff --git a/source/renderer/app/components/news/NewsItem.js b/source/renderer/app/components/news/NewsItem.js new file mode 100644 index 0000000000..05256cac96 --- /dev/null +++ b/source/renderer/app/components/news/NewsItem.js @@ -0,0 +1,171 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import classNames from 'classnames'; +import ReactMarkdown from 'react-markdown'; +import moment from 'moment'; +import { get } from 'lodash'; +import SVGInline from 'react-svg-inline'; +import AnimateHeight from 'react-animate-height'; +import News, { NewsTypes } from '../../domains/News'; +import externalLinkIcon from '../../assets/images/link-ic.inline.svg'; +import styles from './NewsItem.scss'; + +type Props = { + newsItem: News.News, + onMarkNewsAsRead: Function, + onOpenExternalLink: Function, + onOpenAlert?: Function, + onGoToRoute: Function, + expandWithoutTransition?: boolean, + isNewsFeedOpen: boolean, +}; + +type State = { + newsItemExpanded: boolean, + newsItemCollapsible: boolean, +}; + +@observer +export default class NewsItem extends Component { + static defaultProps = { + onNewsItemActionClick: null, + expandWithoutTransition: false, + }; + + localizedDateFormat: 'MM/DD/YYYY'; + + state = { + newsItemExpanded: false, + newsItemCollapsible: true, + }; + + componentWillReceiveProps(nextProps: Props) { + const { newsItemExpanded } = this.state; + if ( + this.props.isNewsFeedOpen && + !nextProps.isNewsFeedOpen && + newsItemExpanded + ) { + this.setState({ newsItemExpanded: false }); + } + } + + componentWillMount() { + this.localizedDateFormat = moment.localeData().longDateFormat('L'); + } + + newsItemClickHandler(event: SyntheticMouseEvent) { + const linkUrl = get(event, ['target', 'href']); + if (linkUrl) { + event.preventDefault(); + this.props.onOpenExternalLink(linkUrl); + } else { + const { type, date } = this.props.newsItem; + const { newsItemCollapsible } = this.state; + if (type === NewsTypes.INFO || type === NewsTypes.ANNOUNCEMENT) { + if (newsItemCollapsible) { + this.setState(prevState => ({ + newsItemExpanded: !prevState.newsItemExpanded, + })); + } else { + this.setState({ newsItemCollapsible: true }); + } + } + if (NewsTypes.ALERT && this.props.onOpenAlert) { + this.props.onOpenAlert(date); + } + this.props.onMarkNewsAsRead(date); + } + } + + newsItemButtonClickHandler(event: SyntheticMouseEvent) { + event.preventDefault(); + event.stopPropagation(); + const { onOpenExternalLink, newsItem, onGoToRoute } = this.props; + const { url, route } = newsItem.action; + + if (url) { + onOpenExternalLink(url, event); + } else if (route) { + onGoToRoute(route); + } + } + + generateTitleWithBadge = (title: string, isRead: boolean) => { + const wordsArray = title.split(' '); + const lastWordIndex = wordsArray.length - 1; + const lastWord = wordsArray[lastWordIndex]; + + // Remove last word from array + wordsArray.splice(lastWordIndex, 1); + // Join words without last one + const firstSentencePart = wordsArray.join(' '); + + return ( +

+ {firstSentencePart ? `${firstSentencePart} ` : null} + + {lastWord}  + {!isRead && } + +

+ ); + }; + + render() { + const { newsItem, expandWithoutTransition } = this.props; + const componentClasses = classNames([ + styles.component, + newsItem.type ? styles[newsItem.type] : null, + this.state.newsItemExpanded ? styles.expanded : null, + newsItem.read ? styles.isRead : null, + ]); + const { route } = newsItem.action; + const title = this.generateTitleWithBadge(newsItem.title, newsItem.read); + + return ( +
+ {title} +
+ {moment(newsItem.date).format(this.localizedDateFormat)} +
+
+ +
+ +
+ +
+
+
+ ); + } +} diff --git a/source/renderer/app/components/news/NewsItem.scss b/source/renderer/app/components/news/NewsItem.scss new file mode 100644 index 0000000000..9729dfab49 --- /dev/null +++ b/source/renderer/app/components/news/NewsItem.scss @@ -0,0 +1,159 @@ +.component, +.isRead, +.info { + background-color: var(--theme-news-item-info-background-color); + border-radius: 5px; + cursor: pointer; + padding: 20px; + position: relative; + width: 100%; + word-break: break-word; + + & + .component { + margin-top: 10px; + } + + &.isRead { + opacity: 0.5; + } + + &.alert { + background-color: var(--theme-news-item-alert-background-color); + } + + &.announcement { + background-color: var(--theme-news-item-announcement-background-color); + } + + .newsItemTitle { + color: var(--theme-news-item-title-color); + font-family: var(--font-regular); + font-size: 16px; + line-height: 1.33; + margin-bottom: 2px; + + .lastWordWrapper { + display: inline-block; + word-break: break-all; + + .newsItemBadge { + background-color: var(--theme-news-item-badge-color); + border-radius: 12.5px; + display: inline-block; + height: 8px; + margin: 3px 8px 3px 4px; + width: 8px; + } + } + } + + .newsItemDate { + color: var(--theme-news-item-title-color); + font-family: var(--font-light); + font-size: 14px; + font-weight: 300; + line-height: 1.36; + opacity: 0.7; + } + + .newsItemContentContainer { + color: var(--theme-news-item-title-color); + line-height: 1.36; + margin-top: 6px; + opacity: 0.7; + + h1, + h2 { + font-family: var(--font-medium); + margin-bottom: 3px; + } + + h1 { + font-size: 16px; + } + + h2 { + font-size: 15px; + } + + * + h2 { + margin-top: 8px; + } + + ol, + ul { + list-style: disc; + margin-left: 20px; + } + + ol { + list-style-type: decimal; + } + + p, + li { + font-family: var(--font-regular); + font-size: 14px; + margin-bottom: 3px; + + strong { + font-weight: 500; + } + } + + a { + color: var(--theme-news-item-content-link-color); + text-decoration: underline; + } + + em { + font-style: italic; + } + } + + .newsItemActionBtn { + background-color: var(--theme-news-item-action-button-background-color); + border: solid 1px var(--theme-news-item-action-button-border-color); + border-radius: 2px; + color: var(--theme-news-item-action-button-color); + cursor: pointer; + font-family: var(--font-regular); + font-size: 14px; + font-weight: 500; + line-height: 20px; + margin: 20px 0 0 0; + padding: 5px 10px; + text-align: center; + width: 100%; + + svg { + height: 13px; + margin: -1px 5px; + width: 13px; + + g { + path { + stroke: var(--theme-news-item-action-button-color); + } + } + } + + &:hover { + background-color: var( + --theme-news-item-action-button-background-color-hover + ); + color: var(--theme-news-item-action-button-color-hover); + + svg { + g { + path { + stroke: var(--theme-news-item-action-button-color-hover); + } + } + } + } + } + &.expanded { + opacity: 1; + } +} diff --git a/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss b/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss index c8e1cf7302..c78c217222 100644 --- a/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss +++ b/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss @@ -13,7 +13,7 @@ .page { height: calc(100% - 50px); - overflow: auto; + overflow: overlay; position: relative; } diff --git a/source/renderer/app/components/widgets/LoadingSpinner.js b/source/renderer/app/components/widgets/LoadingSpinner.js index baea9b2d7a..0d530970e1 100644 --- a/source/renderer/app/components/widgets/LoadingSpinner.js +++ b/source/renderer/app/components/widgets/LoadingSpinner.js @@ -8,22 +8,26 @@ import styles from './LoadingSpinner.scss'; type Props = { big?: boolean, + medium?: boolean, }; export default class LoadingSpinner extends Component { static defaultProps = { big: false, + medium: false, }; root: ?HTMLElement; render() { - const { big } = this.props; + const { big, medium } = this.props; const icon = big ? spinnerIconBig : spinnerIconSmall; const componentClasses = classnames([ styles.component, - big ? styles.big : styles.small, + big ? styles.big : null, + medium ? styles.medium : null, + !big && !medium ? styles.small : null, ]); return ( diff --git a/source/renderer/app/components/widgets/LoadingSpinner.scss b/source/renderer/app/components/widgets/LoadingSpinner.scss index 878794d1dc..3a79f95ec4 100644 --- a/source/renderer/app/components/widgets/LoadingSpinner.scss +++ b/source/renderer/app/components/widgets/LoadingSpinner.scss @@ -6,6 +6,16 @@ width: 44px; } + &.medium { + height: 24px; + margin: 0; + width: 24px; + + .icon svg path { + fill: var(--theme-loading-spinner-medium-color); + } + } + &.small { height: 30px; width: 30px; diff --git a/source/renderer/app/components/widgets/NewsFeedIcon.js b/source/renderer/app/components/widgets/NewsFeedIcon.js new file mode 100644 index 0000000000..2e00282597 --- /dev/null +++ b/source/renderer/app/components/widgets/NewsFeedIcon.js @@ -0,0 +1,28 @@ +// @flow +import React, { Component } from 'react'; +import SVGInline from 'react-svg-inline'; +import classNames from 'classnames'; +import newsFeedIcon from '../../assets/images/top-bar/news-feed-icon.inline.svg'; +import styles from './NewsFeedIcon.scss'; + +type Props = { + onNewsFeedIconClick: Function, + newsFeedIconClass?: string, + showDot: boolean, +}; + +export default class NewsFeedIcon extends Component { + render() { + const { onNewsFeedIconClick, newsFeedIconClass, showDot } = this.props; + const componentClasses = classNames([ + styles.component, + showDot ? styles.withDot : null, + newsFeedIconClass, + ]); + return ( + + ); + } +} diff --git a/source/renderer/app/components/widgets/NewsFeedIcon.scss b/source/renderer/app/components/widgets/NewsFeedIcon.scss new file mode 100644 index 0000000000..fe9be17d71 --- /dev/null +++ b/source/renderer/app/components/widgets/NewsFeedIcon.scss @@ -0,0 +1,57 @@ +.component { + position: absolute; + right: 29px; + top: 20px; + + &.withDot::after { + background: var(--theme-news-feed-icon-dot-background-color); + border-radius: 12.5px; + content: ''; + display: block; + height: 8px; + pointer-events: none; + position: absolute; + right: 7px; + top: 7px; + width: 8px; + } +} + +.icon { + border-radius: 50%; + cursor: pointer; + display: block; + height: 44px; + position: relative; + width: 44px; + + svg { + height: 22px; + left: 11px; + position: absolute; + top: 10px; + width: 22px; + + path { + stroke: var(--theme-news-feed-icon-color); + } + } + + &:hover { + background-color: var(--theme-news-feed-icon-toggle-hover-background-color); + } +} + +:global { + .connectingScreen { + span svg path { + stroke: var(--theme-news-feed-icon-color-connecting-screen); + } + } + + .syncingScreen { + span svg path { + stroke: var(--theme-news-feed-icon-color-syncing-screen); + } + } +} diff --git a/source/renderer/app/components/widgets/NodeSyncStatusIcon.js b/source/renderer/app/components/widgets/NodeSyncStatusIcon.js index 6c573350dd..c1bac8511f 100644 --- a/source/renderer/app/components/widgets/NodeSyncStatusIcon.js +++ b/source/renderer/app/components/widgets/NodeSyncStatusIcon.js @@ -21,7 +21,6 @@ type Props = { +isSynced: boolean, +syncPercentage: number, }, - isMainnet: boolean, }; export default class NodeSyncStatusIcon extends Component { @@ -30,14 +29,13 @@ export default class NodeSyncStatusIcon extends Component { }; render() { - const { networkStatus, isMainnet } = this.props; + const { networkStatus } = this.props; const { isSynced, syncPercentage } = networkStatus; const { intl } = this.context; const statusIcon = isSynced ? syncedIcon : spinnerIcon; const componentClasses = classNames([ styles.component, isSynced ? styles.synced : styles.syncing, - isMainnet && styles.mainnet, ]); return ( diff --git a/source/renderer/app/components/widgets/NodeSyncStatusIcon.scss b/source/renderer/app/components/widgets/NodeSyncStatusIcon.scss index b09dac8acd..95dfb82be0 100644 --- a/source/renderer/app/components/widgets/NodeSyncStatusIcon.scss +++ b/source/renderer/app/components/widgets/NodeSyncStatusIcon.scss @@ -1,24 +1,12 @@ .component { overflow: visible; position: absolute; - right: 12px; + right: 100px; &:hover { .info { opacity: 1; } } - &.mainnet { - right: 40px; - .icon { - svg { - height: 22px; - width: 22px; - path { - fill: var(--theme-node-sync-icon-color); - } - } - } - } } .icon { @@ -26,6 +14,9 @@ svg { height: 22px; width: 22px; + path { + fill: var(--theme-node-sync-icon-color); + } } } @@ -50,6 +41,7 @@ font-size: 14px; opacity: 0; padding: 10px; + pointer-events: none; position: absolute; right: 0; text-align: right; diff --git a/source/renderer/app/components/widgets/WalletTestEnvironmentLabel.scss b/source/renderer/app/components/widgets/WalletTestEnvironmentLabel.scss index 633f826042..292ae9e52b 100644 --- a/source/renderer/app/components/widgets/WalletTestEnvironmentLabel.scss +++ b/source/renderer/app/components/widgets/WalletTestEnvironmentLabel.scss @@ -1,22 +1,13 @@ .component { background-color: var(--theme-test-environment-label-background-color); - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.38); + border-radius: 0 0 0 2px; color: var(--theme-test-environment-label-text-color); font-family: var(--font-regular); - font-size: 14px; - height: 34px; - line-height: 1.21; - padding: 8px 50px 0 0; + font-size: 10px; + justify-content: center; + line-height: 12px; + padding: 3px 6px 4px; position: absolute; right: 0; - &::before { - background: url('../../assets/images/top-bar/sticker.svg') 0 0 no-repeat; - background-size: auto 100%; - content: ''; - height: 42px; - position: absolute; - right: 100%; - top: -3px; - width: 27.5px; - } + top: 0; } diff --git a/source/renderer/app/config/news.dummy.json b/source/renderer/app/config/news.dummy.json new file mode 100644 index 0000000000..f19505e52c --- /dev/null +++ b/source/renderer/app/config/news.dummy.json @@ -0,0 +1,161 @@ +{ + "updatedAt": 1569324863299, + "items": [ + { + "title": { + "en-US": "Info 1 in English", + "ja-JP": "Info 1 in Japanese" + }, + "content": { + "en-US": "# h1 English info content 1\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese info content 1\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569324863299, + "type": "info" + }, + { + "title": { + "en-US": "Info 2 in English - a little bit longer title", + "ja-JP": "Info 2 in Japanese" + }, + "content": { + "en-US": "# h1 English info content 2\n", + "ja-JP": "# h1 Japanese info content 2\n." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1568979341589, + "type": "info" + }, + { + "title": { + "en-US": "Linux news 1 in English", + "ja-JP": "Linux news 1 in Japanese" + }, + "content": { + "en-US": "# h1 English info content 3\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese info content 3\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569152115002, + "type": "info" + }, + { + "title": { + "en-US": "Announcement 1 in English", + "ja-JP": "Announcement 1 in Japanese" + }, + "content": { + "en-US": "# h1 English announcement content 1\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese announcement content 1\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569065729169, + "type": "announcement" + }, + { + "title": { + "en-US": "Announcement 2 in English - a little bit longer title", + "ja-JP": "Announcement 2 in Japanese" + }, + "content": { + "en-US": "# h1 English announcement content 2\n", + "ja-JP": "# h1 Japanese announcement content 2\n." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["darwin", "win32", "linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1569238489658, + "type": "announcement" + }, + { + "title": { + "en-US": "Windows announcement 1 in English", + "ja-JP": "Windows announcement 1 in Japanese" + }, + "content": { + "en-US": "# h1 English announcement content 3\nUt consequat semper viverra nam libero justo laoreet sit.", + "ja-JP": "# h1 Japanese announcement content 3\nUt consequat semper viverra nam libero justo laoreet sit." + }, + "target": { + "daedalusVersion": "0.12.0 - 0.14.0", + "platforms": ["linux"] + }, + "action": { + "label": { + "en-US": "Visit en-US", + "ja-JP": "Visit ja-JP" + }, + "url": { + "en-US": "https://iohk.zendesk.com/hc/en-us/articles/", + "ja-JP": "https://iohk.zendesk.com/hc/ja/articles/" + } + }, + "date": 1568892951736, + "type": "announcement" + } + ] +} diff --git a/source/renderer/app/config/timingConfig.js b/source/renderer/app/config/timingConfig.js index d17f8367d5..318fe8d93b 100644 --- a/source/renderer/app/config/timingConfig.js +++ b/source/renderer/app/config/timingConfig.js @@ -21,4 +21,7 @@ export const BLOCK_CONSOLIDATION_API_REQUEST_INTERVAL = 30 * 1000; // 30 seconds export const WALLET_UTXO_API_REQUEST_INTERVAL = 5 * 1000; // 5 seconds | unit: milliseconds export const STAKE_POOL_TOOLTIP_HOVER_WAIT = 700; // 700 milliseconds | unit: milliseconds export const COPY_STATE_DIRECTORY_PATH_NOTIFICATION_DURATION = 10; // unit: seconds +export const NEWS_POLL_INTERVAL = 30 * 60 * 1000; // 30 minutes | unit: milliseconds +export const NEWS_POLL_INTERVAL_ON_ERROR = 1 * 60 * 1000; // 1 minute | unit: milliseconds +export const NEWS_POLL_INTERVAL_ON_INCIDENT = 10 * 60 * 1000; // 10 minutes | unit: milliseconds /* eslint-disable max-len */ diff --git a/source/renderer/app/config/urlsConfig.js b/source/renderer/app/config/urlsConfig.js index 283d7c4da8..b59819416e 100644 --- a/source/renderer/app/config/urlsConfig.js +++ b/source/renderer/app/config/urlsConfig.js @@ -14,6 +14,16 @@ export const TESTNET_LATEST_VERSION_INFO_URL = 'updates-cardano-testnet.s3.amazonaws.com'; export const STAGING_LATEST_VERSION_INFO_URL = 'update-awstest.iohkdev.io'; +export const DEVELOPMENT_NEWS_URL = 'daedalus.io'; +export const MAINNET_NEWS_URL = 'daedalus.io'; +export const TESTNET_NEWS_URL = 'daedalus.io'; +export const STAGING_NEWS_URL = 'daedalus.io'; + +export const DEVELOPMENT_NEWS_HASH_URL = 'daedaluswallet.io'; +export const MAINNET_NEWS_HASH_URL = 'daedaluswallet.io'; +export const TESTNET_NEWS_HASH_URL = 'daedaluswallet.io'; +export const STAGING_NEWS_HASH_URL = 'daedaluswallet.io'; + export const ALLOWED_EXTERNAL_HOSTNAMES = [ MAINNET_EXPLORER_URL, STAGING_EXPLORER_URL, @@ -21,4 +31,12 @@ export const ALLOWED_EXTERNAL_HOSTNAMES = [ MAINNET_LATEST_VERSION_INFO_URL, TESTNET_LATEST_VERSION_INFO_URL, STAGING_LATEST_VERSION_INFO_URL, + DEVELOPMENT_NEWS_URL, + MAINNET_NEWS_URL, + TESTNET_NEWS_URL, + STAGING_NEWS_URL, + DEVELOPMENT_NEWS_HASH_URL, + MAINNET_NEWS_HASH_URL, + TESTNET_NEWS_HASH_URL, + STAGING_NEWS_HASH_URL, ]; diff --git a/source/renderer/app/containers/TopBarContainer.js b/source/renderer/app/containers/TopBarContainer.js index e58008cdb0..25a2a6c5a2 100644 --- a/source/renderer/app/containers/TopBarContainer.js +++ b/source/renderer/app/containers/TopBarContainer.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; import TopBar from '../components/layout/TopBar'; import NodeSyncStatusIcon from '../components/widgets/NodeSyncStatusIcon'; +import NewsFeedIcon from '../components/widgets/NewsFeedIcon'; import WalletTestEnvironmentLabel from '../components/widgets/WalletTestEnvironmentLabel'; import type { InjectedProps } from '../types/injectedPropsType'; import menuIconOpened from '../assets/images/menu-opened-ic.inline.svg'; @@ -19,7 +20,7 @@ export default class TopBarContainer extends Component { render() { const { actions, stores } = this.props; - const { sidebar, app, networkStatus, wallets } = stores; + const { sidebar, app, networkStatus, wallets, newsFeed } = stores; const { active, isWalletRoute, hasAnyWallets } = wallets; const { currentRoute, @@ -40,6 +41,9 @@ export default class TopBarContainer extends Component { ) : null; + const { unread } = newsFeed.newsFeedData; + const hasUnreadNews = unread.length > 0; + return ( { activeWallet={activeWallet} > {testnetLabel} - + ); diff --git a/source/renderer/app/containers/loading/SyncingConnectingPage.js b/source/renderer/app/containers/loading/SyncingConnectingPage.js index 8736d3c935..9a21a6267e 100644 --- a/source/renderer/app/containers/loading/SyncingConnectingPage.js +++ b/source/renderer/app/containers/loading/SyncingConnectingPage.js @@ -36,6 +36,9 @@ export default class LoadingSyncingConnectingPage extends Component { isNewAppVersionLoaded, } = stores.nodeUpdate; const { hasLoadedCurrentLocale, hasLoadedCurrentTheme } = stores.profile; + const { toggleNewsFeed } = this.props.actions.app; + const { unread } = stores.newsFeed.newsFeedData; + const hasUnreadNews = unread.length > 0; return ( { isNotEnoughDiskSpace={isNotEnoughDiskSpace} isTlsCertInvalid={isTlsCertInvalid} syncPercentage={syncPercentage} + hasUnreadNews={hasUnreadNews} hasLoadedCurrentLocale={hasLoadedCurrentLocale} hasLoadedCurrentTheme={hasLoadedCurrentTheme} isCheckingSystemTime={forceCheckTimeDifferenceRequest.isExecuting} @@ -64,7 +68,9 @@ export default class LoadingSyncingConnectingPage extends Component { onGetAvailableVersions={this.handleGetAvailableVersions} onStatusIconClick={this.openDaedalusDiagnosticsDialog} onDownloadLogs={this.handleDownloadLogs} + onToggleNewsFeedIconClick={toggleNewsFeed.trigger} disableDownloadLogs={stores.app.isDownloadNotificationVisible} + showNewsFeedIcon={!isNodeStopping && !isNodeStopped} /> ); } diff --git a/source/renderer/app/containers/news/NewsFeedContainer.js b/source/renderer/app/containers/news/NewsFeedContainer.js new file mode 100644 index 0000000000..96984b3c40 --- /dev/null +++ b/source/renderer/app/containers/news/NewsFeedContainer.js @@ -0,0 +1,43 @@ +// @flow +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import NewsFeed from '../../components/news/NewsFeed'; +import type { InjectedProps } from '../../types/injectedPropsType'; + +@inject('stores', 'actions') +@observer +export default class NewsFeedContainer extends Component { + static defaultProps = { actions: null, stores: null }; + + handleMarkNewsAsRead = (newsTimestamps: number) => { + const { stores } = this.props; + const { markNewsAsRead } = stores.newsFeed; + markNewsAsRead([newsTimestamps]); + }; + + handleGoToRoute = (route: string) => { + const { actions } = this.props; + actions.router.goToRoute.trigger({ route }); + }; + + render() { + const { stores, actions } = this.props; + const { newsFeedData, isLoadingNews } = stores.newsFeed; + const { toggleNewsFeed } = actions.app; + const { openExternalLink, newsFeedIsOpen } = stores.app; + + return ( + + ); + } +} diff --git a/source/renderer/app/containers/news/NewsOverlayContainer.js b/source/renderer/app/containers/news/NewsOverlayContainer.js new file mode 100644 index 0000000000..895ccf280d --- /dev/null +++ b/source/renderer/app/containers/news/NewsOverlayContainer.js @@ -0,0 +1,62 @@ +// @flow +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import AlertsOverlay from '../../components/news/AlertsOverlay'; +import IncidentOverlay from '../../components/news/IncidentOverlay'; +import type { InjectedProps } from '../../types/injectedPropsType'; + +@inject('stores', 'actions') +@observer +export default class NewsOverlayContainer extends Component { + static defaultProps = { actions: null, stores: null }; + + render() { + const { app, newsFeed } = this.props.stores; + const { openExternalLink } = app; + const { + closeOpenedAlert, + markNewsAsRead, + newsFeedData, + openedAlert, + } = newsFeed; + const { incident, alerts } = newsFeedData; + const unreadAlerts = alerts.unread; + const allAlertsCount = alerts.all ? alerts.all.length : 0; + + const alertToOpen = []; + if (openedAlert) { + alertToOpen.push(openedAlert); + } + + if (incident) + return ( + + ); + if (unreadAlerts.length > 0) + return ( + + ); + if (alertToOpen.length > 0) { + return ( + + ); + } + return null; + } +} diff --git a/source/renderer/app/domains/News.js b/source/renderer/app/domains/News.js new file mode 100644 index 0000000000..d2d08beb8f --- /dev/null +++ b/source/renderer/app/domains/News.js @@ -0,0 +1,150 @@ +// @flow +import { observable, computed, runInAction } from 'mobx'; +import { get, filter, orderBy, includes } from 'lodash'; +import semver from 'semver'; +import type { NewsTarget, NewsType } from '../api/news/types'; + +export type NewsAction = { + label: string, + url?: string, + route?: string, +}; + +export const NewsTypes: { + INCIDENT: NewsType, + ALERT: NewsType, + ANNOUNCEMENT: NewsType, + INFO: NewsType, +} = { + INCIDENT: 'incident', + ALERT: 'alert', + ANNOUNCEMENT: 'announcement', + INFO: 'info', +}; + +export type NewsTypesStateType = { + all: Array, + unread: Array, + read: Array, +}; + +const { version, platform } = global.environment; + +class News { + @observable title: string; + @observable content: string; + @observable target: NewsTarget; + @observable action: NewsAction; + @observable date: number; + @observable type: NewsType; + @observable read: boolean; + + constructor(data: { + title: string, + content: string, + target: NewsTarget, + action: NewsAction, + date: number, + type: NewsType, + read: boolean, + }) { + Object.assign(this, data); + } +} + +class NewsCollection { + @observable all: Array = []; + + constructor(data: Array) { + // Filter news by palatform and versions + const filteredNews = filter(data, newsItem => { + const availableTargetVersionRange = get( + newsItem, + ['target', 'daedalusVersion'], + null + ); + const targetPlatforms = get(newsItem, ['target', 'platforms']); + return ( + (!availableTargetVersionRange || + (availableTargetVersionRange && + semver.satisfies(version, availableTargetVersionRange))) && + (platform === 'browser' || includes(targetPlatforms, platform)) + ); + }); + const orderedNews = orderBy(filteredNews, 'date', 'desc'); + runInAction(() => { + this.all = orderedNews; + }); + } + + @computed get incident(): ?News { + const incidents = filter( + this.all, + item => item.type === NewsTypes.INCIDENT + ); + const lastIncidentIndex = + incidents.length > 0 ? incidents.length - 1 : null; + + if (lastIncidentIndex !== null) { + return incidents[lastIncidentIndex]; + } + return null; + } + + @computed get alerts(): NewsTypesStateType { + const alerts = filter(this.all, item => item.type === NewsTypes.ALERT); + // Order alerts from newest to oldest + const orderedAlerts = orderBy(alerts, 'date', 'asc'); + + const obj = new NewsCollection(orderedAlerts); + return { + all: obj.all, + unread: obj.unread, + read: obj.read, + }; + } + + @computed get announcements(): NewsTypesStateType { + const announcements = filter( + this.all, + item => item.type === NewsTypes.ANNOUNCEMENT && !item.read + ); + const obj = new NewsCollection(announcements); + return { + all: obj.all, + unread: obj.unread, + read: obj.read, + }; + } + + @computed get infos(): NewsTypesStateType { + const infos = filter( + this.all, + item => item.type === NewsTypes.INFO && !item.read + ); + + const obj = new NewsCollection(infos); + return { + all: obj.all, + unread: obj.unread, + read: obj.read, + }; + } + + @computed get unread(): Array { + const unread = filter(this.all, item => !item.read); + // Order unread from newest to oldest + return orderBy(unread, 'date', 'asc'); + } + + @computed get read(): Array { + const read = filter(this.all, item => item.read); + // Order read from newest to oldest + return orderBy(read, 'date', 'asc'); + } +} + +export default { + News, + NewsCollection, +}; diff --git a/source/renderer/app/i18n/locales/de-DE.json b/source/renderer/app/i18n/locales/de-DE.json index 70e90b10b6..2e8944b89d 100644 --- a/source/renderer/app/i18n/locales/de-DE.json +++ b/source/renderer/app/i18n/locales/de-DE.json @@ -161,6 +161,9 @@ "manualUpdate.description1": "!!!You are experiencing network connection issues, and you are not running the latest Daedalus version. Automatic updates are unavailable while Daedalus is not connected to Cardano network.", "manualUpdate.description2": "!!!You are currently running {currentAppVersion} version of Daedalus, and {availableAppVersion} version is available. Please manually update to that version since it may resolve your connecting issues.", "manualUpdate.title": "!!!Software update is available", + "news.newsfeed.empty": "!!!Newsfeed is empty", + "news.newsfeed.noFetch": "!!!Trying to fetch the newsfeed...", + "news.newsfeed.title": "!!!Newsfeed", "noDiskSpace.error.overlayContent": "!!!There is not enough disk space left on your device.
Daedalus requires at least {diskSpaceRequired} to operate. Please free up some disk space to continue.", "noDiskSpace.error.overlayTitle": "!!!Not enough disk space", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "!!!copied", diff --git a/source/renderer/app/i18n/locales/defaultMessages.json b/source/renderer/app/i18n/locales/defaultMessages.json index 1cab4bc44e..04a2b1d6cc 100644 --- a/source/renderer/app/i18n/locales/defaultMessages.json +++ b/source/renderer/app/i18n/locales/defaultMessages.json @@ -1122,6 +1122,53 @@ ], "path": "source/renderer/app/components/loading/system-time-error/SystemTimeError.json" }, + { + "descriptors": [ + { + "defaultMessage": "Newsfeed is empty", + "description": "Newsfeed is empty", + "end": { + "column": 3, + "line": 19 + }, + "file": "source/renderer/app/components/news/NewsFeed.js", + "id": "news.newsfeed.empty", + "start": { + "column": 17, + "line": 15 + } + }, + { + "defaultMessage": "Trying to fetch the newsfeed...", + "description": "Trying to fetch the newsfeed...", + "end": { + "column": 3, + "line": 24 + }, + "file": "source/renderer/app/components/news/NewsFeed.js", + "id": "news.newsfeed.noFetch", + "start": { + "column": 19, + "line": 20 + } + }, + { + "defaultMessage": "Newsfeed", + "description": "Newsfeed", + "end": { + "column": 3, + "line": 29 + }, + "file": "source/renderer/app/components/news/NewsFeed.js", + "id": "news.newsfeed.title", + "start": { + "column": 17, + "line": 25 + } + } + ], + "path": "source/renderer/app/components/news/NewsFeed.json" + }, { "descriptors": [ { diff --git a/source/renderer/app/i18n/locales/en-US.json b/source/renderer/app/i18n/locales/en-US.json index afa6a51096..e4d6cc6415 100644 --- a/source/renderer/app/i18n/locales/en-US.json +++ b/source/renderer/app/i18n/locales/en-US.json @@ -161,6 +161,9 @@ "manualUpdate.description1": "You are experiencing network connection issues, and you are not running the latest Daedalus version. Automatic updates are unavailable while Daedalus is not connected to Cardano network.", "manualUpdate.description2": "You are currently running {currentAppVersion} version of Daedalus, and {availableAppVersion} version is available. Please manually update to that version since it may resolve your connecting issues.", "manualUpdate.title": "Software update is available", + "news.newsfeed.empty": "Newsfeed is empty", + "news.newsfeed.noFetch": "Trying to fetch the newsfeed...", + "news.newsfeed.title": "Newsfeed", "noDiskSpace.error.overlayContent": "Daedalus requires at least {diskSpaceRequired} of hard drive space to operate. Your computer is missing {diskSpaceMissing} of available space. Please delete some files to increase available hard drive space to continue using Daedalus.

It is recommended to have at least 15% of hard drive space available ({diskSpaceRecommended} in your case) for normal and stable operation of the operating system and installed programs. We strongly recommend that you free up at least that amount of space from your hard drive.", "noDiskSpace.error.overlayTitle": "Daedalus requires more hard drive space", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "copied", diff --git a/source/renderer/app/i18n/locales/hr-HR.json b/source/renderer/app/i18n/locales/hr-HR.json index aa17fad81f..5add67dc11 100644 --- a/source/renderer/app/i18n/locales/hr-HR.json +++ b/source/renderer/app/i18n/locales/hr-HR.json @@ -161,6 +161,9 @@ "manualUpdate.description1": "!!!You are experiencing network connection issues, and you are not running the latest Daedalus version. Automatic updates are unavailable while Daedalus is not connected to Cardano network.", "manualUpdate.description2": "!!!You are currently running {currentAppVersion} version of Daedalus, and {availableAppVersion} version is available. Please manually update to that version since it may resolve your connecting issues.", "manualUpdate.title": "!!!Software update is available", + "news.newsfeed.empty": "!!!Newsfeed is empty", + "news.newsfeed.noFetch": "!!!Trying to fetch the newsfeed...", + "news.newsfeed.title": "!!!Newsfeed", "noDiskSpace.error.overlayContent": "!!!There is not enough disk space left on your device.
Daedalus requires at least {diskSpaceRequired} to operate. Please free up some disk space to continue.", "noDiskSpace.error.overlayTitle": "!!!Not enough disk space", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "!!!copied", diff --git a/source/renderer/app/i18n/locales/ja-JP.json b/source/renderer/app/i18n/locales/ja-JP.json index 0b92b7cb78..c90fd6334f 100644 --- a/source/renderer/app/i18n/locales/ja-JP.json +++ b/source/renderer/app/i18n/locales/ja-JP.json @@ -161,6 +161,9 @@ "manualUpdate.description1": "ネットワークへの接続に不具合があり、Daedalusウォレットの最新バージョンが実行されていません。DaedalusがCardanoネットワークに接続されていない間、自動更新は実行できません。", "manualUpdate.description2": "ご使用のDaedalusはバージョン{currentAppVersion}です。現在{availableAppVersion}をご利用いただけます。手動で最新版に更新すると、接続の不具合を解消できる場合があります。", "manualUpdate.title": "ソフトウェアが更新できます", + "news.newsfeed.empty": "ニュースフィードは空です", + "news.newsfeed.noFetch": "ニュースフィードを読み込んでいます…", + "news.newsfeed.title": "ニュースフィード", "noDiskSpace.error.overlayContent": "ダイダロスを動作させるには、ハードディスクに少なくとも{diskSpaceRequired}の空き容量が必要です。お使いのコンピュータは、空き容量が{diskSpaceMissing}不足しています。ダイダロスのご利用を続けるためには、ファイルをいくつか削除して空き容量を増やしてください。

オペレーティングシステムとインストール済みプログラムを正常かつ安定した状態で動作させるには、ハードディスクに少なくとも15%(お使いのコンピュータの場合は{diskSpaceRecommended})の空き容量が必要です。ハードディスクに少なくともこの程度の空き容量を確保することを強くお勧めします。", "noDiskSpace.error.overlayTitle": "ダイダロスを動作させるには、ハードディスクの空き容量が足りません", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "コピーされました", diff --git a/source/renderer/app/i18n/locales/ko-KR.json b/source/renderer/app/i18n/locales/ko-KR.json index ef48e0800d..2121e47441 100644 --- a/source/renderer/app/i18n/locales/ko-KR.json +++ b/source/renderer/app/i18n/locales/ko-KR.json @@ -161,6 +161,9 @@ "manualUpdate.description1": "!!!You are experiencing network connection issues, and you are not running the latest Daedalus version. Automatic updates are unavailable while Daedalus is not connected to Cardano network.", "manualUpdate.description2": "!!!You are currently running {currentAppVersion} version of Daedalus, and {availableAppVersion} version is available. Please manually update to that version since it may resolve your connecting issues.", "manualUpdate.title": "!!!Software update is available", + "news.newsfeed.empty": "!!!Newsfeed is empty", + "news.newsfeed.noFetch": "!!!Trying to fetch the newsfeed...", + "news.newsfeed.title": "!!!Newsfeed", "noDiskSpace.error.overlayContent": "!!!There is not enough disk space left on your device.
Daedalus requires at least {diskSpaceRequired} to operate. Please free up some disk space to continue.", "noDiskSpace.error.overlayTitle": "!!!Not enough disk space", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "!!!copied", diff --git a/source/renderer/app/i18n/locales/zh-CN.json b/source/renderer/app/i18n/locales/zh-CN.json index 3b1eb63a04..841dc90a8d 100644 --- a/source/renderer/app/i18n/locales/zh-CN.json +++ b/source/renderer/app/i18n/locales/zh-CN.json @@ -161,6 +161,9 @@ "manualUpdate.description1": "!!!You are experiencing network connection issues, and you are not running the latest Daedalus version. Automatic updates are unavailable while Daedalus is not connected to Cardano network.", "manualUpdate.description2": "!!!You are currently running {currentAppVersion} version of Daedalus, and {availableAppVersion} version is available. Please manually update to that version since it may resolve your connecting issues.", "manualUpdate.title": "!!!Software update is available", + "news.newsfeed.empty": "!!!Newsfeed is empty", + "news.newsfeed.noFetch": "!!!Trying to fetch the newsfeed...", + "news.newsfeed.title": "!!!Newsfeed", "noDiskSpace.error.overlayContent": "!!!There is not enough disk space left on your device.
Daedalus requires at least {diskSpaceRequired} to operate. Please free up some disk space to continue.", "noDiskSpace.error.overlayTitle": "!!!Not enough disk space", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "!!!copied", diff --git a/source/renderer/app/stores/AppStore.js b/source/renderer/app/stores/AppStore.js index 252fdd61c6..2b08abc365 100644 --- a/source/renderer/app/stores/AppStore.js +++ b/source/renderer/app/stores/AppStore.js @@ -22,6 +22,7 @@ export default class AppStore extends Store { @observable numberOfEpochsConsolidated: number = 0; @observable previousRoute: string = ROUTES.ROOT; @observable activeDialog: ApplicationDialog = null; + @observable newsFeedIsOpen: boolean = false; setup() { this.actions.router.goToRoute.listen(this._updateRouteLocation); @@ -55,6 +56,9 @@ export default class AppStore extends Store { this.actions.app.setNotificationVisibility.listen( this._setDownloadNotification ); + + this.actions.app.toggleNewsFeed.listen(this._toggleNewsFeed); + toggleUiPartChannel.onReceive(this.toggleUiPart); showUiPartChannel.onReceive(this.showUiPart); } @@ -76,6 +80,10 @@ export default class AppStore extends Store { return this.activeDialog === dialog; }; + @action _toggleNewsFeed = () => { + this.newsFeedIsOpen = !this.newsFeedIsOpen; + }; + /** * Toggles the dialog specified by the constant string identifier. */ diff --git a/source/renderer/app/stores/NewsFeedStore.js b/source/renderer/app/stores/NewsFeedStore.js new file mode 100644 index 0000000000..cac103f616 --- /dev/null +++ b/source/renderer/app/stores/NewsFeedStore.js @@ -0,0 +1,193 @@ +// @flow +import { observable, action, runInAction, computed } from 'mobx'; +import { map, get, find } from 'lodash'; +import Store from './lib/Store'; +import Request from './lib/LocalizedRequest'; +import { + NEWS_POLL_INTERVAL, + NEWS_POLL_INTERVAL_ON_ERROR, + NEWS_POLL_INTERVAL_ON_INCIDENT, +} from '../config/timingConfig'; +import News, { NewsTypes } from '../domains/News'; +import type { + GetNewsResponse, + GetReadNewsResponse, + NewsItem, + NewsTimestamp, + MarkNewsAsReadResponse, +} from '../api/news/types'; + +const { isTest } = global.environment; + +export default class NewsFeedStore extends Store { + @observable rawNews: ?Array = null; + @observable newsUpdatedAt: ?Date = null; + @observable fetchingNewsFailed = false; + @observable getNewsRequest: Request = new Request( + this.api.ada.getNews + ); + @observable getReadNewsRequest: Request = new Request( + this.api.localStorage.getReadNews + ); + @observable + markNewsAsReadRequest: Request = new Request( + this.api.localStorage.markNewsAsRead + ); + @observable openedAlert: ?News.News = null; + + pollingNewsIntervalId: ?IntervalID = null; + pollingNewsOnErrorIntervalId: ?IntervalID = null; + pollingNewsOnIncidentIntervalId: ?IntervalID = null; + + setup() { + // Fetch news on app start + this.getNews(); + if (!isTest) { + // Refetch news every 30 mins + this.pollingNewsIntervalId = setInterval( + this.getNews, + NEWS_POLL_INTERVAL + ); + } + } + + @action getNews = async () => { + let rawNews; + try { + rawNews = await this.getNewsRequest.execute().promise; + const hasIncident = find( + rawNews.items, + news => news.type === NewsTypes.INCIDENT + ); + + // Reset "getNews" fast polling interval if set and set regular polling interval + if (!isTest && this.pollingNewsOnErrorIntervalId) { + // Reset fast error interval + clearInterval(this.pollingNewsOnErrorIntervalId); + this.pollingNewsOnErrorIntervalId = null; + + if (!hasIncident) { + // set 30 min time interval if NO incidents + this.pollingNewsIntervalId = setInterval( + this.getNews, + NEWS_POLL_INTERVAL + ); + } + } + + // If incident occured, reset regular interval and set faster incident interval + if (hasIncident && !this.pollingNewsOnIncidentIntervalId) { + // Clear regular interval if set + if (this.pollingNewsIntervalId) { + clearInterval(this.pollingNewsIntervalId); + this.pollingNewsIntervalId = null; + } + + // Set 10 min time interval and + this.pollingNewsOnIncidentIntervalId = setInterval( + this.getNews, + NEWS_POLL_INTERVAL_ON_INCIDENT + ); + } + + // If no incidents and incident poller interval active, reset interval and set regular one + if (!hasIncident && this.pollingNewsOnIncidentIntervalId) { + // Clear regulat interval + if (this.pollingNewsOnIncidentIntervalId) { + clearInterval(this.pollingNewsOnIncidentIntervalId); + this.pollingNewsOnIncidentIntervalId = null; + } + + // Set 30 min time interval + this.pollingNewsIntervalId = setInterval( + this.getNews, + NEWS_POLL_INTERVAL + ); + } + + this._setFetchingNewsFailed(false); + } catch (error) { + // Decrease "getNews" fetching timer in case we got an error and there are no initial news set in store + if (!isTest && !this.rawNews) { + // Reset all regular intervals + if (this.pollingNewsIntervalId) { + clearInterval(this.pollingNewsIntervalId); + this.pollingNewsIntervalId = null; + } + if (this.pollingNewsOnIncidentIntervalId) { + clearInterval(this.pollingNewsOnIncidentIntervalId); + this.pollingNewsOnIncidentIntervalId = null; + } + + // Set fast ERROR interval + if (!this.pollingNewsOnErrorIntervalId) { + this.pollingNewsOnErrorIntervalId = setInterval( + this.getNews, + NEWS_POLL_INTERVAL_ON_ERROR + ); + } + } + this._setFetchingNewsFailed(true); + } + + await this.getReadNewsRequest.execute(); + + if (rawNews) { + runInAction('set news data', () => { + this.rawNews = get(rawNews, 'items', []); + this.newsUpdatedAt = get(rawNews, 'updatedAt', null); + }); + } + }; + + @action markNewsAsRead = async newsTimestamps => { + // Set news timestamp to LC + await this.markNewsAsReadRequest.execute(newsTimestamps); + // Get all read news to force @computed change + await this.getReadNewsRequest.execute(); + }; + + @action openAlert = (newsTimestamp: NewsTimestamp) => { + if (this.getNewsRequest.wasExecuted) { + const alertToOpen = this.newsFeedData.alerts.all.find( + newsItem => newsItem.date === newsTimestamp + ); + if (alertToOpen) { + this.openedAlert = alertToOpen; + } + } + }; + + @action closeOpenedAlert = () => { + this.openedAlert = null; + }; + + @action _setFetchingNewsFailed = (fetchingNewsFailed: boolean) => { + this.fetchingNewsFailed = fetchingNewsFailed; + }; + + @computed get newsFeedData(): News.NewsCollection { + const { currentLocale } = this.stores.profile; + const readNews = this.getReadNewsRequest.result; + let news = []; + if (this.getNewsRequest.wasExecuted) { + news = map(this.rawNews, item => ({ + ...item, + title: item.title[currentLocale], + content: item.content[currentLocale], + action: { + ...item.action, + label: item.action.label[currentLocale], + url: get(item, ['action', 'url', currentLocale]), + }, + read: readNews.includes(item.date), + })); + } + + return new News.NewsCollection(news); + } + + @computed get isLoadingNews() { + return this.fetchingNewsFailed || !this.rawNews; + } +} diff --git a/source/renderer/app/stores/index.js b/source/renderer/app/stores/index.js index ab2791e54c..366a26b82c 100644 --- a/source/renderer/app/stores/index.js +++ b/source/renderer/app/stores/index.js @@ -5,6 +5,7 @@ import AddressesStore from './AddressesStore'; import AppStore from './AppStore'; import BlockConsolidationStore from './BlockConsolidationStore'; import NetworkStatusStore from './NetworkStatusStore'; +import NewsFeedStore from './NewsFeedStore'; import NodeUpdateStore from './NodeUpdateStore'; import ProfileStore from './ProfileStore'; import SidebarStore from './SidebarStore'; @@ -22,6 +23,7 @@ export const storeClasses = { app: AppStore, blockConsolidation: BlockConsolidationStore, networkStatus: NetworkStatusStore, + newsFeed: NewsFeedStore, nodeUpdate: NodeUpdateStore, profile: ProfileStore, sidebar: SidebarStore, @@ -40,6 +42,7 @@ export type StoresMap = { app: AppStore, blockConsolidation: BlockConsolidationStore, networkStatus: NetworkStatusStore, + newsFeed: NewsFeedStore, nodeUpdate: NodeUpdateStore, profile: ProfileStore, router: Object, @@ -80,6 +83,7 @@ export default action( app: createStoreInstanceOf(AppStore), blockConsolidation: createStoreInstanceOf(BlockConsolidationStore), networkStatus: createStoreInstanceOf(NetworkStatusStore), + newsFeed: createStoreInstanceOf(NewsFeedStore), nodeUpdate: createStoreInstanceOf(NodeUpdateStore), profile: createStoreInstanceOf(ProfileStore), router, diff --git a/source/renderer/app/themes/daedalus/cardano.js b/source/renderer/app/themes/daedalus/cardano.js index 001d2a8615..f61d6ac33c 100644 --- a/source/renderer/app/themes/daedalus/cardano.js +++ b/source/renderer/app/themes/daedalus/cardano.js @@ -296,8 +296,9 @@ export const CARDANO_THEME_OUTPUT = { '--theme-loading-status-icons-off-color': '#ea4c5b', '--theme-loading-status-icons-unloaded-loading-color': '#fafbfc', '--theme-loading-status-icons-unloaded-syncing-color': '#5e6066', - '--theme-loading-status-icons-tooltip-color': '#5E6066', + '--theme-loading-status-icons-tooltip-color': '#5e6066', '--theme-loading-spinner-color': '#5e6066', + '--theme-loading-spinner-medium-color': '#fff', }, manualUpdate: { '--theme-manual-update-overlay-background-color': 'rgba(32, 34, 37, 0.96)', @@ -355,6 +356,37 @@ export const CARDANO_THEME_OUTPUT = { '--theme-network-window-button-background-color-active': 'rgba(250, 251, 252, 0.8)', }, + newsFeed: { + '--theme-news-feed-background-color': '#34383d', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#ffffff', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.25)', + '--theme-news-feed-header-background-color': '#202225', + '--theme-news-feed-header-title-color': '#fafbfc', + '--theme-news-feed-icon-close-button-color': '#fff', + '--theme-news-feed-icon-close-hover-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-icon-color': '#ffffff', + '--theme-news-feed-icon-color-connecting-screen': '#ffffff', + '--theme-news-feed-icon-color-syncing-screen': '#5e6066', + '--theme-news-feed-icon-dot-background-color': '#ea4c5b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-no-fetch-color': '#fafbfc', + }, + newsItem: { + '--theme-news-item-action-button-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#ffffff', + '--theme-news-item-action-button-border-color': '#ffffff', + '--theme-news-item-action-button-color': '#ffffff', + '--theme-news-item-action-button-color-hover': '#202225', + '--theme-news-item-alert-background-color': 'rgba(242, 162, 24, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(31, 193, 195, 0.2)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#ffffff', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-title-color': '#ffffff', + }, nodeUpdate: { '--theme-node-update-background-color': '#efefef', '--theme-node-update-title-color': '#5e6066', @@ -564,8 +596,8 @@ export const CARDANO_THEME_OUTPUT = { '--rp-tooltip-text-color': '#fafbfc', }, scrollbar: { - '--theme-scrollbar-thumb-background': 'rgba(200, 204, 206, 0.3)', - '--theme-scrollbar-thumb-background-hover': 'rgba(200, 204, 206, 0.5)', + '--theme-scrollbar-thumb-background': 'rgba(94, 96, 102, 0.3)', + '--theme-scrollbar-thumb-background-hover': 'rgba(94, 96, 102, 0.5)', }, sendConfirmation: { '--theme-send-confirmation-dialog-send-values-color': '#ea4c5b', @@ -707,7 +739,7 @@ export const CARDANO_THEME_OUTPUT = { }, topBar: { '--theme-topbar-background-color': '#202225', - '--theme-topbar-layout-body-background-color': '#ebeff2', + '--theme-topbar-layout-body-background-color': '#efefef', '--theme-topbar-wallet-name-color': '#fafbfc', '--theme-topbar-wallet-info-color': '#fafbfc', '--theme-topbar-logo-color': 'rgb(250, 251, 252)', diff --git a/source/renderer/app/themes/daedalus/dark-blue.js b/source/renderer/app/themes/daedalus/dark-blue.js index 8a0589862a..3b80c0fa62 100644 --- a/source/renderer/app/themes/daedalus/dark-blue.js +++ b/source/renderer/app/themes/daedalus/dark-blue.js @@ -300,6 +300,7 @@ export const DARK_BLUE_THEME_OUTPUT = { '--theme-loading-status-icons-unloaded-syncing-color': '#fafbfc', '--theme-loading-status-icons-tooltip-color': '#4b5a68', '--theme-loading-spinner-color': '#e9f4fe', + '--theme-loading-spinner-medium-color': '#e9f4fe', }, manualUpdate: { '--theme-manual-update-overlay-background-color': 'rgba(38, 51, 69, 0.96)', @@ -357,6 +358,37 @@ export const DARK_BLUE_THEME_OUTPUT = { '--theme-network-window-button-background-color-active': 'rgba(250, 251, 252, 0.8)', }, + newsFeed: { + '--theme-news-feed-background-color': '#263345', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#e9f4fe', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.25)', + '--theme-news-feed-header-background-color': '#1b2430', + '--theme-news-feed-header-title-color': '#fafbfc', + '--theme-news-feed-icon-close-button-color': '#fff', + '--theme-news-feed-icon-close-hover-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-icon-color': '#e9f4fe', + '--theme-news-feed-icon-color-connecting-screen': '#e9f4fe', + '--theme-news-feed-icon-color-syncing-screen': '#e9f4fe', + '--theme-news-feed-icon-dot-background-color': '#ea4c5b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-no-fetch-color': '#fafbfc', + }, + newsItem: { + '--theme-news-item-action-button-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#fafbfc', + '--theme-news-item-action-button-border-color': '#fafbfc', + '--theme-news-item-action-button-color': '#fafbfc', + '--theme-news-item-action-button-color-hover': '#263345', + '--theme-news-item-alert-background-color': 'rgba(242, 162, 24, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(31, 193, 195, 0.2)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#fafbfc', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-title-color': '#fafbfc', + }, nodeUpdate: { '--theme-node-update-background-color': '#536370', '--theme-node-update-title-color': '#e9f4fe', diff --git a/source/renderer/app/themes/daedalus/dark-cardano.js b/source/renderer/app/themes/daedalus/dark-cardano.js index 18a83aa579..f523359733 100644 --- a/source/renderer/app/themes/daedalus/dark-cardano.js +++ b/source/renderer/app/themes/daedalus/dark-cardano.js @@ -289,6 +289,7 @@ export const DARK_CARDANO_THEME_OUTPUT = { '--theme-loading-status-icons-unloaded-syncing-color': '#ffffff', '--theme-loading-status-icons-tooltip-color': '#56576b', '--theme-loading-spinner-color': '#ffffff', + '--theme-loading-spinner-medium-color': '#ffffff', }, manualUpdate: { '--theme-manual-update-overlay-background-color': '#36374df5', @@ -340,6 +341,37 @@ export const DARK_CARDANO_THEME_OUTPUT = { '--theme-network-window-button-background-color-hover': '#afafb899', '--theme-network-window-button-background-color-active': '#afafb8cc', }, + newsFeed: { + '--theme-news-feed-background-color': '#2a2b3c', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#ffffff', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.25)', + '--theme-news-feed-header-background-color': '#20212e', + '--theme-news-feed-header-title-color': '#fafbfc', + '--theme-news-feed-icon-close-button-color': '#ffffff', + '--theme-news-feed-icon-close-hover-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-icon-color': '#ffffff', + '--theme-news-feed-icon-color-connecting-screen': '#ffffff', + '--theme-news-feed-icon-color-syncing-screen': '#ffffff', + '--theme-news-feed-icon-dot-background-color': '#ea4c5b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-no-fetch-color': '#fafbfc', + }, + newsItem: { + '--theme-news-item-action-button-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#ffffff', + '--theme-news-item-action-button-border-color': '#ffffff', + '--theme-news-item-action-button-color': '#ffffff', + '--theme-news-item-action-button-color-hover': '#2a2b3c', + '--theme-news-item-alert-background-color': 'rgba(242, 162, 24, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(31, 193, 195, 0.2)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#ffffff', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-title-color': '#ffffff', + }, nodeUpdate: { '--theme-node-update-background-color': '#121326', '--theme-node-update-title-color': '#ffffff', @@ -695,7 +727,7 @@ export const DARK_CARDANO_THEME_OUTPUT = { }, topBar: { '--theme-topbar-background-color': '#2a2b3c', - '--theme-topbar-layout-body-background-color': '#36374d', + '--theme-topbar-layout-body-background-color': '#121326', '--theme-topbar-wallet-name-color': '#ffffff', '--theme-topbar-wallet-info-color': '#ffffff', '--theme-topbar-logo-color': '#ffffff', diff --git a/source/renderer/app/themes/daedalus/light-blue.js b/source/renderer/app/themes/daedalus/light-blue.js index 9bfd6edebc..8f0e0be4be 100644 --- a/source/renderer/app/themes/daedalus/light-blue.js +++ b/source/renderer/app/themes/daedalus/light-blue.js @@ -296,6 +296,7 @@ export const LIGHT_BLUE_THEME_OUTPUT = { '--theme-loading-status-icons-unloaded-syncing-color': '#5e6066', '--theme-loading-status-icons-tooltip-color': '#062148', '--theme-loading-spinner-color': '#5e6066', + '--theme-loading-spinner-medium-color': '#fafbfc', }, manualUpdate: { '--theme-manual-update-overlay-background-color': 'rgba(36, 62, 98, 0.96)', @@ -352,6 +353,37 @@ export const LIGHT_BLUE_THEME_OUTPUT = { '--theme-network-window-button-background-color-active': 'rgba(250, 251, 252, 0.8)', }, + newsFeed: { + '--theme-news-feed-background-color': '#233856', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#fafbfc', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.25)', + '--theme-news-feed-header-background-color': '#1e304a', + '--theme-news-feed-header-title-color': '#fafbfc', + '--theme-news-feed-icon-close-button-color': '#fff', + '--theme-news-feed-icon-close-hover-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-icon-color': '#fafbfc', + '--theme-news-feed-icon-color-connecting-screen': '#fafbfc', + '--theme-news-feed-icon-color-syncing-screen': '#5e6066', + '--theme-news-feed-icon-dot-background-color': '#ea4c5b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-no-fetch-color': '#fafbfc', + }, + newsItem: { + '--theme-news-item-action-button-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#fafbfc', + '--theme-news-item-action-button-border-color': '#fafbfc', + '--theme-news-item-action-button-color': '#fafbfc', + '--theme-news-item-action-button-color-hover': '#243e62', + '--theme-news-item-alert-background-color': 'rgba(242, 162, 24, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(31, 193, 195, 0.2)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#fafbfc', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-title-color': '#fafbfc', + }, nodeUpdate: { '--theme-node-update-background-color': '#ebeff2', '--theme-node-update-title-color': '#5e6066', @@ -561,8 +593,8 @@ export const LIGHT_BLUE_THEME_OUTPUT = { '--rp-tooltip-text-color': '#fafbfc', }, scrollbar: { - '--theme-scrollbar-thumb-background': 'rgba(200, 204, 206, 0.3)', - '--theme-scrollbar-thumb-background-hover': 'rgba(200, 204, 206, 0.5)', + '--theme-scrollbar-thumb-background': 'rgba(94, 96, 102, 0.3)', + '--theme-scrollbar-thumb-background-hover': 'rgba(94, 96, 102, 0.5)', }, sendConfirmation: { '--theme-send-confirmation-dialog-send-values-color': '#ea4c5b', diff --git a/source/renderer/app/themes/daedalus/white.js b/source/renderer/app/themes/daedalus/white.js index 36222c940d..c7ab57083d 100644 --- a/source/renderer/app/themes/daedalus/white.js +++ b/source/renderer/app/themes/daedalus/white.js @@ -283,6 +283,7 @@ export const WHITE_THEME_OUTPUT = { '--theme-loading-status-icons-unloaded-syncing-color': '#2d2d2d', '--theme-loading-status-icons-tooltip-color': '#2d2d2d', '--theme-loading-spinner-color': '#2d2d2d', + '--theme-loading-spinner-medium-color': '#2d2d2d', }, manualUpdate: { '--theme-manual-update-overlay-background-color': '#fffffff5', @@ -337,6 +338,39 @@ export const WHITE_THEME_OUTPUT = { '--theme-network-window-button-background-color-active': 'rgba(94, 96, 102, 0.8)', }, + newsFeed: { + '--theme-news-feed-background-color': '#f4f4f4', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#ffffff', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.08)', + '--theme-news-feed-header-background-color': '#e9e9e9', + '--theme-news-feed-header-title-color': '#2d2d2d', + '--theme-news-feed-icon-close-button-color': '#2d2d2d', + '--theme-news-feed-icon-close-hover-background-color': + 'rgba(41, 181, 149, 0.1)', + '--theme-news-feed-icon-color': '#2d2d2d', + '--theme-news-feed-icon-color-connecting-screen': '#2d2d2d', + '--theme-news-feed-icon-color-syncing-screen': '#2d2d2d', + '--theme-news-feed-icon-dot-background-color': '#ea4c5b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(41, 181, 149, 0.1)', + '--theme-news-feed-no-fetch-color': '#2d2d2d', + }, + newsItem: { + '--theme-news-item-action-button-background-color': + 'rgba(41, 181, 149, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#29b595', + '--theme-news-item-action-button-border-color': '#29b595', + '--theme-news-item-action-button-color': '#29b595', + '--theme-news-item-action-button-color-hover': '#ffffff', + '--theme-news-item-alert-background-color': 'rgba(242, 162, 24, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(31, 193, 195, 0.2)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#2d2d2d', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.05)', + '--theme-news-item-title-color': '#2d2d2d', + }, nodeUpdate: { '--theme-node-update-background-color': '#f9f9f9', '--theme-node-update-title-color': '#2d2d2d', @@ -688,7 +722,7 @@ export const WHITE_THEME_OUTPUT = { }, topBar: { '--theme-topbar-background-color': '#ffffff', - '--theme-topbar-layout-body-background-color': '#ffffff', + '--theme-topbar-layout-body-background-color': '#f9f9f9', '--theme-topbar-wallet-name-color': '#2d2d2d', '--theme-topbar-wallet-info-color': '#2d2d2d', '--theme-topbar-logo-color': '#2d2d2d', diff --git a/source/renderer/app/themes/daedalus/yellow.js b/source/renderer/app/themes/daedalus/yellow.js index 99f275c9a9..594846fc36 100644 --- a/source/renderer/app/themes/daedalus/yellow.js +++ b/source/renderer/app/themes/daedalus/yellow.js @@ -287,6 +287,7 @@ export const YELLOW_THEME_OUTPUT = { '--theme-loading-status-icons-unloaded-syncing-color': '#2d2d2d', '--theme-loading-status-icons-tooltip-color': '#2d2d2d', '--theme-loading-spinner-color': '#2d2d2d', + '--theme-loading-spinner-medium-color': '#2d2d2d', }, manualUpdate: { '--theme-manual-update-overlay-background-color': @@ -343,6 +344,37 @@ export const YELLOW_THEME_OUTPUT = { '--theme-network-window-button-background-color-active': 'rgba(45, 45, 45, 0.8)', }, + newsFeed: { + '--theme-news-feed-background-color': '#ffb923', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#ffffff', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.25)', + '--theme-news-feed-header-background-color': '#eda70e', + '--theme-news-feed-header-title-color': '#2d2d2d', + '--theme-news-feed-icon-close-button-color': '#2d2d2d', + '--theme-news-feed-icon-close-hover-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-icon-color': '#2d2d2d', + '--theme-news-feed-icon-color-connecting-screen': '#2d2d2d', + '--theme-news-feed-icon-color-syncing-screen': '#2d2d2d', + '--theme-news-feed-icon-dot-background-color': '#be0b0b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-no-fetch-color': '#fafbfc', + }, + newsItem: { + '--theme-news-item-action-button-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#2d2d2d', + '--theme-news-item-action-button-border-color': '#2d2d2d', + '--theme-news-item-action-button-color': '#2d2d2d', + '--theme-news-item-action-button-color-hover': '#ffffff', + '--theme-news-item-alert-background-color': 'rgba(249, 128, 10, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(31, 193, 195, 0.3)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#2d2d2d', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-title-color': '#2d2d2d', + }, nodeUpdate: { '--theme-node-update-background-color': '#f8f3ed', '--theme-node-update-title-color': '#2d2d2d', @@ -696,7 +728,7 @@ export const YELLOW_THEME_OUTPUT = { }, topBar: { '--theme-topbar-background-color': '#ffb923', - '--theme-topbar-layout-body-background-color': '#fdcd68', + '--theme-topbar-layout-body-background-color': '#f8f3ed', '--theme-topbar-wallet-name-color': '#2d2d2d', '--theme-topbar-wallet-info-color': '#2d2d2d', '--theme-topbar-logo-color': '#2d2d2d', diff --git a/source/renderer/app/themes/utils/createTheme.js b/source/renderer/app/themes/utils/createTheme.js index 56c23d4883..95a95daea9 100644 --- a/source/renderer/app/themes/utils/createTheme.js +++ b/source/renderer/app/themes/utils/createTheme.js @@ -640,6 +640,7 @@ export const createDaedalusComponentsTheme = ( '--theme-loading-status-icons-unloaded-syncing-color': `${text.primary}`, '--theme-loading-status-icons-tooltip-color': `${text.primary}`, '--theme-loading-spinner-color': `${text.primary}`, + '--theme-loading-spinner-medium-color': '#fff', }, manualUpdate: { '--theme-manual-update-overlay-background-color': `${chroma( @@ -724,6 +725,38 @@ export const createDaedalusComponentsTheme = ( background.secondary.lightest ).alpha(0.8)}`, }, + newsFeed: { + '--theme-news-feed-background-color': '#233856', + '--theme-news-feed-badge-background-color': '#ea4c5b', + '--theme-news-feed-badge-text-color': '#ffffff', + '--theme-news-feed-box-shadow-color': '-5px 0 20px 0 rgba(0, 0, 0, 0.25)', + '--theme-news-feed-header-background-color': '#1e304a', + '--theme-news-feed-header-title-color': '#fafbfc', + '--theme-news-feed-icon-close-button-color': '#fff', + '--theme-news-feed-icon-close-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-icon-color': '#fafbfc', + '--theme-news-feed-icon-color-connecting-screen': '#fafbfc', + '--theme-news-feed-icon-color-syncing-screen': '#5e6066', + '--theme-news-feed-icon-dot-background-color': '#ea4c5b', + '--theme-news-feed-icon-toggle-hover-background-color': + 'rgba(0, 0, 0, 0.1)', + '--theme-news-feed-no-fetch-color': '#fafbfc', + }, + newsItem: { + '--theme-news-item-action-button-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-action-button-background-color-hover': '#29b595', + '--theme-news-item-action-button-border-color': '#fafbfc', + '--theme-news-item-action-button-color': '#fafbfc', + '--theme-news-item-action-button-color-hover': '#ffffff', + '--theme-news-item-alert-background-color': 'rgba(242, 162, 24, 0.5)', + '--theme-news-item-announcement-background-color': + 'rgba(234, 76, 91, 0.25)', + '--theme-news-item-badge-color': '#ea4c5b', + '--theme-news-item-content-link-color': '#fafbfc', + '--theme-news-item-info-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-news-item-title-color': '#fafbfc', + }, nodeUpdate: { '--theme-node-update-background-color': `${background.primary.regular}`, '--theme-node-update-title-color': `${text.primary}`, diff --git a/source/renderer/app/utils/network.js b/source/renderer/app/utils/network.js index 5d71f86c38..da19588e28 100644 --- a/source/renderer/app/utils/network.js +++ b/source/renderer/app/utils/network.js @@ -9,6 +9,14 @@ import { MAINNET_LATEST_VERSION_INFO_URL, STAGING_LATEST_VERSION_INFO_URL, TESTNET_LATEST_VERSION_INFO_URL, + MAINNET_NEWS_URL, + TESTNET_NEWS_URL, + STAGING_NEWS_URL, + DEVELOPMENT_NEWS_URL, + MAINNET_NEWS_HASH_URL, + STAGING_NEWS_HASH_URL, + TESTNET_NEWS_HASH_URL, + DEVELOPMENT_NEWS_HASH_URL, } from '../config/urlsConfig'; import { MAINNET, @@ -72,3 +80,39 @@ export const getLatestVersionInfoUrl = (network: string): string => { } return latestVersionInfoUrl; }; + +export const getNewsURL = (network: string): string => { + // sets default to mainnet in case env.NETWORK is undefined + let newsUrl = MAINNET_NEWS_URL; + if (network === MAINNET) { + newsUrl = MAINNET_NEWS_URL; + } + if (network === STAGING) { + newsUrl = STAGING_NEWS_URL; + } + if (network === TESTNET) { + newsUrl = TESTNET_NEWS_URL; + } + if (network === DEVELOPMENT) { + newsUrl = DEVELOPMENT_NEWS_URL; + } + return newsUrl; +}; + +export const getNewsHashURL = (network: string): string => { + // sets default to mainnet in case env.NETWORK is undefined + let newsUrl = MAINNET_NEWS_HASH_URL; + if (network === MAINNET) { + newsUrl = MAINNET_NEWS_HASH_URL; + } + if (network === STAGING) { + newsUrl = STAGING_NEWS_HASH_URL; + } + if (network === TESTNET) { + newsUrl = TESTNET_NEWS_HASH_URL; + } + if (network === DEVELOPMENT) { + newsUrl = DEVELOPMENT_NEWS_HASH_URL; + } + return newsUrl; +}; diff --git a/storybook/stories/Loading-SyncingConnecting.stories.js b/storybook/stories/Loading-SyncingConnecting.stories.js index 70551d4d71..be6ae66f2b 100644 --- a/storybook/stories/Loading-SyncingConnecting.stories.js +++ b/storybook/stories/Loading-SyncingConnecting.stories.js @@ -9,6 +9,10 @@ import { CardanoNodeStates } from '../../source/common/types/cardano-node.types' export const DefaultSyncingConnectingStory = () => ( ( onGetAvailableVersions={action('onGetAvailableVersions')} onStatusIconClick={linkTo('Diagnostics', () => 'default')} disableDownloadLogs={boolean('disableDownloadLogs', true)} + showNewsFeedIcon /> ); export const ConnectivityIssuesSyncingConnectingStory = () => ( ( onGetAvailableVersions={action('onGetAvailableVersions')} onStatusIconClick={linkTo('Diagnostics', () => 'default')} disableDownloadLogs={boolean('disableDownloadLogs', false)} + showNewsFeedIcon /> ); export const SyncIssuesSyncingConnectingStory = () => ( ( onGetAvailableVersions={action('onGetAvailableVersions')} onStatusIconClick={linkTo('Diagnostics', () => 'default')} disableDownloadLogs={boolean('disableDownloadLogs', false)} + showNewsFeedIcon /> ); diff --git a/storybook/stories/TopBar.stories.js b/storybook/stories/TopBar.stories.js index c300457cc2..d7d571460b 100644 --- a/storybook/stories/TopBar.stories.js +++ b/storybook/stories/TopBar.stories.js @@ -1,6 +1,7 @@ // @flow import React from 'react'; import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; import StoryDecorator from './support/StoryDecorator'; import SidebarLayout from '../../source/renderer/app/components/layout/SidebarLayout'; import TopBar from '../../source/renderer/app/components/layout/TopBar'; @@ -8,6 +9,7 @@ import NodeSyncStatusIcon from '../../source/renderer/app/components/widgets/Nod import WalletTestEnvironmentLabel from '../../source/renderer/app/components/widgets/WalletTestEnvironmentLabel'; import { formattedWalletAmount } from '../../source/renderer/app/utils/formatters'; import menuIconClosed from '../../source/renderer/app/assets/images/menu-ic.inline.svg'; +import NewsFeedIcon from '../../source/renderer/app/components/widgets/NewsFeedIcon'; const topBarTestEnv = ( + ); @@ -43,6 +49,10 @@ const topBarProductionEnv = ( isProduction isMainnet /> + ); diff --git a/storybook/stories/index.js b/storybook/stories/index.js index a17b21a1b1..9282694543 100644 --- a/storybook/stories/index.js +++ b/storybook/stories/index.js @@ -35,3 +35,8 @@ import './Widgets.stories'; // Notifications import './Notifications.stories'; + +// Newsfeed +import './news/NewsFeed.stories'; +import './news/IncidentOverlay.stories'; +import './news/AlertsOverlay.stories'; diff --git a/storybook/stories/news/AlertsOverlay.stories.js b/storybook/stories/news/AlertsOverlay.stories.js new file mode 100644 index 0000000000..38d263c6ef --- /dev/null +++ b/storybook/stories/news/AlertsOverlay.stories.js @@ -0,0 +1,59 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import StoryDecorator from '../support/StoryDecorator'; +import AlertsOverlay from '../../../source/renderer/app/components/news/AlertsOverlay'; + +storiesOf('NewsFeed', module) + .addDecorator(story => ( + + {story([ + { + alerts: [ + { + action: { + label: 'Read More', + url: 'https://www.daedalus.io', + }, + content: + '# h1 Heading\nUt consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu.\n\n## h2 Heading\n\nConsequat mauris nunc congue nisi vitae suscipit. Dictum non consectetur a erat nam. Laoreet non curabitur gravida arcu ac tortor dignissim. Eu augue ut lectus arcu bibendum at. Facilisis gravida neque convallis a cras semper. Ut consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu. Nam aliquam sem et tortor consequat id porta nibh venenatis.\n\nViverra nam libero justo laoreet sit amet. Pharetra diam sit amet nisl. Quam viverra orci sagittis eu. Rhoncus dolor purus non enim. Posuere urna nec tincidunt praesent semper feugiat. Suspendisse in est ante in nibh mauris cursus. Sit amet consectetur adipiscing elit duis. Tortor id aliquet lectus proin nibh nisl condimentum id. At in tellus integer feugiat scelerisque. Maecenas sed enim ut sem viverra aliquet. Pellentesque pulvinar pellentesque habitant morbi. Ultrices neque ornare aenean euismod elementum nisi quis eleifend. Praesent tristique magna sit amet purus gravida. Diam volutpat commodo sed egestas egestas. Ut placerat orci nulla pellentesque dignissim enim. Ultrices in iaculis nunc sed augue lacus viverra. Etiam sit amet nisl purus.\n\n## Typographic replacements\n\nEnable typographer option to see result.\n\n(c) (C) (r) (R) (tm) (TM) (p) (P) +-\n\ntest.. test... test..... test?..... test!....\n\n!!!!!! ???? ,, -- ---\n\n"Smartypants, double quotes" and \'single quotes\'\n\n## Emphasis\n\n**This is bold text**\n\n__This is bold text__\n\n*This is italic text*\n\n_This is italic text_\n\n## Lists\n\nUnordered\n\n+ Create a list by starting a line with +, -, or *\n+ Sub-lists are made by indenting 2 spaces:\n+ Very easy!\n\nOrdered\n\n1. Lorem ipsum dolor sit amet\n2. Consectetur adipiscing elit\n3. Integer molestie lorem at massa\n\n\n1. You can use sequential numbers...\n1. ...or keep all the numbers as `1.`\n\n## Links\n\n[link text](http://dev.nodeca.com)\n\n[link with title](http://nodeca.github.io/pica/demo/ "title text!")\n\nAutoconverted link https://github.com/nodeca/pica (enable linkify to see)\n\n### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)\n\n- 19^th^\n- H~2~O\n', + date: Date.now(), + id: '123', + target: { + daedalus: 'v0.13', + platform: 'macOS', + platformVersion: '10.14.6', + }, + title: 'Failure Alert', + }, + { + action: { + label: 'Read More', + url: 'https://www.daedalus.io', + }, + content: + '# h1 Heading\nUt consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu.\n\n## h2 Heading\n\nConsequat mauris nunc congue nisi vitae suscipit. Dictum non consectetur a erat nam. Laoreet non curabitur gravida arcu ac tortor dignissim. Eu augue ut lectus arcu bibendum at. Facilisis gravida neque convallis a cras semper. Ut consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu. Nam aliquam sem et tortor consequat id porta nibh venenatis.\n\nViverra nam libero justo laoreet sit amet. Pharetra diam sit amet nisl. Quam viverra orci sagittis eu. Rhoncus dolor purus non enim. Posuere urna nec tincidunt praesent semper feugiat. Suspendisse in est ante in nibh mauris cursus. Sit amet consectetur adipiscing elit duis. Tortor id aliquet lectus proin nibh nisl condimentum id. At in tellus integer feugiat scelerisque. Maecenas sed enim ut sem viverra aliquet. Pellentesque pulvinar pellentesque habitant morbi. Ultrices neque ornare aenean euismod elementum nisi quis eleifend. Praesent tristique magna sit amet purus gravida. Diam volutpat commodo sed egestas egestas. Ut placerat orci nulla pellentesque dignissim enim. Ultrices in iaculis nunc sed augue lacus viverra. Etiam sit amet nisl purus.\n\n## Typographic replacements\n\nEnable typographer option to see result.\n\n(c) (C) (r) (R) (tm) (TM) (p) (P) +-\n\ntest.. test... test..... test?..... test!....\n\n!!!!!! ???? ,, -- ---\n\n"Smartypants, double quotes" and \'single quotes\'\n\n## Emphasis\n\n**This is bold text**\n\n__This is bold text__\n\n*This is italic text*\n\n_This is italic text_\n\n## Lists\n\nUnordered\n\n+ Create a list by starting a line with +, -, or *\n+ Sub-lists are made by indenting 2 spaces:\n+ Very easy!\n\nOrdered\n\n1. Lorem ipsum dolor sit amet\n2. Consectetur adipiscing elit\n3. Integer molestie lorem at massa\n\n\n1. You can use sequential numbers...\n1. ...or keep all the numbers as `1.`\n\n## Links\n\n[link text](http://dev.nodeca.com)\n\n[link with title](http://nodeca.github.io/pica/demo/ "title text!")\n\nAutoconverted link https://github.com/nodeca/pica (enable linkify to see)\n\n### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)\n\n- 19^th^\n- H~2~O\n', + date: Date.now(), + id: '1234', + target: { + daedalus: 'v0.13', + platform: 'macOS', + platformVersion: '10.14.6', + }, + title: 'Node Bug Alert', + }, + ], + }, + ])} + + )) + .add('Alerts Overlay', props => ( + {}} + onMarkNewsAsRead={action('onMarkNewsAsRead')} + onOpenExternalLink={() => {}} + /> + )); diff --git a/storybook/stories/news/IncidentOverlay.stories.js b/storybook/stories/news/IncidentOverlay.stories.js new file mode 100644 index 0000000000..b3b9a3d864 --- /dev/null +++ b/storybook/stories/news/IncidentOverlay.stories.js @@ -0,0 +1,29 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import StoryDecorator from '../support/StoryDecorator'; +import IncidentOverlay from '../../../source/renderer/app/components/news/IncidentOverlay'; + +storiesOf('NewsFeed', module) + .addDecorator(story => ( + + {story({ + action: { + label: 'Read More', + url: 'https://www.daedalus.io', + }, + content: + '# h1 Heading\nUt consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu.\n\n## h2 Heading\n\nConsequat mauris nunc congue nisi vitae suscipit. Dictum non consectetur a erat nam. Laoreet non curabitur gravida arcu ac tortor dignissim. Eu augue ut lectus arcu bibendum at. Facilisis gravida neque convallis a cras semper. Ut consequat semper viverra nam libero justo laoreet sit. Sagittis vitae et leo duis. Eget nullam non nisi est sit amet facilisis magna etiam. Nisl tincidunt eget nullam non nisi est sit amet facilisis. Auctor neque vitae tempus quam pellentesque. Vel facilisis volutpat est velit egestas dui id ornare arcu. Nam aliquam sem et tortor consequat id porta nibh venenatis.\n\nViverra nam libero justo laoreet sit amet. Pharetra diam sit amet nisl. Quam viverra orci sagittis eu. Rhoncus dolor purus non enim. Posuere urna nec tincidunt praesent semper feugiat. Suspendisse in est ante in nibh mauris cursus. Sit amet consectetur adipiscing elit duis. Tortor id aliquet lectus proin nibh nisl condimentum id. At in tellus integer feugiat scelerisque. Maecenas sed enim ut sem viverra aliquet. Pellentesque pulvinar pellentesque habitant morbi. Ultrices neque ornare aenean euismod elementum nisi quis eleifend. Praesent tristique magna sit amet purus gravida. Diam volutpat commodo sed egestas egestas. Ut placerat orci nulla pellentesque dignissim enim. Ultrices in iaculis nunc sed augue lacus viverra. Etiam sit amet nisl purus.\n\n## Typographic replacements\n\nEnable typographer option to see result.\n\n(c) (C) (r) (R) (tm) (TM) (p) (P) +-\n\ntest.. test... test..... test?..... test!....\n\n!!!!!! ???? ,, -- ---\n\n"Smartypants, double quotes" and \'single quotes\'\n\n## Emphasis\n\n**This is bold text**\n\n__This is bold text__\n\n*This is italic text*\n\n_This is italic text_\n\n## Lists\n\nUnordered\n\n+ Create a list by starting a line with +, -, or *\n+ Sub-lists are made by indenting 2 spaces:\n+ Very easy!\n\nOrdered\n\n1. Lorem ipsum dolor sit amet\n2. Consectetur adipiscing elit\n3. Integer molestie lorem at massa\n\n\n1. You can use sequential numbers...\n1. ...or keep all the numbers as `1.`\n\n## Links\n\n[link text](http://dev.nodeca.com)\n\n[link with title](http://nodeca.github.io/pica/demo/ "title text!")\n\nAutoconverted link https://github.com/nodeca/pica (enable linkify to see)\n\n### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)\n\n- 19^th^\n- H~2~O\n', + date: Date.now(), + target: { + daedalus: 'v0.13', + platform: 'macOS', + platformVersion: '10.14.6', + }, + title: 'Lazarus Incident', + })} + + )) + .add('Incident Overlay', props => ( + {}} /> + )); diff --git a/storybook/stories/news/NewsFeed.stories.js b/storybook/stories/news/NewsFeed.stories.js new file mode 100644 index 0000000000..2575fd87cf --- /dev/null +++ b/storybook/stories/news/NewsFeed.stories.js @@ -0,0 +1,184 @@ +// @flow +// eslint-disable-file no-unused-vars +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { boolean } from '@storybook/addon-knobs'; +import StoryDecorator from '../support/StoryDecorator'; +import NewsFeed from '../../../source/renderer/app/components/news/NewsFeed'; +import News from '../../../source/renderer/app/domains/News'; + +const news = [ + new News.News({ + title: 'Some title 1 in English', + content: 'Some title 1 in English', + target: { daedalusVersion: null, platform: 'darwin' }, + action: { + label: 'Visit en-US', + url: 'https://iohk.zendesk.com/hc/en-us/articles/', + }, + date: 1568650464961, + type: 'incident', + read: false, + }), + new News.News({ + title: 'Some title 2 in English', + content: 'Some title 2 in English', + target: { daedalusVersion: null, platform: 'win32' }, + action: { + label: 'Visit en-US', + url: 'https://iohk.zendesk.com/hc/en-us/articles/', + }, + date: 1568736864962, + type: 'incident', + read: false, + }), + new News.News({ + title: 'Some title 3 in English', + content: 'Some title 3 in English', + target: { daedalusVersion: null, platform: 'linux' }, + action: { label: 'Check en-US', route: '/wallets' }, + date: 1568823264963, + type: 'alert', + read: false, + }), + new News.News({ + title: 'Some title 4 in English', + content: 'Some title 4 in English', + target: { daedalusVersion: null, platform: 'darwin' }, + action: { + label: 'Visit en-US', + url: 'https://iohk.zendesk.com/hc/en-us/articles/', + }, + date: 1568909664963, + type: 'alert', + read: false, + }), + new News.News({ + title: 'Some title 5 in English', + content: 'Some title 5 in English', + target: { daedalusVersion: null, platform: 'darwin' }, + action: { label: 'Check en-US', route: '/settings' }, + date: 1568996064964, + type: 'announcement', + read: false, + }), + new News.News({ + title: 'Some title 6 in English', + content: 'Some title 6 in English', + target: { daedalusVersion: null, platform: 'win32' }, + action: { + label: 'Visit en-US', + url: 'https://iohk.zendesk.com/hc/en-us/articles/', + }, + date: 1569082464964, + type: 'announcement', + read: false, + }), + new News.News({ + title: 'Some title 7 in English', + content: 'Some title 7 in English', + target: { daedalusVersion: null, platform: 'darwin' }, + action: { label: 'Check en-US', route: '/settings' }, + date: 1569168864965, + type: 'info', + read: false, + }), + new News.News({ + title: 'Some title 8 in English', + content: 'Some title 8 in English', + target: { daedalusVersion: null, platform: 'linux' }, + action: { + label: 'Visit en-US', + url: 'https://iohk.zendesk.com/hc/en-us/articles/', + }, + date: 1569255264965, + type: 'info', + read: false, + }), + new News.News({ + title: 'Some title 9 in English', + content: 'Some title 9 in English', + target: { daedalusVersion: null, platform: 'darwin' }, + action: { + label: 'Visit https://markdown-it.github.io/', + url: 'https://markdown-it.github.io/', + }, + date: 1569255294965, + type: 'alert', + read: false, + }), +]; + +const newsCollection = new News.NewsCollection(news); + +storiesOf('NewsFeed', module) + .addDecorator(story => ( + {story(newsCollection)} + )) + + // ====== Stories ====== + + .add('NewsFeed - no news items fetched from server', () => ( +
+ {}} + onOpenAlert={() => {}} + /> +
+ )) + + .add('NewsFeed - newsfeed empty', () => ( +
+ {}} + onOpenAlert={() => {}} + /> +
+ )) + + .add('NewsFeed - loading', () => ( +
+ {}} + onOpenAlert={() => {}} + /> +
+ )) + + .add('NewsFeed', () => ( +
+ {}} + onOpenAlert={() => {}} + /> +
+ )); diff --git a/storybook/stories/support/StoryLayout.js b/storybook/stories/support/StoryLayout.js index cf268151a6..68ecd31306 100644 --- a/storybook/stories/support/StoryLayout.js +++ b/storybook/stories/support/StoryLayout.js @@ -11,6 +11,7 @@ import { CATEGORIES_BY_NAME } from '../../../source/renderer/app/config/sidebarC import { formattedWalletAmount } from '../../../source/renderer/app/utils/formatters'; import NodeSyncStatusIcon from '../../../source/renderer/app/components/widgets/NodeSyncStatusIcon'; import Wallet from '../../../source/renderer/app/domains/Wallet.js'; +import NewsFeedIcon from '../../../source/renderer/app/components/widgets/NewsFeedIcon'; import type { SidebarMenus } from '../../../source/renderer/app/components/sidebar/Sidebar'; import type { SidebarWalletType } from '../../../source/renderer/app/types/sidebarTypes'; // import type { Wallet } from '../../../source/renderer/app/domains/WalletTransaction'; @@ -190,6 +191,10 @@ export default class StoryLayout extends Component { isProduction isMainnet /> + ); } diff --git a/yarn.lock b/yarn.lock index 77dea6e7ac..ea01cd2e29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10683,6 +10683,13 @@ rcedit@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/rcedit/-/rcedit-1.1.2.tgz#7a28edf981953f75b5f3e5d4cbc1f9ffa0abbc78" +react-animate-height@2.0.15: + version "2.0.15" + resolved "https://registry.yarnpkg.com/react-animate-height/-/react-animate-height-2.0.15.tgz#155f42e1ab0befe785b36b3e76233230172c1857" + dependencies: + classnames "^2.2.5" + prop-types "^15.6.1" + react-clientside-effect@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837" @@ -11791,14 +11798,14 @@ semver-greatest-satisfied-range@^1.1.0: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" +semver@6.3.0, semver@^6.0.0, semver@^6.1.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + semver@^4.1.0: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" -semver@^6.0.0, semver@^6.1.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"