Merge branch 'master' into hackrush
This commit is contained in:
commit
deefd9c824
108 changed files with 4334 additions and 1889 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,3 +26,4 @@ build/daemon.zip
|
|||
.vimrc
|
||||
|
||||
package-lock.json
|
||||
ui/yarn.lock
|
||||
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -8,23 +8,39 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
*
|
||||
*
|
||||
* Added option to release claim when deleting a file
|
||||
* Added transition to card hovers to smooth animation
|
||||
* Support markdown makeup in claim description
|
||||
* Replaced free speech flag (used when image is missing) with labeled color tiles
|
||||
* Added a loading message to file actions
|
||||
* URL is auto suggested in Publish Page
|
||||
|
||||
### Changed
|
||||
*
|
||||
*
|
||||
* Publishes now uses claims rather than files
|
||||
* Publishing revamped. Editing claims is much easier.
|
||||
|
||||
### 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 thinking downloads with 0 progress were downloaded after restart
|
||||
|
||||
### Deprecated
|
||||
*
|
||||
*
|
||||
|
||||
### Removed
|
||||
*
|
||||
* Removed bandwidth caps from settings, because the daemon was not respecting them anyway.
|
||||
*
|
||||
|
||||
## [0.13.0] - 2017-06-30
|
||||
|
|
11
app/main.js
11
app/main.js
|
@ -27,7 +27,7 @@ const {version: localVersion} = require(app.getAppPath() + '/package.json');
|
|||
|
||||
const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
|
||||
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({
|
||||
host: 'localhost',
|
||||
|
@ -207,13 +207,8 @@ function handleDaemonSubprocessExited() {
|
|||
function launchDaemon() {
|
||||
assert(!daemonSubprocess, 'Tried to launch daemon twice');
|
||||
|
||||
if (process.env.LBRY_DAEMON) {
|
||||
executable = process.env.LBRY_DAEMON;
|
||||
} else {
|
||||
executable = path.join(__dirname, 'dist', 'lbrynet-daemon');
|
||||
}
|
||||
console.log('Launching daemon:', executable)
|
||||
daemonSubprocess = child_process.spawn(executable)
|
||||
console.log('Launching daemon:', DAEMON_PATH)
|
||||
daemonSubprocess = child_process.spawn(DAEMON_PATH)
|
||||
// Need to handle the data event instead of attaching to
|
||||
// process.stdout because the latter doesn't work. I believe on
|
||||
// windows it buffers stdout and we don't get any meaningful output
|
||||
|
|
|
@ -18,5 +18,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"electron-rebuild": "^1.5.11"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.14.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
https://github.com/lbryio/lbry/releases/download/v0.13.1/lbrynet-daemon-v0.13.1-OSNAME.zip
|
|
@ -46,8 +46,8 @@ dir dist # verify that binary was built/named correctly
|
|||
|
||||
# sign binary
|
||||
nuget install secure-file -ExcludeVersion
|
||||
secure-file\tools\secure-file -decrypt build\lbry2.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
|
||||
secure-file\tools\secure-file -decrypt build\lbry3.pfx.enc -secret "$env:pfx_key"
|
||||
& ${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
|
|
@ -79,11 +79,14 @@ if $OSX; then
|
|||
else
|
||||
OSNAME="linux"
|
||||
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"
|
||||
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/"
|
||||
rm "$BUILD_DIR/daemon.zip"
|
||||
|
||||
|
||||
|
||||
###################
|
||||
# Build the app #
|
||||
###################
|
||||
|
|
Binary file not shown.
BIN
build/lbry3.pfx.enc
Normal file
BIN
build/lbry3.pfx.enc
Normal file
Binary file not shown.
|
@ -4,7 +4,6 @@ import {
|
|||
selectUpdateUrl,
|
||||
selectUpgradeDownloadPath,
|
||||
selectUpgradeDownloadItem,
|
||||
selectUpgradeFilename,
|
||||
selectPageTitle,
|
||||
selectCurrentPage,
|
||||
selectCurrentParams,
|
||||
|
@ -13,31 +12,43 @@ import { doSearch } from "actions/search";
|
|||
import { doFetchDaemonSettings } from "actions/settings";
|
||||
import { doAuthenticate } from "actions/user";
|
||||
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 path = require("path");
|
||||
const app = require("electron").remote.app;
|
||||
const { download } = remote.require("electron-dl");
|
||||
const fs = remote.require("fs");
|
||||
|
||||
const queryStringFromParams = params => {
|
||||
return Object.keys(params).map(key => `${key}=${params[key]}`).join("&");
|
||||
};
|
||||
const { lbrySettings: config } = require("../../../app/package.json");
|
||||
|
||||
export function doNavigate(path, params = {}) {
|
||||
return function(dispatch, getState) {
|
||||
let url = path;
|
||||
if (params) url = `${url}?${queryStringFromParams(params)}`;
|
||||
if (params) url = `${url}?${toQueryString(params)}`;
|
||||
|
||||
dispatch(doChangePath(url));
|
||||
|
||||
const state = getState();
|
||||
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) {
|
||||
dispatch({
|
||||
type: types.CHANGE_PATH,
|
||||
|
@ -48,8 +59,12 @@ export function doChangePath(path) {
|
|||
|
||||
const state = getState();
|
||||
const pageTitle = selectPageTitle(state);
|
||||
const scrollY = options.scrollY;
|
||||
|
||||
window.document.title = pageTitle;
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
if (scrollY) window.scrollTo(0, scrollY);
|
||||
else window.scrollTo(0, 0);
|
||||
|
||||
const currentPage = selectCurrentPage(state);
|
||||
if (currentPage === "search") {
|
||||
|
@ -62,15 +77,32 @@ export function doChangePath(path) {
|
|||
export function doHistoryBack() {
|
||||
return function(dispatch, getState) {
|
||||
if (!history.state) return;
|
||||
if (history.state.index === 0) return;
|
||||
|
||||
history.back();
|
||||
};
|
||||
}
|
||||
|
||||
export function doHistoryPush(params, title, relativeUrl) {
|
||||
export function doHistoryPush(currentState, title, relativeUrl) {
|
||||
return function(dispatch, getState) {
|
||||
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 +149,9 @@ export function doDownloadUpgrade() {
|
|||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
// 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 upgradeFilename = selectUpgradeFilename(state);
|
||||
const dir = fs.mkdtempSync(
|
||||
remote.app.getPath("temp") + require("path").sep
|
||||
);
|
||||
|
||||
let options = {
|
||||
onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
|
||||
|
@ -202,11 +235,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) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
console.log("do alert error");
|
||||
console.log(errorList);
|
||||
dispatch({
|
||||
type: types.OPEN_MODAL,
|
||||
data: {
|
||||
|
@ -219,6 +262,9 @@ export function doAlertError(errorList) {
|
|||
|
||||
export function doDaemonReady() {
|
||||
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({
|
||||
type: types.DAEMON_READY,
|
||||
|
@ -248,3 +294,10 @@ export function doClearCache() {
|
|||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
export function doQuitAndLaunchDaemonHelp() {
|
||||
return function(dispatch, getState) {
|
||||
shell.openExternal("https://lbry.io/faq/incompatible-protocol-version");
|
||||
remote.app.quit();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import lbryuri from "lbryuri";
|
|||
import { selectBalance } from "selectors/wallet";
|
||||
import {
|
||||
selectFileInfoForUri,
|
||||
selectUrisDownloading,
|
||||
selectDownloadingByOutpoint,
|
||||
} from "selectors/file_info";
|
||||
import { selectResolvingUris } from "selectors/content";
|
||||
import { selectCostInfoForUri } from "selectors/cost_info";
|
||||
|
@ -16,6 +16,7 @@ import { selectTotalDownloadProgress } from "selectors/file_info";
|
|||
import setBadge from "util/setBadge";
|
||||
import setProgressBar from "util/setProgressBar";
|
||||
import batchActions from "util/batchActions";
|
||||
import * as modals from "constants/modal_types";
|
||||
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
|
@ -264,8 +265,9 @@ export function doPurchaseUri(uri, purchaseModalName) {
|
|||
const state = getState();
|
||||
const balance = selectBalance(state);
|
||||
const fileInfo = selectFileInfoForUri(state, { uri });
|
||||
const downloadingByUri = selectUrisDownloading(state);
|
||||
const alreadyDownloading = !!downloadingByUri[uri];
|
||||
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
||||
const alreadyDownloading =
|
||||
fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
||||
|
||||
// we already fully downloaded the file.
|
||||
if (fileInfo && fileInfo.completed) {
|
||||
|
@ -292,7 +294,7 @@ export function doPurchaseUri(uri, purchaseModalName) {
|
|||
}
|
||||
|
||||
if (cost > balance) {
|
||||
dispatch(doOpenModal("notEnoughCredits"));
|
||||
dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
|
||||
} else {
|
||||
dispatch(doOpenModal(purchaseModalName));
|
||||
}
|
||||
|
@ -339,3 +341,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);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,15 +3,18 @@ import lbry from "lbry";
|
|||
import { doFetchClaimListMine } from "actions/content";
|
||||
import {
|
||||
selectClaimsByUri,
|
||||
selectClaimListMineIsPending,
|
||||
selectIsFetchingClaimListMine,
|
||||
selectMyClaimsOutpoints,
|
||||
} from "selectors/claims";
|
||||
import {
|
||||
selectFileListIsPending,
|
||||
selectIsFetchingFileList,
|
||||
selectFileInfosByOutpoint,
|
||||
selectUrisLoading,
|
||||
selectTotalDownloadProgress,
|
||||
} 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");
|
||||
|
||||
|
@ -48,16 +51,16 @@ export function doFetchFileInfo(uri) {
|
|||
export function doFileList() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
const isPending = selectFileListIsPending(state);
|
||||
const isFetching = selectIsFetchingFileList(state);
|
||||
|
||||
if (!isPending) {
|
||||
if (!isFetching) {
|
||||
dispatch({
|
||||
type: types.FILE_LIST_STARTED,
|
||||
});
|
||||
|
||||
lbry.file_list().then(fileInfos => {
|
||||
dispatch({
|
||||
type: types.FILE_LIST_COMPLETED,
|
||||
type: types.FILE_LIST_SUCCEEDED,
|
||||
data: {
|
||||
fileInfos,
|
||||
},
|
||||
|
@ -102,14 +105,12 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
|||
},
|
||||
});
|
||||
|
||||
const success = () => {
|
||||
dispatch({
|
||||
type: types.ABANDON_CLAIM_COMPLETED,
|
||||
const success = dispatch({
|
||||
type: types.ABANDON_CLAIM_SUCCEEDED,
|
||||
data: {
|
||||
claimId: fileInfo.claim_id,
|
||||
},
|
||||
});
|
||||
};
|
||||
lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success);
|
||||
}
|
||||
}
|
||||
|
@ -121,17 +122,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() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState(),
|
||||
isClaimListMinePending = selectClaimListMineIsPending(state),
|
||||
isFileInfoListPending = selectFileListIsPending(state);
|
||||
isFetchingClaimListMine = selectIsFetchingClaimListMine(state),
|
||||
isFetchingFileInfo = selectIsFetchingFileList(state);
|
||||
|
||||
dispatch(doFetchClaimListMine());
|
||||
dispatch(doFileList());
|
||||
if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine());
|
||||
if (!isFetchingFileInfo) dispatch(doFileList());
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as types from "constants/action_types";
|
||||
import * as modals from "constants/modal_types";
|
||||
import lbryio from "lbryio";
|
||||
import rewards from "rewards";
|
||||
import { selectRewardsByType } from "selectors/rewards";
|
||||
|
@ -58,6 +59,12 @@ export function doClaimReward(reward, saveError = false) {
|
|||
reward,
|
||||
},
|
||||
});
|
||||
if (reward.reward_type == rewards.TYPE_NEW_USER) {
|
||||
dispatch({
|
||||
type: types.OPEN_MODAL,
|
||||
data: { modal: modals.FIRST_REWARD },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const failure = error => {
|
||||
|
@ -99,9 +106,7 @@ export function doClaimEligiblePurchaseRewards() {
|
|||
if (unclaimedType) {
|
||||
dispatch(doClaimRewardType(unclaimedType));
|
||||
}
|
||||
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as types from "constants/action_types";
|
||||
import lbryio from "lbryio";
|
||||
import { setLocal } from "utils";
|
||||
import { doRewardList } from "actions/rewards";
|
||||
import { selectEmailToVerify } from "selectors/user";
|
||||
import { doRewardList, doClaimRewardType } from "actions/rewards";
|
||||
import { selectEmailToVerify, selectUser } from "selectors/user";
|
||||
import rewards from "rewards";
|
||||
|
||||
export function doAuthenticate() {
|
||||
return function(dispatch, getState) {
|
||||
|
@ -136,3 +137,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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import store from "store.js";
|
|||
import lbry from "./lbry.js";
|
||||
|
||||
const env = ENV;
|
||||
const config = require(`./config/${env}`);
|
||||
const config = {
|
||||
...require(`./config/${env}`),
|
||||
};
|
||||
const language = lbry.getClientSetting("language")
|
||||
? lbry.getClientSetting("language")
|
||||
: "en";
|
||||
|
|
|
@ -1,19 +1,43 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { selectCurrentModal } from "selectors/app";
|
||||
import { doCheckUpgradeAvailable, doAlertError } from "actions/app";
|
||||
import {
|
||||
doCheckUpgradeAvailable,
|
||||
doOpenModal,
|
||||
doAlertError,
|
||||
doRecordScroll,
|
||||
} from "actions/app";
|
||||
import { doUpdateBalance } from "actions/wallet";
|
||||
import { selectWelcomeModalAcknowledged } from "selectors/app";
|
||||
import rewards from "rewards";
|
||||
import {
|
||||
selectFetchingRewards,
|
||||
makeSelectHasClaimedReward,
|
||||
} from "selectors/rewards";
|
||||
import { selectUser } from "selectors/user";
|
||||
import App from "./view";
|
||||
import * as modals from "constants/modal_types";
|
||||
|
||||
const select = state => ({
|
||||
const select = (state, props) => {
|
||||
const selectHasClaimed = makeSelectHasClaimedReward();
|
||||
|
||||
return {
|
||||
modal: selectCurrentModal(state),
|
||||
});
|
||||
isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state),
|
||||
isFetchingRewards: selectFetchingRewards(state),
|
||||
isWelcomeRewardClaimed: selectHasClaimed(state, {
|
||||
reward_type: rewards.TYPE_NEW_USER,
|
||||
}),
|
||||
user: selectUser(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
alertError: errorList => dispatch(doAlertError(errorList)),
|
||||
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
|
||||
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)),
|
||||
updateBalance: balance => dispatch(doUpdateBalance(balance)),
|
||||
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(App);
|
||||
|
|
|
@ -3,24 +3,60 @@ import Router from "component/router";
|
|||
import Header from "component/header";
|
||||
import ModalError from "component/modalError";
|
||||
import ModalDownloading from "component/modalDownloading";
|
||||
import UpgradeModal from "component/modalUpgrade";
|
||||
import WelcomeModal from "component/modalWelcome";
|
||||
import ModalInsufficientCredits from "component/modalInsufficientCredits";
|
||||
import ModalUpgrade from "component/modalUpgrade";
|
||||
import ModalWelcome from "component/modalWelcome";
|
||||
import ModalFirstReward from "component/modalFirstReward";
|
||||
import lbry from "lbry";
|
||||
import { Line } from "rc-progress";
|
||||
import * as modals from "constants/modal_types";
|
||||
|
||||
class App extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
const { alertError, checkUpgradeAvailable, updateBalance } = this.props;
|
||||
|
||||
document.addEventListener("unhandledError", event => {
|
||||
this.props.alertError(event.detail);
|
||||
alertError(event.detail);
|
||||
});
|
||||
|
||||
if (!this.props.upgradeSkipped) {
|
||||
this.props.checkUpgradeAvailable();
|
||||
checkUpgradeAvailable();
|
||||
}
|
||||
|
||||
lbry.balanceSubscribe(balance => {
|
||||
this.props.updateBalance(balance);
|
||||
updateBalance(balance);
|
||||
});
|
||||
|
||||
this.showWelcome(this.props);
|
||||
|
||||
this.scrollListener = () => this.props.recordScroll(window.scrollY);
|
||||
|
||||
window.addEventListener("scroll", this.scrollListener);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.showWelcome(nextProps);
|
||||
}
|
||||
|
||||
showWelcome(props) {
|
||||
const {
|
||||
isFetchingRewards,
|
||||
isWelcomeAcknowledged,
|
||||
isWelcomeRewardClaimed,
|
||||
openWelcomeModal,
|
||||
user,
|
||||
} = props;
|
||||
|
||||
if (
|
||||
!isWelcomeAcknowledged &&
|
||||
user &&
|
||||
(isFetchingRewards === false && isWelcomeRewardClaimed === false)
|
||||
) {
|
||||
openWelcomeModal();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("scroll", this.scrollListener);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -32,10 +68,12 @@ class App extends React.PureComponent {
|
|||
<div id="main-content">
|
||||
<Router />
|
||||
</div>
|
||||
{modal == "upgrade" && <UpgradeModal />}
|
||||
{modal == "downloading" && <ModalDownloading />}
|
||||
{modal == "error" && <ModalError />}
|
||||
{modal == "welcome" && <WelcomeModal />}
|
||||
{modal == modals.UPGRADE && <ModalUpgrade />}
|
||||
{modal == modals.DOWNLOADING && <ModalDownloading />}
|
||||
{modal == modals.ERROR && <ModalError />}
|
||||
{modal == modals.INSUFFICIENT_CREDITS && <ModalInsufficientCredits />}
|
||||
{modal == modals.WELCOME && <ModalWelcome />}
|
||||
{modal == modals.FIRST_REWARD && <ModalFirstReward />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
8
ui/js/component/cardMedia/index.js
Normal file
8
ui/js/component/cardMedia/index.js
Normal 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);
|
54
ui/js/component/cardMedia/view.jsx
Normal file
54
ui/js/component/cardMedia/view.jsx
Normal 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;
|
12
ui/js/component/cardVerify/index.js
Normal file
12
ui/js/component/cardVerify/index.js
Normal 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);
|
377
ui/js/component/cardVerify/view.jsx
Normal file
377
ui/js/component/cardVerify/view.jsx
Normal file
|
@ -0,0 +1,377 @@
|
|||
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 defaultProps = {
|
||||
label: "Verify",
|
||||
locale: "auto",
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
// If included, will render the default blue button with label text.
|
||||
// (Requires including stripe-checkout.css or adding the .styl file
|
||||
// to your pipeline)
|
||||
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,
|
||||
|
||||
// ==========================
|
||||
// Highly Recommended Options
|
||||
// ==========================
|
||||
|
||||
// Name of the company or website.
|
||||
name: PropTypes.string,
|
||||
|
||||
// A description of the product or service being purchased.
|
||||
description: PropTypes.string,
|
||||
|
||||
// Specify auto to display Checkout in the user's preferred language, if
|
||||
// available. English will be used by default.
|
||||
//
|
||||
// https://stripe.com/docs/checkout#supported-languages
|
||||
// for more info.
|
||||
locale: PropTypes.oneOf([
|
||||
"auto", // (Default) Automatically chosen by checkout
|
||||
"zh", // Simplified Chinese
|
||||
"da", // Danish
|
||||
"nl", // Dutch
|
||||
"en", // English
|
||||
"fr", // French
|
||||
"de", // German
|
||||
"it", // Italian
|
||||
"ja", // Japanease
|
||||
"no", // Norwegian
|
||||
"es", // Spanish
|
||||
"sv", // Swedish
|
||||
]),
|
||||
|
||||
// ==============
|
||||
// Optional Props
|
||||
// ==============
|
||||
|
||||
// The currency of the amount (3-letter ISO code). The default is USD.
|
||||
currency: PropTypes.oneOf([
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
"AMD",
|
||||
"ANG",
|
||||
"AOA",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"AWG",
|
||||
"AZN",
|
||||
"BAM",
|
||||
"BBD",
|
||||
"BDT",
|
||||
"BGN",
|
||||
"BIF",
|
||||
"BMD",
|
||||
"BND",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"BSD",
|
||||
"BWP",
|
||||
"BZD",
|
||||
"CAD",
|
||||
"CDF",
|
||||
"CHF",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CRC",
|
||||
"CVE",
|
||||
"CZK",
|
||||
"DJF",
|
||||
"DKK",
|
||||
"DOP",
|
||||
"DZD",
|
||||
"EEK",
|
||||
"EGP",
|
||||
"ETB",
|
||||
"EUR",
|
||||
"FJD",
|
||||
"FKP",
|
||||
"GBP",
|
||||
"GEL",
|
||||
"GIP",
|
||||
"GMD",
|
||||
"GNF",
|
||||
"GTQ",
|
||||
"GYD",
|
||||
"HKD",
|
||||
"HNL",
|
||||
"HRK",
|
||||
"HTG",
|
||||
"HUF",
|
||||
"IDR",
|
||||
"ILS",
|
||||
"INR",
|
||||
"ISK",
|
||||
"JMD",
|
||||
"JPY",
|
||||
"KES",
|
||||
"KGS",
|
||||
"KHR",
|
||||
"KMF",
|
||||
"KRW",
|
||||
"KYD",
|
||||
"KZT",
|
||||
"LAK",
|
||||
"LBP",
|
||||
"LKR",
|
||||
"LRD",
|
||||
"LSL",
|
||||
"LTL",
|
||||
"LVL",
|
||||
"MAD",
|
||||
"MDL",
|
||||
"MGA",
|
||||
"MKD",
|
||||
"MNT",
|
||||
"MOP",
|
||||
"MRO",
|
||||
"MUR",
|
||||
"MVR",
|
||||
"MWK",
|
||||
"MXN",
|
||||
"MYR",
|
||||
"MZN",
|
||||
"NAD",
|
||||
"NGN",
|
||||
"NIO",
|
||||
"NOK",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"PAB",
|
||||
"PEN",
|
||||
"PGK",
|
||||
"PHP",
|
||||
"PKR",
|
||||
"PLN",
|
||||
"PYG",
|
||||
"QAR",
|
||||
"RON",
|
||||
"RSD",
|
||||
"RUB",
|
||||
"RWF",
|
||||
"SAR",
|
||||
"SBD",
|
||||
"SCR",
|
||||
"SEK",
|
||||
"SGD",
|
||||
"SHP",
|
||||
"SLL",
|
||||
"SOS",
|
||||
"SRD",
|
||||
"STD",
|
||||
"SVC",
|
||||
"SZL",
|
||||
"THB",
|
||||
"TJS",
|
||||
"TOP",
|
||||
"TRY",
|
||||
"TTD",
|
||||
"TWD",
|
||||
"TZS",
|
||||
"UAH",
|
||||
"UGX",
|
||||
"USD",
|
||||
"UYU",
|
||||
"UZS",
|
||||
"VND",
|
||||
"VUV",
|
||||
"WST",
|
||||
"XAF",
|
||||
"XCD",
|
||||
"XOF",
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMW",
|
||||
]),
|
||||
|
||||
// The label of the payment button in the Checkout form (e.g. “Subscribe”,
|
||||
// “Pay {{amount}}”, etc.). If you include {{amount}}, it will be replaced
|
||||
// by the provided amount. Otherwise, the amount will be appended to the
|
||||
// end of your label.
|
||||
panelLabel: PropTypes.string,
|
||||
};
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
getConfig = () =>
|
||||
["token", "name", "description"].reduce(
|
||||
(config, key) =>
|
||||
Object.assign(
|
||||
{},
|
||||
config,
|
||||
this.props.hasOwnProperty(key) && {
|
||||
[key]: this.props[key],
|
||||
}
|
||||
),
|
||||
{
|
||||
allowRememberMe: false,
|
||||
closed: this.onClosed,
|
||||
description: __("Confirm Identity"),
|
||||
email: this.props.email,
|
||||
panelLabel: "Verify",
|
||||
}
|
||||
);
|
||||
|
||||
updateStripeHandler() {
|
||||
if (!CardVerify.stripeHandler) {
|
||||
CardVerify.stripeHandler = StripeCheckout.configure({
|
||||
key: this.props.stripeKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showStripeDialog() {
|
||||
this.setState({ open: true });
|
||||
CardVerify.stripeHandler.open(this.getConfig());
|
||||
}
|
||||
|
||||
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}
|
||||
disabled={
|
||||
this.props.disabled || this.state.open || this.hasPendingClick
|
||||
}
|
||||
onClick={this.onClick.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CardVerify;
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { formatCredits } from "utils";
|
||||
import lbry from "../lbry.js";
|
||||
|
||||
//component/icon.js
|
||||
|
@ -78,7 +79,7 @@ export class CreditAmount extends React.PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const formattedAmount = lbry.formatCredits(
|
||||
const formattedAmount = formatCredits(
|
||||
this.props.amount,
|
||||
this.props.precision
|
||||
);
|
||||
|
@ -140,7 +141,7 @@ export class Address extends React.PureComponent {
|
|||
}}
|
||||
style={addressStyle}
|
||||
readOnly="readonly"
|
||||
value={this.props.address}
|
||||
value={this.props.address || ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -118,7 +118,10 @@ class FileActions extends React.PureComponent {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (fileInfo === null && !downloading) {
|
||||
} else if (
|
||||
(fileInfo === null || (fileInfo && fileInfo.written_bytes === 0)) &&
|
||||
!downloading
|
||||
) {
|
||||
if (!costInfo) {
|
||||
content = <BusyMessage message={__("Fetching cost info")} />;
|
||||
} else {
|
||||
|
@ -142,6 +145,8 @@ class FileActions extends React.PureComponent {
|
|||
onClick={() => openInShell(fileInfo)}
|
||||
/>
|
||||
);
|
||||
} else if (!fileInfo) {
|
||||
content = <BusyMessage message={__("Fetching file info")} />;
|
||||
} else {
|
||||
console.log("handle this case of file action props?");
|
||||
}
|
||||
|
@ -176,13 +181,6 @@ class FileActions extends React.PureComponent {
|
|||
</strong>{" "}
|
||||
{__("credits")}.
|
||||
</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
|
||||
isOpen={modal == "timedOut"}
|
||||
contentLabel={__("Download failed")}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from "react";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import CardMedia from "component/cardMedia";
|
||||
import Link from "component/link";
|
||||
import { TruncatedText, Icon } from "component/common";
|
||||
import { Thumbnail, TruncatedText, Icon } from "component/common";
|
||||
import FilePrice from "component/filePrice";
|
||||
import UriIndicator from "component/uriIndicator";
|
||||
import NsfwOverlay from "component/nsfwOverlay";
|
||||
import TruncatedMarkdown from "component/truncatedMarkdown";
|
||||
|
||||
class FileCard extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -48,6 +50,9 @@ class FileCard extends React.PureComponent {
|
|||
|
||||
const uri = lbryuri.normalize(this.props.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;
|
||||
|
||||
let description = "";
|
||||
|
@ -73,10 +78,11 @@ class FileCard extends React.PureComponent {
|
|||
onClick={() => navigate("/show", { uri })}
|
||||
className="card__link"
|
||||
>
|
||||
<CardMedia title={title} thumbnail={thumbnail} />
|
||||
<div className="card__title-identity">
|
||||
<h5 title={title}>
|
||||
<div className="card__title" title={title}>
|
||||
<TruncatedText lines={1}>{title}</TruncatedText>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="card__subtitle">
|
||||
<span style={{ float: "right" }}>
|
||||
<FilePrice uri={uri} />
|
||||
|
@ -87,14 +93,8 @@ class FileCard extends React.PureComponent {
|
|||
<UriIndicator uri={uri} />
|
||||
</div>
|
||||
</div>
|
||||
{metadata &&
|
||||
metadata.thumbnail &&
|
||||
<div
|
||||
className="card__media"
|
||||
style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}
|
||||
/>}
|
||||
<div className="card__content card__subtext card__subtext--two-lines">
|
||||
<TruncatedText lines={2}>{description}</TruncatedText>
|
||||
<TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,9 @@ class FileList extends React.PureComponent {
|
|||
const content = [];
|
||||
|
||||
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
let uriParams = {};
|
||||
let uriParams = {
|
||||
claimId: fileInfo.claim_id,
|
||||
};
|
||||
if (fileInfo.channel_name) {
|
||||
uriParams.channelName = fileInfo.channel_name;
|
||||
uriParams.contentName = fileInfo.name;
|
||||
|
@ -79,7 +81,7 @@ class FileList extends React.PureComponent {
|
|||
|
||||
content.push(
|
||||
<FileTile
|
||||
key={uri}
|
||||
key={fileInfo.outpoint || fileInfo.claim_id}
|
||||
uri={uri}
|
||||
hidePrice={true}
|
||||
showEmpty={this.props.fileTileShowEmpty}
|
||||
|
@ -94,7 +96,6 @@ class FileList extends React.PureComponent {
|
|||
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
|
||||
<option value="date">{__("Date")}</option>
|
||||
<option value="title">{__("Title")}</option>
|
||||
<option value="filename">{__("File name")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{content}
|
||||
|
|
|
@ -67,7 +67,7 @@ class FileListSearch extends React.PureComponent {
|
|||
|
||||
{results && !!results.length
|
||||
? <FileListSearchResults {...this.props} />
|
||||
: <SearchNoResults {...this.props} />}
|
||||
: !isSearching && <SearchNoResults {...this.props} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry.js";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import CardMedia from "component/cardMedia";
|
||||
import Link from "component/link";
|
||||
import { TruncatedText } from "component/common.js";
|
||||
import FilePrice from "component/filePrice";
|
||||
|
@ -64,7 +65,10 @@ class FileTile extends React.PureComponent {
|
|||
const isClaimable = lbryuri.isClaimable(uri);
|
||||
const title = isClaimed && metadata && 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;
|
||||
let onClick = () => navigate("/show", { uri });
|
||||
|
||||
|
@ -98,17 +102,7 @@ class FileTile extends React.PureComponent {
|
|||
>
|
||||
<Link onClick={onClick} className="card__link">
|
||||
<div className={"card__inner file-tile__row"}>
|
||||
<div
|
||||
className="card__media"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('" +
|
||||
(metadata && metadata.thumbnail
|
||||
? metadata.thumbnail
|
||||
: lbry.imagePath("default-thumb.svg")) +
|
||||
"')",
|
||||
}}
|
||||
/>
|
||||
<CardMedia title={title} thumbnail={thumbnail} />
|
||||
<div className="file-tile__content">
|
||||
<div className="card__title-primary">
|
||||
{!hidePrice ? <FilePrice uri={this.props.uri} /> : null}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import FileSelector from "./file-selector.js";
|
||||
import { Icon } from "./common.js";
|
||||
import SimpleMDE from "react-simplemde-editor";
|
||||
import style from "react-simplemde-editor/dist/simplemde.min.css";
|
||||
|
||||
var formFieldCounter = 0,
|
||||
let formFieldCounter = 0,
|
||||
formFieldFileSelectorTypes = ["file", "directory"],
|
||||
formFieldNestedLabelTypes = ["radio", "checkbox"];
|
||||
|
||||
|
@ -24,6 +25,7 @@ export class FormField extends React.PureComponent {
|
|||
this._fieldRequiredText = __("This field is required");
|
||||
this._type = null;
|
||||
this._element = null;
|
||||
this._extraElementProps = {};
|
||||
|
||||
this.state = {
|
||||
isError: null,
|
||||
|
@ -38,6 +40,12 @@ export class FormField extends React.PureComponent {
|
|||
} 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";
|
||||
|
@ -81,6 +89,8 @@ export class FormField extends React.PureComponent {
|
|||
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;
|
||||
}
|
||||
|
@ -90,6 +100,10 @@ export class FormField extends React.PureComponent {
|
|||
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),
|
||||
|
@ -106,7 +120,6 @@ export class FormField extends React.PureComponent {
|
|||
delete otherProps.className;
|
||||
delete otherProps.postfix;
|
||||
delete otherProps.prefix;
|
||||
|
||||
const element = (
|
||||
<this._element
|
||||
id={elementId}
|
||||
|
@ -122,6 +135,7 @@ export class FormField extends React.PureComponent {
|
|||
(isError ? "form-field__input--error" : "")
|
||||
}
|
||||
{...otherProps}
|
||||
{...this._extraElementProps}
|
||||
>
|
||||
{this.props.children}
|
||||
</this._element>
|
||||
|
@ -170,6 +184,10 @@ export class FormRow extends React.PureComponent {
|
|||
React.PropTypes.string,
|
||||
React.PropTypes.element,
|
||||
]),
|
||||
errorMessage: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.object,
|
||||
]),
|
||||
// helper: React.PropTypes.html,
|
||||
};
|
||||
|
||||
|
@ -190,6 +208,8 @@ export class FormRow extends React.PureComponent {
|
|||
isError: !!props.errorMessage,
|
||||
errorMessage: typeof props.errorMessage === "string"
|
||||
? props.errorMessage
|
||||
: props.errorMessage instanceof Error
|
||||
? props.errorMessage.toString()
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
@ -220,6 +240,10 @@ export class FormRow extends React.PureComponent {
|
|||
return this.refs.field.getSelectedElement();
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.refs.field.getOptions();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.field.focus();
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry";
|
||||
import { formatCredits } from "utils";
|
||||
import { connect } from "react-redux";
|
||||
import { selectBalance } from "selectors/wallet";
|
||||
import { doNavigate, doHistoryBack } from "actions/app";
|
||||
import Header from "./view";
|
||||
|
||||
const select = state => ({
|
||||
balance: lbry.formatCredits(selectBalance(state), 1),
|
||||
balance: formatCredits(selectBalance(state), 1),
|
||||
publish: __("Publish"),
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ const Link = props => {
|
|||
icon,
|
||||
badge,
|
||||
button,
|
||||
hidden,
|
||||
disabled,
|
||||
children,
|
||||
} = props;
|
||||
|
|
|
@ -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;
|
20
ui/js/component/modalFirstReward/index.js
Normal file
20
ui/js/component/modalFirstReward/index.js
Normal 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);
|
50
ui/js/component/modalFirstReward/view.jsx
Normal file
50
ui/js/component/modalFirstReward/view.jsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
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">{__("About Your Reward")}</h3>
|
||||
<p>
|
||||
{__("You earned a reward of")}
|
||||
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
|
||||
{" "}{__("LBRY credits, or")} <em>{__("LBC")}</em>.
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"This reward will show in your Wallet momentarily, shown in the top right, probably while you are reading this message."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"LBC is used to compensate creators, to publish, 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, pleaseh know that LBRY is an early beta and that it earns the name."
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalFirstReward;
|
13
ui/js/component/modalIncompatibleDaemon/index.jsx
Normal file
13
ui/js/component/modalIncompatibleDaemon/index.jsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doQuit, doSkipWrongDaemonNotice } from "actions/app";
|
||||
import { doQuitAndLaunchDaemonHelp } from "actions/app";
|
||||
import ModalIncompatibleDaemon from "./view";
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
const perform = dispatch => ({
|
||||
quitAndLaunchDaemonHelp: () => dispatch(doQuitAndLaunchDaemonHelp()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalIncompatibleDaemon);
|
24
ui/js/component/modalIncompatibleDaemon/view.jsx
Normal file
24
ui/js/component/modalIncompatibleDaemon/view.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
|
||||
class ModalIncompatibleDaemon extends React.PureComponent {
|
||||
render() {
|
||||
const { quitAndLaunchDaemonHelp } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
contentLabel={__("Incompatible daemon running")}
|
||||
type="alert"
|
||||
confirmButtonLabel={__("Quit and Learn More")}
|
||||
onConfirmed={quitAndLaunchDaemonHelp}
|
||||
>
|
||||
{__(
|
||||
"This browser is running with an incompatible version of the LBRY protocol and your install must be repaired."
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalIncompatibleDaemon;
|
16
ui/js/component/modalInsufficientCredits/index.js
Normal file
16
ui/js/component/modalInsufficientCredits/index.js
Normal 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);
|
24
ui/js/component/modalInsufficientCredits/view.jsx
Normal file
24
ui/js/component/modalInsufficientCredits/view.jsx
Normal 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;
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal, doHistoryBack } from "actions/app";
|
||||
import { doDeleteFile } from "actions/file_info";
|
||||
import { doDeleteFileAndGoBack } from "actions/file_info";
|
||||
import { makeSelectClaimForUriIsMine } from "selectors/claims";
|
||||
import batchActions from "util/batchActions";
|
||||
|
||||
import ModalRemoveFile from "./view";
|
||||
|
||||
|
@ -19,8 +20,7 @@ const makeSelect = () => {
|
|||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
deleteFile: (fileInfo, deleteFromComputer, abandonClaim) => {
|
||||
dispatch(doHistoryBack());
|
||||
dispatch(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim));
|
||||
dispatch(doDeleteFileAndGoBack(fileInfo, deleteFromComputer, abandonClaim));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
import { downloadUpgrade, skipUpgrade } from "actions/app";
|
||||
|
||||
class ModalUpgrade extends React.PureComponent {
|
||||
render() {
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
import React from "react";
|
||||
import rewards from "rewards";
|
||||
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 {
|
||||
makeSelectHasClaimedReward,
|
||||
makeSelectClaimRewardError,
|
||||
makeSelectRewardByType,
|
||||
} from "selectors/rewards";
|
||||
import WelcomeModal from "./view";
|
||||
import ModalWelcome from "./view";
|
||||
|
||||
const select = (state, props) => {
|
||||
const selectHasClaimed = makeSelectHasClaimedReward(),
|
||||
selectReward = makeSelectRewardByType();
|
||||
|
||||
return {
|
||||
hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }),
|
||||
isRewardApproved: selectUserIsRewardApproved(state),
|
||||
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
});
|
||||
const perform = dispatch => () => {
|
||||
const closeModal = () => {
|
||||
dispatch(doSetClientSetting("welcome_acknowledged", true));
|
||||
dispatch(doCloseModal());
|
||||
};
|
||||
|
||||
export default connect(select, perform)(WelcomeModal);
|
||||
return {
|
||||
verifyAccount: () => {
|
||||
closeModal();
|
||||
dispatch(doAuthNavigate("/rewards"));
|
||||
},
|
||||
closeModal: closeModal,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ModalWelcome);
|
||||
|
|
|
@ -4,12 +4,12 @@ import { CreditAmount } from "component/common";
|
|||
import Link from "component/link";
|
||||
import RewardLink from "component/rewardLink";
|
||||
|
||||
class WelcomeModal extends React.PureComponent {
|
||||
class ModalWelcome extends React.PureComponent {
|
||||
render() {
|
||||
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props;
|
||||
const { closeModal, isRewardApproved, reward, verifyAccount } = this.props;
|
||||
|
||||
return !hasClaimed
|
||||
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
|
||||
return (
|
||||
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
|
||||
<section>
|
||||
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
|
||||
<p>
|
||||
|
@ -25,57 +25,27 @@ class WelcomeModal extends React.PureComponent {
|
|||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__("Thank you for making content freedom possible!")}
|
||||
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}
|
||||
{__("Please have")} {" "}
|
||||
{reward &&
|
||||
<CreditAmount amount={parseFloat(reward.reward_amount)} />}
|
||||
{!reward && <span className="credit-amount">{__("??")}</span>}
|
||||
{" "} {__("as a thank you for building content freedom.")}
|
||||
</p>
|
||||
<div className="text-center">
|
||||
{isRewardApproved
|
||||
? <RewardLink reward_type="new_user" button="primary" />
|
||||
: <Link
|
||||
{isRewardApproved &&
|
||||
<RewardLink reward_type="new_user" button="primary" />}
|
||||
{!isRewardApproved &&
|
||||
<Link
|
||||
button="primary"
|
||||
onClick={closeModal}
|
||||
label={__("Continue")}
|
||||
onClick={verifyAccount}
|
||||
label={__("Get Welcome Credits")}
|
||||
/>}
|
||||
<Link button="alt" onClick={closeModal} label={__("Skip")} />
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
: <Modal
|
||||
type="alert"
|
||||
overlayClassName="modal-overlay modal-overlay--clear"
|
||||
isOpen={true}
|
||||
contentLabel={__("Welcome to LBRY")}
|
||||
onConfirmed={closeModal}
|
||||
>
|
||||
<section>
|
||||
<h3 className="modal__header">{__("About Your Reward")}</h3>
|
||||
<p>
|
||||
{__("You earned a reward of")}
|
||||
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
|
||||
{" "}{__("LBRY credits, or")} <em>{__("LBC")}</em>.
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"This reward will show in your Wallet momentarily, probably while you are reading this message."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"LBC is used to compensate creators, to publish, 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, know that LBRY is an early beta and that it earns the name."
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
</Modal>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WelcomeModal;
|
||||
export default ModalWelcome;
|
||||
|
|
|
@ -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;
|
5
ui/js/component/publishForm/index.js
Normal file
5
ui/js/component/publishForm/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PublishForm from "./view";
|
||||
|
||||
export default connect()(PublishForm);
|
174
ui/js/component/publishForm/internal/channelSection.jsx
Normal file
174
ui/js/component/publishForm/internal/channelSection.jsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
import React from "react";
|
||||
import lbryuri from "lbryuri";
|
||||
import { FormField, 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 the channel that broadcasts your content.")}
|
||||
{__("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;
|
938
ui/js/component/publishForm/view.jsx
Normal file
938
ui/js/component/publishForm/view.jsx
Normal file
|
@ -0,0 +1,938 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry";
|
||||
import lbryuri from "lbryuri";
|
||||
import { FormField, FormRow } from "component/form.js";
|
||||
import Link from "component/link";
|
||||
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: "USD",
|
||||
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) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeAmountChange(event) {
|
||||
this.setState({
|
||||
feeAmount: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeCurrencyChange(event) {
|
||||
this.setState({
|
||||
feeCurrency: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeePrefChange(feeEnabled) {
|
||||
this.setState({
|
||||
isFee: feeEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
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" : ""}>
|
||||
<FormField
|
||||
type="number"
|
||||
className="form-field__input--inline"
|
||||
step="0.01"
|
||||
placeholder="1.00"
|
||||
min="0.01"
|
||||
onChange={event => this.handleFeeAmountChange(event)}
|
||||
/>{" "}
|
||||
<FormField
|
||||
type="select"
|
||||
onChange={event => {
|
||||
this.handleFeeCurrencyChange(event);
|
||||
}}
|
||||
>
|
||||
<option value="USD">{__("US Dollars")}</option>
|
||||
<option value="LBC">{__("LBRY credits")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{this.state.isFee
|
||||
? <div className="form-field__helper">
|
||||
{__(
|
||||
"If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase."
|
||||
)}
|
||||
</div>
|
||||
: ""}
|
||||
<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;
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
makeSelectHasClaimedReward,
|
||||
makeSelectClaimRewardError,
|
||||
makeSelectRewardByType,
|
||||
makeSelectIsRewardClaimPending,
|
||||
|
@ -11,13 +10,11 @@ import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
|
|||
import RewardLink from "./view";
|
||||
|
||||
const makeSelect = () => {
|
||||
const selectHasClaimedReward = makeSelectHasClaimedReward();
|
||||
const selectIsPending = makeSelectIsRewardClaimPending();
|
||||
const selectReward = makeSelectRewardByType();
|
||||
const selectError = makeSelectClaimRewardError();
|
||||
|
||||
const select = (state, props) => ({
|
||||
isClaimed: selectHasClaimedReward(state, props),
|
||||
errorMessage: selectError(state, props),
|
||||
isPending: selectIsPending(state, props),
|
||||
reward: selectReward(state, props),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import { Icon } from "component/common";
|
||||
import Modal from "component/modal";
|
||||
import Link from "component/link";
|
||||
|
||||
|
@ -10,22 +9,19 @@ const RewardLink = props => {
|
|||
claimReward,
|
||||
clearError,
|
||||
errorMessage,
|
||||
isClaimed,
|
||||
isPending,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="reward-link">
|
||||
{isClaimed
|
||||
? <span><Icon icon="icon-check" /> Reward claimed.</span>
|
||||
: <Link
|
||||
<Link
|
||||
button={button ? button : "alt"}
|
||||
disabled={isPending}
|
||||
label={isPending ? __("Claiming...") : __("Claim Reward")}
|
||||
onClick={() => {
|
||||
claimReward(reward);
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
{errorMessage
|
||||
? <Modal
|
||||
isOpen={true}
|
||||
|
|
|
@ -13,6 +13,7 @@ import FileListDownloaded from "page/fileListDownloaded";
|
|||
import FileListPublished from "page/fileListPublished";
|
||||
import ChannelPage from "page/channel";
|
||||
import SearchPage from "page/search";
|
||||
import AuthPage from "page/auth";
|
||||
|
||||
const route = (page, routesMap) => {
|
||||
const component = routesMap[page];
|
||||
|
@ -24,22 +25,23 @@ const Router = props => {
|
|||
const { currentPage, params } = props;
|
||||
|
||||
return route(currentPage, {
|
||||
settings: <SettingsPage {...params} />,
|
||||
help: <HelpPage {...params} />,
|
||||
report: <ReportPage {...params} />,
|
||||
downloaded: <FileListDownloaded {...params} />,
|
||||
published: <FileListPublished {...params} />,
|
||||
start: <StartPage {...params} />,
|
||||
wallet: <WalletPage {...params} />,
|
||||
send: <WalletPage {...params} />,
|
||||
receive: <WalletPage {...params} />,
|
||||
show: <ShowPage {...params} />,
|
||||
auth: <AuthPage {...params} />,
|
||||
channel: <ChannelPage {...params} />,
|
||||
publish: <PublishPage {...params} />,
|
||||
developer: <DeveloperPage {...params} />,
|
||||
discover: <DiscoverPage {...params} />,
|
||||
downloaded: <FileListDownloaded {...params} />,
|
||||
help: <HelpPage {...params} />,
|
||||
publish: <PublishPage {...params} />,
|
||||
published: <FileListPublished {...params} />,
|
||||
receive: <WalletPage {...params} />,
|
||||
report: <ReportPage {...params} />,
|
||||
rewards: <RewardsPage {...params} />,
|
||||
search: <SearchPage {...params} />,
|
||||
send: <WalletPage {...params} />,
|
||||
settings: <SettingsPage {...params} />,
|
||||
show: <ShowPage {...params} />,
|
||||
start: <StartPage {...params} />,
|
||||
wallet: <WalletPage {...params} />,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
19
ui/js/component/splash/index.js
Normal file
19
ui/js/component/splash/index.js
Normal 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);
|
|
@ -1,6 +1,9 @@
|
|||
import React from "react";
|
||||
import lbry from "../lbry.js";
|
||||
import LoadScreen from "./load_screen.js";
|
||||
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 = {
|
||||
|
@ -14,6 +17,7 @@ export class SplashScreen extends React.PureComponent {
|
|||
this.state = {
|
||||
details: __("Starting daemon"),
|
||||
message: __("Connecting"),
|
||||
isRunning: false,
|
||||
isLagging: false,
|
||||
};
|
||||
}
|
||||
|
@ -35,10 +39,16 @@ export class SplashScreen extends React.PureComponent {
|
|||
message: __("Testing Network"),
|
||||
details: __("Waiting for name resolution"),
|
||||
isLagging: false,
|
||||
isRunning: true,
|
||||
});
|
||||
|
||||
lbry.resolve({ uri: "lbry://one" }).then(() => {
|
||||
this.props.onLoadDone();
|
||||
// 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;
|
||||
}
|
||||
|
@ -54,6 +64,7 @@ export class SplashScreen extends React.PureComponent {
|
|||
componentDidMount() {
|
||||
lbry
|
||||
.connect()
|
||||
.then(this.props.checkDaemonVersion)
|
||||
.then(() => {
|
||||
this.updateStatus();
|
||||
})
|
||||
|
@ -69,12 +80,24 @@ export class SplashScreen extends React.PureComponent {
|
|||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
5
ui/js/component/truncatedMarkdown/index.js
Normal file
5
ui/js/component/truncatedMarkdown/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import TruncatedMarkdown from "./view";
|
||||
|
||||
export default connect()(TruncatedMarkdown);
|
39
ui/js/component/truncatedMarkdown/view.jsx
Normal file
39
ui/js/component/truncatedMarkdown/view.jsx
Normal 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;
|
|
@ -27,7 +27,6 @@ class UserEmailNew extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<form
|
||||
className="form-input-width"
|
||||
onSubmit={event => {
|
||||
this.handleSubmit(event);
|
||||
}}
|
||||
|
|
|
@ -13,7 +13,7 @@ class UserEmailVerify extends React.PureComponent {
|
|||
|
||||
handleCodeChanged(event) {
|
||||
this.setState({
|
||||
code: event.target.value,
|
||||
code: String(event.target.value).trim(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -24,18 +24,16 @@ class UserEmailVerify extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { errorMessage, isPending } = this.props;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="form-input-width"
|
||||
onSubmit={event => {
|
||||
this.handleSubmit(event);
|
||||
}}
|
||||
>
|
||||
<p>{__("Please enter the verification code emailed to you.")}</p>
|
||||
<FormRow
|
||||
type="text"
|
||||
label={__("Verification Code")}
|
||||
placeholder="a94bXXXXXXXXXXXXXX"
|
||||
name="code"
|
||||
value={this.state.code}
|
||||
onChange={event => {
|
||||
|
@ -46,7 +44,7 @@ class UserEmailVerify extends React.PureComponent {
|
|||
{/* render help separately so it always shows */}
|
||||
<div className="form-field__helper">
|
||||
<p>
|
||||
{__("Email")}{" "}
|
||||
{__("Check your email for a verification code. Email")}{" "}
|
||||
<Link href="mailto:help@lbry.io" label="help@lbry.io" />{" "}
|
||||
{__("if you did not receive or are having trouble with your code.")}
|
||||
</p>
|
||||
|
|
26
ui/js/component/userVerify/index.js
Normal file
26
ui/js/component/userVerify/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
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 => ({
|
||||
verifyUserIdentity: token => dispatch(doUserIdentityVerify(token)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(UserVerify);
|
48
ui/js/component/userVerify/view.jsx
Normal file
48
ui/js/component/userVerify/view.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import React from "react";
|
||||
import { CreditAmount } from "component/common";
|
||||
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, reward } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{(!reward || !reward.transaction_id) &&
|
||||
<p>
|
||||
Please link a credit card to confirm your identity and receive{" "}
|
||||
{reward
|
||||
? <CreditAmount amount={parseFloat(reward.reward_amount)} />
|
||||
: <span>your reward</span>}
|
||||
</p>}
|
||||
<p>{__("This is to prevent abuse. You will not be charged.")}</p>
|
||||
{errorMessage && <p className="form-field__error">{errorMessage}</p>}
|
||||
<CardVerify
|
||||
label={__("Link Card and Finish")}
|
||||
disabled={isPending}
|
||||
token={this.onToken.bind(this)}
|
||||
stripeKey="pk_live_e8M4dRNnCCbmpZzduEUZBgJO"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserVerify;
|
|
@ -4,12 +4,31 @@ import Link from "component/link";
|
|||
import Modal from "component/modal";
|
||||
|
||||
class VideoPlayButton extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.keyDownListener = this.onKeyDown.bind(this);
|
||||
document.addEventListener("keydown", this.keyDownListener);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("keydown", this.keyDownListener);
|
||||
}
|
||||
|
||||
onPurchaseConfirmed() {
|
||||
this.props.closeModal();
|
||||
this.props.startPlaying();
|
||||
this.props.loadVideo(this.props.uri);
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (
|
||||
"input" !== event.target.tagName.toLowerCase() &&
|
||||
"Space" === event.code
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.onWatchClick();
|
||||
}
|
||||
}
|
||||
|
||||
onWatchClick() {
|
||||
this.props.purchaseUri(this.props.uri).then(() => {
|
||||
if (!this.props.modal) {
|
||||
|
@ -59,13 +78,6 @@ class VideoPlayButton extends React.PureComponent {
|
|||
icon={icon}
|
||||
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
|
||||
type="confirm"
|
||||
isOpen={modal == "affirmPurchaseAndPlay"}
|
||||
|
@ -73,9 +85,11 @@ class VideoPlayButton extends React.PureComponent {
|
|||
onConfirmed={this.onPurchaseConfirmed.bind(this)}
|
||||
onAborted={closeModal}
|
||||
>
|
||||
{__("This will purchase")} <strong>{title}</strong> {__("for")}
|
||||
{" "}<strong><FilePrice uri={uri} look="plain" /></strong>
|
||||
{" "}{__("credits")}.
|
||||
{__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
|
||||
<strong>
|
||||
<FilePrice uri={uri} look="plain" />
|
||||
</strong>{" "}
|
||||
{__("credits")}.
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={modal == "timedOut"}
|
||||
|
|
|
@ -7,6 +7,8 @@ import { setSession, getSession } from "utils";
|
|||
import LoadingScreen from "./loading-screen";
|
||||
|
||||
class VideoPlayer extends React.PureComponent {
|
||||
static MP3_CONTENT_TYPES = ["audio/mpeg3", "audio/mpeg"];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -15,11 +17,13 @@ class VideoPlayer extends React.PureComponent {
|
|||
startedPlaying: false,
|
||||
unplayable: false,
|
||||
};
|
||||
|
||||
this.togglePlayListener = this.togglePlay.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const container = this.refs.media;
|
||||
const { mediaType } = this.props;
|
||||
const { contentType, downloadPath, mediaType } = this.props;
|
||||
const loadedMetadata = e => {
|
||||
this.setState({ hasMetadata: true, startedPlaying: true });
|
||||
this.refs.media.children[0].play();
|
||||
|
@ -37,15 +41,22 @@ class VideoPlayer extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
// use renderAudio override for mp3
|
||||
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
|
||||
this.renderAudio(container, null, false);
|
||||
} else {
|
||||
player.append(
|
||||
this.file(),
|
||||
container,
|
||||
{ autoplay: false, controls: true },
|
||||
renderMediaCallback.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", this.togglePlayListener);
|
||||
const mediaElement = this.refs.media.children[0];
|
||||
if (mediaElement) {
|
||||
mediaElement.addEventListener("click", this.togglePlayListener);
|
||||
mediaElement.addEventListener(
|
||||
"loadedmetadata",
|
||||
loadedMetadata.bind(this),
|
||||
|
@ -53,7 +64,6 @@ class VideoPlayer extends React.PureComponent {
|
|||
once: true,
|
||||
}
|
||||
);
|
||||
|
||||
mediaElement.addEventListener(
|
||||
"webkitfullscreenchange",
|
||||
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() {
|
||||
const volumePreference = parseFloat(getSession("prefs_volume"));
|
||||
return isNaN(volumePreference) ? 1 : volumePreference;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { mediaType, downloadCompleted } = this.props;
|
||||
const { contentType, downloadCompleted } = this.props;
|
||||
const { startedPlaying } = this.state;
|
||||
|
||||
if (this.playableType() && !startedPlaying && downloadCompleted) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,21 @@ class Video extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// reset playing state upon change path action
|
||||
if (!this.isMediaSame(nextProps) && this.state.isPlaying) {
|
||||
this.state.isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
isMediaSame(nextProps) {
|
||||
return (
|
||||
this.props.fileInfo &&
|
||||
nextProps.fileInfo &&
|
||||
this.props.fileInfo.outpoint === nextProps.fileInfo.outpoint
|
||||
);
|
||||
}
|
||||
|
||||
startPlaying() {
|
||||
this.setState({
|
||||
isPlaying: true,
|
||||
|
@ -98,6 +113,7 @@ class Video extends React.PureComponent {
|
|||
poster={poster}
|
||||
downloadPath={fileInfo.download_path}
|
||||
mediaType={mediaType}
|
||||
contentType={contentType}
|
||||
downloadCompleted={fileInfo.completed}
|
||||
/>)}
|
||||
{!isPlaying &&
|
||||
|
|
|
@ -5,8 +5,10 @@ export const HISTORY_BACK = "HISTORY_BACK";
|
|||
export const SHOW_SNACKBAR = "SHOW_SNACKBAR";
|
||||
export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK";
|
||||
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_VERSION_MATCH = "DAEMON_VERSION_MATCH";
|
||||
export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH";
|
||||
|
||||
// Upgrades
|
||||
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 =
|
||||
"FETCH_CLAIM_LIST_MINE_COMPLETED";
|
||||
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_COMPLETED = "FETCH_FILE_INFO_COMPLETED";
|
||||
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 FILE_DELETE = "FILE_DELETE";
|
||||
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
|
||||
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_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS";
|
||||
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_SUCCESS = "USER_FETCH_SUCCESS";
|
||||
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";
|
||||
export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS";
|
||||
|
||||
// Rewards
|
||||
export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED";
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
export const WELCOME = "welcome";
|
||||
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";
|
||||
|
|
|
@ -223,28 +223,18 @@ lbry.publishDeprecated = function(
|
|||
) {
|
||||
lbry.publish(params).then(
|
||||
result => {
|
||||
if (returnedPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(returnPendingTimeout);
|
||||
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
|
||||
publishedCallback(result);
|
||||
},
|
||||
err => {
|
||||
if (returnedPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(returnPendingTimeout);
|
||||
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
|
||||
errorCallback(err);
|
||||
}
|
||||
);
|
||||
|
||||
let returnedPending = false;
|
||||
// Give a short grace period in case publish() returns right away or (more likely) gives an error
|
||||
const returnPendingTimeout = setTimeout(() => {
|
||||
returnedPending = true;
|
||||
|
||||
const returnPendingTimeout = setTimeout(
|
||||
() => {
|
||||
if (publishedCallback) {
|
||||
savePendingPublish({
|
||||
name: params.name,
|
||||
|
@ -261,15 +251,17 @@ lbry.publishDeprecated = function(
|
|||
});
|
||||
fileListedCallback(true);
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
2000,
|
||||
{ once: true }
|
||||
);
|
||||
};
|
||||
|
||||
lbry.getClientSettings = function() {
|
||||
var outSettings = {};
|
||||
for (let setting of Object.keys(lbry.defaultClientSettings)) {
|
||||
var localStorageVal = localStorage.getItem("setting_" + setting);
|
||||
outSettings[setting] =
|
||||
localStorageVal === null
|
||||
outSettings[setting] = localStorageVal === null
|
||||
? lbry.defaultClientSettings[setting]
|
||||
: JSON.parse(localStorageVal);
|
||||
}
|
||||
|
@ -296,15 +288,10 @@ lbry.setClientSetting = function(setting, 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) {
|
||||
// Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes)
|
||||
name = name.replace("/s+/g", "-");
|
||||
name = name.toLowerCase().replace(/[^a-z0-9\-]/g, "");
|
||||
name = name.toLowerCase().replace(lbryuri.REGEXP_INVALID_URI, "");
|
||||
return name;
|
||||
};
|
||||
|
||||
|
|
|
@ -136,8 +136,9 @@ lbryio.authenticate = function() {
|
|||
resolve({
|
||||
id: 1,
|
||||
language: "en",
|
||||
has_email: true,
|
||||
primary_email: "disabled@lbry.io",
|
||||
has_verified_email: true,
|
||||
is_identity_verified: true,
|
||||
is_reward_approved: false,
|
||||
is_reward_eligible: false,
|
||||
});
|
||||
|
|
|
@ -3,6 +3,8 @@ const CLAIM_ID_MAX_LEN = 40;
|
|||
|
||||
const lbryuri = {};
|
||||
|
||||
lbryuri.REGEXP_INVALID_URI = /[^A-Za-z0-9-]/g;
|
||||
|
||||
/**
|
||||
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
||||
* messages for invalid names.
|
||||
|
@ -70,7 +72,7 @@ lbryuri.parse = function(uri, requireProto = false) {
|
|||
contentName = path;
|
||||
}
|
||||
|
||||
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
|
||||
const nameBadChars = (channelName || name).match(lbryuri.REGEXP_INVALID_URI);
|
||||
if (nameBadChars) {
|
||||
throw new Error(
|
||||
__(
|
||||
|
@ -119,7 +121,7 @@ lbryuri.parse = function(uri, requireProto = false) {
|
|||
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) {
|
||||
throw new Error(
|
||||
__(`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
|
||||
* consists of adding the lbry:// prefix if needed) */
|
||||
lbryuri.normalize = function(uri) {
|
||||
if (uri.match(/pending_claim/)) return uri;
|
||||
|
||||
const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
|
||||
uri
|
||||
);
|
||||
|
|
|
@ -5,11 +5,9 @@ import App from "component/app/index.js";
|
|||
import SnackBar from "component/snackBar";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "store.js";
|
||||
import SplashScreen from "component/splash.js";
|
||||
import AuthOverlay from "component/authOverlay";
|
||||
import SplashScreen from "component/splash";
|
||||
import { doChangePath, doNavigate, doDaemonReady } from "actions/app";
|
||||
import { toQueryString } from "util/query_params";
|
||||
import { selectBadgeNumber } from "selectors/app";
|
||||
import * as types from "constants/action_types";
|
||||
|
||||
const env = ENV;
|
||||
|
@ -37,10 +35,10 @@ window.addEventListener("popstate", (event, param) => {
|
|||
|
||||
if (hash !== "") {
|
||||
const url = hash.split("#")[1];
|
||||
const params = event.state;
|
||||
const { params, scrollY } = event.state || {};
|
||||
const queryString = toQueryString(params);
|
||||
|
||||
app.store.dispatch(doChangePath(`${url}?${queryString}`));
|
||||
app.store.dispatch(doChangePath(`${url}?${queryString}`, { scrollY }));
|
||||
} else {
|
||||
app.store.dispatch(doChangePath("/discover"));
|
||||
}
|
||||
|
@ -97,19 +95,6 @@ const updateProgress = () => {
|
|||
|
||||
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() {
|
||||
function onDaemonReady() {
|
||||
window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again
|
||||
|
@ -117,7 +102,7 @@ var init = function() {
|
|||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<div><AuthOverlay /><App /><SnackBar /></div>
|
||||
<div><App /><SnackBar /></div>
|
||||
</Provider>,
|
||||
canvas
|
||||
);
|
||||
|
@ -126,7 +111,12 @@ var init = function() {
|
|||
if (window.sessionStorage.getItem("loaded") == "y") {
|
||||
onDaemonReady();
|
||||
} else {
|
||||
ReactDOM.render(<SplashScreen onLoadDone={onDaemonReady} />, canvas);
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<SplashScreen onReadyToLaunch={onDaemonReady} />
|
||||
</Provider>,
|
||||
canvas
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
30
ui/js/page/auth/index.js
Normal file
30
ui/js/page/auth/index.js
Normal 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);
|
89
ui/js/page/auth/view.jsx
Normal file
89
ui/js/page/auth/view.jsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import React from "react";
|
||||
import { BusyMessage } from "component/common";
|
||||
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_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) {
|
||||
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 } = 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.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 optional and only collected to provide communication and prevent abuse. You may use LBRY without providing this information."
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthPage;
|
|
@ -45,16 +45,20 @@ class DiscoverPage extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { featuredUris, fetchingFeaturedUris } = this.props;
|
||||
const failedToLoad =
|
||||
!fetchingFeaturedUris &&
|
||||
(featuredUris === undefined ||
|
||||
(featuredUris !== undefined && Object.keys(featuredUris).length === 0));
|
||||
const hasContent =
|
||||
typeof featuredUris === "object" && Object.keys(featuredUris).length,
|
||||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<main>
|
||||
{fetchingFeaturedUris &&
|
||||
<main
|
||||
className={
|
||||
hasContent && fetchingFeaturedUris ? "main--refreshing" : null
|
||||
}
|
||||
>
|
||||
{!hasContent &&
|
||||
fetchingFeaturedUris &&
|
||||
<BusyMessage message={__("Fetching content")} />}
|
||||
{typeof featuredUris === "object" &&
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(
|
||||
category =>
|
||||
featuredUris[category].length
|
||||
|
|
|
@ -3,15 +3,22 @@ import { connect } from "react-redux";
|
|||
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
|
||||
import {
|
||||
selectFileInfosDownloaded,
|
||||
selectFileListDownloadedOrPublishedIsPending,
|
||||
selectIsFetchingFileListDownloadedOrPublished,
|
||||
} from "selectors/file_info";
|
||||
import {
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingClaimListMine,
|
||||
} from "selectors/claims";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { doCancelAllResolvingUris } from "actions/content";
|
||||
import FileListDownloaded from "./view";
|
||||
|
||||
const select = state => ({
|
||||
fileInfos: selectFileInfosDownloaded(state),
|
||||
isPending: selectFileListDownloadedOrPublishedIsPending(state),
|
||||
isFetching: selectIsFetchingFileListDownloadedOrPublished(state),
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
isFetchingClaims: selectIsFetchingClaimListMine(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -19,6 +26,7 @@ const perform = dispatch => ({
|
|||
fetchFileInfosDownloaded: () =>
|
||||
dispatch(doFetchFileInfosAndPublishedClaims()),
|
||||
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
|
||||
fetchClaims: () => dispatch(doFetchClaimListMine()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileListDownloaded);
|
||||
|
|
|
@ -12,7 +12,8 @@ import SubHeader from "component/subHeader";
|
|||
|
||||
class FileListDownloaded extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
if (!this.props.isPending) this.props.fetchFileInfosDownloaded();
|
||||
if (!this.props.isFetchingClaims) this.props.fetchClaims();
|
||||
if (!this.props.isFetching) this.props.fetchFileInfosDownloaded();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -20,13 +21,13 @@ class FileListDownloaded extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fileInfos, isPending, navigate } = this.props;
|
||||
const { fileInfos, isFetching, navigate } = this.props;
|
||||
|
||||
let content;
|
||||
if (fileInfos && fileInfos.length > 0) {
|
||||
content = <FileList fileInfos={fileInfos} fetching={isPending} />;
|
||||
content = <FileList fileInfos={fileInfos} fetching={isFetching} />;
|
||||
} else {
|
||||
if (isPending) {
|
||||
if (isFetching) {
|
||||
content = <BusyMessage message={__("Loading")} />;
|
||||
} else {
|
||||
content = (
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import React from "react";
|
||||
import rewards from "rewards";
|
||||
import { connect } from "react-redux";
|
||||
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import {
|
||||
selectFileInfosPublished,
|
||||
selectFileListDownloadedOrPublishedIsPending,
|
||||
} from "selectors/file_info";
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingClaimListMine,
|
||||
} from "selectors/claims";
|
||||
import { doClaimRewardType } from "actions/rewards";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { doCancelAllResolvingUris } from "actions/content";
|
||||
import FileListPublished from "./view";
|
||||
|
||||
const select = state => ({
|
||||
fileInfos: selectFileInfosPublished(state),
|
||||
isPending: selectFileListDownloadedOrPublishedIsPending(state),
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
isFetching: selectIsFetchingClaimListMine(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: path => dispatch(doNavigate(path)),
|
||||
fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()),
|
||||
fetchClaims: () => dispatch(doFetchClaimListMine()),
|
||||
claimFirstPublishReward: () =>
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)),
|
||||
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
|
||||
|
|
|
@ -12,11 +12,11 @@ import SubHeader from "component/subHeader";
|
|||
|
||||
class FileListPublished extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
if (!this.props.isPending) this.props.fetchFileListPublished();
|
||||
if (!this.props.isFetching) this.props.fetchClaims();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward();
|
||||
// if (this.props.claims.length > 0) this.props.fetchClaims();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -24,20 +24,20 @@ class FileListPublished extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fileInfos, isPending, navigate } = this.props;
|
||||
const { claims, isFetching, navigate } = this.props;
|
||||
|
||||
let content;
|
||||
|
||||
if (fileInfos && fileInfos.length > 0) {
|
||||
if (claims && claims.length > 0) {
|
||||
content = (
|
||||
<FileList
|
||||
fileInfos={fileInfos}
|
||||
fetching={isPending}
|
||||
fileInfos={claims}
|
||||
fetching={isFetching}
|
||||
fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (isPending) {
|
||||
if (isFetching) {
|
||||
content = <BusyMessage message={__("Loading")} />;
|
||||
} else {
|
||||
content = (
|
||||
|
|
|
@ -25,7 +25,7 @@ const makeSelect = () => {
|
|||
contentType: selectContentType(state, props),
|
||||
costInfo: selectCostInfo(state, props),
|
||||
metadata: selectMetadata(state, props),
|
||||
showNsfw: !selectShowNsfw(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
fileInfo: selectFileInfo(state, props),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import lbry from "lbry.js";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import Video from "component/video";
|
||||
|
@ -119,7 +120,11 @@ class FilePage extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
||||
{metadata && metadata.description}
|
||||
<ReactMarkdown
|
||||
source={(metadata && metadata.description) || ""}
|
||||
escapeHtml={true}
|
||||
disallowedTypes={["Heading", "HtmlInline", "HtmlBlock"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{metadata
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import React from "react";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { connect } from "react-redux";
|
||||
import { doFetchAccessToken } from "actions/user";
|
||||
import { selectAccessToken, selectUser } from "selectors/user";
|
||||
import HelpPage from "./view";
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
accessToken: selectAccessToken(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
fetchAccessToken: () => dispatch(doFetchAccessToken()),
|
||||
});
|
||||
|
||||
export default connect(null, perform)(HelpPage);
|
||||
export default connect(select, perform)(HelpPage);
|
||||
|
|
|
@ -14,6 +14,7 @@ class HelpPage extends React.PureComponent {
|
|||
lbryId: null,
|
||||
uiVersion: null,
|
||||
upgradeAvailable: null,
|
||||
accessTokenHidden: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,12 +37,20 @@ class HelpPage extends React.PureComponent {
|
|||
lbryId: info.lbry_id,
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.props.accessToken) this.props.fetchAccessToken();
|
||||
}
|
||||
|
||||
showAccessToken() {
|
||||
this.setState({
|
||||
accessTokenHidden: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let ver, osName, platform, newVerLink;
|
||||
|
||||
const { navigate } = this.props;
|
||||
const { navigate, user } = this.props;
|
||||
|
||||
if (this.state.versionInfo) {
|
||||
ver = this.state.versionInfo;
|
||||
|
@ -121,6 +130,7 @@ class HelpPage extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary"><h3>{__("About")}</h3></div>
|
||||
<div className="card__content">
|
||||
|
@ -136,16 +146,24 @@ class HelpPage extends React.PureComponent {
|
|||
? <table className="table-standard">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{__("daemon (lbrynet)")}</th>
|
||||
<th>{__("App")}</th>
|
||||
<td>{this.state.uiVersion}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{__("Daemon (lbrynet)")}</th>
|
||||
<td>{ver.lbrynet_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{__("wallet (lbryum)")}</th>
|
||||
<th>{__("Wallet (lbryum)")}</th>
|
||||
<td>{ver.lbryum_version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{__("interface")}</th>
|
||||
<td>{this.state.uiVersion}</td>
|
||||
<th>{__("Connected Email")}</th>
|
||||
<td>
|
||||
{user && user.primary_email
|
||||
? user.primary_email
|
||||
: <span className="empty">{__("none")}</span>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{__("Platform")}</th>
|
||||
|
@ -155,6 +173,18 @@ class HelpPage extends React.PureComponent {
|
|||
<th>{__("Installation ID")}</th>
|
||||
<td>{this.state.lbryId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{__("Access Token")}</th>
|
||||
<td>
|
||||
{this.state.accessTokenHidden &&
|
||||
<Link
|
||||
label={__("show")}
|
||||
onClick={this.showAccessToken.bind(this)}
|
||||
/>}
|
||||
{!this.state.accessTokenHidden &&
|
||||
this.props.accessToken}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
: <BusyMessage message={__("Looking up version info")} />}
|
||||
|
|
|
@ -2,13 +2,29 @@ import React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { doNavigate, doHistoryBack } from "actions/app";
|
||||
import { doClaimRewardType } from "actions/rewards";
|
||||
import { selectMyClaims } from "selectors/claims";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import {
|
||||
selectMyClaims,
|
||||
selectFetchingMyChannels,
|
||||
selectMyChannelClaims,
|
||||
selectClaimsByUri,
|
||||
} from "selectors/claims";
|
||||
import { selectResolvingUris } from "selectors/content";
|
||||
import {
|
||||
doFetchClaimListMine,
|
||||
doFetchChannelListMine,
|
||||
doResolveUri,
|
||||
doCreateChannel,
|
||||
doPublish,
|
||||
} from "actions/content";
|
||||
import rewards from "rewards";
|
||||
import PublishPage from "./view";
|
||||
|
||||
const select = state => ({
|
||||
myClaims: selectMyClaims(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
channels: selectMyChannelClaims(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
resolvingUris: selectResolvingUris(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -17,6 +33,10 @@ const perform = dispatch => ({
|
|||
fetchClaimListMine: () => dispatch(doFetchClaimListMine()),
|
||||
claimFirstChannelReward: () =>
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)),
|
||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
|
||||
publish: params => dispatch(doPublish(params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(PublishPage);
|
||||
|
|
|
@ -1,922 +1,8 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry";
|
||||
import lbryuri from "lbryuri";
|
||||
import { FormField, FormRow } from "component/form.js";
|
||||
import Link from "component/link";
|
||||
import rewards from "rewards";
|
||||
import Modal from "component/modal";
|
||||
import PublishForm from "component/publishForm";
|
||||
|
||||
class PublishPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._requiredFields = ["meta_title", "name", "bid", "tos_agree"];
|
||||
|
||||
this.state = {
|
||||
channels: null,
|
||||
rawName: "",
|
||||
name: "",
|
||||
bid: 10,
|
||||
hasFile: false,
|
||||
feeAmount: "",
|
||||
feeCurrency: "USD",
|
||||
channel: "anonymous",
|
||||
newChannelName: "@",
|
||||
newChannelBid: 10,
|
||||
nameResolved: null,
|
||||
myClaimExists: null,
|
||||
topClaimValue: 0.0,
|
||||
myClaimValue: 0.0,
|
||||
myClaimMetadata: null,
|
||||
copyrightNotice: "",
|
||||
otherLicenseDescription: "",
|
||||
otherLicenseUrl: "",
|
||||
uploadProgress: 0.0,
|
||||
uploaded: false,
|
||||
errorMessage: null,
|
||||
submitting: false,
|
||||
creatingChannel: false,
|
||||
modal: null,
|
||||
const PublishPage = props => {
|
||||
return <PublishForm {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
_updateChannelList(channel) {
|
||||
// Calls API to update displayed list of channels. If a channel name is provided, will select
|
||||
// that channel at the same time (used immediately after creating a channel)
|
||||
lbry.channel_list_mine().then(channels => {
|
||||
this.props.claimFirstChannelReward();
|
||||
this.setState({
|
||||
channels: channels,
|
||||
...(channel ? { channel } : {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
if (typeof event !== "undefined") {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
submitting: true,
|
||||
});
|
||||
|
||||
let checkFields = this._requiredFields;
|
||||
if (!this.state.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;
|
||||
}
|
||||
|
||||
if (this.state.nameIsMine) {
|
||||
// Pre-populate with existing metadata
|
||||
var metadata = Object.assign({}, this.state.myClaimMetadata);
|
||||
if (this.refs.file.getValue() !== "") {
|
||||
delete metadata.sources;
|
||||
}
|
||||
} else {
|
||||
var metadata = {};
|
||||
}
|
||||
|
||||
for (let metaField of [
|
||||
"title",
|
||||
"description",
|
||||
"thumbnail",
|
||||
"license",
|
||||
"license_url",
|
||||
"language",
|
||||
]) {
|
||||
var value = this.refs["meta_" + metaField].getValue();
|
||||
if (value !== "") {
|
||||
metadata[metaField] = value;
|
||||
}
|
||||
}
|
||||
|
||||
metadata.nsfw = parseInt(this.refs.meta_nsfw.getValue()) === 1;
|
||||
|
||||
const licenseUrl = this.refs.meta_license_url.getValue();
|
||||
if (licenseUrl) {
|
||||
metadata.license_url = licenseUrl;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
lbry.publishDeprecated(
|
||||
publishArgs,
|
||||
message => {
|
||||
this.handlePublishStarted();
|
||||
},
|
||||
null,
|
||||
error => {
|
||||
this.handlePublishError(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (this.state.isFee) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
handleNameChange(event) {
|
||||
var rawName = event.target.value;
|
||||
|
||||
if (!rawName) {
|
||||
this.setState({
|
||||
rawName: "",
|
||||
name: "",
|
||||
nameResolved: false,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lbryuri.isValidName(rawName, false)) {
|
||||
this.refs.name.showError(
|
||||
__("LBRY names must contain only letters, numbers and dashes.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = rawName.toLowerCase();
|
||||
this.setState({
|
||||
rawName: rawName,
|
||||
name: name,
|
||||
nameResolved: null,
|
||||
myClaimExists: null,
|
||||
});
|
||||
|
||||
const myClaimInfo = Object.values(this.props.myClaims).find(
|
||||
claim => claim.name === name
|
||||
);
|
||||
|
||||
this.setState({
|
||||
myClaimExists: !!myClaimInfo,
|
||||
});
|
||||
lbry.resolve({ uri: name }).then(
|
||||
claimInfo => {
|
||||
if (name != this.state.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!claimInfo) {
|
||||
this.setState({
|
||||
nameResolved: false,
|
||||
});
|
||||
} else {
|
||||
const topClaimIsMine =
|
||||
myClaimInfo && myClaimInfo.amount >= claimInfo.amount;
|
||||
const newState = {
|
||||
nameResolved: true,
|
||||
topClaimValue: parseFloat(claimInfo.amount),
|
||||
myClaimExists: !!myClaimInfo,
|
||||
myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null,
|
||||
myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
|
||||
topClaimIsMine: topClaimIsMine,
|
||||
};
|
||||
|
||||
if (topClaimIsMine) {
|
||||
newState.bid = myClaimInfo.amount;
|
||||
} else if (this.state.myClaimMetadata) {
|
||||
// Just changed away from a name we have a claim on, so clear pre-fill
|
||||
newState.bid = "";
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// Assume an error means the name is available
|
||||
this.setState({
|
||||
name: name,
|
||||
nameResolved: false,
|
||||
myClaimExists: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleBidChange(event) {
|
||||
this.setState({
|
||||
bid: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeAmountChange(event) {
|
||||
this.setState({
|
||||
feeAmount: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeCurrencyChange(event) {
|
||||
this.setState({
|
||||
feeCurrency: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeePrefChange(feeEnabled) {
|
||||
this.setState({
|
||||
isFee: feeEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
handleLicenseChange(event) {
|
||||
var licenseType = event.target.options[
|
||||
event.target.selectedIndex
|
||||
].getAttribute("data-license-type");
|
||||
var newState = {
|
||||
copyrightChosen: licenseType == "copyright",
|
||||
otherLicenseChosen: licenseType == "other",
|
||||
};
|
||||
|
||||
if (licenseType == "copyright") {
|
||||
newState.copyrightNotice = __("All rights reserved.");
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
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(event) {
|
||||
const channel = event.target.value;
|
||||
|
||||
this.setState({
|
||||
channel: channel,
|
||||
});
|
||||
}
|
||||
|
||||
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: newChannelName,
|
||||
});
|
||||
}
|
||||
|
||||
handleNewChannelBidChange(event) {
|
||||
this.setState({
|
||||
newChannelBid: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleTOSChange(event) {
|
||||
this.setState({
|
||||
TOSAgreed: 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,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getLicenseUrl() {
|
||||
if (!this.refs.meta_license) {
|
||||
return "";
|
||||
} else if (this.state.otherLicenseChosen) {
|
||||
return this.state.otherLicenseUrl;
|
||||
} else {
|
||||
return (
|
||||
this.refs.meta_license.getSelectedElement().getAttribute("data-url") ||
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchClaimListMine();
|
||||
this._updateChannelList();
|
||||
}
|
||||
|
||||
onFileChange() {
|
||||
if (this.refs.file.getValue()) {
|
||||
this.setState({ hasFile: true });
|
||||
} else {
|
||||
this.setState({ hasFile: false });
|
||||
}
|
||||
}
|
||||
|
||||
getNameBidHelpText() {
|
||||
if (!this.state.name) {
|
||||
return __("Select a URL for this publish.");
|
||||
} else if (this.state.nameResolved === false) {
|
||||
return __("This URL is unused.");
|
||||
} else if (this.state.myClaimExists) {
|
||||
return __(
|
||||
"You have already used this URL. Publishing to it again will update your previous publish."
|
||||
);
|
||||
} else if (this.state.topClaimValue) {
|
||||
if (this.state.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.state.topClaimValue,
|
||||
this.state.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.channels === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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.state.myClaimExists
|
||||
? __(
|
||||
"If you don't choose a file, the file from your existing claim will be used."
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!this.state.hasFile
|
||||
? ""
|
||||
: <div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Title")}
|
||||
type="text"
|
||||
ref="meta_title"
|
||||
name="title"
|
||||
placeholder={__("Title")}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="text"
|
||||
label={__("Thumbnail URL")}
|
||||
ref="meta_thumbnail"
|
||||
name="thumbnail"
|
||||
placeholder="http://spee.ch/mylogo"
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Description")}
|
||||
type="textarea"
|
||||
ref="meta_description"
|
||||
name="description"
|
||||
placeholder={__("Description of your content")}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Language")}
|
||||
type="select"
|
||||
defaultValue="en"
|
||||
ref="meta_language"
|
||||
name="language"
|
||||
>
|
||||
<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")}
|
||||
defaultValue="en"
|
||||
ref="meta_nsfw"
|
||||
name="nsfw"
|
||||
>
|
||||
{/* <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"
|
||||
value="1"
|
||||
onChange={() => {
|
||||
this.handleFeePrefChange(false);
|
||||
}}
|
||||
defaultChecked={!this.state.isFee}
|
||||
/>
|
||||
<FormField
|
||||
type="radio"
|
||||
name="isFree"
|
||||
label={!this.state.isFee ? __("Choose price...") : __("Price ")}
|
||||
onChange={() => {
|
||||
this.handleFeePrefChange(true);
|
||||
}}
|
||||
defaultChecked={this.state.isFee}
|
||||
/>
|
||||
<span className={!this.state.isFee ? "hidden" : ""}>
|
||||
<FormField
|
||||
type="number"
|
||||
className="form-field__input--inline"
|
||||
step="0.01"
|
||||
placeholder="1.00"
|
||||
min="0.01"
|
||||
onChange={event => this.handleFeeAmountChange(event)}
|
||||
/>
|
||||
{" "}
|
||||
<FormField
|
||||
type="select"
|
||||
onChange={event => {
|
||||
this.handleFeeCurrencyChange(event);
|
||||
}}
|
||||
>
|
||||
<option value="USD">{__("US Dollars")}</option>
|
||||
<option value="LBC">{__("LBRY credits")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{this.state.isFee
|
||||
? <div className="form-field__helper">
|
||||
{__(
|
||||
"If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase."
|
||||
)}
|
||||
</div>
|
||||
: ""}
|
||||
<FormRow
|
||||
label="License"
|
||||
type="select"
|
||||
ref="meta_license"
|
||||
name="license"
|
||||
onChange={event => {
|
||||
this.handleLicenseChange(event);
|
||||
}}
|
||||
>
|
||||
<option />
|
||||
<option>{__("Public Domain")}</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">
|
||||
{__("Creative Commons Attribution 4.0 International")}
|
||||
</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">
|
||||
{__(
|
||||
"Creative Commons Attribution-ShareAlike 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">
|
||||
{__(
|
||||
"Creative Commons Attribution-NoDerivatives 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">
|
||||
{__(
|
||||
"Creative Commons Attribution-NonCommercial 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">
|
||||
{__(
|
||||
"Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">
|
||||
{__(
|
||||
"Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option
|
||||
data-license-type="copyright"
|
||||
{...(this.state.copyrightChosen
|
||||
? { value: this.state.copyrightNotice }
|
||||
: {})}
|
||||
>
|
||||
{__("Copyrighted...")}
|
||||
</option>
|
||||
<option
|
||||
data-license-type="other"
|
||||
{...(this.state.otherLicenseChosen
|
||||
? { value: this.state.otherLicenseDescription }
|
||||
: {})}
|
||||
>
|
||||
{__("Other...")}
|
||||
</option>
|
||||
</FormRow>
|
||||
<FormField
|
||||
type="hidden"
|
||||
ref="meta_license_url"
|
||||
name="license_url"
|
||||
value={this.getLicenseUrl()}
|
||||
/>
|
||||
{this.state.copyrightChosen
|
||||
? <FormRow
|
||||
label={__("Copyright notice")}
|
||||
type="text"
|
||||
name="copyright-notice"
|
||||
value={this.state.copyrightNotice}
|
||||
onChange={event => {
|
||||
this.handleCopyrightNoticeChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
{this.state.otherLicenseChosen
|
||||
? <FormRow
|
||||
label={__("License description")}
|
||||
type="text"
|
||||
name="other-license-description"
|
||||
onChange={event => {
|
||||
this.handleOtherLicenseDescriptionChange();
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
{this.state.otherLicenseChosen
|
||||
? <FormRow
|
||||
label={__("License URL")}
|
||||
type="text"
|
||||
name="other-license-url"
|
||||
onChange={event => {
|
||||
this.handleOtherLicenseUrlChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Identity")}</h4>
|
||||
<div className="card__subtitle">
|
||||
{__("Who created this content?")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="select"
|
||||
tabIndex="1"
|
||||
onChange={event => {
|
||||
this.handleChannelChange(event);
|
||||
}}
|
||||
value={this.state.channel}
|
||||
>
|
||||
<option key="anonymous" value="anonymous">
|
||||
{__("Anonymous")}
|
||||
</option>
|
||||
{this.state.channels.map(({ name }) =>
|
||||
<option key={name} value={name}>{name}</option>
|
||||
)}
|
||||
<option key="new" value="new">{__("New identity...")}</option>
|
||||
</FormRow>
|
||||
</div>
|
||||
{this.state.channel == "new"
|
||||
? <div className="card__content">
|
||||
<FormRow
|
||||
label={__("Name")}
|
||||
type="text"
|
||||
onChange={event => {
|
||||
this.handleNewChannelNameChange(event);
|
||||
}}
|
||||
ref={newChannelName => {
|
||||
this.refs.newChannelName = newChannelName;
|
||||
}}
|
||||
value={this.state.newChannelName}
|
||||
/>
|
||||
<FormRow
|
||||
label={__("Deposit")}
|
||||
postfix={__("LBC")}
|
||||
step="0.01"
|
||||
min="0"
|
||||
type="number"
|
||||
helper={lbcInputHelp}
|
||||
onChange={event => {
|
||||
this.handleNewChannelBidChange(event);
|
||||
}}
|
||||
value={this.state.newChannelBid}
|
||||
/>
|
||||
<div className="form-row-submit">
|
||||
<Link
|
||||
button="primary"
|
||||
label={
|
||||
!this.state.creatingChannel
|
||||
? __("Create identity")
|
||||
: __("Creating identity...")
|
||||
}
|
||||
onClick={event => {
|
||||
this.handleCreateChannelClick(event);
|
||||
}}
|
||||
disabled={this.state.creatingChannel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Address")}</h4>
|
||||
<div className="card__subtitle">
|
||||
{__("Where should this content permanently reside?")}
|
||||
{" "}
|
||||
<Link
|
||||
label={__("Read more")}
|
||||
href="https://lbry.io/faq/naming"
|
||||
/>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
prefix="lbry://"
|
||||
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.state.nameResolved
|
||||
? this.state.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")}
|
||||
checked={this.state.TOSAgreed}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
type="checkbox"
|
||||
name="tos_agree"
|
||||
ref={field => {
|
||||
this.refs.tos_agree = field;
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
<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>lbry://{this.state.name}</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 PublishPage;
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { selectFetchingRewards, selectRewards } from "selectors/rewards";
|
||||
import {
|
||||
selectUserIsRewardEligible,
|
||||
selectUserHasEmail,
|
||||
selectUserIsVerificationCandidate,
|
||||
} from "selectors/user";
|
||||
makeSelectRewardByType,
|
||||
selectFetchingRewards,
|
||||
selectRewards,
|
||||
} from "selectors/rewards";
|
||||
import { selectUser } from "selectors/user";
|
||||
import { doAuthNavigate, doNavigate } from "actions/app";
|
||||
import { doRewardList } from "actions/rewards";
|
||||
import rewards from "rewards";
|
||||
import RewardsPage from "./view";
|
||||
|
||||
const select = state => ({
|
||||
const select = (state, props) => {
|
||||
const selectReward = makeSelectRewardByType();
|
||||
|
||||
return {
|
||||
fetching: selectFetchingRewards(state),
|
||||
rewards: selectRewards(state),
|
||||
hasEmail: selectUserHasEmail(state),
|
||||
isEligible: selectUserIsRewardEligible(state),
|
||||
isVerificationCandidate: selectUserIsVerificationCandidate(state),
|
||||
});
|
||||
newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
|
||||
user: selectUser(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
navigate: path => dispatch(doNavigate(path)),
|
||||
doAuth: () => {
|
||||
dispatch(doAuthNavigate("/rewards"));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(select, perform)(RewardsPage);
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from "react";
|
||||
import lbryio from "lbryio";
|
||||
import { BusyMessage, CreditAmount, Icon } from "component/common";
|
||||
import SubHeader from "component/subHeader";
|
||||
import Auth from "component/auth";
|
||||
import Link from "component/link";
|
||||
import RewardLink from "component/rewardLink";
|
||||
|
||||
|
@ -41,65 +39,91 @@ class RewardsPage extends React.PureComponent {
|
|||
fetchRewards(props) {
|
||||
const { fetching, rewards, fetchRewards } = props;
|
||||
|
||||
if (!fetching && Object.keys(rewards).length < 1) fetchRewards();
|
||||
if (!fetching && (!rewards || !rewards.length)) {
|
||||
fetchRewards();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fetching,
|
||||
isEligible,
|
||||
isVerificationCandidate,
|
||||
hasEmail,
|
||||
rewards,
|
||||
} = this.props;
|
||||
const { doAuth, fetching, navigate, rewards, user } = this.props;
|
||||
|
||||
let content,
|
||||
isCard = false;
|
||||
let content, cardHeader;
|
||||
|
||||
if (!hasEmail || isVerificationCandidate) {
|
||||
if (fetching) {
|
||||
content = (
|
||||
<div className="card__content">
|
||||
<BusyMessage message={__("Fetching rewards")} />
|
||||
</div>
|
||||
);
|
||||
} else if (rewards.length > 0) {
|
||||
content = (
|
||||
<div>
|
||||
<p>
|
||||
{__(
|
||||
"Additional information is required to be eligible for the rewards program."
|
||||
)}
|
||||
</p>
|
||||
<Auth />
|
||||
</div>
|
||||
);
|
||||
isCard = true;
|
||||
} else if (!isEligible) {
|
||||
isCard = true;
|
||||
content = (
|
||||
<div className="empty">
|
||||
<p>{__("You are not eligible to claim rewards.")}</p>
|
||||
<p>
|
||||
{__("To become eligible, email")}
|
||||
{" "}<Link href="mailto:help@lbry.io" label="help@lbry.io" />{" "}
|
||||
{__("with a link to a public social media profile.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else if (fetching) {
|
||||
content = <BusyMessage message={__("Fetching rewards")} />;
|
||||
} else if (rewards.length > 0) {
|
||||
content = rewards.map(reward =>
|
||||
{rewards.map(reward =>
|
||||
<RewardTile key={reward.reward_type} reward={reward} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = <div className="empty">{__("Failed to load rewards.")}</div>;
|
||||
content = (
|
||||
<div className="card__content empty">
|
||||
{__("Failed to load rewards.")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
user &&
|
||||
(!user.primary_email ||
|
||||
!user.has_verified_email ||
|
||||
!user.is_identity_verified)
|
||||
) {
|
||||
cardHeader = (
|
||||
<div>
|
||||
<div className="card__content empty">
|
||||
<p>{__("Only verified accounts are eligible to earn rewards.")}</p>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<Link onClick={doAuth} button="primary" label="Become Verified" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (user && !user.is_reward_approved) {
|
||||
cardHeader = (
|
||||
<div className="card__content">
|
||||
<p>
|
||||
{__(
|
||||
"This account must undergo review before you can participate in the rewards program."
|
||||
)}
|
||||
{" "}
|
||||
{__("This can take anywhere from several minutes to several days.")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{__(
|
||||
"We apologize for this inconvenience, but have added this additional step to prevent fraud."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__("You will receive an email when this process is complete.") +
|
||||
" " +
|
||||
__("Please enjoy free content in the meantime!")}
|
||||
</p>
|
||||
<p>
|
||||
<Link
|
||||
onClick={() => navigate("/discover")}
|
||||
button="primary"
|
||||
label="Return Home"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<SubHeader />
|
||||
{isCard
|
||||
? <section className="card">
|
||||
<div className="card__content">
|
||||
{cardHeader && <section className="card">{cardHeader}</section>}
|
||||
{content}
|
||||
</div>
|
||||
</section>
|
||||
: content}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ class SettingsPage extends React.PureComponent {
|
|||
const { daemonSettings } = this.props;
|
||||
|
||||
this.state = {
|
||||
isMaxUpload: daemonSettings && daemonSettings.max_upload != 0,
|
||||
isMaxDownload: daemonSettings && daemonSettings.max_download != 0,
|
||||
// isMaxUpload: daemonSettings && daemonSettings.max_upload != 0,
|
||||
// isMaxDownload: daemonSettings && daemonSettings.max_download != 0,
|
||||
showUnavailable: lbry.getClientSetting("showUnavailable"),
|
||||
language: lbry.getClientSetting("language"),
|
||||
clearingCache: false,
|
||||
|
@ -55,15 +55,6 @@ class SettingsPage extends React.PureComponent {
|
|||
this.setDaemonSetting("download_directory", event.target.value);
|
||||
}
|
||||
|
||||
onMaxUploadPrefChange(isLimited) {
|
||||
if (!isLimited) {
|
||||
this.setDaemonSetting("max_upload", 0.0);
|
||||
}
|
||||
this.setState({
|
||||
isMaxUpload: isLimited,
|
||||
});
|
||||
}
|
||||
|
||||
onKeyFeeChange(event) {
|
||||
var oldSettings = this.props.daemonSettings.max_key_fee;
|
||||
var newSettings = {
|
||||
|
@ -90,22 +81,31 @@ class SettingsPage extends React.PureComponent {
|
|||
this.setDaemonSetting("disable_max_key_fee", isDisabled);
|
||||
}
|
||||
|
||||
onMaxUploadFieldChange(event) {
|
||||
this.setDaemonSetting("max_upload", Number(event.target.value));
|
||||
}
|
||||
|
||||
onMaxDownloadPrefChange(isLimited) {
|
||||
if (!isLimited) {
|
||||
this.setDaemonSetting("max_download", 0.0);
|
||||
}
|
||||
this.setState({
|
||||
isMaxDownload: isLimited,
|
||||
});
|
||||
}
|
||||
|
||||
onMaxDownloadFieldChange(event) {
|
||||
this.setDaemonSetting("max_download", Number(event.target.value));
|
||||
}
|
||||
// onMaxUploadPrefChange(isLimited) {
|
||||
// if (!isLimited) {
|
||||
// this.setDaemonSetting("max_upload", 0.0);
|
||||
// }
|
||||
// this.setState({
|
||||
// isMaxUpload: isLimited,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// onMaxUploadFieldChange(event) {
|
||||
// this.setDaemonSetting("max_upload", Number(event.target.value));
|
||||
// }
|
||||
//
|
||||
// onMaxDownloadPrefChange(isLimited) {
|
||||
// if (!isLimited) {
|
||||
// this.setDaemonSetting("max_download", 0.0);
|
||||
// }
|
||||
// this.setState({
|
||||
// isMaxDownload: isLimited,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// onMaxDownloadFieldChange(event) {
|
||||
// this.setDaemonSetting("max_download", Number(event.target.value));
|
||||
// }
|
||||
|
||||
onShowNsfwChange(event) {
|
||||
this.props.setClientSetting("showNsfw", event.target.checked);
|
||||
|
@ -174,24 +174,8 @@ class SettingsPage extends React.PureComponent {
|
|||
</div>
|
||||
</section>
|
||||
*/
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<SubHeader />
|
||||
<section className="card">
|
||||
<div className="card__content">
|
||||
<h3>{__("Download Directory")}</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="directory"
|
||||
name="download_directory"
|
||||
defaultValue={daemonSettings.download_directory}
|
||||
helper={__("LBRY downloads will be saved here.")}
|
||||
onChange={this.onDownloadDirChange.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
/*
|
||||
<section className="card">
|
||||
<div className="card__content">
|
||||
<h3>{__("Bandwidth Limits")}</h3>
|
||||
|
@ -279,10 +263,27 @@ class SettingsPage extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
*/
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<SubHeader />
|
||||
<section className="card">
|
||||
<div className="card__content">
|
||||
<h3>{__("Key Fee")}</h3>
|
||||
<h3>{__("Download Directory")}</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="directory"
|
||||
name="download_directory"
|
||||
defaultValue={daemonSettings.download_directory}
|
||||
helper={__("LBRY downloads will be saved here.")}
|
||||
onChange={this.onDownloadDirChange.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="card">
|
||||
<div className="card__content">
|
||||
<h3>{__("Max Purchase Price")}</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="form-row__label-row">
|
||||
|
|
|
@ -24,7 +24,7 @@ class ShowPage extends React.PureComponent {
|
|||
|
||||
let innerContent = "";
|
||||
|
||||
if (isResolvingUri && !claim) {
|
||||
if ((isResolvingUri && !claim) || !claim) {
|
||||
innerContent = (
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as types from "constants/action_types";
|
||||
import * as modalTypes from "constants/modal_types";
|
||||
import lbry from "lbry";
|
||||
|
||||
const currentPath = () => {
|
||||
|
@ -15,8 +16,10 @@ const reducers = {};
|
|||
const defaultState = {
|
||||
isLoaded: false,
|
||||
currentPath: currentPath(),
|
||||
pathAfterAuth: "/discover",
|
||||
platform: process.platform,
|
||||
upgradeSkipped: sessionStorage.getItem("upgradeSkipped"),
|
||||
daemonVersionMatched: null,
|
||||
daemonReady: false,
|
||||
hasSignature: false,
|
||||
badgeNumber: 0,
|
||||
|
@ -28,12 +31,31 @@ reducers[types.DAEMON_READY] = function(state, action) {
|
|||
});
|
||||
};
|
||||
|
||||
reducers[types.DAEMON_VERSION_MATCH] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
daemonVersionMatched: true,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.DAEMON_VERSION_MISMATCH] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
daemonVersionMatched: false,
|
||||
modal: modalTypes.INCOMPATIBLE_DAEMON,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.CHANGE_PATH] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
currentPath: action.data.path,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
pathAfterAuth: action.data.path,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.UPGRADE_CANCELLED] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
downloadProgress: null,
|
||||
|
|
|
@ -15,10 +15,16 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
|||
byUri[uri] = claim.claim_id;
|
||||
} else if (claim === undefined && certificate !== undefined) {
|
||||
byId[certificate.claim_id] = certificate;
|
||||
// Don't point URI at the channel certificate unless it actually is
|
||||
// a channel URI. This is brittle.
|
||||
if (!uri.split(certificate.name)[1].match(/\//)) {
|
||||
byUri[uri] = certificate.claim_id;
|
||||
} else {
|
||||
byUri[uri] = null;
|
||||
}
|
||||
} else {
|
||||
byUri[uri] = null;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byId,
|
||||
|
@ -28,43 +34,72 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
|||
|
||||
reducers[types.FETCH_CLAIM_LIST_MINE_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
isClaimListMinePending: true,
|
||||
isFetchingClaimListMine: true,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) {
|
||||
const { claims } = action.data;
|
||||
const myClaims = new Set(state.myClaims);
|
||||
const byUri = Object.assign({}, state.claimsByUri);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const pendingById = Object.assign({}, state.pendingById);
|
||||
const abandoningById = Object.assign({}, state.abandoningById);
|
||||
const myClaims = new Set(
|
||||
claims
|
||||
.map(claim => claim.claim_id)
|
||||
.filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1)
|
||||
);
|
||||
|
||||
claims.forEach(claim => {
|
||||
myClaims.add(claim.claim_id);
|
||||
byId[claim.claim_id] = claim;
|
||||
|
||||
const pending = Object.values(pendingById).find(pendingClaim => {
|
||||
return (
|
||||
pendingClaim.name == claim.name &&
|
||||
pendingClaim.channel_name == claim.channel_name
|
||||
);
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
delete pendingById[pending.claim_id];
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old timed out pending publishes
|
||||
const old = Object.values(pendingById)
|
||||
.filter(pendingClaim => Date.now() - pendingClaim.time >= 20 * 60 * 1000)
|
||||
.forEach(pendingClaim => {
|
||||
delete pendingById[pendingClaim.claim_id];
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isClaimListMinePending: false,
|
||||
isFetchingClaimListMine: false,
|
||||
myClaims: myClaims,
|
||||
byId,
|
||||
pendingById,
|
||||
});
|
||||
};
|
||||
|
||||
// reducers[types.FETCH_CHANNEL_CLAIMS_STARTED] = function(state, action) {
|
||||
// const {
|
||||
// uri,
|
||||
// } = action.data
|
||||
//
|
||||
// const newClaims = Object.assign({}, state.claimsByChannel)
|
||||
//
|
||||
// if (claims !== undefined) {
|
||||
// newClaims[uri] = claims
|
||||
// }
|
||||
//
|
||||
// return Object.assign({}, state, {
|
||||
// claimsByChannel: newClaims
|
||||
// })
|
||||
// }
|
||||
reducers[types.FETCH_CHANNEL_LIST_MINE_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, { fetchingMyChannels: true });
|
||||
};
|
||||
|
||||
reducers[types.FETCH_CHANNEL_LIST_MINE_COMPLETED] = function(state, action) {
|
||||
const { claims } = action.data;
|
||||
const myChannelClaims = new Set(state.myChannelClaims);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
|
||||
claims.forEach(claim => {
|
||||
myChannelClaims.add(claim.claim_id);
|
||||
byId[claims.claim_id] = claim;
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byId,
|
||||
fetchingMyChannels: false,
|
||||
myChannelClaims,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
|
||||
const { uri, claims } = action.data;
|
||||
|
@ -80,7 +115,18 @@ reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
|
|||
});
|
||||
};
|
||||
|
||||
reducers[types.ABANDON_CLAIM_COMPLETED] = function(state, action) {
|
||||
reducers[types.ABANDON_CLAIM_STARTED] = function(state, action) {
|
||||
const { claimId } = action.data;
|
||||
const abandoningById = Object.assign({}, state.abandoningById);
|
||||
|
||||
abandoningById[claimId] = true;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
abandoningById,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.ABANDON_CLAIM_SUCCEEDED] = function(state, action) {
|
||||
const { claimId } = action.data;
|
||||
const myClaims = new Set(state.myClaims);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
|
@ -103,6 +149,20 @@ reducers[types.ABANDON_CLAIM_COMPLETED] = function(state, action) {
|
|||
});
|
||||
};
|
||||
|
||||
reducers[types.CREATE_CHANNEL_COMPLETED] = function(state, action) {
|
||||
const { channelClaim } = action.data;
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const myChannelClaims = new Set(state.myChannelClaims);
|
||||
|
||||
byId[channelClaim.claim_id] = channelClaim;
|
||||
myChannelClaims.add(channelClaim.claim_id);
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byId,
|
||||
myChannelClaims,
|
||||
});
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
|
|
|
@ -6,14 +6,15 @@ const defaultState = {};
|
|||
|
||||
reducers[types.FILE_LIST_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
isFileListPending: true,
|
||||
isFetchingFileList: true,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
|
||||
reducers[types.FILE_LIST_SUCCEEDED] = function(state, action) {
|
||||
const { fileInfos } = action.data;
|
||||
|
||||
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
|
||||
|
||||
fileInfos.forEach(fileInfo => {
|
||||
const { outpoint } = fileInfo;
|
||||
|
||||
|
@ -21,8 +22,9 @@ reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
|
|||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isFileListPending: false,
|
||||
isFetchingFileList: false,
|
||||
byOutpoint: newByOutpoint,
|
||||
pendingByOutpoint,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -56,15 +58,15 @@ reducers[types.DOWNLOADING_STARTED] = function(state, action) {
|
|||
const { uri, outpoint, fileInfo } = action.data;
|
||||
|
||||
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||
const newDownloading = Object.assign({}, state.urisDownloading);
|
||||
const newDownloading = Object.assign({}, state.downloadingByOutpoin);
|
||||
const newLoading = Object.assign({}, state.urisLoading);
|
||||
|
||||
newDownloading[uri] = true;
|
||||
newDownloading[outpoint] = true;
|
||||
newByOutpoint[outpoint] = fileInfo;
|
||||
delete newLoading[uri];
|
||||
|
||||
return Object.assign({}, state, {
|
||||
urisDownloading: newDownloading,
|
||||
downloadingByOutpoint: newDownloading,
|
||||
urisLoading: newLoading,
|
||||
byOutpoint: newByOutpoint,
|
||||
});
|
||||
|
@ -74,14 +76,14 @@ reducers[types.DOWNLOADING_PROGRESSED] = function(state, action) {
|
|||
const { uri, outpoint, fileInfo } = action.data;
|
||||
|
||||
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||
const newDownloading = Object.assign({}, state.urisDownloading);
|
||||
const newDownloading = Object.assign({}, state.downloadingByOutpoint);
|
||||
|
||||
newByOutpoint[outpoint] = fileInfo;
|
||||
newDownloading[uri] = true;
|
||||
newDownloading[outpoint] = true;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byOutpoint: newByOutpoint,
|
||||
urisDownloading: newDownloading,
|
||||
downloadingByOutpoint: newDownloading,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -89,14 +91,14 @@ reducers[types.DOWNLOADING_COMPLETED] = function(state, action) {
|
|||
const { uri, outpoint, fileInfo } = action.data;
|
||||
|
||||
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||
const newDownloading = Object.assign({}, state.urisDownloading);
|
||||
const newDownloading = Object.assign({}, state.downloadingByOutpoint);
|
||||
|
||||
newByOutpoint[outpoint] = fileInfo;
|
||||
delete newDownloading[uri];
|
||||
delete newDownloading[outpoint];
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byOutpoint: newByOutpoint,
|
||||
urisDownloading: newDownloading,
|
||||
downloadingByOutpoint: newDownloading,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -104,11 +106,14 @@ reducers[types.FILE_DELETE] = function(state, action) {
|
|||
const { outpoint } = action.data;
|
||||
|
||||
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||
const downloadingByOutpoint = Object.assign({}, state.downloadingByOutpoint);
|
||||
|
||||
delete newByOutpoint[outpoint];
|
||||
delete downloadingByOutpoint[outpoint];
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byOutpoint: newByOutpoint,
|
||||
downloadingByOutpoint,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ reducers[types.USER_EMAIL_NEW_STARTED] = function(state, action) {
|
|||
|
||||
reducers[types.USER_EMAIL_NEW_SUCCESS] = function(state, action) {
|
||||
let user = Object.assign({}, state.user);
|
||||
user.has_email = true;
|
||||
user.primary_email = action.data.email;
|
||||
return Object.assign({}, state, {
|
||||
emailToVerify: action.data.email,
|
||||
emailNewIsPending: false,
|
||||
|
@ -105,7 +105,7 @@ reducers[types.USER_EMAIL_VERIFY_STARTED] = function(state, action) {
|
|||
|
||||
reducers[types.USER_EMAIL_VERIFY_SUCCESS] = function(state, action) {
|
||||
let user = Object.assign({}, state.user);
|
||||
user.has_email = true;
|
||||
user.primary_email = action.data.email;
|
||||
return Object.assign({}, state, {
|
||||
emailToVerify: "",
|
||||
emailVerifyIsPending: false,
|
||||
|
@ -120,6 +120,36 @@ reducers[types.USER_EMAIL_VERIFY_FAILURE] = function(state, action) {
|
|||
});
|
||||
};
|
||||
|
||||
reducers[types.USER_IDENTITY_VERIFY_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
identityVerifyIsPending: true,
|
||||
identityVerifyErrorMessage: "",
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.USER_IDENTITY_VERIFY_SUCCESS] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
identityVerifyIsPending: false,
|
||||
identityVerifyErrorMessage: "",
|
||||
user: action.data.user,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.USER_IDENTITY_VERIFY_FAILURE] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
identityVerifyIsPending: false,
|
||||
identityVerifyErrorMessage: action.data.error,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) {
|
||||
const { token } = action.data;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
accessToken: token,
|
||||
});
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
|
|
|
@ -30,6 +30,10 @@ function rewardMessage(type, amount) {
|
|||
"You earned %s LBC for making your first publication.",
|
||||
amount
|
||||
),
|
||||
featured_download: __(
|
||||
"You earned %s LBC for watching a featured download.",
|
||||
amount
|
||||
),
|
||||
}[type];
|
||||
}
|
||||
|
||||
|
|
|
@ -177,6 +177,11 @@ export const selectDaemonReady = createSelector(
|
|||
state => state.daemonReady
|
||||
);
|
||||
|
||||
export const selectDaemonVersionMatched = createSelector(
|
||||
_selectState,
|
||||
state => state.daemonVersionMatched
|
||||
);
|
||||
|
||||
export const selectSnackBar = createSelector(
|
||||
_selectState,
|
||||
state => state.snackBar || {}
|
||||
|
@ -187,7 +192,17 @@ export const selectSnackBarSnacks = createSelector(
|
|||
snackBar => snackBar.snacks || []
|
||||
);
|
||||
|
||||
export const selectWelcomeModalAcknowledged = createSelector(
|
||||
_selectState,
|
||||
state => lbry.getClientSetting("welcome_acknowledged")
|
||||
);
|
||||
|
||||
export const selectBadgeNumber = createSelector(
|
||||
_selectState,
|
||||
state => state.badgeNumber
|
||||
);
|
||||
|
||||
export const selectPathAfterAuth = createSelector(
|
||||
_selectState,
|
||||
state => state.pathAfterAuth
|
||||
);
|
||||
|
|
|
@ -51,7 +51,7 @@ export const makeSelectClaimForUri = () => {
|
|||
const selectClaimForUriIsMine = (state, props) => {
|
||||
const uri = lbryuri.normalize(props.uri);
|
||||
const claim = selectClaimsByUri(state)[uri];
|
||||
const myClaims = selectMyClaims(state);
|
||||
const myClaims = selectMyClaimsRaw(state);
|
||||
|
||||
return myClaims.has(claim.claim_id);
|
||||
};
|
||||
|
@ -100,27 +100,72 @@ export const makeSelectContentTypeForUri = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const selectClaimListMineIsPending = createSelector(
|
||||
export const selectIsFetchingClaimListMine = createSelector(
|
||||
_selectState,
|
||||
state => state.isClaimListMinePending
|
||||
state => !!state.isFetchingClaimListMine
|
||||
);
|
||||
|
||||
export const selectMyClaims = createSelector(
|
||||
export const selectMyClaimsRaw = createSelector(
|
||||
_selectState,
|
||||
state => new Set(state.myClaims)
|
||||
);
|
||||
|
||||
export const selectAbandoningIds = createSelector(_selectState, state =>
|
||||
Object.keys(state.abandoningById || {})
|
||||
);
|
||||
|
||||
export const selectPendingClaims = createSelector(_selectState, state =>
|
||||
Object.values(state.pendingById || {})
|
||||
);
|
||||
|
||||
export const selectMyClaims = createSelector(
|
||||
selectMyClaimsRaw,
|
||||
selectClaimsById,
|
||||
selectAbandoningIds,
|
||||
selectPendingClaims,
|
||||
(myClaimIds, byId, abandoningIds, pendingClaims) => {
|
||||
const claims = [];
|
||||
|
||||
myClaimIds.forEach(id => {
|
||||
const claim = byId[id];
|
||||
|
||||
if (claim && abandoningIds.indexOf(id) == -1) claims.push(claim);
|
||||
});
|
||||
|
||||
return [...claims, ...pendingClaims];
|
||||
}
|
||||
);
|
||||
|
||||
export const selectMyClaimsWithoutChannels = createSelector(
|
||||
selectMyClaims,
|
||||
myClaims => myClaims.filter(claim => !claim.name.match(/^@/))
|
||||
);
|
||||
|
||||
export const selectMyClaimsOutpoints = createSelector(
|
||||
selectMyClaims,
|
||||
selectClaimsById,
|
||||
(claimIds, byId) => {
|
||||
myClaims => {
|
||||
const outpoints = [];
|
||||
|
||||
claimIds.forEach(claimId => {
|
||||
const claim = byId[claimId];
|
||||
if (claim) outpoints.push(`${claim.txid}:${claim.nout}`);
|
||||
});
|
||||
myClaims.forEach(claim => outpoints.push(`${claim.txid}:${claim.nout}`));
|
||||
|
||||
return outpoints;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFetchingMyChannels = createSelector(
|
||||
_selectState,
|
||||
state => !!state.fetchingMyChannels
|
||||
);
|
||||
|
||||
export const selectMyChannelClaims = createSelector(
|
||||
_selectState,
|
||||
selectClaimsById,
|
||||
(state, byId) => {
|
||||
const ids = state.myChannelClaims || [];
|
||||
const claims = [];
|
||||
|
||||
ids.forEach(id => claims.push(byId[id]));
|
||||
|
||||
return claims;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -2,7 +2,8 @@ import lbry from "lbry";
|
|||
import { createSelector } from "reselect";
|
||||
import {
|
||||
selectClaimsByUri,
|
||||
selectClaimListMineIsPending,
|
||||
selectIsFetchingClaimListMine,
|
||||
selectMyClaims,
|
||||
selectMyClaimsOutpoints,
|
||||
} from "selectors/claims";
|
||||
|
||||
|
@ -13,16 +14,16 @@ export const selectFileInfosByOutpoint = createSelector(
|
|||
state => state.byOutpoint || {}
|
||||
);
|
||||
|
||||
export const selectFileListIsPending = createSelector(
|
||||
export const selectIsFetchingFileList = createSelector(
|
||||
_selectState,
|
||||
state => state.isFileListPending
|
||||
state => !!state.isFetchingFileList
|
||||
);
|
||||
|
||||
export const selectFileListDownloadedOrPublishedIsPending = createSelector(
|
||||
selectFileListIsPending,
|
||||
selectClaimListMineIsPending,
|
||||
(isFileListPending, isClaimListMinePending) =>
|
||||
isFileListPending || isClaimListMinePending
|
||||
export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
|
||||
selectIsFetchingFileList,
|
||||
selectIsFetchingClaimListMine,
|
||||
(isFetchingFileList, isFetchingClaimListMine) =>
|
||||
isFetchingFileList || isFetchingClaimListMine
|
||||
);
|
||||
|
||||
export const selectFileInfoForUri = (state, props) => {
|
||||
|
@ -38,14 +39,18 @@ export const makeSelectFileInfoForUri = () => {
|
|||
return createSelector(selectFileInfoForUri, fileInfo => fileInfo);
|
||||
};
|
||||
|
||||
export const selectUrisDownloading = createSelector(
|
||||
export const selectDownloadingByOutpoint = createSelector(
|
||||
_selectState,
|
||||
state => state.urisDownloading || {}
|
||||
state => state.downloadingByOutpoint || {}
|
||||
);
|
||||
|
||||
const selectDownloadingForUri = (state, props) => {
|
||||
const byUri = selectUrisDownloading(state);
|
||||
return byUri[props.uri];
|
||||
const byOutpoint = selectDownloadingByOutpoint(state);
|
||||
const fileInfo = selectFileInfoForUri(state, props);
|
||||
|
||||
if (!fileInfo) return false;
|
||||
|
||||
return byOutpoint[fileInfo.outpoint];
|
||||
};
|
||||
|
||||
export const makeSelectDownloadingForUri = () => {
|
||||
|
@ -69,42 +74,38 @@ export const makeSelectLoadingForUri = () => {
|
|||
return createSelector(selectLoadingForUri, loading => !!loading);
|
||||
};
|
||||
|
||||
export const selectFileInfosDownloaded = createSelector(
|
||||
selectFileInfosByOutpoint,
|
||||
selectMyClaimsOutpoints,
|
||||
(byOutpoint, myClaimOutpoints) => {
|
||||
const fileInfoList = [];
|
||||
Object.values(byOutpoint).forEach(fileInfo => {
|
||||
if (
|
||||
fileInfo &&
|
||||
myClaimOutpoints.indexOf(fileInfo.outpoint) === -1 &&
|
||||
(fileInfo.completed || fileInfo.written_bytes)
|
||||
) {
|
||||
fileInfoList.push(fileInfo);
|
||||
}
|
||||
});
|
||||
return fileInfoList;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFileInfosPendingPublish = createSelector(
|
||||
_selectState,
|
||||
state => {
|
||||
return lbry.getPendingPublishes();
|
||||
state => Object.values(state.pendingByOutpoint || {})
|
||||
);
|
||||
|
||||
export const selectFileInfosDownloaded = createSelector(
|
||||
selectFileInfosByOutpoint,
|
||||
selectMyClaims,
|
||||
(byOutpoint, myClaims) => {
|
||||
return Object.values(byOutpoint).filter(fileInfo => {
|
||||
const myClaimIds = myClaims.map(claim => claim.claim_id);
|
||||
|
||||
return (
|
||||
fileInfo &&
|
||||
myClaimIds.indexOf(fileInfo.claim_id) === -1 &&
|
||||
(fileInfo.completed || fileInfo.written_bytes)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFileInfosPublished = createSelector(
|
||||
selectFileInfosByOutpoint,
|
||||
selectFileInfosPendingPublish,
|
||||
selectMyClaimsOutpoints,
|
||||
(byOutpoint, pendingFileInfos, outpoints) => {
|
||||
selectFileInfosPendingPublish,
|
||||
(byOutpoint, outpoints, pendingPublish) => {
|
||||
const fileInfos = [];
|
||||
outpoints.forEach(outpoint => {
|
||||
const fileInfo = byOutpoint[outpoint];
|
||||
if (fileInfo) fileInfos.push(fileInfo);
|
||||
});
|
||||
return [...fileInfos, ...pendingFileInfos];
|
||||
return [...fileInfos, ...pendingPublish];
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -133,20 +134,19 @@ export const selectFileInfosByUri = createSelector(
|
|||
if (fileInfo) fileInfos[uri] = fileInfo;
|
||||
}
|
||||
});
|
||||
|
||||
return fileInfos;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectDownloadingFileInfos = createSelector(
|
||||
selectUrisDownloading,
|
||||
selectFileInfosByUri,
|
||||
(urisDownloading, byUri) => {
|
||||
const uris = Object.keys(urisDownloading);
|
||||
selectDownloadingByOutpoint,
|
||||
selectFileInfosByOutpoint,
|
||||
(downloadingByOutpoint, fileInfosByOutpoint) => {
|
||||
const outpoints = Object.keys(downloadingByOutpoint);
|
||||
const fileInfos = [];
|
||||
|
||||
uris.forEach(uri => {
|
||||
const fileInfo = byUri[uri];
|
||||
outpoints.forEach(outpoint => {
|
||||
const fileInfo = fileInfosByOutpoint[outpoint];
|
||||
|
||||
if (fileInfo) fileInfos.push(fileInfo);
|
||||
});
|
||||
|
|
|
@ -12,25 +12,16 @@ export const selectUserIsPending = createSelector(
|
|||
state => state.userIsPending
|
||||
);
|
||||
|
||||
export const selectUser = createSelector(
|
||||
_selectState,
|
||||
state => state.user || {}
|
||||
);
|
||||
export const selectUser = createSelector(_selectState, state => state.user);
|
||||
|
||||
export const selectEmailToVerify = createSelector(
|
||||
_selectState,
|
||||
state => state.emailToVerify
|
||||
);
|
||||
|
||||
export const selectUserHasEmail = createSelector(
|
||||
export const selectUserEmail = createSelector(
|
||||
selectUser,
|
||||
selectEmailToVerify,
|
||||
(user, email) => (user && user.has_email) || !!email
|
||||
);
|
||||
|
||||
export const selectUserIsRewardEligible = createSelector(
|
||||
selectUser,
|
||||
user => user && user.is_reward_eligible
|
||||
user => (user ? user.primary_email : null)
|
||||
);
|
||||
|
||||
export const selectUserIsRewardApproved = createSelector(
|
||||
|
@ -63,16 +54,22 @@ export const selectEmailVerifyErrorMessage = createSelector(
|
|||
state => state.emailVerifyErrorMessage
|
||||
);
|
||||
|
||||
export const selectUserIsVerificationCandidate = createSelector(
|
||||
selectUser,
|
||||
user => user && !user.has_verified_email
|
||||
export const selectIdentityVerifyIsPending = createSelector(
|
||||
_selectState,
|
||||
state => state.identityVerifyIsPending
|
||||
);
|
||||
|
||||
export const selectUserIsAuthRequested = createSelector(
|
||||
selectEmailNewDeclined,
|
||||
selectAuthenticationIsPending,
|
||||
selectUserIsVerificationCandidate,
|
||||
selectUserHasEmail,
|
||||
(isEmailDeclined, isPending, isVerificationCandidate, hasEmail) =>
|
||||
!isEmailDeclined && (isPending || !hasEmail || isVerificationCandidate)
|
||||
export const selectIdentityVerifyErrorMessage = createSelector(
|
||||
_selectState,
|
||||
state => state.identityVerifyErrorMessage
|
||||
);
|
||||
|
||||
export const selectUserIsVerificationCandidate = createSelector(
|
||||
selectUser,
|
||||
user => user && (!user.has_verified_email || !user.is_identity_verified)
|
||||
);
|
||||
|
||||
export const selectAccessToken = createSelector(
|
||||
_selectState,
|
||||
state => state.accessToken
|
||||
);
|
||||
|
|
|
@ -86,18 +86,14 @@ const createStoreWithMiddleware = redux.compose(
|
|||
|
||||
const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
|
||||
const compressor = createCompressor();
|
||||
const saveClaimsFilter = createFilter("claims", [
|
||||
"byId",
|
||||
"claimsByUri",
|
||||
"myClaims",
|
||||
]);
|
||||
const saveClaimsFilter = createFilter("claims", ["byId", "claimsByUri"]);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ["claims"],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, compressor],
|
||||
debounce: 1000,
|
||||
debounce: 10000,
|
||||
storage: localForage,
|
||||
};
|
||||
window.cacheStore = persistStore(reduxStore, persistOptions);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const { remote } = require("electron");
|
||||
const application = remote.app;
|
||||
const win = remote.BrowserWindow.getFocusedWindow();
|
||||
const win = remote.getCurrentWindow();
|
||||
|
||||
const setProgressBar = progress => {
|
||||
win.setProgressBar(progress);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const { remote } = require("electron");
|
||||
|
||||
/**
|
||||
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
|
||||
* is not set yet.
|
||||
|
@ -29,3 +31,7 @@ export function getSession(key, fallback = undefined) {
|
|||
export function setSession(key, value) {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
export function formatCredits(amount, precision) {
|
||||
return amount.toFixed(precision || 1).replace(/\.?0+$/, "");
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue