reduxify pull request 325

This commit is contained in:
Akinwale Ariwodola 2017-08-08 10:36:14 +01:00
commit d591886df5
144 changed files with 5872 additions and 2358 deletions

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.13.0 current_version = 0.14.3
commit = True commit = True
tag = True tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?

2
.gitignore vendored
View file

@ -14,6 +14,7 @@
/app/node_modules /app/node_modules
/build/venv /build/venv
/lbry-app-venv /lbry-app-venv
/lbry-app
/lbry-venv /lbry-venv
/daemon/build /daemon/build
/daemon/venv /daemon/venv
@ -27,3 +28,4 @@ build/daemon.zip
.vimrc .vimrc
package-lock.json package-lock.json
ui/yarn.lock

View file

@ -8,15 +8,19 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
* * Added a new component, `FormFieldPrice` which is now used in Publish and Settings
* *
### Changed ### Changed
* * Some form field refactoring as we progress towards form sanity.
* When an "Open" button is clicked on a show page, if the file fails to open, the app will try to open the file's folder.
* *
### Fixed ### Fixed
* * Tiles will no longer be blurry on hover (Windows only bug)
* Removed placeholder values from price selection form fields, which was causing confusion that these were real values (#426)
* Fixed showing "other currency" help tip in publish form, which was caused due to not "setting" state for price
* Now using setState in formFieldPrice
* *
### Deprecated ### Deprecated
@ -24,8 +28,90 @@ Web UI version numbers should always match the corresponding version of LBRY App
* *
### Removed ### Removed
* Removed the label "Max Purchase Price" from settings page. It was redundant.
* Unused old files from previous commit(9c3d633)
* *
*
## [0.14.3] - 2017-08-03
### Added
* Add tooltips to controls in header
* New flow for rewards authentication failure
### Changed
* Make it clearer how to skip identity verification and add link to FAQ
* Reward-eligible content icon is now a rocket ship :D :D :D
* Change install description shown by operating systems
* Improved flow for when app is run with incompatible daemon
### Fixed
* Corrected improper pluralization on loading screen
## [0.14.2] - 2017-07-30
### Added
* Replaced horizontal scrollbars with scroll arrows
* Featured weekly reward content shows with an orange star
* Added pagination to channel pages
### Fixed
* Fixed requirement to double click play button on many videos
* Fixed errors from calls to `get` not bubbling correctly
* Fixed some corner-case flows that could break file pages
## [0.14.1] - 2017-07-28
### Fixed
* Fixed upgrade file path missing file name
## [0.14.0] - 2017-07-28
### Added
* Identity verification for new reward participants
* Support rich markup in publishing descriptions and show pages.
* Release past publishing claims (and recover LBC) via the UI
* Added transition to card hovers to smooth animation
* Use randomly colored tiles when image is missing from metadata
* Added a loading message to file actions
* URL is auto suggested in Publish Page
### Changed
* Publishing revamped. Editing claims is much easier.
* Daemon updated from v0.13.1 to [v0.14.2](https://github.com/lbryio/lbry/releases/tag/v0.14.2)
* Publish page now use `claim_list` rather than `file_list`
### Removed
* Removed bandwidth caps from settings, because the daemon was not respecting them anyway.
### Fixed
* Fixed bug with download notice when switching window focus
* Fixed newly published files appearing twice
* Fixed unconfirmed published files missing channel name
* Fixed old files from updated published claims appearing in downloaded list
* Fixed inappropriate text showing on searches
* Stop discover page from pushing jumping vertically while loading
* Restored feedback on claim amounts
* Fixed hiding price input when Free is checked on publish form
* Fixed hiding new identity fields on publish form
* Fixed files on downloaded tab not showing download progress
* Fixed downloading files that are deleted not being removed from the downloading list
* Fixed download progress bar not being cleared when a downloading file is deleted
* Fixed refresh regression after adding scroll position to history state
* Fixed app not monitoring download progress on files in progress between restarts
## [0.13.0] - 2017-06-30 ## [0.13.0] - 2017-06-30

View file

@ -27,7 +27,7 @@ const {version: localVersion} = require(app.getAppPath() + '/package.json');
const VERSION_CHECK_INTERVAL = 30 * 60 * 1000; const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest'; const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
const DAEMON_PATH = process.env.LBRY_DAEMON || path.join(__dirname, 'dist', 'lbrynet-daemon');
let client = jayson.client.http({ let client = jayson.client.http({
host: 'localhost', host: 'localhost',
@ -207,13 +207,8 @@ function handleDaemonSubprocessExited() {
function launchDaemon() { function launchDaemon() {
assert(!daemonSubprocess, 'Tried to launch daemon twice'); assert(!daemonSubprocess, 'Tried to launch daemon twice');
if (process.env.LBRY_DAEMON) { console.log('Launching daemon:', DAEMON_PATH)
executable = process.env.LBRY_DAEMON; daemonSubprocess = child_process.spawn(DAEMON_PATH)
} else {
executable = path.join(__dirname, 'dist', 'lbrynet-daemon');
}
console.log('Launching daemon:', executable)
daemonSubprocess = child_process.spawn(executable)
// Need to handle the data event instead of attaching to // Need to handle the data event instead of attaching to
// process.stdout because the latter doesn't work. I believe on // process.stdout because the latter doesn't work. I believe on
// windows it buffers stdout and we don't get any meaningful output // windows it buffers stdout and we don't get any meaningful output

View file

@ -1,8 +1,8 @@
{ {
"name": "LBRY", "name": "LBRY",
"version": "0.13.0", "version": "0.14.3",
"main": "main.js", "main": "main.js",
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"author": { "author": {
"name": "LBRY Inc.", "name": "LBRY Inc.",
"email": "hello@lbry.io" "email": "hello@lbry.io"
@ -18,5 +18,8 @@
}, },
"devDependencies": { "devDependencies": {
"electron-rebuild": "^1.5.11" "electron-rebuild": "^1.5.11"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.14.2"
} }
} }

View file

@ -1 +1 @@
https://github.com/lbryio/lbry/releases/download/v0.13.1/lbrynet-daemon-v0.13.1-OSNAME.zip https://github.com/lbryio/lbry/releases/download/v0.14.2/lbrynet-daemon-v0.14.2-OSNAME.zip

View file

@ -46,8 +46,8 @@ dir dist # verify that binary was built/named correctly
# sign binary # sign binary
nuget install secure-file -ExcludeVersion nuget install secure-file -ExcludeVersion
secure-file\tools\secure-file -decrypt build\lbry2.pfx.enc -secret "$env:pfx_key" secure-file\tools\secure-file -decrypt build\lbry3.pfx.enc -secret "$env:pfx_key"
& ${env:SIGNTOOL_PATH} sign /f build\lbry2.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe & ${env:SIGNTOOL_PATH} sign /f build\lbry3.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe
python build\upload_assets.py python build\upload_assets.py

View file

@ -79,11 +79,14 @@ if $OSX; then
else else
OSNAME="linux" OSNAME="linux"
fi fi
DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" DAEMON_VER=$(node -e "console.log(require(\"$ROOT/app/package.json\").lbrySettings.lbrynetDaemonVersion)")
DAEMON_URL="https://github.com/lbryio/lbry/releases/download/v${DAEMON_VER}/lbrynet-daemon-v${DAEMON_VER}-${OSNAME}.zip"
wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip"
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/"
rm "$BUILD_DIR/daemon.zip" rm "$BUILD_DIR/daemon.zip"
################### ###################
# Build the app # # Build the app #
################### ###################

Binary file not shown.

BIN
build/lbry3.pfx.enc Normal file

Binary file not shown.

View file

@ -57,8 +57,8 @@
}, },
"devDependencies": { "devDependencies": {
"devtron": "^1.4.0", "devtron": "^1.4.0",
"electron": "^1.4.15", "electron": "^1.7.5",
"electron-builder": "^11.7.0", "electron-builder": "^11.7.0",
"electron-debug": "^1.1.0" "electron-debug": "^1.4.0"
} }
} }

View file

@ -13,31 +13,43 @@ import { doSearch } from "actions/search";
import { doFetchDaemonSettings } from "actions/settings"; import { doFetchDaemonSettings } from "actions/settings";
import { doAuthenticate } from "actions/user"; import { doAuthenticate } from "actions/user";
import { doFileList } from "actions/file_info"; import { doFileList } from "actions/file_info";
import { toQueryString } from "util/query_params";
import { parseQueryParams } from "util/query_params";
const { remote, ipcRenderer, shell } = require("electron"); const { remote, ipcRenderer, shell } = require("electron");
const path = require("path"); const path = require("path");
const app = require("electron").remote.app;
const { download } = remote.require("electron-dl"); const { download } = remote.require("electron-dl");
const fs = remote.require("fs"); const fs = remote.require("fs");
const { lbrySettings: config } = require("../../../app/package.json");
const queryStringFromParams = params => { export function doNavigate(path, params = {}, options = {}) {
return Object.keys(params).map(key => `${key}=${params[key]}`).join("&");
};
export function doNavigate(path, params = {}) {
return function(dispatch, getState) { return function(dispatch, getState) {
let url = path; let url = path;
if (params) url = `${url}?${queryStringFromParams(params)}`; if (params) url = `${url}?${toQueryString(params)}`;
dispatch(doChangePath(url)); dispatch(doChangePath(url));
const state = getState(); const state = getState();
const pageTitle = selectPageTitle(state); const pageTitle = selectPageTitle(state);
dispatch(doHistoryPush(params, pageTitle, url)); dispatch(doHistoryPush({ params }, pageTitle, url));
}; };
} }
export function doChangePath(path) { export function doAuthNavigate(pathAfterAuth = null, params = {}) {
return function(dispatch, getState) {
if (pathAfterAuth) {
dispatch({
type: types.CHANGE_AFTER_AUTH_PATH,
data: {
path: `${pathAfterAuth}?${toQueryString(params)}`,
},
});
}
dispatch(doNavigate("/auth"));
};
}
export function doChangePath(path, options = {}) {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({
type: types.CHANGE_PATH, type: types.CHANGE_PATH,
@ -48,8 +60,12 @@ export function doChangePath(path) {
const state = getState(); const state = getState();
const pageTitle = selectPageTitle(state); const pageTitle = selectPageTitle(state);
const scrollY = options.scrollY;
window.document.title = pageTitle; window.document.title = pageTitle;
window.scrollTo(0, 0);
if (scrollY) window.scrollTo(0, scrollY);
else window.scrollTo(0, 0);
const currentPage = selectCurrentPage(state); const currentPage = selectCurrentPage(state);
if (currentPage === "search") { if (currentPage === "search") {
@ -62,15 +78,32 @@ export function doChangePath(path) {
export function doHistoryBack() { export function doHistoryBack() {
return function(dispatch, getState) { return function(dispatch, getState) {
if (!history.state) return; if (!history.state) return;
if (history.state.index === 0) return;
history.back(); history.back();
}; };
} }
export function doHistoryPush(params, title, relativeUrl) { export function doHistoryPush(currentState, title, relativeUrl) {
return function(dispatch, getState) { return function(dispatch, getState) {
title += " - LBRY"; title += " - LBRY";
history.pushState(params, title, `#${relativeUrl}`); history.pushState(currentState, title, `#${relativeUrl}`);
};
}
export function doRecordScroll(scroll) {
return function(dispatch, getState) {
const state = getState();
const historyState = history.state;
if (!historyState) return;
historyState.scrollY = scroll;
history.replaceState(
historyState,
document.title,
`#${state.app.currentPath}`
);
}; };
} }
@ -117,8 +150,10 @@ export function doDownloadUpgrade() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
// Make a new directory within temp directory so the filename is guaranteed to be available // Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath("temp") + require("path").sep); const dir = fs.mkdtempSync(
const upgradeFilename = selectUpgradeFilename(state); remote.app.getPath("temp") + require("path").sep
),
upgradeFilename = selectUpgradeFilename(state);
let options = { let options = {
onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))), onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
@ -202,11 +237,21 @@ export function doCheckUpgradeAvailable() {
}; };
} }
export function doCheckDaemonVersion() {
return function(dispatch, getState) {
lbry.version().then(({ lbrynet_version }) => {
dispatch({
type: config.lbrynetDaemonVersion == lbrynet_version
? types.DAEMON_VERSION_MATCH
: types.DAEMON_VERSION_MISMATCH,
});
});
};
}
export function doAlertError(errorList) { export function doAlertError(errorList) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
console.log("do alert error");
console.log(errorList);
dispatch({ dispatch({
type: types.OPEN_MODAL, type: types.OPEN_MODAL,
data: { data: {
@ -219,6 +264,9 @@ export function doAlertError(errorList) {
export function doDaemonReady() { export function doDaemonReady() {
return function(dispatch, getState) { return function(dispatch, getState) {
const path = window.location.hash || "#/discover";
const params = parseQueryParams(path.split("?")[1] || "");
history.replaceState({ params, index: 0 }, document.title, `${path}`);
dispatch(doAuthenticate()); dispatch(doAuthenticate());
dispatch({ dispatch({
type: types.DAEMON_READY, type: types.DAEMON_READY,
@ -256,3 +304,9 @@ export function doChangeLanguage(newLanguage) {
data: { newLanguage: newLanguage }, data: { newLanguage: newLanguage },
}; };
} }
export function doQuit() {
return function(dispatch, getState) {
remote.app.quit();
};
}

View file

@ -5,17 +5,18 @@ import lbryuri from "lbryuri";
import { selectBalance } from "selectors/wallet"; import { selectBalance } from "selectors/wallet";
import { import {
selectFileInfoForUri, selectFileInfoForUri,
selectUrisDownloading, selectDownloadingByOutpoint,
} from "selectors/file_info"; } from "selectors/file_info";
import { selectResolvingUris } from "selectors/content"; import { selectResolvingUris } from "selectors/content";
import { selectCostInfoForUri } from "selectors/cost_info"; import { selectCostInfoForUri } from "selectors/cost_info";
import { doOpenModal } from "actions/app"; import { doAlertError, doOpenModal } from "actions/app";
import { doClaimEligiblePurchaseRewards } from "actions/rewards"; import { doClaimEligiblePurchaseRewards } from "actions/rewards";
import { selectBadgeNumber } from "selectors/app"; import { selectBadgeNumber } from "selectors/app";
import { selectTotalDownloadProgress } from "selectors/file_info"; import { selectTotalDownloadProgress } from "selectors/file_info";
import setBadge from "util/setBadge"; import setBadge from "util/setBadge";
import setProgressBar from "util/setProgressBar"; import setProgressBar from "util/setProgressBar";
import batchActions from "util/batchActions"; import batchActions from "util/batchActions";
import * as modals from "constants/modal_types";
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
@ -132,6 +133,34 @@ export function doFetchFeaturedUris() {
}; };
} }
export function doFetchRewardedContent() {
return function(dispatch, getState) {
const state = getState();
const success = nameToClaimId => {
dispatch({
type: types.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: Object.values(nameToClaimId),
success: true,
},
});
};
const failure = () => {
dispatch({
type: types.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: [],
success: false,
},
});
};
lbryio.call("reward", "list_featured").then(success, failure);
};
}
export function doUpdateLoadStatus(uri, outpoint) { export function doUpdateLoadStatus(uri, outpoint) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
@ -198,24 +227,38 @@ export function doUpdateLoadStatus(uri, outpoint) {
}; };
} }
export function doStartDownload(uri, outpoint) {
return function(dispatch, getState) {
const state = getState();
if (!outpoint) {
throw new Error("outpoint is required to begin a download");
}
const { downloadingByOutpoint = {} } = state.fileInfo;
if (downloadingByOutpoint[outpoint]) return;
lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
dispatch({
type: types.DOWNLOADING_STARTED,
data: {
uri,
outpoint,
fileInfo,
},
});
dispatch(doUpdateLoadStatus(uri, outpoint));
});
};
}
export function doDownloadFile(uri, streamInfo) { export function doDownloadFile(uri, streamInfo) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
lbry dispatch(doStartDownload(uri, streamInfo.outpoint));
.file_list({ outpoint: streamInfo.outpoint, full_status: true })
.then(([fileInfo]) => {
dispatch({
type: types.DOWNLOADING_STARTED,
data: {
uri,
outpoint: streamInfo.outpoint,
fileInfo,
},
});
dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint));
});
lbryio lbryio
.call("file", "view", { .call("file", "view", {
@ -240,22 +283,32 @@ export function doLoadVideo(uri) {
}, },
}); });
lbry.get({ uri }).then(streamInfo => { lbry
const timeout = .get({ uri })
streamInfo === null || .then(streamInfo => {
typeof streamInfo !== "object" || const timeout =
streamInfo.error == "Timeout"; streamInfo === null ||
typeof streamInfo !== "object" ||
streamInfo.error == "Timeout";
if (timeout) { if (timeout) {
dispatch({
type: types.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doOpenModal("timedOut"));
} else {
dispatch(doDownloadFile(uri, streamInfo));
}
})
.catch(error => {
dispatch({ dispatch({
type: types.LOADING_VIDEO_FAILED, type: types.LOADING_VIDEO_FAILED,
data: { uri }, data: { uri },
}); });
dispatch(doOpenModal("timedOut")); dispatch(doAlertError(error));
} else { });
dispatch(doDownloadFile(uri, streamInfo));
}
});
}; };
} }
@ -264,8 +317,9 @@ export function doPurchaseUri(uri, purchaseModalName) {
const state = getState(); const state = getState();
const balance = selectBalance(state); const balance = selectBalance(state);
const fileInfo = selectFileInfoForUri(state, { uri }); const fileInfo = selectFileInfoForUri(state, { uri });
const downloadingByUri = selectUrisDownloading(state); const downloadingByOutpoint = selectDownloadingByOutpoint(state);
const alreadyDownloading = !!downloadingByUri[uri]; const alreadyDownloading =
fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
// we already fully downloaded the file. // we already fully downloaded the file.
if (fileInfo && fileInfo.completed) { if (fileInfo && fileInfo.completed) {
@ -292,7 +346,7 @@ export function doPurchaseUri(uri, purchaseModalName) {
} }
if (cost > balance) { if (cost > balance) {
dispatch(doOpenModal("notEnoughCredits")); dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
} else { } else {
dispatch(doOpenModal(purchaseModalName)); dispatch(doOpenModal(purchaseModalName));
} }
@ -301,22 +355,28 @@ export function doPurchaseUri(uri, purchaseModalName) {
}; };
} }
export function doFetchClaimsByChannel(uri, page = 1) { export function doFetchClaimsByChannel(uri, page) {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({
type: types.FETCH_CHANNEL_CLAIMS_STARTED, type: types.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri }, data: { uri, page },
}); });
lbry.claim_list_by_channel({ uri, page }).then(result => { lbry.claim_list_by_channel({ uri, page }).then(result => {
const claimResult = result[uri], const claimResult = result[uri],
claims = claimResult ? claimResult.claims_in_channel : []; claims = claimResult ? claimResult.claims_in_channel : [],
totalPages = claimResult
? claimResult.claims_in_channel_pages
: undefined,
currentPage = claimResult ? claimResult.returned_page : undefined;
dispatch({ dispatch({
type: types.FETCH_CHANNEL_CLAIMS_COMPLETED, type: types.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: { data: {
uri, uri,
claims: claims, claims,
totalPages,
page: currentPage,
}, },
}); });
}); });
@ -339,3 +399,68 @@ export function doFetchClaimListMine() {
}); });
}; };
} }
export function doFetchChannelListMine() {
return function(dispatch, getState) {
dispatch({
type: types.FETCH_CHANNEL_LIST_MINE_STARTED,
});
const callback = channels => {
dispatch({
type: types.FETCH_CHANNEL_LIST_MINE_COMPLETED,
data: { claims: channels },
});
};
lbry.channel_list_mine().then(callback);
};
}
export function doCreateChannel(name, amount) {
return function(dispatch, getState) {
dispatch({
type: types.CREATE_CHANNEL_STARTED,
});
return new Promise((resolve, reject) => {
lbry
.channel_new({
channel_name: name,
amount: parseFloat(amount),
})
.then(
channelClaim => {
channelClaim.name = name;
dispatch({
type: types.CREATE_CHANNEL_COMPLETED,
data: { channelClaim },
});
resolve(channelClaim);
},
err => {
reject(err);
}
);
});
};
}
export function doPublish(params) {
return function(dispatch, getState) {
return new Promise((resolve, reject) => {
const success = claim => {
resolve(claim);
if (claim === true) dispatch(doFetchClaimListMine());
else
setTimeout(() => dispatch(doFetchClaimListMine()), 20000, {
once: true,
});
};
const failure = err => reject(err);
lbry.publishDeprecated(params, null, success, failure);
});
};
}

View file

