diff --git a/package.json b/package.json index 545647347..31f87d353 100644 --- a/package.json +++ b/package.json @@ -130,8 +130,8 @@ "husky": "^0.14.3", "json-loader": "^0.5.4", "lbry-format": "https://github.com/lbryio/lbry-format.git", - "lbry-redux": "lbryio/lbry-redux#0a1c95a08835a6b892d853b156d4934e469c9589", - "lbryinc": "lbryio/lbryinc#d1dba98bb6f1dc67bc0db4c0a20fc13b8a0de98b", + "lbry-redux": "lbryio/lbry-redux#6edcf747e10919605b05b905214fe1d3286898e3", + "lbryinc": "lbryio/lbryinc#b8e1708ee4491db342c81576265e1b58f542bedb", "lint-staged": "^7.0.2", "localforage": "^1.7.1", "lodash-es": "^4.17.14", diff --git a/src/platforms/electron/createWindow.js b/src/platforms/electron/createWindow.js index 0b0be66c1..ba8c536c7 100644 --- a/src/platforms/electron/createWindow.js +++ b/src/platforms/electron/createWindow.js @@ -21,6 +21,7 @@ export default appState => { minWidth: 950, minHeight: 600, autoHideMenuBar: true, + titleBarStyle: 'hiddenInset', show: false, // Create the window using the state information. x: windowState.x, diff --git a/src/ui/component/app/view.jsx b/src/ui/component/app/view.jsx index db57419e0..b500adac7 100644 --- a/src/ui/component/app/view.jsx +++ b/src/ui/component/app/view.jsx @@ -1,6 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import classnames from 'classnames'; import analytics from 'analytics'; import { buildURI, parseURI } from 'lbry-redux'; import Router from 'component/router/index'; @@ -15,6 +16,9 @@ import usePrevious from 'effects/use-previous'; import Button from 'component/button'; export const MAIN_WRAPPER_CLASS = 'main-wrapper'; +// @if TARGET='app' +export const IS_MAC = process.platform === 'darwin'; +// @endif type Props = { alertError: (string | {}) => void, @@ -54,6 +58,7 @@ function App(props: Props) { } = props; const appRef = useRef(); const isEnhancedLayout = useKonamiListener(); + const [hasSignedIn, setHasSignedIn] = useState(false); const userId = user && user.id; const hasVerifiedEmail = user && user.has_verified_email; const isRewardApproved = user && user.is_reward_approved; @@ -110,17 +115,27 @@ function App(props: Props) { useEffect(() => { // Wait for balance to be populated on desktop so we know when we can begin syncing // @syncwithbalancefixme - if (hasVerifiedEmail && (IS_WEB || balance !== undefined)) { + if (!hasSignedIn && hasVerifiedEmail && (IS_WEB || balance !== undefined)) { signIn(); + + setHasSignedIn(true); } - }, [hasVerifiedEmail, signIn, balance]); + }, [hasVerifiedEmail, signIn, balance, hasSignedIn]); if (!user) { return null; } return ( -
openContextMenu(e)}> +
openContextMenu(e)} + > diff --git a/src/ui/component/header/view.jsx b/src/ui/component/header/view.jsx index af57ab392..e0bf15763 100644 --- a/src/ui/component/header/view.jsx +++ b/src/ui/component/header/view.jsx @@ -11,6 +11,9 @@ import WunderBar from 'component/wunderbar'; import Icon from 'component/common/icon'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; import Tooltip from 'component/common/tooltip'; +// @if TARGET='app' +import { IS_MAC } from 'component/app/view'; +// @endif type Props = { balance: string, @@ -62,7 +65,14 @@ const Header = (props: Props) => { } return ( -
+
), - showSyncPassword && , // @endif + showSyncPassword && , showLoadingSpinner && (
diff --git a/src/ui/index.jsx b/src/ui/index.jsx index 12c9d0592..fd02df782 100644 --- a/src/ui/index.jsx +++ b/src/ui/index.jsx @@ -54,7 +54,7 @@ if (process.env.SEARCH_API_URL) { } // @if TARGET='web' -const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/proxy'; +const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/v1/proxy'; Lbry.setDaemonConnectionString(SDK_API_URL); // @endif diff --git a/src/ui/page/settings/view.jsx b/src/ui/page/settings/view.jsx index 5a8351096..d744158f8 100644 --- a/src/ui/page/settings/view.jsx +++ b/src/ui/page/settings/view.jsx @@ -52,7 +52,7 @@ type Props = { themes: Array, automaticDarkModeEnabled: boolean, autoplay: boolean, - autoDownload: boolean, + // autoDownload: boolean, encryptWallet: () => void, decryptWallet: () => void, updateWalletStatus: () => void, @@ -198,7 +198,7 @@ class SettingsPage extends React.PureComponent { autoplay, walletEncrypted, osNotificationsEnabled, - autoDownload, + // autoDownload, setDaemonSetting, setClientSetting, supportOption, @@ -588,6 +588,8 @@ class SettingsPage extends React.PureComponent { /> {/* @if TARGET='app' */} + {/* + Disabling below until we get downloads to work with shared subscriptions code { helper={__( "The latest file from each of your subscriptions will be downloaded for quick access as soon as it's published." )} - /> + /> */} { + function handlePreferencesAfterSync() { + function successCb(savedPreferences) { + dispatch(doPopulateSharedUserState(savedPreferences)); + } + + function failCb() { + dispatch( + doToast({ + isError: true, + message: __('Unable to load your saved preferences.'), + }) + ); + } + + doPreferenceGet('shared', successCb, failCb); + } + // The balance is subscribed to on launch for desktop // @if TARGET='web' const { auth_token: authToken } = cookie.parse(document.cookie); Lbry.setApiHeader('X-Lbry-Auth-Token', authToken); dispatch(doBalanceSubscribe()); dispatch(doFetchChannelListMine()); - dispatch(doCheckSubscriptionsInit()); // @endif - // @if TARGET='app' const state = getState(); const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); + const hasSyncedBefore = selectSyncHash(state); const balance = selectBalance(state); // For existing users, check if they've synced before, or have 0 balance - if (syncEnabled && (hasSyncedBefore || balance === 0)) { + // Always sync for web + const canSync = syncEnabled && (hasSyncedBefore || balance === 0 || IS_WEB); + + if (canSync) { getSavedPassword().then(password => { const passwordArgument = password === null ? '' : password; - dispatch(doGetSync(passwordArgument, !hasSyncedBefore)); + + // Only set the default account if they have never synced before + dispatch(doGetSync(passwordArgument, handlePreferencesAfterSync)); setInterval(() => { - dispatch(doGetSync(passwordArgument)); + dispatch(doGetSync(passwordArgument, handlePreferencesAfterSync)); }, 1000 * 60 * 5); }); } - // @endif - - Lbryio.call('user_settings', 'get').then(settings => { - dispatch(doPopulateSharedUserState(settings)); - }); }; } diff --git a/src/ui/redux/actions/subscriptions.js b/src/ui/redux/actions/subscriptions.js index bd910724a..71e0e9885 100644 --- a/src/ui/redux/actions/subscriptions.js +++ b/src/ui/redux/actions/subscriptions.js @@ -1,16 +1,8 @@ // @flow -import { PAGE_SIZE } from 'constants/claim'; import * as ACTIONS from 'constants/action_types'; -import * as SETTINGS from 'constants/settings'; -import * as NOTIFICATION_TYPES from 'constants/subscriptions'; import { Lbryio, rewards, doClaimRewardType } from 'lbryinc'; -import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions'; -import { makeSelectClientSetting } from 'redux/selectors/settings'; -import { Lbry, parseURI, doResolveUris } from 'lbry-redux'; -import { doPlayUri } from 'redux/actions/content'; - -const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000; -const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; +import { selectUnreadByChannel } from 'redux/selectors/subscriptions'; +import { parseURI, doResolveUris } from 'lbry-redux'; export const doSetViewMode = (viewMode: ViewMode) => (dispatch: Dispatch) => dispatch({ @@ -73,7 +65,6 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetSt }); dispatch(doResolveUris(subscriptions.map(({ uri }) => uri))); - dispatch(doCheckSubscriptions()); }) .catch(() => { dispatch({ @@ -190,106 +181,6 @@ export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri])); }; -export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => ( - dispatch: Dispatch, - getState: GetState -) => { - // no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on - - const state = getState(); - const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state); - const savedSubscription = state.subscriptions.subscriptions.find(sub => sub.uri === subscriptionUri); - - if (!savedSubscription) { - throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`); - } - dispatch({ - type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED, - data: { - uri: subscriptionUri, - page: 1, - }, - }); - // We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel? - Lbry.claim_search({ - channel: subscriptionUri, - valid_channel_signature: true, - order_by: ['release_time'], - page: 1, - page_size: PAGE_SIZE, - }).then(claimListByChannel => { - const { items: claimsInChannel } = claimListByChannel; - - // may happen if subscribed to an abandoned channel or an empty channel - if (!claimsInChannel || !claimsInChannel.length) { - return; - } - - dispatch({ - type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED, - data: { - uri: subscriptionUri, - claims: claimsInChannel || [], - page: 1, - }, - }); - - // Determine if the latest subscription currently saved is actually the latest subscription - const latestIndex = claimsInChannel.findIndex(claim => claim.permanent_url === savedSubscription.latest); - - // If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed - const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex; - - // If latest is 0, nothing has changed - // Do not download/notify about new content, it would download/notify 10 claims per channel - if (latestIndex !== 0 && savedSubscription.latest) { - let downloadCount = 0; - - const newUnread = []; - claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => { - const uri = claim.canonical_url; - const shouldDownload = - shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee); - - // Add the new content to the list of "un-read" subscriptions - if (shouldNotify) { - newUnread.push(uri); - } - - if (shouldDownload) { - downloadCount += 1; - // this fails since something is not resolved/saved somewhere... - dispatch(doPlayUri(uri, true, true)); - } - }); - - dispatch( - doUpdateUnreadSubscriptions( - subscriptionUri, - newUnread, - downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY - ) - ); - } - - // Set the latest piece of content for a channel - // This allows the app to know if there has been new content since it was last set - const latest = claimsInChannel[0]; - dispatch( - setSubscriptionLatest( - { - channelName: latest.signing_channel.name, - uri: latest.signing_channel.permanent_url, - }, - latest.permanent_url - ) - ); - - // calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED - // means it will delete a non-existent fetchingChannelClaims[uri] - }); -}; - export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => { const { settings: { daemonSettings }, @@ -318,8 +209,6 @@ export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dis dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true })); } - - dispatch(doCheckSubscription(subscription.uri, true)); }; export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => { @@ -342,27 +231,6 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D } }; -export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const subscriptions = selectSubscriptions(state); - - subscriptions.forEach((sub: Subscription) => { - dispatch(doCheckSubscription(sub.uri, true)); - }); -}; - -export const doCheckSubscriptionsInit = () => (dispatch: Dispatch) => { - // doCheckSubscriptionsInit is called by doDaemonReady - // setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked - // this will be replaced with which requires a package upgrade - setTimeout(() => dispatch(doFetchMySubscriptions()), 5000); - const checkSubscriptionsTimer = setInterval(() => dispatch(doCheckSubscriptions()), CHECK_SUBSCRIPTIONS_INTERVAL); - dispatch({ - type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE, - data: { checkSubscriptionsTimer }, - }); -}; - export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => { dispatch({ type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START, diff --git a/src/ui/scss/component/_header.scss b/src/ui/scss/component/_header.scss index 12cadb6ed..dcee54242 100644 --- a/src/ui/scss/component/_header.scss +++ b/src/ui/scss/component/_header.scss @@ -7,6 +7,8 @@ border-bottom: 1px solid $lbry-gray-1; box-shadow: var(--card-box-shadow) $lbry-gray-1; font-size: var(--font-body); + -webkit-user-select: none; + -webkit-app-region: drag; [data-mode='dark'] & { background-color: var(--dm-color-05); @@ -26,6 +28,10 @@ border-bottom: none; } +.header--mac { + padding-top: var(--mac-titlebar-height); +} + .header__contents { max-width: var(--page-max-width); height: calc(var(--header-height) - 1px); diff --git a/src/ui/scss/component/_main.scss b/src/ui/scss/component/_main.scss index 85f999ecd..9b8b8748b 100644 --- a/src/ui/scss/component/_main.scss +++ b/src/ui/scss/component/_main.scss @@ -6,6 +6,10 @@ } } +.main-wrapper--mac { + margin-top: calc(var(--header-height) + var(--mac-titlebar-height)); +} + .main-wrapper__inner { display: flex; align-items: flex-start; diff --git a/src/ui/scss/init/_vars.scss b/src/ui/scss/init/_vars.scss index 8097fef4e..242cb1951 100644 --- a/src/ui/scss/init/_vars.scss +++ b/src/ui/scss/init/_vars.scss @@ -8,6 +8,7 @@ $large-breakpoint: 1921px; :root { // Width & spacing --page-max-width: 1420px; + --mac-titlebar-height: 1.5rem; --mobile: 600px; --side-nav-width: 170px; --spacing-miniscule: calc(2rem / 5); diff --git a/src/ui/store.js b/src/ui/store.js index ceeb6a379..78751d785 100644 --- a/src/ui/store.js +++ b/src/ui/store.js @@ -1,3 +1,5 @@ +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'; @@ -8,8 +10,10 @@ import thunk from 'redux-thunk'; import { createHashHistory, createBrowserHistory } from 'history'; import { routerMiddleware } from 'connected-react-router'; import createRootReducer from './reducers'; -import { Lbryio } from 'lbryinc'; -import isEqual from 'util/deep-equal'; +import { buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; +import { doGetSync, selectUserVerifiedEmail } from 'lbryinc'; +import { getSavedPassword } from 'util/saved-passwords'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; function isFunction(object) { return typeof object === 'function'; @@ -50,6 +54,7 @@ const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpand const walletFilter = createFilter('wallet', ['receiveAddress']); const searchFilter = createFilter('search', ['options']); const tagsFilter = createFilter('tags', ['followedTags']); +const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); const blockedFilter = createFilter('blocked', ['blockedChannels']); const whiteListedReducers = [ // @if TARGET='app' @@ -64,6 +69,7 @@ const whiteListedReducers = [ 'blocked', 'settings', 'sync', + 'subscriptions', ]; const transforms = [ @@ -77,6 +83,7 @@ const transforms = [ searchFilter, tagsFilter, contentFilter, + subscriptionsFilter, createCompressor(), ]; @@ -98,10 +105,51 @@ history = createHashHistory(); history = createBrowserHistory(); // @endif +const triggerSharedStateActions = [ + ACTIONS.CHANNEL_SUBSCRIBE, + ACTIONS.CHANNEL_UNSUBSCRIBE, + LBRY_REDUX_ACTIONS.TOGGLE_TAG_FOLLOW, + LBRY_REDUX_ACTIONS.TOGGLE_BLOCK_CHANNEL, + LBRY_REDUX_ACTIONS.CREATE_CHANNEL_COMPLETED, +]; + +/** + * source: the reducer name + * property: the property in the reducer-specific state + * transform: optional method to modify the value to be stored + * + * See https://github.com/lbryio/lbry-redux/blob/master/src/redux/middleware/shared-state.js for the source + * This is based off v0.1 + * If lbry-redux changes to another version, this code will need to be changed when upgrading + */ +const sharedStateFilters = { + tags: { source: 'tags', property: 'followedTags' }, + subscriptions: { + source: 'subscriptions', + property: 'subscriptions', + transform: function(value) { + return value.map(({ uri }) => uri); + }, + }, + blocked: { source: 'blocked', property: 'blockedChannels' }, +}; + +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)); + }); + } +}; + +const sharedStateMiddleware = buildSharedStateMiddleware(triggerSharedStateActions, sharedStateFilters, sharedStateCb); const rootReducer = createRootReducer(history); const persistedReducer = persistReducer(persistOptions, rootReducer); const bulkThunk = createBulkThunkMiddleware(); -const middleware = [routerMiddleware(history), thunk, bulkThunk]; +const middleware = [sharedStateMiddleware, routerMiddleware(history), thunk, bulkThunk]; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore( enableBatching(persistedReducer), @@ -109,29 +157,6 @@ const store = createStore( composeEnhancers(applyMiddleware(...middleware)) ); -let currentPayload; -store.subscribe(() => { - const state = store.getState(); - const subscriptions = state.subscriptions.subscriptions.map(({ uri }) => uri); - const tags = state.tags.followedTags; - const authToken = state.user.accessToken; - - const newPayload = { - version: '0.1', - shared: { - subscriptions, - tags, - }, - }; - - if (!isEqual(newPayload, currentPayload)) { - currentPayload = newPayload; - if (authToken) { - Lbryio.call('user_settings', 'set', { settings: newPayload }); - } - } -}); - const persistor = persistStore(store); window.persistor = persistor; diff --git a/src/ui/util/saved-passwords.js b/src/ui/util/saved-passwords.js index 03d0f6a6e..90139262d 100644 --- a/src/ui/util/saved-passwords.js +++ b/src/ui/util/saved-passwords.js @@ -11,10 +11,17 @@ export const setSavedPassword = value => { export const getSavedPassword = () => { return new Promise(resolve => { + // @if TARGET='app' ipcRenderer.once('get-password-response', (event, password) => { resolve(password); }); ipcRenderer.send('get-password'); + // @endif + + // @if TARGET='web' + // Will handle saved passwords on web differently + resolve(''); + // @endif }); }; diff --git a/static/app-strings.json b/static/app-strings.json index 1deaff334..13d64047d 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -803,5 +803,6 @@ "Enter a LBRY URL or search for videos, music, games and more": "Enter a LBRY URL or search for videos, music, games and more", "This app will automatically download new free content from channels you are subscribed to. You may configure this in Settings or on the Subscriptions page.": "This app will automatically download new free content from channels you are subscribed to. You may configure this in Settings or on the Subscriptions page.", "(Only available on the desktop app.)": "(Only available on the desktop app.)", - "If we have your email address, we will send you notifications related to new content. You may configure these emails from the Help page.": "If we have your email address, we will send you notifications related to new content. You may configure these emails from the Help page." + "If we have your email address, we will send you notifications related to new content. You may configure these emails from the Help page.": "If we have your email address, we will send you notifications related to new content. You may configure these emails from the Help page.", + "Light": "Light" } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3318de9e2..8013d109e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6870,17 +6870,17 @@ lazy-val@^1.0.3, lazy-val@^1.0.4: yargs "^13.2.2" zstd-codec "^0.1.1" -lbry-redux@lbryio/lbry-redux#0a1c95a08835a6b892d853b156d4934e469c9589: +lbry-redux@lbryio/lbry-redux#6edcf747e10919605b05b905214fe1d3286898e3: version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/0a1c95a08835a6b892d853b156d4934e469c9589" + resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/6edcf747e10919605b05b905214fe1d3286898e3" dependencies: proxy-polyfill "0.1.6" reselect "^3.0.0" uuid "^3.3.2" -lbryinc@lbryio/lbryinc#d1dba98bb6f1dc67bc0db4c0a20fc13b8a0de98b: +lbryinc@lbryio/lbryinc#b8e1708ee4491db342c81576265e1b58f542bedb: version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/d1dba98bb6f1dc67bc0db4c0a20fc13b8a0de98b" + resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/b8e1708ee4491db342c81576265e1b58f542bedb" dependencies: reselect "^3.0.0"