privacy changes:

users see welcome screen once and choose preference
SETTINGS moved to redux
took steps toward eliminating unwanted analytics in app based on preferences
settings page update to privacy controls and copy

persist welcome version

default tv on

cleanup

clean up appstrings

populate prefs app only

wallet custody, app only router

settings on startup

welcome sync, 3p share sync, emojis

bump redux

cleanup

fix app not building

fix sync bug, remove tvWelcomeVersion

cleanup

disable internalshare setting while signed in
This commit is contained in:
jessop 2020-02-19 01:31:40 -05:00 committed by Sean Yesmunt
parent 45bbd77109
commit 6e13fcfbd3
24 changed files with 635 additions and 71 deletions

View file

@ -7,6 +7,7 @@ const config = {
SITE_TITLE: 'lbry.tv',
LBRY_TV_API: 'https://api.lbry.tv',
LBRY_TV_STREAMING_API: 'https://player.lbry.tv',
WELCOME_VERSION: 1.0,
};
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;

View file

@ -131,7 +131,7 @@
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#3d64f8acc6c2ce37252f59feff89e1fc58cb74c1",
"lbry-redux": "lbryio/lbry-redux#b4fbc212ca6008ec05c31116182bf6dfa7a1cbcb",
"lbryinc": "lbryio/lbryinc#6a59102c52673502569d2c43bd4ee58c315fb2e4",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",

View file

@ -963,5 +963,24 @@
"Find new channels to follow": "Find new channels to follow",
"You aren't currently following any channels. %discover_channels_link%.": "You aren't currently following any channels. %discover_channels_link%.",
"LBRY Works Better If You Are Following Channels": "LBRY Works Better If You Are Following Channels",
"Saved zip archive to %outputPath%": "Saved zip archive to %outputPath%"
"Saved zip archive to %outputPath%": "Saved zip archive to %outputPath%",
"Share Usage and Diagnostic Data": "Share Usage and Diagnostic Data",
"This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)": "This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)",
"Allow the app to share data to LBRY.inc": "Allow the app to share data to LBRY.inc",
"Internal sharing is required to participate in rewards programs.": "Internal sharing is required to participate in rewards programs.",
"Allow the App to access third party analytics platforms": "Allow the App to access third party analytics platforms",
"We use detailed analytics to improve all aspects of the LBRY experience.": "We use detailed analytics to improve all aspects of the LBRY experience.",
"Welcome": "Welcome",
"LBRY takes privacy and choice seriously. Just a few questions before you enter the land of content freedom. ": "LBRY takes privacy and choice seriously. Just a few questions before you enter the land of content freedom. ",
"Can this app send information about your usage to inform publishers and improve the software?": "Can this app send information about your usage to inform publishers and improve the software?",
"Yes, including with third-party analytics platforms": "Yes, including with third-party analytics platforms",
"Sending information to third parties (e.g. Google Analytics or Mixpanel) allows us to use detailed\n analytical reports to improve all aspects of LBRY.": "Sending information to third parties (e.g. Google Analytics or Mixpanel) allows us to use detailed\n analytical reports to improve all aspects of LBRY.",
"Yes, but only with LBRY, Inc.": "Yes, but only with LBRY, Inc.",
"Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.": "Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as\n well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.",
"No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.": "No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as\n peer-to-peer software, your IP address and potentially other system information can be sent to other\n users, though this information is not stored permanently.",
"Let's go": "Let's go",
"Do you agree to the %terms%?": "Do you agree to the %terms%?",
"While we respect the desire for maximally private usage, please note that choosing this option hurts the ability for creators to understand how their content is performing.": "While we respect the desire for maximally private usage, please note that choosing this option hurts the ability for creators to understand how their content is performing.",
"A copy of your wallet is synced to lbry.tv": "A copy of your wallet is synced to lbry.tv",
"Internal sharing is required while signed in.": "Internal sharing is required while signed in."
}