@ -3,15 +3,18 @@ import lbry from "lbry";
import { doFetchClaimListMine } from "actions/content"; import { doFetchClaimListMine } from "actions/content";
import { import {
selectClaimsByUri, selectClaimsByUri,
selectClaimListMineIsPending, selectIsFetchingClaimListMine,
selectMyClaimsOutpoints, selectMyClaimsOutpoints,
} from "selectors/claims"; } from "selectors/claims";
import { import {
selectFileListIsPending, selectIsFetchingFileList,
selectFileInfosByOutpoint, selectFileInfosByOutpoint,
selectUrisLoading, selectUrisLoading,
selectTotalDownloadProgress,
} from "selectors/file_info"; } from "selectors/file_info";
import { doCloseModal } from "actions/app"; import { doCloseModal, doHistoryBack } from "actions/app";
import setProgressBar from "util/setProgressBar";
import batchActions from "util/batchActions";
const { shell } = require("electron"); const { shell } = require("electron");
@ -48,16 +51,16 @@ export function doFetchFileInfo(uri) {
export function doFileList() { export function doFileList() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
const isPending = selectFileListIsPending(state); const isFetching = selectIsFetchingFileList(state);
if (!isPending) { if (!isFetching) {
dispatch({ dispatch({
type: types.FILE_LIST_STARTED, type: types.FILE_LIST_STARTED,
}); });
lbry.file_list().then(fileInfos => { lbry.file_list().then(fileInfos => {
dispatch({ dispatch({
type: types.FILE_LIST_COMPLETED, type: types.FILE_LIST_SUCCEEDED,
data: { data: {
fileInfos, fileInfos,
}, },
@ -69,7 +72,10 @@ export function doFileList() {
export function doOpenFileInShell(fileInfo) { export function doOpenFileInShell(fileInfo) {
return function(dispatch, getState) { return function(dispatch, getState) {
shell.openItem(fileInfo.download_path); const success = shell.openItem(fileInfo.download_path);
if (!success) {
dispatch(doOpenFileInFolder(fileInfo));
}
}; };
} }
@ -102,14 +108,12 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
}, },
}); });
const success = () => { const success = dispatch({
dispatch({ type: types.ABANDON_CLAIM_SUCCEEDED,
type: types.ABANDON_CLAIM_COMPLETED, data: {
data: { claimId: fileInfo.claim_id,
claimId: fileInfo.claim_id, },
}, });
});
};
lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success); lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success);
} }
} }
@ -121,17 +125,32 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
}, },
}); });
dispatch(doCloseModal()); const totalProgress = selectTotalDownloadProgress(getState());
setProgressBar(totalProgress);
};
}
export function doDeleteFileAndGoBack(
fileInfo,
deleteFromComputer,
abandonClaim
) {
return function(dispatch, getState) {
const actions = [];
actions.push(doCloseModal());
actions.push(doHistoryBack());
actions.push(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim));
dispatch(batchActions(...actions));
}; };
} }
export function doFetchFileInfosAndPublishedClaims() { export function doFetchFileInfosAndPublishedClaims() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(), const state = getState(),
isClaimListMinePending = selectClaimListMineIsPending(state), isFetchingClaimListMine = selectIsFetchingClaimListMine(state),
isFileInfoListPending = selectFileListIsPending(state); isFetchingFileInfo = selectIsFetchingFileList(state);
dispatch(doFetchClaimListMine()); if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine());
dispatch(doFileList()); if (!isFetchingFileInfo) dispatch(doFileList());
}; };
} }

View file

@ -1,4 +1,5 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import * as modals from "constants/modal_types";
import lbryio from "lbryio"; import lbryio from "lbryio";
import rewards from "rewards"; import rewards from "rewards";
import { selectRewardsByType } from "selectors/rewards"; import { selectRewardsByType } from "selectors/rewards";
@ -58,6 +59,12 @@ export function doClaimReward(reward, saveError = false) {
reward, reward,
}, },
}); });
if (reward.reward_type == rewards.TYPE_NEW_USER) {
dispatch({
type: types.OPEN_MODAL,
data: { modal: modals.FIRST_REWARD },
});
}
}; };
const failure = error => { const failure = error => {
@ -99,9 +106,7 @@ export function doClaimEligiblePurchaseRewards() {
if (unclaimedType) { if (unclaimedType) {
dispatch(doClaimRewardType(unclaimedType)); dispatch(doClaimRewardType(unclaimedType));
} }
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) { dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}
}; };
} }

View file

@ -1,5 +1,8 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import batchActions from "util/batchActions";
import lbry from "lbry"; import lbry from "lbry";
import fs from "fs";
import http from "http";
export function doFetchDaemonSettings() { export function doFetchDaemonSettings() {
return function(dispatch, getState) { return function(dispatch, getState) {
@ -41,3 +44,296 @@ export function doSetClientSetting(key, value) {
}, },
}; };
} }
export function doResolveLanguage(locale) {
const langs = {
aa: ["Afar", "Afar"],
ab: ["Abkhazian", "Аҧсуа"],
af: ["Afrikaans", "Afrikaans"],
ak: ["Akan", "Akana"],
am: ["Amharic", "አማርኛ"],
an: ["Aragonese", "Aragonés"],
ar: ["Arabic", "العربية"],
as: ["Assamese", "অসমীয়া"],
av: ["Avar", "Авар"],
ay: ["Aymara", "Aymar"],
az: ["Azerbaijani", "Azərbaycanca / آذربايجان"],
ba: ["Bashkir", "Башҡорт"],
be: ["Belarusian", "Беларуская"],
bg: ["Bulgarian", "Български"],
bh: ["Bihari", "भोजपुरी"],
bi: ["Bislama", "Bislama"],
bm: ["Bambara", "Bamanankan"],
bn: ["Bengali", "বাংলা"],
bo: ["Tibetan", "བོད་ཡིག / Bod skad"],
br: ["Breton", "Brezhoneg"],
bs: ["Bosnian", "Bosanski"],
ca: ["Catalan", "Català"],
ce: ["Chechen", "Нохчийн"],
ch: ["Chamorro", "Chamoru"],
co: ["Corsican", "Corsu"],
cr: ["Cree", "Nehiyaw"],
cs: ["Czech", "Česky"],
cu: ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"],
cv: ["Chuvash", "Чăваш"],
cy: ["Welsh", "Cymraeg"],
da: ["Danish", "Dansk"],
de: ["German", "Deutsch"],
dv: ["Divehi", "ދިވެހިބަސް"],
dz: ["Dzongkha", "ཇོང་ཁ"],
ee: ["Ewe", "Ɛʋɛ"],
el: ["Greek", "Ελληνικά"],
en: ["English", "English"],
eo: ["Esperanto", "Esperanto"],
es: ["Spanish", "Español"],
et: ["Estonian", "Eesti"],
eu: ["Basque", "Euskara"],
fa: ["Persian", "فارسی"],
ff: ["Peul", "Fulfulde"],
fi: ["Finnish", "Suomi"],
fj: ["Fijian", "Na Vosa Vakaviti"],
fo: ["Faroese", "Føroyskt"],
fr: ["French", "Français"],
fy: ["West Frisian", "Frysk"],
ga: ["Irish", "Gaeilge"],
gd: ["Scottish Gaelic", "Gàidhlig"],
gl: ["Galician", "Galego"],
gn: ["Guarani", "Avañe'ẽ"],
gu: ["Gujarati", "ગુજરાતી"],
gv: ["Manx", "Gaelg"],
ha: ["Hausa", "هَوُسَ"],
he: ["Hebrew", "עברית"],
hi: ["Hindi", "हिन्दी"],
ho: ["Hiri Motu", "Hiri Motu"],
hr: ["Croatian", "Hrvatski"],
ht: ["Haitian", "Krèyol ayisyen"],
hu: ["Hungarian", "Magyar"],
hy: ["Armenian", "Հայերեն"],
hz: ["Herero", "Otsiherero"],
ia: ["Interlingua", "Interlingua"],
id: ["Indonesian", "Bahasa Indonesia"],
ie: ["Interlingue", "Interlingue"],
ig: ["Igbo", "Igbo"],
ii: ["Sichuan Yi", "ꆇꉙ / 四川彝语"],
ik: ["Inupiak", "Iñupiak"],
io: ["Ido", "Ido"],
is: ["Icelandic", "Íslenska"],
it: ["Italian", "Italiano"],
iu: ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"],
ja: ["Japanese", "日本語"],
jv: ["Javanese", "Basa Jawa"],
ka: ["Georgian", "ქართული"],
kg: ["Kongo", "KiKongo"],
ki: ["Kikuyu", "Gĩkũyũ"],
kj: ["Kuanyama", "Kuanyama"],
kk: ["Kazakh", "Қазақша"],
kl: ["Greenlandic", "Kalaallisut"],
km: ["Cambodian", "ភាសាខ្មែរ"],
kn: ["Kannada", "ಕನ್ನಡ"],
ko: ["Korean", "한국어"],
kr: ["Kanuri", "Kanuri"],
ks: ["Kashmiri", "कश्मीरी / كشميري"],
ku: ["Kurdish", "Kurdî / كوردی"],
kv: ["Komi", "Коми"],
kw: ["Cornish", "Kernewek"],
ky: ["Kirghiz", "Kırgızca / Кыргызча"],
la: ["Latin", "Latina"],
lb: ["Luxembourgish", "Lëtzebuergesch"],
lg: ["Ganda", "Luganda"],
li: ["Limburgian", "Limburgs"],
ln: ["Lingala", "Lingála"],
lo: ["Laotian", "ລາວ / Pha xa lao"],
lt: ["Lithuanian", "Lietuvių"],
lv: ["Latvian", "Latviešu"],
mg: ["Malagasy", "Malagasy"],
mh: ["Marshallese", "Kajin Majel / Ebon"],
mi: ["Maori", "Māori"],
mk: ["Macedonian", "Македонски"],
ml: ["Malayalam", "മലയാളം"],
mn: ["Mongolian", "Монгол"],
mo: ["Moldovan", "Moldovenească"],
mr: ["Marathi", "मराठी"],
ms: ["Malay", "Bahasa Melayu"],
mt: ["Maltese", "bil-Malti"],
my: ["Burmese", "Myanmasa"],
na: ["Nauruan", "Dorerin Naoero"],
nd: ["North Ndebele", "Sindebele"],
ne: ["Nepali", "नेपाली"],
ng: ["Ndonga", "Oshiwambo"],
nl: ["Dutch", "Nederlands"],
nn: ["Norwegian Nynorsk", "Norsk (nynorsk)"],
no: ["Norwegian", "Norsk (bokmål / riksmål)"],
nr: ["South Ndebele", "isiNdebele"],
nv: ["Navajo", "Diné bizaad"],
ny: ["Chichewa", "Chi-Chewa"],
oc: ["Occitan", "Occitan"],
oj: ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
om: ["Oromo", "Oromoo"],
or: ["Oriya", "ଓଡ଼ିଆ"],
os: ["Ossetian / Ossetic", "Иронау"],
pa: ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
pi: ["Pali", "Pāli / पाऴि"],
pl: ["Polish", "Polski"],
ps: ["Pashto", "پښتو"],
pt: ["Portuguese", "Português"],
qu: ["Quechua", "Runa Simi"],
rm: ["Raeto Romance", "Rumantsch"],
rn: ["Kirundi", "Kirundi"],
ro: ["Romanian", "Română"],
ru: ["Russian", "Русский"],
rw: ["Rwandi", "Kinyarwandi"],
sa: ["Sanskrit", "संस्कृतम्"],
sc: ["Sardinian", "Sardu"],
sd: ["Sindhi", "सिनधि"],
se: ["Northern Sami", "Sámegiella"],
sg: ["Sango", "Sängö"],
sh: ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"],
si: ["Sinhalese", "සිංහල"],
sk: ["Slovak", "Slovenčina"],
sl: ["Slovenian", "Slovenščina"],
sm: ["Samoan", "Gagana Samoa"],
sn: ["Shona", "chiShona"],
so: ["Somalia", "Soomaaliga"],
sq: ["Albanian", "Shqip"],
sr: ["Serbian", "Српски"],
ss: ["Swati", "SiSwati"],
st: ["Southern Sotho", "Sesotho"],
su: ["Sundanese", "Basa Sunda"],
sv: ["Swedish", "Svenska"],
sw: ["Swahili", "Kiswahili"],
ta: ["Tamil", "தமிழ்"],
te: ["Telugu", "తెలుగు"],
tg: ["Tajik", "Тоҷикӣ"],
th: ["Thai", "ไทย / Phasa Thai"],
ti: ["Tigrinya", "ትግርኛ"],
tk: ["Turkmen", "Туркмен / تركمن"],
tl: ["Tagalog / Filipino", "Tagalog"],
tn: ["Tswana", "Setswana"],
to: ["Tonga", "Lea Faka-Tonga"],
tr: ["Turkish", "Türkçe"],
ts: ["Tsonga", "Xitsonga"],
tt: ["Tatar", "Tatarça"],
tw: ["Twi", "Twi"],
ty: ["Tahitian", "Reo Mā`ohi"],
ug: ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"],
uk: ["Ukrainian", "Українська"],
ur: ["Urdu", "اردو"],
uz: ["Uzbek", "Ўзбек"],
ve: ["Venda", "Tshivenḓa"],
vi: ["Vietnamese", "Tiếng Việt"],
vo: ["Volapük", "Volapük"],
wa: ["Walloon", "Walon"],
wo: ["Wolof", "Wollof"],
xh: ["Xhosa", "isiXhosa"],
yi: ["Yiddish", "ייִדיש"],
yo: ["Yoruba", "Yorùbá"],
za: ["Zhuang", "Cuengh / Tôô / 壮语"],
zh: ["Chinese", "中文"],
zu: ["Zulu", "isiZulu"],
};
const lang = locale.substring(0, 2);
return {
type: types.LANGUAGE_RESOLVED,
data: { key: locale, value: `${langs[lang][0]} (${langs[lang][1]})` },
};
}
export function doDownloadLanguage(lang, destinationPath) {
return function(dispatch, getState) {
const plainLanguage = lang.replace(".json", "");
dispatch({
type: types.DOWNLOAD_LANGUAGE_STARTED,
data: plainLanguage,
});
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destinationPath);
const req = http.request(
`http://i18n.lbry.io/langs/${lang}`,
response => {
file.on("finish", () => {
file.close();
// push to our local list
dispatch({
type: types.DOWNLOAD_LANGUAGE_SUCCEEDED,
data: plainLanguage,
});
resolve();
});
response.pipe(file);
}
);
const errorHandler = err => {
if (file) {
file.close();
}
fs.unlink(destinationPath); // Delete the file async. (But we don't check the result)
dispatch({
type: types.DOWNLOAD_LANGUAGE_FAILED,
data: plainLanguage,
});
reject(err);
};
req.on("error", errorHandler);
file.on("error", errorHandler);
req.end();
});
};
}
export function doDownloadLanguages() {
return function(dispatch, getState) {
if (!fs.existsSync("app/locales")) {
fs.mkdirSync("app/locales");
}
dispatch({
type: types.DOWNLOAD_LANGUAGES_STARTED,
data: {},
});
return new Promise((resolve, reject) => {
const req = http.request(
{ host: "i18n.lbry.io", path: "/" },
response => {
let str = "";
response.on("data", chunk => {
str += chunk;
});
response.on("end", () => {
const files = JSON.parse(str);
const actions = [];
files.forEach(file => {
actions.push(doDownloadLanguage(file, `app/locales/${file}`));
});
dispatch(batchActions(...actions));
dispatch({
type: types.DOWNLOAD_LANGUAGES_COMPLETED,
data: {},
});
resolve();
});
}
);
req.on("error", err => {
reject(err);
});
req.end();
});
};
}

View file

@ -1,8 +1,11 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import * as modals from "constants/modal_types";
import lbryio from "lbryio"; import lbryio from "lbryio";
import { setLocal } from "utils"; import { setLocal } from "utils";
import { doRewardList } from "actions/rewards"; import { doOpenModal } from "actions/app";
import { selectEmailToVerify } from "selectors/user"; import { doRewardList, doClaimRewardType } from "actions/rewards";
import { selectEmailToVerify, selectUser } from "selectors/user";
import rewards from "rewards";
export function doAuthenticate() { export function doAuthenticate() {
return function(dispatch, getState) { return function(dispatch, getState) {
@ -19,6 +22,7 @@ export function doAuthenticate() {
dispatch(doRewardList()); dispatch(doRewardList());
}) })
.catch(error => { .catch(error => {
dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE));
dispatch({ dispatch({
type: types.AUTHENTICATION_FAILURE, type: types.AUTHENTICATION_FAILURE,
data: { error }, data: { error },
@ -136,3 +140,45 @@ export function doUserEmailVerify(verificationToken) {
}); });
}; };
} }
export function doUserIdentityVerify(stripeToken) {
return function(dispatch, getState) {
dispatch({
type: types.USER_IDENTITY_VERIFY_STARTED,
token: stripeToken,
});
lbryio
.call("user", "verify_identity", { stripe_token: stripeToken }, "post")
.then(user => {
if (user.is_identity_verified) {
dispatch({
type: types.USER_IDENTITY_VERIFY_SUCCESS,
data: { user },
});
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
} else {
throw new Error(
"Your identity is still not verified. This should not happen."
); //shouldn't happen
}
})
.catch(error => {
dispatch({
type: types.USER_IDENTITY_VERIFY_FAILURE,
data: { error: error.toString() },
});
});
};
}
export function doFetchAccessToken() {
return function(dispatch, getState) {
const success = token =>
dispatch({
type: types.FETCH_ACCESS_TOKEN_SUCCESS,
data: { token },
});
lbryio.getAuthToken().then(success);
};
}

View file

@ -2,7 +2,9 @@ import store from "store.js";
import lbry from "./lbry.js"; import lbry from "./lbry.js";
const env = ENV; const env = ENV;
const config = require(`./config/${env}`); const config = {
...require(`./config/${env}`),
};
const language = lbry.getClientSetting("language") const language = lbry.getClientSetting("language")
? lbry.getClientSetting("language") ? lbry.getClientSetting("language")
: "en"; : "en";

View file

@ -1,19 +1,33 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectCurrentModal } from "selectors/app"; import { selectCurrentModal } from "selectors/app";
import { doCheckUpgradeAvailable, doAlertError } from "actions/app"; import {
import { doUpdateBalance } from "actions/wallet"; doCheckUpgradeAvailable,
import App from "./view"; doOpenModal,
doAlertError,
doRecordScroll,
} from "actions/app";
import { doFetchRewardedContent } from "actions/content";
const select = state => ({ import { doUpdateBalance } from "actions/wallet";
import { selectWelcomeModalAcknowledged } from "selectors/app";
import { selectUser } from "selectors/user";
import App from "./view";
import * as modals from "constants/modal_types";
const select = (state, props) => ({
modal: selectCurrentModal(state), modal: selectCurrentModal(state),
isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state),
user: selectUser(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
alertError: errorList => dispatch(doAlertError(errorList)), alertError: errorList => dispatch(doAlertError(errorList)),
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()), checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)),
updateBalance: balance => dispatch(doUpdateBalance(balance)), updateBalance: balance => dispatch(doUpdateBalance(balance)),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),
}); });
export default connect(select, perform)(App); export default connect(select, perform)(App);

View file

