Merge branch 'master' into hackrush

This commit is contained in:
Jeremy Kauffman 2017-07-25 10:10:05 -04:00 committed by GitHub
commit deefd9c824
108 changed files with 4334 additions and 1889 deletions

1
.gitignore vendored
View file

@ -26,3 +26,4 @@ build/daemon.zip
.vimrc
package-lock.json
ui/yarn.lock

View file

@ -8,24 +8,40 @@ 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

View file

@ -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

View file

@ -18,5 +18,8 @@
},
"devDependencies": {
"electron-rebuild": "^1.5.11"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.14.1"
}
}

View file

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

View file

@ -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
python build\upload_assets.py

View file

@ -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

Binary file not shown.

View file

@ -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();
};
}

View file

@ -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);
});
};
}

View file

@ -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,
data: {
claimId: fileInfo.claim_id,
},
});
};
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());
};
}

View file

@ -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));
}
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
};
}

View file

@ -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);
};
}

View file

@ -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";

View file

@ -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 => ({
modal: selectCurrentModal(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);

View file

@ -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>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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;

View file

@ -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 || ""}
/>
);
}

View file

@ -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")}

View file

@ -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>

View file

@ -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}

View file

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

View file

@ -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}

View file

@ -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,7 +208,9 @@ 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();
}

View file

@ -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"),
});

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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;

View 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);

View 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;

View file

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

View file

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

View file

@ -1,8 +1,9 @@
import React from "react";
import { 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));
},
});

View file

@ -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() {

View file

@ -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);

View file

@ -4,78 +4,48 @@ 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">
<section>
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
<p>
{__(
"Using LBRY is like dating a centaur. Totally normal up top, and"
)}
{" "}<em>{__("way different")}</em> {__("underneath.")}
</p>
<p>{__("Up top, LBRY is similar to popular media sites.")}</p>
<p>
{__(
"Below, LBRY is controlled by users -- you -- via blockchain and decentralization."
)}
</p>
<p>
{__("Thank you for making content freedom possible!")}
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}
</p>
<div className="text-center">
{isRewardApproved
? <RewardLink reward_type="new_user" button="primary" />
: <Link
button="primary"
onClick={closeModal}
label={__("Continue")}
/>}
</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>;
return (
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section>
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
<p>
{__(
"Using LBRY is like dating a centaur. Totally normal up top, and"
)}
{" "}<em>{__("way different")}</em> {__("underneath.")}
</p>
<p>{__("Up top, LBRY is similar to popular media sites.")}</p>
<p>
{__(
"Below, LBRY is controlled by users -- you -- via blockchain and decentralization."
)}
</p>
<p>
{__("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" />}
{!isRewardApproved &&
<Link
button="primary"
onClick={verifyAccount}
label={__("Get Welcome Credits")}
/>}
<Link button="alt" onClick={closeModal} label={__("Skip")} />
</div>
</section>
</Modal>
);
}
}
export default WelcomeModal;
export default ModalWelcome;

View file

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

View file

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

View 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;

View 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;

View file

@ -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),

View file

@ -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
button={button ? button : "alt"}
disabled={isPending}
label={isPending ? __("Claiming...") : __("Claim Reward")}
onClick={() => {
claimReward(reward);
}}
/>}
<Link
button={button ? button : "alt"}
disabled={isPending}
label={isPending ? __("Claiming...") : __("Claim Reward")}
onClick={() => {
claimReward(reward);
}}
/>
{errorMessage
? <Modal
isOpen={true}

View file

@ -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} />,
});
};

View file

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

View file

@ -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 (
<LoadScreen
message={this.state.message}
details={this.state.details}
isWarning={this.state.isLagging}
/>
<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>
);
}
}

View file

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

View file

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

View file

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

View file

@ -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>

View 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);

View 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;

View file

@ -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"}

View file

@ -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 {
}
};
player.append(
this.file(),
container,
{ autoplay: false, controls: true },
renderMediaCallback.bind(this)
);
// 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,
});
}
}
}

View file

@ -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 &&

View file

@ -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";

View file

@ -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";

View file

@ -223,55 +223,47 @@ 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,
channel_name: params.channel_name,
});
publishedCallback(true);
}
if (publishedCallback) {
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
publishedCallback(true);
}
if (fileListedCallback) {
const { name, channel_name } = params;
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
fileListedCallback(true);
}
}, 2000);
if (fileListedCallback) {
const { name, channel_name } = params;
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
fileListedCallback(true);
}
},
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
? lbry.defaultClientSettings[setting]
: JSON.parse(localStorageVal);
outSettings[setting] = localStorageVal === null
? lbry.defaultClientSettings[setting]
: JSON.parse(localStorageVal);
}
return outSettings;
};
@ -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;
};

View file

@ -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,
});

View file

@ -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
);

View file

@ -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
View file

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

89
ui/js/page/auth/view.jsx Normal file
View 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;

View file

@ -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

View file

@ -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);

View file

@ -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 = (

View file

@ -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()),

View file

@ -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 = (

View file

@ -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),
});

View file

@ -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

View file

@ -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);

View file

@ -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")} />}

View file

@ -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);

View file

@ -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,
};
}
_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>
);
}
}
const PublishPage = props => {
return <PublishForm {...props} />;
};
export default PublishPage;

View file

