diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b00c73445..206eca6b6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.1 +current_version = 0.16.0 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/CHANGELOG.md b/CHANGELOG.md index 331fb8d39..0e4786a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,29 @@ Web UI version numbers should always match the corresponding version of LBRY App * * +## [0.16.0] - 2017-09-21 + +### Added + * Added a tipping button to send LBRY Credits to a creator. + * Added an edit button on published content. Significantly improved UX for editing claims. + * Added theme settings option and new Dark theme. + * Significantly more detail is shown about past transactions and new filtering options for transactions. + * File pages now show the time of a publish. + * The "auth token" displayable on Help offers security warning + * Added a new component for rendering dates and times. This component can render the date and time of a block height, as well. + * Added a `Form` component, to further progress towards form sanity. + * Added `gnome-keyring` dependency to .deb + + +### Changed + * CSS significantly refactored to support CSS vars (and consequently easy theming). + + +### Fixed + * URLs on cards no longer wrap and show an ellipsis if longer than one line + + + ## [0.15.1] - 2017-09-08 ### Added diff --git a/README.md b/README.md index 5e94a8bf5..623ee135a 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,52 @@ to create distributable packages, which is run by calling: `node_modules/.bin/build -p never` -### Development on Windows +## Development on Windows -This project has currently only been worked on in Linux and macOS. If you are on Windows, you can -checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/.appveyor.yml) and probably figure out something from there. +### Windows Dependency + +1. Download and install `npm` and `node` from nodejs.org +2. Download and install `python 2.7` from python.org +3. Download and Install `Microsoft Visual C++ Compiler for Python 2.7` from Microsoft +4. Download and install `.NET Framework 2.0 Software Development Kit (SDK) (x64)` from Microsoft + +### One-time Setup +1. Open command prompt in the root of the project and run the following; +``` +python -m pip install -r build\requirements.txt +python build\set_version.py +npm install -g yarn +yarn install +``` +2. Change directory to `app` and run the following; +``` +yarn install +node_modules\.bin\electron-rebuild +node_modules\.bin\electron-rebuild +cd .. +``` +3. Change directory to `ui` and run the following +``` +yarn install +npm rebuild node-sass +node node_modules\node-sass\bin\node-sass --output dist\css --sourcemap=none scss\ +node_modules\.bin\webpack --config webpack.dev.config.js +xcopy dist ..\app\dist +cd .. +``` +4. Download the lbry daemon and cli binaries and place them in `app\dist\` + +### Building lbry-app +1. run `node_modules\.bin\build -p never` from the root of the project. + +### Running the electron app +1. Run `./node_modules/.bin/electron app` + +### Ongoing Development +1. `cd ui` +2. `watch.bat` + +This will set up a monitor that will automatically compile any changes to JS or CSS folders inside of the `ui` folder. This allows you to make changes and see them immediately by reloading the app. ## Internationalization diff --git a/app/main.js b/app/main.js index b572eba82..4bc9944df 100644 --- a/app/main.js +++ b/app/main.js @@ -402,4 +402,4 @@ ipcMain.on('get-auth-token', (event) => { ipcMain.on('set-auth-token', (event, token) => { keytar.setPassword("LBRY", "auth_token", token ? token.toString().trim() : null); -}); \ No newline at end of file +}); diff --git a/app/package.json b/app/package.json index 9c2b5d5fb..15878e6e7 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.15.1", + "version": "0.16.0", "main": "main.js", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "author": { @@ -20,8 +20,8 @@ "electron-rebuild": "^1.5.11" }, "lbrySettings": { - "lbrynetDaemonVersion": "0.15.2", + "lbrynetDaemonVersion": "0.16.1", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/build.sh b/build.sh index 1105489f8..6dc7044f1 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,4 @@ #!/bin/bash # this is here because teamcity runs /build.sh to build the project -set -euxo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -$DIR/build/build.sh \ No newline at end of file +$DIR/build/build.sh diff --git a/build/build.sh b/build/build.sh index 92c2c5e8f..86c9a6217 100755 --- a/build/build.sh +++ b/build/build.sh @@ -1,7 +1,6 @@ #!/bin/bash set -euo pipefail -set -x ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" cd "$ROOT" @@ -10,27 +9,33 @@ BUILD_DIR="$ROOT/build" LINUX=false OSX=false if [ "$(uname)" == "Darwin" ]; then + echo -e "\033[0;32mBuilding for OSX\x1b[m" OSX=true elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + echo -e "\033[0;32mBuilding for Linux\x1b[m" LINUX=true else - echo "Platform detection failed" + echo -e "\033[1;31mPlatform detection failed\x1b[m" exit 1 fi if $OSX; then ICON="$BUILD_DIR/icon.icns" else - ICON="$BUILD_DIR/icons/lbry48.png" + ICON="$BUILD_DIR/icons/48x48.png" fi FULL_BUILD="${FULL_BUILD:-false}" if [ -n "${TEAMCITY_VERSION:-}" -o -n "${APPVEYOR:-}" ]; then FULL_BUILD="true" fi +if [ "$FULL_BUILD" != "true" ]; then + echo -e "\033[1;36mDependencies will NOT be installed. Run with 'FULL_BUILD=true' to install dependencies.\x1b[m" +fi if [ "$FULL_BUILD" == "true" ]; then # install dependencies + echo -e "\033[0;32mInstalling Dependencies\x1b[m" $BUILD_DIR/prebuild.sh VENV="$BUILD_DIR/venv" @@ -57,7 +62,7 @@ yarn install ############ # UI # ############ - +echo -e "\033[0;32mCompiling UI\x1b[m" ( cd "$ROOT/ui" yarn install @@ -73,7 +78,7 @@ yarn install #################### # daemon and cli # #################### - +echo -e "\033[0;32mGrabbing Daemon and CLI\x1b[m" if $OSX; then OSNAME="macos" else @@ -90,7 +95,7 @@ if [[ ! -f $DAEMON_VER_PATH || ! -f $ROOT/app/dist/lbrynet-daemon || "$(< "$DAEM rm "$BUILD_DIR/daemon.zip" echo "$DAEMON_VER" > "$DAEMON_VER_PATH" else - echo "Already have daemon version $DAEMON_VER, skipping download" + echo -e "\033[4;31mAlready have daemon version $DAEMON_VER, skipping download\x1b[m" fi @@ -99,7 +104,7 @@ fi ################### # Build the app # ################### - +echo -e '\033[0;32mBuilding Lbry-app\x1b[m' ( cd "$ROOT/app" yarn install @@ -133,7 +138,7 @@ if [ "$FULL_BUILD" == "true" ]; then deactivate - echo 'Build and packaging complete.' + echo -e '\033[0;32mBuild and packaging complete.\x1b[m' else - echo 'Build complete. Run `./node_modules/.bin/electron app` to launch the app' + echo -e 'Build complete. Run \033[1;31m./node_modules/.bin/electron app\x1b[m to launch the app' fi diff --git a/package.json b/package.json index 673ea306b..18565cf6b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,9 @@ "Exec": "/opt/LBRY/lbry %U" } }, + "deb": { + "depends": ["gconf2", "gconf-service", "libnotify4", "libappindicator1", "libxtst6", "libnss3", "libsecret-1-0"] + }, "win": { "target": "nsis" }, diff --git a/ui/dist/quit.html b/ui/dist/quit.html index 161d7ab4c..eff68e049 100644 --- a/ui/dist/quit.html +++ b/ui/dist/quit.html @@ -6,7 +6,6 @@ - diff --git a/ui/dist/themes/dark.css b/ui/dist/themes/dark.css new file mode 100644 index 000000000..c32108393 --- /dev/null +++ b/ui/dist/themes/dark.css @@ -0,0 +1,46 @@ +:root { + /* Colors */ + --color-primary: #009688; + --color-canvas: #0f1517; + --color-bg: #1a2327; + --color-bg-alt: #314048; + --color-help: #AAA; + --color-error: #a94442; + --color-load-screen-text: #FFF; + --color-money: var(--color-primary); + --color-meta-light: #757575; + --color-dark-overlay: rgba(15, 21, 23, 0.85); + + /* Text */ + --text-color: #FFF; + --text-selection-bg: rgba(0,150,136, 0.95); + + /* Input */ + --input-bg: transparent; + --input-active-bg: rgba(0,0,0, 0.5); + --input-border-color: rgba(255,255,255, 0.25); + + /* Search */ + --search-bg: rgba(0,0,0, 0.45); + --search-color: #757575; + --search-active-bg: rgba(0,0,0, 0.75); + --search-border: 1px solid rgba(0,0,0, 0.25); + + /* Tab */ + --tab-color: #757575; + --tab-active-color: #CCC; + + /* Header */ + --header-color: #CCC; + --header-active-color: #FFF; + --header-button-bg: transparent; + + /* Table */ + --table-border: 0; + --table-item-even: var(--color-bg-alt); + --table-item-odd: transparent; + + /* Modla */ + --modal-overlay-bg: var(--color-dark-overlay); + --modal-border: 1px solid rgba(0, 0, 0, 0.25); +} diff --git a/ui/dist/themes/light.css b/ui/dist/themes/light.css new file mode 100644 index 000000000..c95f61136 --- /dev/null +++ b/ui/dist/themes/light.css @@ -0,0 +1,4 @@ +:root { + /* Colors */ + --color-primary: #155B4A; +} diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 3d81d6c69..67fb76071 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -1,4 +1,5 @@ import * as types from "constants/action_types"; +import * as settings from "constants/settings"; import lbry from "lbry"; import { selectUpdateUrl, @@ -8,7 +9,7 @@ import { } from "selectors/app"; import { doFetchDaemonSettings } from "actions/settings"; import { doAuthenticate } from "actions/user"; -import { doFileList } from "actions/file_info"; +import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; const { remote, ipcRenderer, shell } = require("electron"); const path = require("path"); @@ -16,11 +17,12 @@ const { download } = remote.require("electron-dl"); const fs = remote.require("fs"); const { lbrySettings: config } = require("../../../app/package.json"); -export function doOpenModal(modal) { +export function doOpenModal(modal, modalProps = {}) { return { type: types.OPEN_MODAL, data: { modal, + modalProps, }, }; } @@ -165,7 +167,7 @@ export function doAlertError(errorList) { type: types.OPEN_MODAL, data: { modal: "error", - extraContent: errorList, + modalProps: { error: errorList }, }, }); }; @@ -176,7 +178,7 @@ export function doDaemonReady() { dispatch(doAuthenticate()); dispatch({ type: types.DAEMON_READY }); dispatch(doFetchDaemonSettings()); - dispatch(doFileList()); + dispatch(doFetchFileInfosAndPublishedClaims()); }; } diff --git a/ui/js/actions/availability.js b/ui/js/actions/availability.js index 2c7cbc3cb..4cde9d2ed 100644 --- a/ui/js/actions/availability.js +++ b/ui/js/actions/availability.js @@ -4,6 +4,11 @@ import { selectFetchingAvailability } from "selectors/availability"; export function doFetchAvailability(uri) { return function(dispatch, getState) { + /* + this is disabled atm - Jeremy + */ + return; + const state = getState(); const alreadyFetching = !!selectFetchingAvailability(state)[uri]; diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index 69c717e88..eb62506a2 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -4,11 +4,11 @@ import lbryio from "lbryio"; import lbryuri from "lbryuri"; import { selectBalance } from "selectors/wallet"; import { - selectFileInfoForUri, + makeSelectFileInfoForUri, selectDownloadingByOutpoint, } from "selectors/file_info"; import { selectResolvingUris } from "selectors/content"; -import { selectCostInfoForUri } from "selectors/cost_info"; +import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { doAlertError, doOpenModal } from "actions/app"; import { doClaimEligiblePurchaseRewards } from "actions/rewards"; import { selectBadgeNumber } from "selectors/app"; @@ -299,13 +299,12 @@ export function doLoadVideo(uri) { data: { uri }, }); - dispatch(doOpenModal("timedOut")); + dispatch(doOpenModal(modals.FILE_TIMEOUT, { uri })); } else { dispatch(doDownloadFile(uri, streamInfo)); } }) .catch(error => { - console.log(error); dispatch({ type: types.LOADING_VIDEO_FAILED, data: { uri }, @@ -315,46 +314,37 @@ export function doLoadVideo(uri) { }; } -export function doPurchaseUri(uri, purchaseModalName) { +export function doPurchaseUri(uri) { return function(dispatch, getState) { const state = getState(); const balance = selectBalance(state); - const fileInfo = selectFileInfoForUri(state, { uri }); + const fileInfo = makeSelectFileInfoForUri(uri)(state); const downloadingByOutpoint = selectDownloadingByOutpoint(state); const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint]; - - // we already fully downloaded the file. - if (fileInfo && fileInfo.completed) { - // If written_bytes is false that means the user has deleted/moved the - // file manually on their file system, so we need to dispatch a - // doLoadVideo action to reconstruct the file from the blobs - if (!fileInfo.written_bytes) dispatch(doLoadVideo(uri)); - - return Promise.resolve(); - } - - // we are already downloading the file - if (alreadyDownloading) { - return Promise.resolve(); - } - - const costInfo = selectCostInfoForUri(state, { uri }); + const costInfo = makeSelectCostInfoForUri(uri)(state); const { cost } = costInfo; - // the file is free or we have partially downloaded it - if (cost === 0 || (fileInfo && fileInfo.download_directory)) { - dispatch(doLoadVideo(uri)); - return Promise.resolve(); + if ( + alreadyDownloading || + (fileInfo && fileInfo.completed && fileInfo.written_bytes > 0) + ) { + return; + } + + // we already fully downloaded the file. + if ( + cost === 0 || + (fileInfo && (fileInfo.completed || fileInfo.download_directory)) + ) { + return dispatch(doLoadVideo(uri)); } if (cost > balance) { - dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS)); - } else { - dispatch(doOpenModal(purchaseModalName)); + return dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS)); } - return Promise.resolve(); + return dispatch(doOpenModal(modals.AFFIRM_PURCHASE, { uri })); }; } @@ -365,7 +355,7 @@ export function doFetchClaimsByChannel(uri, page) { data: { uri, page }, }); - lbry.claim_list_by_channel({ uri, page }).then(result => { + lbry.claim_list_by_channel({ uri, page: page || 1 }).then(result => { const claimResult = result[uri], claims = claimResult ? claimResult.claims_in_channel : [], currentPage = claimResult ? claimResult.returned_page : undefined; @@ -421,6 +411,22 @@ export function doFetchClaimListMine() { }; } +export function doPlayUri(uri) { + return function(dispatch, getState) { + dispatch(doSetPlayingUri(uri)); + dispatch(doPurchaseUri(uri)); + }; +} + +export function doSetPlayingUri(uri) { + return function(dispatch, getState) { + dispatch({ + type: types.SET_PLAYING_URI, + data: { uri }, + }); + }; +} + export function doFetchChannelListMine() { return function(dispatch, getState) { dispatch({ diff --git a/ui/js/actions/cost_info.js b/ui/js/actions/cost_info.js index 6568a3575..001c4ae56 100644 --- a/ui/js/actions/cost_info.js +++ b/ui/js/actions/cost_info.js @@ -33,26 +33,53 @@ export function doFetchCostInfoForUri(uri) { }); } - if (isGenerous && claim) { - let cost; - const fee = claim.value && - claim.value.stream && - claim.value.stream.metadata - ? claim.value.stream.metadata.fee - : undefined; - if (fee === undefined) { - resolve({ cost: 0, includesData: true }); - } else if (fee.currency == "LBC") { - resolve({ cost: fee.amount, includesData: true }); - } else { - begin(); - lbryio.getExchangeRates().then(({ lbc_usd }) => { - resolve({ cost: fee.amount / lbc_usd, includesData: true }); - }); - } + /** + * "Generous" check below is disabled. We're no longer attempting to include or estimate data fees regardless of settings. + * + * This should be modified when lbry.stream_cost_estimate is reliable and performant. + */ + + /* + lbry.stream_cost_estimate({ uri }).then(cost => { + cacheAndResolve(cost); + }, reject); + */ + + const fee = claim.value && claim.value.stream && claim.value.stream.metadata + ? claim.value.stream.metadata.fee + : undefined; + + if (fee === undefined) { + resolve({ cost: 0, includesData: true }); + } else if (fee.currency == "LBC") { + resolve({ cost: fee.amount, includesData: true }); } else { - begin(); - lbry.getCostInfo(uri).then(resolve); + // begin(); + lbryio.getExchangeRates().then(({ lbc_usd }) => { + resolve({ cost: fee.amount / lbc_usd, includesData: true }); + }); } + + // if (isGenerous && claim) { + // let cost; + // const fee = claim.value && + // claim.value.stream && + // claim.value.stream.metadata + // ? claim.value.stream.metadata.fee + // : undefined; + // if (fee === undefined) { + // resolve({ cost: 0, includesData: true }); + // } else if (fee.currency == "LBC") { + // resolve({ cost: fee.amount, includesData: true }); + // } else { + // // begin(); + // lbryio.getExchangeRates().then(({ lbc_usd }) => { + // resolve({ cost: fee.amount / lbc_usd, includesData: true }); + // }); + // } + // } else { + // begin(); + // lbry.getCostInfo(uri).then(resolve); + // } }; } diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js index 7d9d102c6..49c949e5c 100644 --- a/ui/js/actions/file_info.js +++ b/ui/js/actions/file_info.js @@ -13,7 +13,7 @@ import { selectTotalDownloadProgress, } from "selectors/file_info"; import { doCloseModal } from "actions/app"; -import { doHistoryBack } from "actions/navigation"; +import { doNavigate, doHistoryBack } from "actions/navigation"; import setProgressBar from "util/setProgressBar"; import batchActions from "util/batchActions"; @@ -71,18 +71,18 @@ export function doFileList() { }; } -export function doOpenFileInShell(fileInfo) { +export function doOpenFileInShell(path) { return function(dispatch, getState) { - const success = shell.openItem(fileInfo.download_path); + const success = shell.openItem(path); if (!success) { - dispatch(doOpenFileInFolder(fileInfo)); + dispatch(doOpenFileInFolder(path)); } }; } -export function doOpenFileInFolder(fileInfo) { +export function doOpenFileInFolder(path) { return function(dispatch, getState) { - shell.showItemInFolder(fileInfo.download_path); + shell.showItemInFolder(path); }; } diff --git a/ui/js/actions/navigation.js b/ui/js/actions/navigation.js index b34a2e876..0ab3a22eb 100644 --- a/ui/js/actions/navigation.js +++ b/ui/js/actions/navigation.js @@ -1,5 +1,6 @@ import * as types from "constants/action_types"; import { + computePageFromPath, selectPageTitle, selectCurrentPage, selectCurrentParams, @@ -20,9 +21,17 @@ export function doNavigate(path, params = {}, options = {}) { url += "?" + toQueryString(params); } - dispatch(doChangePath(url, options)); + const state = getState(), + currentPage = selectCurrentPage(state), + nextPage = computePageFromPath(path), + scrollY = options.scrollY; - const pageTitle = selectPageTitle(getState()) + " - LBRY"; + if (currentPage != nextPage) { + //I wasn't seeing it scroll to the proper position without this -- possibly because the page isn't fully rendered? Not sure - Jeremy + setTimeout(() => { + window.scrollTo(0, scrollY ? scrollY : 0); + }, 100); + } dispatch({ type: types.HISTORY_NAVIGATE, @@ -45,31 +54,6 @@ export function doAuthNavigate(pathAfterAuth = null, params = {}) { }; } -export function doChangePath(path, options = {}) { - return function(dispatch, getState) { - dispatch({ - type: types.CHANGE_PATH, - data: { - path, - }, - }); - - const state = getState(); - const scrollY = options.scrollY; - - //I wasn't seeing it scroll to the proper position without this -- possibly because the page isn't fully rendered? Not sure - Jeremy - setTimeout(() => { - window.scrollTo(0, scrollY ? scrollY : 0); - }, 100); - - const currentPage = selectCurrentPage(state); - if (currentPage === "search") { - const params = selectCurrentParams(state); - dispatch(doSearch(params.query)); - } - }; -} - export function doHistoryTraverse(dispatch, state, modifier) { const stack = selectHistoryStack(state), index = selectHistoryIndex(state) + modifier; diff --git a/ui/js/actions/settings.js b/ui/js/actions/settings.js index 21e27f1fb..4395c6603 100644 --- a/ui/js/actions/settings.js +++ b/ui/js/actions/settings.js @@ -1,10 +1,16 @@ import * as types from "constants/action_types"; import * as settings from "constants/settings"; +import { doAlertError } from "actions/app"; import batchActions from "util/batchActions"; + import lbry from "lbry"; import fs from "fs"; import http from "http"; +const { remote } = require("electron"); +const { extname } = require("path"); +const { readdir } = remote.require("fs"); + export function doFetchDaemonSettings() { return function(dispatch, getState) { lbry.settings_get().then(settings => { @@ -46,6 +52,27 @@ export function doSetClientSetting(key, value) { }; } +export function doGetThemes() { + return function(dispatch, getState) { + const dir = `${remote.app.getAppPath()}/dist/themes`; + + readdir(dir, (error, files) => { + if (!error) { + dispatch( + doSetClientSetting( + settings.THEMES, + files + .filter(file => extname(file) === ".css") + .map(file => file.replace(".css", "")) + ) + ); + } else { + dispatch(doAlertError(error)); + } + }); + }; +} + export function doDownloadLanguage(langFile) { return function(dispatch, getState) { const destinationPath = app.i18n.directory + "/" + langFile; @@ -137,7 +164,7 @@ export function doDownloadLanguages() { export function doChangeLanguage(language) { return function(dispatch, getState) { - lbry.setClientSetting(settings.LANGUAGE, language); + dispatch(doSetClientSetting(settings.LANGUAGE, language)); app.i18n.setLocale(language); }; } diff --git a/ui/js/actions/wallet.js b/ui/js/actions/wallet.js index dd48c71fa..0e90266fb 100644 --- a/ui/js/actions/wallet.js +++ b/ui/js/actions/wallet.js @@ -5,7 +5,9 @@ import { selectDraftTransactionAmount, selectBalance, } from "selectors/wallet"; -import { doOpenModal } from "actions/app"; +import { doOpenModal, doShowSnackBar } from "actions/app"; +import { doNavigate } from "actions/navigation"; +import * as modals from "constants/modal_types"; export function doUpdateBalance(balance) { return { @@ -22,7 +24,7 @@ export function doFetchTransactions() { type: types.FETCH_TRANSACTIONS_STARTED, }); - lbry.transaction_list().then(results => { + lbry.transaction_list({ include_tip_info: true }).then(results => { dispatch({ type: types.FETCH_TRANSACTIONS_COMPLETED, data: { @@ -83,8 +85,8 @@ export function doSendDraftTransaction() { const balance = selectBalance(state); const amount = selectDraftTransactionAmount(state); - if (balance - amount < 1) { - return dispatch(doOpenModal("insufficientBalance")); + if (balance - amount <= 0) { + return dispatch(doOpenModal(modals.INSUFFICIENT_BALANCE)); } dispatch({ @@ -96,13 +98,19 @@ export function doSendDraftTransaction() { dispatch({ type: types.SEND_TRANSACTION_COMPLETED, }); - dispatch(doOpenModal("transactionSuccessful")); + dispatch( + doShowSnackBar({ + message: __(`You sent ${amount} LBC`), + linkText: __("History"), + linkTarget: __("/wallet"), + }) + ); } else { dispatch({ type: types.SEND_TRANSACTION_FAILED, data: { error: results }, }); - dispatch(doOpenModal("transactionFailed")); + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); } }; @@ -111,7 +119,7 @@ export function doSendDraftTransaction() { type: types.SEND_TRANSACTION_FAILED, data: { error: error.message }, }); - dispatch(doOpenModal("transactionFailed")); + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); }; lbry @@ -136,3 +144,55 @@ export function doSetDraftTransactionAddress(address) { data: { address }, }; } + +export function doSendSupport(amount, claim_id, uri) { + return function(dispatch, getState) { + const state = getState(); + const balance = selectBalance(state); + + if (balance - amount <= 0) { + return dispatch(doOpenModal(modals.INSUFFICIENT_BALANCE)); + } + + dispatch({ + type: types.SUPPORT_TRANSACTION_STARTED, + }); + + const successCallback = results => { + if (results.txid) { + dispatch({ + type: types.SUPPORT_TRANSACTION_COMPLETED, + }); + dispatch( + doShowSnackBar({ + message: __(`You sent ${amount} LBC as support, Mahalo!`), + linkText: __("History"), + linkTarget: __("/wallet"), + }) + ); + dispatch(doNavigate("/show", { uri })); + } else { + dispatch({ + type: types.SUPPORT_TRANSACTION_FAILED, + data: { error: results.code }, + }); + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); + } + }; + + const errorCallback = error => { + dispatch({ + type: types.SUPPORT_TRANSACTION_FAILED, + data: { error: error.code }, + }); + dispatch(doOpenModal(modals.TRANSACTION_FAILED)); + }; + + lbry + .wallet_send({ + claim_id: claim_id, + amount: amount, + }) + .then(successCallback, errorCallback); + }; +} diff --git a/ui/js/app.js b/ui/js/app.js index 0aa908904..1768f246c 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -1,19 +1,14 @@ import store from "store.js"; -import lbry from "./lbry.js"; import { remote } from "electron"; -import * as settings from "constants/settings"; const env = ENV; const config = { ...require(`./config/${env}`), }; -const language = lbry.getClientSetting(settings.LANGUAGE) - ? lbry.getClientSetting(settings.LANGUAGE) - : "en"; const i18n = require("y18n")({ directory: remote.app.getAppPath() + "/locales", updateFiles: false, - locale: language, + locale: "en", }); const logs = []; const app = { diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js index 38527d94b..574c5f862 100644 --- a/ui/js/component/app/index.js +++ b/ui/js/component/app/index.js @@ -2,10 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import { selectPageTitle } from "selectors/navigation"; import { selectUser } from "selectors/user"; -import { - doCheckUpgradeAvailable, - doAlertError, -} from "actions/app"; +import { doCheckUpgradeAvailable, doAlertError } from "actions/app"; import { doRecordScroll } from "actions/navigation"; import { doFetchRewardedContent } from "actions/content"; import { doUpdateBalance } from "actions/wallet"; diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index e3c2465de..37bd05c72 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -1,6 +1,7 @@ import React from "react"; import Router from "component/router/index"; import Header from "component/header"; +import Theme from "component/theme"; import ModalRouter from "modal/modalRouter"; import lbry from "lbry"; @@ -49,6 +50,7 @@ class App extends React.PureComponent { render() { return (
+
diff --git a/ui/js/component/common.js b/ui/js/component/common.js index d92669262..598e61a31 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -70,7 +70,7 @@ export class CreditAmount extends React.PureComponent { showFree: React.PropTypes.bool, showFullPrice: React.PropTypes.bool, showPlus: React.PropTypes.bool, - look: React.PropTypes.oneOf(["indicator", "plain"]), + look: React.PropTypes.oneOf(["indicator", "plain", "fee"]), }; static defaultProps = { @@ -78,7 +78,6 @@ export class CreditAmount extends React.PureComponent { label: true, showFree: false, look: "indicator", - showFree: false, showFullPrice: false, showPlus: false, }; diff --git a/ui/js/component/dateTime/view.jsx b/ui/js/component/dateTime/view.jsx index 72d47c130..747286daa 100644 --- a/ui/js/component/dateTime/view.jsx +++ b/ui/js/component/dateTime/view.jsx @@ -1,6 +1,10 @@ import React from "react"; class DateTime extends React.PureComponent { + static SHOW_DATE = "date"; + static SHOW_TIME = "time"; + static SHOW_BOTH = "both"; + componentWillMount() { this.refreshDate(this.props); } @@ -17,9 +21,20 @@ class DateTime extends React.PureComponent { } render() { - const { date } = this.props; + const { date, formatOptions } = this.props; + const show = this.props.show || DateTime.SHOW_BOTH; - return {date && date.toLocaleString()}; + return ( + + {date && + (show == DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) && + date.toLocaleDateString()} + {show == DateTime.SHOW_BOTH && " "} + {date && + (show == DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) && + date.toLocaleTimeString()} + + ); } } diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js index 759abee47..68597a571 100644 --- a/ui/js/component/fileActions/index.js +++ b/ui/js/component/fileActions/index.js @@ -1,52 +1,20 @@ import React from "react"; import { connect } from "react-redux"; -import { selectPlatform, selectCurrentModal } from "selectors/app"; -import { - makeSelectFileInfoForUri, - makeSelectDownloadingForUri, - makeSelectLoadingForUri, -} from "selectors/file_info"; -import { makeSelectIsAvailableForUri } from "selectors/availability"; +import { makeSelectFileInfoForUri } from "selectors/file_info"; import { makeSelectCostInfoForUri } from "selectors/cost_info"; -import { doCloseModal, doOpenModal } from "actions/app"; -import { doFetchAvailability } from "actions/availability"; -import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info"; -import { makeSelectClaimForUriIsMine } from "selectors/claims"; -import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content"; +import { doOpenModal } from "actions/app"; +import { makeSelectClaimIsMine } from "selectors/claims"; import FileActions from "./view"; -const makeSelect = () => { - const selectFileInfoForUri = makeSelectFileInfoForUri(); - const selectIsAvailableForUri = makeSelectIsAvailableForUri(); - const selectDownloadingForUri = makeSelectDownloadingForUri(); - const selectCostInfoForUri = makeSelectCostInfoForUri(); - const selectLoadingForUri = makeSelectLoadingForUri(); - const selectClaimForUriIsMine = makeSelectClaimForUriIsMine(); - - const select = (state, props) => ({ - fileInfo: selectFileInfoForUri(state, props), - /*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/ - isAvailable: true, //selectIsAvailableForUri(state, props), - platform: selectPlatform(state), - modal: selectCurrentModal(state), - downloading: selectDownloadingForUri(state, props), - costInfo: selectCostInfoForUri(state, props), - loading: selectLoadingForUri(state, props), - claimIsMine: selectClaimForUriIsMine(state, props), - }); - - return select; -}; - -const perform = dispatch => ({ - checkAvailability: uri => dispatch(doFetchAvailability(uri)), - closeModal: () => dispatch(doCloseModal()), - openInFolder: fileInfo => dispatch(doOpenFileInFolder(fileInfo)), - openInShell: fileInfo => dispatch(doOpenFileInShell(fileInfo)), - openModal: modal => dispatch(doOpenModal(modal)), - startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")), - loadVideo: uri => dispatch(doLoadVideo(uri)), - restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), +const select = (state, props) => ({ + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + /*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/ + costInfo: makeSelectCostInfoForUri(props.uri)(state), + claimIsMine: makeSelectClaimIsMine(props.uri)(state), }); -export default connect(makeSelect, perform)(FileActions); +const perform = dispatch => ({ + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), +}); + +export default connect(select, perform)(FileActions); diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx index 73b48ba3b..ecb218228 100644 --- a/ui/js/component/fileActions/view.jsx +++ b/ui/js/component/fileActions/view.jsx @@ -1,213 +1,40 @@ import React from "react"; -import { Icon, BusyMessage } from "component/common"; -import FilePrice from "component/filePrice"; -import { Modal } from "modal/modal"; import Link from "component/link"; -import { ToolTip } from "component/tooltip"; -import { DropDownMenu, DropDownMenuItem } from "component/menu"; -import ModalRemoveFile from "modal/modalRemoveFile"; +import FileDownloadLink from "component/fileDownloadLink"; import * as modals from "constants/modal_types"; class FileActions extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - forceShowActions: false, - }; - } - - componentWillMount() { - this.checkAvailability(this.props.uri); - } - - componentWillReceiveProps(nextProps) { - this.checkAvailability(nextProps.uri); - this.restartDownload(nextProps); - } - - restartDownload(props) { - const { downloading, fileInfo, uri, restartDownload } = props; - - if ( - !downloading && - fileInfo && - !fileInfo.completed && - fileInfo.written_bytes !== false && - fileInfo.written_bytes < fileInfo.total_bytes - ) { - restartDownload(uri, fileInfo.outpoint); - } - } - - checkAvailability(uri) { - if (!this._uri || uri !== this._uri) { - this._uri = uri; - this.props.checkAvailability(uri); - } - } - - onShowFileActionsRowClicked() { - this.setState({ - forceShowActions: true, - }); - } - - onAffirmPurchase() { - this.props.closeModal(); - this.props.loadVideo(this.props.uri); - } - render() { - const { - fileInfo, - isAvailable, - platform, - downloading, - uri, - openInFolder, - openInShell, - modal, - openModal, - closeModal, - startDownload, - costInfo, - loading, - claimIsMine, - } = this.props; + const { fileInfo, uri, openModal, claimIsMine } = this.props; - const metadata = fileInfo ? fileInfo.metadata : null, - openInFolderMessage = platform.startsWith("Mac") - ? __("Open in Finder") - : __("Open in Folder"), - showMenu = fileInfo && Object.keys(fileInfo).length > 0, - title = metadata ? metadata.title : uri; - - let content; - - if (loading || downloading) { - const progress = fileInfo && fileInfo.written_bytes - ? fileInfo.written_bytes / fileInfo.total_bytes * 100 - : 0, - label = fileInfo - ? progress.toFixed(0) + __("% complete") - : __("Connecting..."), - labelWithIcon = ( - - - - {label} - - - ); - - content = ( -
-
- {labelWithIcon} -
- {labelWithIcon} -
- ); - } else if (!fileInfo && isAvailable === undefined) { - content = ; - } else if (!fileInfo && !isAvailable && !this.state.forceShowActions) { - content = ( -
-
- {__("Content unavailable.")} -
- - -
- ); - } else if (fileInfo === null && !downloading) { - if (!costInfo) { - content = ; - } else { - content = ( - { - startDownload(uri); - }} - /> - ); - } - } else if (fileInfo && fileInfo.download_path) { - content = ( - openInShell(fileInfo)} - /> - ); - } else if (!fileInfo) { - content = ; - } else { - console.log("handle this case of file action props?"); - } + const claimId = fileInfo ? fileInfo.claim_id : null, + showDelete = fileInfo && Object.keys(fileInfo).length > 0; return ( -
- {content} - {showMenu - ?
- - openInFolder(fileInfo)} - label={openInFolderMessage} - /> - openModal(modals.CONFIRM_FILE_REMOVE)} - label={__("Remove...")} - /> - -
- : ""} - - {__("This will purchase")} {title} {__("for")}{" "} - - - {" "} - {__("credits")}. - - - {__("LBRY was unable to download the stream")}{" "}{" "} - {title}. - - {modal == modals.CONFIRM_FILE_REMOVE && - + {claimIsMine && + } + + + {showDelete && + openModal(modals.CONFIRM_FILE_REMOVE, { uri })} />}
); diff --git a/ui/js/component/fileCard/index.js b/ui/js/component/fileCard/index.js index d933736e8..a0de51640 100644 --- a/ui/js/component/fileCard/index.js +++ b/ui/js/component/fileCard/index.js @@ -9,32 +9,23 @@ import { } from "selectors/claims"; import { makeSelectFileInfoForUri } from "selectors/file_info"; import { - makeSelectIsResolvingForUri, + makeSelectIsUriResolving, selectRewardContentClaimIds, } from "selectors/content"; import FileCard from "./view"; -const makeSelect = () => { - const selectClaimForUri = makeSelectClaimForUri(); - const selectFileInfoForUri = makeSelectFileInfoForUri(); - const selectMetadataForUri = makeSelectMetadataForUri(); - const selectResolvingUri = makeSelectIsResolvingForUri(); - - const select = (state, props) => ({ - claim: selectClaimForUri(state, props), - fileInfo: selectFileInfoForUri(state, props), - obscureNsfw: !selectShowNsfw(state), - metadata: selectMetadataForUri(state, props), - rewardedContentClaimIds: selectRewardContentClaimIds(state, props), - isResolvingUri: selectResolvingUri(state, props), - }); - - return select; -}; +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + obscureNsfw: !selectShowNsfw(state), + metadata: makeSelectMetadataForUri(props.uri)(state), + rewardedContentClaimIds: selectRewardContentClaimIds(state, props), + isResolvingUri: makeSelectIsUriResolving(props.uri)(state), +}); const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), resolveUri: uri => dispatch(doResolveUri(uri)), }); -export default connect(makeSelect, perform)(FileCard); +export default connect(select, perform)(FileCard); diff --git a/ui/js/component/fileDetails/index.js b/ui/js/component/fileDetails/index.js new file mode 100644 index 000000000..738746d0a --- /dev/null +++ b/ui/js/component/fileDetails/index.js @@ -0,0 +1,23 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + makeSelectClaimForUri, + makeSelectContentTypeForUri, + makeSelectMetadataForUri, +} from "selectors/claims"; +import FileDetails from "./view"; +import { doOpenFileInFolder } from "actions/file_info"; +import { makeSelectFileInfoForUri } from "selectors/file_info"; + +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + contentType: makeSelectContentTypeForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + metadata: makeSelectMetadataForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + openFolder: path => dispatch(doOpenFileInFolder(path)), +}); + +export default connect(select, perform)(FileDetails); diff --git a/ui/js/component/fileDetails/view.jsx b/ui/js/component/fileDetails/view.jsx new file mode 100644 index 000000000..c2cfda468 --- /dev/null +++ b/ui/js/component/fileDetails/view.jsx @@ -0,0 +1,85 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import lbry from "lbry.js"; +import FileActions from "component/fileActions"; +import Link from "component/link"; +import DateTime from "component/dateTime"; + +const path = require("path"); + +class FileDetails extends React.PureComponent { + render() { + const { + claim, + contentType, + fileInfo, + metadata, + openFolder, + uri, + } = this.props; + + if (!claim || !metadata) { + return ( +
+ {__("Empty claim or metadata info.")} +
+ ); + } + + const { description, language, license } = metadata; + const { height } = claim; + const mediaType = lbry.getMediaType(contentType); + const directory = fileInfo && fileInfo.download_path + ? path.dirname(fileInfo.download_path) + : null; + + return ( +
+ +
+ +
+
+ + + + + + + + + + + + + + + + {directory && + + + + } + +
{__("Published on")}
{__("Content-Type")}{mediaType}
{__("Language")}{language}
{__("License")}{license}
{__("Downloaded to")} + openFolder(directory)}> + {directory} + +
+

+ +

+
+
+ ); + } +} + +export default FileDetails; diff --git a/ui/js/component/fileDownloadLink/index.js b/ui/js/component/fileDownloadLink/index.js new file mode 100644 index 000000000..2131b5f78 --- /dev/null +++ b/ui/js/component/fileDownloadLink/index.js @@ -0,0 +1,29 @@ +import React from "react"; +import { connect } from "react-redux"; +import { + makeSelectFileInfoForUri, + makeSelectDownloadingForUri, + makeSelectLoadingForUri, +} from "selectors/file_info"; +import { makeSelectCostInfoForUri } from "selectors/cost_info"; +import { doFetchAvailability } from "actions/availability"; +import { doOpenFileInShell } from "actions/file_info"; +import { doPurchaseUri, doStartDownload } from "actions/content"; +import FileDownloadLink from "./view"; + +const select = (state, props) => ({ + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + /*availability check is disabled due to poor performance, TBD if it dies forever or requires daemon fix*/ + downloading: makeSelectDownloadingForUri(props.uri)(state), + costInfo: makeSelectCostInfoForUri(props.uri)(state), + loading: makeSelectLoadingForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + checkAvailability: uri => dispatch(doFetchAvailability(uri)), + openInShell: path => dispatch(doOpenFileInShell(path)), + purchaseUri: uri => dispatch(doPurchaseUri(uri)), + restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), +}); + +export default connect(select, perform)(FileDownloadLink); diff --git a/ui/js/component/fileDownloadLink/view.jsx b/ui/js/component/fileDownloadLink/view.jsx new file mode 100644 index 000000000..49c6c29ce --- /dev/null +++ b/ui/js/component/fileDownloadLink/view.jsx @@ -0,0 +1,104 @@ +import React from "react"; +import { Icon, BusyMessage } from "component/common"; +import Link from "component/link"; + +class FileDownloadLink extends React.PureComponent { + componentWillMount() { + this.checkAvailability(this.props.uri); + } + + componentWillReceiveProps(nextProps) { + this.checkAvailability(nextProps.uri); + this.restartDownload(nextProps); + } + + restartDownload(props) { + const { downloading, fileInfo, uri, restartDownload } = props; + + if ( + !downloading && + fileInfo && + !fileInfo.completed && + fileInfo.written_bytes !== false && + fileInfo.written_bytes < fileInfo.total_bytes + ) { + restartDownload(uri, fileInfo.outpoint); + } + } + + checkAvailability(uri) { + if (!this._uri || uri !== this._uri) { + this._uri = uri; + this.props.checkAvailability(uri); + } + } + + render() { + const { + fileInfo, + downloading, + uri, + openInShell, + purchaseUri, + costInfo, + loading, + } = this.props; + + if (loading || downloading) { + const progress = fileInfo && fileInfo.written_bytes + ? fileInfo.written_bytes / fileInfo.total_bytes * 100 + : 0, + label = fileInfo + ? progress.toFixed(0) + __("% complete") + : __("Connecting..."), + labelWithIcon = ( + + + + {label} + + + ); + + return ( +
+
+ {labelWithIcon} +
+ {labelWithIcon} +
+ ); + } else if (fileInfo === null && !downloading) { + if (!costInfo) { + return ; + } else { + return ( + { + purchaseUri(uri); + }} + /> + ); + } + } else if (fileInfo && fileInfo.download_path) { + return ( + openInShell(fileInfo.download_path)} + /> + ); + } + + return null; + } +} + +export default FileDownloadLink; diff --git a/ui/js/component/fileList/view.jsx b/ui/js/component/fileList/view.jsx index 57d4d991f..8fad10cdc 100644 --- a/ui/js/component/fileList/view.jsx +++ b/ui/js/component/fileList/view.jsx @@ -88,6 +88,7 @@ class FileList extends React.PureComponent { key={fileInfo.outpoint || fileInfo.claim_id} uri={uri} hidePrice={true} + showActions={true} showEmpty={this.props.fileTileShowEmpty} /> ); diff --git a/ui/js/component/fileListSearch/view.jsx b/ui/js/component/fileListSearch/view.jsx index 682d07e7c..59884da8e 100644 --- a/ui/js/component/fileListSearch/view.jsx +++ b/ui/js/component/fileListSearch/view.jsx @@ -49,7 +49,17 @@ const FileListSearchResults = props => { class FileListSearch extends React.PureComponent { componentWillMount() { - this.props.search(this.props.query); + this.doSearch(this.props); + } + + componentWillReceiveProps(props) { + if (props.query != this.props.query) { + this.doSearch(props); + } + } + + doSearch(props) { + this.props.search(props.query); } render() { diff --git a/ui/js/component/filePrice/index.js b/ui/js/component/filePrice/index.js index be675d924..bac5cafe2 100644 --- a/ui/js/component/filePrice/index.js +++ b/ui/js/component/filePrice/index.js @@ -8,23 +8,15 @@ import { import { makeSelectClaimForUri } from "selectors/claims"; import FilePrice from "./view"; -const makeSelect = () => { - const selectCostInfoForUri = makeSelectCostInfoForUri(); - const selectFetchingCostInfoForUri = makeSelectFetchingCostInfoForUri(); - const selectClaim = makeSelectClaimForUri(); - - const select = (state, props) => ({ - costInfo: selectCostInfoForUri(state, props), - fetching: selectFetchingCostInfoForUri(state, props), - claim: selectClaim(state, props), - }); - - return select; -}; +const select = (state, props) => ({ + costInfo: makeSelectCostInfoForUri(props.uri)(state), + fetching: makeSelectFetchingCostInfoForUri(props.uri)(state), + claim: makeSelectClaimForUri(props.uri)(state), +}); const perform = dispatch => ({ fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), // cancelFetchCostInfo: (uri) => dispatch(doCancelFetchCostInfoForUri(uri)) }); -export default connect(makeSelect, perform)(FilePrice); +export default connect(select, perform)(FilePrice); diff --git a/ui/js/component/fileTile/index.js b/ui/js/component/fileTile/index.js index 2df504226..75b6ce62b 100644 --- a/ui/js/component/fileTile/index.js +++ b/ui/js/component/fileTile/index.js @@ -9,32 +9,23 @@ import { import { makeSelectFileInfoForUri } from "selectors/file_info"; import { selectShowNsfw } from "selectors/settings"; import { - makeSelectIsResolvingForUri, + makeSelectIsUriResolving, selectRewardContentClaimIds, } from "selectors/content"; import FileTile from "./view"; -const makeSelect = () => { - const selectClaimForUri = makeSelectClaimForUri(); - const selectFileInfoForUri = makeSelectFileInfoForUri(); - const selectMetadataForUri = makeSelectMetadataForUri(); - const selectResolvingUri = makeSelectIsResolvingForUri(); - - const select = (state, props) => ({ - claim: selectClaimForUri(state, props), - fileInfo: selectFileInfoForUri(state, props), - obscureNsfw: !selectShowNsfw(state), - metadata: selectMetadataForUri(state, props), - isResolvingUri: selectResolvingUri(state, props), - rewardedContentClaimIds: selectRewardContentClaimIds(state, props), - }); - - return select; -}; +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + obscureNsfw: !selectShowNsfw(state), + metadata: makeSelectMetadataForUri(props.uri)(state), + isResolvingUri: makeSelectIsUriResolving(props.uri)(state), + rewardedContentClaimIds: selectRewardContentClaimIds(state, props), +}); const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), resolveUri: uri => dispatch(doResolveUri(uri)), }); -export default connect(makeSelect, perform)(FileTile); +export default connect(select, perform)(FileTile); diff --git a/ui/js/component/fileTile/view.jsx b/ui/js/component/fileTile/view.jsx index c57172408..1abd44952 100644 --- a/ui/js/component/fileTile/view.jsx +++ b/ui/js/component/fileTile/view.jsx @@ -1,6 +1,7 @@ import React from "react"; import lbryuri from "lbryuri.js"; import CardMedia from "component/cardMedia"; +import FileActions from "component/fileActions"; import Link from "component/link"; import { TruncatedText } from "component/common.js"; import FilePrice from "component/filePrice"; @@ -53,6 +54,7 @@ class FileTile extends React.PureComponent { render() { const { claim, + showActions, metadata, isResolvingUri, showEmpty, @@ -104,7 +106,7 @@ class FileTile extends React.PureComponent { onMouseEnter={this.handleMouseOver.bind(this)} onMouseLeave={this.handleMouseOut.bind(this)} > - +
@@ -116,14 +118,15 @@ class FileTile extends React.PureComponent { {title}
-
- - {description} - -
+ {description && +
+ + {description} + +
}
- +
{this.state.showNsfwHelp && } ); diff --git a/ui/js/component/form.js b/ui/js/component/form.js index d47d7445a..7eaac87e4 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -1,5 +1,6 @@ import React from "react"; import FormField from "component/formField"; +import { Icon } from "component/common.js"; let formFieldCounter = 0; @@ -9,6 +10,29 @@ export function formFieldId() { return "form-field-" + ++formFieldCounter; } +export class Form extends React.PureComponent { + static propTypes = { + onSubmit: React.PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.onSubmit(); + } + + render() { + return ( +
this.handleSubmit(event)}> + {this.props.children} +
+ ); + } +} + export class FormRow extends React.PureComponent { static propTypes = { label: React.PropTypes.oneOfType([ @@ -74,11 +98,6 @@ export class FormRow extends React.PureComponent { } getOptions() { - if (!this._field || !this._field.getOptions) { - console.log(this); - console.log(this._field); - console.log(this._field.getOptions); - } return this._field.getOptions(); } @@ -136,3 +155,27 @@ export class FormRow extends React.PureComponent { ); } } + +export const Submit = props => { + const { title, label, icon, disabled } = props; + + const className = + "button-block" + + " button-primary" + + " button-set-item" + + " button--submit" + + (disabled ? " disabled" : ""); + + const content = ( + + {"icon" in props ? : null} + {label ? {label} : null} + + ); + + return ( + + ); +}; diff --git a/ui/js/component/inviteList/view.jsx b/ui/js/component/inviteList/view.jsx index 8d748116a..8e6e0c938 100644 --- a/ui/js/component/inviteList/view.jsx +++ b/ui/js/component/inviteList/view.jsx @@ -47,9 +47,9 @@ class InviteList extends React.PureComponent { {invitee.invite_reward_claimed ? - : invitee.invite_accepted + : invitee.invite_reward_claimable ? : diff --git a/ui/js/component/inviteNew/view.jsx b/ui/js/component/inviteNew/view.jsx index 3f37e9c1b..af15a5fd2 100644 --- a/ui/js/component/inviteNew/view.jsx +++ b/ui/js/component/inviteNew/view.jsx @@ -1,7 +1,6 @@ import React from "react"; import { BusyMessage, CreditAmount } from "component/common"; -import Link from "component/link"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; class FormInviteNew extends React.PureComponent { constructor(props) { @@ -18,16 +17,16 @@ class FormInviteNew extends React.PureComponent { }); } - handleSubmit(event) { - event.preventDefault(); - this.props.inviteNew(this.state.email); + handleSubmit() { + const { email } = this.state; + this.props.inviteNew(email); } render() { const { errorMessage, isPending } = this.props; return ( -
+
- { - this.handleSubmit(event); - }} - /> +
- + ); } } diff --git a/ui/js/component/link/index.js b/ui/js/component/link/index.js index 5847c97d9..37340987a 100644 --- a/ui/js/component/link/index.js +++ b/ui/js/component/link/index.js @@ -4,7 +4,7 @@ import { doNavigate } from "actions/navigation"; import Link from "./view"; const perform = dispatch => ({ - doNavigate: path => dispatch(doNavigate(path)), + doNavigate: (path, params) => dispatch(doNavigate(path, params)), }); export default connect(null, perform)(Link); diff --git a/ui/js/component/link/view.jsx b/ui/js/component/link/view.jsx index 6760c6cd3..1ba8f4b52 100644 --- a/ui/js/component/link/view.jsx +++ b/ui/js/component/link/view.jsx @@ -8,11 +8,11 @@ const Link = props => { style, label, icon, - badge, button, disabled, children, navigate, + navigateParams, doNavigate, } = props; @@ -24,7 +24,7 @@ const Link = props => { const onClick = !props.onClick && navigate ? () => { - doNavigate(navigate); + doNavigate(navigate, navigateParams || {}); } : props.onClick; @@ -36,7 +36,6 @@ const Link = props => { {"icon" in props ? : null} {label ? {label} : null} - {"badge" in props ? {badge} : null} ); } diff --git a/ui/js/component/linkTransaction/index.js b/ui/js/component/linkTransaction/index.js index 601927420..9983f1bfc 100644 --- a/ui/js/component/linkTransaction/index.js +++ b/ui/js/component/linkTransaction/index.js @@ -1,5 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import Link from "./view"; +import LinkTransaction from "./view"; -export default connect(null, null)(Link); +export default connect(null, null)(LinkTransaction); diff --git a/ui/js/component/publishForm/view.jsx b/ui/js/component/publishForm/view.jsx index fd0886935..b502f4add 100644 --- a/ui/js/component/publishForm/view.jsx +++ b/ui/js/component/publishForm/view.jsx @@ -2,7 +2,7 @@ import React from "react"; import lbry from "lbry"; import lbryuri from "lbryuri"; import FormField from "component/formField"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; import Link from "component/link"; import FormFieldPrice from "component/formFieldPrice"; import Modal from "modal/modal"; @@ -19,6 +19,7 @@ class PublishForm extends React.PureComponent { this._defaultPaidPrice = 0.01; this.state = { + id: null, rawName: "", name: "", bid: 10, @@ -48,6 +49,7 @@ class PublishForm extends React.PureComponent { isFee: false, customUrl: false, source: null, + mode: "publish", }; } @@ -57,11 +59,7 @@ class PublishForm extends React.PureComponent { if (!fetchingChannels) fetchChannelListMine(); } - handleSubmit(event) { - if (typeof event !== "undefined") { - event.preventDefault(); - } - + handleSubmit() { this.setState({ submitting: true, }); @@ -116,7 +114,7 @@ class PublishForm extends React.PureComponent { ? { channel_name: this.state.channel } : {}), }; - + const { source } = this.state; if (this.refs.file.getValue() !== "") { @@ -187,6 +185,14 @@ class PublishForm extends React.PureComponent { return !!myClaims.find(claim => claim.name === name); } + handleEditClaim() { + const claimInfo = this.claim() || this.myClaimInfo(); + + if (claimInfo) { + this.handlePrefillClaim(claimInfo); + } + } + topClaimIsMine() { const myClaimInfo = this.myClaimInfo(); const { claimsByUri } = this.props; @@ -203,10 +209,10 @@ class PublishForm extends React.PureComponent { } myClaimInfo() { - const { name } = this.state; + const { id } = this.state; return Object.values(this.props.myClaims).find( - claim => claim.name === name + claim => claim.claim_id === id ); } @@ -226,6 +232,7 @@ class PublishForm extends React.PureComponent { name: "", uri: "", prefillDone: false, + mode: "publish", }); return; @@ -247,6 +254,7 @@ class PublishForm extends React.PureComponent { rawName: rawName, name: name, prefillDone: false, + mode: "publish", uri, }); @@ -261,9 +269,10 @@ class PublishForm extends React.PureComponent { }); } - handlePrefillClicked() { - const claimInfo = this.myClaimInfo(); - const { source } = claimInfo.value.stream; + handlePrefillClaim(claimInfo) { + const { claim_id, name, channel_name, amount } = claimInfo; + const { source, metadata } = claimInfo.value.stream; + const { license, licenseUrl, @@ -272,16 +281,21 @@ class PublishForm extends React.PureComponent { description, language, nsfw, - } = claimInfo.value.stream.metadata; + } = metadata; let newState = { + id: claim_id, + channel: channel_name || "anonymous", + bid: amount, meta_title: title, meta_thumbnail: thumbnail, meta_description: description, meta_language: language, meta_nsfw: nsfw, + mode: "edit", prefillDone: true, - bid: claimInfo.amount, + rawName: name, + name, source, }; @@ -375,6 +389,7 @@ class PublishForm extends React.PureComponent { handleChannelChange(channelName) { this.setState({ + mode: "publish", channel: channelName, }); const nameChanged = () => this.nameChanged(this.state.rawName); @@ -412,6 +427,13 @@ class PublishForm extends React.PureComponent { componentWillMount() { this.props.fetchClaimListMine(); this._updateChannelList(); + + const { id } = this.props.params; + this.setState({ id }); + } + + componentDidMount() { + this.handleEditClaim(); } onFileChange() { @@ -436,37 +458,38 @@ class PublishForm extends React.PureComponent { } getNameBidHelpText() { - if (this.state.prefillDone) { + const { prefillDone, name, uri } = this.state; + const { resolvingUris } = this.props; + const claim = this.claim(); + + if (prefillDone) { return __("Existing claim data was prefilled"); } - if ( - this.state.uri && - this.props.resolvingUris.indexOf(this.state.uri) !== -1 && - this.claim() === undefined - ) { + if (uri && resolvingUris.indexOf(uri) !== -1 && claim === undefined) { return __("Checking..."); - } else if (!this.state.name) { + } else if (!name) { return __("Select a URL for this publish."); - } else if (!this.claim()) { + } else if (!claim) { return __("This URL is unused."); - } else if (this.myClaimExists() && !this.state.prefillDone) { + } else if (this.myClaimExists() && !prefillDone) { return ( {__("You already have a claim with this name.")}{" "} this.handlePrefillClicked()} + label={__("Edit existing claim")} + onClick={() => this.handleEditClaim()} /> ); - } else if (this.claim()) { - if (this.topClaimValue() === 1) { + } else if (claim) { + const topClaimValue = this.topClaimValue(); + if (topClaimValue === 1) { return ( {__( 'A deposit of at least one credit is required to win "%s". However, you can still get a permanent URL for any amount.', - this.state.name + name )} ); @@ -475,8 +498,8 @@ class PublishForm extends React.PureComponent { {__( 'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.', - this.topClaimValue(), - this.state.name + topClaimValue, + name )} ); @@ -493,17 +516,21 @@ class PublishForm extends React.PureComponent { } render() { + const { mode, submitting } = this.state; + const lbcInputHelp = __( "This LBC remains yours and the deposit can be undone at any time." ); + let submitLabel = !submitting ? __("Publish") : __("Publishing..."); + + if (mode === "edit") { + submitLabel = !submitting ? __("Update") : __("Updating..."); + } + return (
-
{ - this.handleSubmit(event); - }} - > +

{__("Content")}

@@ -837,14 +864,10 @@ class PublishForm extends React.PureComponent {
- { - this.handleSubmit(event); - }} disabled={ this.state.submitting || (this.state.uri && @@ -859,9 +882,8 @@ class PublishForm extends React.PureComponent { onClick={this.props.back} label={__("Cancel")} /> -
-
+ { search: , send: , settings: , - show: , + show: , wallet: , }); }; diff --git a/ui/js/component/theme/index.js b/ui/js/component/theme/index.js new file mode 100644 index 000000000..ad95b5297 --- /dev/null +++ b/ui/js/component/theme/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectThemePath } from "selectors/settings.js"; +import Theme from "./view"; + +const select = state => ({ + themePath: selectThemePath(state), +}); + +export default connect(select, null)(Theme); diff --git a/ui/js/component/theme/view.jsx b/ui/js/component/theme/view.jsx new file mode 100644 index 000000000..1ff7c8480 --- /dev/null +++ b/ui/js/component/theme/view.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const Theme = props => { + const { themePath } = props; + + if (!themePath) { + return null; + } + + return ( + + ); +}; + +export default Theme; diff --git a/ui/js/component/transactionList/index.js b/ui/js/component/transactionList/index.js index 9e2d9ec6b..5a6c10b6b 100644 --- a/ui/js/component/transactionList/index.js +++ b/ui/js/component/transactionList/index.js @@ -1,5 +1,15 @@ import React from "react"; import { connect } from "react-redux"; +import { doNavigate } from "actions/navigation"; +import { selectClaimedRewardsByTransactionId } from "selectors/rewards"; import TransactionList from "./view"; -export default connect(null, null)(TransactionList); +const select = state => ({ + rewards: selectClaimedRewardsByTransactionId(state), +}); + +const perform = dispatch => ({ + navigate: (path, params) => dispatch(doNavigate(path, params)), +}); + +export default connect(null, perform)(TransactionList); diff --git a/ui/js/component/transactionList/internal/TransactionListItem.jsx b/ui/js/component/transactionList/internal/TransactionListItem.jsx new file mode 100644 index 000000000..2a4ba28ae --- /dev/null +++ b/ui/js/component/transactionList/internal/TransactionListItem.jsx @@ -0,0 +1,78 @@ +import React from "react"; +import LinkTransaction from "component/linkTransaction"; +import { CreditAmount } from "component/common"; +import DateTime from "component/dateTime"; +import Link from "component/link"; +import lbryuri from "lbryuri"; + +class TransactionListItem extends React.PureComponent { + render() { + const { reward, transaction } = this.props; + const { + amount, + claim_id: claimId, + claim_name: name, + date, + fee, + txid, + type, + } = transaction; + + return ( + + + {date + ?
+ +
+ +
+
+ : + {__("(Transaction pending)")} + } + + + +
+ {fee != 0 && + } + + + {type} + + + {reward && + + {__("Reward: %s", reward.reward_title)} + } + {name && + claimId && + + {name} + } + + + + + + ); + } +} + +export default TransactionListItem; diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index 0ad7f4eeb..beb2f8514 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,57 +1,82 @@ import React from "react"; -import LinkTransaction from "component/linkTransaction"; -import { CreditAmount } from "component/common"; +import TransactionListItem from "./internal/TransactionListItem"; +import FormField from "component/formField"; -const TransactionList = props => { - const { emptyMessage, transactions } = props; +class TransactionList extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + filter: null, + }; + } + + handleFilterChanged(event) { + this.setState({ + filter: event.target.value, + }); + } + + filterTransaction(transaction) { + const { filter } = this.state; + + return !filter || filter == transaction.type; + } + + render() { + const { emptyMessage, rewards, transactions } = this.props; + + let transactionList = transactions.filter( + this.filterTransaction.bind(this) + ); - if (!transactions || !transactions.length) { return ( -
- {emptyMessage || __("No transactions to list.")} +
+ {(transactionList.length || this.state.filter) && + + {__("Filter")} {" "} + + + + + + + + + + + } + {!transactionList.length && +
+ {emptyMessage || __("No transactions to list.")} +
} + {Boolean(transactionList.length) && + + + + + + + + + + + + {transactionList.map(t => + + )} + +
{__("Date")}{__("Amount (Fee)")}{__("Type")} {__("Details")} {__("Transaction")}
}
); } - - return ( - - - - - - - - - - {transactions.map(item => { - return ( - - - - - - ); - })} - -
{__("Date")}{__("Amount")}{__("Transaction")}
- {item.date - ? item.date.toLocaleDateString() + - " " + - item.date.toLocaleTimeString() - : - {__("(Transaction pending)")} - } - - {" "} - - -
- ); -}; +} export default TransactionList; diff --git a/ui/js/component/uriIndicator/index.js b/ui/js/component/uriIndicator/index.js index 839f54575..a57c33b15 100644 --- a/ui/js/component/uriIndicator/index.js +++ b/ui/js/component/uriIndicator/index.js @@ -1,25 +1,19 @@ import React from "react"; import lbryuri from "lbryuri"; import { connect } from "react-redux"; -import { makeSelectIsResolvingForUri } from "selectors/content"; +import { doResolveUri } from "actions/content"; +import { makeSelectIsUriResolving } from "selectors/content"; import { makeSelectClaimForUri } from "selectors/claims"; import UriIndicator from "./view"; -const makeSelect = () => { - const selectClaim = makeSelectClaimForUri(), - selectIsResolving = makeSelectIsResolvingForUri(); - - const select = (state, props) => ({ - claim: selectClaim(state, props), - isResolvingUri: selectIsResolving(state, props), - uri: lbryuri.normalize(props.uri), - }); - - return select; -}; +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + isResolvingUri: makeSelectIsUriResolving(props.uri)(state), + uri: lbryuri.normalize(props.uri), +}); const perform = dispatch => ({ resolveUri: uri => dispatch(doResolveUri(uri)), }); -export default connect(makeSelect, perform)(UriIndicator); +export default connect(select, perform)(UriIndicator); diff --git a/ui/js/component/uriIndicator/view.jsx b/ui/js/component/uriIndicator/view.jsx index b69abd591..a6dcb1a50 100644 --- a/ui/js/component/uriIndicator/view.jsx +++ b/ui/js/component/uriIndicator/view.jsx @@ -1,5 +1,7 @@ import React from "react"; import { Icon } from "component/common"; +import Link from "component/link"; +import lbryuri from "lbryuri.js"; class UriIndicator extends React.PureComponent { componentWillMount() { @@ -19,7 +21,7 @@ class UriIndicator extends React.PureComponent { } render() { - const { claim, uri, isResolvingUri } = this.props; + const { claim, link, uri, isResolvingUri } = this.props; if (isResolvingUri && !claim) { return Validating...; @@ -33,21 +35,30 @@ class UriIndicator extends React.PureComponent { channel_name: channelName, has_signature: hasSignature, signature_is_valid: signatureIsValid, + value, } = claim; + const channelClaimId = + value && + value.publisherSignature && + value.publisherSignature.certificateId; if (!hasSignature || !channelName) { return Anonymous; } - let icon, modifier; + let icon, channelLink, modifier; + if (signatureIsValid) { modifier = "valid"; + channelLink = link + ? lbryuri.build({ channelName, claimId: channelClaimId }, false) + : false; } else { icon = "icon-times-circle"; modifier = "invalid"; } - return ( + const inner = ( {channelName} {" "} {!signatureIsValid @@ -58,6 +69,16 @@ class UriIndicator extends React.PureComponent { : ""} ); + + if (!channelLink) { + return inner; + } + + return ( + + {inner} + + ); } } diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 70ed0f45e..5b34e62e6 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -1,6 +1,6 @@ import React from "react"; import Link from "component/link"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; class UserEmailNew extends React.PureComponent { constructor(props) { @@ -17,20 +17,16 @@ class UserEmailNew extends React.PureComponent { }); } - handleSubmit(event) { - event.preventDefault(); - this.props.addUserEmail(this.state.email); + handleSubmit() { + const { email } = this.state; + this.props.addUserEmail(email); } render() { const { errorMessage, isPending } = this.props; return ( -
{ - this.handleSubmit(event); - }} - > +

{__( "This process is required to prevent abuse of the rewards program." @@ -53,16 +49,9 @@ class UserEmailNew extends React.PureComponent { }} />

- { - this.handleSubmit(event); - }} - /> +
-
+ ); } } diff --git a/ui/js/component/userEmailVerify/view.jsx b/ui/js/component/userEmailVerify/view.jsx index 47ed10bc9..1ec92202e 100644 --- a/ui/js/component/userEmailVerify/view.jsx +++ b/ui/js/component/userEmailVerify/view.jsx @@ -1,6 +1,6 @@ import React from "react"; import Link from "component/link"; -import { FormRow } from "component/form.js"; +import { Form, FormRow, Submit } from "component/form.js"; class UserEmailVerify extends React.PureComponent { constructor(props) { @@ -17,19 +17,15 @@ class UserEmailVerify extends React.PureComponent { }); } - handleSubmit(event) { - event.preventDefault(); - this.props.verifyUserEmail(this.state.code); + handleSubmit() { + const { code } = this.state; + this.props.verifyUserEmail(code); } render() { const { errorMessage, isPending } = this.props; return ( -
{ - this.handleSubmit(event); - }} - > +

{__("Please enter the verification code emailed to you.")}

- { - this.handleSubmit(event); - }} - /> +
- + ); } } diff --git a/ui/js/component/video/index.js b/ui/js/component/video/index.js index f28fb94b8..3c80869dc 100644 --- a/ui/js/component/video/index.js +++ b/ui/js/component/video/index.js @@ -1,9 +1,8 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal } from "actions/app"; import { doChangeVolume } from "actions/app"; -import { selectCurrentModal, selectVolume } from "selectors/app"; -import { doPurchaseUri, doLoadVideo } from "actions/content"; +import { selectVolume } from "selectors/app"; +import { doPlayUri, doSetPlayingUri } from "actions/content"; import { makeSelectMetadataForUri, makeSelectContentTypeForUri, @@ -16,35 +15,24 @@ import { import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { selectShowNsfw } from "selectors/settings"; import Video from "./view"; +import { selectPlayingUri } from "selectors/content"; -const makeSelect = () => { - const selectCostInfo = makeSelectCostInfoForUri(); - const selectFileInfo = makeSelectFileInfoForUri(); - const selectIsLoading = makeSelectLoadingForUri(); - const selectIsDownloading = makeSelectDownloadingForUri(); - const selectMetadata = makeSelectMetadataForUri(); - const selectContentType = makeSelectContentTypeForUri(); - - const select = (state, props) => ({ - costInfo: selectCostInfo(state, props), - fileInfo: selectFileInfo(state, props), - metadata: selectMetadata(state, props), - obscureNsfw: !selectShowNsfw(state), - modal: selectCurrentModal(state), - isLoading: selectIsLoading(state, props), - isDownloading: selectIsDownloading(state, props), - contentType: selectContentType(state, props), - volume: selectVolume(state, props), - }); - - return select; -}; +const select = (state, props) => ({ + costInfo: makeSelectCostInfoForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + metadata: makeSelectMetadataForUri(props.uri)(state), + obscureNsfw: !selectShowNsfw(state), + isLoading: makeSelectLoadingForUri(props.uri)(state), + isDownloading: makeSelectDownloadingForUri(props.uri)(state), + playingUri: selectPlayingUri(state), + contentType: makeSelectContentTypeForUri(props.uri)(state), + volume: selectVolume(state), +}); const perform = dispatch => ({ - loadVideo: uri => dispatch(doLoadVideo(uri)), - purchaseUri: uri => dispatch(doPurchaseUri(uri, "affirmPurchaseAndPlay")), - closeModal: () => dispatch(doCloseModal()), + play: uri => dispatch(doPlayUri(uri)), + cancelPlay: () => dispatch(doSetPlayingUri(null)), changeVolume: volume => dispatch(doChangeVolume(volume)), }); -export default connect(makeSelect, perform)(Video); +export default connect(select, perform)(Video); diff --git a/ui/js/component/video/internal/play-button.jsx b/ui/js/component/video/internal/play-button.jsx index 2ae39fe7c..73905f298 100644 --- a/ui/js/component/video/internal/play-button.jsx +++ b/ui/js/component/video/internal/play-button.jsx @@ -1,7 +1,5 @@ import React from "react"; -import FilePrice from "component/filePrice"; import Link from "component/link"; -import Modal from "modal/modal"; class VideoPlayButton extends React.PureComponent { componentDidMount() { @@ -13,43 +11,22 @@ class VideoPlayButton extends React.PureComponent { document.removeEventListener("keydown", this.keyDownListener); } - onPurchaseConfirmed() { - this.props.closeModal(); - this.props.startPlaying(); - this.props.loadVideo(this.props.uri); - } - onKeyDown(event) { if ( "input" !== event.target.tagName.toLowerCase() && "Space" === event.code ) { event.preventDefault(); - this.onWatchClick(); + this.watch(); } } - onWatchClick() { - this.props.purchaseUri(this.props.uri).then(() => { - if (!this.props.modal) { - this.props.startPlaying(); - } - }); + watch() { + this.props.play(this.props.uri); } render() { - const { - button, - label, - metadata, - metadata: { title }, - uri, - modal, - closeModal, - isLoading, - fileInfo, - mediaType, - } = this.props; + const { button, label, isLoading, fileInfo, mediaType } = this.props; /* title={ @@ -65,36 +42,14 @@ class VideoPlayButton extends React.PureComponent { : "icon-folder-o"; return ( -
- - - {__("This will purchase")} {title} {__("for")}{" "} - - - {" "} - {__("credits")}. - - - {__("Sorry, your download timed out :(")} - -
+ this.watch()} + /> ); } } diff --git a/ui/js/component/video/view.jsx b/ui/js/component/video/view.jsx index 4e396ddd4..fb1bd3420 100644 --- a/ui/js/component/video/view.jsx +++ b/ui/js/component/video/view.jsx @@ -9,20 +9,12 @@ class Video extends React.PureComponent { constructor(props) { super(props); this.state = { - isPlaying: false, showNsfwHelp: false, }; } - componentWillReceiveProps(nextProps) { - // reset playing state upon change path action - if ( - !this.isMediaSame(nextProps) && - this.props.fileInfo && - this.state.isPlaying - ) { - this.state.isPlaying = false; - } + componentWillUnmount() { + this.props.cancelPlay(); } isMediaSame(nextProps) { @@ -33,12 +25,6 @@ class Video extends React.PureComponent { ); } - startPlaying() { - this.setState({ - isPlaying: true, - }); - } - handleMouseOver() { if ( this.props.obscureNsfw && @@ -64,13 +50,15 @@ class Video extends React.PureComponent { metadata, isLoading, isDownloading, + playingUri, fileInfo, contentType, changeVolume, volume, + uri, } = this.props; - const { isPlaying = false } = this.state; + const isPlaying = playingUri === uri; const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const mediaType = lbry.getMediaType( @@ -129,11 +117,7 @@ class Video extends React.PureComponent { className="video__cover" style={{ backgroundImage: 'url("' + metadata.thumbnail + '")' }} > - +
} {this.state.showNsfwHelp && } diff --git a/ui/js/component/walletSend/index.js b/ui/js/component/walletSend/index.js index 57e761c30..d396dc92c 100644 --- a/ui/js/component/walletSend/index.js +++ b/ui/js/component/walletSend/index.js @@ -1,12 +1,10 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal } from "actions/app"; import { doSendDraftTransaction, doSetDraftTransactionAmount, doSetDraftTransactionAddress, } from "actions/wallet"; -import { selectCurrentModal } from "selectors/app"; import { selectDraftTransactionAmount, selectDraftTransactionAddress, @@ -16,14 +14,12 @@ import { import WalletSend from "./view"; const select = state => ({ - modal: selectCurrentModal(state), address: selectDraftTransactionAddress(state), amount: selectDraftTransactionAmount(state), error: selectDraftTransactionError(state), }); const perform = dispatch => ({ - closeModal: () => dispatch(doCloseModal()), sendToAddress: () => dispatch(doSendDraftTransaction()), setAmount: event => dispatch(doSetDraftTransactionAmount(event.target.value)), setAddress: event => diff --git a/ui/js/component/walletSend/view.jsx b/ui/js/component/walletSend/view.jsx index 2751c42ab..00beb2a86 100644 --- a/ui/js/component/walletSend/view.jsx +++ b/ui/js/component/walletSend/view.jsx @@ -1,90 +1,69 @@ import React from "react"; -import Link from "component/link"; -import Modal from "modal/modal"; -import { FormRow } from "component/form"; +import { Form, FormRow, Submit } from "component/form"; import lbryuri from "lbryuri"; -const WalletSend = props => { - const { - sendToAddress, - closeModal, - modal, - setAmount, - setAddress, - amount, - address, - error, - } = props; +class WalletSend extends React.PureComponent { + handleSubmit() { + const { amount, address, sendToAddress } = this.props; + const validSubmit = parseFloat(amount) > 0.0 && address; - return ( -
-
-
-

{__("Send Credits")}

-
-
- -
-
- -
- 0.0) || !address} - /> - + if (validSubmit) { + sendToAddress(); + } + } + + render() { + const { + closeModal, + modal, + setAmount, + setAddress, + amount, + address, + error, + } = this.props; + + return ( +
+ +
+

{__("Send Credits")}

-
- - {modal == "insufficientBalance" && - - {__( - "Insufficient balance: after this transaction you would have less than 1 LBC in your wallet." - )} - } - {modal == "transactionSuccessful" && - - {__("Your transaction was successfully placed in the queue.")} - } - {modal == "transactionFailed" && - - {error} - } -
- ); -}; +
+ +
+
+ +
+ 0.0) || !address} + /> +
+
+ + + ); + } +} export default WalletSend; diff --git a/ui/js/component/walletSendTip/index.js b/ui/js/component/walletSendTip/index.js new file mode 100644 index 000000000..6d3275321 --- /dev/null +++ b/ui/js/component/walletSendTip/index.js @@ -0,0 +1,18 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doSendSupport } from "actions/wallet"; +import WalletSendTip from "./view"; +import { makeSelectTitleForUri } from "selectors/claims"; +import { selectIsSendingSupport } from "selectors/wallet"; + +const select = (state, props) => ({ + isPending: selectIsSendingSupport(state), + title: makeSelectTitleForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + sendSupport: (amount, claim_id, uri) => + dispatch(doSendSupport(amount, claim_id, uri)), +}); + +export default connect(select, perform)(WalletSendTip); diff --git a/ui/js/component/walletSendTip/view.jsx b/ui/js/component/walletSendTip/view.jsx new file mode 100644 index 000000000..9b8ce0338 --- /dev/null +++ b/ui/js/component/walletSendTip/view.jsx @@ -0,0 +1,79 @@ +import React from "react"; +import Link from "component/link"; +import { FormRow } from "component/form"; +import UriIndicator from "component/uriIndicator"; + +class WalletSendTip extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + amount: 0.0, + }; + } + + handleSendButtonClicked() { + const { claim_id, uri } = this.props; + let amount = this.state.amount; + this.props.sendSupport(amount, claim_id, uri); + } + + handleSupportPriceChange(event) { + this.setState({ + amount: Number(event.target.value), + }); + } + + render() { + const { errorMessage, isPending, title, uri } = this.props; + + return ( +
+
+

{__("Support")}

+
+
+ + {__( + 'This will appear as a tip for "%s" located at %s.', + title, + uri + ) + " "} + + + } + placeholder="1.00" + onChange={event => this.handleSupportPriceChange(event)} + /> +
+ + +
+
+
+ ); + } +} + +export default WalletSendTip; diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 099d4c563..140509461 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -9,7 +9,6 @@ export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH"; export const VOLUME_CHANGED = "VOLUME_CHANGED"; // Navigation -export const CHANGE_PATH = "CHANGE_PATH"; export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; export const WINDOW_SCROLLED = "WINDOW_SCROLLED"; export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE"; @@ -40,8 +39,11 @@ export const SEND_TRANSACTION_STARTED = "SEND_TRANSACTION_STARTED"; export const SEND_TRANSACTION_COMPLETED = "SEND_TRANSACTION_COMPLETED"; export const SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED"; export const FETCH_BLOCK_SUCCESS = "FETCH_BLOCK_SUCCESS"; +export const SUPPORT_TRANSACTION_STARTED = "SUPPORT_TRANSACTION_STARTED"; +export const SUPPORT_TRANSACTION_COMPLETED = "SUPPORT_TRANSACTION_COMPLETED"; +export const SUPPORT_TRANSACTION_FAILED = "SUPPORT_TRANSACTION_FAILED"; -// Content +// Claims export const FETCH_FEATURED_CONTENT_STARTED = "FETCH_FEATURED_CONTENT_STARTED"; export const FETCH_FEATURED_CONTENT_COMPLETED = "FETCH_FEATURED_CONTENT_COMPLETED"; @@ -57,6 +59,20 @@ export const FETCH_CHANNEL_CLAIM_COUNT_COMPLETED = export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED"; export const FETCH_CLAIM_LIST_MINE_COMPLETED = "FETCH_CLAIM_LIST_MINE_COMPLETED"; +export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED"; +export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED"; +export const FETCH_CHANNEL_LIST_MINE_STARTED = + "FETCH_CHANNEL_LIST_MINE_STARTED"; +export const FETCH_CHANNEL_LIST_MINE_COMPLETED = + "FETCH_CHANNEL_LIST_MINE_COMPLETED"; +export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED"; +export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED"; +export const PUBLISH_STARTED = "PUBLISH_STARTED"; +export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED"; +export const PUBLISH_FAILED = "PUBLISH_FAILED"; +export const SET_PLAYING_URI = "PLAY_URI"; + +// Files export const FILE_LIST_STARTED = "FILE_LIST_STARTED"; export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED"; export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED"; @@ -73,17 +89,6 @@ export const PLAY_VIDEO_STARTED = "PLAY_VIDEO_STARTED"; export const FETCH_AVAILABILITY_STARTED = "FETCH_AVAILABILITY_STARTED"; export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED"; export const FILE_DELETE = "FILE_DELETE"; -export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED"; -export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED"; -export const FETCH_CHANNEL_LIST_MINE_STARTED = - "FETCH_CHANNEL_LIST_MINE_STARTED"; -export const FETCH_CHANNEL_LIST_MINE_COMPLETED = - "FETCH_CHANNEL_LIST_MINE_COMPLETED"; -export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED"; -export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED"; -export const PUBLISH_STARTED = "PUBLISH_STARTED"; -export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED"; -export const PUBLISH_FAILED = "PUBLISH_FAILED"; // Search export const SEARCH_STARTED = "SEARCH_STARTED"; diff --git a/ui/js/constants/modal_types.js b/ui/js/constants/modal_types.js index 7dd057046..9e82be50a 100644 --- a/ui/js/constants/modal_types.js +++ b/ui/js/constants/modal_types.js @@ -1,5 +1,6 @@ export const CONFIRM_FILE_REMOVE = "confirmFileRemove"; export const INCOMPATIBLE_DAEMON = "incompatibleDaemon"; +export const FILE_TIMEOUT = "file_timeout"; export const DOWNLOADING = "downloading"; export const ERROR = "error"; export const INSUFFICIENT_CREDITS = "insufficient_credits"; @@ -7,5 +8,8 @@ export const UPGRADE = "upgrade"; export const WELCOME = "welcome"; export const FIRST_REWARD = "first_reward"; export const AUTHENTICATION_FAILURE = "auth_failure"; +export const TRANSACTION_FAILED = "transaction_failed"; +export const INSUFFICIENT_BALANCE = "insufficient_balance"; export const REWARD_APPROVAL_REQUIRED = "reward_approval_required"; +export const AFFIRM_PURCHASE = "affirm_purchase"; export const CREDIT_INTRO = "credit_intro"; diff --git a/ui/js/constants/settings.js b/ui/js/constants/settings.js index 146c882cc..a1a2e04e2 100644 --- a/ui/js/constants/settings.js +++ b/ui/js/constants/settings.js @@ -6,3 +6,5 @@ export const NEW_USER_ACKNOWLEDGED = "welcome_acknowledged"; export const LANGUAGE = "language"; export const SHOW_NSFW = "showNsfw"; export const SHOW_UNAVAILABLE = "showUnavailable"; +export const THEME = "theme"; +export const THEMES = "themes"; diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 23cf16c20..767d4479c 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -39,6 +39,8 @@ let lbry = { customLighthouseServers: [], showDeveloperMenu: false, language: "en", + theme: "light", + themes: [], }, }; @@ -174,60 +176,6 @@ lbry.connect = function() { return lbry._connectPromise; }; -/** - * Takes a LBRY URI; will first try and calculate a total cost using - * Lighthouse. If Lighthouse can't be reached, it just retrives the - * key fee. - * - * Returns an object with members: - * - cost: Number; the calculated cost of the name - * - includes_data: Boolean; indicates whether or not the data fee info - * from Lighthouse is included. - */ -lbry.costPromiseCache = {}; -lbry.getCostInfo = function(uri) { - if (lbry.costPromiseCache[uri] === undefined) { - lbry.costPromiseCache[uri] = new Promise((resolve, reject) => { - const COST_INFO_CACHE_KEY = "cost_info_cache"; - let costInfoCache = getSession(COST_INFO_CACHE_KEY, {}); - - function cacheAndResolve(cost, includesData) { - costInfoCache[uri] = { cost, includesData }; - setSession(COST_INFO_CACHE_KEY, costInfoCache); - resolve({ cost, includesData }); - } - - if (!uri) { - return reject(new Error(`URI required.`)); - } - - if (costInfoCache[uri] && costInfoCache[uri].cost) { - return resolve(costInfoCache[uri]); - } - - function getCost(uri, size) { - lbry - .stream_cost_estimate({ uri, ...(size !== null ? { size } : {}) }) - .then(cost => { - cacheAndResolve(cost, size !== null); - }, reject); - } - - const uriObj = lbryuri.parse(uri); - const name = uriObj.path || uriObj.name; - - lighthouse.get_size_for_name(name).then(size => { - if (size) { - getCost(name, size); - } else { - getCost(name, null); - } - }); - }); - } - return lbry.costPromiseCache[uri]; -}; - /** * Publishes a file. The optional fileListedCallback is called when the file becomes available in * lbry.file_list() during the publish process. @@ -277,17 +225,6 @@ lbry.publishDeprecated = function( ); }; -lbry.getClientSettings = function() { - var outSettings = {}; - for (let setting of Object.keys(lbry.defaultClientSettings)) { - var localStorageVal = localStorage.getItem("setting_" + setting); - outSettings[setting] = localStorageVal === null - ? lbry.defaultClientSettings[setting] - : JSON.parse(localStorageVal); - } - return outSettings; -}; - lbry.getClientSetting = function(setting) { var localStorageVal = localStorage.getItem("setting_" + setting); if (setting == "showDeveloperMenu") { @@ -298,12 +235,6 @@ lbry.getClientSetting = function(setting) { : JSON.parse(localStorageVal); }; -lbry.setClientSettings = function(settings) { - for (let setting of Object.keys(settings)) { - lbry.setClientSetting(setting, settings[setting]); - } -}; - lbry.setClientSetting = function(setting, value) { return localStorage.setItem("setting_" + setting, JSON.stringify(value)); }; diff --git a/ui/js/modal/modalAffirmPurchase/index.js b/ui/js/modal/modalAffirmPurchase/index.js new file mode 100644 index 000000000..d0f4affa2 --- /dev/null +++ b/ui/js/modal/modalAffirmPurchase/index.js @@ -0,0 +1,21 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal } from "actions/app"; +import { doLoadVideo, doSetPlayingUri } from "actions/content"; +import { makeSelectMetadataForUri } from "selectors/claims"; +import ModalAffirmPurchase from "./view"; + +const select = (state, props) => ({ + metadata: makeSelectMetadataForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + cancelPurchase: () => { + dispatch(doSetPlayingUri(null)); + dispatch(doCloseModal()); + }, + closeModal: () => dispatch(doCloseModal()), + loadVideo: uri => dispatch(doLoadVideo(uri)), +}); + +export default connect(select, perform)(ModalAffirmPurchase); diff --git a/ui/js/modal/modalAffirmPurchase/view.jsx b/ui/js/modal/modalAffirmPurchase/view.jsx new file mode 100644 index 000000000..96dd6c1c9 --- /dev/null +++ b/ui/js/modal/modalAffirmPurchase/view.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import FilePrice from "component/filePrice"; +import { Modal } from "modal/modal"; + +class ModalAffirmPurchase extends React.PureComponent { + onAffirmPurchase() { + this.props.closeModal(); + this.props.loadVideo(this.props.uri); + } + + render() { + const { cancelPurchase, metadata: { title }, uri } = this.props; + + return ( + + {__("This will purchase")} {title} {__("for")}{" "} + + + {" "} + {__("credits")}. + + ); + } +} + +export default ModalAffirmPurchase; diff --git a/ui/js/modal/modalAuthFailure/index.js b/ui/js/modal/modalAuthFailure/index.js index 08acf62f8..1a7fbfa3a 100644 --- a/ui/js/modal/modalAuthFailure/index.js +++ b/ui/js/modal/modalAuthFailure/index.js @@ -9,4 +9,4 @@ const perform = dispatch => ({ close: () => dispatch(doCloseModal()), }); -export default connect(select, perform)(ModalAuthFailure); +export default connect(null, null)(ModalAuthFailure); diff --git a/ui/js/modal/modalError/index.js b/ui/js/modal/modalError/index.js index 47680ecc9..b1cb30f1b 100644 --- a/ui/js/modal/modalError/index.js +++ b/ui/js/modal/modalError/index.js @@ -1,16 +1,10 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentModal, selectModalExtraContent } from "selectors/app"; import { doCloseModal } from "actions/app"; import ModalError from "./view"; -const select = state => ({ - modal: selectCurrentModal(state), - error: selectModalExtraContent(state), -}); - const perform = dispatch => ({ closeModal: () => dispatch(doCloseModal()), }); -export default connect(select, perform)(ModalError); +export default connect(null, perform)(ModalError); diff --git a/ui/js/modal/modalError/view.jsx b/ui/js/modal/modalError/view.jsx index ceb998dee..bbae36a15 100644 --- a/ui/js/modal/modalError/view.jsx +++ b/ui/js/modal/modalError/view.jsx @@ -4,7 +4,7 @@ import { ExpandableModal } from "modal/modal"; class ModalError extends React.PureComponent { render() { - const { modal, closeModal, error } = this.props; + const { closeModal, error } = this.props; const errorObj = typeof error === "string" ? { message: error } : error; @@ -33,7 +33,7 @@ class ModalError extends React.PureComponent { return ( ({ + metadata: makeSelectMetadataForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(select, perform)(ModalFileTimeout); diff --git a/ui/js/modal/modalFileTimeout/view.jsx b/ui/js/modal/modalFileTimeout/view.jsx new file mode 100644 index 000000000..9a0916c66 --- /dev/null +++ b/ui/js/modal/modalFileTimeout/view.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import { Modal } from "modal/modal"; + +class ModalFileTimeout extends React.PureComponent { + render() { + const { metadata: { title } } = this.props; + + return ( + + {__("LBRY was unable to download the stream")}{" "} + {title}. + + ); + } +} + +export default ModalFileTimeout; diff --git a/ui/js/modal/modalInsufficientBalance/index.js b/ui/js/modal/modalInsufficientBalance/index.js new file mode 100644 index 000000000..5e94a9f9e --- /dev/null +++ b/ui/js/modal/modalInsufficientBalance/index.js @@ -0,0 +1,17 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal } from "actions/app"; +import { doNavigate } from "actions/navigation"; +import ModalInsufficientBalance from "./view"; + +const select = state => ({}); + +const perform = dispatch => ({ + addBalance: () => { + dispatch(doNavigate("/wallet")); + dispatch(doCloseModal()); + }, + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(select, perform)(ModalInsufficientBalance); diff --git a/ui/js/modal/modalInsufficientBalance/view.jsx b/ui/js/modal/modalInsufficientBalance/view.jsx new file mode 100644 index 000000000..5b43583cd --- /dev/null +++ b/ui/js/modal/modalInsufficientBalance/view.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import { Modal } from "modal/modal"; + +class ModalInsufficientBalance extends React.PureComponent { + render() { + const { addBalance, closeModal } = this.props; + + return ( + + {__( + "Insufficient balance: after this transaction you would have less than 0 LBCs in your wallet." + )} + + ); + } +} + +export default ModalInsufficientBalance; diff --git a/ui/js/modal/modalRemoveFile/index.js b/ui/js/modal/modalRemoveFile/index.js index f64c1987e..39ae89ceb 100644 --- a/ui/js/modal/modalRemoveFile/index.js +++ b/ui/js/modal/modalRemoveFile/index.js @@ -2,19 +2,15 @@ import React from "react"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; import { doDeleteFileAndGoBack } from "actions/file_info"; -import { makeSelectClaimForUriIsMine } from "selectors/claims"; - +import { makeSelectTitleForUri, makeSelectClaimIsMine } from "selectors/claims"; +import { makeSelectFileInfoForUri } from "selectors/file_info"; import ModalRemoveFile from "./view"; -const makeSelect = () => { - const selectClaimForUriIsMine = makeSelectClaimForUriIsMine(); - - const select = (state, props) => ({ - claimIsMine: selectClaimForUriIsMine(state, props), - }); - - return select; -}; +const select = (state, props) => ({ + claimIsMine: makeSelectClaimIsMine(props.uri)(state), + title: makeSelectTitleForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), +}); const perform = dispatch => ({ closeModal: () => dispatch(doCloseModal()), @@ -23,4 +19,4 @@ const perform = dispatch => ({ }, }); -export default connect(makeSelect, perform)(ModalRemoveFile); +export default connect(select, perform)(ModalRemoveFile); diff --git a/ui/js/modal/modalRemoveFile/view.jsx b/ui/js/modal/modalRemoveFile/view.jsx index 6dfa9152f..c18acb6f2 100644 --- a/ui/js/modal/modalRemoveFile/view.jsx +++ b/ui/js/modal/modalRemoveFile/view.jsx @@ -25,7 +25,13 @@ class ModalRemoveFile extends React.PureComponent { } render() { - const { claimIsMine, closeModal, deleteFile, outpoint, title } = this.props; + const { + claimIsMine, + closeModal, + deleteFile, + fileInfo: { outpoint }, + title, + } = this.props; const { deleteChecked, abandonClaimChecked } = this.state; return ( diff --git a/ui/js/modal/modalRouter/index.js b/ui/js/modal/modalRouter/index.js index 79f02385f..c0c637167 100644 --- a/ui/js/modal/modalRouter/index.js +++ b/ui/js/modal/modalRouter/index.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import { doOpenModal } from "actions/app"; import * as settings from "constants/settings"; -import { selectCurrentModal } from "selectors/app"; +import { selectCurrentModal, selectModalProps } from "selectors/app"; import { selectCurrentPage } from "selectors/navigation"; import { selectCostForCurrentPageUri } from "selectors/cost_info"; import { makeSelectClientSetting } from "selectors/settings"; @@ -14,6 +14,7 @@ const select = (state, props) => ({ balance: selectBalance(state), showPageCost: selectCostForCurrentPageUri(state), modal: selectCurrentModal(state), + modalProps: selectModalProps(state), page: selectCurrentPage(state), isWelcomeAcknowledged: makeSelectClientSetting( settings.NEW_USER_ACKNOWLEDGED diff --git a/ui/js/modal/modalRouter/view.jsx b/ui/js/modal/modalRouter/view.jsx index a21851c3b..8d2bf912e 100644 --- a/ui/js/modal/modalRouter/view.jsx +++ b/ui/js/modal/modalRouter/view.jsx @@ -7,8 +7,13 @@ import ModalUpgrade from "modal/modalUpgrade"; import ModalWelcome from "modal/modalWelcome"; import ModalFirstReward from "modal/modalFirstReward"; import ModalRewardApprovalRequired from "modal/modalRewardApprovalRequired"; -import * as modals from "constants/modal_types"; import ModalCreditIntro from "modal/modalCreditIntro"; +import ModalRemoveFile from "modal/modalRemoveFile"; +import ModalTransactionFailed from "modal/modalTransactionFailed"; +import ModalInsufficientBalance from "modal/modalInsufficientBalance"; +import ModalFileTimeout from "modal/modalFileTimeout"; +import ModalAffirmPurchase from "modal/modalAffirmPurchase"; +import * as modals from "constants/modal_types"; class ModalRouter extends React.PureComponent { constructor(props) { @@ -29,7 +34,7 @@ class ModalRouter extends React.PureComponent { } showTransitionModals(props) { - const { modal, openModal, page } = props; + const { modal, modalProps, openModal, page } = props; if (modal) { return; @@ -96,27 +101,37 @@ class ModalRouter extends React.PureComponent { } render() { - const { modal } = this.props; + const { modal, modalProps } = this.props; switch (modal) { case modals.UPGRADE: - return ; + return ; case modals.DOWNLOADING: - return ; + return ; case modals.ERROR: - return ; + return ; + case modals.FILE_TIMEOUT: + return ; case modals.INSUFFICIENT_CREDITS: - return ; + return ; case modals.WELCOME: - return ; + return ; case modals.FIRST_REWARD: - return ; + return ; case modals.AUTHENTICATION_FAILURE: - return ; + return ; case modals.CREDIT_INTRO: - return ; + return ; + case modals.TRANSACTION_FAILED: + return ; + case modals.INSUFFICIENT_BALANCE: + return ; case modals.REWARD_APPROVAL_REQUIRED: - return ; + return ; + case modals.CONFIRM_FILE_REMOVE: + return ; + case modals.AFFIRM_PURCHASE: + return ; default: return null; } diff --git a/ui/js/modal/modalTransactionFailed/index.js b/ui/js/modal/modalTransactionFailed/index.js new file mode 100644 index 000000000..4b370a7c8 --- /dev/null +++ b/ui/js/modal/modalTransactionFailed/index.js @@ -0,0 +1,12 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal } from "actions/app"; +import ModalTransactionFailed from "./view"; + +const select = state => ({}); + +const perform = dispatch => ({ + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(select, perform)(ModalTransactionFailed); diff --git a/ui/js/modal/modalTransactionFailed/view.jsx b/ui/js/modal/modalTransactionFailed/view.jsx new file mode 100644 index 000000000..b2d4a3558 --- /dev/null +++ b/ui/js/modal/modalTransactionFailed/view.jsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Modal } from "modal/modal"; + +class ModalTransactionFailed extends React.PureComponent { + render() { + const { closeModal } = this.props; + + return ( + + {__("Something went wrong")}: + + ); + } +} + +export default ModalTransactionFailed; diff --git a/ui/js/page/channel/index.js b/ui/js/page/channel/index.js index 95134bdc3..f69d5b15e 100644 --- a/ui/js/page/channel/index.js +++ b/ui/js/page/channel/index.js @@ -9,27 +9,22 @@ import { makeSelectClaimsInChannelForCurrentPage, makeSelectFetchingChannelClaims, } from "selectors/claims"; -import { selectCurrentParams } from "selectors/navigation"; +import { + makeSelectCurrentParam, + selectCurrentParams, +} from "selectors/navigation"; import { doNavigate } from "actions/navigation"; import { makeSelectTotalPagesForChannel } from "selectors/content"; import ChannelPage from "./view"; -const makeSelect = () => { - const selectClaim = makeSelectClaimForUri(), - selectClaimsInChannel = makeSelectClaimsInChannelForCurrentPage(), - selectFetchingChannelClaims = makeSelectFetchingChannelClaims(), - selectTotalPagesForChannel = makeSelectTotalPagesForChannel(); - - const select = (state, props) => ({ - claim: selectClaim(state, props), - claimsInChannel: selectClaimsInChannel(state, props), - fetching: selectFetchingChannelClaims(state, props), - totalPages: selectTotalPagesForChannel(state, props), - params: selectCurrentParams(state), - }); - - return select; -}; +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + claimsInChannel: makeSelectClaimsInChannelForCurrentPage(props.uri)(state), + fetching: makeSelectFetchingChannelClaims(props.uri)(state), + page: makeSelectCurrentParam("page")(state), + params: selectCurrentParams(state), + totalPages: makeSelectTotalPagesForChannel(props.uri)(state), +}); const perform = dispatch => ({ fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)), @@ -37,4 +32,4 @@ const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), }); -export default connect(makeSelect, perform)(ChannelPage); +export default connect(select, perform)(ChannelPage); diff --git a/ui/js/page/channel/view.jsx b/ui/js/page/channel/view.jsx index 1d17fe8b5..f7ccd1c8f 100644 --- a/ui/js/page/channel/view.jsx +++ b/ui/js/page/channel/view.jsx @@ -2,31 +2,29 @@ import React from "react"; import lbryuri from "lbryuri"; import { BusyMessage } from "component/common"; import FileTile from "component/fileTile"; -import Link from "component/link"; import ReactPaginate from "react-paginate"; class ChannelPage extends React.PureComponent { componentDidMount() { - const { uri, params, fetchClaims, fetchClaimCount } = this.props; + const { uri, page, fetchClaims, fetchClaimCount } = this.props; - fetchClaims(uri, params.page || 1); + fetchClaims(uri, page || 1); fetchClaimCount(uri); } componentWillReceiveProps(nextProps) { - const { params, fetching, fetchClaims, fetchClaimCount } = this.props; - const nextParams = nextProps.params; + const { page, uri, fetching, fetchClaims, fetchClaimCount } = this.props; - if (fetching !== nextParams.page && params.page !== nextParams.page) { - fetchClaims(nextProps.uri, nextParams.page); + if (nextProps.page && page !== nextProps.page) { + fetchClaims(nextProps.uri, nextProps.page); } - if (nextProps.uri != this.props.uri) { + if (nextProps.uri != uri) { fetchClaimCount(uri); } } changePage(pageNumber) { - const { params, currentPage } = this.props; + const { params } = this.props; const newParams = Object.assign({}, params, { page: pageNumber }); this.props.navigate("/show", newParams); @@ -38,16 +36,15 @@ class ChannelPage extends React.PureComponent { claimsInChannel, claim, uri, - params, + page, totalPages, } = this.props; - const { page } = params; let contentList; - if (claimsInChannel === undefined) { + if (fetching) { contentList = ; - } else if (claimsInChannel) { - contentList = claimsInChannel.length + } else { + contentList = claimsInChannel && claimsInChannel.length ? claimsInChannel.map(claim => +
{!hasContent && fetchingFeaturedUris && } diff --git a/ui/js/page/file/index.js b/ui/js/page/file/index.js index 19cdde31f..e56391051 100644 --- a/ui/js/page/file/index.js +++ b/ui/js/page/file/index.js @@ -13,26 +13,18 @@ import { import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { selectShowNsfw } from "selectors/settings"; import FilePage from "./view"; +import { makeSelectCurrentParam } from "selectors/navigation"; -const makeSelect = () => { - const selectClaim = makeSelectClaimForUri(), - selectContentType = makeSelectContentTypeForUri(), - selectFileInfo = makeSelectFileInfoForUri(), - selectCostInfo = makeSelectCostInfoForUri(), - selectMetadata = makeSelectMetadataForUri(); - - const select = (state, props) => ({ - claim: selectClaim(state, props), - contentType: selectContentType(state, props), - costInfo: selectCostInfo(state, props), - metadata: selectMetadata(state, props), - obscureNsfw: !selectShowNsfw(state), - fileInfo: selectFileInfo(state, props), - rewardedContentClaimIds: selectRewardContentClaimIds(state, props), - }); - - return select; -}; +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + contentType: makeSelectContentTypeForUri(props.uri)(state), + costInfo: makeSelectCostInfoForUri(props.uri)(state), + metadata: makeSelectMetadataForUri(props.uri)(state), + obscureNsfw: !selectShowNsfw(state), + tab: makeSelectCurrentParam("tab")(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + rewardedContentClaimIds: selectRewardContentClaimIds(state, props), +}); const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), @@ -40,4 +32,4 @@ const perform = dispatch => ({ fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), }); -export default connect(makeSelect, perform)(FilePage); +export default connect(select, perform)(FilePage); diff --git a/ui/js/page/file/view.jsx b/ui/js/page/file/view.jsx index eb920a779..35209ae18 100644 --- a/ui/js/page/file/view.jsx +++ b/ui/js/page/file/view.jsx @@ -1,45 +1,13 @@ import React from "react"; -import ReactMarkdown from "react-markdown"; import lbry from "lbry.js"; import lbryuri from "lbryuri.js"; import Video from "component/video"; import { Thumbnail } from "component/common"; import FilePrice from "component/filePrice"; -import FileActions from "component/fileActions"; -import Link from "component/link"; +import FileDetails from "component/fileDetails"; import UriIndicator from "component/uriIndicator"; import IconFeatured from "component/iconFeatured"; -import DateTime from "component/dateTime"; - -const FormatItem = props => { - const { - publishedDate, - contentType, - claim: { height }, - metadata: { language, license }, - } = props; - - const mediaType = lbry.getMediaType(contentType); - - return ( - - - - - - - - - - - - - - - -
{__("Published on")}
{__("Content-Type")}{mediaType}
{__("Language")}{language}
{__("License")}{license}
- ); -}; +import WalletSendTip from "component/walletSendTip"; class FilePage extends React.PureComponent { componentDidMount() { @@ -69,34 +37,20 @@ class FilePage extends React.PureComponent { fileInfo, metadata, contentType, + tab, uri, rewardedContentClaimIds, } = this.props; + const showTipBox = tab == "tip"; + if (!claim || !metadata) { return ( {__("Empty claim or metadata info.")} ); } - const { - txid, - nout, - channel_name: channelName, - has_signature: hasSignature, - signature_is_valid: signatureIsValid, - value, - } = claim; - - const outpoint = txid + ":" + nout; const title = metadata.title; - const channelClaimId = claim.value && claim.value.publisherSignature - ? claim.value.publisherSignature.certificateId - : null; - const channelUri = signatureIsValid && hasSignature && channelName - ? lbryuri.build({ channelName, claimId: channelClaimId }, false) - : null; - const uriIndicator = ; const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); const mediaType = lbry.getMediaType(contentType); const player = require("render-media"); @@ -106,7 +60,7 @@ class FilePage extends React.PureComponent { mediaType === "audio"; return ( -
+
{isPlayable ?
-
- {!fileInfo || fileInfo.written_bytes <= 0 - ? - - {isRewardContent && {" "}} - - : null} -

{title}

-
- {channelUri - ? - this.props.navigate("/show", { uri: channelUri })} - > - {uriIndicator} - - : uriIndicator} -
-
- -
-
-
- -
-
- {metadata && claim - ?
- -
- : ""} -
- + {(!tab || tab === "details") && +
+ {" "} {" "} +
+ {!fileInfo || fileInfo.written_bytes <= 0 + ? + + {isRewardContent && {" "}} + + : null} +

{title}

+
+ +
+
+ +
} + {tab === "tip" && + }
-
+ ); } } diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 51d75e406..423d45faa 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -6,21 +6,27 @@ import SubHeader from "component/subHeader"; import Link from "component/link"; class RewardsPage extends React.PureComponent { - componentDidMount() { - this.fetchRewards(this.props); - } + /* + Below is broken for users who have claimed all rewards. - componentWillReceiveProps(nextProps) { - this.fetchRewards(nextProps); - } + It can safely be disabled since we fetch all rewards after authentication, but should be re-enabled once fixed. - fetchRewards(props) { - const { fetching, rewards, fetchRewards } = props; - - if (!fetching && (!rewards || !rewards.length)) { - fetchRewards(); - } - } + */ + // componentDidMount() { + // this.fetchRewards(this.props); + // } + // + // componentWillReceiveProps(nextProps) { + // this.fetchRewards(nextProps); + // } + // + // fetchRewards(props) { + // const { fetching, rewards, fetchRewards } = props; + // + // if (!fetching && (!rewards || !rewards.length)) { + // fetchRewards(); + // } + // } renderPageHeader() { const { doAuth, navigate, user } = this.props; diff --git a/ui/js/page/settings/index.js b/ui/js/page/settings/index.js index e97ae7e4e..a44bf670c 100644 --- a/ui/js/page/settings/index.js +++ b/ui/js/page/settings/index.js @@ -1,14 +1,17 @@ import React from "react"; import { connect } from "react-redux"; +import * as settings from "constants/settings"; import { doClearCache } from "actions/app"; import { doSetDaemonSetting, doSetClientSetting, + doGetThemes, + doSetTheme, doChangeLanguage, } from "actions/settings"; import { + makeSelectClientSetting, selectDaemonSettings, - selectShowNsfw, selectLanguages, } from "selectors/settings"; import { selectCurrentLanguage } from "selectors/app"; @@ -16,7 +19,10 @@ import SettingsPage from "./view"; const select = state => ({ daemonSettings: selectDaemonSettings(state), - showNsfw: selectShowNsfw(state), + showNsfw: makeSelectClientSetting(settings.SHOW_NSFW)(state), + showUnavailable: makeSelectClientSetting(settings.SHOW_UNAVAILABLE)(state), + theme: makeSelectClientSetting(settings.THEME)(state), + themes: makeSelectClientSetting(settings.THEMES)(state), language: selectCurrentLanguage(state), languages: selectLanguages(state), }); @@ -25,6 +31,7 @@ const perform = dispatch => ({ setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), clearCache: () => dispatch(doClearCache()), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), + getThemes: () => dispatch(doGetThemes()), changeLanguage: newLanguage => dispatch(doChangeLanguage(newLanguage)), }); diff --git a/ui/js/page/settings/view.jsx b/ui/js/page/settings/view.jsx index ad8c375b6..c09c1be13 100644 --- a/ui/js/page/settings/view.jsx +++ b/ui/js/page/settings/view.jsx @@ -6,20 +6,13 @@ import * as settings from "constants/settings"; import lbry from "lbry.js"; import Link from "component/link"; import FormFieldPrice from "component/formFieldPrice"; - -const { remote } = require("electron"); +import { remote } from "electron"; class SettingsPage extends React.PureComponent { constructor(props) { super(props); - const { daemonSettings } = this.props || {}; - this.state = { - // isMaxUpload: daemonSettings && daemonSettings.max_upload != 0, - // isMaxDownload: daemonSettings && daemonSettings.max_download != 0, - showUnavailable: lbry.getClientSetting(settings.SHOW_UNAVAILABLE), - language: lbry.getClientSetting(settings.LANGUAGE), clearingCache: false, }; } @@ -41,11 +34,6 @@ class SettingsPage extends React.PureComponent { this.props.setDaemonSetting(name, value); } - setClientSetting(name, value) { - lbry.setClientSetting(name, value); - this._onSettingSaveSuccess(); - } - onRunOnStartChange(event) { this.setDaemonSetting("run_on_startup", event.target.checked); } @@ -66,6 +54,11 @@ class SettingsPage extends React.PureComponent { this.setDaemonSetting("disable_max_key_fee", isDisabled); } + onThemeChange(event) { + const { value } = event.target; + this.props.setClientSetting(settings.THEME, value); + } + // onMaxUploadPrefChange(isLimited) { // if (!isLimited) { // this.setDaemonSetting("max_upload", 0.0); @@ -101,10 +94,29 @@ class SettingsPage extends React.PureComponent { this.forceUpdate(); } - onShowUnavailableChange(event) {} + onShowUnavailableChange(event) { + this.props.setClientSetting( + settings.SHOW_UNAVAILABLE, + event.target.checked + ); + } + + componentWillMount() { + this.props.getThemes(); + } + + componentDidMount() {} render() { - const { daemonSettings, language, languages } = this.props; + const { + daemonSettings, + language, + languages, + showNsfw, + showUnavailable, + theme, + themes, + } = this.props; if (!daemonSettings || Object.keys(daemonSettings).length === 0) { return ( @@ -209,7 +221,7 @@ class SettingsPage extends React.PureComponent { @@ -218,7 +230,7 @@ class SettingsPage extends React.PureComponent { label={__("Show NSFW content")} type="checkbox" onChange={this.onShowNsfwChange.bind(this)} - defaultChecked={this.props.showNsfw} + defaultChecked={showNsfw} helper={__( "NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. " )} @@ -242,6 +254,27 @@ class SettingsPage extends React.PureComponent { +
+
+

{__("Theme")}

+
+
+ + {themes.map((theme, index) => + + )} + + +
+
+

{__("Application Cache")}

diff --git a/ui/js/page/show/index.js b/ui/js/page/show/index.js index 6306e5e33..94b380381 100644 --- a/ui/js/page/show/index.js +++ b/ui/js/page/show/index.js @@ -2,23 +2,16 @@ import React from "react"; import { connect } from "react-redux"; import { doResolveUri } from "actions/content"; import { makeSelectClaimForUri } from "selectors/claims"; -import { makeSelectIsResolvingForUri } from "selectors/content"; +import { makeSelectIsUriResolving } from "selectors/content"; import ShowPage from "./view"; -const makeSelect = () => { - const selectClaim = makeSelectClaimForUri(), - selectIsResolving = makeSelectIsResolvingForUri(); - - const select = (state, props) => ({ - claim: selectClaim(state, props.params), - isResolvingUri: selectIsResolving(state, props.params), - }); - - return select; -}; +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + isResolvingUri: makeSelectIsUriResolving(props.uri)(state), +}); const perform = dispatch => ({ resolveUri: uri => dispatch(doResolveUri(uri)), }); -export default connect(makeSelect, perform)(ShowPage); +export default connect(select, perform)(ShowPage); diff --git a/ui/js/page/show/view.jsx b/ui/js/page/show/view.jsx index aae0aabfe..8b1e05789 100644 --- a/ui/js/page/show/view.jsx +++ b/ui/js/page/show/view.jsx @@ -6,15 +6,13 @@ import FilePage from "page/file"; class ShowPage extends React.PureComponent { componentWillMount() { - const { isResolvingUri, resolveUri, params } = this.props; - const { uri } = params; + const { isResolvingUri, resolveUri, uri } = this.props; if (!isResolvingUri) resolveUri(uri); } componentWillReceiveProps(nextProps) { - const { isResolvingUri, resolveUri, claim, params } = nextProps; - const { uri } = params; + const { isResolvingUri, resolveUri, claim, uri } = nextProps; if (!isResolvingUri && claim === undefined && uri) { resolveUri(uri); @@ -22,8 +20,7 @@ class ShowPage extends React.PureComponent { } render() { - const { claim, params, isResolvingUri } = this.props; - const { uri } = params; + const { claim, isResolvingUri, uri } = this.props; let innerContent = ""; @@ -39,6 +36,7 @@ class ShowPage extends React.PureComponent { message={__("Loading magic decentralized data...")} />} {claim === null && + !isResolvingUri && {__("There's nothing at this location.")} } diff --git a/ui/js/page/transactionHistory/view.jsx b/ui/js/page/transactionHistory/view.jsx index 8b9f1a90b..2f3a75715 100644 --- a/ui/js/page/transactionHistory/view.jsx +++ b/ui/js/page/transactionHistory/view.jsx @@ -10,18 +10,26 @@ class TransactionHistoryPage extends React.PureComponent { render() { const { fetchingTransactions, transactions } = this.props; + return (
-
+

{__("Transaction History")}

- {fetchingTransactions && - } - {!fetchingTransactions && - } + {fetchingTransactions && !transactions.length + ? + : ""} + {transactions && transactions.length + ? + : ""}
diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js index 857a5a708..4ca5f888b 100644 --- a/ui/js/reducers/app.js +++ b/ui/js/reducers/app.js @@ -8,6 +8,8 @@ const win = remote.BrowserWindow.getFocusedWindow(); const reducers = {}; const defaultState = { isLoaded: false, + modal: null, + modalProps: {}, platform: process.platform, upgradeSkipped: sessionStorage.getItem("upgradeSkipped"), daemonVersionMatched: null, @@ -76,14 +78,14 @@ reducers[types.UPDATE_VERSION] = function(state, action) { reducers[types.OPEN_MODAL] = function(state, action) { return Object.assign({}, state, { modal: action.data.modal, - modalExtraContent: action.data.extraContent, + modalProps: action.data.modalProps || {}, }); }; reducers[types.CLOSE_MODAL] = function(state, action) { return Object.assign({}, state, { modal: undefined, - modalExtraContent: undefined, + modalProps: {}, }); }; diff --git a/ui/js/reducers/claims.js b/ui/js/reducers/claims.js index f47f50a02..7f883a4ea 100644 --- a/ui/js/reducers/claims.js +++ b/ui/js/reducers/claims.js @@ -1,6 +1,7 @@ import * as types from "constants/action_types"; const reducers = {}; + const defaultState = {}; reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) { @@ -49,21 +50,23 @@ reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) { .filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1) ); - claims.filter(claim => claim.category.match(/claim/)).forEach(claim => { - byId[claim.claim_id] = claim; + claims + .filter(claim => claim.category && claim.category.match(/claim/)) + .forEach(claim => { + byId[claim.claim_id] = claim; - const pending = Object.values(pendingById).find(pendingClaim => { - return ( - pendingClaim.name == claim.name && - pendingClaim.channel_name == claim.channel_name - ); + const pending = Object.values(pendingById).find(pendingClaim => { + return ( + pendingClaim.name == claim.name && + pendingClaim.channel_name == claim.channel_name + ); + }); + + if (pending) { + delete pendingById[pending.claim_id]; + } }); - if (pending) { - delete pendingById[pending.claim_id]; - } - }); - // Remove old timed out pending publishes const old = Object.values(pendingById) .filter(pendingClaim => Date.now() - pendingClaim.time >= 20 * 60 * 1000) diff --git a/ui/js/reducers/content.js b/ui/js/reducers/content.js index 9d8c5867e..56195e917 100644 --- a/ui/js/reducers/content.js +++ b/ui/js/reducers/content.js @@ -2,6 +2,7 @@ import * as types from "constants/action_types"; const reducers = {}; const defaultState = { + playingUri: null, rewardedContentClaimIds: [], channelPages: {}, }; @@ -58,6 +59,12 @@ reducers[types.RESOLVE_URI_CANCELED] = reducers[ }); }; +reducers[types.SET_PLAYING_URI] = (state, action) => { + return Object.assign({}, state, { + playingUri: action.data.uri, + }); +}; + // reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) { // const channelPages = Object.assign({}, state.channelPages); // const { uri, claims } = action.data; @@ -73,7 +80,7 @@ reducers[types.FETCH_CHANNEL_CLAIM_COUNT_COMPLETED] = function(state, action) { const channelPages = Object.assign({}, state.channelPages); const { uri, totalClaims } = action.data; - channelPages[uri] = totalClaims / 10; + channelPages[uri] = Math.ceil(totalClaims / 10); return Object.assign({}, state, { channelPages, diff --git a/ui/js/reducers/navigation.js b/ui/js/reducers/navigation.js index 821db9183..1ebcab2c9 100644 --- a/ui/js/reducers/navigation.js +++ b/ui/js/reducers/navigation.js @@ -24,12 +24,6 @@ reducers[types.DAEMON_READY] = function(state, action) { }); }; -reducers[types.CHANGE_PATH] = function(state, action) { - return Object.assign({}, state, { - currentPath: action.data.path, - }); -}; - reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { return Object.assign({}, state, { pathAfterAuth: action.data.path, @@ -38,15 +32,16 @@ reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { reducers[types.HISTORY_NAVIGATE] = (state, action) => { const { stack, index } = state; - - let newState = {}; - const path = action.data.url; - // Check for duplicated + let newState = { + currentPath: path, + }; + if (action.data.index >= 0) { newState.index = action.data.index; } else if (!stack[index] || stack[index].path !== path) { + // ^ Check for duplicated newState.stack = [...stack.slice(0, index + 1), { path, scrollY: 0 }]; newState.index = newState.stack.length - 1; } diff --git a/ui/js/reducers/search.js b/ui/js/reducers/search.js index be6c3bec7..ed500b987 100644 --- a/ui/js/reducers/search.js +++ b/ui/js/reducers/search.js @@ -1,5 +1,4 @@ import * as types from "constants/action_types"; -import lbryuri from "lbryuri"; const reducers = {}; const defaultState = {}; @@ -9,7 +8,6 @@ reducers[types.SEARCH_STARTED] = function(state, action) { return Object.assign({}, state, { searching: true, - query: query, }); }; @@ -31,7 +29,6 @@ reducers[types.SEARCH_COMPLETED] = function(state, action) { reducers[types.SEARCH_CANCELLED] = function(state, action) { return Object.assign({}, state, { searching: false, - query: undefined, }); }; diff --git a/ui/js/reducers/settings.js b/ui/js/reducers/settings.js index 16e53e2da..2656c03fd 100644 --- a/ui/js/reducers/settings.js +++ b/ui/js/reducers/settings.js @@ -6,12 +6,15 @@ import lbry from "lbry"; const reducers = {}; const defaultState = { clientSettings: { - showNsfw: lbry.getClientSetting("showNsfw"), + showNsfw: lbry.getClientSetting(settings.SHOW_NSFW), + showUnavailable: lbry.getClientSetting(settings.SHOW_UNAVAILABLE), welcome_acknowledged: lbry.getClientSetting(settings.NEW_USER_ACKNOWLEDGED), credit_intro_acknowledged: lbry.getClientSetting( settings.CREDIT_INTRO_ACKNOWLEDGED ), language: lbry.getClientSetting(settings.LANGUAGE), + theme: lbry.getClientSetting(settings.THEME), + themes: lbry.getClientSetting(settings.THEMES), }, languages: {}, }; diff --git a/ui/js/reducers/wallet.js b/ui/js/reducers/wallet.js index e704222a6..a4e25b964 100644 --- a/ui/js/reducers/wallet.js +++ b/ui/js/reducers/wallet.js @@ -10,11 +10,12 @@ const buildDraftTransaction = () => ({ const defaultState = { balance: undefined, blocks: {}, - transactions: [], + transactions: {}, fetchingTransactions: false, receiveAddress: address, gettingNewAddress: false, draftTransaction: buildDraftTransaction(), + sendingSupport: false, }; reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) { @@ -24,20 +25,16 @@ reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) { }; reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) { - const oldTransactions = Object.assign({}, state.transactions); - const byId = Object.assign({}, oldTransactions.byId); + let byId = Object.assign({}, state.transactions); + const { transactions } = action.data; transactions.forEach(transaction => { byId[transaction.txid] = transaction; }); - const newTransactions = Object.assign({}, oldTransactions, { - byId: byId, - }); - return Object.assign({}, state, { - transactions: newTransactions, + transactions: byId, fetchingTransactions: false, }); }; @@ -125,6 +122,25 @@ reducers[types.SEND_TRANSACTION_FAILED] = function(state, action) { }); }; +reducers[types.SUPPORT_TRANSACTION_STARTED] = function(state, action) { + return Object.assign({}, state, { + sendingSupport: true, + }); +}; + +reducers[types.SUPPORT_TRANSACTION_COMPLETED] = function(state, action) { + return Object.assign({}, state, { + sendingSupport: false, + }); +}; + +reducers[types.SUPPORT_TRANSACTION_FAILED] = function(state, action) { + return Object.assign({}, state, { + error: action.data.error, + sendingSupport: false, + }); +}; + reducers[types.FETCH_BLOCK_SUCCESS] = (state, action) => { const { block, block: { height } } = action.data, blocks = Object.assign({}, state.blocks); diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 3e8e68223..c721916c0 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -71,9 +71,9 @@ export const selectUpgradeDownloadItem = createSelector( state => state.downloadItem ); -export const selectModalExtraContent = createSelector( +export const selectModalProps = createSelector( _selectState, - state => state.modalExtraContent + state => state.modalProps ); export const selectDaemonReady = createSelector( diff --git a/ui/js/selectors/availability.js b/ui/js/selectors/availability.js index 1200dd904..99bf63ad9 100644 --- a/ui/js/selectors/availability.js +++ b/ui/js/selectors/availability.js @@ -7,14 +7,10 @@ export const selectAvailabilityByUri = createSelector( state => state.byUri || {} ); -const selectAvailabilityForUri = (state, props) => { - return selectAvailabilityByUri(state)[props.uri]; -}; - -export const makeSelectIsAvailableForUri = () => { +export const makeSelectIsAvailableForUri = uri => { return createSelector( - selectAvailabilityForUri, - availability => (availability === undefined ? undefined : availability > 0) + selectAvailabilityByUri, + byUri => (!byUri || byUri[uri] === undefined ? undefined : byUri[uri] > 0) ); }; @@ -23,10 +19,9 @@ export const selectFetchingAvailability = createSelector( state => state.fetching || {} ); -const selectFetchingAvailabilityForUri = (state, props) => { - return selectFetchingAvailability(state)[props.uri]; -}; - -export const makeSelectFetchingAvailabilityForUri = () => { - return createSelector(selectFetchingAvailabilityForUri, fetching => fetching); +export const makeSelectFetchingAvailabilityForUri = uri => { + return createSelector( + selectFetchingAvailability, + byUri => byUri && byUri[uri] + ); }; diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js index 630e72c3e..c90b78843 100644 --- a/ui/js/selectors/claims.js +++ b/ui/js/selectors/claims.js @@ -1,6 +1,7 @@ import { createSelector } from "reselect"; import { selectCurrentParams } from "selectors/navigation"; import lbryuri from "lbryuri"; +import { makeSelectCurrentParam } from "./navigation"; const _selectState = state => state.claims || {}; @@ -40,25 +41,24 @@ export const selectAllClaimsByChannel = createSelector( state => state.claimsByChannel || {} ); -const selectClaimForUri = (state, props) => { - const uri = lbryuri.normalize(props.uri); - return selectClaimsByUri(state)[uri]; +export const makeSelectClaimForUri = uri => { + return createSelector( + selectClaimsByUri, + claims => claims && claims[lbryuri.normalize(uri)] + ); }; -export const makeSelectClaimForUri = () => { - return createSelector(selectClaimForUri, claim => claim); -}; - -const selectClaimForUriIsMine = (state, props) => { - const uri = lbryuri.normalize(props.uri); - const claim = selectClaimsByUri(state)[uri]; - const myClaims = selectMyClaimsRaw(state); - - return myClaims.has(claim.claim_id); -}; - -export const makeSelectClaimForUriIsMine = () => { - return createSelector(selectClaimForUriIsMine, isMine => isMine); +export const makeSelectClaimIsMine = rawUri => { + const uri = lbryuri.normalize(rawUri); + return createSelector( + selectClaimsByUri, + selectMyClaimsRaw, + (claims, myClaims) => + claims && + claims[uri] && + claims[uri].claim_id && + myClaims.has(claims[uri].claim_id) + ); }; export const selectAllFetchingChannelClaims = createSelector( @@ -66,86 +66,56 @@ export const selectAllFetchingChannelClaims = createSelector( state => state.fetchingChannelClaims || {} ); -const selectFetchingChannelClaims = (state, props) => { - const allFetchingChannelClaims = selectAllFetchingChannelClaims(state); - - return allFetchingChannelClaims[props.uri]; -}; - -export const makeSelectFetchingChannelClaims = (state, props) => { - return createSelector(selectFetchingChannelClaims, fetching => fetching); -}; - -export const selectClaimsInChannelForUri = (state, props) => { - const byId = selectClaimsById(state); - const byChannel = selectAllClaimsByChannel(state)[props.uri] || {}; - const claimIds = byChannel["all"]; - - if (!claimIds) return claimIds; - - const claims = []; - - claimIds.forEach(claimId => claims.push(byId[claimId])); - - return claims; -}; - -export const makeSelectClaimsInChannelForUri = () => { - return createSelector(selectClaimsInChannelForUri, claims => claims); -}; - -export const selectClaimsInChannelForCurrentPage = (state, props) => { - const byId = selectClaimsById(state); - const byChannel = selectAllClaimsByChannel(state)[props.uri] || {}; - const params = selectCurrentParams(state); - const page = params && params.page ? params.page : 1; - const claimIds = byChannel[page]; - - if (!claimIds) return claimIds; - - const claims = []; - - claimIds.forEach(claimId => claims.push(byId[claimId])); - - return claims; -}; - -export const makeSelectClaimsInChannelForCurrentPage = () => { - return createSelector(selectClaimsInChannelForCurrentPage, claims => claims); -}; - -const selectMetadataForUri = (state, props) => { - const claim = selectClaimForUri(state, props); - const metadata = - claim && claim.value && claim.value.stream && claim.value.stream.metadata; - - const value = metadata ? metadata : claim === undefined ? undefined : null; - return value; -}; - -export const makeSelectMetadataForUri = () => { - return createSelector(selectMetadataForUri, metadata => metadata); -}; - -const selectSourceForUri = (state, props) => { - const claim = selectClaimForUri(state, props); - const source = - claim && claim.value && claim.value.stream && claim.value.stream.source; - - return source ? source : claim === undefined ? undefined : null; -}; - -export const makeSelectSourceForUri = () => { - return createSelector(selectSourceForUri, source => source); -}; - -export const makeSelectContentTypeForUri = () => { +export const makeSelectFetchingChannelClaims = uri => { return createSelector( - selectSourceForUri, - source => (source ? source.contentType : source) + selectAllFetchingChannelClaims, + fetching => fetching && fetching[uri] ); }; +export const makeSelectClaimsInChannelForCurrentPage = uri => { + const pageSelector = makeSelectCurrentParam("page"); + + return createSelector( + selectClaimsById, + selectAllClaimsByChannel, + pageSelector, + (byId, allClaims, page) => { + const byChannel = allClaims[uri] || {}; + const claimIds = byChannel[page || 1]; + + if (!claimIds) return claimIds; + + return claimIds.map(claimId => byId[claimId]); + } + ); +}; + +export const makeSelectMetadataForUri = uri => { + return createSelector(makeSelectClaimForUri(uri), claim => { + const metadata = + claim && claim.value && claim.value.stream && claim.value.stream.metadata; + + const value = metadata ? metadata : claim === undefined ? undefined : null; + return value; + }); +}; + +export const makeSelectTitleForUri = uri => { + return createSelector( + makeSelectMetadataForUri(uri), + metadata => metadata && metadata.title + ); +}; + +export const makeSelectContentTypeForUri = uri => { + return createSelector(makeSelectClaimForUri(uri), claim => { + const source = + claim && claim.value && claim.value.stream && claim.value.stream.source; + return source ? source.contentType : undefined; + }); +}; + export const selectIsFetchingClaimListMine = createSelector( _selectState, state => !!state.isFetchingClaimListMine @@ -210,7 +180,12 @@ export const selectMyChannelClaims = createSelector( const ids = state.myChannelClaims || []; const claims = []; - ids.forEach(id => claims.push(byId[id])); + ids.forEach(id => { + if (byId[id]) { + //I'm not sure why this check is necessary, but it ought to be a quick fix for https://github.com/lbryio/lbry-app/issues/544 + claims.push(byId[id]); + } + }); return claims; } diff --git a/ui/js/selectors/content.js b/ui/js/selectors/content.js index 3bf6926a6..0818134de 100644 --- a/ui/js/selectors/content.js +++ b/ui/js/selectors/content.js @@ -17,12 +17,16 @@ export const selectResolvingUris = createSelector( state => state.resolvingUris || [] ); -const selectResolvingUri = (state, props) => { - return selectResolvingUris(state).indexOf(props.uri) != -1; -}; +export const selectPlayingUri = createSelector( + _selectState, + state => state.playingUri +); -export const makeSelectIsResolvingForUri = () => { - return createSelector(selectResolvingUri, resolving => resolving); +export const makeSelectIsUriResolving = uri => { + return createSelector( + selectResolvingUris, + resolvingUris => resolvingUris && resolvingUris.indexOf(uri) != -1 + ); }; export const selectChannelPages = createSelector( @@ -30,12 +34,8 @@ export const selectChannelPages = createSelector( state => state.channelPages || {} ); -const selectTotalPagesForChannel = (state, props) => { - return selectChannelPages(state)[props.uri]; -}; - -export const makeSelectTotalPagesForChannel = () => { - return createSelector(selectTotalPagesForChannel, totalPages => totalPages); +export const makeSelectTotalPagesForChannel = uri => { + return createSelector(selectChannelPages, byUri => byUri && byUri[uri]); }; export const selectRewardContentClaimIds = createSelector( diff --git a/ui/js/selectors/cost_info.js b/ui/js/selectors/cost_info.js index b40d62d5e..d7c17cefa 100644 --- a/ui/js/selectors/cost_info.js +++ b/ui/js/selectors/cost_info.js @@ -8,12 +8,11 @@ export const selectAllCostInfoByUri = createSelector( state => state.byUri || {} ); -export const selectCostInfoForUri = (state, props) => { - return selectAllCostInfoByUri(state)[props.uri]; -}; - -export const makeSelectCostInfoForUri = () => { - return createSelector(selectCostInfoForUri, costInfo => costInfo); +export const makeSelectCostInfoForUri = uri => { + return createSelector( + selectAllCostInfoByUri, + costInfos => costInfos && costInfos[uri] + ); }; export const selectCostForCurrentPageUri = createSelector( @@ -28,10 +27,9 @@ export const selectFetchingCostInfo = createSelector( state => state.fetching || {} ); -const selectFetchingCostInfoForUri = (state, props) => { - return selectFetchingCostInfo(state)[props.uri]; -}; - -export const makeSelectFetchingCostInfoForUri = () => { - return createSelector(selectFetchingCostInfoForUri, fetching => !!fetching); +export const makeSelectFetchingCostInfoForUri = uri => { + return createSelector( + selectFetchingCostInfo, + fetchingByUri => fetchingByUri && fetchingByUri[uri] + ); }; diff --git a/ui/js/selectors/file_info.js b/ui/js/selectors/file_info.js index 1b3dc7982..431454dfc 100644 --- a/ui/js/selectors/file_info.js +++ b/ui/js/selectors/file_info.js @@ -26,17 +26,17 @@ export const selectIsFetchingFileListDownloadedOrPublished = createSelector( isFetchingFileList || isFetchingClaimListMine ); -export const selectFileInfoForUri = (state, props) => { - const claims = selectClaimsByUri(state), - claim = claims[props.uri], - byOutpoint = selectFileInfosByOutpoint(state), - outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined; +export const makeSelectFileInfoForUri = uri => { + return createSelector( + selectClaimsByUri, + selectFileInfosByOutpoint, + (claims, byOutpoint) => { + const claim = claims[uri], + outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined; - return outpoint ? byOutpoint[outpoint] : undefined; -}; - -export const makeSelectFileInfoForUri = () => { - return createSelector(selectFileInfoForUri, fileInfo => fileInfo); + return outpoint ? byOutpoint[outpoint] : undefined; + } + ); }; export const selectDownloadingByOutpoint = createSelector( @@ -44,19 +44,14 @@ export const selectDownloadingByOutpoint = createSelector( state => state.downloadingByOutpoint || {} ); -const selectDownloadingForUri = (state, props) => { - const byOutpoint = selectDownloadingByOutpoint(state); - const fileInfo = selectFileInfoForUri(state, props); - - if (!fileInfo) return false; - - return byOutpoint[fileInfo.outpoint]; -}; - -export const makeSelectDownloadingForUri = () => { +export const makeSelectDownloadingForUri = uri => { return createSelector( - selectDownloadingForUri, - downloadingForUri => !!downloadingForUri + selectDownloadingByOutpoint, + makeSelectFileInfoForUri(uri), + (byOutpoint, fileInfo) => { + if (!fileInfo) return false; + return byOutpoint[fileInfo.outpoint]; + } ); }; @@ -65,13 +60,8 @@ export const selectUrisLoading = createSelector( state => state.urisLoading || {} ); -const selectLoadingForUri = (state, props) => { - const byUri = selectUrisLoading(state); - return byUri[props.uri]; -}; - -export const makeSelectLoadingForUri = () => { - return createSelector(selectLoadingForUri, loading => !!loading); +export const makeSelectLoadingForUri = uri => { + return createSelector(selectUrisLoading, byUri => byUri && byUri[uri]); }; export const selectFileInfosPendingPublish = createSelector( diff --git a/ui/js/selectors/navigation.js b/ui/js/selectors/navigation.js index ca95e259f..b5eb98ade 100644 --- a/ui/js/selectors/navigation.js +++ b/ui/js/selectors/navigation.js @@ -10,8 +10,11 @@ export const selectCurrentPath = createSelector( state => state.currentPath ); +export const computePageFromPath = path => + path.replace(/^\//, "").split("?")[0]; + export const selectCurrentPage = createSelector(selectCurrentPath, path => { - return path.replace(/^\//, "").split("?")[0]; + return computePageFromPath(path); }); export const selectCurrentParams = createSelector(selectCurrentPath, path => { @@ -21,6 +24,13 @@ export const selectCurrentParams = createSelector(selectCurrentPath, path => { return parseQueryParams(path.split("?")[1]); }); +export const makeSelectCurrentParam = param => { + return createSelector( + selectCurrentParams, + params => (params ? params[param] : undefined) + ); +}; + export const selectHeaderLinks = createSelector(selectCurrentPage, page => { // This contains intentional fall throughs switch (page) { @@ -80,7 +90,7 @@ export const selectPageTitle = createSelector( case "start": return __("Start"); case "publish": - return __("Publish"); + return params.id ? __("Edit") : __("Publish"); case "help": return __("Help"); case "developer": diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index c2a646563..f9fbc4fe4 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -19,6 +19,15 @@ export const selectClaimedRewards = createSelector( byId => Object.values(byId) || [] ); +export const selectClaimedRewardsByTransactionId = createSelector( + selectClaimedRewards, + rewards => + rewards.reduce((map, reward) => { + map[reward.transaction_id] = reward; + return map; + }, {}) +); + export const selectUnclaimedRewards = createSelector( selectUnclaimedRewardsByType, byType => diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index e3c6fbb4a..9b967d66e 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -1,11 +1,16 @@ import { createSelector } from "reselect"; -import { selectPageTitle, selectCurrentPage } from "selectors/navigation"; +import { + selectPageTitle, + selectCurrentPage, + selectCurrentParams, +} from "selectors/navigation"; export const _selectState = state => state.search || {}; export const selectSearchQuery = createSelector( - _selectState, - state => state.query + selectCurrentPage, + selectCurrentParams, + (page, params) => (page === "search" ? params && params.query : null) ); export const selectIsSearching = createSelector( @@ -36,45 +41,49 @@ export const selectWunderBarAddress = createSelector( (page, title, query) => (page != "search" ? title : query ? query : title) ); -export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { - switch (page) { - case "auth": - return "icon-user"; - case "search": - return "icon-search"; - case "settings": - return "icon-gear"; - case "help": - return "icon-question"; - case "report": - return "icon-file"; - case "downloaded": - return "icon-folder"; - case "published": - return "icon-folder"; - case "history": - return "icon-history"; - case "send": - return "icon-send"; - case "rewards": - return "icon-rocket"; - case "invite": - return "icon-envelope-open"; - case "address": - case "receive": - return "icon-address-book"; - case "wallet": - case "backup": - return "icon-bank"; - case "show": - return "icon-file"; - case "publish": - return "icon-upload"; - case "developer": - return "icon-code"; - case "discover": - return "icon-home"; - default: - return "icon-file"; +export const selectWunderBarIcon = createSelector( + selectCurrentPage, + selectCurrentParams, + (page, params) => { + switch (page) { + case "auth": + return "icon-user"; + case "search": + return "icon-search"; + case "settings": + return "icon-gear"; + case "help": + return "icon-question"; + case "report": + return "icon-file"; + case "downloaded": + return "icon-folder"; + case "published": + return "icon-folder"; + case "history": + return "icon-history"; + case "send": + return "icon-send"; + case "rewards": + return "icon-rocket"; + case "invite": + return "icon-envelope-open"; + case "address": + case "receive": + return "icon-address-book"; + case "wallet": + case "backup": + return "icon-bank"; + case "show": + return "icon-file"; + case "publish": + return params.id ? __("icon-pencil") : __("icon-upload"); + case "developer": + return "icon-code"; + case "discover": + return "icon-home"; + default: + return "icon-file"; + } } -}); +); diff --git a/ui/js/selectors/settings.js b/ui/js/selectors/settings.js index 10b9191df..5bf7f7f74 100644 --- a/ui/js/selectors/settings.js +++ b/ui/js/selectors/settings.js @@ -1,3 +1,4 @@ +import * as settings from "constants/settings"; import { createSelector } from "reselect"; const _selectState = state => state.settings || {}; @@ -24,12 +25,15 @@ export const selectSettingsIsGenerous = createSelector( settings => settings && settings.is_generous_host ); -export const selectShowNsfw = createSelector( - selectClientSettings, - clientSettings => !!clientSettings.showNsfw -); +//refactor me +export const selectShowNsfw = makeSelectClientSetting(settings.SHOW_NSFW); export const selectLanguages = createSelector( _selectState, state => state.languages || {} ); + +export const selectThemePath = createSelector( + makeSelectClientSetting(settings.THEME), + theme => "themes/" + (theme || "light") + ".css" +); diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index 54dacbe26..2c9b6704b 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -7,30 +7,76 @@ export const selectBalance = createSelector( state => state.balance ); -export const selectTransactions = createSelector( - _selectState, - state => state.transactions || {} -); - export const selectTransactionsById = createSelector( - selectTransactions, - transactions => transactions.byId || {} + _selectState, + state => state.transactions ); export const selectTransactionItems = createSelector( selectTransactionsById, byId => { - const transactionItems = []; - const txids = Object.keys(byId); - txids.forEach(txid => { + const items = []; + + Object.keys(byId).forEach(txid => { const tx = byId[txid]; - transactionItems.push({ - id: txid, - date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null, - amount: parseFloat(tx.value), - }); + + //ignore dust/fees + if (Math.abs(tx.amount) === Math.abs(tx.fee)) { + return; + } + + let append = []; + + append.push( + ...tx.claim_info.map(item => + Object.assign({}, tx, item, { + type: item.claim_name[0] === "@" ? "channel" : "publish", + }) + ) + ); + append.push( + ...tx.support_info.map(item => + Object.assign({}, tx, item, { + type: !item.is_tip ? "support" : "tip", + }) + ) + ); + append.push( + ...tx.update_info.map(item => + Object.assign({}, tx, item, { type: "update" }) + ) + ); + + if (!append.length) { + append.push( + Object.assign({}, tx, { + type: tx.value < 0 ? "spend" : "receive", + }) + ); + } + + items.push( + ...append.map(item => { + //value on transaction, amount on outpoint + //amount is always positive, but should match sign of value + const amount = parseFloat( + item.amount ? (item.value < 0 ? -1 : 1) * item.amount : item.value + ); + + return { + txid: txid, + date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null, + amount: amount, + fee: amount < 0 ? -1 * tx.fee / append.length : 0, + claim_id: item.claim_id, + claim_name: item.claim_name, + type: item.type || "send", + nout: item.nout, + }; + }) + ); }); - return transactionItems.reverse(); + return items.reverse(); } ); @@ -57,6 +103,11 @@ export const selectIsFetchingTransactions = createSelector( state => state.fetchingTransactions ); +export const selectIsSendingSupport = createSelector( + _selectState, + state => state.sendingSupport +); + export const selectReceiveAddress = createSelector( _selectState, state => state.receiveAddress diff --git a/ui/package.json b/ui/package.json index c0e6e73b5..82f5d0889 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.15.1", + "version": "0.16.0", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss deleted file mode 100644 index 9a838b2d7..000000000 --- a/ui/scss/_global.scss +++ /dev/null @@ -1,195 +0,0 @@ -@charset "UTF-8"; - -$spacing-vertical: 24px; - -$padding-button: $spacing-vertical * 2/3; -$padding-text-link: 0px; - -$color-primary: #155B4A; -$color-primary-light: saturate(lighten($color-primary, 50%), 20%); -$color-light-alt: hsl(hue($color-primary), 15, 85); -$color-text-dark: #000; -$color-black-transparent: rgba(32,32,32,0.9); -$color-help: rgba(0,0,0,.6); -$color-notice: #8a6d3b; -$color-error: #a94442; -$color-load-screen-text: #c3c3c3; -$color-canvas: #f5f5f5; -$color-bg: #ffffff; -$color-bg-alt: #D9D9D9; -$color-money: #216C2A; -$color-meta-light: #505050; -$color-form-border: rgba(160,160,160,.5); - -$font-size: 16px; -$font-line-height: 1.3333; - -$mobile-width-threshold: 801px; -$max-content-width: 1000px; -$max-text-width: 660px; - -$width-page-constrained: 800px; -$width-input-text: 330px; - -$height-button: $spacing-vertical * 1.5; -$height-header: $spacing-vertical * 2.5; -$height-video-embedded: $width-page-constrained * 9 / 16; - -$box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); -$box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12); - -$transition-standard: .225s ease; - -$blur-intensity-nsfw: 20px; - -@mixin clearfix() -{ - &:before, &:after - { - content: " "; - display: table; - } - &:after - { - clear: both; - } -} - -@mixin border-radius($radius) -{ - -webkit-border-radius: $radius; - -moz-border-radius: $radius; - -ms-border-radius: $radius; - border-radius: $radius; -} - -@mixin placeholder-color($color) { - /*do not group these it breaks because CSS*/ - &:-moz-placeholder { - color: $color; - } - &::-moz-placeholder { - color: $color; - } - &:-ms-input-placeholder { - color: $color; - } - &::-webkit-input-placeholder { - color: $color; - } -} - -@mixin display-flex() -{ - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; -} - -@mixin flex($columns) -{ - -webkit-flex: $columns; - -moz-flex: $columns; - -ms-flex: $columns; - flex: $columns; -} - -@mixin flex-flow($flow) { - -webkit-flex-flow: $flow; - -moz-flex-flow: $flow; - -ms-flex-flow: $flow; - flex-flow: $flow; -} - -@mixin flex-direction($direction) { - -webkit-flex-direction: $direction; - -moz-flex-direction: $direction; - -ms-flex-direction: $direction; - flex-direction: $direction; -} - -@mixin absolute-center() -{ - @include display-flex(); - -webkit-box-align: center; - -moz-box-align: center; - -ms-flex-align: center; - -webkit-align-items: center; - align-items: center; - -webkit-box-pack: center; - -moz-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; -} - -@mixin linear-gradient($from-color, $to-color) { - background-color: $to-color; /* Fallback Color */ - background-image: -webkit-linear-gradient(top, $from-color, $to-color); /* Chrome 10+, Saf5.1+, iOS 5+ */ - background-image: -moz-linear-gradient(top, $from-color, $to-color); /* FF3.6 */ - background-image: -ms-linear-gradient(top, $from-color, $to-color); /* IE10 */ - background-image: linear-gradient(top, $from-color, $to-color); -} - -@mixin box-sizing( $type: border-box ) { - -webkit-box-sizing: $type; - -moz-box-sizing: $type; - -o-box-sizing: $type; - -ms-box-sizing: $type; - box-sizing: $type; -} - -@mixin background-size ($size) { - -webkit-background-size: $size; - -moz-background-size: $size; - -o-background-size: $size; - background-size: $size; -} - -@mixin placeholder { - &::-webkit-input-placeholder {@content} - &:-moz-placeholder {@content} - &:-ms-input-placeholder {@content} -} - -@mixin offscreen() { - position: absolute; - left: -9999px; - top:auto; - width:1px; - height:1px; - overflow:hidden; -} - -@mixin text-link($color: $color-primary, $hover-opacity: 0.70) { - .icon - { - &:first-child { - padding-right: 5px; - } - &:last-child:not(:only-child) { - padding-left: 5px; - } - } - - &:not(.no-underline) { - text-decoration: underline; - .icon { - text-decoration: none; - } - } - &:hover - { - opacity: $hover-opacity; - transition: opacity $transition-standard; - text-decoration: underline; - .icon { - text-decoration: none; - } - } - - color: $color; - cursor: pointer; -} \ No newline at end of file diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index 338ccac3c..9ca64b4e5 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -1,40 +1,43 @@ -@import "global"; - html { height: 100%; - font-size: $font-size; + font-size: var(--font-size); } + body { + color: var(--text-color); font-family: 'Source Sans Pro', sans-serif; - line-height: $font-line-height; + line-height: var(--font-line-height); +} + +/* Custom text selection */ +*::selection { + background: var(--text-selection-bg); + color: var(--text-selection-color); } #window { min-height: 100vh; - background: $color-canvas; + background: var(--window-bg); } -.badge -{ - background: $color-money; - display: inline-block; - padding: 2px; - color: white; - border-radius: 2px; -} .credit-amount--indicator { font-weight: bold; - color: $color-money; + color: var(--color-money); +} +.credit-amount--fee +{ + font-size: 0.9em; + color: var(--color-meta-light); } #main-content { padding: $spacing-vertical; - margin-top: $height-header; + margin-top: var(--header-height); display: flex; flex-direction: column; main { @@ -47,7 +50,8 @@ body width: $width-page-constrained; } } -main.main--refreshing { + +.reloading { &:before { $width: 30px; position: absolute; @@ -87,7 +91,7 @@ sub { top: 0.4em; } code { font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace; - background-color: #eee; + background-color: var(--color-bg-alt); } p @@ -136,18 +140,18 @@ p .help { font-size: .85em; - color: $color-help; + color: var(--color-help); } .meta { font-size: 0.9em; - color: $color-meta-light; + color: var(--color-meta-light); } .empty { - color: $color-meta-light; + color:var(--color-meta-light); font-style: italic; } @@ -167,7 +171,7 @@ p text-align: right; line-height: 1; font-size: 0.85em; - color: $color-help; + color: var(--color-help); } section.section-spaced { diff --git a/ui/scss/_icons.scss b/ui/scss/_icons.scss index cb487975b..537531e19 100644 --- a/ui/scss/_icons.scss +++ b/ui/scss/_icons.scss @@ -1,5 +1,3 @@ -@import "global"; - @font-face { font-family: 'FontAwesome'; src: url('../font/fontawesome-webfont.eot?v=4.7.0'); @@ -1658,3 +1656,9 @@ .icon-medium:before { content: "\f23a"; } +.icon-address-book:before { + content: "\f2b9"; +} +.icon-envelope-open:before { + content: "\f2b6"; +} diff --git a/ui/scss/_vars.scss b/ui/scss/_vars.scss new file mode 100644 index 000000000..0a86d1d76 --- /dev/null +++ b/ui/scss/_vars.scss @@ -0,0 +1,128 @@ +/* +Both of these should probably die and become variables as well + */ +$spacing-vertical: 24px; +$width-page-constrained: 800px; +$text-color: #000; + +:root { + + /* Colors */ + --color-brand: #155B4A; + --color-primary: #155B4A; + --color-primary-light: saturate(lighten(#155B4A, 50%), 20%); + --color-light-alt: hsl(hue(#155B4A), 15, 85); + --color-dark-overlay: rgba(32,32,32,0.9); + --color-help: rgba(0,0,0,.6); + --color-notice: #8a6d3b; + --color-error: #a94442; + --color-load-screen-text: #c3c3c3; + --color-meta-light: #505050; + --color-money: #216C2A; + --color-download: #444; + --color-canvas: #f5f5f5; + --color-bg: #ffffff; + --color-bg-alt: #D9D9D9; + + + /* Misc */ + --content-max-width: 1000px; + --nsfw-blur-intensity: 20px; + --height-video-embedded: $width-page-constrained * 9 / 16 ; + + /* Font */ + --font-size: 16px; + --font-line-height: 1.3333; + --font-size-subtext-multiple: 0.82; + + /* Shadows */ + --box-shadow-layer: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); + --box-shadow-focus: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12); + + /* Transition */ + --transition-duration: .225s; + --transition-type: ease; + + /* Text */ + --text-color: $text-color; + --text-help-color: #EEE; + --text-max-width: 660px; + --text-link-padding: 4px; + --text-selection-bg: rgba(saturate(lighten(#155B4A, 20%), 20%), 1); // temp color + --text-selection-color: #FFF; + + /* Window */ + --window-bg: var(--color-canvas); + + /* Input */ + --input-bg: transparent; + --input-active-bg: transparent; + --input-width: 330px; + --input-color: var(--text-color); + --input-border-size: 2px; + --input-border-color: rgba(0,0,0,.25); + + /* Select */ + --select-bg: var(--color-bg-alt); + --select-color: var(--text-color); + + /* Button */ + --button-bg: var(--color-bg-alt); + --button-color: #FFF; + --button-primary-bg: var(--color-primary); + --button-primary-color: #FFF; + --button-padding: $spacing-vertical * 2/3; + --button-height: $spacing-vertical * 1.5; + --button-intra-margin: $spacing-vertical; + + /* Header */ + --header-bg: var(--color-bg); + --header-color: #666; + --header-active-color: rgba(0,0,0, 0.85); + --header-height: $spacing-vertical * 2.5; + --header-button-bg: var(--button-bg); + + /* Header -> search */ + --search-bg: rgba(255, 255, 255, 0.7); + --search-border:1px solid #ccc; + --search-color: #666; + --search-active-color: var(--header-active-color); + + /* Tabs */ + --tab-bg: transparent; + --tab-color: #666; + --tab-active-color: var(--header-active-color); + --tab-border-size: 2px; + --tab-border: var(--tab-border-size) solid var(--tab-active-color); + + /* Table */ + --table-border: 1px solid #e2e2e2; + --table-item-even: white; + --table-item-odd: #f4f4f4; + + /* Card */ + --card-bg: var(--color-bg); + --card-hover-translate: 10px; + --card-margin: $spacing-vertical * 2/3; + --card-max-width: $width-page-constrained; + --card-padding: $spacing-vertical * 2/3; + --card-radius: 2px; + --card-link-scaling: 1.1; + --card-small-width: $spacing-vertical * 10; + + /* Modal */ + --modal-bg: var(--color-bg); + --modal-overlay-bg: rgba(#F5F5F5, 0.75); // --color-canvas: #F5F5F5 + --modal-border: 1px solid rgb(204, 204, 204); + + /* Menu */ + --menu-bg: var(--color-bg); + --menu-radius: 2px; + --menu-item-hover-bg: var(--color-bg-alt); + + /* Tooltip */ + --tooltip-width: 300px; + --tooltip-bg: var(--color-bg); + --tooltip-color: var(--text-color); + --tooltip-border: 1px solid #aaa; +} diff --git a/ui/scss/all.scss b/ui/scss/all.scss index d899566ab..69656fa41 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -1,10 +1,12 @@ +@charset "UTF-8"; @import "_reset"; +@import "_vars"; @import "_icons"; @import "_gui"; @import "component/_table"; @import "component/_button.scss"; @import "component/_card.scss"; -@import "component/_file-actions.scss"; +@import "component/_file-download.scss"; @import "component/_file-selector.scss"; @import "component/_file-tile.scss"; @import "component/_form-field.scss"; @@ -18,6 +20,6 @@ @import "component/_snack-bar.scss"; @import "component/_video.scss"; @import "component/_pagination.scss"; +@import "component/_markdown-editor.scss"; @import "page/_developer.scss"; -@import "page/_reward.scss"; @import "page/_show.scss"; diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index cd00b8fe4..acc67e0aa 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -1,4 +1,4 @@ -@import "../global"; +@import "../mixin/link.scss"; $button-focus-shift: 12%; @@ -8,15 +8,15 @@ $button-focus-shift: 12%; + .button-set-item { - margin-left: $spacing-vertical; + margin-left: var(--button-intra-margin); } } .button-block, .faux-button-block { display: inline-block; - height: $height-button; - line-height: $height-button; + height: var(--button-height); + line-height: var(--button-height); text-decoration: none; border: 0 none; text-align: center; @@ -46,25 +46,26 @@ $button-focus-shift: 12%; } .button__content { - margin: 0 $padding-button; + margin: 0 var(--button-padding); } .button-primary { - $color-button-text: white; - color: darken($color-button-text, $button-focus-shift * 0.5); - background-color: $color-primary; - box-shadow: $box-shadow-layer; + + color: var(--button-primary-color); + background-color: var(--button-primary-bg); + box-shadow: var(--box-shadow-layer); + &:focus { - color: $color-button-text; + //color: var(--button-primary-active-color); + //background-color:color: var(--button-primary-active-bg); //box-shadow: $box-shadow-focus; - background-color: mix(black, $color-primary, $button-focus-shift) } } .button-alt { - background-color: $color-bg-alt; - box-shadow: $box-shadow-layer; + background-color: var(--button-bg); + box-shadow: var(--box-shadow-layer); } .button-text @@ -73,15 +74,21 @@ $button-focus-shift: 12%; display: inline-block; .button__content { - margin: 0 $padding-text-link; + margin: 0 var(--text-link-padding); } } .button-text-help { - @include text-link(#aaa); + @include text-link(var(--text-help-color)); font-size: 0.8em; } .button--flat { box-shadow: none !important; -} \ No newline at end of file +} + +.button--submit { + font-family: inherit; + font-size: inherit; + line-height: 0; +} diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 81959a4b4..0562b4040 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -1,33 +1,29 @@ -@import "../global"; - -$padding-card-horizontal: $spacing-vertical * 2/3; -$translate-card-hover: 10px; -$width-card-small: $spacing-vertical * 10; - .card { margin-left: auto; margin-right: auto; - max-width: $width-page-constrained; - background: $color-bg; - box-shadow: $box-shadow-layer; - border-radius: 2px; - margin-bottom: $spacing-vertical * 2/3; + max-width: var(--card-max-width); + background: var(--card-bg); + box-shadow: var(--box-shadow-layer); + border-radius: var(--card-radius); + margin-bottom: var(--card-margin); overflow: auto; + + //below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution + overflow-x: hidden; } .card--obscured { position: relative; } .card--obscured .card__inner { - filter: blur($blur-intensity-nsfw); + filter: blur( var(--nsfw-blur-intensity) ); } .card__title-primary, .card__title-identity, .card__content, .card__subtext, .card__actions { - padding-left: $padding-card-horizontal; - padding-right: $padding-card-horizontal; + padding: 0 var(--card-padding); } .card--small { .card__title-primary, @@ -35,29 +31,48 @@ $width-card-small: $spacing-vertical * 10; .card__actions, .card__content, .card__subtext { - padding: 0 $padding-card-horizontal / 2; + padding: 0 calc(var(--card-padding) / 2); } } .card__title-primary { - margin-top: $spacing-vertical * 2/3; - margin-bottom: $spacing-vertical * 2/3; + margin-top: var(--card-margin); + margin-bottom: var(--card-margin); +} +.card__title-primary .meta { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .card__title-identity { margin-top: $spacing-vertical * 1/3; margin-bottom: $spacing-vertical * 1/3; } .card__actions { - margin-top: $spacing-vertical * 2/3; - margin-bottom: $spacing-vertical * 2/3; + margin-top: var(--card-margin); + margin-bottom: var(--card-margin); +} +.card__actions--bottom { + margin-top: $spacing-vertical * 1/3; + margin-bottom: $spacing-vertical * 1/3; +} +.card__actions--form-submit { + margin-top: $spacing-vertical; + margin-bottom: var(--card-margin); +} +.card__action--right { + float: right; } .card__content { - margin-top: $spacing-vertical * 2/3; - margin-bottom: $spacing-vertical * 2/3; + margin-top: var(--card-margin); + margin-bottom: var(--card-margin); + table:not(:last-child) { + margin-bottom: var(--card-margin); + } } $font-size-subtext-multiple: 0.82; .card__subtext { - color: $color-meta-light; - font-size: $font-size-subtext-multiple * 1.0em; + color: var(--color-meta-light); + font-size: calc( var(--font-size-subtext-multiple) * 1.0em ); margin-top: $spacing-vertical * 1/3; margin-bottom: $spacing-vertical * 1/3; } @@ -65,7 +80,7 @@ $font-size-subtext-multiple: 0.82; white-space: pre-wrap; } .card__subtext--two-lines { - height: $font-size * $font-size-subtext-multiple * $font-line-height * 2; /*this is so one line text still has the proper height*/ + height: calc( var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2); /*this is so one line text still has the proper height*/ } .card-overlay { position: absolute; @@ -74,16 +89,16 @@ $font-size-subtext-multiple: 0.82; top: 0px; bottom: 0px; padding: 20px; - background-color: rgba(128, 128, 128, 0.8); + background-color: var(--color-dark-overlay); color: #fff; display: flex; align-items: center; font-weight: 600; } -$card-link-scaling: 1.1; .card__link { display: block; + cursor: pointer; } .card--link { transition: transform 120ms ease-in-out; @@ -91,14 +106,14 @@ $card-link-scaling: 1.1; .card--link:hover { position: relative; z-index: 1; - box-shadow: $box-shadow-focus; - transform: scale($card-link-scaling) translateX($translate-card-hover); + box-shadow: var(--box-shadow-focus); + transform: scale(var(--card-link-scaling)) translateX(var(--card-hover-translate)); transform-origin: 50% 50%; overflow-x: visible; overflow-y: visible } .card--link:hover ~ .card--link { - transform: translateX($translate-card-hover * 2); + transform: translateX(calc(var(--card-hover-translate) * 2)); } .card__media { @@ -152,33 +167,30 @@ $card-link-scaling: 1.1; position: absolute; top: 36% } - - - .card--small { - width: $width-card-small; + width: var(--card-small-width); overflow-x: hidden; white-space: normal; } .card--small .card__media { - height: $width-card-small * 9 / 16; + height: calc( var(--card-small-width) * 9 / 16); } .card--form { - width: $width-input-text + $padding-card-horizontal * 2; + width: calc( var(--input-width) + var(--card-padding) * 2); } .card__subtitle { - color: $color-help; + color: var(--color-help); font-size: 0.85em; - line-height: $font-line-height * 1 / 0.85; + line-height: calc( var(--font-line-height) * 1 / 0.85); } .card-series-submit { margin-left: auto; margin-right: auto; - max-width: $width-page-constrained; + max-width: var(--card-max-width); padding: $spacing-vertical / 2; } @@ -187,8 +199,10 @@ $card-link-scaling: 1.1; margin-top: $spacing-vertical * 1/3; } } + $padding-top-card-hover-hack: 20px; $padding-right-card-hover-hack: 30px; + .card-row__items { width: 100%; overflow: hidden; @@ -211,7 +225,7 @@ $padding-right-card-hover-hack: 30px; overflow: hidden; white-space: nowrap; width: 100%; - min-width: $width-card-small; + min-width: var(--card-small-width); margin-right: $spacing-vertical; } .card-row__header { @@ -226,14 +240,14 @@ $padding-right-card-hover-hack: 30px; .card-row__nav { position: absolute; - padding: 0 $spacing-vertical * 2 / 3; + padding: 0 var(--card-margin); height: 100%; - top: $padding-top-card-hover-hack - $spacing-vertical * 2 / 3; + top: calc( $padding-top-card-hover-hack - var(--card-margin) ); } .card-row__nav .card-row__scroll-button { - background: $color-bg; - color: $color-help; - box-shadow: $box-shadow-layer; + background: var(--card-bg); + color: var(--color-help); + box-shadow: var(--box-shadow-layer); padding: $spacing-vertical $spacing-vertical / 2; position: absolute; cursor: pointer; @@ -245,7 +259,7 @@ $padding-right-card-hover-hack: 30px; &:hover { opacity: 1.0; - transform: scale($card-link-scaling * 1.1) + transform: scale(calc( var(--card-link-scaling) * 1.1)); } } .card-row__nav--left { @@ -259,7 +273,6 @@ $padding-right-card-hover-hack: 30px; color: orangered; } - /* if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy */ diff --git a/ui/scss/component/_channel-indicator.scss b/ui/scss/component/_channel-indicator.scss index 52a0baed6..2e80c096e 100644 --- a/ui/scss/component/_channel-indicator.scss +++ b/ui/scss/component/_channel-indicator.scss @@ -1,5 +1,4 @@ -@import "../global"; .channel-indicator__icon--invalid { - color: $color-error; + color: var(--color-error); } diff --git a/ui/scss/component/_file-actions.scss b/ui/scss/component/_file-actions.scss deleted file mode 100644 index 4eda16b51..000000000 --- a/ui/scss/component/_file-actions.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import "../global"; - -$color-download: #444; - -.file-actions -{ - line-height: $height-button; - min-height: $height-button; -} - -.file-actions__download-status-bar, .file-actions__download-status-bar-overlay { - .button__content { - margin: 0 $padding-text-link; - } -} - -.file-actions__download-status-bar -{ - position: relative; - color: $color-download; -} -.file-actions__download-status-bar-overlay -{ - background: $color-download; - color: white; - position: absolute; - white-space: nowrap; - overflow: hidden; - z-index: 1; - top: 0px; - left: 0px; -} \ No newline at end of file diff --git a/ui/scss/component/_file-download.scss b/ui/scss/component/_file-download.scss new file mode 100644 index 000000000..13ce6e1f3 --- /dev/null +++ b/ui/scss/component/_file-download.scss @@ -0,0 +1,32 @@ +.file-download, .file-download__overlay { + .button__content { + margin: 0 var(--text-link-padding); + } +} + +.file-download +{ + position: relative; + color: var(--color-download); +} +.file-download__overlay +{ + background: var(--color-download); + color: white; + position: absolute; + white-space: nowrap; + overflow: hidden; + z-index: 1; + top: 0px; + left: 0px; +} + +/* + +.file-actions +{ + line-height: var(--button-height); + min-height: var(--button-height); +} + + */ \ No newline at end of file diff --git a/ui/scss/component/_file-tile.scss b/ui/scss/component/_file-tile.scss index 7630f5517..7341cc606 100644 --- a/ui/scss/component/_file-tile.scss +++ b/ui/scss/component/_file-tile.scss @@ -1,4 +1,3 @@ -@import "../global"; $height-file-tile: $spacing-vertical * 6; .file-tile__row { diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index 9dee7240b..86cd8cd34 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -1,6 +1,9 @@ -@import "../global"; -$width-input-border: 2px; +@mixin placeholder { + &::-webkit-input-placeholder {@content} + &:-moz-placeholder {@content} + &:-ms-input-placeholder {@content} +} .form-row-submit { @@ -15,7 +18,7 @@ $width-input-border: 2px; margin-top: $spacing-vertical * 5/6; margin-bottom: $spacing-vertical * 1/6; line-height: 1; - font-size: 0.9 * $font-size; + font-size:calc( 0.9 * var(--font-size)); } .form-row__label-row--prefix { float: left; @@ -23,15 +26,17 @@ $width-input-border: 2px; } input[type="text"].input-copyable { - border: 1px solid $color-form-border; + background: var(--input-bg); + border-bottom: var(--input-border-size) solid var(--input-border-color); + color: var(--input-color); line-height: 1; padding-top: $spacing-vertical * 1/3; padding-bottom: $spacing-vertical * 1/3; - width: $width-input-text; + width: var(--input-width); padding-left: 5px; padding-right: 5px; width: 100%; - &:focus { border-color: black; } + &:focus { border-color: var(--color-primary); } } .form-field { @@ -43,14 +48,15 @@ input[type="text"].input-copyable { } select { - transition: outline $transition-standard; - cursor: pointer; + transition: outline var(--transition-duration) var(--transition-type); box-sizing: border-box; padding-left: 5px; padding-right: 5px; height: $spacing-vertical; + background: var(--select-bg); + color: var(--select-color); &:focus { - outline: $width-input-border solid $color-primary; + outline: var(--input-border-size) solid var(--color-primary); } } @@ -62,16 +68,18 @@ input[type="text"].input-copyable { input[type="search"], input[type="date"] { @include placeholder { - color: lighten($color-text-dark, 60%); + color: lighten($text-color, 60%); } - transition: all $transition-standard; + transition: all var(--transition-duration) var(--transition-type); cursor: pointer; padding-left: 1px; padding-right: 1px; box-sizing: border-box; -webkit-appearance: none; + background: var(--input-bg); + color: var(--input-color); &[readonly] { - background-color: #bbb; + background-color: var(--input-bg); } } @@ -81,12 +89,13 @@ input[type="text"].input-copyable { input[type="number"], input[type="search"], input[type="date"] { - border-bottom: $width-input-border solid $color-form-border; + border-bottom: var(--input-border-size) solid var(--input-border-color); line-height: 1; padding-top: $spacing-vertical * 1/3; padding-bottom: $spacing-vertical * 1/3; + &.form-field__input--error { - border-color: $color-error; + border-color: var(--color-error); } &.form-field__input--inline { padding-top: 0; @@ -104,12 +113,13 @@ input[type="text"].input-copyable { input[type="number"]:focus, input[type="search"]:focus, input[type="date"]:focus { - border-color: $color-primary; + border-color: var(--color-primary); + background: var(--input-active-bg); } textarea { padding: 2px; - border: $width-input-border solid $color-form-border; + border: var(--input-border-size) solid var(--input-border-color); } } .form-field--SimpleMDE { @@ -117,18 +127,20 @@ input[type="text"].input-copyable { } .form-field__label { + color: var(--color-help); &[for] { cursor: pointer; } > input[type="checkbox"], input[type="radio"] { margin-right: 6px; } } -.form-field__label--error { - color: $color-error; +.form-row__label-row .form-field__label--error { + /*the row restriction is to prevent coloring checkboxes and radio labels*/ + color: var(--color-error); } .form-field__input-text { - width: $width-input-text; + width: var(--input-width); } .form-field__prefix { @@ -153,16 +165,16 @@ input[type="text"].input-copyable { .form-field__error, .form-field__helper { margin-top: $spacing-vertical * 1/3; font-size: 0.8em; - transition: opacity $transition-standard; + transition: opacity var(--transition-duration) var(--transition-type); } .form-field__error { - color: $color-error; + color: var(--color-error); } .form-field__helper { - color: $color-help; + color:var(--color-help); } .form-field__input.form-field__input-SimpleMDE .CodeMirror-scroll { height: auto; -} \ No newline at end of file +} diff --git a/ui/scss/component/_header.scss b/ui/scss/component/_header.scss index 5cc51541e..9428298e2 100644 --- a/ui/scss/component/_header.scss +++ b/ui/scss/component/_header.scss @@ -1,15 +1,11 @@ -@import "../global"; - -$color-header: #666; -$color-header-active: darken($color-header, 20%); #header { - color: $color-header; - background: #fff; + color: var(--header-color); + background: var(--header-bg); display: flex; position: fixed; - box-shadow: $box-shadow-layer; + box-shadow: var(--box-shadow-layer); top: 0; left: 0; width: 100%; @@ -21,7 +17,11 @@ $color-header-active: darken($color-header, 20%); flex: 0 0 content; padding-left: $spacing-vertical / 4; padding-right: $spacing-vertical / 4; + .button-alt { + background: var(--header-button-bg) !important; + } } + .header__item--wunderbar { flex-grow: 1; } @@ -36,23 +36,24 @@ $color-header-active: darken($color-header, 20%); } } -.wunderbar--active .icon-search { color: $color-primary; } +.wunderbar--active .icon-search { color: var(--color-primary); } .wunderbar__input { - background: rgba(255, 255, 255, 0.7); + background: var(--search-bg); width: 100%; - color: $color-header; + color: var(--search-color); height: $spacing-vertical * 1.5; line-height: $spacing-vertical * 1.5; padding-left: 38px; padding-right: 5px; - border: 1px solid $color-text-dark; - @include border-radius(2px); - border: 1px solid #ccc; + border-radius: 2px; + border: var(--search-border); + transition: box-shadow var(--transition-duration) var(--transition-type); &:focus { - color: $color-header-active; - box-shadow: $box-shadow-focus; - border-color: $color-primary; + background: var(--search-active-bg); + color: var(--search-active-color); + box-shadow: var(--box-shadow-focus); + border-color: var(--color-primary); } } @@ -65,12 +66,12 @@ nav.sub-header margin-right: auto; > a { - $sub-header-selected-underline-height: 2px; display: inline-block; margin: 0 15px; padding: 0 5px; - line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height; - color: $color-header; + line-height:calc(var(--header-height) - $spacing-vertical - var(--tab-border-size)); + color: var(--tab-color); + &:first-child { margin-left: 0; @@ -81,12 +82,12 @@ nav.sub-header } &.sub-header-selected { - border-bottom: $sub-header-selected-underline-height solid $color-header-active; - color: $color-header-active; + border-bottom: var(--tab-border); + color: var(--tab-active-color); } &:hover { - color: $color-header-active; + color: var(--tab-active-color); } } -} \ No newline at end of file +} diff --git a/ui/scss/component/_load-screen.scss b/ui/scss/component/_load-screen.scss index 0caa74f65..cfd369c95 100644 --- a/ui/scss/component/_load-screen.scss +++ b/ui/scss/component/_load-screen.scss @@ -1,8 +1,7 @@ -@import "../global"; .load-screen { color: white; - background: $color-primary; + background: var(--color-brand); background-size: cover; min-height: 100vh; min-width: 100vw; @@ -19,7 +18,7 @@ } .load-screen__details { - color: $color-load-screen-text; + color: var(--color-load-screen-text); } .load-screen__details--warning { diff --git a/ui/scss/component/_markdown-editor.scss b/ui/scss/component/_markdown-editor.scss new file mode 100644 index 000000000..a2b311f5b --- /dev/null +++ b/ui/scss/component/_markdown-editor.scss @@ -0,0 +1,99 @@ +.CodeMirror { + background: var(--input-active-bg) !important; + border: 1px solid var(--input-border-color) !important; + color: var(--text-color) !important; +} + +.CodeMirror-fullscreen { + background: var(--input-bg); +} + +.editor-toolbar { + border: 1px solid var(--input-border-color) !important; + border-bottom: 0 !important; +} + +.editor-toolbar i.separator { + border-color: var(--input-border-color) !important; +} + +.editor-toolbar.fullscreen { + background: var(--color-bg) !important; +} + +div.editor-toolbar a { + color: var(--text-color) !important; +} + +.editor-toolbar a.active, +.editor-toolbar a:hover { + background: var(--button-bg) !important; + border-color: transparent !important; +} + +.editor-toolbar.disabled-for-preview a:not(.no-disable) { + background: transparent !important; + border-color: transparent !important; +} + +.editor-statusbar { + color: #959694; +} + +.editor-preview { + background: var(--card-bg) !important; +} + +.editor-preview-side { + background: var(--color-bg-alt) !important; + border: 1px solid var(--input-border-color) !important; +} + +.editor-preview pre, +.editor-preview-side pre { + background: #eee; +} + +.editor-preview table td, +.editor-preview table th, +.editor-preview-side table td, +.editor-preview-side table th { + border: 1px solid var(--input-border-color) !important; +} + +.CodeMirror .CodeMirror-code .cm-tag { + color: #63a35c; +} + +.CodeMirror .CodeMirror-code .cm-attribute { + color: #795da3; +} + +.CodeMirror .CodeMirror-code .cm-string { + color: #183691; +} + +.CodeMirror .CodeMirror-selected { + background: var(--text-selection-bg) !important; + color: var(--text-selection-color) !important; +} + +.CodeMirror .CodeMirror-cursor{ + border-color: var(--text-color) !important; +} + +.CodeMirror .CodeMirror-code .cm-comment { + background: rgba(0, 0, 0, .05); +} + +.CodeMirror .CodeMirror-code .cm-link { + color: #7f8c8d; +} + +.CodeMirror .CodeMirror-code .cm-url { + color: #aab2b3; +} + +.CodeMirror .CodeMirror-placeholder { + opacity: .5; +} diff --git a/ui/scss/component/_menu.scss b/ui/scss/component/_menu.scss index d8e79be28..19af2af66 100644 --- a/ui/scss/component/_menu.scss +++ b/ui/scss/component/_menu.scss @@ -1,4 +1,3 @@ -@import "../global"; $border-radius-menu: 2px; @@ -9,9 +8,9 @@ $border-radius-menu: 2px; .menu { position: absolute; white-space: nowrap; - background-color: white; - box-shadow: $box-shadow-layer; - border-radius: $border-radius-menu; + background-color: var(--menu-bg); + box-shadow: var(--box-shadow-layer); + border-radius: var(--menu-radius); padding-top: ($spacing-vertical / 5) 0px; z-index: 1; } @@ -20,6 +19,6 @@ $border-radius-menu: 2px; display: block; padding: ($spacing-vertical / 4) ($spacing-vertical / 2); &:hover { - background: $color-bg-alt; + background: var(--menu-item-hover-bg); } -} \ No newline at end of file +} diff --git a/ui/scss/component/_modal.scss b/ui/scss/component/_modal.scss index 4ebba6799..cb664e4c1 100644 --- a/ui/scss/component/_modal.scss +++ b/ui/scss/component/_modal.scss @@ -1,4 +1,3 @@ -@import "../global"; .modal-overlay, .error-modal-overlay { position: fixed; @@ -10,7 +9,7 @@ left: 0px; right: 0px; bottom: 0px; - background-color: rgba(255, 255, 255, 0.74902); + background-color: var(--modal-overlay-bg); z-index: 9999; } @@ -24,12 +23,12 @@ justify-content: center; align-items: center; - border: 1px solid rgb(204, 204, 204); - background: rgb(255, 255, 255); + border: var(--modal-border); + background: var(--modal-bg); overflow: auto; border-radius: 4px; padding: $spacing-vertical; - box-shadow: $box-shadow-layer; + box-shadow: var(--box-shadow-layer); max-width: 400px; word-break: break-word; @@ -52,7 +51,7 @@ } .error-modal-overlay { - background: rgba(#000, .88); + background: var(--modal-overlay-bg); } .error-modal__content { @@ -74,7 +73,7 @@ width: 400px; } .error-modal__error-list { /*shitty hack/temp fix for long errors making modal unusable*/ - border: 1px solid #eee; + border: 1px solid var(--input-border-color); padding: 8px; list-style: none; max-height: 400px; diff --git a/ui/scss/component/_notice.scss b/ui/scss/component/_notice.scss index b77ba2a5a..112658987 100644 --- a/ui/scss/component/_notice.scss +++ b/ui/scss/component/_notice.scss @@ -1,4 +1,3 @@ -@import "../global"; .notice { padding: 10px 20px; diff --git a/ui/scss/component/_pagination.scss b/ui/scss/component/_pagination.scss index fd5ca528e..4851f366a 100644 --- a/ui/scss/component/_pagination.scss +++ b/ui/scss/component/_pagination.scss @@ -1,4 +1,3 @@ -@import "../global"; .pagination { display: block; @@ -32,5 +31,5 @@ .pagination__item--selected { color: white; - background: $color-primary; + background: var(--color-primary); } diff --git a/ui/scss/component/_snack-bar.scss b/ui/scss/component/_snack-bar.scss index c3df3ab92..acc9f98a6 100644 --- a/ui/scss/component/_snack-bar.scss +++ b/ui/scss/component/_snack-bar.scss @@ -1,4 +1,3 @@ -@import "../global"; $padding-snack-horizontal: $spacing-vertical; @@ -16,7 +15,7 @@ $padding-snack-horizontal: $spacing-vertical; margin-right: auto; min-width: 300px; max-width: 500px; - background: $color-black-transparent; + background: var(--color-dark-overlay); color: #f0f0f0; display: flex; @@ -25,7 +24,7 @@ $padding-snack-horizontal: $spacing-vertical; border-radius: 2px; - transition: all $transition-standard; + transition: all var(--transition-duration) var(--transition-type); z-index: 10000; /*hack to get it over react modal */ } @@ -33,7 +32,7 @@ $padding-snack-horizontal: $spacing-vertical; .snack-bar__action { display: inline-block; text-transform: uppercase; - color: $color-primary-light; + color: var(--color-primary-light); margin: 0px 0px 0px $padding-snack-horizontal; min-width: min-content; &:hover { diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index 38893eae5..59edeb480 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -1,4 +1,3 @@ -@import "../global"; table.table-standard { word-wrap: break-word; @@ -20,7 +19,7 @@ table.table-standard { font-size: 0.9em; padding: $spacing-vertical/4+1 8px $spacing-vertical/4-2; text-align: left; - border-bottom: 1px solid #e2e2e2; + border-bottom: var(--table-border); img { vertical-align: text-bottom; } @@ -29,7 +28,7 @@ table.table-standard { } } tr.thead:not(:first-child) th { - border-top: 1px solid #e2e2e2; + border-top: var(--table-border); } tfoot td { padding: $spacing-vertical / 2 8px; @@ -38,10 +37,10 @@ table.table-standard { tbody { tr { &:nth-child(even):not(.odd) { - background-color: #f4f4f4; + background-color: var(--table-item-odd); } &:nth-child(odd):not(.even) { - background-color: white; + background-color: var(--table-item-even); } &.thead { background: none; @@ -60,4 +59,12 @@ table.table-standard { table.table-stretch { width: 100%; +} + +table.table-transactions { + td:nth-of-type(1) { width: 15%; } + td:nth-of-type(2) { width: 15%; } + td:nth-of-type(3) { width: 15%; } + td:nth-of-type(4) { width: 40%; } + td:nth-of-type(5) { width: 15%; } } \ No newline at end of file diff --git a/ui/scss/component/_tooltip.scss b/ui/scss/component/_tooltip.scss index 0be9b1db8..58889f657 100644 --- a/ui/scss/component/_tooltip.scss +++ b/ui/scss/component/_tooltip.scss @@ -1,4 +1,4 @@ -@import "../global"; +@import "../mixin/link.scss"; .tooltip { position: relative; @@ -9,28 +9,25 @@ } .tooltip__body { - $tooltip-body-width: 300px; - position: absolute; z-index: 1; left: 50%; - margin-left: $tooltip-body-width * -1 / 2; + margin-left: calc(var(--tooltip-width) * -1 / 2); white-space: normal; - box-sizing: border-box; padding: $spacing-vertical / 2; - width: $tooltip-body-width; - border: 1px solid #aaa; - color: $color-text-dark; - background-color: $color-bg; - font-size: $font-size * 7/8; - line-height: $font-line-height; - box-shadow: $box-shadow-layer; + width: var(--tooltip-width); + border: var(--tooltip-border); + color: var(--tooltip-color); + background-color: var(--tooltip-bg); + font-size: calc(var(--font-size) * 7/8); + line-height: var(--font-line-height); + box-shadow: var(--box-shadow-layer); } .tooltip--header .tooltip__link { @include text-link(#aaa); - font-size: $font-size * 3/4; - margin-left: $padding-button; + font-size: calc( var(--font-size) * 3/4 ); + margin-left: var(--button-padding); vertical-align: middle; -} \ No newline at end of file +} diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss index 61143a2dd..46cd07eee 100644 --- a/ui/scss/component/_video.scss +++ b/ui/scss/component/_video.scss @@ -1,4 +1,5 @@ -@import "../global"; + +$height-video-embedded: $width-page-constrained * 9 / 16; video { object-fit: contain; @@ -37,7 +38,7 @@ video { .video--obscured .video__cover { position: relative; - filter: blur($blur-intensity-nsfw); + filter: blur(var(--nsfw-blur-intensity)); } @@ -108,7 +109,11 @@ video { background-position: center center; background-repeat: no-repeat; position: relative; - .video__play-button { @include absolute-center(); } + .video__play-button { + display: flex; + align-items: center; + justify-content: center; + } } .video__play-button { @@ -120,12 +125,12 @@ video { font-size: $spacing-vertical * 3; color: white; z-index: 1; - background: $color-black-transparent; + background: var(--color-dark-overlay); opacity: 0.6; left: 0; top: 0; &:hover { opacity: 1; - transition: opacity $transition-standard; + transition: opacity var(--transition-duration) var(--transition-type); } } diff --git a/ui/scss/mixin/link.scss b/ui/scss/mixin/link.scss new file mode 100644 index 000000000..ae6e752da --- /dev/null +++ b/ui/scss/mixin/link.scss @@ -0,0 +1,30 @@ +@mixin text-link($color: var(--color-primary), $hover-opacity: 0.70) { + .icon + { + &:first-child { + padding-right: 5px; + } + &:last-child:not(:only-child) { + padding-left: 5px; + } + } + + &:not(.no-underline) { + text-decoration: underline; + .icon { + text-decoration: none; + } + } + &:hover + { + opacity: $hover-opacity; + transition: opacity var(--transition-duration) var(--transition-type); + text-decoration: underline; + .icon { + text-decoration: none; + } + } + + color: $color; + cursor: pointer; +} diff --git a/ui/scss/page/_reward.scss b/ui/scss/page/_reward.scss deleted file mode 100644 index a550c01c3..000000000 --- a/ui/scss/page/_reward.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import "../global"; - -.reward-page__details { - background-color: lighten($color-canvas, 1.5%); -} \ No newline at end of file diff --git a/ui/scss/page/_show.scss b/ui/scss/page/_show.scss index 0dbfea2e0..d52f28381 100644 --- a/ui/scss/page/_show.scss +++ b/ui/scss/page/_show.scss @@ -1,5 +1,3 @@ -@import "../global"; - .show-page-media { text-align: center; margin-bottom: $spacing-vertical; diff --git a/ui/watch.bat b/ui/watch.bat new file mode 100644 index 000000000..0c064759a --- /dev/null +++ b/ui/watch.bat @@ -0,0 +1,11 @@ +@echo off + +set found= +for %%F in ( + "%~dp0\node_modules\node-sass\bin\node-sass" + "%~dp0\node_modules\.bin\webpack" +) do if exist %%F (set found=1) +if not defined found EXIT + +node %~dp0\node_modules\node-sass\bin\node-sass --output %~dp0\..\app\dist\css --sourcemap=none %~dp0\scss +%~dp0\node_modules\.bin\webpack --config %~dp0\webpack.dev.config.js --progress --colors --watch \ No newline at end of file diff --git a/ui/watch.sh b/ui/watch.sh index 4ea14c42b..6a60eb10c 100755 --- a/ui/watch.sh +++ b/ui/watch.sh @@ -22,4 +22,4 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" node_modules/.bin/node-sass --output $DIR/../app/dist/css --sourcemap=none --watch $DIR/scss/ & node_modules/.bin/webpack --config webpack.dev.config.js --progress --colors --watch -) \ No newline at end of file +)