@ -2,25 +2,64 @@ import React from "react";
import Router from "component/router"; import Router from "component/router";
import Header from "component/header"; import Header from "component/header";
import ModalError from "component/modalError"; import ModalError from "component/modalError";
import ModalAuthFailure from "component/modalAuthFailure";
import ModalDownloading from "component/modalDownloading"; import ModalDownloading from "component/modalDownloading";
import UpgradeModal from "component/modalUpgrade"; import ModalInsufficientCredits from "component/modalInsufficientCredits";
import WelcomeModal from "component/modalWelcome"; import ModalUpgrade from "component/modalUpgrade";
import ModalWelcome from "component/modalWelcome";
import ModalFirstReward from "component/modalFirstReward";
import lbry from "lbry"; import lbry from "lbry";
import { Line } from "rc-progress"; import * as modals from "constants/modal_types";
class App extends React.PureComponent { class App extends React.PureComponent {
componentWillMount() { componentWillMount() {
const {
alertError,
checkUpgradeAvailable,
updateBalance,
fetchRewardedContent,
} = this.props;
document.addEventListener("unhandledError", event => { document.addEventListener("unhandledError", event => {
this.props.alertError(event.detail); alertError(event.detail);
}); });
if (!this.props.upgradeSkipped) { if (!this.props.upgradeSkipped) {
this.props.checkUpgradeAvailable(); checkUpgradeAvailable();
} }
lbry.balanceSubscribe(balance => { lbry.balanceSubscribe(balance => {
this.props.updateBalance(balance); updateBalance(balance);
}); });
fetchRewardedContent();
this.showWelcome(this.props);
this.scrollListener = () => this.props.recordScroll(window.scrollY);
window.addEventListener("scroll", this.scrollListener);
}
componentWillReceiveProps(nextProps) {
this.showWelcome(nextProps);
}
showWelcome(props) {
const { isWelcomeAcknowledged, openWelcomeModal, user } = props;
if (
!isWelcomeAcknowledged &&
user &&
!user.is_reward_approved &&
!user.is_identity_verified
) {
openWelcomeModal();
}
}
componentWillUnmount() {
window.removeEventListener("scroll", this.scrollListener);
} }
render() { render() {
@ -32,10 +71,13 @@ class App extends React.PureComponent {
<div id="main-content"> <div id="main-content">
<Router /> <Router />
</div> </div>
{modal == "upgrade" && <UpgradeModal />} {modal == modals.UPGRADE && <ModalUpgrade />}
{modal == "downloading" && <ModalDownloading />} {modal == modals.DOWNLOADING && <ModalDownloading />}
{modal == "error" && <ModalError />} {modal == modals.ERROR && <ModalError />}
{modal == "welcome" && <WelcomeModal />} {modal == modals.INSUFFICIENT_CREDITS && <ModalInsufficientCredits />}
{modal == modals.WELCOME && <ModalWelcome />}
{modal == modals.FIRST_REWARD && <ModalFirstReward />}
{modal == modals.AUTHENTICATION_FAILURE && <ModalAuthFailure />}
</div> </div>
); );
} }

View file

@ -1,16 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import {
selectAuthenticationIsPending,
selectEmailToVerify,
selectUserIsVerificationCandidate,
} from "selectors/user";
import Auth from "./view";
const select = state => ({
isPending: selectAuthenticationIsPending(state),
email: selectEmailToVerify(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
export default connect(select, null)(Auth);

View file

@ -1,22 +0,0 @@
import React from "react";
import { BusyMessage } from "component/common";
import UserEmailNew from "component/userEmailNew";
import UserEmailVerify from "component/userEmailVerify";
export class Auth extends React.PureComponent {
render() {
const { isPending, email, isVerificationCandidate } = this.props;
if (isPending) {
return <BusyMessage message={__("Authenticating")} />;
} else if (!email) {
return <UserEmailNew />;
} else if (isVerificationCandidate) {
return <UserEmailVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
}
export default Auth;

View file

@ -1,33 +0,0 @@
import React from "react";
import * as modal from "constants/modal_types";
import rewards from "rewards.js";
import { connect } from "react-redux";
import { doUserEmailDecline } from "actions/user";
import { doOpenModal } from "actions/app";
import {
selectAuthenticationIsPending,
selectUserHasEmail,
selectUserIsAuthRequested,
} from "selectors/user";
import { makeSelectHasClaimedReward } from "selectors/rewards";
import AuthOverlay from "./view";
const select = (state, props) => {
const selectHasClaimed = makeSelectHasClaimedReward();
return {
hasEmail: selectUserHasEmail(state),
isPending: selectAuthenticationIsPending(state),
isShowing: selectUserIsAuthRequested(state),
hasNewUserReward: selectHasClaimed(state, {
reward_type: rewards.TYPE_NEW_USER,
}),
};
};
const perform = dispatch => ({
userEmailDecline: () => dispatch(doUserEmailDecline()),
openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)),
});
export default connect(select, perform)(AuthOverlay);

View file

@ -1,90 +0,0 @@
import React from "react";
import lbryio from "lbryio.js";
import ModalPage from "component/modal-page.js";
import Auth from "component/auth";
import Link from "component/link";
import { getLocal, setLocal } from "utils";
export class AuthOverlay extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showNoEmailConfirm: false,
};
}
componentWillReceiveProps(nextProps) {
if (
this.props.isShowing &&
!this.props.isPending &&
!nextProps.hasNewUserReward &&
!nextProps.isShowing /* && !getLocal("welcome_screen_shown")*/
) {
setLocal("welcome_screen_shown", true);
setTimeout(() => this.props.openWelcomeModal(), 1);
}
}
onEmailSkipClick() {
this.setState({ showNoEmailConfirm: true });
}
onEmailSkipConfirm() {
this.props.userEmailDecline();
}
render() {
if (!lbryio.enabled) {
return null;
}
const { isPending, isShowing, hasEmail } = this.props;
if (isShowing) {
return (
<ModalPage
className="modal-page--full"
isOpen={true}
contentLabel="Authentication"
>
<h1>LBRY Early Access</h1>
<Auth />
{isPending
? ""
: <div className="form-row-submit">
{!hasEmail && this.state.showNoEmailConfirm
? <div className="help form-input-width">
<p>
{__(
"If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications."
)}
</p>
<Link
onClick={() => {
this.onEmailSkipConfirm();
}}
label={__("Continue without email")}
/>
</div>
: <Link
className={"button-text-help"}
onClick={() => {
hasEmail
? this.onEmailSkipConfirm()
: this.onEmailSkipClick();
}}
label={
hasEmail ? __("Skip for now") : __("Do I have to?")
}
/>}
</div>}
</ModalPage>
);
}
return null;
}
}
export default AuthOverlay;

View file

@ -0,0 +1,8 @@
import React from "react";
import { connect } from "react-redux";
import CardMedia from "./view";
const select = state => ({});
const perform = dispatch => ({});
export default connect(select, perform)(CardMedia);

View file

@ -0,0 +1,54 @@
import React from "react";
class CardMedia extends React.PureComponent {
static AUTO_THUMB_CLASSES = [
"purple",
"red",
"pink",
"indigo",
"blue",
"light-blue",
"cyan",
"teal",
"green",
"yellow",
"orange",
];
componentWillMount() {
this.setState({
autoThumbClass:
CardMedia.AUTO_THUMB_CLASSES[
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
],
});
}
render() {
const { title, thumbnail } = this.props;
const atClass = this.state.autoThumbClass;
if (thumbnail) {
return (
<div
className="card__media"
style={{ backgroundImage: "url('" + thumbnail + "')" }}
/>
);
}
return (
<div className={`card__media card__media--autothumb ${atClass}`}>
<div className="card__autothumb__text">
{title &&
title
.replace(/\s+/g, "")
.substring(0, Math.min(title.replace(" ", "").length, 5))
.toUpperCase()}
</div>
</div>
);
}
}
export default CardMedia;

View file

@ -0,0 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import { selectUserEmail } from "selectors/user";
import CardVerify from "./view";
const select = state => ({
email: selectUserEmail(state),
});
const perform = dispatch => ({});
export default connect(select, perform)(CardVerify);

View file

@ -0,0 +1,180 @@
import React from "react";
import PropTypes from "prop-types";
import Link from "component/link";
let scriptLoading = false;
let scriptLoaded = false;
let scriptDidError = false;
class CardVerify extends React.Component {
static propTypes = {
disabled: PropTypes.bool,
label: PropTypes.string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: PropTypes.string.isRequired,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
open: false,
};
}
componentDidMount() {
if (scriptLoaded) {
return;
}
if (scriptLoading) {
return;
}
scriptLoading = true;
const script = document.createElement("script");
script.src = "https://checkout.stripe.com/checkout.js";
script.async = 1;
this.loadPromise = (() => {
let canceled = false;
const promise = new Promise((resolve, reject) => {
script.onload = () => {
scriptLoaded = true;
scriptLoading = false;
resolve();
this.onScriptLoaded();
};
script.onerror = event => {
scriptDidError = true;
scriptLoading = false;
reject(event);
this.onScriptError(event);
};
});
const wrappedPromise = new Promise((accept, cancel) => {
promise.then(
() => (canceled ? cancel({ isCanceled: true }) : accept())
);
promise.catch(
error => (canceled ? cancel({ isCanceled: true }) : cancel(error))
);
});
return {
promise: wrappedPromise,
cancel() {
canceled = true;
},
};
})();
this.loadPromise.promise
.then(this.onScriptLoaded)
.catch(this.onScriptError);
document.body.appendChild(script);
}
componentDidUpdate() {
if (!scriptLoading) {
this.updateStripeHandler();
}
}
componentWillUnmount() {
if (this.loadPromise) {
this.loadPromise.cancel();
}
if (CardVerify.stripeHandler && this.state.open) {
CardVerify.stripeHandler.close();
}
}
onScriptLoaded = () => {
if (!CardVerify.stripeHandler) {
CardVerify.stripeHandler = StripeCheckout.configure({
key: this.props.stripeKey,
});
if (this.hasPendingClick) {
this.showStripeDialog();
}
}
};
onScriptError = (...args) => {
throw new Error("Unable to load credit validation script.");
};
onClosed = () => {
this.setState({ open: false });
};
updateStripeHandler() {
if (!CardVerify.stripeHandler) {
CardVerify.stripeHandler = StripeCheckout.configure({
key: this.props.stripeKey,
});
}
}
showStripeDialog() {
this.setState({ open: true });
CardVerify.stripeHandler.open({
allowRememberMe: false,
closed: this.onClosed,
description: __("Confirm Identity"),
email: this.props.email,
locale: "auto",
panelLabel: "Verify",
token: this.props.token,
zipCode: true,
});
}
onClick = () => {
if (scriptDidError) {
try {
throw new Error(
"Tried to call onClick, but StripeCheckout failed to load"
);
} catch (x) {}
} else if (CardVerify.stripeHandler) {
this.showStripeDialog();
} else {
this.hasPendingClick = true;
}
};
render() {
return (
<Link
button="primary"
label={this.props.label}
icon="icon-lock"
disabled={
this.props.disabled || this.state.open || this.hasPendingClick
}
onClick={this.onClick.bind(this)}
/>
);
}
}
export default CardVerify;

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { formatCredits } from "utils";
import lbry from "../lbry.js"; import lbry from "../lbry.js";
//component/icon.js //component/icon.js
@ -78,7 +79,7 @@ export class CreditAmount extends React.PureComponent {
}; };
render() { render() {
const formattedAmount = lbry.formatCredits( const formattedAmount = formatCredits(
this.props.amount, this.props.amount,
this.props.precision this.props.precision
); );
@ -140,7 +141,7 @@ export class Address extends React.PureComponent {
}} }}
style={addressStyle} style={addressStyle}
readOnly="readonly" readOnly="readonly"
value={this.props.address} value={this.props.address || ""}
/> />
); );
} }

View file

@ -13,7 +13,7 @@ import { doCloseModal, doOpenModal } from "actions/app";
import { doFetchAvailability } from "actions/availability"; import { doFetchAvailability } from "actions/availability";
import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info"; import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info";
import { makeSelectClaimForUriIsMine } from "selectors/claims"; import { makeSelectClaimForUriIsMine } from "selectors/claims";
import { doPurchaseUri, doLoadVideo } from "actions/content"; import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content";
import FileActions from "./view"; import FileActions from "./view";
const makeSelect = () => { const makeSelect = () => {
@ -47,6 +47,7 @@ const perform = dispatch => ({
openModal: modal => dispatch(doOpenModal(modal)), openModal: modal => dispatch(doOpenModal(modal)),
startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")), startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")),
loadVideo: uri => dispatch(doLoadVideo(uri)), loadVideo: uri => dispatch(doLoadVideo(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
}); });
export default connect(makeSelect, perform)(FileActions); export default connect(makeSelect, perform)(FileActions);

View file

@ -22,6 +22,21 @@ class FileActions extends React.PureComponent {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.checkAvailability(nextProps.uri); 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) { checkAvailability(uri) {
@ -142,6 +157,8 @@ class FileActions extends React.PureComponent {
onClick={() => openInShell(fileInfo)} onClick={() => openInShell(fileInfo)}
/> />
); );
} else if (!fileInfo) {
content = <BusyMessage message={__("Fetching file info")} />;
} else { } else {
console.log("handle this case of file action props?"); console.log("handle this case of file action props?");
} }
@ -176,13 +193,6 @@ class FileActions extends React.PureComponent {
</strong>{" "} </strong>{" "}
{__("credits")}. {__("credits")}.
</Modal> </Modal>
<Modal
isOpen={modal == "notEnoughCredits"}
contentLabel={__("Not enough credits")}
onConfirmed={closeModal}
>
{__("You don't have enough LBRY credits to pay for this stream.")}
</Modal>
<Modal <Modal
isOpen={modal == "timedOut"} isOpen={modal == "timedOut"}
contentLabel={__("Download failed")} contentLabel={__("Download failed")}

View file

@ -8,7 +8,10 @@ import {
makeSelectMetadataForUri, makeSelectMetadataForUri,
} from "selectors/claims"; } from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info"; import { makeSelectFileInfoForUri } from "selectors/file_info";
import { makeSelectIsResolvingForUri } from "selectors/content"; import {
makeSelectIsResolvingForUri,
selectRewardContentClaimIds,
} from "selectors/content";
import FileCard from "./view"; import FileCard from "./view";
const makeSelect = () => { const makeSelect = () => {
@ -22,6 +25,7 @@ const makeSelect = () => {
fileInfo: selectFileInfoForUri(state, props), fileInfo: selectFileInfoForUri(state, props),
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
metadata: selectMetadataForUri(state, props), metadata: selectMetadataForUri(state, props),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
isResolvingUri: selectResolvingUri(state, props), isResolvingUri: selectResolvingUri(state, props),
}); });

View file

