Random fixes #2729

Merged
neb-b merged 12 commits from fixes into master 2019-08-15 05:11:48 +02:00
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');
}, },
}; };
kauffj commented 2019-08-15 00:26:25 +02:00 (Migrated from github.com)
Review

Making a sendReactGAEvent or equivalent function would eliminate these repeated if checks and could also help ensure the checks are not accidentally left out in the future.

Making a `sendReactGAEvent` or equivalent function would eliminate these repeated if checks and could also help ensure the checks are not accidentally left out in the future.
neb-b commented 2019-08-15 05:11:31 +02:00 (Migrated from github.com)
Review

Yeah I've been meaning to do this, just did and it's way nicer now.

Yeah I've been meaning to do this, just did and it's way nicer now.
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,
kauffj commented 2019-08-15 00:28:26 +02:00 (Migrated from github.com)
Review

😂

:joy:
y: newY,
});
}
const hidePlayer = !isPlaying || !uri || (!inline && (!floatingPlayerEnabled || !isStreamable)); const hidePlayer = !isPlaying || !uri || (!inline && (!floatingPlayerEnabled || !isStreamable));
neb-b commented 2019-08-14 20:12:29 +02:00 (Migrated from github.com)
Review

@tzarebczan brought up that we might not want to do play time (or keep play time separate) for non-streamable content.

@tzarebczan brought up that we might not want to do play time (or keep play time separate) for non-streamable content.
kauffj commented 2019-08-15 00:28:05 +02:00 (Migrated from github.com)
Review

I'm not particularly concerned about this, I think it can be cleaned up in internal analytics.

I'm not particularly concerned about this, I think it can be cleaned up in internal analytics.
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)),
kauffj commented 2019-08-15 00:29:01 +02:00 (Migrated from github.com)
Review

should these 3rd and 4th params be re-ordered? (meh)

should these 3rd and 4th params be re-ordered? (meh)
neb-b commented 2019-08-15 04:57:51 +02:00 (Migrated from github.com)
Review

The third param is the android equivalent of fs

The third param is the android equivalent of `fs`
}); });

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)),
kauffj commented 2019-08-15 00:29:31 +02:00 (Migrated from github.com)
Review

same question as above, if fs is regularly provided but other params are not, consider moving fs up in param order

when does JS get named parameters?!

same question as above, if `fs` is regularly provided but other params are not, consider moving fs up in param order when does JS get named parameters?!
}); });

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
kauffj commented 2019-08-15 00:30:46 +02:00 (Migrated from github.com)
Review

Good job noting why the rules are being broken so a future dev and/or future you can understand

Good job noting why the rules are being broken so a future dev and/or future you can understand
// 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 {
kauffj commented 2019-08-15 00:31:16 +02:00 (Migrated from github.com)
Review

Confident in no consequences to this rule being so universal?

Confident in no consequences to this rule being so universal?
neb-b commented 2019-08-15 04:59:16 +02:00 (Migrated from github.com)
Review

97% confidence

97% confidence
-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"