diff --git a/package.json b/package.json index 7a2949a3d..cd75d4a03 100644 --- a/package.json +++ b/package.json @@ -136,8 +136,8 @@ "imagesloaded": "^4.1.4", "json-loader": "^0.5.4", "lbry-format": "https://github.com/lbryio/lbry-format.git", - "lbry-redux": "lbryio/lbry-redux#210bb80f2c49f6a166a8adc602a68b7490d5d23a", - "lbryinc": "lbryio/lbryinc#cff5dd60934c4c6080e135f47ebbece1548c658c", + "lbry-redux": "lbryio/lbry-redux#760623f99c90407ca3f1d06f1cdd2506fb2e443a", + "lbryinc": "lbryio/lbryinc#35df87d1e69e435e25fc12832b6b1b788f76baaa", "lint-staged": "^7.0.2", "localforage": "^1.7.1", "lodash-es": "^4.17.14", diff --git a/ui/component/app/index.js b/ui/component/app/index.js index 98871dbb0..32ea986f7 100644 --- a/ui/component/app/index.js +++ b/ui/component/app/index.js @@ -5,13 +5,18 @@ import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user'; import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUnclaimedRewards } from 'redux/selectors/rewards'; import { doFetchChannelListMine, SETTINGS } from 'lbry-redux'; -import { makeSelectClientSetting, selectLoadedLanguages, selectThemePath } from 'redux/selectors/settings'; +import { + makeSelectClientSetting, + selectLoadedLanguages, + selectSyncSigninPref, + selectThemePath, +} from 'redux/selectors/settings'; import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app'; -import { doSetLanguage } from 'redux/actions/settings'; +import { doSetLanguage, doUpdateSyncPref } from 'redux/actions/settings'; +import { doSyncSubscribe } from 'redux/actions/syncwrapper'; import { doDownloadUpgradeRequested, doSignIn, - doSyncWithPreferences, doGetAndPopulatePreferences, doAnalyticsTagSync, } from 'redux/actions/app'; @@ -22,14 +27,15 @@ const select = state => ({ accessToken: selectAccessToken(state), theme: selectThemePath(state), language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), + syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state), languages: selectLoadedLanguages(state), autoUpdateDownloaded: selectAutoUpdateDownloaded(state), isUpgradeAvailable: selectIsUpgradeAvailable(state), - syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state), syncError: selectGetSyncErrorMessage(state), uploadCount: selectUploadCount(state), rewards: selectUnclaimedRewards(state), isAuthenticated: selectUserVerifiedEmail(state), + signInSyncPref: selectSyncSigninPref(state), }); const perform = dispatch => ({ @@ -39,8 +45,9 @@ const perform = dispatch => ({ setLanguage: language => dispatch(doSetLanguage(language)), signIn: () => dispatch(doSignIn()), requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()), - checkSync: () => dispatch(doSyncWithPreferences()), updatePreferences: () => dispatch(doGetAndPopulatePreferences()), + updateSyncPref: () => dispatch(doUpdateSyncPref()), + syncSubscribe: () => dispatch(doSyncSubscribe()), setReferrer: (referrer, doClaim) => dispatch(doUserSetReferrer(referrer, doClaim)), }); diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index 0a5a1fd95..0f6de49a3 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -38,7 +38,6 @@ export const MAIN_WRAPPER_CLASS = 'main-wrapper'; // @if TARGET='app' export const IS_MAC = process.platform === 'darwin'; // @endif -const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes // button numbers pulled from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button const MOUSE_BACK_BTN = 3; @@ -67,17 +66,20 @@ type Props = { setLanguage: string => void, isUpgradeAvailable: boolean, autoUpdateDownloaded: boolean, - checkSync: () => void, - updatePreferences: () => void, - syncEnabled: boolean, + updatePreferences: () => Promise, + updateSyncPref: () => void, uploadCount: number, balance: ?number, syncError: ?string, + syncEnabled: boolean, rewards: Array, setReferrer: (string, boolean) => void, analyticsTagSync: () => void, isAuthenticated: boolean, socketConnect: () => void, + syncSubscribe: () => void, + syncEnabled: boolean, + signInSyncPref: boolean, }; function App(props: Props) { @@ -90,8 +92,6 @@ function App(props: Props) { autoUpdateDownloaded, isUpgradeAvailable, requestDownloadUpgrade, - syncEnabled, - checkSync, uploadCount, history, syncError, @@ -99,15 +99,19 @@ function App(props: Props) { languages, setLanguage, updatePreferences, + updateSyncPref, rewards, setReferrer, - analyticsTagSync, isAuthenticated, + syncSubscribe, + signInSyncPref, } = props; const appRef = useRef(); const isEnhancedLayout = useKonamiListener(); const [hasSignedIn, setHasSignedIn] = useState(false); + const [readyForSync, setReadyForSync] = useState(false); + const [readyForPrefs, setReadyForPrefs] = useState(false); const hasVerifiedEmail = user && user.has_verified_email; const isRewardApproved = user && user.is_reward_approved; const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail); @@ -229,35 +233,40 @@ function App(props: Props) { } }, [previousRewardApproved, isRewardApproved]); - // Keep this at the end to ensure initial setup effects are run first - useEffect(() => { - if (!hasSignedIn && hasVerifiedEmail) { - signIn(); - setHasSignedIn(true); - } - }, [hasVerifiedEmail, signIn, hasSignedIn]); - // @if TARGET='app' useEffect(() => { - updatePreferences(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (updatePreferences && readyForPrefs) { + updatePreferences().then(() => { + setReadyForSync(true); + }); + } + }, [updatePreferences, setReadyForSync, readyForPrefs, hasVerifiedEmail]); // @endif + // ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too. useEffect(() => { - if (hasVerifiedEmail && syncEnabled) { - checkSync(); - analyticsTagSync(); - let syncInterval = setInterval(() => { - checkSync(); - }, SYNC_INTERVAL); - - return () => { - clearInterval(syncInterval); - }; + if (readyForSync && hasVerifiedEmail) { + // Copy sync checkbox to settings and push to preferences + // before sync if false, after sync if true so as not to change timestamp. + if (signInSyncPref === false) { + updateSyncPref(); + } + syncSubscribe(); + if (signInSyncPref === true) { + updateSyncPref(); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasVerifiedEmail, syncEnabled, checkSync]); + }, [readyForSync, hasVerifiedEmail, signInSyncPref, updateSyncPref, syncSubscribe]); + + // We know someone is logging in or not when we get their user object {} + // We'll use this to determine when it's time to pull preferences + // This will no longer work if desktop users no longer get a user object from lbryinc + useEffect(() => { + if (user) { + setReadyForPrefs(true); + } + }, [user, setReadyForPrefs]); useEffect(() => { if (syncError && isAuthenticated) { @@ -266,6 +275,15 @@ function App(props: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [syncError, pathname, isAuthenticated]); + // Keep this at the end to ensure initial setup effects are run first + useEffect(() => { + if (!hasSignedIn && hasVerifiedEmail) { + signIn(); + setHasSignedIn(true); + if (IS_WEB) setReadyForSync(true); + } + }, [hasVerifiedEmail, signIn, hasSignedIn]); + // @if TARGET='web' useDegradedPerformance(setLbryTvApiStatus); // @endif diff --git a/ui/component/header/index.js b/ui/component/header/index.js index ad0ffa13b..4da1e5304 100644 --- a/ui/component/header/index.js +++ b/ui/component/header/index.js @@ -4,7 +4,7 @@ import { selectBalance, formatCredits, SETTINGS } from 'lbry-redux'; import { selectGetSyncErrorMessage } from 'lbryinc'; import { selectUserVerifiedEmail, selectUserEmail, selectEmailToVerify } from 'redux/selectors/user'; import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user'; -import { doSetClientSetting, doSyncClientSettings } from 'redux/actions/settings'; +import { doSetClientSetting } from 'redux/actions/settings'; import { doSignOut, doOpenModal } from 'redux/actions/app'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import Header from './view'; @@ -25,8 +25,7 @@ const select = state => ({ }); const perform = dispatch => ({ - setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), - syncSettings: () => dispatch(doSyncClientSettings()), + setClientSetting: (key, value, push) => dispatch(doSetClientSetting(key, value, push)), signOut: () => dispatch(doSignOut()), openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)), openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)), diff --git a/ui/component/header/view.jsx b/ui/component/header/view.jsx index aab8d04cb..82938a3cc 100644 --- a/ui/component/header/view.jsx +++ b/ui/component/header/view.jsx @@ -37,7 +37,7 @@ type Props = { }, currentTheme: string, automaticDarkModeEnabled: boolean, - setClientSetting: (string, boolean | string) => void, + setClientSetting: (string, boolean | string, ?boolean) => void, hideBalance: boolean, email: ?string, authenticated: boolean, @@ -56,7 +56,6 @@ type Props = { clearEmailEntry: () => void, clearPasswordEntry: () => void, hasNavigated: boolean, - syncSettings: () => void, sidebarOpen: boolean, setSidebarOpen: boolean => void, isAbsoluteSideNavHidden: boolean, @@ -80,7 +79,6 @@ const Header = (props: Props) => { clearPasswordEntry, emailToVerify, backout, - syncSettings, sidebarOpen, setSidebarOpen, isAbsoluteSideNavHidden, @@ -149,11 +147,10 @@ const Header = (props: Props) => { } if (currentTheme === 'dark') { - setClientSetting(SETTINGS.THEME, 'light'); + setClientSetting(SETTINGS.THEME, 'light', true); } else { - setClientSetting(SETTINGS.THEME, 'dark'); + setClientSetting(SETTINGS.THEME, 'dark', true); } - syncSettings(); } function getWalletTitle() { diff --git a/ui/component/publishAdditionalOptions/index.js b/ui/component/publishAdditionalOptions/index.js index 98ec5272f..43bfaaf30 100644 --- a/ui/component/publishAdditionalOptions/index.js +++ b/ui/component/publishAdditionalOptions/index.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux'; import { selectPublishFormValues, doUpdatePublishForm } from 'lbry-redux'; import PublishPage from './view'; -import { selectUser, selectAccessToken } from '../../redux/selectors/user'; -import {doFetchAccessToken} from '../../redux/actions/user'; +import { selectUser, selectAccessToken } from 'redux/selectors/user'; +import { doFetchAccessToken } from 'redux/actions/user'; const select = state => ({ ...selectPublishFormValues(state), diff --git a/ui/component/router/index.js b/ui/component/router/index.js index 4fe62a966..15d18e16f 100644 --- a/ui/component/router/index.js +++ b/ui/component/router/index.js @@ -3,8 +3,7 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectHasNavigated, selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app'; import Router from './view'; import { normalizeURI, makeSelectTitleForUri } from 'lbry-redux'; -import { doSetHasNavigated, doSyncWithPreferences } from 'redux/actions/app'; -import { doSyncClientSettings } from 'redux/actions/settings'; +import { doSetHasNavigated } from 'redux/actions/app'; const select = state => { const { pathname, hash } = state.router.location; const urlPath = pathname + hash; @@ -34,8 +33,6 @@ const select = state => { const perform = dispatch => ({ setHasNavigated: () => dispatch(doSetHasNavigated()), - syncSettings: () => dispatch(doSyncClientSettings()), - checkSync: () => dispatch(doSyncWithPreferences()), }); export default connect(select, perform)(Router); diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index 08a5f20bc..3b02421cf 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -97,8 +97,6 @@ type Props = { welcomeVersion: number, hasNavigated: boolean, setHasNavigated: () => void, - syncSettings: () => void, - checkSync: () => void, }; function AppRouter(props: Props) { @@ -112,14 +110,11 @@ function AppRouter(props: Props) { welcomeVersion, hasNavigated, setHasNavigated, - syncSettings, - checkSync, } = props; const { entries } = history; const entryIndex = history.index; const urlParams = new URLSearchParams(search); const resetScroll = urlParams.get('reset_scroll'); - const [prevPath, setPrevPath] = React.useState(pathname); // For people arriving at settings page from deeplinks, know whether they can "go back" useEffect(() => { @@ -131,27 +126,6 @@ function AppRouter(props: Props) { return unlisten; }, [hasNavigated, setHasNavigated]); - // Sync when no longer on a settings page, or when entering settings pages - useEffect(() => { - const unlisten = history.listen(location => { - if (!location.pathname.includes(PAGES.SETTINGS) && prevPath.includes(PAGES.SETTINGS)) { - syncSettings(); - } else if (location.pathname.includes(PAGES.SETTINGS) && !prevPath.includes(PAGES.SETTINGS)) { - checkSync(); - } - }); - return unlisten; - }, [prevPath, syncSettings, checkSync]); - - useEffect(() => { - const unlisten = history.listen(location => { - if (prevPath !== location.pathname && setPrevPath) { - setPrevPath(location.pathname); - } - }); - return unlisten; - }, [prevPath, setPrevPath]); - useEffect(() => { if (uri) { const { channelName, streamName } = parseURI(uri); diff --git a/ui/component/syncPassword/index.js b/ui/component/syncPassword/index.js index a7bf5f4b3..eebc17e0c 100644 --- a/ui/component/syncPassword/index.js +++ b/ui/component/syncPassword/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doGetSync, selectGetSyncIsPending, selectSyncApplyPasswordError } from 'lbryinc'; +import { selectGetSyncIsPending, selectSyncApplyPasswordError } from 'lbryinc'; +import { doGetSyncDesktop } from 'redux/actions/syncwrapper'; import { selectUserEmail } from 'redux/selectors/user'; import { doSetClientSetting } from 'redux/actions/settings'; import { doSignOut, doHandleSyncComplete } from 'redux/actions/app'; @@ -12,7 +13,7 @@ const select = state => ({ }); const perform = dispatch => ({ - getSync: (password, cb) => dispatch(doGetSync(password, cb)), + getSync: (cb, password) => dispatch(doGetSyncDesktop(cb, password)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), handleSyncComplete: (error, hasDataChanged) => dispatch(doHandleSyncComplete(error, hasDataChanged)), signOut: () => dispatch(doSignOut()), diff --git a/ui/component/syncPassword/view.jsx b/ui/component/syncPassword/view.jsx index 462e64585..ef90a6296 100644 --- a/ui/component/syncPassword/view.jsx +++ b/ui/component/syncPassword/view.jsx @@ -8,7 +8,7 @@ import usePersistedState from 'effects/use-persisted-state'; import I18nMessage from 'component/i18nMessage'; type Props = { - getSync: (?string, (any, boolean) => void) => void, + getSync: ((any, boolean) => void, ?string) => void, getSyncIsPending: boolean, email: string, passwordError: boolean, @@ -22,13 +22,13 @@ function SyncPassword(props: Props) { const [rememberPassword, setRememberPassword] = usePersistedState(true); function handleSubmit() { - getSync(password, (error, hasDataChanged) => { + getSync((error, hasDataChanged) => { handleSyncComplete(error, hasDataChanged); if (!error) { setSavedPassword(password, rememberPassword); } - }); + }, password); } return ( diff --git a/ui/component/userEmailNew/index.js b/ui/component/userEmailNew/index.js index 174cec6f7..04e94100b 100644 --- a/ui/component/userEmailNew/index.js +++ b/ui/component/userEmailNew/index.js @@ -7,7 +7,7 @@ import { selectUser, } from 'redux/selectors/user'; import { DAEMON_SETTINGS, SETTINGS } from 'lbry-redux'; -import { doSetClientSetting, doSetDaemonSetting } from 'redux/actions/settings'; +import { doSetSyncPref, doSetDaemonSetting } from 'redux/actions/settings'; import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings'; import UserEmailNew from './view'; @@ -21,7 +21,7 @@ const select = state => ({ }); const perform = dispatch => ({ - setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)), + setSync: value => dispatch(doSetSyncPref(value)), setShareDiagnosticData: shouldShareData => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, shouldShareData)), doSignUp: (email, password) => dispatch(doUserSignUp(email, password)), diff --git a/ui/component/userEmailReturning/index.js b/ui/component/userEmailReturning/index.js index 3ed5c10a6..95b7f3fb9 100644 --- a/ui/component/userEmailReturning/index.js +++ b/ui/component/userEmailReturning/index.js @@ -8,7 +8,7 @@ import { selectEmailNewIsPending, } from 'redux/selectors/user'; import { doUserCheckIfEmailExists, doClearEmailEntry } from 'redux/actions/user'; -import { doSetClientSetting } from 'redux/actions/settings'; +import { doSetSyncPref } from 'redux/actions/settings'; import UserEmailReturning from './view'; const select = state => ({ @@ -23,5 +23,5 @@ const select = state => ({ export default connect(select, { doUserCheckIfEmailExists, doClearEmailEntry, - doSetClientSetting, + doSetSyncPref, })(UserEmailReturning); diff --git a/ui/component/userEmailReturning/view.jsx b/ui/component/userEmailReturning/view.jsx index 505d59de9..2812f9974 100644 --- a/ui/component/userEmailReturning/view.jsx +++ b/ui/component/userEmailReturning/view.jsx @@ -1,5 +1,4 @@ // @flow -import { SETTINGS } from 'lbry-redux'; import * as PAGES from 'constants/pages'; import React, { useState } from 'react'; import { FormField, Form } from 'component/common/form'; @@ -18,7 +17,8 @@ type Props = { doClearEmailEntry: () => void, doUserSignIn: (string, ?string) => void, doUserCheckIfEmailExists: string => void, - doSetClientSetting: (string, boolean) => void, + doSetSyncPref: boolean => void, + doSetClientSetting: (string, boolean, ?boolean) => void, isPending: boolean, }; @@ -30,7 +30,7 @@ function UserEmailReturning(props: Props) { emailToVerify, doClearEmailEntry, emailDoesNotExist, - doSetClientSetting, + doSetSyncPref, isPending, } = props; const { push, location } = useHistory(); @@ -48,7 +48,7 @@ function UserEmailReturning(props: Props) { function handleSubmit() { // @if TARGET='app' - doSetClientSetting(SETTINGS.ENABLE_SYNC, syncEnabled); + doSetSyncPref(syncEnabled); // @endif doUserCheckIfEmailExists(email); } diff --git a/ui/component/userSignIn/view.jsx b/ui/component/userSignIn/view.jsx index e63b91849..6e646019d 100644 --- a/ui/component/userSignIn/view.jsx +++ b/ui/component/userSignIn/view.jsx @@ -7,7 +7,7 @@ import Spinner from 'component/spinner'; type Props = { user: ?User, - history: { push: string => void }, + history: { push: string => void, replace: string => void }, location: { search: string }, userFetchPending: boolean, doUserSignIn: string => void, @@ -28,7 +28,7 @@ function UserSignIn(props: Props) { React.useEffect(() => { if (hasVerifiedEmail || (!showEmail && !showPassword && !showLoading)) { - history.push(redirect || '/'); + history.replace(redirect || '/'); } }, [showEmail, showPassword, showLoading, hasVerifiedEmail]); diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 2a228ae05..5540ae0e9 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -27,6 +27,7 @@ export const PASSWORD_SAVED = 'PASSWORD_SAVED'; export const SET_WELCOME_VERSION = 'SET_WELCOME_VERSION'; export const SET_ALLOW_ANALYTICS = 'SET_ALLOW_ANALYTICS'; export const SET_HAS_NAVIGATED = 'SET_HAS_NAVIGATED'; +export const SET_SYNC_LOCK = 'SET_SYNC_LOCK'; // Navigation export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH'; @@ -132,6 +133,7 @@ export const UPDATE_IS_NIGHT = 'UPDATE_IS_NIGHT'; export const FINDING_FFMPEG_STARTED = 'FINDING_FFMPEG_STARTED'; export const FINDING_FFMPEG_COMPLETED = 'FINDING_FFMPEG_COMPLETED'; export const SYNC_CLIENT_SETTINGS = 'SYNC_CLIENT_SETTINGS'; +export const SYNC_PREFERENCE_CHANGED = 'SYNC_PREFERENCE_CHANGED'; // User export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED'; diff --git a/ui/index.jsx b/ui/index.jsx index b85827b7f..52e072158 100644 --- a/ui/index.jsx +++ b/ui/index.jsx @@ -117,30 +117,10 @@ doAuthTokenRefresh(); // We keep a local variable for authToken because `ipcRenderer.send` does not // contain a response, so there is no way to know when it's been set let authToken; -Lbryio.setOverride( - 'setAuthToken', - status => - new Promise(resolve => { - Lbryio.call( - 'user', - 'new', - { - auth_token: '', - language: DEFAULT_LANGUAGE, - app_id: status.installation_id, - }, - 'post' - ).then(response => { - if (!response.auth_token) { - throw new Error(__('auth_token is missing from response')); - } - - authToken = response.auth_token; - setAuthToken(authToken); - resolve(authToken); - }); - }) -); +Lbryio.setOverride('setAuthToken', authToken => { + setAuthToken(authToken); + return authToken; +}); Lbryio.setOverride( 'getAuthToken', diff --git a/ui/page/settings/index.js b/ui/page/settings/index.js index 37101bd1a..16ee2bbcc 100644 --- a/ui/page/settings/index.js +++ b/ui/page/settings/index.js @@ -6,7 +6,8 @@ import { doClearDaemonSetting, doSetClientSetting, doSetDarkTime, - doSyncClientSettings, + doEnterSettingsPage, + doExitSettingsPage, } from 'redux/actions/settings'; import { doSetPlayingUri } from 'redux/actions/content'; import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings'; @@ -37,7 +38,6 @@ const select = state => ({ const perform = dispatch => ({ setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), - syncSettings: () => dispatch(doSyncClientSettings()), clearDaemonSetting: key => dispatch(doClearDaemonSetting(key)), toggle3PAnalytics: allow => dispatch(doToggle3PAnalytics(allow)), clearCache: () => dispatch(doClearCache()), @@ -47,6 +47,8 @@ const perform = dispatch => ({ clearPlayingUri: () => dispatch(doSetPlayingUri(null)), setDarkTime: (time, options) => dispatch(doSetDarkTime(time, options)), openModal: (id, params) => dispatch(doOpenModal(id, params)), + enterSettings: () => dispatch(doEnterSettingsPage()), + exitSettings: () => dispatch(doExitSettingsPage()), }); export default connect(select, perform)(SettingsPage); diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx index 41e7e85f1..900513b68 100644 --- a/ui/page/settings/view.jsx +++ b/ui/page/settings/view.jsx @@ -4,7 +4,6 @@ import * as MODALS from 'constants/modal_types'; import * as ICONS from 'constants/icons'; import * as React from 'react'; import { SETTINGS } from 'lbry-redux'; - import { FormField } from 'component/common/form'; import Button from 'component/button'; import Page from 'component/page'; @@ -66,6 +65,8 @@ type Props = { openModal: string => void, language?: string, syncEnabled: boolean, + enterSettings: () => void, + exitSettings: () => void, }; type State = { @@ -89,7 +90,7 @@ class SettingsPage extends React.PureComponent { } componentDidMount() { - const { isAuthenticated } = this.props; + const { isAuthenticated, enterSettings } = this.props; if (isAuthenticated || !IS_WEB) { this.props.updateWalletStatus(); @@ -99,6 +100,12 @@ class SettingsPage extends React.PureComponent { } }); } + enterSettings(); + } + + componentWillUnmount() { + const { exitSettings } = this.props; + exitSettings(); } onThemeChange(event: SyntheticInputEvent<*>) { @@ -211,11 +218,11 @@ class SettingsPage extends React.PureComponent { } + actions={} /> {/* @endif */} diff --git a/ui/page/settingsAdvanced/index.js b/ui/page/settingsAdvanced/index.js index e1d559dac..1e8d3336e 100644 --- a/ui/page/settingsAdvanced/index.js +++ b/ui/page/settingsAdvanced/index.js @@ -6,7 +6,8 @@ import { doClearDaemonSetting, doSetClientSetting, doFindFFmpeg, - doSyncClientSettings, + doEnterSettingsPage, + doExitSettingsPage, } from 'redux/actions/settings'; import { makeSelectClientSetting, @@ -34,7 +35,6 @@ const select = state => ({ const perform = dispatch => ({ setDaemonSetting: (key, value) => dispatch(doSetDaemonSetting(key, value)), - syncSettings: () => dispatch(doSyncClientSettings()), clearDaemonSetting: key => dispatch(doClearDaemonSetting(key)), clearCache: () => dispatch(doClearCache()), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), @@ -43,6 +43,8 @@ const perform = dispatch => ({ updateWalletStatus: () => dispatch(doWalletStatus()), confirmForgetPassword: modalProps => dispatch(doNotifyForgetPassword(modalProps)), findFFmpeg: () => dispatch(doFindFFmpeg()), + enterSettings: () => dispatch(doEnterSettingsPage()), + exitSettings: () => dispatch(doExitSettingsPage()), }); export default connect(select, perform)(SettingsPage); diff --git a/ui/page/settingsAdvanced/view.jsx b/ui/page/settingsAdvanced/view.jsx index 2863f765e..a0df40293 100644 --- a/ui/page/settingsAdvanced/view.jsx +++ b/ui/page/settingsAdvanced/view.jsx @@ -55,6 +55,8 @@ type Props = { language?: string, syncEnabled: boolean, syncSettings: () => void, + enterSettings: () => void, + exitSettings: () => void, }; type State = { @@ -81,7 +83,7 @@ class SettingsPage extends React.PureComponent { } componentDidMount() { - const { isAuthenticated, ffmpegStatus, daemonSettings, findFFmpeg } = this.props; + const { isAuthenticated, ffmpegStatus, daemonSettings, findFFmpeg, enterSettings } = this.props; // @if TARGET='app' const { available } = ffmpegStatus; @@ -102,6 +104,12 @@ class SettingsPage extends React.PureComponent { } }); } + enterSettings(); + } + + componentWillUnmount() { + const { exitSettings } = this.props; + exitSettings(); } onFFmpegFolder(path: string) { diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index 6501970e9..61f3623b1 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -46,11 +46,11 @@ import { import { selectDaemonSettings } from 'redux/selectors/settings'; import { selectUser } from 'redux/selectors/user'; // import { selectDaemonSettings } from 'redux/selectors/settings'; -import { doGetSync } from 'lbryinc'; +import { doSyncSubscribe } from 'redux/actions/syncwrapper'; import { doAuthenticate } from 'redux/actions/user'; import { lbrySettings as config, version as appVersion } from 'package.json'; import analytics, { SHARE_INTERNAL } from 'analytics'; -import { doSignOutCleanup, deleteSavedPassword, getSavedPassword } from 'util/saved-passwords'; +import { doSignOutCleanup, deleteSavedPassword } from 'util/saved-passwords'; import { doSocketConnect } from 'redux/actions/websocket'; import { stringifyServerParam, shouldSetSetting } from 'util/sync-settings'; import sha256 from 'crypto-js/sha256'; @@ -89,6 +89,13 @@ export function doUpdateDownloadProgress(percent) { }; } +export function doSetSyncLock(lock) { + return { + type: ACTIONS.SET_SYNC_LOCK, + data: lock, + }; +} + export function doSkipUpgrade() { return { type: ACTIONS.SKIP_UPGRADE, @@ -622,6 +629,7 @@ export function doGetAndPopulatePreferences() { } // @endif } + return true; } function failCb() { @@ -633,9 +641,10 @@ export function doGetAndPopulatePreferences() { }) ); }); + return false; } - doPreferenceGet(preferenceKey, successCb, failCb); + return doPreferenceGet(preferenceKey, successCb, failCb); }; } @@ -653,11 +662,5 @@ export function doHandleSyncComplete(error, hasNewData) { } export function doSyncWithPreferences() { - return dispatch => { - return getSavedPassword().then(password => { - const passwordArgument = password === null ? '' : password; - - dispatch(doGetSync(passwordArgument, (error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData)))); - }); - }; + return dispatch => dispatch(doSyncSubscribe()); } diff --git a/ui/redux/actions/settings.js b/ui/redux/actions/settings.js index 6b1f949a6..9428b23d3 100644 --- a/ui/redux/actions/settings.js +++ b/ui/redux/actions/settings.js @@ -5,6 +5,9 @@ import analytics from 'analytics'; import SUPPORTED_LANGUAGES from 'constants/supported_languages'; import { launcher } from 'util/autoLaunch'; import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { doGetSyncDesktop, doSyncUnsubscribe } from 'redux/actions/syncwrapper'; +import { doGetAndPopulatePreferences, doSetSyncLock } from 'redux/actions/app'; + const { DEFAULT_LANGUAGE } = require('config'); const { SDK_SYNC_KEYS } = SHARED_PREFERENCES; @@ -119,7 +122,7 @@ export function doSaveCustomWalletServers(servers) { }; } -export function doSetClientSetting(key, value) { +export function doSetClientSetting(key, value, pushPrefs) { return dispatch => { dispatch({ type: ACTIONS.CLIENT_SETTING_CHANGED, @@ -128,6 +131,30 @@ export function doSetClientSetting(key, value) { value, }, }); + + if (pushPrefs) { + dispatch(doPushSettingsToPrefs()); + } + }; +} + +export function doUpdateSyncPref() { + return (dispatch, getState) => { + const { settings } = getState(); + const { syncEnabledPref } = settings || {}; + if (syncEnabledPref !== undefined) { + dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, syncEnabledPref, true)); + dispatch(doSetSyncPref(undefined)); + } + }; +} + +export function doSetSyncPref(isEnabled) { + return dispatch => { + dispatch({ + type: LOCAL_ACTIONS.SYNC_PREFERENCE_CHANGED, + data: isEnabled, + }); }; } @@ -167,15 +194,36 @@ export function doSetDarkTime(value, options) { }; } -export function doSyncClientSettings() { - return (dispatch, getState) => { - const state = getState(); - const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); - if (syncEnabled) { +export function doPushSettingsToPrefs() { + return dispatch => { + return new Promise((resolve, reject) => { dispatch({ type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS, }); + resolve(); + }); + }; +} + +export function doEnterSettingsPage() { + return async (dispatch, getState) => { + const state = getState(); + const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); + const hasVerifiedEmail = state.user && state.user.user && state.user.user.has_verified_email; + dispatch(doSyncUnsubscribe()); + if (syncEnabled && hasVerifiedEmail) { + await dispatch(doGetSyncDesktop()); + } else { + await dispatch(doGetAndPopulatePreferences()); } + dispatch(doSetSyncLock(true)); + }; +} + +export function doExitSettingsPage() { + return (dispatch, getState) => { + dispatch(doSetSyncLock(false)); + dispatch(doPushSettingsToPrefs()); }; } diff --git a/ui/redux/actions/syncwrapper.js b/ui/redux/actions/syncwrapper.js new file mode 100644 index 000000000..1bfb898ae --- /dev/null +++ b/ui/redux/actions/syncwrapper.js @@ -0,0 +1,57 @@ +// @flow +import { doGetSync, selectGetSyncIsPending, selectSetSyncIsPending } from 'lbryinc'; +import { SETTINGS } from 'lbry-redux'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { getSavedPassword } from 'util/saved-passwords'; +import { doAnalyticsTagSync, doHandleSyncComplete } from 'redux/actions/app'; +import { selectSyncIsLocked } from 'redux/selectors/app'; +import { selectUserVerifiedEmail } from 'redux/selectors/user'; + +let syncTimer = null; +const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes + +export const doGetSyncDesktop = (cb?: () => void, password?: string) => (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); + const getSyncPending = selectGetSyncIsPending(state); + const setSyncPending = selectSetSyncIsPending(state); + const syncLocked = selectSyncIsLocked(state); + + return getSavedPassword().then(savedPassword => { + const passwordArgument = password || password === '' ? password : savedPassword === null ? '' : savedPassword; + + if (syncEnabled && !getSyncPending && !setSyncPending && !syncLocked) { + return dispatch(doGetSync(passwordArgument, cb)); + } + }); +}; + +export function doSyncSubscribe() { + return (dispatch: Dispatch, getState: GetState) => { + if (syncTimer) clearInterval(syncTimer); + const state = getState(); + const hasVerifiedEmail = selectUserVerifiedEmail(state); + const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); + const syncLocked = selectSyncIsLocked(state); + if (hasVerifiedEmail && syncEnabled && !syncLocked) { + dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData)))); + dispatch(doAnalyticsTagSync()); + syncTimer = setInterval(() => { + const state = getState(); + const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); + if (syncEnabled) { + dispatch(doGetSyncDesktop((error, hasNewData) => dispatch(doHandleSyncComplete(error, hasNewData)))); + dispatch(doAnalyticsTagSync()); + } + }, SYNC_INTERVAL); + } + }; +} + +export function doSyncUnsubscribe() { + return (dispatch: Dispatch) => { + if (syncTimer) { + clearInterval(syncTimer); + } + }; +} diff --git a/ui/redux/reducers/app.js b/ui/redux/reducers/app.js index b00e85688..2eddf2f81 100644 --- a/ui/redux/reducers/app.js +++ b/ui/redux/reducers/app.js @@ -42,6 +42,7 @@ export type AppState = { welcomeVersion: number, allowAnalytics: boolean, hasNavigated: boolean, + syncLocked: boolean, }; const defaultState: AppState = { @@ -76,6 +77,7 @@ const defaultState: AppState = { welcomeVersion: 0.0, allowAnalytics: false, hasNavigated: false, + syncLocked: false, }; // @@router comes from react-router @@ -109,6 +111,11 @@ reducers[ACTIONS.DAEMON_READY] = state => daemonReady: true, }); +reducers[ACTIONS.SET_SYNC_LOCK] = (state, action) => + Object.assign({}, state, { + syncLocked: action.data, + }); + reducers[ACTIONS.PASSWORD_SAVED] = (state, action) => Object.assign({}, state, { isPasswordSaved: action.data, diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js index a59a4c79c..005fb7bf9 100644 --- a/ui/redux/reducers/settings.js +++ b/ui/redux/reducers/settings.js @@ -24,6 +24,7 @@ const defaultState = { findingFFmpeg: false, loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'], customWalletServers: [], + syncEnabledPref: undefined, // set this during sign in, copy it to clientSettings immediately after prefGet after signedin but before sync sharedPreferences: {}, daemonSettings: {}, daemonStatus: { ffmpeg_status: {} }, @@ -144,17 +145,6 @@ reducers[LBRY_REDUX_ACTIONS.SHARED_PREFERENCE_SET] = (state, action) => { }); }; -reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => { - const { key, value } = action.data; - const clientSettings = Object.assign({}, state.clientSettings); - - clientSettings[key] = value; - - return Object.assign({}, state, { - clientSettings, - }); -}; - reducers[ACTIONS.SYNC_CLIENT_SETTINGS] = state => { const { clientSettings } = state; const sharedPreferences = Object.assign({}, state.sharedPreferences); @@ -163,6 +153,12 @@ reducers[ACTIONS.SYNC_CLIENT_SETTINGS] = state => { return Object.assign({}, state, { sharedPreferences: newSharedPreferences }); }; +reducers[ACTIONS.SYNC_PREFERENCE_CHANGED] = (state, action) => { + return Object.assign({}, state, { + syncEnabledPref: action.data, + }); +}; + reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => { const { clientSettings: currentClientSettings } = state; const { settings: sharedPreferences } = action.data; diff --git a/ui/redux/selectors/app.js b/ui/redux/selectors/app.js index cdc0dd892..a6ee855fc 100644 --- a/ui/redux/selectors/app.js +++ b/ui/redux/selectors/app.js @@ -80,3 +80,5 @@ export const selectAllowAnalytics = createSelector(selectState, state => state.a export const selectScrollStartingPosition = createSelector(selectState, state => state.currentScroll); export const selectIsPasswordSaved = createSelector(selectState, state => state.isPasswordSaved); + +export const selectSyncIsLocked = createSelector(selectState, state => state.syncLocked); diff --git a/ui/redux/selectors/settings.js b/ui/redux/selectors/settings.js index 8e7f3580f..30479d1de 100644 --- a/ui/redux/selectors/settings.js +++ b/ui/redux/selectors/settings.js @@ -11,6 +11,8 @@ export const selectFfmpegStatus = createSelector(selectDaemonStatus, status => s export const selectFindingFFmpeg = createSelector(selectState, state => state.findingFFmpeg || false); +export const selectSyncSigninPref = createSelector(selectState, state => state.syncEnabledPref || undefined); + export const selectClientSettings = createSelector(selectState, state => state.clientSettings || {}); export const selectLoadedLanguages = createSelector(selectState, state => state.loadedLanguages || {}); diff --git a/ui/store.js b/ui/store.js index 5975b5501..5e7a4930f 100644 --- a/ui/store.js +++ b/ui/store.js @@ -9,11 +9,9 @@ import thunk from 'redux-thunk'; import { createMemoryHistory, createBrowserHistory } from 'history'; import { routerMiddleware } from 'connected-react-router'; import createRootReducer from './reducers'; -import { Lbry, buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS, SETTINGS } from 'lbry-redux'; -import { doGetSync } from 'lbryinc'; -import { selectUserVerifiedEmail } from 'redux/selectors/user'; -import { getSavedPassword, getAuthToken } from 'util/saved-passwords'; -import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { Lbry, buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; +import { doSyncSubscribe } from 'redux/actions/syncwrapper'; +import { getAuthToken } from 'util/saved-passwords'; import { generateInitialUrl } from 'util/url'; import { X_LBRY_AUTH_TOKEN } from 'constants/token'; @@ -152,14 +150,7 @@ const sharedStateFilters = { }; const sharedStateCb = ({ dispatch, getState }) => { - const state = getState(); - const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); - const emailVerified = selectUserVerifiedEmail(state); - if (syncEnabled && emailVerified) { - getSavedPassword().then(savedPassword => { - dispatch(doGetSync(savedPassword)); - }); - } + dispatch(doSyncSubscribe()); }; const populateAuthTokenHeader = () => { diff --git a/yarn.lock b/yarn.lock index 11a36ffbe..c4a1cc0f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6411,17 +6411,17 @@ lazy-val@^1.0.4: yargs "^13.2.2" zstd-codec "^0.1.1" -lbry-redux@lbryio/lbry-redux#210bb80f2c49f6a166a8adc602a68b7490d5d23a: +lbry-redux@lbryio/lbry-redux#760623f99c90407ca3f1d06f1cdd2506fb2e443a: version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/210bb80f2c49f6a166a8adc602a68b7490d5d23a" + resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/760623f99c90407ca3f1d06f1cdd2506fb2e443a" dependencies: proxy-polyfill "0.1.6" reselect "^3.0.0" uuid "^3.3.2" -lbryinc@lbryio/lbryinc#cff5dd60934c4c6080e135f47ebbece1548c658c: +lbryinc@lbryio/lbryinc#35df87d1e69e435e25fc12832b6b1b788f76baaa: version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/cff5dd60934c4c6080e135f47ebbece1548c658c" + resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/35df87d1e69e435e25fc12832b6b1b788f76baaa" dependencies: reselect "^3.0.0"