@ -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 => ({
fetching: selectFetchingRewards(state),
rewards: selectRewards(state),
hasEmail: selectUserHasEmail(state),
isEligible: selectUserIsRewardEligible(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
const select = (state, props) => {
const selectReward = makeSelectRewardByType();
return {
fetching: selectFetchingRewards(state),
rewards: selectRewards(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);

View file

@ -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 />
{rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
)}
</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 =>
<RewardTile key={reward.reward_type} reward={reward} />
);
} 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">
{content}
</div>
</section>
: content}
{cardHeader && <section className="card">{cardHeader}</section>}
{content}
</main>
);
}

View file

@ -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 = {
@ -89,23 +80,32 @@ class SettingsPage extends React.PureComponent {
onKeyFeeDisableChange(isDisabled) {
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,6 +174,96 @@ class SettingsPage extends React.PureComponent {
</div>
</section>
*/
/*
<section className="card">
<div className="card__content">
<h3>{__("Bandwidth Limits")}</h3>
</div>
<div className="card__content">
<div className="form-row__label-row">
<div className="form-field__label">{__("Max Upload")}</div>
</div>
<FormRow
type="radio"
name="max_upload_pref"
onChange={() => {
this.onMaxUploadPrefChange(false);
}}
defaultChecked={!this.state.isMaxUpload}
label={__("Unlimited")}
/>
<div className="form-row">
<FormField
type="radio"
name="max_upload_pref"
onChange={() => {
this.onMaxUploadPrefChange(true);
}}
defaultChecked={this.state.isMaxUpload}
label={
this.state.isMaxUpload ? __("Up to") : __("Choose limit...")
}
/>
{this.state.isMaxUpload
? <FormField
type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_upload}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxUploadFieldChange.bind(this)}
/>
: ""}
{this.state.isMaxUpload
? <span className="form-field__label">MB/s</span>
: ""}
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<div className="form-field__label">{__("Max Download")}</div>
</div>
<FormRow
label={__("Unlimited")}
type="radio"
name="max_download_pref"
onChange={() => {
this.onMaxDownloadPrefChange(false);
}}
defaultChecked={!this.state.isMaxDownload}
/>
<div className="form-row">
<FormField
type="radio"
name="max_download_pref"
onChange={() => {
this.onMaxDownloadPrefChange(true);
}}
defaultChecked={this.state.isMaxDownload}
label={
this.state.isMaxDownload ? __("Up to") : __("Choose limit...")
}
/>
{this.state.isMaxDownload
? <FormField
type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange.bind(this)}
/>
: ""}
{this.state.isMaxDownload
? <span className="form-field__label">MB/s</span>
: ""}
</div>
</div>
</section>
*/
return (
<main className="main--single-column">
<SubHeader />
@ -191,98 +281,9 @@ class SettingsPage extends React.PureComponent {
/>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__("Bandwidth Limits")}</h3>
</div>
<div className="card__content">
<div className="form-row__label-row">
<div className="form-field__label">{__("Max Upload")}</div>
</div>
<FormRow
type="radio"
name="max_upload_pref"
onChange={() => {
this.onMaxUploadPrefChange(false);
}}
defaultChecked={!this.state.isMaxUpload}
label={__("Unlimited")}
/>
<div className="form-row">
<FormField
type="radio"
name="max_upload_pref"
onChange={() => {
this.onMaxUploadPrefChange(true);
}}
defaultChecked={this.state.isMaxUpload}
label={
this.state.isMaxUpload ? __("Up to") : __("Choose limit...")
}
/>
{this.state.isMaxUpload
? <FormField
type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_upload}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxUploadFieldChange.bind(this)}
/>
: ""}
{this.state.isMaxUpload
? <span className="form-field__label">MB/s</span>
: ""}
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<div className="form-field__label">{__("Max Download")}</div>
</div>
<FormRow
label={__("Unlimited")}
type="radio"
name="max_download_pref"
onChange={() => {
this.onMaxDownloadPrefChange(false);
}}
defaultChecked={!this.state.isMaxDownload}
/>
<div className="form-row">
<FormField
type="radio"
name="max_download_pref"
onChange={() => {
this.onMaxDownloadPrefChange(true);
}}
defaultChecked={this.state.isMaxDownload}
label={
this.state.isMaxDownload ? __("Up to") : __("Choose limit...")
}
/>
{this.state.isMaxDownload
? <FormField
type="number"
min="0"
step=".5"
defaultValue={daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange.bind(this)}
/>
: ""}
{this.state.isMaxDownload
? <span className="form-field__label">MB/s</span>
: ""}
</div>
</div>
</section>
<section className="card">
<div className="card__content">
<h3>{__("Key Fee")}</h3>
<h3>{__("Max Purchase Price")}</h3>
</div>
<div className="card__content">
<div className="form-row__label-row">

View file

@ -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">

View file

@ -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,

View file

@ -15,7 +15,13 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
byUri[uri] = claim.claim_id;
} else if (claim === undefined && certificate !== undefined) {
byId[certificate.claim_id] = certificate;
byUri[uri] = certificate.claim_id;
// 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;
}
@ -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);

View file

@ -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,
});
};

View file

@ -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);

View file

@ -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];
}

View file

@ -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
);

View file

@ -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;
}
);

View file

@ -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);
});

View file

@ -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
);

View file

@ -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);

View file

@ -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);

View file

@ -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