@ -1,10 +1,13 @@
import React from "react"; import React from "react";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import CardMedia from "component/cardMedia";
import Link from "component/link"; import Link from "component/link";
import { TruncatedText, Icon } from "component/common"; import { TruncatedText, Icon } from "component/common";
import IconFeatured from "component/iconFeatured";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
import UriIndicator from "component/uriIndicator"; import UriIndicator from "component/uriIndicator";
import NsfwOverlay from "component/nsfwOverlay"; import NsfwOverlay from "component/nsfwOverlay";
import TruncatedMarkdown from "component/truncatedMarkdown";
class FileCard extends React.PureComponent { class FileCard extends React.PureComponent {
constructor(props) { constructor(props) {
@ -44,11 +47,23 @@ class FileCard extends React.PureComponent {
} }
render() { render() {
const { claim, fileInfo, metadata, isResolvingUri, navigate } = this.props; const {
claim,
fileInfo,
metadata,
isResolvingUri,
navigate,
rewardedContentClaimIds,
} = this.props;
const uri = lbryuri.normalize(this.props.uri); const uri = lbryuri.normalize(this.props.uri);
const title = metadata && metadata.title ? metadata.title : uri; const title = metadata && metadata.title ? metadata.title : uri;
const thumbnail = metadata && metadata.thumbnail
? metadata.thumbnail
: null;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent =
claim && rewardedContentClaimIds.includes(claim.claim_id);
let description = ""; let description = "";
if (isResolvingUri && !claim) { if (isResolvingUri && !claim) {
@ -73,28 +88,23 @@ class FileCard extends React.PureComponent {
onClick={() => navigate("/show", { uri })} onClick={() => navigate("/show", { uri })}
className="card__link" className="card__link"
> >
<CardMedia title={title} thumbnail={thumbnail} />
<div className="card__title-identity"> <div className="card__title-identity">
<h5 title={title}> <div className="card__title" title={title}>
<TruncatedText lines={1}>{title}</TruncatedText> <TruncatedText lines={1}>{title}</TruncatedText>
</h5> </div>
<div className="card__subtitle"> <div className="card__subtitle">
<span style={{ float: "right" }}> <span style={{ float: "right" }}>
<FilePrice uri={uri} /> <FilePrice uri={uri} />
{fileInfo {isRewardContent && <span>{" "}<IconFeatured /></span>}
? <span>{" "}<Icon fixed icon="icon-folder" /></span> {fileInfo &&
: ""} <span>{" "}<Icon fixed icon="icon-folder" /></span>}
</span> </span>
<UriIndicator uri={uri} /> <UriIndicator uri={uri} />
</div> </div>
</div> </div>
{metadata &&
metadata.thumbnail &&
<div
className="card__media"
style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}
/>}
<div className="card__content card__subtext card__subtext--two-lines"> <div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>{description}</TruncatedText> <TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
</div> </div>
</Link> </Link>
</div> </div>

View file

@ -2,7 +2,7 @@ import React from "react";
import lbry from "lbry.js"; import lbry from "lbry.js";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import Link from "component/link"; import Link from "component/link";
import { FormField } from "component/form.js"; import FormField from "component/formField";
import FileTile from "component/fileTile"; import FileTile from "component/fileTile";
import rewards from "rewards.js"; import rewards from "rewards.js";
import lbryio from "lbryio.js"; import lbryio from "lbryio.js";
@ -67,7 +67,9 @@ class FileList extends React.PureComponent {
const content = []; const content = [];
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => { this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
let uriParams = {}; let uriParams = {
claimId: fileInfo.claim_id,
};
if (fileInfo.channel_name) { if (fileInfo.channel_name) {
uriParams.channelName = fileInfo.channel_name; uriParams.channelName = fileInfo.channel_name;
uriParams.contentName = fileInfo.name; uriParams.contentName = fileInfo.name;
@ -79,7 +81,7 @@ class FileList extends React.PureComponent {
content.push( content.push(
<FileTile <FileTile
key={uri} key={fileInfo.outpoint || fileInfo.claim_id}
uri={uri} uri={uri}
hidePrice={true} hidePrice={true}
showEmpty={this.props.fileTileShowEmpty} showEmpty={this.props.fileTileShowEmpty}
@ -94,7 +96,6 @@ class FileList extends React.PureComponent {
<FormField type="select" onChange={this.handleSortChanged.bind(this)}> <FormField type="select" onChange={this.handleSortChanged.bind(this)}>
<option value="date">{__("Date")}</option> <option value="date">{__("Date")}</option>
<option value="title">{__("Title")}</option> <option value="title">{__("Title")}</option>
<option value="filename">{__("File name")}</option>
</FormField> </FormField>
</span> </span>
{content} {content}

View file

@ -67,7 +67,7 @@ class FileListSearch extends React.PureComponent {
{results && !!results.length {results && !!results.length
? <FileListSearchResults {...this.props} /> ? <FileListSearchResults {...this.props} />
: <SearchNoResults {...this.props} />} : !isSearching && <SearchNoResults {...this.props} />}
</div> </div>
); );
} }

View file

@ -8,7 +8,10 @@ import {
} from "selectors/claims"; } from "selectors/claims";
import { makeSelectFileInfoForUri } from "selectors/file_info"; import { makeSelectFileInfoForUri } from "selectors/file_info";
import { selectShowNsfw } from "selectors/settings"; import { selectShowNsfw } from "selectors/settings";
import { makeSelectIsResolvingForUri } from "selectors/content"; import {
makeSelectIsResolvingForUri,
selectRewardContentClaimIds,
} from "selectors/content";
import FileTile from "./view"; import FileTile from "./view";
const makeSelect = () => { const makeSelect = () => {
@ -23,6 +26,7 @@ const makeSelect = () => {
obscureNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
metadata: selectMetadataForUri(state, props), metadata: selectMetadataForUri(state, props),
isResolvingUri: selectResolvingUri(state, props), isResolvingUri: selectResolvingUri(state, props),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
}); });
return select; return select;

View file

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import lbry from "lbry.js";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import CardMedia from "component/cardMedia";
import Link from "component/link"; import Link from "component/link";
import { TruncatedText } from "component/common.js"; import { TruncatedText } from "component/common.js";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
import NsfwOverlay from "component/nsfwOverlay"; import NsfwOverlay from "component/nsfwOverlay";
import IconFeatured from "component/iconFeatured";
class FileTile extends React.PureComponent { class FileTile extends React.PureComponent {
static SHOW_EMPTY_PUBLISH = "publish"; static SHOW_EMPTY_PUBLISH = "publish";
@ -57,6 +58,7 @@ class FileTile extends React.PureComponent {
showEmpty, showEmpty,
navigate, navigate,
hidePrice, hidePrice,
rewardedContentClaimIds,
} = this.props; } = this.props;
const uri = lbryuri.normalize(this.props.uri); const uri = lbryuri.normalize(this.props.uri);
@ -64,8 +66,14 @@ class FileTile extends React.PureComponent {
const isClaimable = lbryuri.isClaimable(uri); const isClaimable = lbryuri.isClaimable(uri);
const title = isClaimed && metadata && metadata.title const title = isClaimed && metadata && metadata.title
? metadata.title ? metadata.title
: uri; : lbryuri.parse(uri).contentName;
const thumbnail = metadata && metadata.thumbnail
? metadata.thumbnail
: null;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent =
claim && rewardedContentClaimIds.includes(claim.claim_id);
let onClick = () => navigate("/show", { uri }); let onClick = () => navigate("/show", { uri });
let description = ""; let description = "";
@ -98,22 +106,15 @@ class FileTile extends React.PureComponent {
> >
<Link onClick={onClick} className="card__link"> <Link onClick={onClick} className="card__link">
<div className={"card__inner file-tile__row"}> <div className={"card__inner file-tile__row"}>
<div <CardMedia title={title} thumbnail={thumbnail} />
className="card__media"
style={{
backgroundImage:
"url('" +
(metadata && metadata.thumbnail
? metadata.thumbnail
: lbry.imagePath("default-thumb.svg")) +
"')",
}}
/>
<div className="file-tile__content"> <div className="file-tile__content">
<div className="card__title-primary"> <div className="card__title-primary">
{!hidePrice ? <FilePrice uri={this.props.uri} /> : null} {!hidePrice ? <FilePrice uri={this.props.uri} /> : null}
{isRewardContent && <IconFeatured />}
<div className="meta">{uri}</div> <div className="meta">{uri}</div>
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3> <h3>
<TruncatedText lines={1}>{title}</TruncatedText>
</h3>
</div> </div>
<div className="card__content card__subtext"> <div className="card__content card__subtext">
<TruncatedText lines={3}> <TruncatedText lines={3}>

View file

@ -1,181 +1,32 @@
import React from "react"; import React from "react";
import FileSelector from "./file-selector.js"; import FormField from "component/formField";
import { Icon } from "./common.js";
var formFieldCounter = 0, let formFieldCounter = 0;
formFieldFileSelectorTypes = ["file", "directory"],
formFieldNestedLabelTypes = ["radio", "checkbox"];
function formFieldId() { export const formFieldNestedLabelTypes = ["radio", "checkbox"];
export function formFieldId() {
return "form-field-" + ++formFieldCounter; return "form-field-" + ++formFieldCounter;
} }
export class FormField extends React.PureComponent {
static propTypes = {
type: React.PropTypes.string.isRequired,
prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool,
};
constructor(props) {
super(props);
this._fieldRequiredText = __("This field is required");
this._type = null;
this._element = null;
this.state = {
isError: null,
errorMessage: null,
};
}
componentWillMount() {
if (["text", "number", "radio", "checkbox"].includes(this.props.type)) {
this._element = "input";
this._type = this.props.type;
} else if (this.props.type == "text-number") {
this._element = "input";
this._type = "text";
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = "input";
this._type = "hidden";
} else {
// Non <input> field, e.g. <select>, <textarea>
this._element = this.props.type;
}
}
componentDidMount() {
/**
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
* https://github.com/facebook/react/issues/3468
*/
if (this.props.type == "directory") {
this.refs.field.webkitdirectory = true;
}
}
handleFileChosen(path) {
this.refs.field.value = path;
if (this.props.onChange) {
// Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event("change", { bubbles: true });
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
this.props.onChange(event);
}
}
showError(text) {
this.setState({
isError: true,
errorMessage: text,
});
}
focus() {
this.refs.field.focus();
}
getValue() {
if (this.props.type == "checkbox") {
return this.refs.field.checked;
} else {
return this.refs.field.value;
}
}
getSelectedElement() {
return this.refs.field.options[this.refs.field.selectedIndex];
}
render() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null
? this.state.isError
: this.props.hasError,
elementId = this.props.id ? this.props.id : formFieldId(),
renderElementInsideLabel =
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
delete otherProps.type;
delete otherProps.label;
delete otherProps.hasError;
delete otherProps.className;
delete otherProps.postfix;
delete otherProps.prefix;
const element = (
<this._element
id={elementId}
type={this._type}
name={this.props.name}
ref="field"
placeholder={this.props.placeholder}
className={
"form-field__input form-field__input-" +
this.props.type +
" " +
(this.props.className || "") +
(isError ? "form-field__input--error" : "")
}
{...otherProps}
>
{this.props.children}
</this._element>
);
return (
<div className={"form-field form-field--" + this.props.type}>
{this.props.prefix
? <span className="form-field__prefix">{this.props.prefix}</span>
: ""}
{renderElementInsideLabel
? <label
htmlFor={elementId}
className={
"form-field__label " +
(isError ? "form-field__label--error" : "")
}
>
{element}
{this.props.label}
</label>
: element}
{formFieldFileSelectorTypes.includes(this.props.type)
? <FileSelector
type={this.props.type}
onFileChosen={this.handleFileChosen.bind(this)}
{...(this.props.defaultValue
? { initPath: this.props.defaultValue }
: {})}
/>
: null}
{this.props.postfix
? <span className="form-field__postfix">{this.props.postfix}</span>
: ""}
{isError && this.state.errorMessage
? <div className="form-field__error">{this.state.errorMessage}</div>
: ""}
</div>
);
}
}
export class FormRow extends React.PureComponent { export class FormRow extends React.PureComponent {
static propTypes = { static propTypes = {
label: React.PropTypes.oneOfType([ label: React.PropTypes.oneOfType([
React.PropTypes.string, React.PropTypes.string,
React.PropTypes.element, React.PropTypes.element,
]), ]),
errorMessage: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.object,
]),
// helper: React.PropTypes.html, // helper: React.PropTypes.html,
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this._field = null;
this._fieldRequiredText = __("This field is required"); this._fieldRequiredText = __("This field is required");
this.state = this.getStateFromProps(props); this.state = this.getStateFromProps(props);
@ -190,7 +41,9 @@ export class FormRow extends React.PureComponent {
isError: !!props.errorMessage, isError: !!props.errorMessage,
errorMessage: typeof props.errorMessage === "string" errorMessage: typeof props.errorMessage === "string"
? props.errorMessage ? props.errorMessage
: "", : props.errorMessage instanceof Error
? props.errorMessage.toString()
: "",
}; };
} }
@ -213,15 +66,24 @@ export class FormRow extends React.PureComponent {
} }
getValue() { getValue() {
return this.refs.field.getValue(); return this._field.getValue();
} }
getSelectedElement() { getSelectedElement() {
return this.refs.field.getSelectedElement(); return this._field.getSelectedElement();
}
getOptions() {
if (!this._field || !this._field.getOptions) {
console.log(this);
console.log(this._field);
console.log(this._field.getOptions);
}
return this._field.getOptions();
} }
focus() { focus() {
this.refs.field.focus(); this._field.focus();
} }
render() { render() {
@ -257,7 +119,13 @@ export class FormRow extends React.PureComponent {
</label> </label>
</div> </div>
: ""} : ""}
<FormField ref="field" hasError={this.state.isError} {...fieldProps} /> <FormField
ref={ref => {
this._field = ref ? ref.getWrappedInstance() : null;
}}
hasError={this.state.isError}
{...fieldProps}
/>
{!this.state.isError && this.props.helper {!this.state.isError && this.props.helper
? <div className="form-field__helper">{this.props.helper}</div> ? <div className="form-field__helper">{this.props.helper}</div>
: ""} : ""}

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import FormField from "./view";
export default connect(null, null, null, { withRef: true })(FormField);

View file

@ -0,0 +1,178 @@
import React from "react";
import FileSelector from "component/file-selector.js";
import SimpleMDE from "react-simplemde-editor";
import { formFieldNestedLabelTypes, formFieldId } from "../form";
import style from "react-simplemde-editor/dist/simplemde.min.css";
const formFieldFileSelectorTypes = ["file", "directory"];
class FormField extends React.PureComponent {
static propTypes = {
type: React.PropTypes.string.isRequired,
prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool,
};
constructor(props) {
super(props);
this._fieldRequiredText = __("This field is required");
this._type = null;
this._element = null;
this._extraElementProps = {};
this.state = {
isError: null,
errorMessage: null,
};
}
componentWillMount() {
if (["text", "number", "radio", "checkbox"].includes(this.props.type)) {
this._element = "input";
this._type = this.props.type;
} else if (this.props.type == "text-number") {
this._element = "input";
this._type = "text";
} else if (this.props.type == "SimpleMDE") {
this._element = SimpleMDE;
this._type = "textarea";
this._extraElementProps.options = {
hideIcons: ["guide", "heading", "image", "fullscreen", "side-by-side"],
};
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = "input";
this._type = "hidden";
} else {
// Non <input> field, e.g. <select>, <textarea>
this._element = this.props.type;
}
}
componentDidMount() {
/**
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
* https://github.com/facebook/react/issues/3468
*/
if (this.props.type == "directory") {
this.refs.field.webkitdirectory = true;
}
}
handleFileChosen(path) {
this.refs.field.value = path;
if (this.props.onChange) {
// Updating inputs programmatically doesn't generate an event, so we have to make our own
const event = new Event("change", { bubbles: true });
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
this.props.onChange(event);
}
}
showError(text) {
this.setState({
isError: true,
errorMessage: text,
});
}
focus() {
this.refs.field.focus();
}
getValue() {
if (this.props.type == "checkbox") {
return this.refs.field.checked;
} else if (this.props.type == "SimpleMDE") {
return this.refs.field.simplemde.value();
} else {
return this.refs.field.value;
}
}
getSelectedElement() {
return this.refs.field.options[this.refs.field.selectedIndex];
}
getOptions() {
return this.refs.field.options;
}
render() {
// Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props),
isError = this.state.isError !== null
? this.state.isError
: this.props.hasError,
elementId = this.props.elementId ? this.props.elementId : formFieldId(),
renderElementInsideLabel =
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
delete otherProps.type;
delete otherProps.label;
delete otherProps.hasError;
delete otherProps.className;
delete otherProps.postfix;
delete otherProps.prefix;
delete otherProps.dispatch;
const element = (
<this._element
id={elementId}
type={this._type}
name={this.props.name}
ref="field"
placeholder={this.props.placeholder}
className={
"form-field__input form-field__input-" +
this.props.type +
" " +
(this.props.className || "") +
(isError ? "form-field__input--error" : "")
}
{...otherProps}
{...this._extraElementProps}
>
{this.props.children}
</this._element>
);
return (
<div className={"form-field form-field--" + this.props.type}>
{this.props.prefix
? <span className="form-field__prefix">{this.props.prefix}</span>
: ""}
{renderElementInsideLabel
? <label
htmlFor={elementId}
className={
"form-field__label " +
(isError ? "form-field__label--error" : "")
}
>
{element}
{this.props.label}
</label>
: element}
{formFieldFileSelectorTypes.includes(this.props.type)
? <FileSelector
type={this.props.type}
onFileChosen={this.handleFileChosen.bind(this)}
{...(this.props.defaultValue
? { initPath: this.props.defaultValue }
: {})}
/>
: null}
{this.props.postfix
? <span className="form-field__postfix">{this.props.postfix}</span>
: ""}
{isError && this.state.errorMessage
? <div className="form-field__error">{this.state.errorMessage}</div>
: ""}
</div>
);
}
}
export default FormField;

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import FormFieldPrice from "./view";
export default connect(null, null)(FormFieldPrice);

View file

@ -0,0 +1,70 @@
import React from "react";
import FormField from "component/formField";
class FormFieldPrice extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
amount: props.defaultValue && props.defaultValue.amount
? props.defaultValue.amount
: "",
currency: props.defaultValue && props.defaultValue.currency
? props.defaultValue.currency
: "LBC",
};
}
handleChange(newValues) {
const newState = Object.assign({}, this.state, newValues);
this.setState(newState);
this.props.onChange({
amount: newState.amount,
currency: newState.currency,
});
}
handleFeeAmountChange(event) {
this.handleChange({
amount: event.target.value ? Number(event.target.value) : null,
});
}
handleFeeCurrencyChange(event) {
this.handleChange({ currency: event.target.value });
}
render() {
const { defaultValue, placeholder, min, step } = this.props;
return (
<span className="form-field">
<FormField
type="number"
name="amount"
min={min}
placeholder={placeholder || null}
step={step}
onChange={event => this.handleFeeAmountChange(event)}
defaultValue={
defaultValue && defaultValue.amount ? defaultValue.amount : ""
}
className="form-field__input--inline"
/>
<FormField
type="select"
name="currency"
onChange={event => this.handleFeeCurrencyChange(event)}
defaultValue={
defaultValue && defaultValue.currency ? defaultValue.currency : ""
}
className="form-field__input--inline"
>
<option value="LBC">{__("LBRY Credits (LBC)")}</option>
<option value="USD">{__("US Dollars")}</option>
</FormField>
</span>
);
}
}
export default FormFieldPrice;

View file

@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import lbry from "lbry"; import { formatCredits } from "utils";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { selectBalance } from "selectors/wallet"; import { selectBalance } from "selectors/wallet";
import { doNavigate, doHistoryBack } from "actions/app"; import { doNavigate, doHistoryBack } from "actions/app";
import Header from "./view"; import Header from "./view";
const select = state => ({ const select = state => ({
balance: lbry.formatCredits(selectBalance(state), 1), balance: formatCredits(selectBalance(state), 1),
publish: __("Publish"), publish: __("Publish"),
}); });

View file

@ -8,13 +8,19 @@ export const Header = props => {
return ( return (
<header id="header"> <header id="header">
<div className="header__item"> <div className="header__item">
<Link onClick={back} button="alt button--flat" icon="icon-arrow-left" /> <Link
onClick={back}
button="alt button--flat"
icon="icon-arrow-left"
title={__("Back")}
/>
</div> </div>
<div className="header__item"> <div className="header__item">
<Link <Link
onClick={() => navigate("/discover")} onClick={() => navigate("/discover")}
button="alt button--flat" button="alt button--flat"
icon="icon-home" icon="icon-home"
title={__("Discover Content")}
/> />
</div> </div>
<div className="header__item header__item--wunderbar"> <div className="header__item header__item--wunderbar">
@ -26,6 +32,7 @@ export const Header = props => {
button="text" button="text"
icon="icon-bank" icon="icon-bank"
label={balance} label={balance}
title={__("Wallet")}
/> />
</div> </div>
<div className="header__item"> <div className="header__item">
@ -41,6 +48,7 @@ export const Header = props => {
onClick={() => navigate("/downloaded")} onClick={() => navigate("/downloaded")}
button="alt button--flat" button="alt button--flat"
icon="icon-folder" icon="icon-folder"
title={__("Downloads and Publishes")}
/> />
</div> </div>
<div className="header__item"> <div className="header__item">
@ -48,6 +56,7 @@ export const Header = props => {
onClick={() => navigate("/settings")} onClick={() => navigate("/settings")}
button="alt button--flat" button="alt button--flat"
icon="icon-gear" icon="icon-gear"
title={__("Settings")}
/> />
</div> </div>
</header> </header>

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import IconFeatured from "./view";
export default connect(null, null)(IconFeatured);

View file

@ -0,0 +1,15 @@
import React from "react";
import { Icon } from "component/common.js";
const IconFeatured = props => {
return (
<span
className="icon-featured"
title={__("Watch content with this icon to earn weekly rewards.")}
>
<Icon icon="icon-rocket" fixed className="card__icon-featured-content" />
</span>
);
};
export default IconFeatured;

View file

@ -11,7 +11,6 @@ const Link = props => {
icon, icon,
badge, badge,
button, button,
hidden,
disabled, disabled,
children, children,
} = props; } = props;

View file

@ -1,21 +0,0 @@
import React from "react";
import ReactModal from "react-modal";
export class ModalPage extends React.PureComponent {
render() {
return (
<ReactModal
onCloseRequested={this.props.onAborted || this.props.onConfirmed}
{...this.props}
className={(this.props.className || "") + " modal-page"}
overlayClassName="modal-overlay"
>
<div className="modal-page__content">
{this.props.children}
</div>
</ReactModal>
);
}
}
export default ModalPage;

View file

@ -0,0 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import ModalAuthFailure from "./view";
const select = state => ({});
const perform = dispatch => ({
close: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalAuthFailure);

View file

@ -0,0 +1,31 @@
import React from "react";
import { Modal } from "component/modal";
class ModalAuthFailure extends React.PureComponent {
render() {
const { close } = this.props;
return (
<Modal
isOpen={true}
contentLabel={__("Unable to Authenticate")}
type="confirm"
confirmButtonLabel={__("Reload")}
abortButtonLabel={__("Continue")}
onConfirmed={() => {
window.location.reload();
}}
onAborted={close}
>
<h3>{__("Authentication Failure")}</h3>
<p>
{__(
"If reloading does not fix this, or you see this at every start up, please email help@lbry.io."
)}
</p>
</Modal>
);
}
}
export default ModalAuthFailure;

View file

@ -6,7 +6,7 @@ class ModalError extends React.PureComponent {
render() { render() {
const { modal, closeModal, error } = this.props; const { modal, closeModal, error } = this.props;
const errorObj = typeof error === "string" ? { error: error } : error; const errorObj = typeof error === "string" ? { message: error } : error;
const error_key_labels = { const error_key_labels = {
connectionString: __("API connection string"), connectionString: __("API connection string"),
@ -18,10 +18,10 @@ class ModalError extends React.PureComponent {
}; };
const errorInfoList = []; const errorInfoList = [];
for (let key of Object.keys(error)) { for (let key of Object.keys(errorObj)) {
let val = typeof error[key] == "string" let val = typeof errorObj[key] == "string"
? error[key] ? errorObj[key]
: JSON.stringify(error[key]); : JSON.stringify(errorObj[key]);
let label = error_key_labels[key]; let label = error_key_labels[key];
errorInfoList.push( errorInfoList.push(
<li key={key}><strong>{label}</strong>: <code>{val}</code></li> <li key={key}><strong>{label}</strong>: <code>{val}</code></li>

View file

@ -0,0 +1,20 @@
import React from "react";
import rewards from "rewards";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { makeSelectRewardByType } from "selectors/rewards";
import ModalFirstReward from "./view";
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalFirstReward);

View file

@ -0,0 +1,49 @@
import React from "react";
import { Modal } from "component/modal";
import { CreditAmount } from "component/common";
class ModalFirstReward extends React.PureComponent {
render() {
const { closeModal, reward } = this.props;
return (
<Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel={__("Welcome to LBRY")}
onConfirmed={closeModal}
>
<section>
<h3 className="modal__header">{__("Your First Reward")}</h3>
<p>
{__("You just earned your first reward of")}
{" "}<CreditAmount amount={reward.reward_amount} />.
</p>
<p>
{__(
"This reward will show in your Wallet in the top right momentarily (if it hasn't already)."
)}
</p>
<p>
{__(
"These credits are used to compensate creators, to publish your own content, and to have say in how the network works."
)}
</p>
<p>
{__(
"No need to understand it all just yet! Try watching or downloading something next."
)}
</p>
<p>
{__(
"Finally, please know that LBRY is an early beta and that it earns the name."
)}
</p>
</section>
</Modal>
);
}
}
export default ModalFirstReward;

View file

@ -0,0 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import { doQuit, doSkipWrongDaemonNotice } from "actions/app";
import ModalIncompatibleDaemon from "./view";
const select = state => ({});
const perform = dispatch => ({
quit: () => dispatch(doQuit()),
});
export default connect(select, perform)(ModalIncompatibleDaemon);

View file

@ -0,0 +1,29 @@
import React from "react";
import { Modal } from "component/modal";
import Link from "component/link";
class ModalIncompatibleDaemon extends React.PureComponent {
render() {
const { quit } = this.props;
return (
<Modal
isOpen={true}
contentLabel={__("Incompatible daemon running")}
type="alert"
confirmButtonLabel={__("Quit")}
onConfirmed={quit}
>
{__(
"This browser is running with an incompatible version of the LBRY protocol and your install must be repaired. "
)}
<Link
label={__("Learn more")}
href="https://lbry.io/faq/incompatible-protocol-version"
/>
</Modal>
);
}
}
export default ModalIncompatibleDaemon;

View file

@ -0,0 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal, doNavigate } from "actions/app";
import ModalInsufficientCredits from "./view";
const select = state => ({});
const perform = dispatch => ({
addFunds: () => {
dispatch(doNavigate("/rewards"));
dispatch(doCloseModal());
},
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(ModalInsufficientCredits);

View file

@ -0,0 +1,24 @@
import React from "react";
import { Modal } from "component/modal";
class ModalInsufficientCredits extends React.PureComponent {
render() {
const { addFunds, closeModal } = this.props;
return (
<Modal
isOpen={true}
type="confirm"
contentLabel={__("Not enough credits")}
confirmButtonLabel={__("Get Credits")}
abortButtonLabel={__("Cancel")}
onAborted={closeModal}
onConfirmed={addFunds}
>
{__("More LBRY credits are required to purchase this.")}
</Modal>
);
}
}
export default ModalInsufficientCredits;

View file

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal, doHistoryBack } from "actions/app"; import { doCloseModal, doHistoryBack } from "actions/app";
import { doDeleteFile } from "actions/file_info"; import { doDeleteFileAndGoBack } from "actions/file_info";
import { makeSelectClaimForUriIsMine } from "selectors/claims"; import { makeSelectClaimForUriIsMine } from "selectors/claims";
import batchActions from "util/batchActions";
import ModalRemoveFile from "./view"; import ModalRemoveFile from "./view";
@ -19,8 +20,7 @@ const makeSelect = () => {
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()), closeModal: () => dispatch(doCloseModal()),
deleteFile: (fileInfo, deleteFromComputer, abandonClaim) => { deleteFile: (fileInfo, deleteFromComputer, abandonClaim) => {
dispatch(doHistoryBack()); dispatch(doDeleteFileAndGoBack(fileInfo, deleteFromComputer, abandonClaim));
dispatch(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim));
}, },
}); });

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { Modal } from "component/modal"; import { Modal } from "component/modal";
import { FormField } from "component/form.js"; import FormField from "component/formField";
class ModalRemoveFile extends React.PureComponent { class ModalRemoveFile extends React.PureComponent {
constructor(props) { constructor(props) {

View file

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { Modal } from "component/modal"; import { Modal } from "component/modal";
import { downloadUpgrade, skipUpgrade } from "actions/app";
class ModalUpgrade extends React.PureComponent { class ModalUpgrade extends React.PureComponent {
render() { render() {

View file

@ -1,28 +1,40 @@
import React from "react"; import React from "react";
import rewards from "rewards"; import rewards from "rewards";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal } from "actions/app"; import { doCloseModal, doAuthNavigate } from "actions/app";
import { doSetClientSetting } from "actions/settings";
import { selectUserIsRewardApproved } from "selectors/user"; import { selectUserIsRewardApproved } from "selectors/user";
import { import {
makeSelectHasClaimedReward, makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType, makeSelectRewardByType,
selectTotalRewardValue,
} from "selectors/rewards"; } from "selectors/rewards";
import WelcomeModal from "./view"; import ModalWelcome from "./view";
const select = (state, props) => { const select = (state, props) => {
const selectHasClaimed = makeSelectHasClaimedReward(), const selectHasClaimed = makeSelectHasClaimedReward(),
selectReward = makeSelectRewardByType(); selectReward = makeSelectRewardByType();
return { return {
hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }),
isRewardApproved: selectUserIsRewardApproved(state), isRewardApproved: selectUserIsRewardApproved(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
totalRewardValue: selectTotalRewardValue(state),
}; };
}; };
const perform = dispatch => ({ const perform = dispatch => () => {
closeModal: () => dispatch(doCloseModal()), const closeModal = () => {
}); dispatch(doSetClientSetting("welcome_acknowledged", true));
dispatch(doCloseModal());
};
export default connect(select, perform)(WelcomeModal); return {
verifyAccount: () => {
closeModal();
dispatch(doAuthNavigate("/discover"));
},
closeModal: closeModal,
};
};
export default connect(select, perform)(ModalWelcome);

View file

@ -4,14 +4,24 @@ import { CreditAmount } from "component/common";
import Link from "component/link"; import Link from "component/link";
import RewardLink from "component/rewardLink"; import RewardLink from "component/rewardLink";
class WelcomeModal extends React.PureComponent { class ModalWelcome extends React.PureComponent {
render() { constructor(props) {
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props; super(props);
this.state = {
isFirstScreen: true,
};
}
return !hasClaimed render() {
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY"> const { closeModal, totalRewardValue, verifyAccount } = this.props;
const totalRewardRounded = Math.round(totalRewardValue / 10) * 10;
return (
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
{this.state.isFirstScreen &&
<section> <section>
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3> <h3 className="modal__header">{__("Welcome to LBRY")}</h3>
<p> <p>
{__( {__(
"Using LBRY is like dating a centaur. Totally normal up top, and" "Using LBRY is like dating a centaur. Totally normal up top, and"
@ -24,58 +34,51 @@ class WelcomeModal extends React.PureComponent {
"Below, LBRY is controlled by users -- you -- via blockchain and decentralization." "Below, LBRY is controlled by users -- you -- via blockchain and decentralization."
)} )}
</p> </p>
<p> <div className="modal__buttons">
{__("Thank you for making content freedom possible!")} <Link
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""} button="primary"
</p> onClick={() => {
<div className="text-center"> this.setState({ isFirstScreen: false });
{isRewardApproved }}
? <RewardLink reward_type="new_user" button="primary" /> label={__("Continue")}
: <Link />
button="primary"
onClick={closeModal}
label={__("Continue")}
/>}
</div> </div>
</section> </section>}
</Modal> {!this.state.isFirstScreen &&
: <Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel={__("Welcome to LBRY")}
onConfirmed={closeModal}
>
<section> <section>
<h3 className="modal__header">{__("About Your Reward")}</h3> <h3 className="modal__header">{__("Claim Your Credits")}</h3>
<p> <p>
{__("You earned a reward of")} The LBRY network is controlled and powered by credits called{" "}
{" "}<CreditAmount amount={reward.reward_amount} label={false} /> <em>LBC</em>, a blockchain asset.
{" "}{__("LBRY credits, or")} <em>{__("LBC")}</em>. </p>
<p>
{__("New patrons receive ")} {" "}
{totalRewardValue
? <CreditAmount amount={totalRewardRounded} />
: <span className="credit-amount">{__("credits")}</span>}
{" "} {__("in rewards for usage and influence of the network.")}
</p> </p>
<p> <p>
{__( {__(
"This reward will show in your Wallet momentarily, probably while you are reading this message." "You'll also earn weekly bonuses for checking out the greatest new stuff."
)} )}
</p> </p>
<p> <div className="modal__buttons">
{__( <Link
"LBC is used to compensate creators, to publish, and to have say in how the network works." button="primary"
)} onClick={verifyAccount}
</p> label={__("You Had Me At Free LBC")}
<p> />
{__( <Link
"No need to understand it all just yet! Try watching or downloading something next." button="alt"
)} onClick={closeModal}
</p> label={__("I Burn Money")}
<p> />
{__( </div>
"Finally, know that LBRY is an early beta and that it earns the name." </section>}
)} </Modal>
</p> );
</section>
</Modal>;
} }
} }
export default WelcomeModal; export default ModalWelcome;

View file

@ -1,27 +0,0 @@
import React from "react";
export class Notice extends React.PureComponent {
static propTypes = {
isError: React.PropTypes.bool,
};
static defaultProps = {
isError: false,
};
render() {
return (
<section
className={
"notice " +
(this.props.isError ? "notice--error " : "") +
(this.props.className || "")
}
>
{this.props.children}
</section>
);
}
}
export default Notice;

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import PublishForm from "./view";
export default connect(null, null)(PublishForm);

View file

@ -0,0 +1,177 @@
import React from "react";
import lbryuri from "lbryuri";
import { FormRow } from "component/form.js";
import { BusyMessage } from "component/common";
import Link from "component/link";
class ChannelSection extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
newChannelName: "@",
newChannelBid: 10,
addingChannel: false,
};
}
handleChannelChange(event) {
const channel = event.target.value;
if (channel === "new") this.setState({ addingChannel: true });
else {
this.setState({ addingChannel: false });
this.props.handleChannelChange(event.target.value);
}
}
handleNewChannelNameChange(event) {
const newChannelName = event.target.value.startsWith("@")
? event.target.value
: "@" + event.target.value;
if (
newChannelName.length > 1 &&
!lbryuri.isValidName(newChannelName.substr(1), false)
) {
this.refs.newChannelName.showError(
__("LBRY channel names must contain only letters, numbers and dashes.")
);
return;
} else {
this.refs.newChannelName.clearError();
}
this.setState({
newChannelName,
});
}
handleNewChannelBidChange(event) {
this.setState({
newChannelBid: event.target.value,
});
}
handleCreateChannelClick(event) {
if (this.state.newChannelName.length < 5) {
this.refs.newChannelName.showError(
__("LBRY channel names must be at least 4 characters in length.")
);
return;
}
this.setState({
creatingChannel: true,
});
const newChannelName = this.state.newChannelName;
const amount = parseFloat(this.state.newChannelBid);
this.setState({
creatingChannel: true,
});
const success = () => {
this.setState({
creatingChannel: false,
addingChannel: false,
channel: newChannelName,
});
this.props.handleChannelChange(newChannelName);
};
const failure = err => {
this.setState({
creatingChannel: false,
});
this.refs.newChannelName.showError(
__("Unable to create channel due to an internal error.")
);
};
this.props.createChannel(newChannelName, amount).then(success, failure);
}
render() {
const lbcInputHelp = __(
"This LBC remains yours and the deposit can be undone at any time."
);
const channel = this.state.addingChannel ? "new" : this.props.channel;
const { fetchingChannels, channels = [] } = this.props;
let channelContent = [];
channelContent.push(
<FormRow
key="channel"
type="select"
tabIndex="1"
onChange={this.handleChannelChange.bind(this)}
value={channel}
>
<option key="anonymous" value="anonymous">
{__("Anonymous")}
</option>
{this.props.channels.map(({ name }) =>
<option key={name} value={name}>{name}</option>
)}
<option key="new" value="new">
{__("New channel...")}
</option>
</FormRow>
);
if (fetchingChannels) {
channelContent.push(
<BusyMessage message="Updating channels" key="loading" />
);
}
return (
<section className="card">
<div className="card__title-primary">
<h4>{__("Channel Name")}</h4>
<div className="card__subtitle">
{__(
"This is a username or handle that your content can be found under."
)}
{" "}
{__("Ex. @Marvel, @TheBeatles, @BooksByJoe")}
</div>
</div>
<div className="card__content">
{channelContent}
</div>
{this.state.addingChannel &&
<div className="card__content">
<FormRow
label={__("Name")}
type="text"
onChange={this.handleNewChannelNameChange.bind(this)}
value={this.state.newChannelName}
/>
<FormRow
label={__("Deposit")}
postfix="LBC"
step="0.1"
min="0"
type="number"
helper={lbcInputHelp}
ref="newChannelName"
onChange={this.handleNewChannelBidChange.bind(this)}
value={this.state.newChannelBid}
/>
<div className="form-row-submit">
<Link
button="primary"
label={
!this.state.creatingChannel
? __("Create channel")
: __("Creating channel...")
}
onClick={this.handleCreateChannelClick.bind(this)}
disabled={this.state.creatingChannel}
/>
</div>
</div>}
</section>
);
}
}
export default ChannelSection;

View file

@ -0,0 +1,926 @@
import React from "react";
import lbry from "lbry";
import lbryuri from "lbryuri";
import FormField from "component/formField";
import { FormRow } from "component/form.js";
import Link from "component/link";
import FormFieldPrice from "component/formFieldPrice";
import Modal from "component/modal";
import { BusyMessage } from "component/common";
import ChannelSection from "./internal/channelSection";
class PublishForm extends React.PureComponent {
constructor(props) {
super(props);
this._requiredFields = ["name", "bid", "meta_title", "tosAgree"];
this._defaultCopyrightNotice = "All rights reserved.";
this.state = {
rawName: "",
name: "",
bid: 10,
hasFile: false,
feeAmount: "",
feeCurrency: "LBC",
channel: "anonymous",
newChannelName: "@",
newChannelBid: 10,
meta_title: "",
meta_thumbnail: "",
meta_description: "",
meta_language: "en",
meta_nsfw: "0",
licenseType: "",
copyrightNotice: this._defaultCopyrightNotice,
otherLicenseDescription: "",
otherLicenseUrl: "",
tosAgree: false,
prefillDone: false,
uploadProgress: 0.0,
uploaded: false,
errorMessage: null,
submitting: false,
creatingChannel: false,
modal: null,
isFee: false,
customUrl: false,
};
}
_updateChannelList(channel) {
const { fetchingChannels, fetchChannelListMine } = this.props;
if (!fetchingChannels) fetchChannelListMine();
}
handleSubmit(event) {
if (typeof event !== "undefined") {
event.preventDefault();
}
this.setState({
submitting: true,
});
let checkFields = this._requiredFields;
if (!this.myClaimExists()) {
checkFields.unshift("file");
}
let missingFieldFound = false;
for (let fieldName of checkFields) {
const field = this.refs[fieldName];
if (field) {
if (field.getValue() === "" || field.getValue() === false) {
field.showRequiredError();
if (!missingFieldFound) {
field.focus();
missingFieldFound = true;
}
} else {
field.clearError();
}
}
}
if (missingFieldFound) {
this.setState({
submitting: false,
});
return;
}
let metadata = {};
for (let metaField of ["title", "description", "thumbnail", "language"]) {
const value = this.state["meta_" + metaField];
if (value) {
metadata[metaField] = value;
}
}
metadata.license = this.getLicense();
metadata.licenseUrl = this.getLicenseUrl();
metadata.nsfw = !!parseInt(this.state.meta_nsfw);
var doPublish = () => {
var publishArgs = {
name: this.state.name,
bid: parseFloat(this.state.bid),
metadata: metadata,
...(this.state.channel != "new" && this.state.channel != "anonymous"
? { channel_name: this.state.channel }
: {}),
};
if (this.refs.file.getValue() !== "") {
publishArgs.file_path = this.refs.file.getValue();
}
const success = claim => {};
const failure = error => this.handlePublishError(error);
this.handlePublishStarted();
this.props.publish(publishArgs).then(success, failure);
};
if (this.state.isFee && parseFloat(this.state.feeAmount) > 0) {
lbry.wallet_unused_address().then(address => {
metadata.fee = {
currency: this.state.feeCurrency,
amount: parseFloat(this.state.feeAmount),
address: address,
};
doPublish();
});
} else {
doPublish();
}
}
handlePublishStarted() {
this.setState({
modal: "publishStarted",
});
}
handlePublishStartedConfirmed() {
this.props.navigate("/published");
}
handlePublishError(error) {
this.setState({
submitting: false,
modal: "error",
errorMessage: error.message,
});
}
claim() {
const { claimsByUri } = this.props;
const { uri } = this.state;
return claimsByUri[uri];
}
topClaimValue() {
if (!this.claim()) return null;
return parseFloat(this.claim().amount);
}
myClaimExists() {
const { myClaims } = this.props;
const { name } = this.state;
if (!name) return false;
return !!myClaims.find(claim => claim.name === name);
}
topClaimIsMine() {
const myClaimInfo = this.myClaimInfo();
const { claimsByUri } = this.props;
const { uri } = this.state;
if (!uri) return null;
const claim = claimsByUri[uri];
if (!claim) return true;
if (!myClaimInfo) return false;
return myClaimInfo.amount >= claim.amount;
}
myClaimInfo() {
const { name } = this.state;
return Object.values(this.props.myClaims).find(
claim => claim.name === name
);
}
handleNameChange(event) {
var rawName = event.target.value;
this.setState({
customUrl: Boolean(rawName.length),
});
this.nameChanged(rawName);
}
nameChanged(rawName) {
if (!rawName) {
this.setState({
rawName: "",
name: "",
uri: "",
prefillDone: false,
});
return;
}
if (!lbryuri.isValidName(rawName, false)) {
this.refs.name.showError(
__("LBRY names must contain only letters, numbers and dashes.")
);
return;
}
let channel = "";
if (this.state.channel !== "anonymous") channel = this.state.channel;
const name = rawName.toLowerCase();
const uri = lbryuri.build({ contentName: name, channelName: channel });
this.setState({
rawName: rawName,
name: name,
prefillDone: false,
uri,
});
if (this.resolveUriTimeout) {
clearTimeout(this.resolveUriTimeout);
this.resolveUriTimeout = undefined;
}
const resolve = () => this.props.resolveUri(uri);
this.resolveUriTimeout = setTimeout(resolve.bind(this), 500, {
once: true,
});
}
handlePrefillClicked() {
const claimInfo = this.myClaimInfo();
const {
license,
licenseUrl,
title,
thumbnail,
description,
language,
nsfw,
} = claimInfo.value.stream.metadata;
let newState = {
meta_title: title,
meta_thumbnail: thumbnail,
meta_description: description,
meta_language: language,
meta_nsfw: nsfw,
prefillDone: true,
bid: claimInfo.amount,
};
if (license == this._defaultCopyrightNotice) {
newState.licenseType = "copyright";
newState.copyrightNotice = this._defaultCopyrightNotice;
} else {
// If the license URL or description matches one of the drop-down options, use that
let licenseType = "other"; // Will be overridden if we find a match
for (let option of this._meta_license.getOptions()) {
if (
option.getAttribute("data-url") === licenseUrl ||
option.text === license
) {
licenseType = option.value;
}
}
if (licenseType == "other") {
newState.otherLicenseDescription = license;
newState.otherLicenseUrl = licenseUrl;
}
newState.licenseType = licenseType;
}
this.setState(newState);
}
handleBidChange(event) {
this.setState({
bid: event.target.value,
});
}
handleFeeChange(newValue) {
this.setState({
feeAmount: newValue.amount,
feeCurrency: newValue.currency,
});
}
handleFeePrefChange(feeEnabled) {
this.setState({
isFee: feeEnabled,
feeAmount: this.state.feeAmount == "" ? "5.00" : this.state.feeAmount,
});
}
handleMetadataChange(event) {
/**
* This function is used for all metadata inputs that store the final value directly into state.
* The only exceptions are inputs related to license description and license URL, which require
* more complex logic and the final value is determined at submit time.
*/
this.setState({
["meta_" + event.target.name]: event.target.value,
});
}
handleDescriptionChanged(text) {
this.setState({
meta_description: text,
});
}
handleLicenseTypeChange(event) {
this.setState({
licenseType: event.target.value,
});
}
handleCopyrightNoticeChange(event) {
this.setState({
copyrightNotice: event.target.value,
});
}
handleOtherLicenseDescriptionChange(event) {
this.setState({
otherLicenseDescription: event.target.value,
});
}
handleOtherLicenseUrlChange(event) {
this.setState({
otherLicenseUrl: event.target.value,
});
}
handleChannelChange(channelName) {
this.setState({
channel: channelName,
});
const nameChanged = () => this.nameChanged(this.state.rawName);
setTimeout(nameChanged.bind(this), 500, { once: true });
}
handleTOSChange(event) {
this.setState({
tosAgree: event.target.checked,
});
}
handleCreateChannelClick(event) {
if (this.state.newChannelName.length < 5) {
this.refs.newChannelName.showError(
__("LBRY channel names must be at least 4 characters in length.")
);
return;
}
this.setState({
creatingChannel: true,
});
const newChannelName = this.state.newChannelName;
lbry
.channel_new({
channel_name: newChannelName,
amount: parseFloat(this.state.newChannelBid),
})
.then(
() => {
setTimeout(() => {
this.setState({
creatingChannel: false,
});
this._updateChannelList(newChannelName);
}, 10000);
},
error => {
// TODO: better error handling
this.refs.newChannelName.showError(
__("Unable to create channel due to an internal error.")
);
this.setState({
creatingChannel: false,
});
}
);
}
getLicense() {
switch (this.state.licenseType) {
case "copyright":
return this.state.copyrightNotice;
case "other":
return this.state.otherLicenseDescription;
default:
return this._meta_license.getSelectedElement().text;
}
}
getLicenseUrl() {
switch (this.state.licenseType) {
case "copyright":
return "";
case "other":
return this.state.otherLicenseUrl;
default:
return this._meta_license.getSelectedElement().getAttribute("data-url");
}
}
componentWillMount() {
this.props.fetchClaimListMine();
this._updateChannelList();
}
onFileChange() {
if (this.refs.file.getValue()) {
this.setState({ hasFile: true });
if (!this.state.customUrl) {
let fileName = this._getFileName(this.refs.file.getValue());
this.nameChanged(fileName);
}
} else {
this.setState({ hasFile: false });
}
}
_getFileName(fileName) {
const path = require("path");
const extension = path.extname(fileName);
fileName = path.basename(fileName, extension);
fileName = fileName.replace(lbryuri.REGEXP_INVALID_URI, "");
return fileName;
}
getNameBidHelpText() {
if (this.state.prefillDone) {
return __("Existing claim data was prefilled");
}
if (
this.state.uri &&
this.props.resolvingUris.indexOf(this.state.uri) !== -1 &&
this.claim() === undefined
) {
return __("Checking...");
} else if (!this.state.name) {
return __("Select a URL for this publish.");
} else if (!this.claim()) {
return __("This URL is unused.");
} else if (this.myClaimExists() && !this.state.prefillDone) {
return (
<span>
{__("You already have a claim with this name.")}{" "}
<Link
label={__("Use data from my existing claim")}
onClick={() => this.handlePrefillClicked()}
/>
</span>
);
} else if (this.claim()) {
if (this.topClaimValue() === 1) {
return (
<span>
{__(
'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
)}
</span>
);
} else {
return (
<span>
{__(
'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
)}
</span>
);
}
} else {
return "";
}
}
closeModal() {
this.setState({
modal: null,
});
}
render() {
const lbcInputHelp = __(
"This LBC remains yours and the deposit can be undone at any time."
);
return (
<main className="main--single-column">
<form
onSubmit={event => {
this.handleSubmit(event);
}}
>
<section className="card">
<div className="card__title-primary">
<h4>{__("Content")}</h4>
<div className="card__subtitle">
{__("What are you publishing?")}
</div>
</div>
<div className="card__content">
<FormRow
name="file"
label="File"
ref="file"
type="file"
onChange={event => {
this.onFileChange(event);
}}
helper={
this.myClaimExists()
? __(
"If you don't choose a file, the file from your existing claim will be used."
)
: null
}
/>
</div>
{!this.state.hasFile && !this.myClaimExists()
? null
: <div>
<div className="card__content">
<FormRow
label={__("Title")}
type="text"
name="title"
value={this.state.meta_title}
placeholder="Titular Title"
onChange={event => {
this.handleMetadataChange(event);
}}
/>
</div>
<div className="card__content">
<FormRow
type="text"
label={__("Thumbnail URL")}
name="thumbnail"
value={this.state.meta_thumbnail}
placeholder="http://spee.ch/mylogo"
onChange={event => {
this.handleMetadataChange(event);
}}
/>
</div>
<div className="card__content">
<FormRow
label={__("Description")}
type="SimpleMDE"
ref="meta_description"
name="description"
value={this.state.meta_description}
placeholder={__("Description of your content")}
onChange={text => {
this.handleDescriptionChanged(text);
}}
/>
</div>
<div className="card__content">
<FormRow
label={__("Language")}
type="select"
value={this.state.meta_language}
name="language"
onChange={event => {
this.handleMetadataChange(event);
}}
>
<option value="en">{__("English")}</option>
<option value="zh">{__("Chinese")}</option>
<option value="fr">{__("French")}</option>
<option value="de">{__("German")}</option>
<option value="jp">{__("Japanese")}</option>
<option value="ru">{__("Russian")}</option>
<option value="es">{__("Spanish")}</option>
</FormRow>
</div>
<div className="card__content">
<FormRow
type="select"
label={__("Maturity")}
value={this.state.meta_nsfw}
name="nsfw"
onChange={event => {
this.handleMetadataChange(event);
}}
>
{/* <option value=""></option> */}
<option value="0">{__("All Ages")}</option>
<option value="1">{__("Adults Only")}</option>
</FormRow>
</div>
</div>}
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__("Access")}</h4>
<div className="card__subtitle">
{__("How much does this content cost?")}
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<label className="form-row__label">{__("Price")}</label>
</div>
<FormRow
label={__("Free")}
type="radio"
name="isFree"
onChange={() => this.handleFeePrefChange(false)}
checked={!this.state.isFee}
/>
<FormField
type="radio"
name="isFree"
label={!this.state.isFee ? __("Choose price...") : __("Price ")}
onChange={() => {
this.handleFeePrefChange(true);
}}
checked={this.state.isFee}
/>
<span className={!this.state.isFee ? "hidden" : ""}>
{/*min=0.01 caused weird interactions with step (e.g. down from 5 equals 4.91 rather than 4.9) */}
<FormFieldPrice
min="0"
step="0.1"
defaultValue={{ amount: 5.0, currency: "LBC" }}
onChange={val => this.handleFeeChange(val)}
/>
</span>
{this.state.isFee && this.state.feeCurrency.toUpperCase() != "LBC"
? <div className="form-field__helper">
{__(
"All content fees are charged in LBC. For non-LBC payment methods, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase."
)}
</div>
: null}
<FormRow
label="License"
type="select"
value={this.state.licenseType}
ref={row => {
this._meta_license = row;
}}
onChange={event => {
this.handleLicenseTypeChange(event);
}}
>
<option />
<option value="publicDomain">{__("Public Domain")}</option>
<option
value="cc-by"
data-url="https://creativecommons.org/licenses/by/4.0/legalcode"
>
{__("Creative Commons Attribution 4.0 International")}
</option>
<option
value="cc-by-sa"
data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode"
>
{__(
"Creative Commons Attribution-ShareAlike 4.0 International"
)}
</option>
<option
value="cc-by-nd"
data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode"
>
{__(
"Creative Commons Attribution-NoDerivatives 4.0 International"
)}
</option>
<option
value="cc-by-nc"
data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode"
>
{__(
"Creative Commons Attribution-NonCommercial 4.0 International"
)}
</option>
<option
value="cc-by-nc-sa"
data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode"
>
{__(
"Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International"
)}
</option>
<option
value="cc-by-nc-nd"
data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode"
>
{__(
"Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International"
)}
</option>
<option value="copyright">
{__("Copyrighted...")}
</option>
<option value="other">
{__("Other...")}
</option>
</FormRow>
{this.state.licenseType == "copyright"
? <FormRow
label={__("Copyright notice")}
type="text"
name="copyright-notice"
value={this.state.copyrightNotice}
onChange={event => {
this.handleCopyrightNoticeChange(event);
}}
/>
: null}
{this.state.licenseType == "other"
? <FormRow
label={__("License description")}
type="text"
name="other-license-description"
value={this.state.otherLicenseDescription}
onChange={event => {
this.handleOtherLicenseDescriptionChange(event);
}}
/>
: null}
{this.state.licenseType == "other"
? <FormRow
label={__("License URL")}
type="text"
name="other-license-url"
value={this.state.otherLicenseUrl}
onChange={event => {
this.handleOtherLicenseUrlChange(event);
}}
/>
: null}
</div>
</section>
<ChannelSection
{...this.props}
handleChannelChange={this.handleChannelChange.bind(this)}
channel={this.state.channel}
/>
<section className="card">
<div className="card__title-primary">
<h4>{__("Content URL")}</h4>
<div className="card__subtitle">
{__(
"This is the exact address where people find your content (ex. lbry://myvideo)."
)}
{" "}
<Link
label={__("Learn more")}
href="https://lbry.io/faq/naming"
/>.
</div>
</div>
<div className="card__content">
<FormRow
prefix={`lbry://${this.state.channel === "anonymous"
? ""
: `${this.state.channel}/`}`}
type="text"
ref="name"
placeholder="myname"
value={this.state.rawName}
onChange={event => {
this.handleNameChange(event);
}}
helper={this.getNameBidHelpText()}
/>
</div>
{this.state.rawName
? <div className="card__content">
<FormRow
ref="bid"
type="number"
step="0.01"
label={__("Deposit")}
postfix="LBC"
onChange={event => {
this.handleBidChange(event);
}}
value={this.state.bid}
placeholder={this.claim() ? this.topClaimValue() + 10 : 100}
helper={lbcInputHelp}
/>
</div>
: ""}
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__("Terms of Service")}</h4>
</div>
<div className="card__content">
<FormRow
label={
<span>
{__("I agree to the")}
{" "}
<Link
href="https://www.lbry.io/termsofservice"
label={__("LBRY terms of service")}
/>
</span>
}
type="checkbox"
checked={this.state.tosAgree}
onChange={event => {
this.handleTOSChange(event);
}}
/>
</div>
</section>
<div className="card-series-submit">
<Link
button="primary"
label={
!this.state.submitting ? __("Publish") : __("Publishing...")
}
onClick={event => {
this.handleSubmit(event);
}}
disabled={
this.state.submitting ||
(this.state.uri &&
this.props.resolvingUris.indexOf(this.state.uri) !== -1) ||
(this.claim() &&
!this.topClaimIsMine() &&
this.state.bid <= this.topClaimValue())
}
/>
<Link
button="cancel"
onClick={this.props.back}
label={__("Cancel")}
/>
<input type="submit" className="hidden" />
</div>
</form>
<Modal
isOpen={this.state.modal == "publishStarted"}
contentLabel={__("File published")}
onConfirmed={event => {
this.handlePublishStartedConfirmed(event);
}}
>
<p>
{__("Your file has been published to LBRY at the address")}
{" "}<code>{this.state.uri}</code>!
</p>
<p>
{__(
'The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.'
)}
</p>
</Modal>
<Modal
isOpen={this.state.modal == "error"}
contentLabel={__("Error publishing file")}
onConfirmed={event => {
this.closeModal(event);
}}
>
{__(
"The following error occurred when attempting to publish your file"
)}: {this.state.errorMessage}
</Modal>
</main>
);
}
}
export default PublishForm;

View file

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError, makeSelectClaimRewardError,
makeSelectRewardByType, makeSelectRewardByType,
makeSelectIsRewardClaimPending, makeSelectIsRewardClaimPending,
@ -11,13 +10,11 @@ import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
import RewardLink from "./view"; import RewardLink from "./view";
const makeSelect = () => { const makeSelect = () => {
const selectHasClaimedReward = makeSelectHasClaimedReward();
const selectIsPending = makeSelectIsRewardClaimPending(); const selectIsPending = makeSelectIsRewardClaimPending();
const selectReward = makeSelectRewardByType(); const selectReward = makeSelectRewardByType();
const selectError = makeSelectClaimRewardError(); const selectError = makeSelectClaimRewardError();
const select = (state, props) => ({ const select = (state, props) => ({
isClaimed: selectHasClaimedReward(state, props),
errorMessage: selectError(state, props), errorMessage: selectError(state, props),
isPending: selectIsPending(state, props), isPending: selectIsPending(state, props),
reward: selectReward(state, props), reward: selectReward(state, props),

View file

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { Icon } from "component/common";
import Modal from "component/modal"; import Modal from "component/modal";
import Link from "component/link"; import Link from "component/link";
@ -10,22 +9,19 @@ const RewardLink = props => {
claimReward, claimReward,
clearError, clearError,
errorMessage, errorMessage,
isClaimed,
isPending, isPending,
} = props; } = props;
return ( return (
<div className="reward-link"> <div className="reward-link">
{isClaimed <Link
? <span><Icon icon="icon-check" /> Reward claimed.</span> button={button ? button : "alt"}
: <Link disabled={isPending}
button={button ? button : "alt"} label={isPending ? __("Claiming...") : __("Claim Reward")}
disabled={isPending} onClick={() => {
label={isPending ? __("Claiming...") : __("Claim Reward")} claimReward(reward);
onClick={() => { }}
claimReward(reward); />
}}
/>}
{errorMessage {errorMessage
? <Modal ? <Modal
isOpen={true} isOpen={true}

View file

@ -13,6 +13,7 @@ import FileListDownloaded from "page/fileListDownloaded";
import FileListPublished from "page/fileListPublished"; import FileListPublished from "page/fileListPublished";
import ChannelPage from "page/channel"; import ChannelPage from "page/channel";
import SearchPage from "page/search"; import SearchPage from "page/search";
import AuthPage from "page/auth";
const route = (page, routesMap) => { const route = (page, routesMap) => {
const component = routesMap[page]; const component = routesMap[page];
@ -24,22 +25,23 @@ const Router = props => {
const { currentPage, params } = props; const { currentPage, params } = props;
return route(currentPage, { return route(currentPage, {
settings: <SettingsPage {...params} />, auth: <AuthPage params={params} />,
help: <HelpPage {...params} />, channel: <ChannelPage params={params} />,
report: <ReportPage {...params} />, developer: <DeveloperPage params={params} />,
downloaded: <FileListDownloaded {...params} />, discover: <DiscoverPage params={params} />,
published: <FileListPublished {...params} />, downloaded: <FileListDownloaded params={params} />,
start: <StartPage {...params} />, help: <HelpPage params={params} />,
wallet: <WalletPage {...params} />, publish: <PublishPage params={params} />,
send: <WalletPage {...params} />, published: <FileListPublished params={params} />,
receive: <WalletPage {...params} />, receive: <WalletPage params={params} />,
show: <ShowPage {...params} />, report: <ReportPage params={params} />,
channel: <ChannelPage {...params} />, rewards: <RewardsPage params={params} />,
publish: <PublishPage {...params} />, search: <SearchPage params={params} />,
developer: <DeveloperPage {...params} />, send: <WalletPage params={params} />,
discover: <DiscoverPage {...params} />, settings: <SettingsPage params={params} />,
rewards: <RewardsPage {...params} />, show: <ShowPage params={params} />,
search: <SearchPage {...params} />, start: <StartPage params={params} />,
wallet: <WalletPage params={params} />,
}); });
}; };

View file

@ -1,82 +0,0 @@
import React from "react";
import lbry from "../lbry.js";
import LoadScreen from "./load_screen.js";
export class SplashScreen extends React.PureComponent {
static propTypes = {
message: React.PropTypes.string,
onLoadDone: React.PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
details: __("Starting daemon"),
message: __("Connecting"),
isLagging: false,
};
}
updateStatus() {
lbry.status().then(status => {
this._updateStatusCallback(status);
});
}
_updateStatusCallback(status) {
const startupStatus = status.startup_status;
if (startupStatus.code == "started") {
// Wait until we are able to resolve a name before declaring
// that we are done.
// TODO: This is a hack, and the logic should live in the daemon
// to give us a better sense of when we are actually started
this.setState({
message: __("Testing Network"),
details: __("Waiting for name resolution"),
isLagging: false,
});
lbry.resolve({ uri: "lbry://one" }).then(() => {
this.props.onLoadDone();
});
return;
}
this.setState({
details: startupStatus.message + (startupStatus.is_lagging ? "" : "..."),
isLagging: startupStatus.is_lagging,
});
setTimeout(() => {
this.updateStatus();
}, 500);
}
componentDidMount() {
lbry
.connect()
.then(() => {
this.updateStatus();
})
.catch(() => {
this.setState({
isLagging: true,
message: __("Connection Failure"),
details: __(
"Try closing all LBRY processes and starting again. If this still happens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug."
),
});
});
}
render() {
return (
<LoadScreen
message={this.state.message}
details={this.state.details}
isWarning={this.state.isLagging}
/>
);
}
}
export default SplashScreen;

View file

@ -0,0 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import { selectCurrentModal, selectDaemonVersionMatched } from "selectors/app";
import { doCheckDaemonVersion } from "actions/app";
import SplashScreen from "./view";
const select = state => {
return {
modal: selectCurrentModal(state),
daemonVersionMatched: selectDaemonVersionMatched(state),
};
};
const perform = dispatch => ({
checkDaemonVersion: () => dispatch(doCheckDaemonVersion()),
});
export default connect(select, perform)(SplashScreen);

View file

@ -0,0 +1,121 @@
import React from "react";
import lbry from "../../lbry.js";
import LoadScreen from "../load_screen.js";
import ModalIncompatibleDaemon from "../modalIncompatibleDaemon";
import ModalUpgrade from "component/modalUpgrade";
import ModalDownloading from "component/modalDownloading";
export class SplashScreen extends React.PureComponent {
static propTypes = {
message: React.PropTypes.string,
onLoadDone: React.PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
details: __("Starting daemon"),
message: __("Connecting"),
isRunning: false,
isLagging: false,
};
}
updateStatus() {
lbry.status().then(status => {
this._updateStatusCallback(status);
});
}
_updateStatusCallback(status) {
const startupStatus = status.startup_status;
if (startupStatus.code == "started") {
// Wait until we are able to resolve a name before declaring
// that we are done.
// TODO: This is a hack, and the logic should live in the daemon
// to give us a better sense of when we are actually started
this.setState({
message: __("Testing Network"),
details: __("Waiting for name resolution"),
isLagging: false,
isRunning: true,
});
lbry.resolve({ uri: "lbry://one" }).then(() => {
// Only leave the load screen if the daemon version matched;
// otherwise we'll notify the user at the end of the load screen.
if (this.props.daemonVersionMatched) {
this.props.onReadyToLaunch();
}
});
return;
}
if (
status.blockchain_status &&
status.blockchain_status.blocks_behind > 0
) {
const format = status.blockchain_status.blocks_behind == 1
? "%s block behind"
: "%s blocks behind";
this.setState({
message: __("Blockchain Sync"),
details: __(format, status.blockchain_status.blocks_behind),
isLagging: startupStatus.is_lagging,
});
} else {
this.setState({
message: __("Network Loading"),
details:
startupStatus.message + (startupStatus.is_lagging ? "" : "..."),
isLagging: startupStatus.is_lagging,
});
}
setTimeout(() => {
this.updateStatus();
}, 500);
}
componentDidMount() {
lbry
.connect()
.then(this.props.checkDaemonVersion)
.then(() => {
this.updateStatus();
})
.catch(() => {
this.setState({
isLagging: true,
message: __("Connection Failure"),
details: __(
"Try closing all LBRY processes and starting again. If this still happens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug."
),
});
});
}
render() {
const { modal } = this.props;
return (
<div>
<LoadScreen
message={this.state.message}
details={this.state.details}
isWarning={this.state.isLagging}
/>
{/* Temp hack: don't show any modals on splash screen daemon is running;
daemon doesn't let you quit during startup, so the "Quit" buttons
in the modals won't work. */}
{modal == "incompatibleDaemon" &&
this.state.isRunning &&
<ModalIncompatibleDaemon />}
{modal == "upgrade" && this.state.isRunning && <ModalUpgrade />}
{modal == "downloading" && this.state.isRunning && <ModalDownloading />}
</div>
);
}
}
export default SplashScreen;

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import TruncatedMarkdown from "./view";
export default connect()(TruncatedMarkdown);

View file

@ -0,0 +1,39 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import ReactDOMServer from "react-dom/server";
class TruncatedMarkdown extends React.PureComponent {
static propTypes = {
lines: React.PropTypes.number,
};
static defaultProps = {
lines: null,
};
transformMarkdown(text) {
// render markdown to html string then trim html tag
let htmlString = ReactDOMServer.renderToStaticMarkup(
<ReactMarkdown source={this.props.children} />
);
var txt = document.createElement("textarea");
txt.innerHTML = htmlString;
return txt.value.replace(/<(?:.|\n)*?>/gm, "");
}
render() {
let content = this.props.children && typeof this.props.children === "string"
? this.transformMarkdown(this.props.children)
: this.props.children;
return (
<span
className="truncated-text"
style={{ WebkitLineClamp: this.props.lines }}
>
{content}
</span>
);
}
}
export default TruncatedMarkdown;

View file

@ -27,7 +27,6 @@ class UserEmailNew extends React.PureComponent {
return ( return (
<form <form
className="form-input-width"
onSubmit={event => { onSubmit={event => {
this.handleSubmit(event); this.handleSubmit(event);
}} }}

View file

@ -13,7 +13,7 @@ class UserEmailVerify extends React.PureComponent {
handleCodeChanged(event) { handleCodeChanged(event) {
this.setState({ this.setState({
code: event.target.value, code: String(event.target.value).trim(),
}); });
} }
@ -24,18 +24,16 @@ class UserEmailVerify extends React.PureComponent {
render() { render() {
const { errorMessage, isPending } = this.props; const { errorMessage, isPending } = this.props;
return ( return (
<form <form
className="form-input-width"
onSubmit={event => { onSubmit={event => {
this.handleSubmit(event); this.handleSubmit(event);
}} }}
> >
<p>{__("Please enter the verification code emailed to you.")}</p>
<FormRow <FormRow
type="text" type="text"
label={__("Verification Code")} label={__("Verification Code")}
placeholder="a94bXXXXXXXXXXXXXX"
name="code" name="code"
value={this.state.code} value={this.state.code}
onChange={event => { onChange={event => {
@ -48,7 +46,7 @@ class UserEmailVerify extends React.PureComponent {
<p> <p>
{__("Email")}{" "} {__("Email")}{" "}
<Link href="mailto:help@lbry.io" label="help@lbry.io" />{" "} <Link href="mailto:help@lbry.io" label="help@lbry.io" />{" "}
{__("if you did not receive or are having trouble with your code.")} {__("if you encounter any trouble with your code.")}
</p> </p>
</div> </div>
<div className="form-row-submit form-row-submit--with-footer"> <div className="form-row-submit form-row-submit--with-footer">

View file

@ -0,0 +1,28 @@
import React from "react";
import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { doUserIdentityVerify } from "actions/user";
import rewards from "rewards";
import { makeSelectRewardByType } from "selectors/rewards";
import {
selectIdentityVerifyIsPending,
selectIdentityVerifyErrorMessage,
} from "selectors/user";
import UserVerify from "./view";
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
isPending: selectIdentityVerifyIsPending(state),
errorMessage: selectIdentityVerifyErrorMessage(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};
const perform = dispatch => ({
navigate: uri => dispatch(doNavigate(uri)),
verifyUserIdentity: token => dispatch(doUserIdentityVerify(token)),
});
export default connect(select, perform)(UserVerify);

View file

@ -0,0 +1,65 @@
import React from "react";
import { CreditAmount } from "component/common";
import Link from "component/link";
import CardVerify from "component/cardVerify";
class UserVerify extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
code: "",
};
}
handleCodeChanged(event) {
this.setState({
code: event.target.value,
});
}
onToken(data) {
this.props.verifyUserIdentity(data.id);
}
render() {
const { errorMessage, isPending, navigate } = this.props;
return (
<div>
<p>
{__(
"To ensure you are a real person, we require a valid credit or debit card."
) +
" " +
__("There is no charge at all, now or in the future.") +
" "}
<Link
href="https://lbry.io/faq/identity-requirements"
label={__("Read More")}
/>
</p>
{errorMessage && <p className="form-field__error">{errorMessage}</p>}
<p>
<CardVerify
label={__("Link Card and Finish")}
disabled={isPending}
token={this.onToken.bind(this)}
stripeKey="pk_live_e8M4dRNnCCbmpZzduEUZBgJO"
/>
</p>
<p>
{__(
"You can continue without this step, but you will not be eligible to earn rewards."
)}
</p>
<Link
onClick={() => navigate("/discover")}
button="alt"
label={__("Skip Rewards")}
/>
</div>
);
}
}
export default UserVerify;

View file

@ -4,12 +4,31 @@ import Link from "component/link";
import Modal from "component/modal"; import Modal from "component/modal";
class VideoPlayButton extends React.PureComponent { class VideoPlayButton extends React.PureComponent {
componentDidMount() {
this.keyDownListener = this.onKeyDown.bind(this);
document.addEventListener("keydown", this.keyDownListener);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.keyDownListener);
}
onPurchaseConfirmed() { onPurchaseConfirmed() {
this.props.closeModal(); this.props.closeModal();
this.props.startPlaying(); this.props.startPlaying();
this.props.loadVideo(this.props.uri); this.props.loadVideo(this.props.uri);
} }
onKeyDown(event) {
if (
"input" !== event.target.tagName.toLowerCase() &&
"Space" === event.code
) {
event.preventDefault();
this.onWatchClick();
}
}
onWatchClick() { onWatchClick() {
this.props.purchaseUri(this.props.uri).then(() => { this.props.purchaseUri(this.props.uri).then(() => {
if (!this.props.modal) { if (!this.props.modal) {
@ -28,7 +47,6 @@ class VideoPlayButton extends React.PureComponent {
modal, modal,
closeModal, closeModal,
isLoading, isLoading,
costInfo,
fileInfo, fileInfo,
mediaType, mediaType,
} = this.props; } = this.props;
@ -41,10 +59,7 @@ class VideoPlayButton extends React.PureComponent {
} }
*/ */
const disabled = const disabled = isLoading || fileInfo === undefined;
isLoading ||
fileInfo === undefined ||
(fileInfo === null && (!costInfo || costInfo.cost === undefined));
const icon = ["audio", "video"].indexOf(mediaType) !== -1 const icon = ["audio", "video"].indexOf(mediaType) !== -1
? "icon-play" ? "icon-play"
: "icon-folder-o"; : "icon-folder-o";
@ -59,13 +74,6 @@ class VideoPlayButton extends React.PureComponent {
icon={icon} icon={icon}
onClick={this.onWatchClick.bind(this)} onClick={this.onWatchClick.bind(this)}
/> />
<Modal
contentLabel={__("Not enough credits")}
isOpen={modal == "notEnoughCredits"}
onConfirmed={closeModal}
>
{__("You don't have enough LBRY credits to pay for this stream.")}
</Modal>
<Modal <Modal
type="confirm" type="confirm"
isOpen={modal == "affirmPurchaseAndPlay"} isOpen={modal == "affirmPurchaseAndPlay"}
@ -73,9 +81,11 @@ class VideoPlayButton extends React.PureComponent {
onConfirmed={this.onPurchaseConfirmed.bind(this)} onConfirmed={this.onPurchaseConfirmed.bind(this)}
onAborted={closeModal} onAborted={closeModal}
> >
{__("This will purchase")} <strong>{title}</strong> {__("for")} {__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
{" "}<strong><FilePrice uri={uri} look="plain" /></strong> <strong>
{" "}{__("credits")}. <FilePrice uri={uri} look="plain" />
</strong>{" "}
{__("credits")}.
</Modal> </Modal>
<Modal <Modal
isOpen={modal == "timedOut"} isOpen={modal == "timedOut"}

View file

@ -7,6 +7,8 @@ import { setSession, getSession } from "utils";
import LoadingScreen from "./loading-screen"; import LoadingScreen from "./loading-screen";
class VideoPlayer extends React.PureComponent { class VideoPlayer extends React.PureComponent {
static MP3_CONTENT_TYPES = ["audio/mpeg3", "audio/mpeg"];
constructor(props) { constructor(props) {
super(props); super(props);
@ -15,11 +17,13 @@ class VideoPlayer extends React.PureComponent {
startedPlaying: false, startedPlaying: false,
unplayable: false, unplayable: false,
}; };
this.togglePlayListener = this.togglePlay.bind(this);
} }
componentDidMount() { componentDidMount() {
const container = this.refs.media; const container = this.refs.media;
const { mediaType } = this.props; const { contentType, downloadPath, mediaType } = this.props;
const loadedMetadata = e => { const loadedMetadata = e => {
this.setState({ hasMetadata: true, startedPlaying: true }); this.setState({ hasMetadata: true, startedPlaying: true });
this.refs.media.children[0].play(); this.refs.media.children[0].play();
@ -37,15 +41,22 @@ class VideoPlayer extends React.PureComponent {
} }
}; };
player.append( // use renderAudio override for mp3
this.file(), if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
container, this.renderAudio(container, null, false);
{ autoplay: false, controls: true }, } else {
renderMediaCallback.bind(this) player.append(
); this.file(),
container,
{ autoplay: false, controls: true },
renderMediaCallback.bind(this)
);
}
document.addEventListener("keydown", this.togglePlayListener);
const mediaElement = this.refs.media.children[0]; const mediaElement = this.refs.media.children[0];
if (mediaElement) { if (mediaElement) {
mediaElement.addEventListener("click", this.togglePlayListener);
mediaElement.addEventListener( mediaElement.addEventListener(
"loadedmetadata", "loadedmetadata",
loadedMetadata.bind(this), loadedMetadata.bind(this),
@ -53,7 +64,6 @@ class VideoPlayer extends React.PureComponent {
once: true, once: true,
} }
); );
mediaElement.addEventListener( mediaElement.addEventListener(
"webkitfullscreenchange", "webkitfullscreenchange",
win32FullScreenChange.bind(this) win32FullScreenChange.bind(this)
@ -65,19 +75,67 @@ class VideoPlayer extends React.PureComponent {
} }
} }
componentWillUnmount() {
document.removeEventListener("keydown", this.togglePlayListener);
const mediaElement = this.refs.media.children[0];
if (mediaElement) {
mediaElement.removeEventListener("click", this.togglePlayListener);
}
}
renderAudio(container, autoplay) {
if (container.firstChild) {
container.firstChild.remove();
}
// clear the container
const { downloadPath } = this.props;
const audio = document.createElement("audio");
audio.autoplay = autoplay;
audio.controls = true;
audio.src = downloadPath;
container.appendChild(audio);
}
togglePlay(event) {
// ignore all events except click and spacebar keydown, or input events in a form control
if (
"keydown" === event.type &&
("Space" !== event.code || "input" === event.target.tagName.toLowerCase())
) {
return;
}
event.preventDefault();
const mediaElement = this.refs.media.children[0];
if (mediaElement) {
if (!mediaElement.paused) {
mediaElement.pause();
} else {
mediaElement.play();
}
}
}
getPreferredVolume() { getPreferredVolume() {
const volumePreference = parseFloat(getSession("prefs_volume")); const volumePreference = parseFloat(getSession("prefs_volume"));
return isNaN(volumePreference) ? 1 : volumePreference; return isNaN(volumePreference) ? 1 : volumePreference;
} }
componentDidUpdate() { componentDidUpdate() {
const { mediaType, downloadCompleted } = this.props; const { contentType, downloadCompleted } = this.props;
const { startedPlaying } = this.state; const { startedPlaying } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) { if (this.playableType() && !startedPlaying && downloadCompleted) {
const container = this.refs.media.children[0]; const container = this.refs.media.children[0];
player.render(this.file(), container, { autoplay: true, controls: true }); if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
this.renderAudio(this.refs.media, true);
} else {
player.render(this.file(), container, {
autoplay: true,
controls: true,
});
}
} }
} }

View file

@ -14,6 +14,25 @@ class Video extends React.PureComponent {
}; };
} }
componentWillReceiveProps(nextProps) {
// reset playing state upon change path action
if (
!this.isMediaSame(nextProps) &&
this.props.fileInfo &&
this.state.isPlaying
) {
this.state.isPlaying = false;
}
}
isMediaSame(nextProps) {
return (
this.props.fileInfo &&
nextProps.fileInfo &&
this.props.fileInfo.outpoint === nextProps.fileInfo.outpoint
);
}
startPlaying() { startPlaying() {
this.setState({ this.setState({
isPlaying: true, isPlaying: true,
@ -98,6 +117,7 @@ class Video extends React.PureComponent {
poster={poster} poster={poster}
downloadPath={fileInfo.download_path} downloadPath={fileInfo.download_path}
mediaType={mediaType} mediaType={mediaType}
contentType={contentType}
downloadCompleted={fileInfo.completed} downloadCompleted={fileInfo.completed}
/>)} />)}
{!isPlaying && {!isPlaying &&

View file

@ -12,8 +12,10 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
onSearch: query => dispatch(doNavigate("/search", { query })), onSearch: query => dispatch(doNavigate("/search", { query })),
onSubmit: query => onSubmit: (query, extraParams) =>
dispatch(doNavigate("/show", { uri: lbryuri.normalize(query) })), dispatch(
doNavigate("/show", { uri: lbryuri.normalize(query), ...extraParams })
),
}); });
export default connect(select, perform)(Wunderbar); export default connect(select, perform)(Wunderbar);

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import { Icon } from "component/common.js"; import { Icon } from "component/common.js";
import { parseQueryParams } from "util/query_params";
class WunderBar extends React.PureComponent { class WunderBar extends React.PureComponent {
static TYPING_TIMEOUT = 800; static TYPING_TIMEOUT = 800;
@ -120,12 +121,15 @@ class WunderBar extends React.PureComponent {
onKeyPress(event) { onKeyPress(event) {
if (event.charCode == 13 && this._input.value) { if (event.charCode == 13 && this._input.value) {
let uri = null, let uri = null,
method = "onSubmit"; method = "onSubmit",
extraParams = {};
this._resetOnNextBlur = false; this._resetOnNextBlur = false;
clearTimeout(this._userTypingTimer); clearTimeout(this._userTypingTimer);
const value = this._input.value.trim(); const parts = this._input.value.trim().split("?");
const value = parts.shift();
if (parts.length > 0) extraParams = parseQueryParams(parts.join(""));
try { try {
uri = lbryuri.normalize(value); uri = lbryuri.normalize(value);
@ -136,7 +140,7 @@ class WunderBar extends React.PureComponent {
method = "onSearch"; method = "onSearch";
} }
this.props[method](uri); this.props[method](uri, extraParams);
this._input.blur(); this._input.blur();
} }
} }

View file

@ -5,8 +5,10 @@ export const HISTORY_BACK = "HISTORY_BACK";
export const SHOW_SNACKBAR = "SHOW_SNACKBAR"; export const SHOW_SNACKBAR = "SHOW_SNACKBAR";
export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK"; export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK";
export const WINDOW_FOCUSED = "WINDOW_FOCUSED"; export const WINDOW_FOCUSED = "WINDOW_FOCUSED";
export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH";
export const DAEMON_READY = "DAEMON_READY"; export const DAEMON_READY = "DAEMON_READY";
export const DAEMON_VERSION_MATCH = "DAEMON_VERSION_MATCH";
export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH";
// Upgrades // Upgrades
export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED"; export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED";
@ -47,7 +49,7 @@ export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED";
export const FETCH_CLAIM_LIST_MINE_COMPLETED = export const FETCH_CLAIM_LIST_MINE_COMPLETED =
"FETCH_CLAIM_LIST_MINE_COMPLETED"; "FETCH_CLAIM_LIST_MINE_COMPLETED";
export const FILE_LIST_STARTED = "FILE_LIST_STARTED"; export const FILE_LIST_STARTED = "FILE_LIST_STARTED";
export const FILE_LIST_COMPLETED = "FILE_LIST_COMPLETED"; export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED";
export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED"; export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED";
export const FETCH_FILE_INFO_COMPLETED = "FETCH_FILE_INFO_COMPLETED"; export const FETCH_FILE_INFO_COMPLETED = "FETCH_FILE_INFO_COMPLETED";
export const FETCH_COST_INFO_STARTED = "FETCH_COST_INFO_STARTED"; export const FETCH_COST_INFO_STARTED = "FETCH_COST_INFO_STARTED";
@ -63,7 +65,16 @@ export const FETCH_AVAILABILITY_STARTED = "FETCH_AVAILABILITY_STARTED";
export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED"; export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED";
export const FILE_DELETE = "FILE_DELETE"; export const FILE_DELETE = "FILE_DELETE";
export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED"; export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED";
export const ABANDON_CLAIM_COMPLETED = "ABANDON_CLAIM_COMPLETED"; 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 // Search
export const SEARCH_STARTED = "SEARCH_STARTED"; export const SEARCH_STARTED = "SEARCH_STARTED";
@ -86,9 +97,13 @@ export const USER_EMAIL_NEW_FAILURE = "USER_EMAIL_NEW_FAILURE";
export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED"; export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED";
export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS"; export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS";
export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE"; export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_VERIFY_FAILURE";
export const USER_IDENTITY_VERIFY_STARTED = "USER_IDENTITY_VERIFY_STARTED";
export const USER_IDENTITY_VERIFY_SUCCESS = "USER_IDENTITY_VERIFY_SUCCESS";
export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE";
export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; export const USER_FETCH_STARTED = "USER_FETCH_STARTED";
export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS";
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";
export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS";
// Rewards // Rewards
export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED"; export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED";
@ -97,6 +112,14 @@ export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED";
export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS"; export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS";
export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE";
export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";
export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED";
//Language //Language
export const CHANGE_LANGUAGE = "CHANGE_LANGUAGE";
export const LANGUAGE_CHANGED = "LANGUAGE_CHANGED"; export const LANGUAGE_CHANGED = "LANGUAGE_CHANGED";
export const LANGUAGE_RESOLVED = "LANGUAGE_RESOLVED";
export const DOWNLOAD_LANGUAGE_STARTED = "DOWNLOAD_LANGUAGE_STARTED";
export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED";
export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED";
export const DOWNLOAD_LANGUAGES_STARTED = "DOWNLOAD_LANGUAGES_STARTED";
export const DOWNLOAD_LANGUAGES_COMPLETED = "DOWNLOAD_LANGUAGES_COMPLETED";

View file

@ -1,2 +1,9 @@
export const WELCOME = "welcome";
export const CONFIRM_FILE_REMOVE = "confirmFileRemove"; export const CONFIRM_FILE_REMOVE = "confirmFileRemove";
export const INCOMPATIBLE_DAEMON = "incompatibleDaemon";
export const DOWNLOADING = "downloading";
export const ERROR = "error";
export const INSUFFICIENT_CREDITS = "insufficient_credits";
export const UPGRADE = "upgrade";
export const WELCOME = "welcome";
export const FIRST_REWARD = "first_reward";
export const AUTHENTICATION_FAILURE = "auth_failure";

View file

@ -27,18 +27,19 @@ jsonrpc.call = function(
xhr.addEventListener("load", function() { xhr.addEventListener("load", function() {
var response = JSON.parse(xhr.responseText); var response = JSON.parse(xhr.responseText);
if (response.error) { let error = response.error || (response.result && response.result.error);
if (error) {
if (errorCallback) { if (errorCallback) {
errorCallback(response.error); errorCallback(error);
} else { } else {
var errorEvent = new CustomEvent("unhandledError", { var errorEvent = new CustomEvent("unhandledError", {
detail: { detail: {
connectionString: connectionString, connectionString: connectionString,
method: method, method: method,
params: params, params: params,
code: response.error.code, code: error.code,
message: response.error.message, message: error.message || error,
data: response.error.data, data: error.data,
}, },
}); });
document.dispatchEvent(errorEvent); document.dispatchEvent(errorEvent);

View file

@ -223,55 +223,47 @@ lbry.publishDeprecated = function(
) { ) {
lbry.publish(params).then( lbry.publish(params).then(
result => { result => {
if (returnedPending) { if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
return;
}
clearTimeout(returnPendingTimeout);
publishedCallback(result); publishedCallback(result);
}, },
err => { err => {
if (returnedPending) { if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
return;
}
clearTimeout(returnPendingTimeout);
errorCallback(err); errorCallback(err);
} }
); );
let returnedPending = false;
// Give a short grace period in case publish() returns right away or (more likely) gives an error // Give a short grace period in case publish() returns right away or (more likely) gives an error
const returnPendingTimeout = setTimeout(() => { const returnPendingTimeout = setTimeout(
returnedPending = true; () => {
if (publishedCallback) {
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
publishedCallback(true);
}
if (publishedCallback) { if (fileListedCallback) {
savePendingPublish({ const { name, channel_name } = params;
name: params.name, savePendingPublish({
channel_name: params.channel_name, name: params.name,
}); channel_name: params.channel_name,
publishedCallback(true); });
} fileListedCallback(true);
}
if (fileListedCallback) { },
const { name, channel_name } = params; 2000,
savePendingPublish({ { once: true }
name: params.name, );
channel_name: params.channel_name,
});
fileListedCallback(true);
}
}, 2000);
}; };
lbry.getClientSettings = function() { lbry.getClientSettings = function() {
var outSettings = {}; var outSettings = {};
for (let setting of Object.keys(lbry.defaultClientSettings)) { for (let setting of Object.keys(lbry.defaultClientSettings)) {
var localStorageVal = localStorage.getItem("setting_" + setting); var localStorageVal = localStorage.getItem("setting_" + setting);
outSettings[setting] = outSettings[setting] = localStorageVal === null
localStorageVal === null ? lbry.defaultClientSettings[setting]
? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal);
: JSON.parse(localStorageVal);
} }
return outSettings; return outSettings;
}; };
@ -296,15 +288,10 @@ lbry.setClientSetting = function(setting, value) {
return localStorage.setItem("setting_" + setting, JSON.stringify(value)); return localStorage.setItem("setting_" + setting, JSON.stringify(value));
}; };
//utilities
lbry.formatCredits = function(amount, precision) {
return amount.toFixed(precision || 1).replace(/\.?0+$/, "");
};
lbry.formatName = function(name) { lbry.formatName = function(name) {
// Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes) // Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes)
name = name.replace("/s+/g", "-"); name = name.replace("/s+/g", "-");
name = name.toLowerCase().replace(/[^a-z0-9\-]/g, ""); name = name.toLowerCase().replace(lbryuri.REGEXP_INVALID_URI, "");
return name; return name;
}; };
@ -429,10 +416,16 @@ lbry.file_list = function(params = {}) {
fileInfos => { fileInfos => {
removePendingPublishIfNeeded({ name, channel_name, outpoint }); removePendingPublishIfNeeded({ name, channel_name, outpoint });
const dummyFileInfos = lbry //if a naked file_list call, append the pending file infos
.getPendingPublishes() if (!name && !channel_name && !outpoint) {
.map(pendingPublishToDummyFileInfo); const dummyFileInfos = lbry
resolve([...fileInfos, ...dummyFileInfos]); .getPendingPublishes()
.map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]);
} else {
resolve(fileInfos);
}
}, },
reject reject
); );

View file

@ -113,16 +113,24 @@ lbryio.call = function(resource, action, params = {}, method = "get") {
}); });
}; };
lbryio._authToken = null;
lbryio.getAuthToken = () => { lbryio.getAuthToken = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ipcRenderer.once("auth-token-response", (event, token) => { if (lbryio._authToken) {
return resolve(token); resolve(lbryio._authToken);
}); } else {
ipcRenderer.send("get-auth-token"); ipcRenderer.once("auth-token-response", (event, token) => {
lbryio._authToken = token;
return resolve(token);
});
ipcRenderer.send("get-auth-token");
}
}); });
}; };
lbryio.setAuthToken = token => { lbryio.setAuthToken = token => {
lbryio._authToken = token ? token.toString().trim() : null;
ipcRenderer.send("set-auth-token", token); ipcRenderer.send("set-auth-token", token);
}; };
@ -136,8 +144,9 @@ lbryio.authenticate = function() {
resolve({ resolve({
id: 1, id: 1,
language: "en", language: "en",
has_email: true, primary_email: "disabled@lbry.io",
has_verified_email: true, has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false, is_reward_approved: false,
is_reward_eligible: false, is_reward_eligible: false,
}); });

View file

@ -3,6 +3,8 @@ const CLAIM_ID_MAX_LEN = 40;
const lbryuri = {}; const lbryuri = {};
lbryuri.REGEXP_INVALID_URI = /[^A-Za-z0-9-]/g;
/** /**
* Parses a LBRY name into its component parts. Throws errors with user-friendly * Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names. * messages for invalid names.
@ -70,7 +72,7 @@ lbryuri.parse = function(uri, requireProto = false) {
contentName = path; contentName = path;
} }
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g); const nameBadChars = (channelName || name).match(lbryuri.REGEXP_INVALID_URI);
if (nameBadChars) { if (nameBadChars) {
throw new Error( throw new Error(
__( __(
@ -119,7 +121,7 @@ lbryuri.parse = function(uri, requireProto = false) {
throw new Error(__("Only channel URIs may have a path.")); throw new Error(__("Only channel URIs may have a path."));
} }
const pathBadChars = path.match(/[^A-Za-z0-9-]/g); const pathBadChars = path.match(lbryuri.REGEXP_INVALID_URI);
if (pathBadChars) { if (pathBadChars) {
throw new Error( throw new Error(
__(`Invalid character in path: %s`, pathBadChars.join(", ")) __(`Invalid character in path: %s`, pathBadChars.join(", "))
@ -203,6 +205,8 @@ lbryuri.build = function(uriObj, includeProto = true, allowExtraProps = false) {
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of adding the lbry:// prefix if needed) */ * consists of adding the lbry:// prefix if needed) */
lbryuri.normalize = function(uri) { lbryuri.normalize = function(uri) {
if (uri.match(/pending_claim/)) return uri;
const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse( const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
uri uri
); );

View file

@ -5,11 +5,10 @@ import App from "component/app/index.js";
import SnackBar from "component/snackBar"; import SnackBar from "component/snackBar";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import store from "store.js"; import store from "store.js";
import SplashScreen from "component/splash.js"; import SplashScreen from "component/splash";
import AuthOverlay from "component/authOverlay";
import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; import { doChangePath, doNavigate, doDaemonReady } from "actions/app";
import { doDownloadLanguages } from "actions/settings";
import { toQueryString } from "util/query_params"; import { toQueryString } from "util/query_params";
import { selectBadgeNumber } from "selectors/app";
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import fs from "fs"; import fs from "fs";
import http from "http"; import http from "http";
@ -39,11 +38,11 @@ window.addEventListener("popstate", (event, param) => {
let action; let action;
if (hash !== "") { if (hash !== "") {
const url = hash.split("#")[1]; const url = hash.replace(/^#/, "");
const params = event.state; const { params, scrollY } = event.state || {};
const queryString = toQueryString(params); const queryString = toQueryString(params);
app.store.dispatch(doChangePath(`${url}?${queryString}`)); app.store.dispatch(doChangePath(`${url}?${queryString}`, { scrollY }));
} else { } else {
app.store.dispatch(doChangePath("/discover")); app.store.dispatch(doChangePath("/discover"));
} }
@ -100,27 +99,16 @@ const updateProgress = () => {
const initialState = app.store.getState(); const initialState = app.store.getState();
// import whyDidYouUpdate from "why-did-you-update";
// if (env === "development") {
// /*
// https://github.com/garbles/why-did-you-update
// "A function that monkey patches React and notifies you in the console when
// potentially unnecessary re-renders occur."
//
// Just checks if props change between updates. Can be fixed by manually
// adding a check in shouldComponentUpdate or using React.PureComponent
// */
// whyDidYouUpdate(React);
// }
var init = function() { var init = function() {
app.store.dispatch(doDownloadLanguages());
function onDaemonReady() { function onDaemonReady() {
window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again
app.store.dispatch(doDaemonReady()); app.store.dispatch(doDaemonReady());
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<div><AuthOverlay /><App /><SnackBar /></div> <div><App /><SnackBar /></div>
</Provider>, </Provider>,
canvas canvas
); );
@ -129,44 +117,13 @@ var init = function() {
if (window.sessionStorage.getItem("loaded") == "y") { if (window.sessionStorage.getItem("loaded") == "y") {
onDaemonReady(); onDaemonReady();
} else { } else {
ReactDOM.render(<SplashScreen onLoadDone={onDaemonReady} />, canvas); ReactDOM.render(
<Provider store={store}>
<SplashScreen onReadyToLaunch={onDaemonReady} />
</Provider>,
canvas
);
} }
}; };
const download = (url, dest, lang, cb) => {
const file = fs.createWriteStream(dest);
const request = http.get(url, response => {
response.pipe(file);
file.on('finish', () => {
file.close(cb); // close() is async, call cb after close completes.
app.i18n.localLanguages.push(lang.replace(".json", "")); // push to our local list
});
}).on('error', err => { // Handle errors
fs.unlink(dest); // Delete the file async. (But we don't check the result)
if (cb) cb(err.message);
});
};
const downloadLanguages = () => {
if (!fs.existsSync("app/locales")){
fs.mkdirSync("app/locales");
}
http.request({ host: 'i18n.lbry.io', path: '/' }, response => {
let str = '';
response.on('data', chunk => {
str += chunk;
});
response.on('end', () => {
const files = JSON.parse(str);JSON.parse(str);
app.i18n.localLanguages = [];
for (let i = 0; i < files.length; i++) {
download(`http://i18n.lbry.io/langs/${files[i]}`, `app/locales/${files[i]}`, files[i], () => {});
}
});
}).end();
};
downloadLanguages();
init(); init();

30
ui/js/page/auth/index.js Normal file
View file

@ -0,0 +1,30 @@
import React from "react";
import { doNavigate } from "actions/app";
import { connect } from "react-redux";
import { selectPathAfterAuth } from "selectors/app";
import {
selectAuthenticationIsPending,
selectEmailToVerify,
selectUserIsVerificationCandidate,
selectUser,
selectUserIsPending,
selectIdentityVerifyIsPending,
} from "selectors/user";
import AuthPage from "./view";
const select = state => ({
isPending:
selectAuthenticationIsPending(state) ||
selectUserIsPending(state) ||
selectIdentityVerifyIsPending(state),
email: selectEmailToVerify(state),
pathAfterAuth: selectPathAfterAuth(state),
user: selectUser(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
});
export default connect(select, perform)(AuthPage);

95
ui/js/page/auth/view.jsx Normal file
View file

@ -0,0 +1,95 @@
import React from "react";
import { BusyMessage } from "component/common";
import Link from "component/link";
import UserEmailNew from "component/userEmailNew";
import UserEmailVerify from "component/userEmailVerify";
import UserVerify from "component/userVerify";
export class AuthPage extends React.PureComponent {
componentWillMount() {
this.navigateIfAuthenticated(this.props);
}
componentWillReceiveProps(nextProps) {
this.navigateIfAuthenticated(nextProps);
}
navigateIfAuthenticated(props) {
const { isPending, user } = props;
if (
!isPending &&
user &&
user.has_verified_email &&
(user.is_reward_approved || user.is_identity_verified)
) {
props.navigate(props.pathAfterAuth);
}
}
getTitle() {
const { email, isPending, isVerificationCandidate, user } = this.props;
if (isPending || (user && !user.has_verified_email && !email)) {
return __("Welcome to LBRY");
} else if (user && !user.has_verified_email) {
return __("Confirm Email");
} else if (user && !user.is_identity_verified && !user.is_reward_approved) {
return __("Confirm Identity");
} else {
return __("Welcome to LBRY");
}
}
renderMain() {
const { email, isPending, isVerificationCandidate, user } = this.props;
if (isPending) {
return <BusyMessage message={__("Authenticating")} />;
} else if (user && !user.has_verified_email && !email) {
return <UserEmailNew />;
} else if (user && !user.has_verified_email) {
return <UserEmailVerify />;
} else if (user && !user.is_identity_verified) {
return <UserVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
render() {
const { email, user, isPending, navigate } = this.props;
return (
<main className="">
<section className="card card--form">
<div className="card__title-primary">
<h1>{this.getTitle()}</h1>
</div>
<div className="card__content">
{!isPending &&
!email &&
user &&
!user.has_verified_email &&
<p>
{__("Create a verified identity and receive LBC rewards.")}
</p>}
{this.renderMain()}
</div>
<div className="card__content">
<div className="help">
{__(
"This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards."
) + " "}
<Link
onClick={() => navigate("/discover")}
label={__("Return home")}
/>.
</div>
</div>
</section>
</main>
);
}
}
export default AuthPage;

View file

@ -3,24 +3,34 @@ import { connect } from "react-redux";
import { doFetchClaimsByChannel } from "actions/content"; import { doFetchClaimsByChannel } from "actions/content";
import { import {
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectClaimsInChannelForUri, makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims,
} from "selectors/claims"; } from "selectors/claims";
import { selectCurrentParams } from "selectors/app";
import { doNavigate } from "actions/app";
import { makeSelectTotalPagesForChannel } from "selectors/content";
import ChannelPage from "./view"; import ChannelPage from "./view";
const makeSelect = () => { const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(), const selectClaim = makeSelectClaimForUri(),
selectClaimsInChannel = makeSelectClaimsInChannelForUri(); selectClaimsInChannel = makeSelectClaimsInChannelForCurrentPage(),
selectFetchingChannelClaims = makeSelectFetchingChannelClaims(),
selectTotalPagesForChannel = makeSelectTotalPagesForChannel();
const select = (state, props) => ({ const select = (state, props) => ({
claim: selectClaim(state, props), claim: selectClaim(state, props),
claimsInChannel: selectClaimsInChannel(state, props), claimsInChannel: selectClaimsInChannel(state, props),
fetching: selectFetchingChannelClaims(state, props),
totalPages: selectTotalPagesForChannel(state, props),
params: selectCurrentParams(state),
}); });
return select; return select;
}; };
const perform = dispatch => ({ const perform = dispatch => ({
fetchClaims: uri => dispatch(doFetchClaimsByChannel(uri)), fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
navigate: (path, params) => dispatch(doNavigate(path, params)),
}); });
export default connect(makeSelect, perform)(ChannelPage); export default connect(makeSelect, perform)(ChannelPage);

View file

@ -2,24 +2,41 @@ import React from "react";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import { BusyMessage } from "component/common"; import { BusyMessage } from "component/common";
import FileTile from "component/fileTile"; import FileTile from "component/fileTile";
import Link from "component/link";
import ReactPaginate from "react-paginate";
class ChannelPage extends React.PureComponent { class ChannelPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.fetchClaims(this.props); const { uri, params, fetchClaims } = this.props;
fetchClaims(uri, params.page || 1);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.fetchClaims(nextProps); const { params, fetching, fetchClaims } = this.props;
const nextParams = nextProps.params;
if (fetching !== nextParams.page && params.page !== nextParams.page)
fetchClaims(nextProps.uri, nextParams.page);
} }
fetchClaims(props) { changePage(pageNumber) {
if (props.claimsInChannel === undefined) { const { params, currentPage } = this.props;
props.fetchClaims(props.uri); const newParams = Object.assign({}, params, { page: pageNumber });
}
this.props.navigate("/show", newParams);
} }
render() { render() {
const { claimsInChannel, claim, uri } = this.props; const {
fetching,
claimsInChannel,
claim,
uri,
params,
totalPages,
} = this.props;
const { page } = params;
let contentList; let contentList;
if (claimsInChannel === undefined) { if (claimsInChannel === undefined) {
@ -29,14 +46,17 @@ class ChannelPage extends React.PureComponent {
? claimsInChannel.map(claim => ? claimsInChannel.map(claim =>
<FileTile <FileTile
key={claim.claim_id} key={claim.claim_id}
uri={lbryuri.build({ name: claim.name, claimId: claim.claim_id })} uri={lbryuri.build({
name: claim.name,
claimId: claim.claim_id,
})}
/> />
) )
: <span className="empty">{__("No content found.")}</span>; : <span className="empty">{__("No content found.")}</span>;
} }
return ( return (
<main className="main--single-column"> <div>
<section className="card"> <section className="card">
<div className="card__inner"> <div className="card__inner">
<div className="card__title-identity"><h1>{uri}</h1></div> <div className="card__title-identity"><h1>{uri}</h1></div>
@ -51,7 +71,24 @@ class ChannelPage extends React.PureComponent {
</section> </section>
<h3 className="card-row__header">{__("Published Content")}</h3> <h3 className="card-row__header">{__("Published Content")}</h3>
{contentList} {contentList}
</main> <div />
{(!fetching || (claimsInChannel && claimsInChannel.length)) &&
totalPages > 1 &&
<ReactPaginate
pageCount={totalPages}
pageRangeDisplayed={2}
previousLabel=""
nextLabel=""
activeClassName="pagination__item--selected"
pageClassName="pagination__item"
previousClassName="pagination__item pagination__item--previous"
nextClassName="pagination__item pagination__item--next"
marginPagesDisplayed={2}
onPageChange={e => this.changePage(e.selected + 1)}
initialPage={parseInt(page - 1)}
containerClassName="pagination"
/>}
</div>
); );
} }
} }

View file

@ -1,6 +1,6 @@
import lbry from "../lbry.js"; import lbry from "../lbry.js";
import React from "react"; import React from "react";
import { FormField } from "../component/form.js"; import FormField from "component/formField";
import Link from "../component/link"; import Link from "../component/link";
const fs = require("fs"); const fs = require("fs");

View file

@ -1,38 +1,203 @@
import React from "react"; import React from "react";
import lbryio from "lbryio.js"; import ReactDOM from "react-dom";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import FileCard from "component/fileCard"; import FileCard from "component/fileCard";
import { BusyMessage } from "component/common.js"; import { Icon, BusyMessage } from "component/common.js";
import ToolTip from "component/tooltip.js"; import ToolTip from "component/tooltip.js";
const FeaturedCategory = props => { class FeaturedCategory extends React.PureComponent {
const { category, names } = props; componentWillMount() {
this.setState({
numItems: this.props.names.length,
canScrollPrevious: false,
canScrollNext: true,
});
}
return ( handleScrollPrevious() {
<div className="card-row card-row--small"> const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
<h3 className="card-row__header"> if (cardRow.scrollLeft > 0) {
{category} // check the visible cards
{category && const cards = cardRow.getElementsByTagName("section");
category.match(/^community/i) && let firstVisibleCard = null;
<ToolTip let firstVisibleIdx = -1;
label={__("What's this?")} for (var i = 0; i < cards.length; i++) {
body={__( if (this.isCardVisible(cards[i], cardRow, false)) {
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!' firstVisibleCard = cards[i];
)} firstVisibleIdx = i;
className="tooltip--header" break;
/>} }
</h3> }
{names &&
names.map(name => const numDisplayed = this.numDisplayedCards(cardRow);
<FileCard const scrollToIdx = firstVisibleIdx - numDisplayed;
key={name} const animationCallback = () => {
displayStyle="card" this.setState({
uri={lbryuri.normalize(name)} canScrollPrevious: cardRow.scrollLeft !== 0,
/> canScrollNext: true,
)} });
</div> };
); this.scrollCardItemsLeftAnimated(
}; cardRow,
scrollToIdx < 0 ? 0 : cards[scrollToIdx].offsetLeft,
100,
animationCallback
);
}
}
handleScrollNext() {
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
// check the visible cards
const cards = cardRow.getElementsByTagName("section");
let lastVisibleCard = null;
let lastVisibleIdx = -1;
for (var i = 0; i < cards.length; i++) {
if (this.isCardVisible(cards[i], cardRow, true)) {
lastVisibleCard = cards[i];
lastVisibleIdx = i;
}
}
if (lastVisibleCard) {
const numDisplayed = this.numDisplayedCards(cardRow);
const animationCallback = () => {
// update last visible index after scroll
for (var i = 0; i < cards.length; i++) {
if (this.isCardVisible(cards[i], cardRow, true)) {
lastVisibleIdx = i;
}
}
this.setState({ canScrollPrevious: true });
if (lastVisibleIdx === cards.length - 1) {
this.setState({ canScrollNext: false });
}
};
this.scrollCardItemsLeftAnimated(
cardRow,
Math.min(
lastVisibleCard.offsetLeft,
cardRow.scrollWidth - cardRow.clientWidth
),
100,
animationCallback
);
}
}
scrollCardItemsLeftAnimated(cardRow, target, duration, callback) {
if (!duration || duration <= diff) {
cardRow.scrollLeft = target;
if (callback) {
callback();
}
return;
}
const component = this;
const diff = target - cardRow.scrollLeft;
const tick = diff / duration * 10;
setTimeout(() => {
cardRow.scrollLeft = cardRow.scrollLeft + tick;
if (cardRow.scrollLeft === target) {
if (callback) {
callback();
}
return;
}
component.scrollCardItemsLeftAnimated(
cardRow,
target,
duration - 10,
callback
);
}, 10);
}
isCardVisible(section, cardRow, partialVisibility) {
// check if a card is fully or partialy visible in its parent
const cardRowWidth = cardRow.offsetWidth;
const cardRowLeft = cardRow.scrollLeft;
const cardRowEnd = cardRowLeft + cardRow.offsetWidth;
const sectionLeft = section.offsetLeft - cardRowLeft;
const sectionEnd = sectionLeft + section.offsetWidth;
return (
(sectionLeft >= 0 && sectionEnd <= cardRowWidth) ||
(((sectionLeft < 0 && sectionEnd > 0) ||
(sectionLeft > 0 && sectionLeft <= cardRowWidth)) &&
partialVisibility)
);
}
numDisplayedCards(cardRow) {
const cards = cardRow.getElementsByTagName("section");
const cardRowWidth = cardRow.offsetWidth;
// get the width of the first card and then calculate
const cardWidth = cards.length > 0 ? cards[0].offsetWidth : 0;
if (cardWidth > 0) {
return Math.ceil(cardRowWidth / cardWidth);
}
// return a default value of 1 card displayed if the card width couldn't be determined
return 1;
}
render() {
const { category, names } = this.props;
return (
<div className="card-row card-row--small">
<h3 className="card-row__header">
{category}
{category &&
category.match(/^community/i) &&
<ToolTip
label={__("What's this?")}
body={__(
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
)}
className="tooltip--header"
/>}
</h3>
<div className="card-row__scrollhouse">
{this.state.canScrollPrevious &&
<div className="card-row__nav card-row__nav--left">
<a
className="card-row__scroll-button"
onClick={this.handleScrollPrevious.bind(this)}
>
<Icon icon="icon-chevron-left" />
</a>
</div>}
{this.state.canScrollNext &&
<div className="card-row__nav card-row__nav--right">
<a
className="card-row__scroll-button"
onClick={this.handleScrollNext.bind(this)}
>
<Icon icon="icon-chevron-right" />
</a>
</div>}
<div ref="rowitems" className="card-row__items">
{names &&
names.map(name =>
<FileCard
key={name}
displayStyle="card"
uri={lbryuri.normalize(name)}
/>
)}
</div>
</div>
</div>
);
}
}
class DiscoverPage extends React.PureComponent { class DiscoverPage extends React.PureComponent {
componentWillMount() { componentWillMount() {
@ -45,16 +210,20 @@ class DiscoverPage extends React.PureComponent {
render() { render() {
const { featuredUris, fetchingFeaturedUris } = this.props; const { featuredUris, fetchingFeaturedUris } = this.props;
const failedToLoad = const hasContent =
!fetchingFeaturedUris && typeof featuredUris === "object" && Object.keys(featuredUris).length,
(featuredUris === undefined || failedToLoad = !fetchingFeaturedUris && !hasContent;
(featuredUris !== undefined && Object.keys(featuredUris).length === 0));
return ( return (
<main> <main
{fetchingFeaturedUris && className={
hasContent && fetchingFeaturedUris ? "main--refreshing" : null
}
>
{!hasContent &&
fetchingFeaturedUris &&
<BusyMessage message={__("Fetching content")} />} <BusyMessage message={__("Fetching content")} />}
{typeof featuredUris === "object" && {hasContent &&
Object.keys(featuredUris).map( Object.keys(featuredUris).map(
category => category =>
featuredUris[category].length featuredUris[category].length

View file

@ -3,15 +3,22 @@ import { connect } from "react-redux";
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
import { import {
selectFileInfosDownloaded, selectFileInfosDownloaded,
selectFileListDownloadedOrPublishedIsPending, selectIsFetchingFileListDownloadedOrPublished,
} from "selectors/file_info"; } from "selectors/file_info";
import {
selectMyClaimsWithoutChannels,
selectIsFetchingClaimListMine,
} from "selectors/claims";
import { doFetchClaimListMine } from "actions/content";
import { doNavigate } from "actions/app"; import { doNavigate } from "actions/app";
import { doCancelAllResolvingUris } from "actions/content"; import { doCancelAllResolvingUris } from "actions/content";
import FileListDownloaded from "./view"; import FileListDownloaded from "./view";
const select = state => ({ const select = state => ({
fileInfos: selectFileInfosDownloaded(state), fileInfos: selectFileInfosDownloaded(state),
isPending: selectFileListDownloadedOrPublishedIsPending(state), isFetching: selectIsFetchingFileListDownloadedOrPublished(state),
claims: selectMyClaimsWithoutChannels(state),
isFetchingClaims: selectIsFetchingClaimListMine(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
@ -19,6 +26,7 @@ const perform = dispatch => ({
fetchFileInfosDownloaded: () => fetchFileInfosDownloaded: () =>
dispatch(doFetchFileInfosAndPublishedClaims()), dispatch(doFetchFileInfosAndPublishedClaims()),
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()), cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
fetchClaims: () => dispatch(doFetchClaimListMine()),
}); });
export default connect(select, perform)(FileListDownloaded); export default connect(select, perform)(FileListDownloaded);

View file

@ -1,18 +1,14 @@
import React from "react"; import React from "react";
import lbry from "lbry.js";
import lbryuri from "lbryuri.js";
import Link from "component/link"; import Link from "component/link";
import { FormField } from "component/form.js";
import { FileTile } from "component/fileTile"; import { FileTile } from "component/fileTile";
import rewards from "rewards.js";
import lbryio from "lbryio.js";
import { BusyMessage, Thumbnail } from "component/common.js"; import { BusyMessage, Thumbnail } from "component/common.js";
import FileList from "component/fileList"; import FileList from "component/fileList";
import SubHeader from "component/subHeader"; import SubHeader from "component/subHeader";
class FileListDownloaded extends React.PureComponent { class FileListDownloaded extends React.PureComponent {
componentWillMount() { componentWillMount() {
if (!this.props.isPending) this.props.fetchFileInfosDownloaded(); if (!this.props.isFetchingClaims) this.props.fetchClaims();
if (!this.props.isFetching) this.props.fetchFileInfosDownloaded();
} }
componentWillUnmount() { componentWillUnmount() {
@ -20,13 +16,13 @@ class FileListDownloaded extends React.PureComponent {
} }
render() { render() {
const { fileInfos, isPending, navigate } = this.props; const { fileInfos, isFetching, navigate } = this.props;
let content; let content;
if (fileInfos && fileInfos.length > 0) { if (fileInfos && fileInfos.length > 0) {
content = <FileList fileInfos={fileInfos} fetching={isPending} />; content = <FileList fileInfos={fileInfos} fetching={isFetching} />;
} else { } else {
if (isPending) { if (isFetching) {
content = <BusyMessage message={__("Loading")} />; content = <BusyMessage message={__("Loading")} />;
} else { } else {
content = ( content = (

Some files were not shown because too many files have changed in this diff Show more