Merge pull request #2729 from lbryio/fixes

Random fixes
This commit is contained in:
Sean Yesmunt 2019-08-14 23:11:47 -04:00 committed by GitHub
commit 594a6a7808
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 249 additions and 101 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "LBRY", "name": "LBRY",
"version": "0.34.2", "version": "0.35.0-rc.1",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [ "keywords": [
"lbry" "lbry"
@ -125,8 +125,8 @@
"jsmediatags": "^3.8.1", "jsmediatags": "^3.8.1",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#05e70648e05c51c51710f6dd698a8e2219b54df2", "lbry-redux": "lbryio/lbry-redux#6005fa245a888e2de045d7e42411847de7943f52",
"lbryinc": "lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb", "lbryinc": "lbryio/lbryinc#1ce266b3c52654190b955e9c869b8e302aa5c585",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"lodash-es": "^4.17.14", "lodash-es": "^4.17.14",

View file

@ -3,6 +3,7 @@ import { Lbryio } from 'lbryinc';
import ReactGA from 'react-ga'; import ReactGA from 'react-ga';
import { history } from './store'; import { history } from './store';
// @if TARGET='app' // @if TARGET='app'
import Native from 'native';
import ElectronCookies from '@exponent/electron-cookies'; import ElectronCookies from '@exponent/electron-cookies';
// @endif // @endif
@ -15,6 +16,9 @@ type Analytics = {
apiLogView: (string, string, string, ?number, ?() => void) => void, apiLogView: (string, string, string, ?number, ?() => void) => void,
apiLogPublish: () => void, apiLogPublish: () => void,
tagFollowEvent: (string, boolean, string) => void, tagFollowEvent: (string, boolean, string) => void,
emailProvidedEvent: () => void,
emailVerifiedEvent: () => void,
rewardEligibleEvent: () => void,
}; };
let analyticsEnabled: boolean = true; let analyticsEnabled: boolean = true;
@ -26,11 +30,15 @@ const analytics: Analytics = {
}, },
setUser: userId => { setUser: userId => {
if (analyticsEnabled && userId) { if (analyticsEnabled && userId) {
ReactGA.event({ ReactGA.set({
category: 'User', userId,
action: 'userId',
value: userId,
}); });
// @if TARGET='app'
Native.getAppVersionInfo().then(({ localVersion }) => {
sendGaEvent('Desktop-Version', localVersion);
});
// @endif
} }
}, },
toggle: (enabled: boolean): void => { toggle: (enabled: boolean): void => {
@ -39,8 +47,8 @@ const analytics: Analytics = {
analyticsEnabled = enabled; analyticsEnabled = enabled;
// @endif // @endif
}, },
apiLogView: (uri, outpoint, claimId, timeToStart, onSuccessCb) => { apiLogView: (uri, outpoint, claimId, timeToStart) => {
if (analyticsEnabled) { if (analyticsEnabled && isProduction) {
const params: { const params: {
uri: string, uri: string,
outpoint: string, outpoint: string,
@ -52,17 +60,12 @@ const analytics: Analytics = {
claim_id: claimId, claim_id: claimId,
}; };
if (timeToStart) { // lbry.tv streams from AWS so we don't care about the time to start
if (timeToStart && !IS_WEB) {
params.time_to_start = timeToStart; params.time_to_start = timeToStart;
} }
Lbryio.call('file', 'view', params) Lbryio.call('file', 'view', params);
.then(() => {
if (onSuccessCb) {
onSuccessCb();
}
})
.catch(() => {});
} }
}, },
apiLogSearch: () => { apiLogSearch: () => {
@ -82,23 +85,31 @@ const analytics: Analytics = {
} }
}, },
tagFollowEvent: (tag, following, location) => { tagFollowEvent: (tag, following, location) => {
if (analyticsEnabled) { sendGaEvent(following ? 'Tag-Follow' : 'Tag-Unfollow', tag);
ReactGA.event({
category: following ? 'Tag-Follow' : 'Tag-Unfollow',
action: tag,
});
}
}, },
channelBlockEvent: (uri, blocked, location) => { channelBlockEvent: (uri, blocked, location) => {
if (analyticsEnabled) { sendGaEvent(blocked ? 'Channel-Hidden' : 'Channel-Unhidden', uri);
ReactGA.event({ },
category: blocked ? 'Channel-Hidden' : 'Channel-Unhidden', emailProvidedEvent: () => {
action: uri, sendGaEvent('Engagement', 'Email-Provided');
}); },
} emailVerifiedEvent: () => {
sendGaEvent('Engagement', 'Email-Verified');
},
rewardEligibleEvent: () => {
sendGaEvent('Engagement', 'Reward-Eligible');
}, },
}; };
function sendGaEvent(category, action) {
if (analyticsEnabled && isProduction) {
ReactGA.event({
category,
action,
});
}
}
// Initialize google analytics // Initialize google analytics
// Set `debug: true` for debug info // Set `debug: true` for debug info
// Will change once we have separate ids for desktop/web // Will change once we have separate ids for desktop/web
@ -113,6 +124,8 @@ ElectronCookies.enable({
ReactGA.initialize(UA_ID, { ReactGA.initialize(UA_ID, {
testMode: process.env.NODE_ENV !== 'production', testMode: process.env.NODE_ENV !== 'production',
cookieDomain: 'auto', cookieDomain: 'auto',
// un-comment to see events as they are sent to google
// debug: true,
}); });
// Manually call the first page view // Manually call the first page view

View file

@ -12,6 +12,7 @@ import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer'; import FileViewer from 'component/fileViewer';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import usePrevious from 'util/use-previous';
export const MAIN_WRAPPER_CLASS = 'main-wrapper'; export const MAIN_WRAPPER_CLASS = 'main-wrapper';
@ -21,7 +22,7 @@ type Props = {
language: string, language: string,
theme: string, theme: string,
accessToken: ?string, accessToken: ?string,
user: ?{ id: string, has_verified_email: boolean }, user: ?{ id: string, has_verified_email: boolean, is_reward_approved: boolean },
location: { pathname: string }, location: { pathname: string },
fetchRewards: () => void, fetchRewards: () => void,
fetchRewardedContent: () => void, fetchRewardedContent: () => void,
@ -35,7 +36,10 @@ function App(props: Props) {
const isEnhancedLayout = useKonamiListener(); const isEnhancedLayout = useKonamiListener();
const userId = user && user.id; const userId = user && user.id;
const hasVerifiedEmail = user && user.has_verified_email; const hasVerifiedEmail = user && user.has_verified_email;
const isRewardApproved = user && user.is_reward_approved;
const previousUserId = usePrevious(userId);
const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail);
const previousRewardApproved = usePrevious(isRewardApproved);
const { pathname } = props.location; const { pathname } = props.location;
const urlParts = pathname.split('/'); const urlParts = pathname.split('/');
const claimName = urlParts[1]; const claimName = urlParts[1];
@ -65,10 +69,24 @@ function App(props: Props) {
}, [theme]); }, [theme]);
useEffect(() => { useEffect(() => {
if (userId) { if (previousUserId === undefined && userId) {
analytics.setUser(userId); analytics.setUser(userId);
} }
}, [userId]); }, [previousUserId, userId]);
useEffect(() => {
// Check that previousHasVerifiedEmail was not undefined instead of just not truthy
// This ensures we don't fire the emailVerified event on the initial user fetch
if (previousHasVerifiedEmail !== undefined && hasVerifiedEmail) {
analytics.emailVerifiedEvent();
}
}, [previousHasVerifiedEmail, hasVerifiedEmail]);
useEffect(() => {
if (previousRewardApproved !== undefined && isRewardApproved) {
analytics.rewardEligibleEvent();
}
}, [previousRewardApproved, isRewardApproved]);
// @if TARGET='web' // @if TARGET='web'
useEffect(() => { useEffect(() => {

View file

@ -1,3 +1,4 @@
import * as PAGES from 'constants/pages';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doResolveUri, doResolveUri,
@ -10,11 +11,15 @@ import {
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
selectBlockedChannels, selectBlockedChannels,
selectChannelIsBlocked, selectChannelIsBlocked,
doClearPublish,
doPrepareEdit,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { push } from 'connected-react-router';
import ClaimPreview from './view'; import ClaimPreview from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -36,6 +41,11 @@ const select = (state, props) => ({
const perform = dispatch => ({ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
beginPublish: name => {
dispatch(doClearPublish());
dispatch(doPrepareEdit({ name }));
dispatch(push(`/$/${PAGES.PUBLISH}`));
},
}); });
export default connect( export default connect(

View file

@ -44,6 +44,7 @@ type Props = {
blockedChannelUris: Array<string>, blockedChannelUris: Array<string>,
channelIsBlocked: boolean, channelIsBlocked: boolean,
isSubscribed: boolean, isSubscribed: boolean,
beginPublish: string => void,
}; };
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -68,6 +69,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
showUserBlocked, showUserBlocked,
channelIsBlocked, channelIsBlocked,
isSubscribed, isSubscribed,
beginPublish,
} = props; } = props;
const haventFetched = claim === undefined; const haventFetched = claim === undefined;
const abandoned = !isResolvingUri && !claim; const abandoned = !isResolvingUri && !claim;
@ -77,8 +79,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
const hideActions = type === 'small' || type === 'tooltip'; const hideActions = type === 'small' || type === 'tooltip';
let isValid; let isValid;
let name;
try { try {
parseURI(uri); ({ claimName: name } = parseURI(uri));
isValid = true; isValid = true;
} catch (e) { } catch (e) {
isValid = false; isValid = false;
@ -191,7 +194,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<Fragment> <Fragment>
<div>{__('Publish something and claim this spot!')}</div> <div>{__('Publish something and claim this spot!')}</div>
<div className="card__actions"> <div className="card__actions">
<Button button="primary" label={`${__('Publish to')} ${uri}`} /> <Button
onClick={() => beginPublish(name)}
button="primary"
label={`${__('Publish to')} ${uri}`}
/>
</div> </div>
</Fragment> </Fragment>
)} )}

View file

@ -25,7 +25,7 @@ class IconComponent extends React.PureComponent<Props> {
case ICONS.FEATURED: case ICONS.FEATURED:
return __('Featured content. Earn rewards for watching.'); return __('Featured content. Earn rewards for watching.');
case ICONS.DOWNLOAD: case ICONS.DOWNLOAD:
return __('This file is downloaded.'); return __('This file is in your library.');
case ICONS.SUBSCRIBE: case ICONS.SUBSCRIBE:
return __('You are subscribed to this channel.'); return __('You are subscribed to this channel.');
case ICONS.SETTINGS: case ICONS.SETTINGS:

View file

@ -16,9 +16,7 @@ type Props = {
class FileActions extends React.PureComponent<Props> { class FileActions extends React.PureComponent<Props> {
render() { render() {
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props; const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props;
const showDelete = const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
claimIsMine ||
(fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed === fileInfo.blobs_in_stream));
return ( return (
<React.Fragment> <React.Fragment>
{showDelete && ( {showDelete && (

View file

@ -77,6 +77,7 @@ class FileDetails extends PureComponent<Props> {
{': '} {': '}
<Button <Button
button="link" button="link"
className="button--download-link"
onClick={() => { onClick={() => {
if (downloadPath) { if (downloadPath) {
openFolder(downloadPath); openFolder(downloadPath);

View file

@ -20,12 +20,10 @@ type Props = {
function FileDownloadLink(props: Props) { function FileDownloadLink(props: Props) {
const { fileInfo, downloading, loading, openModal, pause, claimIsMine, download, uri } = props; const { fileInfo, downloading, loading, openModal, pause, claimIsMine, download, uri } = props;
if (loading || downloading) { if (downloading || loading) {
const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0; const progress = fileInfo && fileInfo.written_bytes > 0 ? (fileInfo.written_bytes / fileInfo.total_bytes) * 100 : 0;
const label = const label =
fileInfo && fileInfo.written_bytes > 0 fileInfo && fileInfo.written_bytes > 0 ? progress.toFixed(0) + __('% downloaded') : __('Connecting...');
? __('Downloading: ') + progress.toFixed(0) + __('% complete')
: __('Connecting...');
return <span>{label}</span>; return <span>{label}</span>;
} }

View file

@ -1,12 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectClaimIsMine } from 'lbry-redux'; import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc'; import { selectRewardContentClaimIds } from 'lbryinc';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
import FileProperties from './view'; import FileProperties from './view';
const select = (state, props) => ({ const select = (state, props) => ({
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
downloaded: !!makeSelectFileInfoForUri(props.uri)(state), downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state), isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state), isNew: makeSelectIsNew(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),

View file

@ -12,6 +12,7 @@ import { makeSelectIsPlaying, makeSelectShouldObscurePreview, selectPlayingUri }
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { doAnalyticsView } from 'redux/actions/app';
import FileViewer from './view'; import FileViewer from './view';
const select = (state, props) => { const select = (state, props) => {
@ -32,6 +33,7 @@ const select = (state, props) => {
const perform = dispatch => ({ const perform = dispatch => ({
clearPlayingUri: () => dispatch(doSetPlayingUri(null)), clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
triggerAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
}); });
export default withRouter( export default withRouter(

View file

@ -1,12 +1,13 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames'; import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import FileRender from 'component/fileRender'; import FileRender from 'component/fileRender';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'util/use-persisted-state';
import usePrevious from 'util/use-previous';
import { FILE_WRAPPER_CLASS } from 'page/file/view'; import { FILE_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
@ -27,6 +28,7 @@ type Props = {
title: ?string, title: ?string,
floatingPlayerEnabled: boolean, floatingPlayerEnabled: boolean,
clearPlayingUri: () => void, clearPlayingUri: () => void,
triggerAnalyticsView: (string, number) => void,
}; };
export default function FileViewer(props: Props) { export default function FileViewer(props: Props) {
@ -40,7 +42,9 @@ export default function FileViewer(props: Props) {
title, title,
clearPlayingUri, clearPlayingUri,
floatingPlayerEnabled, floatingPlayerEnabled,
triggerAnalyticsView,
} = props; } = props;
const [playTime, setPlayTime] = useState();
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect'); const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
const [position, setPosition] = usePersistedState('floating-file-viewer:position', { const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
x: -25, x: -25,
@ -54,15 +58,24 @@ export default function FileViewer(props: Props) {
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.") ? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
: __('Loading'); : __('Loading');
function handleDrag(e, ui) { const previousUri = usePrevious(uri);
const { x, y } = position; const previousIsReadyToPlay = usePrevious(isReadyToPlay);
const newX = x + ui.deltaX; const isNewView = uri && previousUri !== uri && isPlaying;
const newY = y + ui.deltaY; const wasntReadyButNowItIs = isReadyToPlay && !previousIsReadyToPlay;
setPosition({
x: newX, useEffect(() => {
y: newY, if (isNewView) {
}); setPlayTime(Date.now());
} }
}, [isNewView, uri]);
useEffect(() => {
if (playTime && isReadyToPlay && wasntReadyButNowItIs) {
const timeToStart = Date.now() - playTime;
triggerAnalyticsView(uri, timeToStart);
setPlayTime(null);
}
}, [setPlayTime, triggerAnalyticsView, isReadyToPlay, wasntReadyButNowItIs, playTime, uri]);
useEffect(() => { useEffect(() => {
function handleResize() { function handleResize() {
@ -85,9 +98,18 @@ export default function FileViewer(props: Props) {
} }
}, [setFileViewerRect, inline]); }, [setFileViewerRect, inline]);
function handleDrag(e, ui) {
const { x, y } = position;
const newX = x + ui.deltaX;
const newY = y + ui.deltaY;
setPosition({
x: newX,
y: newY,
});
}
const hidePlayer = !isPlaying || !uri || (!inline && (!floatingPlayerEnabled || !isStreamable)); const hidePlayer = !isPlaying || !uri || (!inline && (!floatingPlayerEnabled || !isStreamable));
if (hidePlayer) { if (hidePlayer) {
clearPlayingUri();
return null; return null;
} }

View file

@ -25,7 +25,18 @@ type Props = {
}; };
export default function FileViewer(props: Props) { export default function FileViewer(props: Props) {
const { play, mediaType, isPlaying, fileInfo, uri, obscurePreview, insufficientCredits, thumbnail, autoplay } = props; const {
play,
mediaType,
isPlaying,
fileInfo,
uri,
obscurePreview,
insufficientCredits,
thumbnail,
autoplay,
isStreamable,
} = props;
const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1; const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1;
const fileStatus = fileInfo && fileInfo.status; const fileStatus = fileInfo && fileInfo.status;
@ -64,10 +75,10 @@ export default function FileViewer(props: Props) {
useEffect(() => { useEffect(() => {
const videoOnPage = document.querySelector('video'); const videoOnPage = document.querySelector('video');
if (autoplay && !videoOnPage) { if (autoplay && !videoOnPage && isStreamable) {
viewFile(); viewFile();
} }
}, [autoplay, viewFile]); }, [autoplay, viewFile, isStreamable]);
return ( return (
<div <div

View file

@ -3,6 +3,7 @@ import * as React from 'react';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import analytics from 'analytics';
type Props = { type Props = {
cancelButton: React.Node, cancelButton: React.Node,
@ -37,6 +38,7 @@ class UserEmailNew extends React.PureComponent<Props, State> {
const { email } = this.state; const { email } = this.state;
const { addUserEmail } = this.props; const { addUserEmail } = this.props;
addUserEmail(email); addUserEmail(email);
analytics.emailProvidedEvent();
// @if TARGET='web' // @if TARGET='web'
Lbryio.call('user_tag', 'edit', { add: 'lbrytv' }); Lbryio.call('user_tag', 'edit', { add: 'lbrytv' });

View file

@ -1,11 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { doToast, doUploadThumbnail } from 'lbry-redux'; import { doToast, doUploadThumbnail } from 'lbry-redux';
import fs from 'fs';
import ModalAutoGenerateThumbnail from './view'; import ModalAutoGenerateThumbnail from './view';
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
upload: buffer => dispatch(doUploadThumbnail(null, buffer)), upload: buffer => dispatch(doUploadThumbnail(null, buffer, null, fs)),
showToast: options => dispatch(doToast(options)), showToast: options => dispatch(doToast(options)),
}); });

View file

@ -1,11 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { doUploadThumbnail, doUpdatePublishForm } from 'lbry-redux'; import { doUploadThumbnail, doUpdatePublishForm } from 'lbry-redux';
import fs from 'fs';
import ModalConfirmThumbnailUpload from './view'; import ModalConfirmThumbnailUpload from './view';
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
upload: path => dispatch(doUploadThumbnail(path)), upload: path => dispatch(doUploadThumbnail(path, null, null, fs)),
updatePublishForm: value => dispatch(doUpdatePublishForm(value)), updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
}); });

View file

@ -1,25 +1,19 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doDeleteFileAndMaybeGoBack } from 'redux/actions/file'; import { doDeleteFileAndMaybeGoBack } from 'redux/actions/file';
import { import { makeSelectTitleForUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
makeSelectTitleForUri,
makeSelectClaimIsMine,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
} from 'lbry-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import ModalRemoveFile from './view'; import ModalRemoveFile from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
deleteFile: (fileInfo, deleteFromComputer, abandonClaim) => { deleteFile: (uri, deleteFromComputer, abandonClaim) => {
dispatch(doDeleteFileAndMaybeGoBack(fileInfo, deleteFromComputer, abandonClaim)); dispatch(doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim));
}, },
}); });

View file

@ -6,6 +6,7 @@ import Button from 'component/button';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'util/use-persisted-state';
type Props = { type Props = {
uri: string,
claim: StreamClaim, claim: StreamClaim,
claimIsMine: boolean, claimIsMine: boolean,
closeModal: () => void, closeModal: () => void,
@ -17,11 +18,9 @@ type Props = {
}; };
function ModalRemoveFile(props: Props) { function ModalRemoveFile(props: Props) {
const { claim, claimIsMine, closeModal, deleteFile, fileInfo, title } = props; const { uri, claimIsMine, closeModal, deleteFile, title } = props;
const [deleteChecked, setDeleteChecked] = usePersistedState('modal-remove-file:delete', true); const [deleteChecked, setDeleteChecked] = usePersistedState('modal-remove-file:delete', true);
const [abandonChecked, setAbandonChecked] = usePersistedState('modal-remove-file:abandon', true); const [abandonChecked, setAbandonChecked] = usePersistedState('modal-remove-file:abandon', true);
const { txid, nout } = claim;
const outpoint = fileInfo ? fileInfo.outpoint : `${txid}:${nout}`;
return ( return (
<Modal isOpen title="Remove File" contentLabel={__('Confirm File Remove')} type="custom" onAborted={closeModal}> <Modal isOpen title="Remove File" contentLabel={__('Confirm File Remove')} type="custom" onAborted={closeModal}>
@ -30,7 +29,7 @@ function ModalRemoveFile(props: Props) {
{__("Are you sure you'd like to remove")} <cite>{`"${title}"`}</cite> {__('from the LBRY app?')} {__("Are you sure you'd like to remove")} <cite>{`"${title}"`}</cite> {__('from the LBRY app?')}
</p> </p>
</section> </section>
<Form onSubmit={() => deleteFile(outpoint || '', deleteChecked, abandonChecked)}> <Form onSubmit={() => deleteFile(uri, deleteChecked, claimIsMine ? abandonChecked : false)}>
<FormField <FormField
name="file_delete" name="file_delete"
label={__('Also delete this file from my computer')} label={__('Also delete this file from my computer')}
@ -49,13 +48,7 @@ function ModalRemoveFile(props: Props) {
/> />
)} )}
<div className="card__actions"> <div className="card__actions">
<Button <Button type="submit" button="primary" label={__('OK')} disabled={!deleteChecked && !abandonChecked} />
autoFocus
button="primary"
label={__('OK')}
disabled={!deleteChecked && !abandonChecked}
onClick={() => deleteFile(outpoint || '', deleteChecked, abandonChecked)}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} /> <Button button="link" label={__('Cancel')} onClick={closeModal} />
</div> </div>
</Form> </Form>

View file

@ -22,6 +22,7 @@ import { doFetchViewCount, makeSelectViewCountForUri, makeSelectCostInfoForUri,
import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings'; import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import fs from 'fs';
import FilePage from './view'; import FilePage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -47,7 +48,7 @@ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo)), prepareEdit: (publishData, uri, fileInfo) => dispatch(doPrepareEdit(publishData, uri, fileInfo, fs)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)), setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)), markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),

View file

@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import * as settings from 'constants/settings'; import * as settings from 'constants/settings';
import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet } from 'redux/actions/app'; import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet } from 'redux/actions/app';
import { doSetDaemonSetting, doSetClientSetting, doGetThemes, doChangeLanguage } from 'redux/actions/settings'; import { doSetDaemonSetting, doSetClientSetting, doGetThemes, doChangeLanguage } from 'redux/actions/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import { import {
makeSelectClientSetting, makeSelectClientSetting,
selectDaemonSettings, selectDaemonSettings,
@ -40,6 +41,7 @@ const perform = dispatch => ({
encryptWallet: () => dispatch(doNotifyEncryptWallet()), encryptWallet: () => dispatch(doNotifyEncryptWallet()),
decryptWallet: () => dispatch(doNotifyDecryptWallet()), decryptWallet: () => dispatch(doNotifyDecryptWallet()),
updateWalletStatus: () => dispatch(doWalletStatus()), updateWalletStatus: () => dispatch(doWalletStatus()),
clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
}); });
export default connect( export default connect(

View file

@ -168,6 +168,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
hideBalance, hideBalance,
userBlockedChannelsCount, userBlockedChannelsCount,
floatingPlayer, floatingPlayer,
clearPlayingUri,
} = this.props; } = this.props;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -319,6 +320,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
name="floating_player" name="floating_player"
onChange={() => { onChange={() => {
setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer); setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer);
clearPlayingUri();
}} }}
checked={floatingPlayer} checked={floatingPlayer}
label={__('Floating video player')} label={__('Floating video player')}

View file

@ -6,7 +6,14 @@ import { ipcRenderer, remote } from 'electron';
import path from 'path'; import path from 'path';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import { Lbry, doBalanceSubscribe, doFetchFileInfosAndPublishedClaims, doError } from 'lbry-redux'; import {
Lbry,
doBalanceSubscribe,
doFetchFileInfosAndPublishedClaims,
doError,
makeSelectClaimForUri,
makeSelectClaimIsMine,
} from 'lbry-redux';
import Native from 'native'; import Native from 'native';
import { doFetchDaemonSettings } from 'redux/actions/settings'; import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions'; import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions';
@ -24,6 +31,7 @@ import {
import { doAuthenticate } from 'lbryinc'; import { doAuthenticate } from 'lbryinc';
import { lbrySettings as config, version as appVersion } from 'package.json'; import { lbrySettings as config, version as appVersion } from 'package.json';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import analytics from 'analytics';
// @if TARGET='app' // @if TARGET='app'
const { autoUpdater } = remote.require('electron-updater'); const { autoUpdater } = remote.require('electron-updater');
@ -410,3 +418,18 @@ export function doToggleSearchExpanded() {
type: ACTIONS.TOGGLE_SEARCH_EXPANDED, type: ACTIONS.TOGGLE_SEARCH_EXPANDED,
}; };
} }
export function doAnalyticsView(uri, timeToStart) {
return (dispatch, getState) => {
const state = getState();
const { txid, nout, claim_id: claimId } = makeSelectClaimForUri(uri)(state);
const claimIsMine = makeSelectClaimIsMine(uri)(state);
const outpoint = `${txid}:${nout}`;
if (claimIsMine) {
return;
}
analytics.apiLogView(uri, outpoint, claimId, timeToStart);
};
}

View file

@ -225,6 +225,11 @@ export function doPlayUri(uri: string, skipCostCheck: boolean = false, saveFileO
} }
} }
if (fileInfo && saveFile && (!fileInfo.download_path || !fileInfo.written_bytes)) {
beginGetFile();
return;
}
if (cost === 0 || skipCostCheck) { if (cost === 0 || skipCostCheck) {
beginGetFile(); beginGetFile();
return; return;

View file

@ -2,9 +2,11 @@ import * as ACTIONS from 'constants/action_types';
// @if TARGET='app' // @if TARGET='app'
import { shell } from 'electron'; import { shell } from 'electron';
// @endif // @endif
import { Lbry, batchActions, doAbandonClaim, selectMyClaimsOutpoints } from 'lbry-redux'; import { Lbry, batchActions, doAbandonClaim, selectMyClaimsOutpoints, makeSelectFileInfoForUri } from 'lbry-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { goBack } from 'connected-react-router'; import { goBack } from 'connected-react-router';
import { doSetPlayingUri } from 'redux/actions/content';
import { selectPlayingUri } from 'redux/selectors/content';
export function doOpenFileInFolder(path) { export function doOpenFileInFolder(path) {
return () => { return () => {
@ -47,14 +49,23 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
}; };
} }
export function doDeleteFileAndMaybeGoBack(fileInfo, deleteFromComputer, abandonClaim) { export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim) {
return dispatch => { return (dispatch, getState) => {
const state = getState();
const playingUri = selectPlayingUri(state);
const { outpoint } = makeSelectFileInfoForUri(uri)(state);
const actions = []; const actions = [];
actions.push(doHideModal()); actions.push(doHideModal());
actions.push(doDeleteFile(fileInfo, deleteFromComputer, abandonClaim)); actions.push(doDeleteFile(outpoint, deleteFromComputer, abandonClaim));
dispatch(batchActions(...actions));
if (abandonClaim) { if (playingUri === uri) {
dispatch(goBack()); actions.push(doSetPlayingUri(null));
} }
if (abandonClaim) {
actions.push(goBack());
}
dispatch(batchActions(...actions));
}; };
} }

View file

@ -266,7 +266,11 @@ export const makeSelectIsSubscribed = uri =>
} }
// If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already // If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
const { isChannel } = parseURI(uri); let isChannel;
try {
({ isChannel } = parseURI(uri));
} catch (e) {}
if (isChannel) { if (isChannel) {
const uriWithPrefix = uri.startsWith('lbry://') ? uri : `lbry://${uri}`; const uriWithPrefix = uri.startsWith('lbry://') ? uri : `lbry://${uri}`;
return subscriptions.some(sub => sub.uri === uriWithPrefix); return subscriptions.some(sub => sub.uri === uriWithPrefix);

View file

@ -94,6 +94,16 @@
align-items: flex-start; align-items: flex-start;
} }
// Quick fix because this is a pain
// There is something weird with wrapping buttons. Some places we want to wrap and others we want to ellips
// Probably requires some nested style cleanup
.button--download-link {
.button__label {
white-space: normal;
text-align: left;
}
}
.button__content { .button__content {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -85,11 +85,7 @@
} }
.content__loading { .content__loading {
position: absolute; height: 100%;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;

View file

@ -95,4 +95,11 @@
.vjs-big-play-button { .vjs-big-play-button {
display: none; display: none;
} }
.vjs-modal-dialog .vjs-modal-dialog-content {
position: relative;
padding-top: 5rem;
// Make sure no videojs message interferes with overlaying buttons
pointer-events: none;
}
} }

View file

@ -98,6 +98,11 @@ hr {
} }
} }
img,
a {
-webkit-user-drag: none;
}
.columns { .columns {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -12,7 +12,7 @@ export default function usePersistedState(key, firstTimeDefault) {
parsedItem = JSON.parse(item); parsedItem = JSON.parse(item);
} catch (e) {} } catch (e) {}
if (parsedItem) { if (parsedItem !== undefined) {
defaultValue = parsedItem; defaultValue = parsedItem;
} else { } else {
defaultValue = item; defaultValue = item;

View file

@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
export default function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}

View file

@ -6762,18 +6762,18 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#05e70648e05c51c51710f6dd698a8e2219b54df2: lbry-redux@lbryio/lbry-redux#6005fa245a888e2de045d7e42411847de7943f52:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/05e70648e05c51c51710f6dd698a8e2219b54df2" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/6005fa245a888e2de045d7e42411847de7943f52"
dependencies: dependencies:
mime "^2.4.4" mime "^2.4.4"
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
uuid "^3.3.2" uuid "^3.3.2"
lbryinc@lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb: lbryinc@lbryio/lbryinc#1ce266b3c52654190b955e9c869b8e302aa5c585:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/a93596c51c8fb0a226cb84df04c26a6bb60a45fb" resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/1ce266b3c52654190b955e9c869b8e302aa5c585"
dependencies: dependencies:
reselect "^3.0.0" reselect "^3.0.0"