210
static/img/unlocklbry.svg Normal file
View file

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 757.64 896.74"
version="1.1"
id="svg62"
sodipodi:docname="unlocklbry.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata66">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>unlock</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1051"
id="namedview64"
showgrid="false"
inkscape:zoom="0.37218637"
inkscape:cx="97.177994"
inkscape:cy="700.34675"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg62" />
<defs
id="defs4">
<style
id="style2">.cls-1{fill:#f1f1f1;}.cls-2{fill:#e49d75;}.cls-3{fill:#5fc5d1;}.cls-4{fill:#6bd5e1;}.cls-5{fill:#e0e0e0;}.cls-6{fill:#a4a4a4;}.cls-7{fill:none;stroke:#e0e0e0;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px;}.cls-8{fill:#ffd98e;}.cls-9{fill:#ffe4ad;}.cls-10{fill-rule:evenodd;}</style>
</defs>
<title
id="title6">unlock</title>
<g
id="Layer_2"
data-name="Layer 2">
<g
id="vector">
<path
class="cls-1"
d="M636.5,232.73c68.06,61.33,93.17,180.88,67.33,286.61C677.81,625.07,601,717,496,778.29c-105,61.14-238,91.9-326.82,43.86S36.17,647.63,13.78,523.53c-22.2-124.11-22.56-245.67,36.58-305C109.5,159.4,228,162.31,342.79,165.22,457.62,168.31,568.62,171.41,636.5,232.73Z"
id="path8" />
<rect
class="cls-2"
x="558.8"
y="858.32"
width="22.08"
height="14.74"
id="rect10" />
<rect
class="cls-2"
x="677.14"
y="835.57"
width="20.72"
height="18.36"
transform="translate(-378.13 619.59) rotate(-38.93)"
id="rect12" />
<rect
class="cls-2"
x="586.33"
y="256.37"
width="17.47"
height="20.69"
rx="8.23"
id="rect14" />
<path
class="cls-3"
d="M602.4,567.42c3,6.88,26,144.94,29.38,158s57.45,98.2,61.13,103.29c1.13,7.36-15.57,19.81-23.49,19.81C661.5,842.26,569,708.23,566.78,702.28S557.11,553.94,602.4,567.42Z"
id="path16"
style="fill:#4b8576;fill-opacity:1" />
<path
class="cls-4"
d="M505.83,544.55c-1.05,5.7,14.58,136,16.52,153.5s31.89,158.48,34.54,162,22.81,2.76,26.29,1-6.8-147-6.8-157.53,27-107.65,23.53-135.78S505.83,544.55,505.83,544.55Z"
id="path18"
style="fill:#257761;fill-opacity:1" />
<rect
class="cls-2"
x="561.36"
y="386.6"
width="21.11"
height="36.74"
id="rect20" />
<rect
class="cls-5"
x="94.97"
y="137.85"
width="379.57"
height="379.57"
rx="22.42"
id="rect22" />
<path
class="cls-6"
d="M308.48,310.8a35.09,35.09,0,1,0-47.45,0L234,420.41H335.48Z"
id="path24" />
<path
class="cls-7"
d="M162.9,137.85a121.86,121.86,0,0,1,243.71,0"
id="path26" />
<ellipse
class="cls-2"
cx="569.19"
cy="353.64"
rx="29"
ry="39"
id="ellipse28" />
<path
class="cls-8"
d="M565.24,415.77c-35.16,0-66.5,97.54-60.48,129.91,1.78,9.56,85.92,28.85,98.78,26.15C608.11,565.61,626.76,415.77,565.24,415.77Z"
id="path30"
style="fill:#fff58c;fill-opacity:1" />
<path
class="cls-9"
d="M556.22,470.1c15.74-11.42-9.42-66.76-15.3-67.71-10.83-1.75-37.76,19-36.91,27.72C504.46,434.71,540.19,481.73,556.22,470.1Z"
id="path32"
style="fill:#fff58c;fill-opacity:1" />
<path
class="cls-2"
d="M509,426c-4.24-5.09-14.84-30-16-35.91s-4-94.74-4-100S482,271.56,482,263.64s8.06-18.66,12.16-18.8c4.38,3.11,10.46,21.35,10.74,26,1.53-10,8.77-12.3,10.75-12.3s1.72,15.45-6.48,25.85c.29,7.64,10,83.31,11.57,87.69s19.45,33.09,19.41,35.63S509,426,509,426Z"
id="path34" />
<rect
class="cls-6"
x="368.05"
y="260.35"
width="358.11"
height="10.23"
transform="translate(40.99 -71.64) rotate(7.78)"
id="rect36" />
<rect
class="cls-6"
x="366.9"
y="247.63"
width="21.9"
height="29.63"
transform="translate(39.02 -48.75) rotate(7.78)"
id="rect38" />
<rect
class="cls-6"
x="397.87"
y="251.17"
width="11.78"
height="29.63"
transform="translate(39.74 -52.22) rotate(7.78)"
id="rect40" />
<path
class="cls-2"
d="M604.74,334c-.83-4.74-1.58-39.09-.3-43.94s4.74-8.39,6.29-15.63c.53-2.49-.31-11.11-2-11.47-3.64-.78-3.85,3.3-4.33,6.3-1.86,10.23-5.65,8.36-7.4,8.12s-7.76-1.28-9.73-1.33,3.05,4,1.52,10.47c-1.2,5.09-6.84,39-8,44.55C585,331.24,604.74,334,604.74,334Z"
id="path42" />
<rect
class="cls-6"
x="417.79"
y="253.89"
width="11.78"
height="29.63"
transform="translate(40.29 -54.9) rotate(7.78)"
id="rect44" />
<rect
class="cls-6"
x="368.01"
y="250.51"
width="63.09"
height="10.5"
transform="translate(38.31 -51.75) rotate(7.78)"
id="rect46" />
<path
class="cls-6"
d="M734.69,264.08A26.54,26.54,0,1,0,757.4,294,26.54,26.54,0,0,0,734.69,264.08ZM749,292.82a18.07,18.07,0,1,1-15.45-20.35A18.07,18.07,0,0,1,749,292.82Z"
id="path48" />
<path
class="cls-2"
d="M509.28,284.31c-1.32-5.17-3.89-9.69-4.31-13.58l0,.12c-.28-4.66-6.36-22.9-10.74-26-4.1.14-12.16,10.88-12.16,18.8,0,5.81,3.8,14.5,5.82,20.79h21.35v0Z"
id="path50" />
<path
d="M543.06,329.21c0,13.49,9.52,21.81,24.61,26.45,0,18.46,2.85,19.27,2.85,30.51,0,24.22-4.13,12.11-4.13,53.93,0,25.22,10.56,38.94,30,38.94,23.51,0,37-24.87,37-38.27,0-17-16.82-46.54-18-71.07-1.63-33-11.89-60.61-37.62-60.61C555.79,309.09,543.06,317.27,543.06,329.21Z"
id="path52" />
<circle
class="cls-2"
cx="569.16"
cy="358.82"
r="7.8"
id="circle54" />
<path
class="cls-10"
d="M701.29,842.49l12.5,17.06-26.15,37-24.68.16.32-12.51,9.91-4.38L682,858.08Z"
id="path56" />
<polyline
points="582.36 871.83 582.36 896.74 509.14 896.74 509.14 885.71 557.45 871.83 582.36 871.83"
id="polyline58" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -16,6 +16,9 @@ const LBRY_TV_UA_ID = 'UA-60403362-12';
const DESKTOP_UA_ID = 'UA-60403362-13';
const SECOND_TRACKER_NAME = 'tracker2';
const SHARE_INTERNAL = 'shareInternal';
const SHARE_THIRD_PARTY = 'shareThirdParty';
// @if TARGET='app'
ElectronCookies.enable({
origin: 'https://lbry.tv',
@ -27,7 +30,8 @@ type Analytics = {
sentryError: ({}, {}) => Promise<any>,
pageView: string => void,
setUser: Object => void,
toggle: (boolean, ?boolean) => void,
toggleInternal: (boolean, ?boolean) => void,
toggleThirdParty: (boolean, ?boolean) => void,
apiLogView: (string, string, string, ?number, ?() => void) => Promise<any>,
apiLogPublish: (ChannelClaim | StreamClaim) => void,
tagFollowEvent: (string, boolean, string) => void,
@ -48,12 +52,17 @@ type LogPublishParams = {
channel_claim_id?: string,
};
let analyticsEnabled: boolean = isProduction;
let internalAnalyticsEnabled: boolean = IS_WEB || false;
let thirdPartyAnalyticsEnabled: boolean = IS_WEB || false;
// @if TARGET='app'
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
if (window.localStorage.getItem(SHARE_THIRD_PARTY) === 'true') thirdPartyAnalyticsEnabled = true;
// @endif
const analytics: Analytics = {
error: message => {
return new Promise(resolve => {
if (analyticsEnabled && isProduction) {
if (internalAnalyticsEnabled && isProduction) {
return Lbryio.call('event', 'desktop_error', { error_message: message }).then(() => {
resolve(true);
});
@ -64,7 +73,7 @@ const analytics: Analytics = {
},
sentryError: (error, errorInfo) => {
return new Promise(resolve => {
if (analyticsEnabled && isProduction) {
if (internalAnalyticsEnabled && isProduction) {
Sentry.withScope(scope => {
scope.setExtras(errorInfo);
const eventId = Sentry.captureException(error);
@ -76,12 +85,12 @@ const analytics: Analytics = {
});
},
pageView: path => {
if (analyticsEnabled) {
if (thirdPartyAnalyticsEnabled) {
ReactGA.pageview(path, [SECOND_TRACKER_NAME]);
}
},
setUser: userId => {
if (analyticsEnabled && userId) {
if (thirdPartyAnalyticsEnabled && userId) {
ReactGA.set({
userId,
});
@ -93,15 +102,25 @@ const analytics: Analytics = {
// @endif
}
},
toggle: (enabled: boolean): void => {
toggleInternal: (enabled: boolean): void => {
// Always collect analytics on lbry.tv
// @if TARGET='app'
analyticsEnabled = enabled;
internalAnalyticsEnabled = enabled;
window.localStorage.setItem(SHARE_INTERNAL, enabled);
// @endif
},
toggleThirdParty: (enabled: boolean): void => {
// Always collect analytics on lbry.tv
// @if TARGET='app'
thirdPartyAnalyticsEnabled = enabled;
window.localStorage.setItem(SHARE_THIRD_PARTY, enabled);
// @endif
},
apiLogView: (uri, outpoint, claimId, timeToStart) => {
return new Promise((resolve, reject) => {
if (analyticsEnabled && (isProduction || devInternalApis)) {
if (internalAnalyticsEnabled && (isProduction || devInternalApis)) {
const params: {
uri: string,
outpoint: string,
@ -125,12 +144,12 @@ const analytics: Analytics = {
});
},
apiLogSearch: () => {
if (analyticsEnabled && isProduction) {
if (internalAnalyticsEnabled && isProduction) {
Lbryio.call('event', 'search');
}
},
apiLogPublish: (claimResult: ChannelClaim | StreamClaim) => {
if (analyticsEnabled && isProduction) {
if (internalAnalyticsEnabled && isProduction) {
const { permanent_url: uri, claim_id: claimId, txid, nout, signing_channel: signingChannel } = claimResult;
let channelClaimId;
if (signingChannel) {
@ -185,7 +204,7 @@ const analytics: Analytics = {
};
function sendGaEvent(category, action, label, value) {
if (analyticsEnabled && isProduction) {
if (thirdPartyAnalyticsEnabled && isProduction) {
ReactGA.event(
{
category,
@ -199,7 +218,7 @@ function sendGaEvent(category, action, label, value) {
}
function sendGaTimingEvent(category: string, action: string, timeInMs: number, label?: string) {
if (analyticsEnabled && isProduction) {
if (thirdPartyAnalyticsEnabled && isProduction) {
ReactGA.timing(
{
category,

View file

@ -13,18 +13,19 @@ type Props = {
isUpgradeAvailable: boolean,
authPage: boolean,
authenticated: boolean,
noHeader: boolean,
};
function Page(props: Props) {
const { children, className, authPage = false, authenticated } = props;
const { children, className, authPage = false, authenticated, noHeader } = props;
const obscureSideNavigation = IS_WEB ? !authenticated : false;
return (
<Fragment>
<Header authHeader={authPage} />
{!noHeader && <Header authHeader={authPage} />}
<div className={classnames('main-wrapper__inner')}>
<main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main>
{!authPage && <SideNavigation obscureSideNavigation={obscureSideNavigation} />}
{!authPage && !noHeader && <SideNavigation obscureSideNavigation={obscureSideNavigation} />}
</div>
</Fragment>
);

View file

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import { doSetDaemonSetting } from 'redux/actions/settings';
import { doSetWelcomeVersion, doToggle3PAnalytics } from 'redux/actions/app';
import PrivacyAgreement from './view';
import { DAEMON_SETTINGS } from 'lbry-redux';
import { WELCOME_VERSION } from 'config.js';
const perform = dispatch => ({
setWelcomeVersion: version => dispatch(doSetWelcomeVersion(version || WELCOME_VERSION)),
setShareDataInternal: share => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, share)),
setShareDataThirdParty: share => dispatch(doToggle3PAnalytics(share)),
});
export default connect(
null,
perform
)(PrivacyAgreement);

View file

@ -0,0 +1,148 @@
// @flow
import React, { useState } from 'react';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
import { FormField } from 'component/common/form-components/form-field';
import { Form } from 'component/common/form-components/form';
import { withRouter } from 'react-router-dom';
// $FlowFixMe cannot resolve ...
import image from 'static/img/unlocklbry.svg';
const FREE = 'free';
const LIMITED = 'limited';
const NONE = 'none';
type Props = {
setWelcomeVersion: () => void,
setShareDataInternal: boolean => void,
setShareDataThirdParty: boolean => void,
history: { replace: string => void },
};
function PrivacyAgreement(props: Props) {
const { setWelcomeVersion, setShareDataInternal, setShareDataThirdParty, history } = props;
const [share, setShare] = useState(undefined); // preload
const [agree, setAgree] = useState(false); // preload
function handleSubmit() {
if (share === FREE) {
setShareDataInternal(true);
setShareDataThirdParty(true);
} else if (share === LIMITED) {
setShareDataInternal(true);
setShareDataThirdParty(false);
} else {
setShareDataInternal(false);
setShareDataThirdParty(false);
}
setWelcomeVersion();
history.replace(`/`);
}
return (
<section className="main--contained">
<div className={'columns'}>
<div>
<h1 className="section__title--large">{__('Welcome')}</h1>
<h3 className="section__subtitle">
{__(
`LBRY takes privacy and choice seriously. Just a few questions before you enter the land of content freedom. `
)}
</h3>
</div>
<div>
<img src={image} />
</div>
</div>
<Form onSubmit={handleSubmit} className="section__body">
<p className="section__subtitle">
{__('Can this app send information about your usage to inform publishers and improve the software?')}
</p>
<fieldset>
<FormField
name={'shareFreely'}
type="radio"
label={
<>
<span className="emoji">😄</span> {__('Yes, including with third-party analytics platforms')}
</>
}
helper={__(`Sending information to third parties (e.g. Google Analytics or Mixpanel) allows us to use detailed
analytical reports to improve all aspects of LBRY.`)}
checked={share === FREE}
onChange={e => setShare(FREE)}
/>
<FormField
name={'shareWithLBRY'}
type="radio"
checked={share === LIMITED}
label={
<>
<span className="emoji">🙂</span> {__('Yes, but only with LBRY, Inc.')}
</>
}
helper={__(
`Sharing information with LBRY, Inc. allows us to report to publishers how their content is doing, as
well as track basic usage and performance. This is the minimum required to earn rewards from LBRY, Inc.`
)}
onChange={e => setShare(LIMITED)}
/>
<FormField
name={'shareNot'}
type="radio"
checked={share === NONE}
label={
<>
<span className="emoji">😢</span> {__('No')}
</>
}
helper={__(`No information will be sent directly to LBRY, Inc. or third-parties about your usage. Note that as
peer-to-peer software, your IP address and potentially other system information can be sent to other
users, though this information is not stored permanently.`)}
onChange={e => setShare(NONE)}
/>
</fieldset>
<p className="section__subtitle">
<I18nMessage
tokens={{
terms: <Button button="link" href="https://www.lbry.com/termsofservice" label={__('Terms of Service')} />,
}}
>
Do you agree to the %terms%?
</I18nMessage>
</p>
<fieldset>
<FormField
name={'agreeButton'}
type="radio"
label={'Yes'}
checked={agree === true}
onChange={e => setAgree(e.target.checked)}
/>
<FormField
name={'disagreeButton'}
type="radio"
checked={agree === false}
label={__('No')}
onChange={e => setAgree(!e.target.checked)}
/>
</fieldset>
{share === NONE && (
<>
<p className="help">
{__(
'While we respect the desire for maximally private usage, please note that choosing this option hurts the ability for creators to understand how their content is performing.'
)}
</p>
</>
)}
<div className={'card__actions'}>
<Button button="primary" label={__(`Let's go`)} disabled={!share || !agree} type="submit" />
</div>
</Form>
</section>
);
}
export default withRouter(PrivacyAgreement);

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectScrollStartingPosition } from 'redux/selectors/app';
import { selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app';
import Router from './view';
import { normalizeURI, makeSelectTitleForUri } from 'lbry-redux';
@ -26,6 +26,7 @@ const select = state => {
title: makeSelectTitleForUri(uri)(state),
currentScroll: selectScrollStartingPosition(state),
isAuthenticated: selectUserVerifiedEmail(state),
welcomeVersion: selectWelcomeVersion(state),
};
};

View file

@ -34,7 +34,8 @@ import ChannelsPage from 'page/channels';
import EmbedWrapperPage from 'page/embedWrapper';
import TopPage from 'page/top';
import { parseURI } from 'lbry-redux';
import { SITE_TITLE } from 'config';
import { SITE_TITLE, WELCOME_VERSION } from 'config';
import Welcome from 'page/welcome';
// Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) {
@ -80,6 +81,7 @@ type Props = {
},
uri: string,
title: string,
welcomeVersion: number,
};
function AppRouter(props: Props) {
@ -90,6 +92,7 @@ function AppRouter(props: Props) {
history,
uri,
title,
welcomeVersion,
} = props;
const { entries } = history;
const entryIndex = history.index;
@ -133,11 +136,14 @@ function AppRouter(props: Props) {
return (
<Switch>
{/* @if TARGET='app' */}
{welcomeVersion < WELCOME_VERSION && <Route path="/*" component={Welcome} />}
{/* @endif */}
<Redirect from={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`} to={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`} />
<Route path={`/`} exact component={HomePage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
<Route

View file

@ -20,6 +20,8 @@ export const HIDE_MODAL = 'HIDE_MODAL';
export const CHANGE_MODALS_ALLOWED = 'CHANGE_MODALS_ALLOWED';
export const TOGGLE_SEARCH_EXPANDED = 'TOGGLE_SEARCH_EXPANDED';
export const PASSWORD_SAVED = 'PASSWORD_SAVED';
export const SET_WELCOME_VERSION = 'SET_WELCOME_VERSION';
export const SET_ALLOW_ANALYTICS = 'SET_ALLOW_ANALYTICS';
// Navigation
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';

View file

@ -30,3 +30,4 @@ exports.BLOCKED = 'blocked';
exports.CHANNELS = 'channels';
exports.EMBED = 'embed';
exports.TOP = 'top';
exports.WELCOME = 'welcome';

View file

@ -13,7 +13,7 @@ import * as MODALS from 'constants/modal_types';
import React, { Fragment, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app';
import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal, doToggle3PAnalytics } from 'redux/actions/app';
import { Lbry, doToast, isURIValid, setSearchApi, apiCall } from 'lbry-redux';
import { doSetLanguage, doUpdateIsNightAsync } from 'redux/actions/settings';
import {
@ -277,6 +277,12 @@ function AppWrapper() {
// @endif
}, []);
useEffect(() => {
if (persistDone) {
app.store.dispatch(doToggle3PAnalytics());
}
}, [persistDone]);
useEffect(() => {
if (readyToLaunch && persistDone) {
app.store.dispatch(doUpdateIsNightAsync());

View file

@ -1,15 +1,22 @@
import { connect } from 'react-redux';
import * as SETTINGS from 'constants/settings';
import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet, doNotifyForgetPassword } from 'redux/actions/app';
import {
doClearCache,
doNotifyEncryptWallet,
doNotifyDecryptWallet,
doNotifyForgetPassword,
doToggle3PAnalytics,
} from 'redux/actions/app';
import { selectAllowAnalytics } from 'redux/selectors/app';
import { doSetDaemonSetting, doSetClientSetting, doSetDarkTime } from 'redux/actions/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import { makeSelectClientSetting, selectDaemonSettings, selectosNotificationsEnabled } from 'redux/selectors/settings';
import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount } from 'lbry-redux';
import { doWalletStatus, selectWalletIsEncrypted, selectBlockedChannelsCount, SETTINGS } from 'lbry-redux';
import SettingsPage from './view';
import { selectUserVerifiedEmail } from 'lbryinc';
const select = state => ({
daemonSettings: selectDaemonSettings(state),
allowAnalytics: selectAllowAnalytics(state),
isAuthenticated: selectUserVerifiedEmail(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
instantPurchaseEnabled: makeSelectClientSetting(SETTINGS.INSTANT_PURCHASE_ENABLED)(state),
@ -30,6 +37,7 @@ const select = state => ({
const perform = dispatch => ({
setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)),
toggle3PAnalytics: allow => dispatch(doToggle3PAnalytics(allow)),
clearCache: () => dispatch(doClearCache()),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
encryptWallet: () => dispatch(doNotifyEncryptWallet()),

View file

@ -2,7 +2,6 @@
/* eslint react/no-unescaped-entities:0 */
/* eslint react/jsx-no-comment-textnodes:0 */
import * as SETTINGS from 'constants/settings';
import * as PAGES from 'constants/pages';
import * as React from 'react';
@ -15,6 +14,7 @@ import SettingWalletServer from 'component/settingWalletServer';
import SettingAutoLaunch from 'component/settingAutoLaunch';
import FileSelector from 'component/common/file-selector';
import SyncToggle from 'component/syncToggle';
import { SETTINGS } from 'lbry-redux';
import Card from 'component/common/card';
import { getKeychainPassword } from 'util/saved-passwords';
@ -51,8 +51,10 @@ type DaemonSettings = {
type Props = {
setDaemonSetting: (string, ?SetDaemonSettingArg) => void,
setClientSetting: (string, SetDaemonSettingArg) => void,
toggle3PAnalytics: boolean => void,
clearCache: () => Promise<any>,
daemonSettings: DaemonSettings,
allowAnalytics: boolean,
showNsfw: boolean,
isAuthenticated: boolean,
instantPurchaseEnabled: boolean,
@ -187,6 +189,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
render() {
const {
daemonSettings,
allowAnalytics,
showNsfw,
instantPurchaseEnabled,
instantPurchaseMax,
@ -200,6 +203,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
// autoDownload,
setDaemonSetting,
setClientSetting,
toggle3PAnalytics,
supportOption,
hideBalance,
userBlockedChannelsCount,
@ -445,21 +449,39 @@ class SettingsPage extends React.PureComponent<Props, State> {
}
/>
<Card
title={__('Share Diagnostic Data')}
actions={
<FormField
type="checkbox"
name="share_usage_data"
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
checked={daemonSettings.share_usage_data}
label={
title={__('Share Usage and Diagnostic Data')}
subtitle={
<React.Fragment>
{__('Help make LBRY better by contributing analytics and diagnostic data about my usage.')}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/privacypolicy" />.
{__(
`This is information like error logging, performance tracking, and usage statistics. It includes your IP address and basic system details, but no other identifying information (unless you sign in to lbry.tv)`
)}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/privacypolicy" />
</React.Fragment>
}
helper={__('You will be ineligible to earn rewards while diagnostics are not being shared.')}
actions={
<>
<FormField
type="checkbox"
name="share_internal"
onChange={() => setDaemonSetting('share_usage_data', !daemonSettings.share_usage_data)}
checked={daemonSettings.share_usage_data}
label={<React.Fragment>{__('Allow the app to share data to LBRY.inc')}</React.Fragment>}
helper={
isAuthenticated
? __('Internal sharing is required while signed in.')
: __('Internal sharing is required to participate in rewards programs.')
}
disabled={isAuthenticated}
/>
<FormField
type="checkbox"
name="share_third_party"
onChange={e => toggle3PAnalytics(e.target.checked)}
checked={allowAnalytics}
label={__('Allow the App to access third party analytics platforms')}
helper={__('We use detailed analytics to improve all aspects of the LBRY experience.')}
/>
</>
}
/>
{/* @endif */}

10
ui/page/welcome/index.js Normal file
View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import Welcome from './view';
const select = () => ({});
const perform = () => ({});
export default connect(
select,
perform
)(Welcome);

12
ui/page/welcome/view.jsx Normal file
View file

@ -0,0 +1,12 @@
// @flow
import React from 'react';
import PrivacyAgreement from 'component/privacyAgreement';
import Page from 'component/page';
export default function Welcome() {
return (
<Page noHeader className="main--auth-page">
<PrivacyAgreement />
</Page>
);
}

View file

@ -21,7 +21,7 @@ import {
doClearSupport,
} from 'lbry-redux';
import Native from 'native';
import { doFetchDaemonSettings, doSetAutoLaunch } from 'redux/actions/settings';
import { doFetchDaemonSettings, doSetAutoLaunch, doSetDaemonSetting } from 'redux/actions/settings';
import {
selectIsUpgradeSkipped,
selectUpdateUrl,
@ -32,6 +32,7 @@ import {
selectRemoteVersion,
selectUpgradeTimer,
selectModal,
selectAllowAnalytics,
} from 'redux/selectors/app';
import { doAuthenticate, doGetSync } from 'lbryinc';
import { lbrySettings as config, version as appVersion } from 'package.json';
@ -450,11 +451,47 @@ export function doSignOut() {
};
}
export function doSetWelcomeVersion(version) {
return {
type: ACTIONS.SET_WELCOME_VERSION,
data: version,
};
}
export function doToggle3PAnalytics(allowParam, doNotDispatch) {
return (dispatch, getState) => {
const state = getState();
const allowState = selectAllowAnalytics(state);
const allow = allowParam !== undefined ? allowParam : allowState;
analytics.toggleThirdParty(allow);
if (!doNotDispatch) {
return dispatch({
type: ACTIONS.SET_ALLOW_ANALYTICS,
data: allow,
});
}
};
}
export function doGetAndPopulatePreferences() {
return dispatch => {
return (dispatch, getState) => {
function successCb(savedPreferences) {
const state = getState();
const { daemonSettings } = state;
if (savedPreferences !== null) {
dispatch(doPopulateSharedUserState(savedPreferences));
// @if TARGET='app'
const { settings, sharing_3P: sharing3P } = savedPreferences.value;
Object.entries(settings).forEach(([key, val]) => {
if (daemonSettings[key] !== val) {
dispatch(doSetDaemonSetting(key, val, false));
}
});
if (sharing3P !== undefined) {
doToggle3PAnalytics(sharing3P, true);
}
// @endif
}
}

View file

@ -1,5 +1,4 @@
import { Lbry, ACTIONS, doToast, SHARED_PREFERENCES, doWalletReconnect } from 'lbry-redux';
import * as SETTINGS from 'constants/settings';
import { Lbry, ACTIONS, doToast, SHARED_PREFERENCES, doWalletReconnect, SETTINGS } from 'lbry-redux';
import * as LOCAL_ACTIONS from 'constants/action_types';
import analytics from 'analytics';
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
@ -12,7 +11,7 @@ const UPDATE_IS_NIGHT_INTERVAL = 5 * 60 * 1000;
export function doFetchDaemonSettings() {
return dispatch => {
Lbry.settings_get().then(settings => {
analytics.toggle(settings.share_usage_data);
analytics.toggleInternal(settings.share_usage_data);
dispatch({
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
data: {
@ -33,10 +32,9 @@ export function doGetDaemonStatus() {
},
});
return status;
},
);
});
};
};
}
export function doClearDaemonSetting(key) {
return dispatch => {
@ -55,7 +53,7 @@ export function doClearDaemonSetting(key) {
}
});
Lbry.settings_get().then(settings => {
analytics.toggle(settings.share_usage_data);
analytics.toggleInternal(settings.share_usage_data);
dispatch({
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
data: {
@ -65,27 +63,28 @@ export function doClearDaemonSetting(key) {
});
};
}
export function doSetDaemonSetting(key, value) {
// if doPopulate is applying settings, we don't want to cause a loop; doNotDispatch = true.
export function doSetDaemonSetting(key, value, doNotDispatch = false) {
return dispatch => {
const newSettings = {
key,
value: !value && value !== false ? null : value,
};
Lbry.settings_set(newSettings).then(newSetting => {
if (Object.values(SHARED_PREFERENCES).includes(key)) {
if (Object.values(SHARED_PREFERENCES).includes(key) && !doNotDispatch) {
dispatch({
type: ACTIONS.SHARED_PREFERENCE_SET,
data: {key: key, value: newSetting[key]},
data: { key: key, value: newSetting[key] },
});
}
// hardcoding this in lieu of a better solution
if (key === SHARED_PREFERENCES.WALLET_SERVERS) {
dispatch(doWalletReconnect());
// todo: add sdk reloadsettings() (or it happens automagically?)
}
});
Lbry.settings_get().then(settings => {
analytics.toggle(settings.share_usage_data);
analytics.toggleInternal(settings.share_usage_data);
dispatch({
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
data: {
@ -104,12 +103,14 @@ export function doSaveCustomWalletServers(servers) {
}
export function doSetClientSetting(key, value) {
return {
return dispatch => {
dispatch({
type: ACTIONS.CLIENT_SETTING_CHANGED,
data: {
key,
value,
},
});
};
}

View file

@ -1,6 +1,7 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { remote } from 'electron';
// @if TARGET='app'
@ -38,6 +39,8 @@ export type AppState = {
enhancedLayout: boolean,
searchOptionsExpanded: boolean,
isPasswordSaved: boolean,
welcomeVersion: number,
allowAnalytics: boolean,
};
const defaultState: AppState = {
@ -69,6 +72,8 @@ const defaultState: AppState = {
currentScroll: 0,
scrollHistory: [0],
isPasswordSaved: false,
welcomeVersion: 0.0,
allowAnalytics: false,
};
// @@router comes from react-router
@ -250,6 +255,16 @@ reducers[ACTIONS.SHOW_MODAL] = (state, action) =>
modalProps: action.data.modalProps,
});
reducers[ACTIONS.SET_WELCOME_VERSION] = (state, action) =>
Object.assign({}, state, {
welcomeVersion: action.data,
});
reducers[ACTIONS.SET_ALLOW_ANALYTICS] = (state, action) =>
Object.assign({}, state, {
allowAnalytics: action.data,
});
reducers[ACTIONS.HIDE_MODAL] = state =>
Object.assign({}, state, {
modal: null,
@ -261,6 +276,15 @@ reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state =>
searchOptionsExpanded: !state.searchOptionsExpanded,
});
reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => {
const { welcomeVersion, allowAnalytics } = action.data;
return {
...state,
...(welcomeVersion !== undefined ? { welcomeVersion } : {}),
...(allowAnalytics !== undefined ? { allowAnalytics } : {}),
};
};
export default function reducer(state: AppState = defaultState, action: any) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -1,9 +1,8 @@
import * as ACTIONS from 'constants/action_types';
import * as SETTINGS from 'constants/settings';
import * as APP_SETTINGS from 'constants/settings';
import moment from 'moment';
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
import { ACTIONS as LBRY_REDUX_ACTIONS, SHARED_PREFERENCES } from 'lbry-redux';
import { ACTIONS as LBRY_REDUX_ACTIONS, SHARED_PREFERENCES, SETTINGS } from 'lbry-redux';
const reducers = {};
let settingLanguage = [];
try {
@ -38,7 +37,7 @@ const defaultState = {
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: true,
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false,
[SETTINGS.DARK_MODE_TIMES]: {
[APP_SETTINGS.DARK_MODE_TIMES]: {
from: { hour: '21', min: '00', formattedTime: '21:00' },
to: { hour: '8', min: '00', formattedTime: '8:00' },
},
@ -83,7 +82,7 @@ reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
};
reducers[ACTIONS.UPDATE_IS_NIGHT] = state => {
const { from, to } = state.clientSettings[SETTINGS.DARK_MODE_TIMES];
const { from, to } = state.clientSettings[APP_SETTINGS.DARK_MODE_TIMES];
const momentNow = moment();
const startNightMoment = moment(from.formattedTime, 'HH:mm');
const endNightMoment = moment(to.formattedTime, 'HH:mm');
@ -130,8 +129,8 @@ reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => {
const { settings: sharedPreferences } = action.data;
// todo: populate sharedPreferences that match client settings constants
// process clientSettings and daemonSettings
return Object.assign({}, state, { sharedPreferences });
};

View file

@ -129,6 +129,16 @@ export const selectSearchOptionsExpanded = createSelector(
state => state.searchOptionsExpanded
);
export const selectWelcomeVersion = createSelector(
selectState,
state => state.welcomeVersion
);
export const selectAllowAnalytics = createSelector(
selectState,
state => state.allowAnalytics
);
export const selectScrollStartingPosition = createSelector(
selectState,
state => state.currentScroll

View file

@ -1,5 +1,4 @@
import * as ACTIONS from 'constants/action_types';
import * as SETTINGS from 'constants/settings';
import { persistStore, persistReducer } from 'redux-persist';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import createCompressor from 'redux-persist-transform-compress';
@ -10,7 +9,7 @@ import thunk from 'redux-thunk';
import { createMemoryHistory, createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import createRootReducer from './reducers';
import { buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS, SETTINGS } from 'lbry-redux';
import { doGetSync, selectUserVerifiedEmail } from 'lbryinc';
import { getSavedPassword } from 'util/saved-passwords';
import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -50,7 +49,14 @@ const fileInfoFilter = createFilter('fileInfo', [
'fileListDownloadedSort',
'fileListSubscriptionSort',
]);
const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpanded', 'volume', 'muted']);
const appFilter = createFilter('app', [
'hasClickedComment',
'searchOptionsExpanded',
'volume',
'muted',
'allowAnalytics',
'welcomeVersion',
]);
// We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']);
const searchFilter = createFilter('search', ['options']);
@ -113,6 +119,8 @@ const triggerSharedStateActions = [
LBRY_REDUX_ACTIONS.TOGGLE_BLOCK_CHANNEL,
LBRY_REDUX_ACTIONS.CREATE_CHANNEL_COMPLETED,
LBRY_REDUX_ACTIONS.SHARED_PREFERENCE_SET,
ACTIONS.SET_WELCOME_VERSION,
ACTIONS.SET_ALLOW_ANALYTICS,
];
/**
@ -135,6 +143,8 @@ const sharedStateFilters = {
},
blocked: { source: 'blocked', property: 'blockedChannels' },
settings: { source: 'settings', property: 'sharedPreferences' },
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
sharing_3P: { source: 'app', property: 'allowAnalytics' },
};
const sharedStateCb = ({ dispatch, getState }) => {

View file

@ -7174,9 +7174,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#3d64f8acc6c2ce37252f59feff89e1fc58cb74c1:
lbry-redux@lbryio/lbry-redux#f22bdbfe2f403d13fa8ce99e8849dca0e3d19bef:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/3d64f8acc6c2ce37252f59feff89e1fc58cb74c1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/f22bdbfe2f403d13fa8ce99e8849dca0e3d19bef"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"