reduxify pull request 325
This commit is contained in:
commit
d591886df5
144 changed files with 5872 additions and 2358 deletions
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.13.0
|
||||
current_version = 0.14.3
|
||||
commit = True
|
||||
tag = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,6 +14,7 @@
|
|||
/app/node_modules
|
||||
/build/venv
|
||||
/lbry-app-venv
|
||||
/lbry-app
|
||||
/lbry-venv
|
||||
/daemon/build
|
||||
/daemon/venv
|
||||
|
@ -27,3 +28,4 @@ build/daemon.zip
|
|||
.vimrc
|
||||
|
||||
package-lock.json
|
||||
ui/yarn.lock
|
||||
|
|
94
CHANGELOG.md
94
CHANGELOG.md
|
@ -8,15 +8,19 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
*
|
||||
* Added a new component, `FormFieldPrice` which is now used in Publish and Settings
|
||||
*
|
||||
|
||||
### Changed
|
||||
*
|
||||
* Some form field refactoring as we progress towards form sanity.
|
||||
* When an "Open" button is clicked on a show page, if the file fails to open, the app will try to open the file's folder.
|
||||
*
|
||||
|
||||
### Fixed
|
||||
*
|
||||
* Tiles will no longer be blurry on hover (Windows only bug)
|
||||
* Removed placeholder values from price selection form fields, which was causing confusion that these were real values (#426)
|
||||
* Fixed showing "other currency" help tip in publish form, which was caused due to not "setting" state for price
|
||||
* Now using setState in formFieldPrice
|
||||
*
|
||||
|
||||
### Deprecated
|
||||
|
@ -24,8 +28,90 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
*
|
||||
|
||||
### Removed
|
||||
* Removed the label "Max Purchase Price" from settings page. It was redundant.
|
||||
* Unused old files from previous commit(9c3d633)
|
||||
*
|
||||
*
|
||||
|
||||
## [0.14.3] - 2017-08-03
|
||||
|
||||
### Added
|
||||
* Add tooltips to controls in header
|
||||
* New flow for rewards authentication failure
|
||||
|
||||
|
||||
### Changed
|
||||
* Make it clearer how to skip identity verification and add link to FAQ
|
||||
* Reward-eligible content icon is now a rocket ship :D :D :D
|
||||
* Change install description shown by operating systems
|
||||
* Improved flow for when app is run with incompatible daemon
|
||||
|
||||
|
||||
### Fixed
|
||||
* Corrected improper pluralization on loading screen
|
||||
|
||||
|
||||
|
||||
## [0.14.2] - 2017-07-30
|
||||
|
||||
### Added
|
||||
* Replaced horizontal scrollbars with scroll arrows
|
||||
* Featured weekly reward content shows with an orange star
|
||||
* Added pagination to channel pages
|
||||
|
||||
|
||||
### Fixed
|
||||
* Fixed requirement to double click play button on many videos
|
||||
* Fixed errors from calls to `get` not bubbling correctly
|
||||
* Fixed some corner-case flows that could break file pages
|
||||
|
||||
|
||||
|
||||
## [0.14.1] - 2017-07-28
|
||||
|
||||
### Fixed
|
||||
* Fixed upgrade file path missing file name
|
||||
|
||||
|
||||
|
||||
## [0.14.0] - 2017-07-28
|
||||
|
||||
### Added
|
||||
* Identity verification for new reward participants
|
||||
* Support rich markup in publishing descriptions and show pages.
|
||||
* Release past publishing claims (and recover LBC) via the UI
|
||||
* Added transition to card hovers to smooth animation
|
||||
* Use randomly colored tiles when image is missing from metadata
|
||||
* Added a loading message to file actions
|
||||
* URL is auto suggested in Publish Page
|
||||
|
||||
|
||||
### Changed
|
||||
* Publishing revamped. Editing claims is much easier.
|
||||
* Daemon updated from v0.13.1 to [v0.14.2](https://github.com/lbryio/lbry/releases/tag/v0.14.2)
|
||||
* Publish page now use `claim_list` rather than `file_list`
|
||||
|
||||
|
||||
### Removed
|
||||
* Removed bandwidth caps from settings, because the daemon was not respecting them anyway.
|
||||
|
||||
|
||||
### Fixed
|
||||
* Fixed bug with download notice when switching window focus
|
||||
* Fixed newly published files appearing twice
|
||||
* Fixed unconfirmed published files missing channel name
|
||||
* Fixed old files from updated published claims appearing in downloaded list
|
||||
* Fixed inappropriate text showing on searches
|
||||
* Stop discover page from pushing jumping vertically while loading
|
||||
* Restored feedback on claim amounts
|
||||
* Fixed hiding price input when Free is checked on publish form
|
||||
* Fixed hiding new identity fields on publish form
|
||||
* Fixed files on downloaded tab not showing download progress
|
||||
* Fixed downloading files that are deleted not being removed from the downloading list
|
||||
* Fixed download progress bar not being cleared when a downloading file is deleted
|
||||
* Fixed refresh regression after adding scroll position to history state
|
||||
* Fixed app not monitoring download progress on files in progress between restarts
|
||||
|
||||
|
||||
|
||||
## [0.13.0] - 2017-06-30
|
||||
|
||||
|
|
11
app/main.js
11
app/main.js
|
@ -27,7 +27,7 @@ const {version: localVersion} = require(app.getAppPath() + '/package.json');
|
|||
|
||||
const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
|
||||
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
|
||||
|
||||
const DAEMON_PATH = process.env.LBRY_DAEMON || path.join(__dirname, 'dist', 'lbrynet-daemon');
|
||||
|
||||
let client = jayson.client.http({
|
||||
host: 'localhost',
|
||||
|
@ -207,13 +207,8 @@ function handleDaemonSubprocessExited() {
|
|||
function launchDaemon() {
|
||||
assert(!daemonSubprocess, 'Tried to launch daemon twice');
|
||||
|
||||
if (process.env.LBRY_DAEMON) {
|
||||
executable = process.env.LBRY_DAEMON;
|
||||
} else {
|
||||
executable = path.join(__dirname, 'dist', 'lbrynet-daemon');
|
||||
}
|
||||
console.log('Launching daemon:', executable)
|
||||
daemonSubprocess = child_process.spawn(executable)
|
||||
console.log('Launching daemon:', DAEMON_PATH)
|
||||
daemonSubprocess = child_process.spawn(DAEMON_PATH)
|
||||
// Need to handle the data event instead of attaching to
|
||||
// process.stdout because the latter doesn't work. I believe on
|
||||
// windows it buffers stdout and we don't get any meaningful output
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "LBRY",
|
||||
"version": "0.13.0",
|
||||
"version": "0.14.3",
|
||||
"main": "main.js",
|
||||
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"author": {
|
||||
"name": "LBRY Inc.",
|
||||
"email": "hello@lbry.io"
|
||||
|
@ -18,5 +18,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"electron-rebuild": "^1.5.11"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.14.2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
https://github.com/lbryio/lbry/releases/download/v0.13.1/lbrynet-daemon-v0.13.1-OSNAME.zip
|
||||
https://github.com/lbryio/lbry/releases/download/v0.14.2/lbrynet-daemon-v0.14.2-OSNAME.zip
|
|
@ -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
|
||||
|
|
|
@ -79,11 +79,14 @@ if $OSX; then
|
|||
else
|
||||
OSNAME="linux"
|
||||
fi
|
||||
DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")"
|
||||
DAEMON_VER=$(node -e "console.log(require(\"$ROOT/app/package.json\").lbrySettings.lbrynetDaemonVersion)")
|
||||
DAEMON_URL="https://github.com/lbryio/lbry/releases/download/v${DAEMON_VER}/lbrynet-daemon-v${DAEMON_VER}-${OSNAME}.zip"
|
||||
wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip"
|
||||
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/"
|
||||
rm "$BUILD_DIR/daemon.zip"
|
||||
|
||||
|
||||
|
||||
###################
|
||||
# Build the app #
|
||||
###################
|
||||
|
|
Binary file not shown.
BIN
build/lbry3.pfx.enc
Normal file
BIN
build/lbry3.pfx.enc
Normal file
Binary file not shown.
|
@ -57,8 +57,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"devtron": "^1.4.0",
|
||||
"electron": "^1.4.15",
|
||||
"electron": "^1.7.5",
|
||||
"electron-builder": "^11.7.0",
|
||||
"electron-debug": "^1.1.0"
|
||||
"electron-debug": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,31 +13,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 { lbrySettings: config } = require("../../../app/package.json");
|
||||
|
||||
const queryStringFromParams = params => {
|
||||
return Object.keys(params).map(key => `${key}=${params[key]}`).join("&");
|
||||
};
|
||||
|
||||
export function doNavigate(path, params = {}) {
|
||||
export function doNavigate(path, params = {}, options = {}) {
|
||||
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 +60,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 +78,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 +150,10 @@ 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
|
||||
),
|
||||
upgradeFilename = selectUpgradeFilename(state);
|
||||
|
||||
let options = {
|
||||
onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
|
||||
|
@ -202,11 +237,21 @@ export function doCheckUpgradeAvailable() {
|
|||
};
|
||||
}
|
||||
|
||||
export function doCheckDaemonVersion() {
|
||||
return function(dispatch, getState) {
|
||||
lbry.version().then(({ lbrynet_version }) => {
|
||||
dispatch({
|
||||
type: config.lbrynetDaemonVersion == lbrynet_version
|
||||
? types.DAEMON_VERSION_MATCH
|
||||
: types.DAEMON_VERSION_MISMATCH,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doAlertError(errorList) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
console.log("do alert error");
|
||||
console.log(errorList);
|
||||
dispatch({
|
||||
type: types.OPEN_MODAL,
|
||||
data: {
|
||||
|
@ -219,6 +264,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,
|
||||
|
@ -256,3 +304,9 @@ export function doChangeLanguage(newLanguage) {
|
|||
data: { newLanguage: newLanguage },
|
||||
};
|
||||
}
|
||||
|
||||
export function doQuit() {
|
||||
return function(dispatch, getState) {
|
||||
remote.app.quit();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,17 +5,18 @@ 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";
|
||||
import { doOpenModal } from "actions/app";
|
||||
import { doAlertError, doOpenModal } from "actions/app";
|
||||
import { doClaimEligiblePurchaseRewards } from "actions/rewards";
|
||||
import { selectBadgeNumber } from "selectors/app";
|
||||
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");
|
||||
|
||||
|
@ -132,6 +133,34 @@ export function doFetchFeaturedUris() {
|
|||
};
|
||||
}
|
||||
|
||||
export function doFetchRewardedContent() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
|
||||
const success = nameToClaimId => {
|
||||
dispatch({
|
||||
type: types.FETCH_REWARD_CONTENT_COMPLETED,
|
||||
data: {
|
||||
claimIds: Object.values(nameToClaimId),
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const failure = () => {
|
||||
dispatch({
|
||||
type: types.FETCH_REWARD_CONTENT_COMPLETED,
|
||||
data: {
|
||||
claimIds: [],
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
lbryio.call("reward", "list_featured").then(success, failure);
|
||||
};
|
||||
}
|
||||
|
||||
export function doUpdateLoadStatus(uri, outpoint) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
|
@ -198,24 +227,38 @@ export function doUpdateLoadStatus(uri, outpoint) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doStartDownload(uri, outpoint) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
|
||||
if (!outpoint) {
|
||||
throw new Error("outpoint is required to begin a download");
|
||||
}
|
||||
|
||||
const { downloadingByOutpoint = {} } = state.fileInfo;
|
||||
|
||||
if (downloadingByOutpoint[outpoint]) return;
|
||||
|
||||
lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
|
||||
dispatch({
|
||||
type: types.DOWNLOADING_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadFile(uri, streamInfo) {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
|
||||
lbry
|
||||
.file_list({ outpoint: streamInfo.outpoint, full_status: true })
|
||||
.then(([fileInfo]) => {
|
||||
dispatch({
|
||||
type: types.DOWNLOADING_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint: streamInfo.outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(doUpdateLoadStatus(uri, streamInfo.outpoint));
|
||||
});
|
||||
dispatch(doStartDownload(uri, streamInfo.outpoint));
|
||||
|
||||
lbryio
|
||||
.call("file", "view", {
|
||||
|
@ -240,22 +283,32 @@ export function doLoadVideo(uri) {
|
|||
},
|
||||
});
|
||||
|
||||
lbry.get({ uri }).then(streamInfo => {
|
||||
const timeout =
|
||||
streamInfo === null ||
|
||||
typeof streamInfo !== "object" ||
|
||||
streamInfo.error == "Timeout";
|
||||
lbry
|
||||
.get({ uri })
|
||||
.then(streamInfo => {
|
||||
const timeout =
|
||||
streamInfo === null ||
|
||||
typeof streamInfo !== "object" ||
|
||||
streamInfo.error == "Timeout";
|
||||
|
||||
if (timeout) {
|
||||
if (timeout) {
|
||||
dispatch({
|
||||
type: types.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
|
||||
dispatch(doOpenModal("timedOut"));
|
||||
} else {
|
||||
dispatch(doDownloadFile(uri, streamInfo));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({
|
||||
type: types.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
dispatch(doOpenModal("timedOut"));
|
||||
} else {
|
||||
dispatch(doDownloadFile(uri, streamInfo));
|
||||
}
|
||||
});
|
||||
dispatch(doAlertError(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -264,8 +317,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 +346,7 @@ export function doPurchaseUri(uri, purchaseModalName) {
|
|||
}
|
||||
|
||||
if (cost > balance) {
|
||||
dispatch(doOpenModal("notEnoughCredits"));
|
||||
dispatch(doOpenModal(modals.INSUFFICIENT_CREDITS));
|
||||
} else {
|
||||
dispatch(doOpenModal(purchaseModalName));
|
||||
}
|
||||
|
@ -301,22 +355,28 @@ export function doPurchaseUri(uri, purchaseModalName) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doFetchClaimsByChannel(uri, page = 1) {
|
||||
export function doFetchClaimsByChannel(uri, page) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch({
|
||||
type: types.FETCH_CHANNEL_CLAIMS_STARTED,
|
||||
data: { uri },
|
||||
data: { uri, page },
|
||||
});
|
||||
|
||||
lbry.claim_list_by_channel({ uri, page }).then(result => {
|
||||
const claimResult = result[uri],
|
||||
claims = claimResult ? claimResult.claims_in_channel : [];
|
||||
claims = claimResult ? claimResult.claims_in_channel : [],
|
||||
totalPages = claimResult
|
||||
? claimResult.claims_in_channel_pages
|
||||
: undefined,
|
||||
currentPage = claimResult ? claimResult.returned_page : undefined;
|
||||
|
||||
dispatch({
|
||||
type: types.FETCH_CHANNEL_CLAIMS_COMPLETED,
|
||||
data: {
|
||||
uri,
|
||||
claims: claims,
|
||||
claims,
|
||||
totalPages,
|
||||
page: currentPage,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -339,3 +399,68 @@ export function doFetchClaimListMine() {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doFetchChannelListMine() {
|
||||
return function(dispatch, getState) {
|
||||
dispatch({
|
||||
type: types.FETCH_CHANNEL_LIST_MINE_STARTED,
|
||||
});
|
||||
|
||||
const callback = channels => {
|
||||
dispatch({
|
||||
type: types.FETCH_CHANNEL_LIST_MINE_COMPLETED,
|
||||
data: { claims: channels },
|
||||
});
|
||||
};
|
||||
|
||||
lbry.channel_list_mine().then(callback);
|
||||
};
|
||||
}
|
||||
|
||||
export function doCreateChannel(name, amount) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch({
|
||||
type: types.CREATE_CHANNEL_STARTED,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry
|
||||
.channel_new({
|
||||
channel_name: name,
|
||||
amount: parseFloat(amount),
|
||||
})
|
||||
.then(
|
||||
channelClaim => {
|
||||
channelClaim.name = name;
|
||||
dispatch({
|
||||
type: types.CREATE_CHANNEL_COMPLETED,
|
||||
data: { channelClaim },
|
||||
});
|
||||
resolve(channelClaim);
|
||||
},
|
||||
err => {
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doPublish(params) {
|
||||
return function(dispatch, getState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const success = claim => {
|
||||
resolve(claim);
|
||||
|
||||
if (claim === true) dispatch(doFetchClaimListMine());
|
||||
else
|
||||
setTimeout(() => dispatch(doFetchClaimListMine()), 20000, {
|
||||
once: true,
|
||||
});
|
||||
};
|
||||
const failure = err => reject(err);
|
||||
|
||||
lbry.publishDeprecated(params, null, success, failure);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
@ -69,7 +72,10 @@ export function doFileList() {
|
|||
|
||||
export function doOpenFileInShell(fileInfo) {
|
||||
return function(dispatch, getState) {
|
||||
shell.openItem(fileInfo.download_path);
|
||||
const success = shell.openItem(fileInfo.download_path);
|
||||
if (!success) {
|
||||
dispatch(doOpenFileInFolder(fileInfo));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -102,14 +108,12 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
|||
},
|
||||
});
|
||||
|
||||
const success = () => {
|
||||
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 +125,32 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
|||
},
|
||||
});
|
||||
|
||||
dispatch(doCloseModal());
|
||||
const totalProgress = selectTotalDownloadProgress(getState());
|
||||
setProgressBar(totalProgress);
|
||||
};
|
||||
}
|
||||
|
||||
export function doDeleteFileAndGoBack(
|
||||
fileInfo,
|
||||
deleteFromComputer,
|
||||
abandonClaim
|
||||
) {
|
||||
return function(dispatch, getState) {
|
||||
const actions = [];
|
||||
actions.push(doCloseModal());
|
||||
actions.push(doHistoryBack());
|
||||
actions.push(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim));
|
||||
dispatch(batchActions(...actions));
|
||||
};
|
||||
}
|
||||
|
||||
export function doFetchFileInfosAndPublishedClaims() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState(),
|
||||
isClaimListMinePending = selectClaimListMineIsPending(state),
|
||||
isFileInfoListPending = selectFileListIsPending(state);
|
||||
isFetchingClaimListMine = selectIsFetchingClaimListMine(state),
|
||||
isFetchingFileInfo = selectIsFetchingFileList(state);
|
||||
|
||||
dispatch(doFetchClaimListMine());
|
||||
dispatch(doFileList());
|
||||
if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine());
|
||||
if (!isFetchingFileInfo) dispatch(doFileList());
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as types from "constants/action_types";
|
||||
import * as modals from "constants/modal_types";
|
||||
import lbryio from "lbryio";
|
||||
import rewards from "rewards";
|
||||
import { selectRewardsByType } from "selectors/rewards";
|
||||
|
@ -58,6 +59,12 @@ export function doClaimReward(reward, saveError = false) {
|
|||
reward,
|
||||
},
|
||||
});
|
||||
if (reward.reward_type == rewards.TYPE_NEW_USER) {
|
||||
dispatch({
|
||||
type: types.OPEN_MODAL,
|
||||
data: { modal: modals.FIRST_REWARD },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const failure = error => {
|
||||
|
@ -99,9 +106,7 @@ export function doClaimEligiblePurchaseRewards() {
|
|||
if (unclaimedType) {
|
||||
dispatch(doClaimRewardType(unclaimedType));
|
||||
}
|
||||
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
|
||||
}
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import * as types from "constants/action_types";
|
||||
import batchActions from "util/batchActions";
|
||||
import lbry from "lbry";
|
||||
import fs from "fs";
|
||||
import http from "http";
|
||||
|
||||
export function doFetchDaemonSettings() {
|
||||
return function(dispatch, getState) {
|
||||
|
@ -41,3 +44,296 @@ export function doSetClientSetting(key, value) {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function doResolveLanguage(locale) {
|
||||
const langs = {
|
||||
aa: ["Afar", "Afar"],
|
||||
ab: ["Abkhazian", "Аҧсуа"],
|
||||
af: ["Afrikaans", "Afrikaans"],
|
||||
ak: ["Akan", "Akana"],
|
||||
am: ["Amharic", "አማርኛ"],
|
||||
an: ["Aragonese", "Aragonés"],
|
||||
ar: ["Arabic", "العربية"],
|
||||
as: ["Assamese", "অসমীয়া"],
|
||||
av: ["Avar", "Авар"],
|
||||
ay: ["Aymara", "Aymar"],
|
||||
az: ["Azerbaijani", "Azərbaycanca / آذربايجان"],
|
||||
ba: ["Bashkir", "Башҡорт"],
|
||||
be: ["Belarusian", "Беларуская"],
|
||||
bg: ["Bulgarian", "Български"],
|
||||
bh: ["Bihari", "भोजपुरी"],
|
||||
bi: ["Bislama", "Bislama"],
|
||||
bm: ["Bambara", "Bamanankan"],
|
||||
bn: ["Bengali", "বাংলা"],
|
||||
bo: ["Tibetan", "བོད་ཡིག / Bod skad"],
|
||||
br: ["Breton", "Brezhoneg"],
|
||||
bs: ["Bosnian", "Bosanski"],
|
||||
ca: ["Catalan", "Català"],
|
||||
ce: ["Chechen", "Нохчийн"],
|
||||
ch: ["Chamorro", "Chamoru"],
|
||||
co: ["Corsican", "Corsu"],
|
||||
cr: ["Cree", "Nehiyaw"],
|
||||
cs: ["Czech", "Česky"],
|
||||
cu: ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"],
|
||||
cv: ["Chuvash", "Чăваш"],
|
||||
cy: ["Welsh", "Cymraeg"],
|
||||
da: ["Danish", "Dansk"],
|
||||
de: ["German", "Deutsch"],
|
||||
dv: ["Divehi", "ދިވެހިބަސް"],
|
||||
dz: ["Dzongkha", "ཇོང་ཁ"],
|
||||
ee: ["Ewe", "Ɛʋɛ"],
|
||||
el: ["Greek", "Ελληνικά"],
|
||||
en: ["English", "English"],
|
||||
eo: ["Esperanto", "Esperanto"],
|
||||
es: ["Spanish", "Español"],
|
||||
et: ["Estonian", "Eesti"],
|
||||
eu: ["Basque", "Euskara"],
|
||||
fa: ["Persian", "فارسی"],
|
||||
ff: ["Peul", "Fulfulde"],
|
||||
fi: ["Finnish", "Suomi"],
|
||||
fj: ["Fijian", "Na Vosa Vakaviti"],
|
||||
fo: ["Faroese", "Føroyskt"],
|
||||
fr: ["French", "Français"],
|
||||
fy: ["West Frisian", "Frysk"],
|
||||
ga: ["Irish", "Gaeilge"],
|
||||
gd: ["Scottish Gaelic", "Gàidhlig"],
|
||||
gl: ["Galician", "Galego"],
|
||||
gn: ["Guarani", "Avañe'ẽ"],
|
||||
gu: ["Gujarati", "ગુજરાતી"],
|
||||
gv: ["Manx", "Gaelg"],
|
||||
ha: ["Hausa", "هَوُسَ"],
|
||||
he: ["Hebrew", "עברית"],
|
||||
hi: ["Hindi", "हिन्दी"],
|
||||
ho: ["Hiri Motu", "Hiri Motu"],
|
||||
hr: ["Croatian", "Hrvatski"],
|
||||
ht: ["Haitian", "Krèyol ayisyen"],
|
||||
hu: ["Hungarian", "Magyar"],
|
||||
hy: ["Armenian", "Հայերեն"],
|
||||
hz: ["Herero", "Otsiherero"],
|
||||
ia: ["Interlingua", "Interlingua"],
|
||||
id: ["Indonesian", "Bahasa Indonesia"],
|
||||
ie: ["Interlingue", "Interlingue"],
|
||||
ig: ["Igbo", "Igbo"],
|
||||
ii: ["Sichuan Yi", "ꆇꉙ / 四川彝语"],
|
||||
ik: ["Inupiak", "Iñupiak"],
|
||||
io: ["Ido", "Ido"],
|
||||
is: ["Icelandic", "Íslenska"],
|
||||
it: ["Italian", "Italiano"],
|
||||
iu: ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"],
|
||||
ja: ["Japanese", "日本語"],
|
||||
jv: ["Javanese", "Basa Jawa"],
|
||||
ka: ["Georgian", "ქართული"],
|
||||
kg: ["Kongo", "KiKongo"],
|
||||
ki: ["Kikuyu", "Gĩkũyũ"],
|
||||
kj: ["Kuanyama", "Kuanyama"],
|
||||
kk: ["Kazakh", "Қазақша"],
|
||||
kl: ["Greenlandic", "Kalaallisut"],
|
||||
km: ["Cambodian", "ភាសាខ្មែរ"],
|
||||
kn: ["Kannada", "ಕನ್ನಡ"],
|
||||
ko: ["Korean", "한국어"],
|
||||
kr: ["Kanuri", "Kanuri"],
|
||||
ks: ["Kashmiri", "कश्मीरी / كشميري"],
|
||||
ku: ["Kurdish", "Kurdî / كوردی"],
|
||||
kv: ["Komi", "Коми"],
|
||||
kw: ["Cornish", "Kernewek"],
|
||||
ky: ["Kirghiz", "Kırgızca / Кыргызча"],
|
||||
la: ["Latin", "Latina"],
|
||||
lb: ["Luxembourgish", "Lëtzebuergesch"],
|
||||
lg: ["Ganda", "Luganda"],
|
||||
li: ["Limburgian", "Limburgs"],
|
||||
ln: ["Lingala", "Lingála"],
|
||||
lo: ["Laotian", "ລາວ / Pha xa lao"],
|
||||
lt: ["Lithuanian", "Lietuvių"],
|
||||
lv: ["Latvian", "Latviešu"],
|
||||
mg: ["Malagasy", "Malagasy"],
|
||||
mh: ["Marshallese", "Kajin Majel / Ebon"],
|
||||
mi: ["Maori", "Māori"],
|
||||
mk: ["Macedonian", "Македонски"],
|
||||
ml: ["Malayalam", "മലയാളം"],
|
||||
mn: ["Mongolian", "Монгол"],
|
||||
mo: ["Moldovan", "Moldovenească"],
|
||||
mr: ["Marathi", "मराठी"],
|
||||
ms: ["Malay", "Bahasa Melayu"],
|
||||
mt: ["Maltese", "bil-Malti"],
|
||||
my: ["Burmese", "Myanmasa"],
|
||||
na: ["Nauruan", "Dorerin Naoero"],
|
||||
nd: ["North Ndebele", "Sindebele"],
|
||||
ne: ["Nepali", "नेपाली"],
|
||||
ng: ["Ndonga", "Oshiwambo"],
|
||||
nl: ["Dutch", "Nederlands"],
|
||||
nn: ["Norwegian Nynorsk", "Norsk (nynorsk)"],
|
||||
no: ["Norwegian", "Norsk (bokmål / riksmål)"],
|
||||
nr: ["South Ndebele", "isiNdebele"],
|
||||
nv: ["Navajo", "Diné bizaad"],
|
||||
ny: ["Chichewa", "Chi-Chewa"],
|
||||
oc: ["Occitan", "Occitan"],
|
||||
oj: ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
|
||||
om: ["Oromo", "Oromoo"],
|
||||
or: ["Oriya", "ଓଡ଼ିଆ"],
|
||||
os: ["Ossetian / Ossetic", "Иронау"],
|
||||
pa: ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
|
||||
pi: ["Pali", "Pāli / पाऴि"],
|
||||
pl: ["Polish", "Polski"],
|
||||
ps: ["Pashto", "پښتو"],
|
||||
pt: ["Portuguese", "Português"],
|
||||
qu: ["Quechua", "Runa Simi"],
|
||||
rm: ["Raeto Romance", "Rumantsch"],
|
||||
rn: ["Kirundi", "Kirundi"],
|
||||
ro: ["Romanian", "Română"],
|
||||
ru: ["Russian", "Русский"],
|
||||
rw: ["Rwandi", "Kinyarwandi"],
|
||||
sa: ["Sanskrit", "संस्कृतम्"],
|
||||
sc: ["Sardinian", "Sardu"],
|
||||
sd: ["Sindhi", "सिनधि"],
|
||||
se: ["Northern Sami", "Sámegiella"],
|
||||
sg: ["Sango", "Sängö"],
|
||||
sh: ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"],
|
||||
si: ["Sinhalese", "සිංහල"],
|
||||
sk: ["Slovak", "Slovenčina"],
|
||||
sl: ["Slovenian", "Slovenščina"],
|
||||
sm: ["Samoan", "Gagana Samoa"],
|
||||
sn: ["Shona", "chiShona"],
|
||||
so: ["Somalia", "Soomaaliga"],
|
||||
sq: ["Albanian", "Shqip"],
|
||||
sr: ["Serbian", "Српски"],
|
||||
ss: ["Swati", "SiSwati"],
|
||||
st: ["Southern Sotho", "Sesotho"],
|
||||
su: ["Sundanese", "Basa Sunda"],
|
||||
sv: ["Swedish", "Svenska"],
|
||||
sw: ["Swahili", "Kiswahili"],
|
||||
ta: ["Tamil", "தமிழ்"],
|
||||
te: ["Telugu", "తెలుగు"],
|
||||
tg: ["Tajik", "Тоҷикӣ"],
|
||||
th: ["Thai", "ไทย / Phasa Thai"],
|
||||
ti: ["Tigrinya", "ትግርኛ"],
|
||||
tk: ["Turkmen", "Туркмен / تركمن"],
|
||||
tl: ["Tagalog / Filipino", "Tagalog"],
|
||||
tn: ["Tswana", "Setswana"],
|
||||
to: ["Tonga", "Lea Faka-Tonga"],
|
||||
tr: ["Turkish", "Türkçe"],
|
||||
ts: ["Tsonga", "Xitsonga"],
|
||||
tt: ["Tatar", "Tatarça"],
|
||||
tw: ["Twi", "Twi"],
|
||||
ty: ["Tahitian", "Reo Mā`ohi"],
|
||||
ug: ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"],
|
||||
uk: ["Ukrainian", "Українська"],
|
||||
ur: ["Urdu", "اردو"],
|
||||
uz: ["Uzbek", "Ўзбек"],
|
||||
ve: ["Venda", "Tshivenḓa"],
|
||||
vi: ["Vietnamese", "Tiếng Việt"],
|
||||
vo: ["Volapük", "Volapük"],
|
||||
wa: ["Walloon", "Walon"],
|
||||
wo: ["Wolof", "Wollof"],
|
||||
xh: ["Xhosa", "isiXhosa"],
|
||||
yi: ["Yiddish", "ייִדיש"],
|
||||
yo: ["Yoruba", "Yorùbá"],
|
||||
za: ["Zhuang", "Cuengh / Tôô / 壮语"],
|
||||
zh: ["Chinese", "中文"],
|
||||
zu: ["Zulu", "isiZulu"],
|
||||
};
|
||||
|
||||
const lang = locale.substring(0, 2);
|
||||
return {
|
||||
type: types.LANGUAGE_RESOLVED,
|
||||
data: { key: locale, value: `${langs[lang][0]} (${langs[lang][1]})` },
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadLanguage(lang, destinationPath) {
|
||||
return function(dispatch, getState) {
|
||||
const plainLanguage = lang.replace(".json", "");
|
||||
|
||||
dispatch({
|
||||
type: types.DOWNLOAD_LANGUAGE_STARTED,
|
||||
data: plainLanguage,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(destinationPath);
|
||||
const req = http.request(
|
||||
`http://i18n.lbry.io/langs/${lang}`,
|
||||
response => {
|
||||
file.on("finish", () => {
|
||||
file.close();
|
||||
|
||||
// push to our local list
|
||||
dispatch({
|
||||
type: types.DOWNLOAD_LANGUAGE_SUCCEEDED,
|
||||
data: plainLanguage,
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
}
|
||||
);
|
||||
|
||||
const errorHandler = err => {
|
||||
if (file) {
|
||||
file.close();
|
||||
}
|
||||
|
||||
fs.unlink(destinationPath); // Delete the file async. (But we don't check the result)
|
||||
dispatch({
|
||||
type: types.DOWNLOAD_LANGUAGE_FAILED,
|
||||
data: plainLanguage,
|
||||
});
|
||||
reject(err);
|
||||
};
|
||||
req.on("error", errorHandler);
|
||||
file.on("error", errorHandler);
|
||||
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadLanguages() {
|
||||
return function(dispatch, getState) {
|
||||
if (!fs.existsSync("app/locales")) {
|
||||
fs.mkdirSync("app/locales");
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: types.DOWNLOAD_LANGUAGES_STARTED,
|
||||
data: {},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{ host: "i18n.lbry.io", path: "/" },
|
||||
response => {
|
||||
let str = "";
|
||||
|
||||
response.on("data", chunk => {
|
||||
str += chunk;
|
||||
});
|
||||
|
||||
response.on("end", () => {
|
||||
const files = JSON.parse(str);
|
||||
const actions = [];
|
||||
files.forEach(file => {
|
||||
actions.push(doDownloadLanguage(file, `app/locales/${file}`));
|
||||
});
|
||||
|
||||
dispatch(batchActions(...actions));
|
||||
|
||||
dispatch({
|
||||
type: types.DOWNLOAD_LANGUAGES_COMPLETED,
|
||||
data: {},
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import * as types from "constants/action_types";
|
||||
import * as modals from "constants/modal_types";
|
||||
import lbryio from "lbryio";
|
||||
import { setLocal } from "utils";
|
||||
import { doRewardList } from "actions/rewards";
|
||||
import { selectEmailToVerify } from "selectors/user";
|
||||
import { doOpenModal } from "actions/app";
|
||||
import { doRewardList, doClaimRewardType } from "actions/rewards";
|
||||
import { selectEmailToVerify, selectUser } from "selectors/user";
|
||||
import rewards from "rewards";
|
||||
|
||||
export function doAuthenticate() {
|
||||
return function(dispatch, getState) {
|
||||
|
@ -19,6 +22,7 @@ export function doAuthenticate() {
|
|||
dispatch(doRewardList());
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE));
|
||||
dispatch({
|
||||
type: types.AUTHENTICATION_FAILURE,
|
||||
data: { error },
|
||||
|
@ -136,3 +140,45 @@ export function doUserEmailVerify(verificationToken) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doUserIdentityVerify(stripeToken) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch({
|
||||
type: types.USER_IDENTITY_VERIFY_STARTED,
|
||||
token: stripeToken,
|
||||
});
|
||||
|
||||
lbryio
|
||||
.call("user", "verify_identity", { stripe_token: stripeToken }, "post")
|
||||
.then(user => {
|
||||
if (user.is_identity_verified) {
|
||||
dispatch({
|
||||
type: types.USER_IDENTITY_VERIFY_SUCCESS,
|
||||
data: { user },
|
||||
});
|
||||
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
|
||||
} else {
|
||||
throw new Error(
|
||||
"Your identity is still not verified. This should not happen."
|
||||
); //shouldn't happen
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({
|
||||
type: types.USER_IDENTITY_VERIFY_FAILURE,
|
||||
data: { error: error.toString() },
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doFetchAccessToken() {
|
||||
return function(dispatch, getState) {
|
||||
const success = token =>
|
||||
dispatch({
|
||||
type: types.FETCH_ACCESS_TOKEN_SUCCESS,
|
||||
data: { token },
|
||||
});
|
||||
lbryio.getAuthToken().then(success);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import store from "store.js";
|
|||
import lbry from "./lbry.js";
|
||||
|
||||
const env = ENV;
|
||||
const config = require(`./config/${env}`);
|
||||
const config = {
|
||||
...require(`./config/${env}`),
|
||||
};
|
||||
const language = lbry.getClientSetting("language")
|
||||
? lbry.getClientSetting("language")
|
||||
: "en";
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { selectCurrentModal } from "selectors/app";
|
||||
import { doCheckUpgradeAvailable, doAlertError } from "actions/app";
|
||||
import { doUpdateBalance } from "actions/wallet";
|
||||
import App from "./view";
|
||||
import {
|
||||
doCheckUpgradeAvailable,
|
||||
doOpenModal,
|
||||
doAlertError,
|
||||
doRecordScroll,
|
||||
} from "actions/app";
|
||||
import { doFetchRewardedContent } from "actions/content";
|
||||
|
||||
const select = state => ({
|
||||
import { doUpdateBalance } from "actions/wallet";
|
||||
import { selectWelcomeModalAcknowledged } from "selectors/app";
|
||||
import { selectUser } from "selectors/user";
|
||||
import App from "./view";
|
||||
import * as modals from "constants/modal_types";
|
||||
|
||||
const select = (state, props) => ({
|
||||
modal: selectCurrentModal(state),
|
||||
isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
alertError: errorList => dispatch(doAlertError(errorList)),
|
||||
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
|
||||
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)),
|
||||
updateBalance: balance => dispatch(doUpdateBalance(balance)),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(App);
|
||||
|
|
|
@ -2,25 +2,64 @@ import React from "react";
|
|||
import Router from "component/router";
|
||||
import Header from "component/header";
|
||||
import ModalError from "component/modalError";
|
||||
import ModalAuthFailure from "component/modalAuthFailure";
|
||||
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,
|
||||
fetchRewardedContent,
|
||||
} = 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);
|
||||
});
|
||||
|
||||
fetchRewardedContent();
|
||||
|
||||
this.showWelcome(this.props);
|
||||
|
||||
this.scrollListener = () => this.props.recordScroll(window.scrollY);
|
||||
|
||||
window.addEventListener("scroll", this.scrollListener);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.showWelcome(nextProps);
|
||||
}
|
||||
|
||||
showWelcome(props) {
|
||||
const { isWelcomeAcknowledged, openWelcomeModal, user } = props;
|
||||
|
||||
if (
|
||||
!isWelcomeAcknowledged &&
|
||||
user &&
|
||||
!user.is_reward_approved &&
|
||||
!user.is_identity_verified
|
||||
) {
|
||||
openWelcomeModal();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("scroll", this.scrollListener);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -32,10 +71,13 @@ 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 />}
|
||||
{modal == modals.AUTHENTICATION_FAILURE && <ModalAuthFailure />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
selectAuthenticationIsPending,
|
||||
selectEmailToVerify,
|
||||
selectUserIsVerificationCandidate,
|
||||
} from "selectors/user";
|
||||
import Auth from "./view";
|
||||
|
||||
const select = state => ({
|
||||
isPending: selectAuthenticationIsPending(state),
|
||||
email: selectEmailToVerify(state),
|
||||
isVerificationCandidate: selectUserIsVerificationCandidate(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(Auth);
|
|
@ -1,22 +0,0 @@
|
|||
import React from "react";
|
||||
import { BusyMessage } from "component/common";
|
||||
import UserEmailNew from "component/userEmailNew";
|
||||
import UserEmailVerify from "component/userEmailVerify";
|
||||
|
||||
export class Auth extends React.PureComponent {
|
||||
render() {
|
||||
const { isPending, email, isVerificationCandidate } = this.props;
|
||||
|
||||
if (isPending) {
|
||||
return <BusyMessage message={__("Authenticating")} />;
|
||||
} else if (!email) {
|
||||
return <UserEmailNew />;
|
||||
} else if (isVerificationCandidate) {
|
||||
return <UserEmailVerify />;
|
||||
} else {
|
||||
return <span className="empty">{__("No further steps.")}</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
|
@ -1,33 +0,0 @@
|
|||
import React from "react";
|
||||
import * as modal from "constants/modal_types";
|
||||
import rewards from "rewards.js";
|
||||
import { connect } from "react-redux";
|
||||
import { doUserEmailDecline } from "actions/user";
|
||||
import { doOpenModal } from "actions/app";
|
||||
import {
|
||||
selectAuthenticationIsPending,
|
||||
selectUserHasEmail,
|
||||
selectUserIsAuthRequested,
|
||||
} from "selectors/user";
|
||||
import { makeSelectHasClaimedReward } from "selectors/rewards";
|
||||
import AuthOverlay from "./view";
|
||||
|
||||
const select = (state, props) => {
|
||||
const selectHasClaimed = makeSelectHasClaimedReward();
|
||||
|
||||
return {
|
||||
hasEmail: selectUserHasEmail(state),
|
||||
isPending: selectAuthenticationIsPending(state),
|
||||
isShowing: selectUserIsAuthRequested(state),
|
||||
hasNewUserReward: selectHasClaimed(state, {
|
||||
reward_type: rewards.TYPE_NEW_USER,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
userEmailDecline: () => dispatch(doUserEmailDecline()),
|
||||
openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(AuthOverlay);
|
|
@ -1,90 +0,0 @@
|
|||
import React from "react";
|
||||
import lbryio from "lbryio.js";
|
||||
import ModalPage from "component/modal-page.js";
|
||||
import Auth from "component/auth";
|
||||
import Link from "component/link";
|
||||
import { getLocal, setLocal } from "utils";
|
||||
|
||||
export class AuthOverlay extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showNoEmailConfirm: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
this.props.isShowing &&
|
||||
!this.props.isPending &&
|
||||
!nextProps.hasNewUserReward &&
|
||||
!nextProps.isShowing /* && !getLocal("welcome_screen_shown")*/
|
||||
) {
|
||||
setLocal("welcome_screen_shown", true);
|
||||
setTimeout(() => this.props.openWelcomeModal(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
onEmailSkipClick() {
|
||||
this.setState({ showNoEmailConfirm: true });
|
||||
}
|
||||
|
||||
onEmailSkipConfirm() {
|
||||
this.props.userEmailDecline();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!lbryio.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isPending, isShowing, hasEmail } = this.props;
|
||||
|
||||
if (isShowing) {
|
||||
return (
|
||||
<ModalPage
|
||||
className="modal-page--full"
|
||||
isOpen={true}
|
||||
contentLabel="Authentication"
|
||||
>
|
||||
<h1>LBRY Early Access</h1>
|
||||
<Auth />
|
||||
{isPending
|
||||
? ""
|
||||
: <div className="form-row-submit">
|
||||
{!hasEmail && this.state.showNoEmailConfirm
|
||||
? <div className="help form-input-width">
|
||||
<p>
|
||||
{__(
|
||||
"If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications."
|
||||
)}
|
||||
</p>
|
||||
<Link
|
||||
onClick={() => {
|
||||
this.onEmailSkipConfirm();
|
||||
}}
|
||||
label={__("Continue without email")}
|
||||
/>
|
||||
</div>
|
||||
: <Link
|
||||
className={"button-text-help"}
|
||||
onClick={() => {
|
||||
hasEmail
|
||||
? this.onEmailSkipConfirm()
|
||||
: this.onEmailSkipClick();
|
||||
}}
|
||||
label={
|
||||
hasEmail ? __("Skip for now") : __("Do I have to?")
|
||||
}
|
||||
/>}
|
||||
</div>}
|
||||
</ModalPage>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthOverlay;
|
8
ui/js/component/cardMedia/index.js
Normal file
8
ui/js/component/cardMedia/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import CardMedia from "./view";
|
||||
|
||||
const select = state => ({});
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(CardMedia);
|
54
ui/js/component/cardMedia/view.jsx
Normal file
54
ui/js/component/cardMedia/view.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
|
||||
class CardMedia extends React.PureComponent {
|
||||
static AUTO_THUMB_CLASSES = [
|
||||
"purple",
|
||||
"red",
|
||||
"pink",
|
||||
"indigo",
|
||||
"blue",
|
||||
"light-blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"green",
|
||||
"yellow",
|
||||
"orange",
|
||||
];
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
autoThumbClass:
|
||||
CardMedia.AUTO_THUMB_CLASSES[
|
||||
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, thumbnail } = this.props;
|
||||
const atClass = this.state.autoThumbClass;
|
||||
|
||||
if (thumbnail) {
|
||||
return (
|
||||
<div
|
||||
className="card__media"
|
||||
style={{ backgroundImage: "url('" + thumbnail + "')" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`card__media card__media--autothumb ${atClass}`}>
|
||||
<div className="card__autothumb__text">
|
||||
{title &&
|
||||
title
|
||||
.replace(/\s+/g, "")
|
||||
.substring(0, Math.min(title.replace(" ", "").length, 5))
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CardMedia;
|
12
ui/js/component/cardVerify/index.js
Normal file
12
ui/js/component/cardVerify/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { selectUserEmail } from "selectors/user";
|
||||
import CardVerify from "./view";
|
||||
|
||||
const select = state => ({
|
||||
email: selectUserEmail(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(CardVerify);
|
180
ui/js/component/cardVerify/view.jsx
Normal file
180
ui/js/component/cardVerify/view.jsx
Normal file
|
@ -0,0 +1,180 @@
|
|||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Link from "component/link";
|
||||
|
||||
let scriptLoading = false;
|
||||
let scriptLoaded = false;
|
||||
let scriptDidError = false;
|
||||
|
||||
class CardVerify extends React.Component {
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
label: PropTypes.string,
|
||||
|
||||
// =====================================================
|
||||
// Required by stripe
|
||||
// see Stripe docs for more info:
|
||||
// https://stripe.com/docs/checkout#integration-custom
|
||||
// =====================================================
|
||||
|
||||
// Your publishable key (test or live).
|
||||
// can't use "key" as a prop in react, so have to change the keyname
|
||||
stripeKey: PropTypes.string.isRequired,
|
||||
|
||||
// The callback to invoke when the Checkout process is complete.
|
||||
// function(token)
|
||||
// token is the token object created.
|
||||
// token.id can be used to create a charge or customer.
|
||||
// token.email contains the email address entered by the user.
|
||||
token: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scriptLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
scriptLoading = true;
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://checkout.stripe.com/checkout.js";
|
||||
script.async = 1;
|
||||
|
||||
this.loadPromise = (() => {
|
||||
let canceled = false;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
script.onload = () => {
|
||||
scriptLoaded = true;
|
||||
scriptLoading = false;
|
||||
resolve();
|
||||
this.onScriptLoaded();
|
||||
};
|
||||
script.onerror = event => {
|
||||
scriptDidError = true;
|
||||
scriptLoading = false;
|
||||
reject(event);
|
||||
this.onScriptError(event);
|
||||
};
|
||||
});
|
||||
const wrappedPromise = new Promise((accept, cancel) => {
|
||||
promise.then(
|
||||
() => (canceled ? cancel({ isCanceled: true }) : accept())
|
||||
);
|
||||
promise.catch(
|
||||
error => (canceled ? cancel({ isCanceled: true }) : cancel(error))
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
this.loadPromise.promise
|
||||
.then(this.onScriptLoaded)
|
||||
.catch(this.onScriptError);
|
||||
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!scriptLoading) {
|
||||
this.updateStripeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.loadPromise) {
|
||||
this.loadPromise.cancel();
|
||||
}
|
||||
if (CardVerify.stripeHandler && this.state.open) {
|
||||
CardVerify.stripeHandler.close();
|
||||
}
|
||||
}
|
||||
|
||||
onScriptLoaded = () => {
|
||||
if (!CardVerify.stripeHandler) {
|
||||
CardVerify.stripeHandler = StripeCheckout.configure({
|
||||
key: this.props.stripeKey,
|
||||
});
|
||||
if (this.hasPendingClick) {
|
||||
this.showStripeDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onScriptError = (...args) => {
|
||||
throw new Error("Unable to load credit validation script.");
|
||||
};
|
||||
|
||||
onClosed = () => {
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
updateStripeHandler() {
|
||||
if (!CardVerify.stripeHandler) {
|
||||
CardVerify.stripeHandler = StripeCheckout.configure({
|
||||
key: this.props.stripeKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showStripeDialog() {
|
||||
this.setState({ open: true });
|
||||
CardVerify.stripeHandler.open({
|
||||
allowRememberMe: false,
|
||||
closed: this.onClosed,
|
||||
description: __("Confirm Identity"),
|
||||
email: this.props.email,
|
||||
locale: "auto",
|
||||
panelLabel: "Verify",
|
||||
token: this.props.token,
|
||||
zipCode: true,
|
||||
});
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
if (scriptDidError) {
|
||||
try {
|
||||
throw new Error(
|
||||
"Tried to call onClick, but StripeCheckout failed to load"
|
||||
);
|
||||
} catch (x) {}
|
||||
} else if (CardVerify.stripeHandler) {
|
||||
this.showStripeDialog();
|
||||
} else {
|
||||
this.hasPendingClick = true;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Link
|
||||
button="primary"
|
||||
label={this.props.label}
|
||||
icon="icon-lock"
|
||||
disabled={
|
||||
this.props.disabled || this.state.open || this.hasPendingClick
|
||||
}
|
||||
onClick={this.onClick.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CardVerify;
|
|
@ -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 || ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { doCloseModal, doOpenModal } from "actions/app";
|
|||
import { doFetchAvailability } from "actions/availability";
|
||||
import { doOpenFileInShell, doOpenFileInFolder } from "actions/file_info";
|
||||
import { makeSelectClaimForUriIsMine } from "selectors/claims";
|
||||
import { doPurchaseUri, doLoadVideo } from "actions/content";
|
||||
import { doPurchaseUri, doLoadVideo, doStartDownload } from "actions/content";
|
||||
import FileActions from "./view";
|
||||
|
||||
const makeSelect = () => {
|
||||
|
@ -47,6 +47,7 @@ const perform = dispatch => ({
|
|||
openModal: modal => dispatch(doOpenModal(modal)),
|
||||
startDownload: uri => dispatch(doPurchaseUri(uri, "affirmPurchase")),
|
||||
loadVideo: uri => dispatch(doLoadVideo(uri)),
|
||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
||||
});
|
||||
|
||||
export default connect(makeSelect, perform)(FileActions);
|
||||
|
|
|
@ -22,6 +22,21 @@ class FileActions extends React.PureComponent {
|
|||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.checkAvailability(nextProps.uri);
|
||||
this.restartDownload(nextProps);
|
||||
}
|
||||
|
||||
restartDownload(props) {
|
||||
const { downloading, fileInfo, uri, restartDownload } = props;
|
||||
|
||||
if (
|
||||
!downloading &&
|
||||
fileInfo &&
|
||||
!fileInfo.completed &&
|
||||
fileInfo.written_bytes !== false &&
|
||||
fileInfo.written_bytes < fileInfo.total_bytes
|
||||
) {
|
||||
restartDownload(uri, fileInfo.outpoint);
|
||||
}
|
||||
}
|
||||
|
||||
checkAvailability(uri) {
|
||||
|
@ -142,6 +157,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 +193,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")}
|
||||
|
|
|
@ -8,7 +8,10 @@ import {
|
|||
makeSelectMetadataForUri,
|
||||
} from "selectors/claims";
|
||||
import { makeSelectFileInfoForUri } from "selectors/file_info";
|
||||
import { makeSelectIsResolvingForUri } from "selectors/content";
|
||||
import {
|
||||
makeSelectIsResolvingForUri,
|
||||
selectRewardContentClaimIds,
|
||||
} from "selectors/content";
|
||||
import FileCard from "./view";
|
||||
|
||||
const makeSelect = () => {
|
||||
|
@ -22,6 +25,7 @@ const makeSelect = () => {
|
|||
fileInfo: selectFileInfoForUri(state, props),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
metadata: selectMetadataForUri(state, props),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
isResolvingUri: selectResolvingUri(state, props),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
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 IconFeatured from "component/iconFeatured";
|
||||
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) {
|
||||
|
@ -44,11 +47,23 @@ class FileCard extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { claim, fileInfo, metadata, isResolvingUri, navigate } = this.props;
|
||||
const {
|
||||
claim,
|
||||
fileInfo,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
navigate,
|
||||
rewardedContentClaimIds,
|
||||
} = this.props;
|
||||
|
||||
const uri = lbryuri.normalize(this.props.uri);
|
||||
const title = metadata && metadata.title ? metadata.title : uri;
|
||||
const thumbnail = metadata && metadata.thumbnail
|
||||
? metadata.thumbnail
|
||||
: null;
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent =
|
||||
claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
|
||||
let description = "";
|
||||
if (isResolvingUri && !claim) {
|
||||
|
@ -73,28 +88,23 @@ 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} />
|
||||
{fileInfo
|
||||
? <span>{" "}<Icon fixed icon="icon-folder" /></span>
|
||||
: ""}
|
||||
{isRewardContent && <span>{" "}<IconFeatured /></span>}
|
||||
{fileInfo &&
|
||||
<span>{" "}<Icon fixed icon="icon-folder" /></span>}
|
||||
</span>
|
||||
<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>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import lbry from "lbry.js";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import Link from "component/link";
|
||||
import { FormField } from "component/form.js";
|
||||
import FormField from "component/formField";
|
||||
import FileTile from "component/fileTile";
|
||||
import rewards from "rewards.js";
|
||||
import lbryio from "lbryio.js";
|
||||
|
@ -67,7 +67,9 @@ class FileList extends React.PureComponent {
|
|||
const content = [];
|
||||
|
||||
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
let uriParams = {};
|
||||
let uriParams = {
|
||||
claimId: fileInfo.claim_id,
|
||||
};
|
||||
if (fileInfo.channel_name) {
|
||||
uriParams.channelName = fileInfo.channel_name;
|
||||
uriParams.contentName = fileInfo.name;
|
||||
|
@ -79,7 +81,7 @@ class FileList extends React.PureComponent {
|
|||
|
||||
content.push(
|
||||
<FileTile
|
||||
key={uri}
|
||||
key={fileInfo.outpoint || fileInfo.claim_id}
|
||||
uri={uri}
|
||||
hidePrice={true}
|
||||
showEmpty={this.props.fileTileShowEmpty}
|
||||
|
@ -94,7 +96,6 @@ class FileList extends React.PureComponent {
|
|||
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
|
||||
<option value="date">{__("Date")}</option>
|
||||
<option value="title">{__("Title")}</option>
|
||||
<option value="filename">{__("File name")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{content}
|
||||
|
|
|
@ -67,7 +67,7 @@ class FileListSearch extends React.PureComponent {
|
|||
|
||||
{results && !!results.length
|
||||
? <FileListSearchResults {...this.props} />
|
||||
: <SearchNoResults {...this.props} />}
|
||||
: !isSearching && <SearchNoResults {...this.props} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,10 @@ import {
|
|||
} from "selectors/claims";
|
||||
import { makeSelectFileInfoForUri } from "selectors/file_info";
|
||||
import { selectShowNsfw } from "selectors/settings";
|
||||
import { makeSelectIsResolvingForUri } from "selectors/content";
|
||||
import {
|
||||
makeSelectIsResolvingForUri,
|
||||
selectRewardContentClaimIds,
|
||||
} from "selectors/content";
|
||||
import FileTile from "./view";
|
||||
|
||||
const makeSelect = () => {
|
||||
|
@ -23,6 +26,7 @@ const makeSelect = () => {
|
|||
obscureNsfw: !selectShowNsfw(state),
|
||||
metadata: selectMetadataForUri(state, props),
|
||||
isResolvingUri: selectResolvingUri(state, props),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
});
|
||||
|
||||
return select;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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";
|
||||
import NsfwOverlay from "component/nsfwOverlay";
|
||||
import IconFeatured from "component/iconFeatured";
|
||||
|
||||
class FileTile extends React.PureComponent {
|
||||
static SHOW_EMPTY_PUBLISH = "publish";
|
||||
|
@ -57,6 +58,7 @@ class FileTile extends React.PureComponent {
|
|||
showEmpty,
|
||||
navigate,
|
||||
hidePrice,
|
||||
rewardedContentClaimIds,
|
||||
} = this.props;
|
||||
|
||||
const uri = lbryuri.normalize(this.props.uri);
|
||||
|
@ -64,8 +66,14 @@ 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;
|
||||
const isRewardContent =
|
||||
claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
|
||||
let onClick = () => navigate("/show", { uri });
|
||||
|
||||
let description = "";
|
||||
|
@ -98,22 +106,15 @@ 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}
|
||||
{isRewardContent && <IconFeatured />}
|
||||
<div className="meta">{uri}</div>
|
||||
<h3><TruncatedText lines={1}>{title}</TruncatedText></h3>
|
||||
<h3>
|
||||
<TruncatedText lines={1}>{title}</TruncatedText>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card__content card__subtext">
|
||||
<TruncatedText lines={3}>
|
||||
|
|
|
@ -1,181 +1,32 @@
|
|||
import React from "react";
|
||||
import FileSelector from "./file-selector.js";
|
||||
import { Icon } from "./common.js";
|
||||
import FormField from "component/formField";
|
||||
|
||||
var formFieldCounter = 0,
|
||||
formFieldFileSelectorTypes = ["file", "directory"],
|
||||
formFieldNestedLabelTypes = ["radio", "checkbox"];
|
||||
let formFieldCounter = 0;
|
||||
|
||||
function formFieldId() {
|
||||
export const formFieldNestedLabelTypes = ["radio", "checkbox"];
|
||||
|
||||
export function formFieldId() {
|
||||
return "form-field-" + ++formFieldCounter;
|
||||
}
|
||||
|
||||
export class FormField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
prefix: React.PropTypes.string,
|
||||
postfix: React.PropTypes.string,
|
||||
hasError: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._fieldRequiredText = __("This field is required");
|
||||
this._type = null;
|
||||
this._element = null;
|
||||
|
||||
this.state = {
|
||||
isError: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (["text", "number", "radio", "checkbox"].includes(this.props.type)) {
|
||||
this._element = "input";
|
||||
this._type = this.props.type;
|
||||
} else if (this.props.type == "text-number") {
|
||||
this._element = "input";
|
||||
this._type = "text";
|
||||
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
|
||||
this._element = "input";
|
||||
this._type = "hidden";
|
||||
} else {
|
||||
// Non <input> field, e.g. <select>, <textarea>
|
||||
this._element = this.props.type;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
/**
|
||||
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
|
||||
* https://github.com/facebook/react/issues/3468
|
||||
*/
|
||||
if (this.props.type == "directory") {
|
||||
this.refs.field.webkitdirectory = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleFileChosen(path) {
|
||||
this.refs.field.value = path;
|
||||
if (this.props.onChange) {
|
||||
// Updating inputs programmatically doesn't generate an event, so we have to make our own
|
||||
const event = new Event("change", { bubbles: true });
|
||||
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
|
||||
this.props.onChange(event);
|
||||
}
|
||||
}
|
||||
|
||||
showError(text) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorMessage: text,
|
||||
});
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.field.focus();
|
||||
}
|
||||
|
||||
getValue() {
|
||||
if (this.props.type == "checkbox") {
|
||||
return this.refs.field.checked;
|
||||
} else {
|
||||
return this.refs.field.value;
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedElement() {
|
||||
return this.refs.field.options[this.refs.field.selectedIndex];
|
||||
}
|
||||
|
||||
render() {
|
||||
// Pass all unhandled props to the field element
|
||||
const otherProps = Object.assign({}, this.props),
|
||||
isError = this.state.isError !== null
|
||||
? this.state.isError
|
||||
: this.props.hasError,
|
||||
elementId = this.props.id ? this.props.id : formFieldId(),
|
||||
renderElementInsideLabel =
|
||||
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
|
||||
|
||||
delete otherProps.type;
|
||||
delete otherProps.label;
|
||||
delete otherProps.hasError;
|
||||
delete otherProps.className;
|
||||
delete otherProps.postfix;
|
||||
delete otherProps.prefix;
|
||||
|
||||
const element = (
|
||||
<this._element
|
||||
id={elementId}
|
||||
type={this._type}
|
||||
name={this.props.name}
|
||||
ref="field"
|
||||
placeholder={this.props.placeholder}
|
||||
className={
|
||||
"form-field__input form-field__input-" +
|
||||
this.props.type +
|
||||
" " +
|
||||
(this.props.className || "") +
|
||||
(isError ? "form-field__input--error" : "")
|
||||
}
|
||||
{...otherProps}
|
||||
>
|
||||
{this.props.children}
|
||||
</this._element>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"form-field form-field--" + this.props.type}>
|
||||
{this.props.prefix
|
||||
? <span className="form-field__prefix">{this.props.prefix}</span>
|
||||
: ""}
|
||||
{renderElementInsideLabel
|
||||
? <label
|
||||
htmlFor={elementId}
|
||||
className={
|
||||
"form-field__label " +
|
||||
(isError ? "form-field__label--error" : "")
|
||||
}
|
||||
>
|
||||
{element}
|
||||
{this.props.label}
|
||||
</label>
|
||||
: element}
|
||||
{formFieldFileSelectorTypes.includes(this.props.type)
|
||||
? <FileSelector
|
||||
type={this.props.type}
|
||||
onFileChosen={this.handleFileChosen.bind(this)}
|
||||
{...(this.props.defaultValue
|
||||
? { initPath: this.props.defaultValue }
|
||||
: {})}
|
||||
/>
|
||||
: null}
|
||||
{this.props.postfix
|
||||
? <span className="form-field__postfix">{this.props.postfix}</span>
|
||||
: ""}
|
||||
{isError && this.state.errorMessage
|
||||
? <div className="form-field__error">{this.state.errorMessage}</div>
|
||||
: ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class FormRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.element,
|
||||
]),
|
||||
errorMessage: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.object,
|
||||
]),
|
||||
// helper: React.PropTypes.html,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._field = null;
|
||||
|
||||
this._fieldRequiredText = __("This field is required");
|
||||
|
||||
this.state = this.getStateFromProps(props);
|
||||
|
@ -190,7 +41,9 @@ export class FormRow extends React.PureComponent {
|
|||
isError: !!props.errorMessage,
|
||||
errorMessage: typeof props.errorMessage === "string"
|
||||
? props.errorMessage
|
||||
: "",
|
||||
: props.errorMessage instanceof Error
|
||||
? props.errorMessage.toString()
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -213,15 +66,24 @@ export class FormRow extends React.PureComponent {
|
|||
}
|
||||
|
||||
getValue() {
|
||||
return this.refs.field.getValue();
|
||||
return this._field.getValue();
|
||||
}
|
||||
|
||||
getSelectedElement() {
|
||||
return this.refs.field.getSelectedElement();
|
||||
return this._field.getSelectedElement();
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
if (!this._field || !this._field.getOptions) {
|
||||
console.log(this);
|
||||
console.log(this._field);
|
||||
console.log(this._field.getOptions);
|
||||
}
|
||||
return this._field.getOptions();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.field.focus();
|
||||
this._field.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -257,7 +119,13 @@ export class FormRow extends React.PureComponent {
|
|||
</label>
|
||||
</div>
|
||||
: ""}
|
||||
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
|
||||
<FormField
|
||||
ref={ref => {
|
||||
this._field = ref ? ref.getWrappedInstance() : null;
|
||||
}}
|
||||
hasError={this.state.isError}
|
||||
{...fieldProps}
|
||||
/>
|
||||
{!this.state.isError && this.props.helper
|
||||
? <div className="form-field__helper">{this.props.helper}</div>
|
||||
: ""}
|
||||
|
|
5
ui/js/component/formField/index.js
Normal file
5
ui/js/component/formField/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import FormField from "./view";
|
||||
|
||||
export default connect(null, null, null, { withRef: true })(FormField);
|
178
ui/js/component/formField/view.jsx
Normal file
178
ui/js/component/formField/view.jsx
Normal file
|
@ -0,0 +1,178 @@
|
|||
import React from "react";
|
||||
import FileSelector from "component/file-selector.js";
|
||||
import SimpleMDE from "react-simplemde-editor";
|
||||
import { formFieldNestedLabelTypes, formFieldId } from "../form";
|
||||
import style from "react-simplemde-editor/dist/simplemde.min.css";
|
||||
|
||||
const formFieldFileSelectorTypes = ["file", "directory"];
|
||||
|
||||
class FormField extends React.PureComponent {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
prefix: React.PropTypes.string,
|
||||
postfix: React.PropTypes.string,
|
||||
hasError: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._fieldRequiredText = __("This field is required");
|
||||
this._type = null;
|
||||
this._element = null;
|
||||
this._extraElementProps = {};
|
||||
|
||||
this.state = {
|
||||
isError: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (["text", "number", "radio", "checkbox"].includes(this.props.type)) {
|
||||
this._element = "input";
|
||||
this._type = this.props.type;
|
||||
} else if (this.props.type == "text-number") {
|
||||
this._element = "input";
|
||||
this._type = "text";
|
||||
} else if (this.props.type == "SimpleMDE") {
|
||||
this._element = SimpleMDE;
|
||||
this._type = "textarea";
|
||||
this._extraElementProps.options = {
|
||||
hideIcons: ["guide", "heading", "image", "fullscreen", "side-by-side"],
|
||||
};
|
||||
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
|
||||
this._element = "input";
|
||||
this._type = "hidden";
|
||||
} else {
|
||||
// Non <input> field, e.g. <select>, <textarea>
|
||||
this._element = this.props.type;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
/**
|
||||
* We have to add the webkitdirectory attribute here because React doesn't allow it in JSX
|
||||
* https://github.com/facebook/react/issues/3468
|
||||
*/
|
||||
if (this.props.type == "directory") {
|
||||
this.refs.field.webkitdirectory = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleFileChosen(path) {
|
||||
this.refs.field.value = path;
|
||||
if (this.props.onChange) {
|
||||
// Updating inputs programmatically doesn't generate an event, so we have to make our own
|
||||
const event = new Event("change", { bubbles: true });
|
||||
this.refs.field.dispatchEvent(event); // This alone won't generate a React event, but we use it to attach the field as a target
|
||||
this.props.onChange(event);
|
||||
}
|
||||
}
|
||||
|
||||
showError(text) {
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorMessage: text,
|
||||
});
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.field.focus();
|
||||
}
|
||||
|
||||
getValue() {
|
||||
if (this.props.type == "checkbox") {
|
||||
return this.refs.field.checked;
|
||||
} else if (this.props.type == "SimpleMDE") {
|
||||
return this.refs.field.simplemde.value();
|
||||
} else {
|
||||
return this.refs.field.value;
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedElement() {
|
||||
return this.refs.field.options[this.refs.field.selectedIndex];
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.refs.field.options;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Pass all unhandled props to the field element
|
||||
const otherProps = Object.assign({}, this.props),
|
||||
isError = this.state.isError !== null
|
||||
? this.state.isError
|
||||
: this.props.hasError,
|
||||
elementId = this.props.elementId ? this.props.elementId : formFieldId(),
|
||||
renderElementInsideLabel =
|
||||
this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
|
||||
|
||||
delete otherProps.type;
|
||||
delete otherProps.label;
|
||||
delete otherProps.hasError;
|
||||
delete otherProps.className;
|
||||
delete otherProps.postfix;
|
||||
delete otherProps.prefix;
|
||||
delete otherProps.dispatch;
|
||||
|
||||
const element = (
|
||||
<this._element
|
||||
id={elementId}
|
||||
type={this._type}
|
||||
name={this.props.name}
|
||||
ref="field"
|
||||
placeholder={this.props.placeholder}
|
||||
className={
|
||||
"form-field__input form-field__input-" +
|
||||
this.props.type +
|
||||
" " +
|
||||
(this.props.className || "") +
|
||||
(isError ? "form-field__input--error" : "")
|
||||
}
|
||||
{...otherProps}
|
||||
{...this._extraElementProps}
|
||||
>
|
||||
{this.props.children}
|
||||
</this._element>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"form-field form-field--" + this.props.type}>
|
||||
{this.props.prefix
|
||||
? <span className="form-field__prefix">{this.props.prefix}</span>
|
||||
: ""}
|
||||
{renderElementInsideLabel
|
||||
? <label
|
||||
htmlFor={elementId}
|
||||
className={
|
||||
"form-field__label " +
|
||||
(isError ? "form-field__label--error" : "")
|
||||
}
|
||||
>
|
||||
{element}
|
||||
{this.props.label}
|
||||
</label>
|
||||
: element}
|
||||
{formFieldFileSelectorTypes.includes(this.props.type)
|
||||
? <FileSelector
|
||||
type={this.props.type}
|
||||
onFileChosen={this.handleFileChosen.bind(this)}
|
||||
{...(this.props.defaultValue
|
||||
? { initPath: this.props.defaultValue }
|
||||
: {})}
|
||||
/>
|
||||
: null}
|
||||
{this.props.postfix
|
||||
? <span className="form-field__postfix">{this.props.postfix}</span>
|
||||
: ""}
|
||||
{isError && this.state.errorMessage
|
||||
? <div className="form-field__error">{this.state.errorMessage}</div>
|
||||
: ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormField;
|
5
ui/js/component/formFieldPrice/index.js
Normal file
5
ui/js/component/formFieldPrice/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import FormFieldPrice from "./view";
|
||||
|
||||
export default connect(null, null)(FormFieldPrice);
|
70
ui/js/component/formFieldPrice/view.jsx
Normal file
70
ui/js/component/formFieldPrice/view.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React from "react";
|
||||
import FormField from "component/formField";
|
||||
|
||||
class FormFieldPrice extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
amount: props.defaultValue && props.defaultValue.amount
|
||||
? props.defaultValue.amount
|
||||
: "",
|
||||
currency: props.defaultValue && props.defaultValue.currency
|
||||
? props.defaultValue.currency
|
||||
: "LBC",
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(newValues) {
|
||||
const newState = Object.assign({}, this.state, newValues);
|
||||
this.setState(newState);
|
||||
this.props.onChange({
|
||||
amount: newState.amount,
|
||||
currency: newState.currency,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeAmountChange(event) {
|
||||
this.handleChange({
|
||||
amount: event.target.value ? Number(event.target.value) : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeCurrencyChange(event) {
|
||||
this.handleChange({ currency: event.target.value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { defaultValue, placeholder, min, step } = this.props;
|
||||
|
||||
return (
|
||||
<span className="form-field">
|
||||
<FormField
|
||||
type="number"
|
||||
name="amount"
|
||||
min={min}
|
||||
placeholder={placeholder || null}
|
||||
step={step}
|
||||
onChange={event => this.handleFeeAmountChange(event)}
|
||||
defaultValue={
|
||||
defaultValue && defaultValue.amount ? defaultValue.amount : ""
|
||||
}
|
||||
className="form-field__input--inline"
|
||||
/>
|
||||
<FormField
|
||||
type="select"
|
||||
name="currency"
|
||||
onChange={event => this.handleFeeCurrencyChange(event)}
|
||||
defaultValue={
|
||||
defaultValue && defaultValue.currency ? defaultValue.currency : ""
|
||||
}
|
||||
className="form-field__input--inline"
|
||||
>
|
||||
<option value="LBC">{__("LBRY Credits (LBC)")}</option>
|
||||
<option value="USD">{__("US Dollars")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormFieldPrice;
|
|
@ -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"),
|
||||
});
|
||||
|
||||
|
|
|
@ -8,13 +8,19 @@ export const Header = props => {
|
|||
return (
|
||||
<header id="header">
|
||||
<div className="header__item">
|
||||
<Link onClick={back} button="alt button--flat" icon="icon-arrow-left" />
|
||||
<Link
|
||||
onClick={back}
|
||||
button="alt button--flat"
|
||||
icon="icon-arrow-left"
|
||||
title={__("Back")}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={() => navigate("/discover")}
|
||||
button="alt button--flat"
|
||||
icon="icon-home"
|
||||
title={__("Discover Content")}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item header__item--wunderbar">
|
||||
|
@ -26,6 +32,7 @@ export const Header = props => {
|
|||
button="text"
|
||||
icon="icon-bank"
|
||||
label={balance}
|
||||
title={__("Wallet")}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
|
@ -41,6 +48,7 @@ export const Header = props => {
|
|||
onClick={() => navigate("/downloaded")}
|
||||
button="alt button--flat"
|
||||
icon="icon-folder"
|
||||
title={__("Downloads and Publishes")}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
|
@ -48,6 +56,7 @@ export const Header = props => {
|
|||
onClick={() => navigate("/settings")}
|
||||
button="alt button--flat"
|
||||
icon="icon-gear"
|
||||
title={__("Settings")}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
|
5
ui/js/component/iconFeatured/index.js
Normal file
5
ui/js/component/iconFeatured/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import IconFeatured from "./view";
|
||||
|
||||
export default connect(null, null)(IconFeatured);
|
15
ui/js/component/iconFeatured/view.jsx
Normal file
15
ui/js/component/iconFeatured/view.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from "react";
|
||||
import { Icon } from "component/common.js";
|
||||
|
||||
const IconFeatured = props => {
|
||||
return (
|
||||
<span
|
||||
className="icon-featured"
|
||||
title={__("Watch content with this icon to earn weekly rewards.")}
|
||||
>
|
||||
<Icon icon="icon-rocket" fixed className="card__icon-featured-content" />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconFeatured;
|
|
@ -11,7 +11,6 @@ const Link = props => {
|
|||
icon,
|
||||
badge,
|
||||
button,
|
||||
hidden,
|
||||
disabled,
|
||||
children,
|
||||
} = props;
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import React from "react";
|
||||
import ReactModal from "react-modal";
|
||||
|
||||
export class ModalPage extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<ReactModal
|
||||
onCloseRequested={this.props.onAborted || this.props.onConfirmed}
|
||||
{...this.props}
|
||||
className={(this.props.className || "") + " modal-page"}
|
||||
overlayClassName="modal-overlay"
|
||||
>
|
||||
<div className="modal-page__content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalPage;
|
12
ui/js/component/modalAuthFailure/index.js
Normal file
12
ui/js/component/modalAuthFailure/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal } from "actions/app";
|
||||
import ModalAuthFailure from "./view";
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
const perform = dispatch => ({
|
||||
close: () => dispatch(doCloseModal()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalAuthFailure);
|
31
ui/js/component/modalAuthFailure/view.jsx
Normal file
31
ui/js/component/modalAuthFailure/view.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
|
||||
class ModalAuthFailure extends React.PureComponent {
|
||||
render() {
|
||||
const { close } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
contentLabel={__("Unable to Authenticate")}
|
||||
type="confirm"
|
||||
confirmButtonLabel={__("Reload")}
|
||||
abortButtonLabel={__("Continue")}
|
||||
onConfirmed={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
onAborted={close}
|
||||
>
|
||||
<h3>{__("Authentication Failure")}</h3>
|
||||
<p>
|
||||
{__(
|
||||
"If reloading does not fix this, or you see this at every start up, please email help@lbry.io."
|
||||
)}
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalAuthFailure;
|
|
@ -6,7 +6,7 @@ class ModalError extends React.PureComponent {
|
|||
render() {
|
||||
const { modal, closeModal, error } = this.props;
|
||||
|
||||
const errorObj = typeof error === "string" ? { error: error } : error;
|
||||
const errorObj = typeof error === "string" ? { message: error } : error;
|
||||
|
||||
const error_key_labels = {
|
||||
connectionString: __("API connection string"),
|
||||
|
@ -18,10 +18,10 @@ class ModalError extends React.PureComponent {
|
|||
};
|
||||
|
||||
const errorInfoList = [];
|
||||
for (let key of Object.keys(error)) {
|
||||
let val = typeof error[key] == "string"
|
||||
? error[key]
|
||||
: JSON.stringify(error[key]);
|
||||
for (let key of Object.keys(errorObj)) {
|
||||
let val = typeof errorObj[key] == "string"
|
||||
? errorObj[key]
|
||||
: JSON.stringify(errorObj[key]);
|
||||
let label = error_key_labels[key];
|
||||
errorInfoList.push(
|
||||
<li key={key}><strong>{label}</strong>: <code>{val}</code></li>
|
||||
|
|
20
ui/js/component/modalFirstReward/index.js
Normal file
20
ui/js/component/modalFirstReward/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from "react";
|
||||
import rewards from "rewards";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal } from "actions/app";
|
||||
import { makeSelectRewardByType } from "selectors/rewards";
|
||||
import ModalFirstReward from "./view";
|
||||
|
||||
const select = (state, props) => {
|
||||
const selectReward = makeSelectRewardByType();
|
||||
|
||||
return {
|
||||
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalFirstReward);
|
49
ui/js/component/modalFirstReward/view.jsx
Normal file
49
ui/js/component/modalFirstReward/view.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
import { CreditAmount } from "component/common";
|
||||
|
||||
class ModalFirstReward extends React.PureComponent {
|
||||
render() {
|
||||
const { closeModal, reward } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
type="alert"
|
||||
overlayClassName="modal-overlay modal-overlay--clear"
|
||||
isOpen={true}
|
||||
contentLabel={__("Welcome to LBRY")}
|
||||
onConfirmed={closeModal}
|
||||
>
|
||||
<section>
|
||||
<h3 className="modal__header">{__("Your First Reward")}</h3>
|
||||
<p>
|
||||
{__("You just earned your first reward of")}
|
||||
{" "}<CreditAmount amount={reward.reward_amount} />.
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"This reward will show in your Wallet in the top right momentarily (if it hasn't already)."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"These credits are used to compensate creators, to publish your own content, and to have say in how the network works."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"No need to understand it all just yet! Try watching or downloading something next."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"Finally, please know that LBRY is an early beta and that it earns the name."
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalFirstReward;
|
12
ui/js/component/modalIncompatibleDaemon/index.js
Normal file
12
ui/js/component/modalIncompatibleDaemon/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doQuit, doSkipWrongDaemonNotice } from "actions/app";
|
||||
import ModalIncompatibleDaemon from "./view";
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
const perform = dispatch => ({
|
||||
quit: () => dispatch(doQuit()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalIncompatibleDaemon);
|
29
ui/js/component/modalIncompatibleDaemon/view.jsx
Normal file
29
ui/js/component/modalIncompatibleDaemon/view.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
import Link from "component/link";
|
||||
|
||||
class ModalIncompatibleDaemon extends React.PureComponent {
|
||||
render() {
|
||||
const { quit } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
contentLabel={__("Incompatible daemon running")}
|
||||
type="alert"
|
||||
confirmButtonLabel={__("Quit")}
|
||||
onConfirmed={quit}
|
||||
>
|
||||
{__(
|
||||
"This browser is running with an incompatible version of the LBRY protocol and your install must be repaired. "
|
||||
)}
|
||||
<Link
|
||||
label={__("Learn more")}
|
||||
href="https://lbry.io/faq/incompatible-protocol-version"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalIncompatibleDaemon;
|
16
ui/js/component/modalInsufficientCredits/index.js
Normal file
16
ui/js/component/modalInsufficientCredits/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal, doNavigate } from "actions/app";
|
||||
import ModalInsufficientCredits from "./view";
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
const perform = dispatch => ({
|
||||
addFunds: () => {
|
||||
dispatch(doNavigate("/rewards"));
|
||||
dispatch(doCloseModal());
|
||||
},
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ModalInsufficientCredits);
|
24
ui/js/component/modalInsufficientCredits/view.jsx
Normal file
24
ui/js/component/modalInsufficientCredits/view.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
|
||||
class ModalInsufficientCredits extends React.PureComponent {
|
||||
render() {
|
||||
const { addFunds, closeModal } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
type="confirm"
|
||||
contentLabel={__("Not enough credits")}
|
||||
confirmButtonLabel={__("Get Credits")}
|
||||
abortButtonLabel={__("Cancel")}
|
||||
onAborted={closeModal}
|
||||
onConfirmed={addFunds}
|
||||
>
|
||||
{__("More LBRY credits are required to purchase this.")}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalInsufficientCredits;
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doCloseModal, doHistoryBack } from "actions/app";
|
||||
import { doDeleteFile } from "actions/file_info";
|
||||
import { doDeleteFileAndGoBack } from "actions/file_info";
|
||||
import { makeSelectClaimForUriIsMine } from "selectors/claims";
|
||||
import batchActions from "util/batchActions";
|
||||
|
||||
import ModalRemoveFile from "./view";
|
||||
|
||||
|
@ -19,8 +20,7 @@ const makeSelect = () => {
|
|||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doCloseModal()),
|
||||
deleteFile: (fileInfo, deleteFromComputer, abandonClaim) => {
|
||||
dispatch(doHistoryBack());
|
||||
dispatch(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim));
|
||||
dispatch(doDeleteFileAndGoBack(fileInfo, deleteFromComputer, abandonClaim));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
import { FormField } from "component/form.js";
|
||||
import FormField from "component/formField";
|
||||
|
||||
class ModalRemoveFile extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { Modal } from "component/modal";
|
||||
import { downloadUpgrade, skipUpgrade } from "actions/app";
|
||||
|
||||
class ModalUpgrade extends React.PureComponent {
|
||||
render() {
|
||||
|
|
|
@ -1,28 +1,40 @@
|
|||
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,
|
||||
selectTotalRewardValue,
|
||||
} 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 }),
|
||||
totalRewardValue: selectTotalRewardValue(state),
|
||||
};
|
||||
};
|
||||
|
||||
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("/discover"));
|
||||
},
|
||||
closeModal: closeModal,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ModalWelcome);
|
||||
|
|
|
@ -4,14 +4,24 @@ import { CreditAmount } from "component/common";
|
|||
import Link from "component/link";
|
||||
import RewardLink from "component/rewardLink";
|
||||
|
||||
class WelcomeModal extends React.PureComponent {
|
||||
render() {
|
||||
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props;
|
||||
class ModalWelcome extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFirstScreen: true,
|
||||
};
|
||||
}
|
||||
|
||||
return !hasClaimed
|
||||
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
|
||||
render() {
|
||||
const { closeModal, totalRewardValue, verifyAccount } = this.props;
|
||||
|
||||
const totalRewardRounded = Math.round(totalRewardValue / 10) * 10;
|
||||
|
||||
return (
|
||||
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
|
||||
{this.state.isFirstScreen &&
|
||||
<section>
|
||||
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
|
||||
<h3 className="modal__header">{__("Welcome to LBRY")}</h3>
|
||||
<p>
|
||||
{__(
|
||||
"Using LBRY is like dating a centaur. Totally normal up top, and"
|
||||
|
@ -24,58 +34,51 @@ class WelcomeModal extends React.PureComponent {
|
|||
"Below, LBRY is controlled by users -- you -- via blockchain and decentralization."
|
||||
)}
|
||||
</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 className="modal__buttons">
|
||||
<Link
|
||||
button="primary"
|
||||
onClick={() => {
|
||||
this.setState({ isFirstScreen: false });
|
||||
}}
|
||||
label={__("Continue")}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
: <Modal
|
||||
type="alert"
|
||||
overlayClassName="modal-overlay modal-overlay--clear"
|
||||
isOpen={true}
|
||||
contentLabel={__("Welcome to LBRY")}
|
||||
onConfirmed={closeModal}
|
||||
>
|
||||
</section>}
|
||||
{!this.state.isFirstScreen &&
|
||||
<section>
|
||||
<h3 className="modal__header">{__("About Your Reward")}</h3>
|
||||
<h3 className="modal__header">{__("Claim Your Credits")}</h3>
|
||||
<p>
|
||||
{__("You earned a reward of")}
|
||||
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
|
||||
{" "}{__("LBRY credits, or")} <em>{__("LBC")}</em>.
|
||||
The LBRY network is controlled and powered by credits called{" "}
|
||||
<em>LBC</em>, a blockchain asset.
|
||||
</p>
|
||||
<p>
|
||||
{__("New patrons receive ")} {" "}
|
||||
{totalRewardValue
|
||||
? <CreditAmount amount={totalRewardRounded} />
|
||||
: <span className="credit-amount">{__("credits")}</span>}
|
||||
{" "} {__("in rewards for usage and influence of the network.")}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"This reward will show in your Wallet momentarily, probably while you are reading this message."
|
||||
"You'll also earn weekly bonuses for checking out the greatest new stuff."
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"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>;
|
||||
<div className="modal__buttons">
|
||||
<Link
|
||||
button="primary"
|
||||
onClick={verifyAccount}
|
||||
label={__("You Had Me At Free LBC")}
|
||||
/>
|
||||
<Link
|
||||
button="alt"
|
||||
onClick={closeModal}
|
||||
label={__("I Burn Money")}
|
||||
/>
|
||||
</div>
|
||||
</section>}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WelcomeModal;
|
||||
export default ModalWelcome;
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
export class Notice extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isError: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isError: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section
|
||||
className={
|
||||
"notice " +
|
||||
(this.props.isError ? "notice--error " : "") +
|
||||
(this.props.className || "")
|
||||
}
|
||||
>
|
||||
{this.props.children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Notice;
|
5
ui/js/component/publishForm/index.js
Normal file
5
ui/js/component/publishForm/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PublishForm from "./view";
|
||||
|
||||
export default connect(null, null)(PublishForm);
|
177
ui/js/component/publishForm/internal/channelSection.jsx
Normal file
177
ui/js/component/publishForm/internal/channelSection.jsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
import React from "react";
|
||||
import lbryuri from "lbryuri";
|
||||
import { FormRow } from "component/form.js";
|
||||
import { BusyMessage } from "component/common";
|
||||
import Link from "component/link";
|
||||
|
||||
class ChannelSection extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newChannelName: "@",
|
||||
newChannelBid: 10,
|
||||
addingChannel: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleChannelChange(event) {
|
||||
const channel = event.target.value;
|
||||
if (channel === "new") this.setState({ addingChannel: true });
|
||||
else {
|
||||
this.setState({ addingChannel: false });
|
||||
this.props.handleChannelChange(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
handleNewChannelNameChange(event) {
|
||||
const newChannelName = event.target.value.startsWith("@")
|
||||
? event.target.value
|
||||
: "@" + event.target.value;
|
||||
|
||||
if (
|
||||
newChannelName.length > 1 &&
|
||||
!lbryuri.isValidName(newChannelName.substr(1), false)
|
||||
) {
|
||||
this.refs.newChannelName.showError(
|
||||
__("LBRY channel names must contain only letters, numbers and dashes.")
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
this.refs.newChannelName.clearError();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newChannelName,
|
||||
});
|
||||
}
|
||||
|
||||
handleNewChannelBidChange(event) {
|
||||
this.setState({
|
||||
newChannelBid: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleCreateChannelClick(event) {
|
||||
if (this.state.newChannelName.length < 5) {
|
||||
this.refs.newChannelName.showError(
|
||||
__("LBRY channel names must be at least 4 characters in length.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
creatingChannel: true,
|
||||
});
|
||||
|
||||
const newChannelName = this.state.newChannelName;
|
||||
const amount = parseFloat(this.state.newChannelBid);
|
||||
this.setState({
|
||||
creatingChannel: true,
|
||||
});
|
||||
const success = () => {
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
addingChannel: false,
|
||||
channel: newChannelName,
|
||||
});
|
||||
this.props.handleChannelChange(newChannelName);
|
||||
};
|
||||
const failure = err => {
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
});
|
||||
this.refs.newChannelName.showError(
|
||||
__("Unable to create channel due to an internal error.")
|
||||
);
|
||||
};
|
||||
this.props.createChannel(newChannelName, amount).then(success, failure);
|
||||
}
|
||||
|
||||
render() {
|
||||
const lbcInputHelp = __(
|
||||
"This LBC remains yours and the deposit can be undone at any time."
|
||||
);
|
||||
|
||||
const channel = this.state.addingChannel ? "new" : this.props.channel;
|
||||
const { fetchingChannels, channels = [] } = this.props;
|
||||
|
||||
let channelContent = [];
|
||||
channelContent.push(
|
||||
<FormRow
|
||||
key="channel"
|
||||
type="select"
|
||||
tabIndex="1"
|
||||
onChange={this.handleChannelChange.bind(this)}
|
||||
value={channel}
|
||||
>
|
||||
<option key="anonymous" value="anonymous">
|
||||
{__("Anonymous")}
|
||||
</option>
|
||||
{this.props.channels.map(({ name }) =>
|
||||
<option key={name} value={name}>{name}</option>
|
||||
)}
|
||||
<option key="new" value="new">
|
||||
{__("New channel...")}
|
||||
</option>
|
||||
</FormRow>
|
||||
);
|
||||
if (fetchingChannels) {
|
||||
channelContent.push(
|
||||
<BusyMessage message="Updating channels" key="loading" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Channel Name")}</h4>
|
||||
<div className="card__subtitle">
|
||||
{__(
|
||||
"This is a username or handle that your content can be found under."
|
||||
)}
|
||||
{" "}
|
||||
{__("Ex. @Marvel, @TheBeatles, @BooksByJoe")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{channelContent}
|
||||
</div>
|
||||
{this.state.addingChannel &&
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Name")}
|
||||
type="text"
|
||||
onChange={this.handleNewChannelNameChange.bind(this)}
|
||||
value={this.state.newChannelName}
|
||||
/>
|
||||
<FormRow
|
||||
label={__("Deposit")}
|
||||
postfix="LBC"
|
||||
step="0.1"
|
||||
min="0"
|
||||
type="number"
|
||||
helper={lbcInputHelp}
|
||||
ref="newChannelName"
|
||||
onChange={this.handleNewChannelBidChange.bind(this)}
|
||||
value={this.state.newChannelBid}
|
||||
/>
|
||||
<div className="form-row-submit">
|
||||
<Link
|
||||
button="primary"
|
||||
label={
|
||||
!this.state.creatingChannel
|
||||
? __("Create channel")
|
||||
: __("Creating channel...")
|
||||
}
|
||||
onClick={this.handleCreateChannelClick.bind(this)}
|
||||
disabled={this.state.creatingChannel}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelSection;
|
926
ui/js/component/publishForm/view.jsx
Normal file
926
ui/js/component/publishForm/view.jsx
Normal file
|
@ -0,0 +1,926 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry";
|
||||
import lbryuri from "lbryuri";
|
||||
import FormField from "component/formField";
|
||||
import { FormRow } from "component/form.js";
|
||||
import Link from "component/link";
|
||||
import FormFieldPrice from "component/formFieldPrice";
|
||||
import Modal from "component/modal";
|
||||
import { BusyMessage } from "component/common";
|
||||
import ChannelSection from "./internal/channelSection";
|
||||
|
||||
class PublishForm extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._requiredFields = ["name", "bid", "meta_title", "tosAgree"];
|
||||
|
||||
this._defaultCopyrightNotice = "All rights reserved.";
|
||||
|
||||
this.state = {
|
||||
rawName: "",
|
||||
name: "",
|
||||
bid: 10,
|
||||
hasFile: false,
|
||||
feeAmount: "",
|
||||
feeCurrency: "LBC",
|
||||
channel: "anonymous",
|
||||
newChannelName: "@",
|
||||
newChannelBid: 10,
|
||||
meta_title: "",
|
||||
meta_thumbnail: "",
|
||||
meta_description: "",
|
||||
meta_language: "en",
|
||||
meta_nsfw: "0",
|
||||
licenseType: "",
|
||||
copyrightNotice: this._defaultCopyrightNotice,
|
||||
otherLicenseDescription: "",
|
||||
otherLicenseUrl: "",
|
||||
tosAgree: false,
|
||||
prefillDone: false,
|
||||
uploadProgress: 0.0,
|
||||
uploaded: false,
|
||||
errorMessage: null,
|
||||
submitting: false,
|
||||
creatingChannel: false,
|
||||
modal: null,
|
||||
isFee: false,
|
||||
customUrl: false,
|
||||
};
|
||||
}
|
||||
|
||||
_updateChannelList(channel) {
|
||||
const { fetchingChannels, fetchChannelListMine } = this.props;
|
||||
|
||||
if (!fetchingChannels) fetchChannelListMine();
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
if (typeof event !== "undefined") {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
submitting: true,
|
||||
});
|
||||
|
||||
let checkFields = this._requiredFields;
|
||||
if (!this.myClaimExists()) {
|
||||
checkFields.unshift("file");
|
||||
}
|
||||
|
||||
let missingFieldFound = false;
|
||||
for (let fieldName of checkFields) {
|
||||
const field = this.refs[fieldName];
|
||||
if (field) {
|
||||
if (field.getValue() === "" || field.getValue() === false) {
|
||||
field.showRequiredError();
|
||||
if (!missingFieldFound) {
|
||||
field.focus();
|
||||
missingFieldFound = true;
|
||||
}
|
||||
} else {
|
||||
field.clearError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFieldFound) {
|
||||
this.setState({
|
||||
submitting: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let metadata = {};
|
||||
|
||||
for (let metaField of ["title", "description", "thumbnail", "language"]) {
|
||||
const value = this.state["meta_" + metaField];
|
||||
if (value) {
|
||||
metadata[metaField] = value;
|
||||
}
|
||||
}
|
||||
|
||||
metadata.license = this.getLicense();
|
||||
metadata.licenseUrl = this.getLicenseUrl();
|
||||
metadata.nsfw = !!parseInt(this.state.meta_nsfw);
|
||||
|
||||
var doPublish = () => {
|
||||
var publishArgs = {
|
||||
name: this.state.name,
|
||||
bid: parseFloat(this.state.bid),
|
||||
metadata: metadata,
|
||||
...(this.state.channel != "new" && this.state.channel != "anonymous"
|
||||
? { channel_name: this.state.channel }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (this.refs.file.getValue() !== "") {
|
||||
publishArgs.file_path = this.refs.file.getValue();
|
||||
}
|
||||
|
||||
const success = claim => {};
|
||||
const failure = error => this.handlePublishError(error);
|
||||
|
||||
this.handlePublishStarted();
|
||||
this.props.publish(publishArgs).then(success, failure);
|
||||
};
|
||||
|
||||
if (this.state.isFee && parseFloat(this.state.feeAmount) > 0) {
|
||||
lbry.wallet_unused_address().then(address => {
|
||||
metadata.fee = {
|
||||
currency: this.state.feeCurrency,
|
||||
amount: parseFloat(this.state.feeAmount),
|
||||
address: address,
|
||||
};
|
||||
|
||||
doPublish();
|
||||
});
|
||||
} else {
|
||||
doPublish();
|
||||
}
|
||||
}
|
||||
|
||||
handlePublishStarted() {
|
||||
this.setState({
|
||||
modal: "publishStarted",
|
||||
});
|
||||
}
|
||||
|
||||
handlePublishStartedConfirmed() {
|
||||
this.props.navigate("/published");
|
||||
}
|
||||
|
||||
handlePublishError(error) {
|
||||
this.setState({
|
||||
submitting: false,
|
||||
modal: "error",
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
claim() {
|
||||
const { claimsByUri } = this.props;
|
||||
const { uri } = this.state;
|
||||
|
||||
return claimsByUri[uri];
|
||||
}
|
||||
|
||||
topClaimValue() {
|
||||
if (!this.claim()) return null;
|
||||
|
||||
return parseFloat(this.claim().amount);
|
||||
}
|
||||
|
||||
myClaimExists() {
|
||||
const { myClaims } = this.props;
|
||||
const { name } = this.state;
|
||||
|
||||
if (!name) return false;
|
||||
|
||||
return !!myClaims.find(claim => claim.name === name);
|
||||
}
|
||||
|
||||
topClaimIsMine() {
|
||||
const myClaimInfo = this.myClaimInfo();
|
||||
const { claimsByUri } = this.props;
|
||||
const { uri } = this.state;
|
||||
|
||||
if (!uri) return null;
|
||||
|
||||
const claim = claimsByUri[uri];
|
||||
|
||||
if (!claim) return true;
|
||||
if (!myClaimInfo) return false;
|
||||
|
||||
return myClaimInfo.amount >= claim.amount;
|
||||
}
|
||||
|
||||
myClaimInfo() {
|
||||
const { name } = this.state;
|
||||
|
||||
return Object.values(this.props.myClaims).find(
|
||||
claim => claim.name === name
|
||||
);
|
||||
}
|
||||
|
||||
handleNameChange(event) {
|
||||
var rawName = event.target.value;
|
||||
this.setState({
|
||||
customUrl: Boolean(rawName.length),
|
||||
});
|
||||
|
||||
this.nameChanged(rawName);
|
||||
}
|
||||
|
||||
nameChanged(rawName) {
|
||||
if (!rawName) {
|
||||
this.setState({
|
||||
rawName: "",
|
||||
name: "",
|
||||
uri: "",
|
||||
prefillDone: false,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lbryuri.isValidName(rawName, false)) {
|
||||
this.refs.name.showError(
|
||||
__("LBRY names must contain only letters, numbers and dashes.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let channel = "";
|
||||
if (this.state.channel !== "anonymous") channel = this.state.channel;
|
||||
|
||||
const name = rawName.toLowerCase();
|
||||
const uri = lbryuri.build({ contentName: name, channelName: channel });
|
||||
this.setState({
|
||||
rawName: rawName,
|
||||
name: name,
|
||||
prefillDone: false,
|
||||
uri,
|
||||
});
|
||||
|
||||
if (this.resolveUriTimeout) {
|
||||
clearTimeout(this.resolveUriTimeout);
|
||||
this.resolveUriTimeout = undefined;
|
||||
}
|
||||
const resolve = () => this.props.resolveUri(uri);
|
||||
|
||||
this.resolveUriTimeout = setTimeout(resolve.bind(this), 500, {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
handlePrefillClicked() {
|
||||
const claimInfo = this.myClaimInfo();
|
||||
const {
|
||||
license,
|
||||
licenseUrl,
|
||||
title,
|
||||
thumbnail,
|
||||
description,
|
||||
language,
|
||||
nsfw,
|
||||
} = claimInfo.value.stream.metadata;
|
||||
|
||||
let newState = {
|
||||
meta_title: title,
|
||||
meta_thumbnail: thumbnail,
|
||||
meta_description: description,
|
||||
meta_language: language,
|
||||
meta_nsfw: nsfw,
|
||||
prefillDone: true,
|
||||
bid: claimInfo.amount,
|
||||
};
|
||||
|
||||
if (license == this._defaultCopyrightNotice) {
|
||||
newState.licenseType = "copyright";
|
||||
newState.copyrightNotice = this._defaultCopyrightNotice;
|
||||
} else {
|
||||
// If the license URL or description matches one of the drop-down options, use that
|
||||
let licenseType = "other"; // Will be overridden if we find a match
|
||||
for (let option of this._meta_license.getOptions()) {
|
||||
if (
|
||||
option.getAttribute("data-url") === licenseUrl ||
|
||||
option.text === license
|
||||
) {
|
||||
licenseType = option.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (licenseType == "other") {
|
||||
newState.otherLicenseDescription = license;
|
||||
newState.otherLicenseUrl = licenseUrl;
|
||||
}
|
||||
newState.licenseType = licenseType;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
handleBidChange(event) {
|
||||
this.setState({
|
||||
bid: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeChange(newValue) {
|
||||
this.setState({
|
||||
feeAmount: newValue.amount,
|
||||
feeCurrency: newValue.currency,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeePrefChange(feeEnabled) {
|
||||
this.setState({
|
||||
isFee: feeEnabled,
|
||||
feeAmount: this.state.feeAmount == "" ? "5.00" : this.state.feeAmount,
|
||||
});
|
||||
}
|
||||
|
||||
handleMetadataChange(event) {
|
||||
/**
|
||||
* This function is used for all metadata inputs that store the final value directly into state.
|
||||
* The only exceptions are inputs related to license description and license URL, which require
|
||||
* more complex logic and the final value is determined at submit time.
|
||||
*/
|
||||
this.setState({
|
||||
["meta_" + event.target.name]: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleDescriptionChanged(text) {
|
||||
this.setState({
|
||||
meta_description: text,
|
||||
});
|
||||
}
|
||||
|
||||
handleLicenseTypeChange(event) {
|
||||
this.setState({
|
||||
licenseType: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleCopyrightNoticeChange(event) {
|
||||
this.setState({
|
||||
copyrightNotice: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleOtherLicenseDescriptionChange(event) {
|
||||
this.setState({
|
||||
otherLicenseDescription: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleOtherLicenseUrlChange(event) {
|
||||
this.setState({
|
||||
otherLicenseUrl: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleChannelChange(channelName) {
|
||||
this.setState({
|
||||
channel: channelName,
|
||||
});
|
||||
const nameChanged = () => this.nameChanged(this.state.rawName);
|
||||
setTimeout(nameChanged.bind(this), 500, { once: true });
|
||||
}
|
||||
|
||||
handleTOSChange(event) {
|
||||
this.setState({
|
||||
tosAgree: event.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleCreateChannelClick(event) {
|
||||
if (this.state.newChannelName.length < 5) {
|
||||
this.refs.newChannelName.showError(
|
||||
__("LBRY channel names must be at least 4 characters in length.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
creatingChannel: true,
|
||||
});
|
||||
|
||||
const newChannelName = this.state.newChannelName;
|
||||
lbry
|
||||
.channel_new({
|
||||
channel_name: newChannelName,
|
||||
amount: parseFloat(this.state.newChannelBid),
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
});
|
||||
|
||||
this._updateChannelList(newChannelName);
|
||||
}, 10000);
|
||||
},
|
||||
error => {
|
||||
// TODO: better error handling
|
||||
this.refs.newChannelName.showError(
|
||||
__("Unable to create channel due to an internal error.")
|
||||
);
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getLicense() {
|
||||
switch (this.state.licenseType) {
|
||||
case "copyright":
|
||||
return this.state.copyrightNotice;
|
||||
case "other":
|
||||
return this.state.otherLicenseDescription;
|
||||
default:
|
||||
return this._meta_license.getSelectedElement().text;
|
||||
}
|
||||
}
|
||||
|
||||
getLicenseUrl() {
|
||||
switch (this.state.licenseType) {
|
||||
case "copyright":
|
||||
return "";
|
||||
case "other":
|
||||
return this.state.otherLicenseUrl;
|
||||
default:
|
||||
return this._meta_license.getSelectedElement().getAttribute("data-url");
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchClaimListMine();
|
||||
this._updateChannelList();
|
||||
}
|
||||
|
||||
onFileChange() {
|
||||
if (this.refs.file.getValue()) {
|
||||
this.setState({ hasFile: true });
|
||||
if (!this.state.customUrl) {
|
||||
let fileName = this._getFileName(this.refs.file.getValue());
|
||||
this.nameChanged(fileName);
|
||||
}
|
||||
} else {
|
||||
this.setState({ hasFile: false });
|
||||
}
|
||||
}
|
||||
|
||||
_getFileName(fileName) {
|
||||
const path = require("path");
|
||||
const extension = path.extname(fileName);
|
||||
|
||||
fileName = path.basename(fileName, extension);
|
||||
fileName = fileName.replace(lbryuri.REGEXP_INVALID_URI, "");
|
||||
return fileName;
|
||||
}
|
||||
|
||||
getNameBidHelpText() {
|
||||
if (this.state.prefillDone) {
|
||||
return __("Existing claim data was prefilled");
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.uri &&
|
||||
this.props.resolvingUris.indexOf(this.state.uri) !== -1 &&
|
||||
this.claim() === undefined
|
||||
) {
|
||||
return __("Checking...");
|
||||
} else if (!this.state.name) {
|
||||
return __("Select a URL for this publish.");
|
||||
} else if (!this.claim()) {
|
||||
return __("This URL is unused.");
|
||||
} else if (this.myClaimExists() && !this.state.prefillDone) {
|
||||
return (
|
||||
<span>
|
||||
{__("You already have a claim with this name.")}{" "}
|
||||
<Link
|
||||
label={__("Use data from my existing claim")}
|
||||
onClick={() => this.handlePrefillClicked()}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (this.claim()) {
|
||||
if (this.topClaimValue() === 1) {
|
||||
return (
|
||||
<span>
|
||||
{__(
|
||||
'A deposit of at least one credit is required to win "%s". However, you can still get a permanent URL for any amount.',
|
||||
this.state.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span>
|
||||
{__(
|
||||
'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.',
|
||||
this.topClaimValue(),
|
||||
this.state.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const lbcInputHelp = __(
|
||||
"This LBC remains yours and the deposit can be undone at any time."
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<form
|
||||
onSubmit={event => {
|
||||
this.handleSubmit(event);
|
||||
}}
|
||||
>
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Content")}</h4>
|
||||
<div className="card__subtitle">
|
||||
{__("What are you publishing?")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
name="file"
|
||||
label="File"
|
||||
ref="file"
|
||||
type="file"
|
||||
onChange={event => {
|
||||
this.onFileChange(event);
|
||||
}}
|
||||
helper={
|
||||
this.myClaimExists()
|
||||
? __(
|
||||
"If you don't choose a file, the file from your existing claim will be used."
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!this.state.hasFile && !this.myClaimExists()
|
||||
? null
|
||||
: <div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Title")}
|
||||
type="text"
|
||||
name="title"
|
||||
value={this.state.meta_title}
|
||||
placeholder="Titular Title"
|
||||
onChange={event => {
|
||||
this.handleMetadataChange(event);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="text"
|
||||
label={__("Thumbnail URL")}
|
||||
name="thumbnail"
|
||||
value={this.state.meta_thumbnail}
|
||||
placeholder="http://spee.ch/mylogo"
|
||||
onChange={event => {
|
||||
this.handleMetadataChange(event);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Description")}
|
||||
type="SimpleMDE"
|
||||
ref="meta_description"
|
||||
name="description"
|
||||
value={this.state.meta_description}
|
||||
placeholder={__("Description of your content")}
|
||||
onChange={text => {
|
||||
this.handleDescriptionChanged(text);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Language")}
|
||||
type="select"
|
||||
value={this.state.meta_language}
|
||||
name="language"
|
||||
onChange={event => {
|
||||
this.handleMetadataChange(event);
|
||||
}}
|
||||
>
|
||||
<option value="en">{__("English")}</option>
|
||||
<option value="zh">{__("Chinese")}</option>
|
||||
<option value="fr">{__("French")}</option>
|
||||
<option value="de">{__("German")}</option>
|
||||
<option value="jp">{__("Japanese")}</option>
|
||||
<option value="ru">{__("Russian")}</option>
|
||||
<option value="es">{__("Spanish")}</option>
|
||||
</FormRow>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="select"
|
||||
label={__("Maturity")}
|
||||
value={this.state.meta_nsfw}
|
||||
name="nsfw"
|
||||
onChange={event => {
|
||||
this.handleMetadataChange(event);
|
||||
}}
|
||||
>
|
||||
{/* <option value=""></option> */}
|
||||
<option value="0">{__("All Ages")}</option>
|
||||
<option value="1">{__("Adults Only")}</option>
|
||||
</FormRow>
|
||||
</div>
|
||||
</div>}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Access")}</h4>
|
||||
<div className="card__subtitle">
|
||||
{__("How much does this content cost?")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="form-row__label-row">
|
||||
<label className="form-row__label">{__("Price")}</label>
|
||||
</div>
|
||||
<FormRow
|
||||
label={__("Free")}
|
||||
type="radio"
|
||||
name="isFree"
|
||||
onChange={() => this.handleFeePrefChange(false)}
|
||||
checked={!this.state.isFee}
|
||||
/>
|
||||
<FormField
|
||||
type="radio"
|
||||
name="isFree"
|
||||
label={!this.state.isFee ? __("Choose price...") : __("Price ")}
|
||||
onChange={() => {
|
||||
this.handleFeePrefChange(true);
|
||||
}}
|
||||
checked={this.state.isFee}
|
||||
/>
|
||||
<span className={!this.state.isFee ? "hidden" : ""}>
|
||||
{/*min=0.01 caused weird interactions with step (e.g. down from 5 equals 4.91 rather than 4.9) */}
|
||||
<FormFieldPrice
|
||||
min="0"
|
||||
step="0.1"
|
||||
defaultValue={{ amount: 5.0, currency: "LBC" }}
|
||||
onChange={val => this.handleFeeChange(val)}
|
||||
/>
|
||||
</span>
|
||||
{this.state.isFee && this.state.feeCurrency.toUpperCase() != "LBC"
|
||||
? <div className="form-field__helper">
|
||||
{__(
|
||||
"All content fees are charged in LBC. For non-LBC payment methods, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase."
|
||||
)}
|
||||
</div>
|
||||
: null}
|
||||
<FormRow
|
||||
label="License"
|
||||
type="select"
|
||||
value={this.state.licenseType}
|
||||
ref={row => {
|
||||
this._meta_license = row;
|
||||
}}
|
||||
onChange={event => {
|
||||
this.handleLicenseTypeChange(event);
|
||||
}}
|
||||
>
|
||||
<option />
|
||||
<option value="publicDomain">{__("Public Domain")}</option>
|
||||
<option
|
||||
value="cc-by"
|
||||
data-url="https://creativecommons.org/licenses/by/4.0/legalcode"
|
||||
>
|
||||
{__("Creative Commons Attribution 4.0 International")}
|
||||
</option>
|
||||
<option
|
||||
value="cc-by-sa"
|
||||
data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode"
|
||||
>
|
||||
{__(
|
||||
"Creative Commons Attribution-ShareAlike 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option
|
||||
value="cc-by-nd"
|
||||
data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode"
|
||||
>
|
||||
{__(
|
||||
"Creative Commons Attribution-NoDerivatives 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option
|
||||
value="cc-by-nc"
|
||||
data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode"
|
||||
>
|
||||
{__(
|
||||
"Creative Commons Attribution-NonCommercial 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option
|
||||
value="cc-by-nc-sa"
|
||||
data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode"
|
||||
>
|
||||
{__(
|
||||
"Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option
|
||||
value="cc-by-nc-nd"
|
||||
data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode"
|
||||
>
|
||||
{__(
|
||||
"Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International"
|
||||
)}
|
||||
</option>
|
||||
<option value="copyright">
|
||||
{__("Copyrighted...")}
|
||||
</option>
|
||||
<option value="other">
|
||||
{__("Other...")}
|
||||
</option>
|
||||
</FormRow>
|
||||
|
||||
{this.state.licenseType == "copyright"
|
||||
? <FormRow
|
||||
label={__("Copyright notice")}
|
||||
type="text"
|
||||
name="copyright-notice"
|
||||
value={this.state.copyrightNotice}
|
||||
onChange={event => {
|
||||
this.handleCopyrightNoticeChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{this.state.licenseType == "other"
|
||||
? <FormRow
|
||||
label={__("License description")}
|
||||
type="text"
|
||||
name="other-license-description"
|
||||
value={this.state.otherLicenseDescription}
|
||||
onChange={event => {
|
||||
this.handleOtherLicenseDescriptionChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{this.state.licenseType == "other"
|
||||
? <FormRow
|
||||
label={__("License URL")}
|
||||
type="text"
|
||||
name="other-license-url"
|
||||
value={this.state.otherLicenseUrl}
|
||||
onChange={event => {
|
||||
this.handleOtherLicenseUrlChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ChannelSection
|
||||
{...this.props}
|
||||
handleChannelChange={this.handleChannelChange.bind(this)}
|
||||
channel={this.state.channel}
|
||||
/>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Content URL")}</h4>
|
||||
<div className="card__subtitle">
|
||||
{__(
|
||||
"This is the exact address where people find your content (ex. lbry://myvideo)."
|
||||
)}
|
||||
{" "}
|
||||
<Link
|
||||
label={__("Learn more")}
|
||||
href="https://lbry.io/faq/naming"
|
||||
/>.
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
prefix={`lbry://${this.state.channel === "anonymous"
|
||||
? ""
|
||||
: `${this.state.channel}/`}`}
|
||||
type="text"
|
||||
ref="name"
|
||||
placeholder="myname"
|
||||
value={this.state.rawName}
|
||||
onChange={event => {
|
||||
this.handleNameChange(event);
|
||||
}}
|
||||
helper={this.getNameBidHelpText()}
|
||||
/>
|
||||
</div>
|
||||
{this.state.rawName
|
||||
? <div className="card__content">
|
||||
<FormRow
|
||||
ref="bid"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label={__("Deposit")}
|
||||
postfix="LBC"
|
||||
onChange={event => {
|
||||
this.handleBidChange(event);
|
||||
}}
|
||||
value={this.state.bid}
|
||||
placeholder={this.claim() ? this.topClaimValue() + 10 : 100}
|
||||
helper={lbcInputHelp}
|
||||
/>
|
||||
</div>
|
||||
: ""}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h4>{__("Terms of Service")}</h4>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={
|
||||
<span>
|
||||
{__("I agree to the")}
|
||||
{" "}
|
||||
<Link
|
||||
href="https://www.lbry.io/termsofservice"
|
||||
label={__("LBRY terms of service")}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
type="checkbox"
|
||||
checked={this.state.tosAgree}
|
||||
onChange={event => {
|
||||
this.handleTOSChange(event);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card-series-submit">
|
||||
<Link
|
||||
button="primary"
|
||||
label={
|
||||
!this.state.submitting ? __("Publish") : __("Publishing...")
|
||||
}
|
||||
onClick={event => {
|
||||
this.handleSubmit(event);
|
||||
}}
|
||||
disabled={
|
||||
this.state.submitting ||
|
||||
(this.state.uri &&
|
||||
this.props.resolvingUris.indexOf(this.state.uri) !== -1) ||
|
||||
(this.claim() &&
|
||||
!this.topClaimIsMine() &&
|
||||
this.state.bid <= this.topClaimValue())
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
button="cancel"
|
||||
onClick={this.props.back}
|
||||
label={__("Cancel")}
|
||||
/>
|
||||
<input type="submit" className="hidden" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Modal
|
||||
isOpen={this.state.modal == "publishStarted"}
|
||||
contentLabel={__("File published")}
|
||||
onConfirmed={event => {
|
||||
this.handlePublishStartedConfirmed(event);
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
{__("Your file has been published to LBRY at the address")}
|
||||
{" "}<code>{this.state.uri}</code>!
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
'The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.'
|
||||
)}
|
||||
</p>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={this.state.modal == "error"}
|
||||
contentLabel={__("Error publishing file")}
|
||||
onConfirmed={event => {
|
||||
this.closeModal(event);
|
||||
}}
|
||||
>
|
||||
{__(
|
||||
"The following error occurred when attempting to publish your file"
|
||||
)}: {this.state.errorMessage}
|
||||
</Modal>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishForm;
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
makeSelectHasClaimedReward,
|
||||
makeSelectClaimRewardError,
|
||||
makeSelectRewardByType,
|
||||
makeSelectIsRewardClaimPending,
|
||||
|
@ -11,13 +10,11 @@ import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
|
|||
import RewardLink from "./view";
|
||||
|
||||
const makeSelect = () => {
|
||||
const selectHasClaimedReward = makeSelectHasClaimedReward();
|
||||
const selectIsPending = makeSelectIsRewardClaimPending();
|
||||
const selectReward = makeSelectRewardByType();
|
||||
const selectError = makeSelectClaimRewardError();
|
||||
|
||||
const select = (state, props) => ({
|
||||
isClaimed: selectHasClaimedReward(state, props),
|
||||
errorMessage: selectError(state, props),
|
||||
isPending: selectIsPending(state, props),
|
||||
reward: selectReward(state, props),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import { Icon } from "component/common";
|
||||
import Modal from "component/modal";
|
||||
import Link from "component/link";
|
||||
|
||||
|
@ -10,22 +9,19 @@ const RewardLink = props => {
|
|||
claimReward,
|
||||
clearError,
|
||||
errorMessage,
|
||||
isClaimed,
|
||||
isPending,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="reward-link">
|
||||
{isClaimed
|
||||
? <span><Icon icon="icon-check" /> Reward claimed.</span>
|
||||
: <Link
|
||||
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}
|
||||
|
|
|
@ -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} />,
|
||||
channel: <ChannelPage {...params} />,
|
||||
publish: <PublishPage {...params} />,
|
||||
developer: <DeveloperPage {...params} />,
|
||||
discover: <DiscoverPage {...params} />,
|
||||
rewards: <RewardsPage {...params} />,
|
||||
search: <SearchPage {...params} />,
|
||||
auth: <AuthPage params={params} />,
|
||||
channel: <ChannelPage params={params} />,
|
||||
developer: <DeveloperPage params={params} />,
|
||||
discover: <DiscoverPage params={params} />,
|
||||
downloaded: <FileListDownloaded params={params} />,
|
||||
help: <HelpPage params={params} />,
|
||||
publish: <PublishPage params={params} />,
|
||||
published: <FileListPublished params={params} />,
|
||||
receive: <WalletPage params={params} />,
|
||||
report: <ReportPage params={params} />,
|
||||
rewards: <RewardsPage params={params} />,
|
||||
search: <SearchPage params={params} />,
|
||||
send: <WalletPage params={params} />,
|
||||
settings: <SettingsPage params={params} />,
|
||||
show: <ShowPage params={params} />,
|
||||
start: <StartPage params={params} />,
|
||||
wallet: <WalletPage params={params} />,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import React from "react";
|
||||
import lbry from "../lbry.js";
|
||||
import LoadScreen from "./load_screen.js";
|
||||
|
||||
export class SplashScreen extends React.PureComponent {
|
||||
static propTypes = {
|
||||
message: React.PropTypes.string,
|
||||
onLoadDone: React.PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
details: __("Starting daemon"),
|
||||
message: __("Connecting"),
|
||||
isLagging: false,
|
||||
};
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
lbry.status().then(status => {
|
||||
this._updateStatusCallback(status);
|
||||
});
|
||||
}
|
||||
|
||||
_updateStatusCallback(status) {
|
||||
const startupStatus = status.startup_status;
|
||||
if (startupStatus.code == "started") {
|
||||
// Wait until we are able to resolve a name before declaring
|
||||
// that we are done.
|
||||
// TODO: This is a hack, and the logic should live in the daemon
|
||||
// to give us a better sense of when we are actually started
|
||||
this.setState({
|
||||
message: __("Testing Network"),
|
||||
details: __("Waiting for name resolution"),
|
||||
isLagging: false,
|
||||
});
|
||||
|
||||
lbry.resolve({ uri: "lbry://one" }).then(() => {
|
||||
this.props.onLoadDone();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
details: startupStatus.message + (startupStatus.is_lagging ? "" : "..."),
|
||||
isLagging: startupStatus.is_lagging,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.updateStatus();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
lbry
|
||||
.connect()
|
||||
.then(() => {
|
||||
this.updateStatus();
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
isLagging: true,
|
||||
message: __("Connection Failure"),
|
||||
details: __(
|
||||
"Try closing all LBRY processes and starting again. If this still happens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug."
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LoadScreen
|
||||
message={this.state.message}
|
||||
details={this.state.details}
|
||||
isWarning={this.state.isLagging}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SplashScreen;
|
19
ui/js/component/splash/index.js
Normal file
19
ui/js/component/splash/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { selectCurrentModal, selectDaemonVersionMatched } from "selectors/app";
|
||||
import { doCheckDaemonVersion } from "actions/app";
|
||||
import SplashScreen from "./view";
|
||||
|
||||
const select = state => {
|
||||
return {
|
||||
modal: selectCurrentModal(state),
|
||||
daemonVersionMatched: selectDaemonVersionMatched(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
checkDaemonVersion: () => dispatch(doCheckDaemonVersion()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SplashScreen);
|
121
ui/js/component/splash/view.jsx
Normal file
121
ui/js/component/splash/view.jsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import React from "react";
|
||||
import lbry from "../../lbry.js";
|
||||
import LoadScreen from "../load_screen.js";
|
||||
import ModalIncompatibleDaemon from "../modalIncompatibleDaemon";
|
||||
import ModalUpgrade from "component/modalUpgrade";
|
||||
import ModalDownloading from "component/modalDownloading";
|
||||
|
||||
export class SplashScreen extends React.PureComponent {
|
||||
static propTypes = {
|
||||
message: React.PropTypes.string,
|
||||
onLoadDone: React.PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
details: __("Starting daemon"),
|
||||
message: __("Connecting"),
|
||||
isRunning: false,
|
||||
isLagging: false,
|
||||
};
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
lbry.status().then(status => {
|
||||
this._updateStatusCallback(status);
|
||||
});
|
||||
}
|
||||
|
||||
_updateStatusCallback(status) {
|
||||
const startupStatus = status.startup_status;
|
||||
if (startupStatus.code == "started") {
|
||||
// Wait until we are able to resolve a name before declaring
|
||||
// that we are done.
|
||||
// TODO: This is a hack, and the logic should live in the daemon
|
||||
// to give us a better sense of when we are actually started
|
||||
this.setState({
|
||||
message: __("Testing Network"),
|
||||
details: __("Waiting for name resolution"),
|
||||
isLagging: false,
|
||||
isRunning: true,
|
||||
});
|
||||
|
||||
lbry.resolve({ uri: "lbry://one" }).then(() => {
|
||||
// Only leave the load screen if the daemon version matched;
|
||||
// otherwise we'll notify the user at the end of the load screen.
|
||||
|
||||
if (this.props.daemonVersionMatched) {
|
||||
this.props.onReadyToLaunch();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
status.blockchain_status &&
|
||||
status.blockchain_status.blocks_behind > 0
|
||||
) {
|
||||
const format = status.blockchain_status.blocks_behind == 1
|
||||
? "%s block behind"
|
||||
: "%s blocks behind";
|
||||
this.setState({
|
||||
message: __("Blockchain Sync"),
|
||||
details: __(format, status.blockchain_status.blocks_behind),
|
||||
isLagging: startupStatus.is_lagging,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
message: __("Network Loading"),
|
||||
details:
|
||||
startupStatus.message + (startupStatus.is_lagging ? "" : "..."),
|
||||
isLagging: startupStatus.is_lagging,
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.updateStatus();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
lbry
|
||||
.connect()
|
||||
.then(this.props.checkDaemonVersion)
|
||||
.then(() => {
|
||||
this.updateStatus();
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
isLagging: true,
|
||||
message: __("Connection Failure"),
|
||||
details: __(
|
||||
"Try closing all LBRY processes and starting again. If this still happens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.io if you think this is a software bug."
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { modal } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LoadScreen
|
||||
message={this.state.message}
|
||||
details={this.state.details}
|
||||
isWarning={this.state.isLagging}
|
||||
/>
|
||||
{/* Temp hack: don't show any modals on splash screen daemon is running;
|
||||
daemon doesn't let you quit during startup, so the "Quit" buttons
|
||||
in the modals won't work. */}
|
||||
{modal == "incompatibleDaemon" &&
|
||||
this.state.isRunning &&
|
||||
<ModalIncompatibleDaemon />}
|
||||
{modal == "upgrade" && this.state.isRunning && <ModalUpgrade />}
|
||||
{modal == "downloading" && this.state.isRunning && <ModalDownloading />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SplashScreen;
|
5
ui/js/component/truncatedMarkdown/index.js
Normal file
5
ui/js/component/truncatedMarkdown/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import TruncatedMarkdown from "./view";
|
||||
|
||||
export default connect()(TruncatedMarkdown);
|
39
ui/js/component/truncatedMarkdown/view.jsx
Normal file
39
ui/js/component/truncatedMarkdown/view.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
|
||||
class TruncatedMarkdown extends React.PureComponent {
|
||||
static propTypes = {
|
||||
lines: React.PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
lines: null,
|
||||
};
|
||||
|
||||
transformMarkdown(text) {
|
||||
// render markdown to html string then trim html tag
|
||||
let htmlString = ReactDOMServer.renderToStaticMarkup(
|
||||
<ReactMarkdown source={this.props.children} />
|
||||
);
|
||||
var txt = document.createElement("textarea");
|
||||
txt.innerHTML = htmlString;
|
||||
return txt.value.replace(/<(?:.|\n)*?>/gm, "");
|
||||
}
|
||||
|
||||
render() {
|
||||
let content = this.props.children && typeof this.props.children === "string"
|
||||
? this.transformMarkdown(this.props.children)
|
||||
: this.props.children;
|
||||
return (
|
||||
<span
|
||||
className="truncated-text"
|
||||
style={{ WebkitLineClamp: this.props.lines }}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TruncatedMarkdown;
|
|
@ -27,7 +27,6 @@ class UserEmailNew extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<form
|
||||
className="form-input-width"
|
||||
onSubmit={event => {
|
||||
this.handleSubmit(event);
|
||||
}}
|
||||
|
|
|
@ -13,7 +13,7 @@ class UserEmailVerify extends React.PureComponent {
|
|||
|
||||
handleCodeChanged(event) {
|
||||
this.setState({
|
||||
code: event.target.value,
|
||||
code: String(event.target.value).trim(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -24,18 +24,16 @@ class UserEmailVerify extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { errorMessage, isPending } = this.props;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="form-input-width"
|
||||
onSubmit={event => {
|
||||
this.handleSubmit(event);
|
||||
}}
|
||||
>
|
||||
<p>{__("Please enter the verification code emailed to you.")}</p>
|
||||
<FormRow
|
||||
type="text"
|
||||
label={__("Verification Code")}
|
||||
placeholder="a94bXXXXXXXXXXXXXX"
|
||||
name="code"
|
||||
value={this.state.code}
|
||||
onChange={event => {
|
||||
|
@ -48,7 +46,7 @@ class UserEmailVerify extends React.PureComponent {
|
|||
<p>
|
||||
{__("Email")}{" "}
|
||||
<Link href="mailto:help@lbry.io" label="help@lbry.io" />{" "}
|
||||
{__("if you did not receive or are having trouble with your code.")}
|
||||
{__("if you encounter any trouble with your code.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="form-row-submit form-row-submit--with-footer">
|
||||
|
|
28
ui/js/component/userVerify/index.js
Normal file
28
ui/js/component/userVerify/index.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { doUserIdentityVerify } from "actions/user";
|
||||
import rewards from "rewards";
|
||||
import { makeSelectRewardByType } from "selectors/rewards";
|
||||
import {
|
||||
selectIdentityVerifyIsPending,
|
||||
selectIdentityVerifyErrorMessage,
|
||||
} from "selectors/user";
|
||||
import UserVerify from "./view";
|
||||
|
||||
const select = (state, props) => {
|
||||
const selectReward = makeSelectRewardByType();
|
||||
|
||||
return {
|
||||
isPending: selectIdentityVerifyIsPending(state),
|
||||
errorMessage: selectIdentityVerifyErrorMessage(state),
|
||||
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: uri => dispatch(doNavigate(uri)),
|
||||
verifyUserIdentity: token => dispatch(doUserIdentityVerify(token)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(UserVerify);
|
65
ui/js/component/userVerify/view.jsx
Normal file
65
ui/js/component/userVerify/view.jsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React from "react";
|
||||
import { CreditAmount } from "component/common";
|
||||
import Link from "component/link";
|
||||
import CardVerify from "component/cardVerify";
|
||||
|
||||
class UserVerify extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
code: "",
|
||||
};
|
||||
}
|
||||
|
||||
handleCodeChanged(event) {
|
||||
this.setState({
|
||||
code: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
onToken(data) {
|
||||
this.props.verifyUserIdentity(data.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorMessage, isPending, navigate } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{__(
|
||||
"To ensure you are a real person, we require a valid credit or debit card."
|
||||
) +
|
||||
" " +
|
||||
__("There is no charge at all, now or in the future.") +
|
||||
" "}
|
||||
<Link
|
||||
href="https://lbry.io/faq/identity-requirements"
|
||||
label={__("Read More")}
|
||||
/>
|
||||
</p>
|
||||
{errorMessage && <p className="form-field__error">{errorMessage}</p>}
|
||||
<p>
|
||||
<CardVerify
|
||||
label={__("Link Card and Finish")}
|
||||
disabled={isPending}
|
||||
token={this.onToken.bind(this)}
|
||||
stripeKey="pk_live_e8M4dRNnCCbmpZzduEUZBgJO"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{__(
|
||||
"You can continue without this step, but you will not be eligible to earn rewards."
|
||||
)}
|
||||
</p>
|
||||
<Link
|
||||
onClick={() => navigate("/discover")}
|
||||
button="alt"
|
||||
label={__("Skip Rewards")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UserVerify;
|
|
@ -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) {
|
||||
|
@ -28,7 +47,6 @@ class VideoPlayButton extends React.PureComponent {
|
|||
modal,
|
||||
closeModal,
|
||||
isLoading,
|
||||
costInfo,
|
||||
fileInfo,
|
||||
mediaType,
|
||||
} = this.props;
|
||||
|
@ -41,10 +59,7 @@ class VideoPlayButton extends React.PureComponent {
|
|||
}
|
||||
*/
|
||||
|
||||
const disabled =
|
||||
isLoading ||
|
||||
fileInfo === undefined ||
|
||||
(fileInfo === null && (!costInfo || costInfo.cost === undefined));
|
||||
const disabled = isLoading || fileInfo === undefined;
|
||||
const icon = ["audio", "video"].indexOf(mediaType) !== -1
|
||||
? "icon-play"
|
||||
: "icon-folder-o";
|
||||
|
@ -59,13 +74,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 +81,11 @@ class VideoPlayButton extends React.PureComponent {
|
|||
onConfirmed={this.onPurchaseConfirmed.bind(this)}
|
||||
onAborted={closeModal}
|
||||
>
|
||||
{__("This will purchase")} <strong>{title}</strong> {__("for")}
|
||||
{" "}<strong><FilePrice uri={uri} look="plain" /></strong>
|
||||
{" "}{__("credits")}.
|
||||
{__("This will purchase")} <strong>{title}</strong> {__("for")}{" "}
|
||||
<strong>
|
||||
<FilePrice uri={uri} look="plain" />
|
||||
</strong>{" "}
|
||||
{__("credits")}.
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={modal == "timedOut"}
|
||||
|
|
|
@ -7,6 +7,8 @@ import { setSession, getSession } from "utils";
|
|||
import LoadingScreen from "./loading-screen";
|
||||
|
||||
class VideoPlayer extends React.PureComponent {
|
||||
static MP3_CONTENT_TYPES = ["audio/mpeg3", "audio/mpeg"];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -15,11 +17,13 @@ class VideoPlayer extends React.PureComponent {
|
|||
startedPlaying: false,
|
||||
unplayable: false,
|
||||
};
|
||||
|
||||
this.togglePlayListener = this.togglePlay.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const container = this.refs.media;
|
||||
const { mediaType } = this.props;
|
||||
const { contentType, downloadPath, mediaType } = this.props;
|
||||
const loadedMetadata = e => {
|
||||
this.setState({ hasMetadata: true, startedPlaying: true });
|
||||
this.refs.media.children[0].play();
|
||||
|
@ -37,15 +41,22 @@ class VideoPlayer extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,25 @@ class Video extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// reset playing state upon change path action
|
||||
if (
|
||||
!this.isMediaSame(nextProps) &&
|
||||
this.props.fileInfo &&
|
||||
this.state.isPlaying
|
||||
) {
|
||||
this.state.isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
isMediaSame(nextProps) {
|
||||
return (
|
||||
this.props.fileInfo &&
|
||||
nextProps.fileInfo &&
|
||||
this.props.fileInfo.outpoint === nextProps.fileInfo.outpoint
|
||||
);
|
||||
}
|
||||
|
||||
startPlaying() {
|
||||
this.setState({
|
||||
isPlaying: true,
|
||||
|
@ -98,6 +117,7 @@ class Video extends React.PureComponent {
|
|||
poster={poster}
|
||||
downloadPath={fileInfo.download_path}
|
||||
mediaType={mediaType}
|
||||
contentType={contentType}
|
||||
downloadCompleted={fileInfo.completed}
|
||||
/>)}
|
||||
{!isPlaying &&
|
||||
|
|
|
@ -12,8 +12,10 @@ const select = state => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
onSearch: query => dispatch(doNavigate("/search", { query })),
|
||||
onSubmit: query =>
|
||||
dispatch(doNavigate("/show", { uri: lbryuri.normalize(query) })),
|
||||
onSubmit: (query, extraParams) =>
|
||||
dispatch(
|
||||
doNavigate("/show", { uri: lbryuri.normalize(query), ...extraParams })
|
||||
),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(Wunderbar);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import { Icon } from "component/common.js";
|
||||
import { parseQueryParams } from "util/query_params";
|
||||
|
||||
class WunderBar extends React.PureComponent {
|
||||
static TYPING_TIMEOUT = 800;
|
||||
|
@ -120,12 +121,15 @@ class WunderBar extends React.PureComponent {
|
|||
onKeyPress(event) {
|
||||
if (event.charCode == 13 && this._input.value) {
|
||||
let uri = null,
|
||||
method = "onSubmit";
|
||||
method = "onSubmit",
|
||||
extraParams = {};
|
||||
|
||||
this._resetOnNextBlur = false;
|
||||
clearTimeout(this._userTypingTimer);
|
||||
|
||||
const value = this._input.value.trim();
|
||||
const parts = this._input.value.trim().split("?");
|
||||
const value = parts.shift();
|
||||
if (parts.length > 0) extraParams = parseQueryParams(parts.join(""));
|
||||
|
||||
try {
|
||||
uri = lbryuri.normalize(value);
|
||||
|
@ -136,7 +140,7 @@ class WunderBar extends React.PureComponent {
|
|||
method = "onSearch";
|
||||
}
|
||||
|
||||
this.props[method](uri);
|
||||
this.props[method](uri, extraParams);
|
||||
this._input.blur();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
@ -97,6 +112,14 @@ export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED";
|
|||
export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS";
|
||||
export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE";
|
||||
export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";
|
||||
export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED";
|
||||
|
||||
//Language
|
||||
export const CHANGE_LANGUAGE = "CHANGE_LANGUAGE";
|
||||
export const LANGUAGE_CHANGED = "LANGUAGE_CHANGED";
|
||||
export const LANGUAGE_RESOLVED = "LANGUAGE_RESOLVED";
|
||||
export const DOWNLOAD_LANGUAGE_STARTED = "DOWNLOAD_LANGUAGE_STARTED";
|
||||
export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED";
|
||||
export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED";
|
||||
export const DOWNLOAD_LANGUAGES_STARTED = "DOWNLOAD_LANGUAGES_STARTED";
|
||||
export const DOWNLOAD_LANGUAGES_COMPLETED = "DOWNLOAD_LANGUAGES_COMPLETED";
|
||||
|
|
|
@ -1,2 +1,9 @@
|
|||
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";
|
||||
export const AUTHENTICATION_FAILURE = "auth_failure";
|
||||
|
|
|
@ -27,18 +27,19 @@ jsonrpc.call = function(
|
|||
xhr.addEventListener("load", function() {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.error) {
|
||||
let error = response.error || (response.result && response.result.error);
|
||||
if (error) {
|
||||
if (errorCallback) {
|
||||
errorCallback(response.error);
|
||||
errorCallback(error);
|
||||
} else {
|
||||
var errorEvent = new CustomEvent("unhandledError", {
|
||||
detail: {
|
||||
connectionString: connectionString,
|
||||
method: method,
|
||||
params: params,
|
||||
code: response.error.code,
|
||||
message: response.error.message,
|
||||
data: response.error.data,
|
||||
code: error.code,
|
||||
message: error.message || error,
|
||||
data: error.data,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(errorEvent);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -429,10 +416,16 @@ lbry.file_list = function(params = {}) {
|
|||
fileInfos => {
|
||||
removePendingPublishIfNeeded({ name, channel_name, outpoint });
|
||||
|
||||
const dummyFileInfos = lbry
|
||||
.getPendingPublishes()
|
||||
.map(pendingPublishToDummyFileInfo);
|
||||
resolve([...fileInfos, ...dummyFileInfos]);
|
||||
//if a naked file_list call, append the pending file infos
|
||||
if (!name && !channel_name && !outpoint) {
|
||||
const dummyFileInfos = lbry
|
||||
.getPendingPublishes()
|
||||
.map(pendingPublishToDummyFileInfo);
|
||||
|
||||
resolve([...fileInfos, ...dummyFileInfos]);
|
||||
} else {
|
||||
resolve(fileInfos);
|
||||
}
|
||||
},
|
||||
reject
|
||||
);
|
||||
|
|
|
@ -113,16 +113,24 @@ lbryio.call = function(resource, action, params = {}, method = "get") {
|
|||
});
|
||||
};
|
||||
|
||||
lbryio._authToken = null;
|
||||
|
||||
lbryio.getAuthToken = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.once("auth-token-response", (event, token) => {
|
||||
return resolve(token);
|
||||
});
|
||||
ipcRenderer.send("get-auth-token");
|
||||
if (lbryio._authToken) {
|
||||
resolve(lbryio._authToken);
|
||||
} else {
|
||||
ipcRenderer.once("auth-token-response", (event, token) => {
|
||||
lbryio._authToken = token;
|
||||
return resolve(token);
|
||||
});
|
||||
ipcRenderer.send("get-auth-token");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
lbryio.setAuthToken = token => {
|
||||
lbryio._authToken = token ? token.toString().trim() : null;
|
||||
ipcRenderer.send("set-auth-token", token);
|
||||
};
|
||||
|
||||
|
@ -136,8 +144,9 @@ lbryio.authenticate = function() {
|
|||
resolve({
|
||||
id: 1,
|
||||
language: "en",
|
||||
has_email: true,
|
||||
primary_email: "disabled@lbry.io",
|
||||
has_verified_email: true,
|
||||
is_identity_verified: true,
|
||||
is_reward_approved: false,
|
||||
is_reward_eligible: false,
|
||||
});
|
||||
|
|
|
@ -3,6 +3,8 @@ const CLAIM_ID_MAX_LEN = 40;
|
|||
|
||||
const lbryuri = {};
|
||||
|
||||
lbryuri.REGEXP_INVALID_URI = /[^A-Za-z0-9-]/g;
|
||||
|
||||
/**
|
||||
* Parses a LBRY name into its component parts. Throws errors with user-friendly
|
||||
* messages for invalid names.
|
||||
|
@ -70,7 +72,7 @@ lbryuri.parse = function(uri, requireProto = false) {
|
|||
contentName = path;
|
||||
}
|
||||
|
||||
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
|
||||
const nameBadChars = (channelName || name).match(lbryuri.REGEXP_INVALID_URI);
|
||||
if (nameBadChars) {
|
||||
throw new Error(
|
||||
__(
|
||||
|
@ -119,7 +121,7 @@ lbryuri.parse = function(uri, requireProto = false) {
|
|||
throw new Error(__("Only channel URIs may have a path."));
|
||||
}
|
||||
|
||||
const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
|
||||
const pathBadChars = path.match(lbryuri.REGEXP_INVALID_URI);
|
||||
if (pathBadChars) {
|
||||
throw new Error(
|
||||
__(`Invalid character in path: %s`, pathBadChars.join(", "))
|
||||
|
@ -203,6 +205,8 @@ lbryuri.build = function(uriObj, includeProto = true, allowExtraProps = false) {
|
|||
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
|
||||
* consists of adding the lbry:// prefix if needed) */
|
||||
lbryuri.normalize = function(uri) {
|
||||
if (uri.match(/pending_claim/)) return uri;
|
||||
|
||||
const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
|
||||
uri
|
||||
);
|
||||
|
|
|
@ -5,11 +5,10 @@ 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 { doDownloadLanguages } from "actions/settings";
|
||||
import { toQueryString } from "util/query_params";
|
||||
import { selectBadgeNumber } from "selectors/app";
|
||||
import * as types from "constants/action_types";
|
||||
import fs from "fs";
|
||||
import http from "http";
|
||||
|
@ -39,11 +38,11 @@ window.addEventListener("popstate", (event, param) => {
|
|||
let action;
|
||||
|
||||
if (hash !== "") {
|
||||
const url = hash.split("#")[1];
|
||||
const params = event.state;
|
||||
const url = hash.replace(/^#/, "");
|
||||
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"));
|
||||
}
|
||||
|
@ -100,27 +99,16 @@ 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() {
|
||||
app.store.dispatch(doDownloadLanguages());
|
||||
|
||||
function onDaemonReady() {
|
||||
window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again
|
||||
app.store.dispatch(doDaemonReady());
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<div><AuthOverlay /><App /><SnackBar /></div>
|
||||
<div><App /><SnackBar /></div>
|
||||
</Provider>,
|
||||
canvas
|
||||
);
|
||||
|
@ -129,44 +117,13 @@ 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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const download = (url, dest, lang, cb) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
const request = http.get(url, response => {
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(cb); // close() is async, call cb after close completes.
|
||||
app.i18n.localLanguages.push(lang.replace(".json", "")); // push to our local list
|
||||
});
|
||||
}).on('error', err => { // Handle errors
|
||||
fs.unlink(dest); // Delete the file async. (But we don't check the result)
|
||||
if (cb) cb(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadLanguages = () => {
|
||||
if (!fs.existsSync("app/locales")){
|
||||
fs.mkdirSync("app/locales");
|
||||
}
|
||||
http.request({ host: 'i18n.lbry.io', path: '/' }, response => {
|
||||
let str = '';
|
||||
|
||||
response.on('data', chunk => {
|
||||
str += chunk;
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
const files = JSON.parse(str);JSON.parse(str);
|
||||
app.i18n.localLanguages = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
download(`http://i18n.lbry.io/langs/${files[i]}`, `app/locales/${files[i]}`, files[i], () => {});
|
||||
}
|
||||
});
|
||||
}).end();
|
||||
};
|
||||
|
||||
downloadLanguages();
|
||||
init();
|
||||
|
|
30
ui/js/page/auth/index.js
Normal file
30
ui/js/page/auth/index.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { connect } from "react-redux";
|
||||
import { selectPathAfterAuth } from "selectors/app";
|
||||
import {
|
||||
selectAuthenticationIsPending,
|
||||
selectEmailToVerify,
|
||||
selectUserIsVerificationCandidate,
|
||||
selectUser,
|
||||
selectUserIsPending,
|
||||
selectIdentityVerifyIsPending,
|
||||
} from "selectors/user";
|
||||
import AuthPage from "./view";
|
||||
|
||||
const select = state => ({
|
||||
isPending:
|
||||
selectAuthenticationIsPending(state) ||
|
||||
selectUserIsPending(state) ||
|
||||
selectIdentityVerifyIsPending(state),
|
||||
email: selectEmailToVerify(state),
|
||||
pathAfterAuth: selectPathAfterAuth(state),
|
||||
user: selectUser(state),
|
||||
isVerificationCandidate: selectUserIsVerificationCandidate(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: path => dispatch(doNavigate(path)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(AuthPage);
|
95
ui/js/page/auth/view.jsx
Normal file
95
ui/js/page/auth/view.jsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
import React from "react";
|
||||
import { BusyMessage } from "component/common";
|
||||
import Link from "component/link";
|
||||
import UserEmailNew from "component/userEmailNew";
|
||||
import UserEmailVerify from "component/userEmailVerify";
|
||||
import UserVerify from "component/userVerify";
|
||||
|
||||
export class AuthPage extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.navigateIfAuthenticated(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.navigateIfAuthenticated(nextProps);
|
||||
}
|
||||
|
||||
navigateIfAuthenticated(props) {
|
||||
const { isPending, user } = props;
|
||||
if (
|
||||
!isPending &&
|
||||
user &&
|
||||
user.has_verified_email &&
|
||||
(user.is_reward_approved || user.is_identity_verified)
|
||||
) {
|
||||
props.navigate(props.pathAfterAuth);
|
||||
}
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
const { email, isPending, isVerificationCandidate, user } = this.props;
|
||||
|
||||
if (isPending || (user && !user.has_verified_email && !email)) {
|
||||
return __("Welcome to LBRY");
|
||||
} else if (user && !user.has_verified_email) {
|
||||
return __("Confirm Email");
|
||||
} else if (user && !user.is_identity_verified && !user.is_reward_approved) {
|
||||
return __("Confirm Identity");
|
||||
} else {
|
||||
return __("Welcome to LBRY");
|
||||
}
|
||||
}
|
||||
|
||||
renderMain() {
|
||||
const { email, isPending, isVerificationCandidate, user } = this.props;
|
||||
|
||||
if (isPending) {
|
||||
return <BusyMessage message={__("Authenticating")} />;
|
||||
} else if (user && !user.has_verified_email && !email) {
|
||||
return <UserEmailNew />;
|
||||
} else if (user && !user.has_verified_email) {
|
||||
return <UserEmailVerify />;
|
||||
} else if (user && !user.is_identity_verified) {
|
||||
return <UserVerify />;
|
||||
} else {
|
||||
return <span className="empty">{__("No further steps.")}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { email, user, isPending, navigate } = this.props;
|
||||
|
||||
return (
|
||||
<main className="">
|
||||
<section className="card card--form">
|
||||
<div className="card__title-primary">
|
||||
<h1>{this.getTitle()}</h1>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{!isPending &&
|
||||
!email &&
|
||||
user &&
|
||||
!user.has_verified_email &&
|
||||
<p>
|
||||
{__("Create a verified identity and receive LBC rewards.")}
|
||||
</p>}
|
||||
{this.renderMain()}
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<div className="help">
|
||||
{__(
|
||||
"This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards."
|
||||
) + " "}
|
||||
<Link
|
||||
onClick={() => navigate("/discover")}
|
||||
label={__("Return home")}
|
||||
/>.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthPage;
|
|
@ -3,24 +3,34 @@ import { connect } from "react-redux";
|
|||
import { doFetchClaimsByChannel } from "actions/content";
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimsInChannelForUri,
|
||||
makeSelectClaimsInChannelForCurrentPage,
|
||||
makeSelectFetchingChannelClaims,
|
||||
} from "selectors/claims";
|
||||
import { selectCurrentParams } from "selectors/app";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { makeSelectTotalPagesForChannel } from "selectors/content";
|
||||
import ChannelPage from "./view";
|
||||
|
||||
const makeSelect = () => {
|
||||
const selectClaim = makeSelectClaimForUri(),
|
||||
selectClaimsInChannel = makeSelectClaimsInChannelForUri();
|
||||
selectClaimsInChannel = makeSelectClaimsInChannelForCurrentPage(),
|
||||
selectFetchingChannelClaims = makeSelectFetchingChannelClaims(),
|
||||
selectTotalPagesForChannel = makeSelectTotalPagesForChannel();
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: selectClaim(state, props),
|
||||
claimsInChannel: selectClaimsInChannel(state, props),
|
||||
fetching: selectFetchingChannelClaims(state, props),
|
||||
totalPages: selectTotalPagesForChannel(state, props),
|
||||
params: selectCurrentParams(state),
|
||||
});
|
||||
|
||||
return select;
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchClaims: uri => dispatch(doFetchClaimsByChannel(uri)),
|
||||
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
});
|
||||
|
||||
export default connect(makeSelect, perform)(ChannelPage);
|
||||
|
|
|
@ -2,24 +2,41 @@ import React from "react";
|
|||
import lbryuri from "lbryuri";
|
||||
import { BusyMessage } from "component/common";
|
||||
import FileTile from "component/fileTile";
|
||||
import Link from "component/link";
|
||||
import ReactPaginate from "react-paginate";
|
||||
|
||||
class ChannelPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.fetchClaims(this.props);
|
||||
const { uri, params, fetchClaims } = this.props;
|
||||
|
||||
fetchClaims(uri, params.page || 1);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.fetchClaims(nextProps);
|
||||
const { params, fetching, fetchClaims } = this.props;
|
||||
const nextParams = nextProps.params;
|
||||
|
||||
if (fetching !== nextParams.page && params.page !== nextParams.page)
|
||||
fetchClaims(nextProps.uri, nextParams.page);
|
||||
}
|
||||
|
||||
fetchClaims(props) {
|
||||
if (props.claimsInChannel === undefined) {
|
||||
props.fetchClaims(props.uri);
|
||||
}
|
||||
changePage(pageNumber) {
|
||||
const { params, currentPage } = this.props;
|
||||
const newParams = Object.assign({}, params, { page: pageNumber });
|
||||
|
||||
this.props.navigate("/show", newParams);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { claimsInChannel, claim, uri } = this.props;
|
||||
const {
|
||||
fetching,
|
||||
claimsInChannel,
|
||||
claim,
|
||||
uri,
|
||||
params,
|
||||
totalPages,
|
||||
} = this.props;
|
||||
const { page } = params;
|
||||
|
||||
let contentList;
|
||||
if (claimsInChannel === undefined) {
|
||||
|
@ -29,14 +46,17 @@ class ChannelPage extends React.PureComponent {
|
|||
? claimsInChannel.map(claim =>
|
||||
<FileTile
|
||||
key={claim.claim_id}
|
||||
uri={lbryuri.build({ name: claim.name, claimId: claim.claim_id })}
|
||||
uri={lbryuri.build({
|
||||
name: claim.name,
|
||||
claimId: claim.claim_id,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
: <span className="empty">{__("No content found.")}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<div>
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-identity"><h1>{uri}</h1></div>
|
||||
|
@ -51,7 +71,24 @@ class ChannelPage extends React.PureComponent {
|
|||
</section>
|
||||
<h3 className="card-row__header">{__("Published Content")}</h3>
|
||||
{contentList}
|
||||
</main>
|
||||
<div />
|
||||
{(!fetching || (claimsInChannel && claimsInChannel.length)) &&
|
||||
totalPages > 1 &&
|
||||
<ReactPaginate
|
||||
pageCount={totalPages}
|
||||
pageRangeDisplayed={2}
|
||||
previousLabel="‹"
|
||||
nextLabel="›"
|
||||
activeClassName="pagination__item--selected"
|
||||
pageClassName="pagination__item"
|
||||
previousClassName="pagination__item pagination__item--previous"
|
||||
nextClassName="pagination__item pagination__item--next"
|
||||
marginPagesDisplayed={2}
|
||||
onPageChange={e => this.changePage(e.selected + 1)}
|
||||
initialPage={parseInt(page - 1)}
|
||||
containerClassName="pagination"
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import lbry from "../lbry.js";
|
||||
import React from "react";
|
||||
import { FormField } from "../component/form.js";
|
||||
import FormField from "component/formField";
|
||||
import Link from "../component/link";
|
||||
|
||||
const fs = require("fs");
|
||||
|
|
|
@ -1,38 +1,203 @@
|
|||
import React from "react";
|
||||
import lbryio from "lbryio.js";
|
||||
import ReactDOM from "react-dom";
|
||||
import lbryuri from "lbryuri";
|
||||
import FileCard from "component/fileCard";
|
||||
import { BusyMessage } from "component/common.js";
|
||||
import { Icon, BusyMessage } from "component/common.js";
|
||||
import ToolTip from "component/tooltip.js";
|
||||
|
||||
const FeaturedCategory = props => {
|
||||
const { category, names } = props;
|
||||
class FeaturedCategory extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
numItems: this.props.names.length,
|
||||
canScrollPrevious: false,
|
||||
canScrollNext: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-row card-row--small">
|
||||
<h3 className="card-row__header">
|
||||
{category}
|
||||
{category &&
|
||||
category.match(/^community/i) &&
|
||||
<ToolTip
|
||||
label={__("What's this?")}
|
||||
body={__(
|
||||
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
|
||||
)}
|
||||
className="tooltip--header"
|
||||
/>}
|
||||
</h3>
|
||||
{names &&
|
||||
names.map(name =>
|
||||
<FileCard
|
||||
key={name}
|
||||
displayStyle="card"
|
||||
uri={lbryuri.normalize(name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
handleScrollPrevious() {
|
||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
||||
if (cardRow.scrollLeft > 0) {
|
||||
// check the visible cards
|
||||
const cards = cardRow.getElementsByTagName("section");
|
||||
let firstVisibleCard = null;
|
||||
let firstVisibleIdx = -1;
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
if (this.isCardVisible(cards[i], cardRow, false)) {
|
||||
firstVisibleCard = cards[i];
|
||||
firstVisibleIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const numDisplayed = this.numDisplayedCards(cardRow);
|
||||
const scrollToIdx = firstVisibleIdx - numDisplayed;
|
||||
const animationCallback = () => {
|
||||
this.setState({
|
||||
canScrollPrevious: cardRow.scrollLeft !== 0,
|
||||
canScrollNext: true,
|
||||
});
|
||||
};
|
||||
this.scrollCardItemsLeftAnimated(
|
||||
cardRow,
|
||||
scrollToIdx < 0 ? 0 : cards[scrollToIdx].offsetLeft,
|
||||
100,
|
||||
animationCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollNext() {
|
||||
const cardRow = ReactDOM.findDOMNode(this.refs.rowitems);
|
||||
|
||||
// check the visible cards
|
||||
const cards = cardRow.getElementsByTagName("section");
|
||||
let lastVisibleCard = null;
|
||||
let lastVisibleIdx = -1;
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
if (this.isCardVisible(cards[i], cardRow, true)) {
|
||||
lastVisibleCard = cards[i];
|
||||
lastVisibleIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastVisibleCard) {
|
||||
const numDisplayed = this.numDisplayedCards(cardRow);
|
||||
const animationCallback = () => {
|
||||
// update last visible index after scroll
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
if (this.isCardVisible(cards[i], cardRow, true)) {
|
||||
lastVisibleIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ canScrollPrevious: true });
|
||||
if (lastVisibleIdx === cards.length - 1) {
|
||||
this.setState({ canScrollNext: false });
|
||||
}
|
||||
};
|
||||
|
||||
this.scrollCardItemsLeftAnimated(
|
||||
cardRow,
|
||||
Math.min(
|
||||
lastVisibleCard.offsetLeft,
|
||||
cardRow.scrollWidth - cardRow.clientWidth
|
||||
),
|
||||
100,
|
||||
animationCallback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
scrollCardItemsLeftAnimated(cardRow, target, duration, callback) {
|
||||
if (!duration || duration <= diff) {
|
||||
cardRow.scrollLeft = target;
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const component = this;
|
||||
const diff = target - cardRow.scrollLeft;
|
||||
const tick = diff / duration * 10;
|
||||
setTimeout(() => {
|
||||
cardRow.scrollLeft = cardRow.scrollLeft + tick;
|
||||
if (cardRow.scrollLeft === target) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
component.scrollCardItemsLeftAnimated(
|
||||
cardRow,
|
||||
target,
|
||||
duration - 10,
|
||||
callback
|
||||
);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
isCardVisible(section, cardRow, partialVisibility) {
|
||||
// check if a card is fully or partialy visible in its parent
|
||||
const cardRowWidth = cardRow.offsetWidth;
|
||||
const cardRowLeft = cardRow.scrollLeft;
|
||||
const cardRowEnd = cardRowLeft + cardRow.offsetWidth;
|
||||
const sectionLeft = section.offsetLeft - cardRowLeft;
|
||||
const sectionEnd = sectionLeft + section.offsetWidth;
|
||||
|
||||
return (
|
||||
(sectionLeft >= 0 && sectionEnd <= cardRowWidth) ||
|
||||
(((sectionLeft < 0 && sectionEnd > 0) ||
|
||||
(sectionLeft > 0 && sectionLeft <= cardRowWidth)) &&
|
||||
partialVisibility)
|
||||
);
|
||||
}
|
||||
|
||||
numDisplayedCards(cardRow) {
|
||||
const cards = cardRow.getElementsByTagName("section");
|
||||
const cardRowWidth = cardRow.offsetWidth;
|
||||
// get the width of the first card and then calculate
|
||||
const cardWidth = cards.length > 0 ? cards[0].offsetWidth : 0;
|
||||
|
||||
if (cardWidth > 0) {
|
||||
return Math.ceil(cardRowWidth / cardWidth);
|
||||
}
|
||||
|
||||
// return a default value of 1 card displayed if the card width couldn't be determined
|
||||
return 1;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { category, names } = this.props;
|
||||
|
||||
return (
|
||||
<div className="card-row card-row--small">
|
||||
<h3 className="card-row__header">
|
||||
{category}
|
||||
{category &&
|
||||
category.match(/^community/i) &&
|
||||
<ToolTip
|
||||
label={__("What's this?")}
|
||||
body={__(
|
||||
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
|
||||
)}
|
||||
className="tooltip--header"
|
||||
/>}
|
||||
</h3>
|
||||
<div className="card-row__scrollhouse">
|
||||
{this.state.canScrollPrevious &&
|
||||
<div className="card-row__nav card-row__nav--left">
|
||||
<a
|
||||
className="card-row__scroll-button"
|
||||
onClick={this.handleScrollPrevious.bind(this)}
|
||||
>
|
||||
<Icon icon="icon-chevron-left" />
|
||||
</a>
|
||||
</div>}
|
||||
{this.state.canScrollNext &&
|
||||
<div className="card-row__nav card-row__nav--right">
|
||||
<a
|
||||
className="card-row__scroll-button"
|
||||
onClick={this.handleScrollNext.bind(this)}
|
||||
>
|
||||
<Icon icon="icon-chevron-right" />
|
||||
</a>
|
||||
</div>}
|
||||
<div ref="rowitems" className="card-row__items">
|
||||
{names &&
|
||||
names.map(name =>
|
||||
<FileCard
|
||||
key={name}
|
||||
displayStyle="card"
|
||||
uri={lbryuri.normalize(name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiscoverPage extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
|
@ -45,16 +210,20 @@ class DiscoverPage extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { featuredUris, fetchingFeaturedUris } = this.props;
|
||||
const failedToLoad =
|
||||
!fetchingFeaturedUris &&
|
||||
(featuredUris === undefined ||
|
||||
(featuredUris !== undefined && Object.keys(featuredUris).length === 0));
|
||||
const hasContent =
|
||||
typeof featuredUris === "object" && Object.keys(featuredUris).length,
|
||||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<main>
|
||||
{fetchingFeaturedUris &&
|
||||
<main
|
||||
className={
|
||||
hasContent && fetchingFeaturedUris ? "main--refreshing" : null
|
||||
}
|
||||
>
|
||||
{!hasContent &&
|
||||
fetchingFeaturedUris &&
|
||||
<BusyMessage message={__("Fetching content")} />}
|
||||
{typeof featuredUris === "object" &&
|
||||
{hasContent &&
|
||||
Object.keys(featuredUris).map(
|
||||
category =>
|
||||
featuredUris[category].length
|
||||
|
|
|
@ -3,15 +3,22 @@ import { connect } from "react-redux";
|
|||
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
|
||||
import {
|
||||
selectFileInfosDownloaded,
|
||||
selectFileListDownloadedOrPublishedIsPending,
|
||||
selectIsFetchingFileListDownloadedOrPublished,
|
||||
} from "selectors/file_info";
|
||||
import {
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingClaimListMine,
|
||||
} from "selectors/claims";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { doCancelAllResolvingUris } from "actions/content";
|
||||
import FileListDownloaded from "./view";
|
||||
|
||||
const select = state => ({
|
||||
fileInfos: selectFileInfosDownloaded(state),
|
||||
isPending: selectFileListDownloadedOrPublishedIsPending(state),
|
||||
isFetching: selectIsFetchingFileListDownloadedOrPublished(state),
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
isFetchingClaims: selectIsFetchingClaimListMine(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -19,6 +26,7 @@ const perform = dispatch => ({
|
|||
fetchFileInfosDownloaded: () =>
|
||||
dispatch(doFetchFileInfosAndPublishedClaims()),
|
||||
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
|
||||
fetchClaims: () => dispatch(doFetchClaimListMine()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileListDownloaded);
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import React from "react";
|
||||
import lbry from "lbry.js";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import Link from "component/link";
|
||||
import { FormField } from "component/form.js";
|
||||
import { FileTile } from "component/fileTile";
|
||||
import rewards from "rewards.js";
|
||||
import lbryio from "lbryio.js";
|
||||
import { BusyMessage, Thumbnail } from "component/common.js";
|
||||
import FileList from "component/fileList";
|
||||
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 +16,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 = (
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue