From 2251fe5832fb1053bc69e313d98ce3ba368cb557 Mon Sep 17 00:00:00 2001 From: liamcardenas Date: Mon, 26 Mar 2018 00:31:52 -0700 Subject: [PATCH 1/2] Notify after download, badges and fix error --- src/renderer/component/subHeader/index.js | 2 + src/renderer/component/subHeader/view.jsx | 12 +- src/renderer/constants/action_types.js | 2 + src/renderer/constants/notification_types.js | 3 + src/renderer/page/subscriptions/index.js | 8 +- src/renderer/page/subscriptions/view.jsx | 16 ++- src/renderer/redux/actions/content.js | 68 ++++++++-- src/renderer/redux/actions/subscriptions.js | 119 ++++++++++-------- src/renderer/redux/reducers/subscriptions.js | 49 ++++++++ src/renderer/redux/selectors/subscriptions.js | 4 +- 10 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 src/renderer/constants/notification_types.js diff --git a/src/renderer/component/subHeader/index.js b/src/renderer/component/subHeader/index.js index e550952c2..f445734c5 100644 --- a/src/renderer/component/subHeader/index.js +++ b/src/renderer/component/subHeader/index.js @@ -2,11 +2,13 @@ import React from 'react'; import { connect } from 'react-redux'; import { selectCurrentPage, selectHeaderLinks } from 'redux/selectors/navigation'; import { doNavigate } from 'redux/actions/navigation'; +import { selectNotifications } from 'redux/selectors/subscriptions'; import SubHeader from './view'; const select = (state, props) => ({ currentPage: selectCurrentPage(state), subLinks: selectHeaderLinks(state), + notifications: selectNotifications(state), }); const perform = dispatch => ({ diff --git a/src/renderer/component/subHeader/view.jsx b/src/renderer/component/subHeader/view.jsx index 65227dc72..8ede92169 100644 --- a/src/renderer/component/subHeader/view.jsx +++ b/src/renderer/component/subHeader/view.jsx @@ -1,9 +1,15 @@ import React from 'react'; import Link from 'component/link'; import classnames from 'classnames'; +import * as NOTIFICATION_TYPES from 'constants/notification_types'; const SubHeader = props => { - const { subLinks, currentPage, navigate, fullWidth, smallMargin } = props; + const { subLinks, currentPage, navigate, fullWidth, smallMargin, notifications } = props; + + const badges = Object.keys(notifications).reduce( + (acc, cur) => (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING ? acc : acc + 1), + 0 + ); const links = []; @@ -14,7 +20,9 @@ const SubHeader = props => { key={link} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected'} > - {subLinks[link]} + {subLinks[link] === 'Subscriptions' && badges + ? `Subscriptions (${badges})` + : subLinks[link]} ); } diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index f4a993143..43594a8ce 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -165,6 +165,8 @@ export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE'; export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE'; export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS'; export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST'; +export const SET_SUBSCRIPTION_NOTIFICATION = 'SET_SUBSCRIPTION_NOTIFICATION'; +export const SET_SUBSCRIPTION_NOTIFICATIONS = 'SET_SUBSCRIPTION_NOTIFICATIONS'; export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED'; export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED'; export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE'; diff --git a/src/renderer/constants/notification_types.js b/src/renderer/constants/notification_types.js new file mode 100644 index 000000000..4b1dd975a --- /dev/null +++ b/src/renderer/constants/notification_types.js @@ -0,0 +1,3 @@ +export const DOWNLOADING = 'DOWNLOADING'; +export const DOWNLOADED = 'DOWNLOADED'; +export const NOTIFY_ONLY = 'NOTIFY_ONLY;'; diff --git a/src/renderer/page/subscriptions/index.js b/src/renderer/page/subscriptions/index.js index 1cd401dda..4d1ce6409 100644 --- a/src/renderer/page/subscriptions/index.js +++ b/src/renderer/page/subscriptions/index.js @@ -4,18 +4,24 @@ import { selectSubscriptionsFromClaims, selectSubscriptions, selectHasFetchedSubscriptions, + selectNotifications, } from 'redux/selectors/subscriptions'; import { doFetchClaimsByChannel } from 'redux/actions/content'; -import { setHasFetchedSubscriptions } from 'redux/actions/subscriptions'; +import { + setHasFetchedSubscriptions, + setSubscriptionNotifications, +} from 'redux/actions/subscriptions'; import SubscriptionsPage from './view'; const select = state => ({ hasFetchedSubscriptions: state.subscriptions.hasFetchedSubscriptions, savedSubscriptions: selectSubscriptions(state), subscriptions: selectSubscriptionsFromClaims(state), + notifications: selectNotifications(state), }); export default connect(select, { doFetchClaimsByChannel, setHasFetchedSubscriptions, + setSubscriptionNotifications, })(SubscriptionsPage); diff --git a/src/renderer/page/subscriptions/view.jsx b/src/renderer/page/subscriptions/view.jsx index 3e09748a5..8f0e56840 100644 --- a/src/renderer/page/subscriptions/view.jsx +++ b/src/renderer/page/subscriptions/view.jsx @@ -4,6 +4,7 @@ import SubHeader from 'component/subHeader'; import { BusyMessage } from 'component/common.js'; import { FeaturedCategory } from 'page/discover/view'; import type { Subscription } from 'redux/reducers/subscriptions'; +import * as NOTIFICATION_TYPES from 'constants/notification_types'; type SavedSubscriptions = Array; @@ -23,11 +24,23 @@ export default class extends React.PureComponent { // that causes this component to be rendered with zero savedSubscriptions // we need to wait until persist/REHYDRATE has fired before rendering the page componentDidMount() { - const { savedSubscriptions, setHasFetchedSubscriptions } = this.props; + const { + savedSubscriptions, + setHasFetchedSubscriptions, + notifications, + setSubscriptionNotifications, + } = this.props; if (savedSubscriptions.length) { this.fetchSubscriptions(savedSubscriptions); setHasFetchedSubscriptions(); } + const newNotifications = {}; + Object.keys(notifications).forEach(cur => { + if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) { + newNotifications[cur] = { ...notifications[cur] }; + } + }); + setSubscriptionNotifications(newNotifications); } componentWillReceiveProps(props: Props) { @@ -52,6 +65,7 @@ export default class extends React.PureComponent { render() { const { subscriptions, savedSubscriptions } = this.props; + // TODO: if you are subscribed to an empty channel, this will always be true (but it should not be) const someClaimsNotLoaded = Boolean( subscriptions.find(subscription => !subscription.claims.length) ); diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index 146b9b6d5..2fdffc677 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -1,13 +1,20 @@ import * as ACTIONS from 'constants/action_types'; import * as MODALS from 'constants/modal_types'; import * as SETTINGS from 'constants/settings'; +import * as NOTIFICATION_TYPES from 'constants/notification_types'; import { ipcRenderer } from 'electron'; import Lbry from 'lbry'; import Lbryio from 'lbryio'; import { normalizeURI, buildURI } from 'lbryURI'; import { doAlertError, doOpenModal } from 'redux/actions/app'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; -import { setSubscriptionLatest } from 'redux/actions/subscriptions'; +import { doNavigate } from 'redux/actions/navigation'; +import { + setSubscriptionLatest, + setSubscriptionNotification, + setSubscriptionNotifications, +} from 'redux/actions/subscriptions'; +import { selectNotifications } from 'redux/selectors/subscriptions'; import { selectBadgeNumber } from 'redux/selectors/app'; import { selectMyClaimsRaw } from 'redux/selectors/claims'; import { selectResolvingUris } from 'redux/selectors/content'; @@ -164,13 +171,45 @@ export function doUpdateLoadStatus(uri, outpoint) { const totalProgress = selectTotalDownloadProgress(getState()); setProgressBar(totalProgress); - const notif = new window.Notification('LBRY Download Complete', { - body: fileInfo.metadata.title, - silent: false, - }); - notif.onclick = () => { - ipcRenderer.send('focusWindow', 'main'); - }; + const notifications = selectNotifications(getState()); + if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) { + const count = Object.keys(notifications).reduce( + (acc, cur) => + notifications[cur].subscription.channelName === + notifications[uri].subscription.channelName + ? acc + 1 + : acc, + 0 + ); + const notif = new window.Notification(notifications[uri].subscription.channelName, { + body: `Posted ${fileInfo.metadata.title}${ + count > 1 && count < 10 ? ` and ${count - 1} other new items` : '' + }${count > 9 ? ' and 9+ other new items' : ''}`, + silent: false, + }); + notif.onclick = () => { + dispatch( + doNavigate('/show', { + uri, + }) + ); + }; + dispatch( + setSubscriptionNotification( + notifications[uri].subscription, + uri, + NOTIFICATION_TYPES.DOWNLOADED + ) + ); + } else { + const notif = new window.Notification('LBRY Download Complete', { + body: fileInfo.metadata.title, + silent: false, + }); + notif.onclick = () => { + ipcRenderer.send('focusWindow', 'main'); + }; + } } else { // ready to play const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo; @@ -344,7 +383,7 @@ export function doPurchaseUri(uri, specificCostInfo) { } export function doFetchClaimsByChannel(uri, page) { - return dispatch => { + return (dispatch, getState) => { dispatch({ type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED, data: { uri, page }, @@ -371,6 +410,17 @@ export function doFetchClaimsByChannel(uri, page) { buildURI({ contentName: latest.name, claimId: latest.claim_id }, false) ) ); + const notifications = selectNotifications(getState()); + const newNotifications = {}; + Object.keys(notifications).forEach(cur => { + if ( + notifications[cur].subscription.channelName !== latest.channel_name || + notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING + ) { + newNotifications[cur] = { ...notifications[cur] }; + } + }); + dispatch(setSubscriptionNotifications(newNotifications)); } dispatch({ diff --git a/src/renderer/redux/actions/subscriptions.js b/src/renderer/redux/actions/subscriptions.js index 58269ab40..53160ef8c 100644 --- a/src/renderer/redux/actions/subscriptions.js +++ b/src/renderer/redux/actions/subscriptions.js @@ -1,6 +1,12 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import type { Subscription, Dispatch, SubscriptionState } from 'redux/reducers/subscriptions'; +import * as NOTIFICATION_TYPES from 'constants/notification_types'; +import type { + Subscription, + Dispatch, + SubscriptionState, + SubscriptionNotifications, +} from 'redux/reducers/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import Lbry from 'lbry'; import { doPurchaseUri } from 'redux/actions/content'; @@ -9,6 +15,7 @@ import { buildURI } from 'lbryURI'; import analytics from 'analytics'; const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000; +const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { dispatch({ @@ -59,63 +66,47 @@ export const doCheckSubscription = (subscription: Subscription, notify?: boolean const claimResult = result[subscription.uri] || {}; const { claims_in_channel: claimsInChannel } = claimResult; - const count = subscription.latest - ? claimsInChannel.reduce( - (prev, cur, index) => - buildURI({ contentName: cur.name, claimId: cur.claim_id }, false) === - subscription.latest - ? index - : prev, - -1 - ) - : 1; - - if (count !== 0 && notify) { - if (!claimsInChannel[0].value.stream.metadata.fee) { - dispatch( - doPurchaseUri( - buildURI( - { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, - false - ), - { cost: 0 } - ) - ); + if (claimsInChannel) { + if (notify) { + claimsInChannel.reduce((prev, cur, index) => { + const uri = buildURI({ contentName: cur.name, claimId: cur.claim_id }, false); + if (prev === -1 && uri !== subscription.latest) { + dispatch( + setSubscriptionNotification( + subscription, + uri, + index < SUBSCRIPTION_DOWNLOAD_LIMIT && !cur.value.stream.metadata.fee + ? NOTIFICATION_TYPES.DOWNLOADING + : NOTIFICATION_TYPES.NOTIFY_ONLY + ) + ); + if (index < SUBSCRIPTION_DOWNLOAD_LIMIT && !cur.value.stream.metadata.fee) { + dispatch(doPurchaseUri(uri, { cost: 0 })); + } + } + return uri === subscription.latest || !subscription.latest ? index : prev; + }, -1); } - const notif = new window.Notification(subscription.channelName, { - body: `Posted ${claimsInChannel[0].value.stream.metadata.title}${ - count > 1 ? ` and ${count - 1} other new items` : '' - }${count < 0 ? ' and 9+ other new items' : ''}`, - silent: false, - }); - notif.onclick = () => { - dispatch( - doNavigate('/show', { + dispatch( + setSubscriptionLatest( + { + channelName: claimsInChannel[0].channel_name, uri: buildURI( - { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, - true + { + channelName: claimsInChannel[0].channel_name, + claimId: claimsInChannel[0].claim_id, + }, + false ), - }) - ); - }; - } - - dispatch( - setSubscriptionLatest( - { - channelName: claimsInChannel[0].channel_name, - uri: buildURI( - { channelName: claimsInChannel[0].channel_name, claimId: claimsInChannel[0].claim_id }, + }, + buildURI( + { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false - ), - }, - buildURI( - { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, - false + ) ) - ) - ); + ); + } dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED, @@ -135,5 +126,29 @@ export const setSubscriptionLatest = (subscription: Subscription, uri: string) = }, }); +export const setSubscriptionNotification = ( + subscription: Subscription, + uri: string, + notificationType: string +) => (dispatch: Dispatch) => + dispatch({ + type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION, + data: { + subscription, + uri, + type: notificationType, + }, + }); + +export const setSubscriptionNotifications = (notifications: SubscriptionNotifications) => ( + dispatch: Dispatch +) => + dispatch({ + type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS, + data: { + notifications, + }, + }); + export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); diff --git a/src/renderer/redux/reducers/subscriptions.js b/src/renderer/redux/reducers/subscriptions.js index a8f40746f..7a54e2d46 100644 --- a/src/renderer/redux/reducers/subscriptions.js +++ b/src/renderer/redux/reducers/subscriptions.js @@ -1,5 +1,6 @@ // @flow import * as ACTIONS from 'constants/action_types'; +import * as NOTIFICATION_TYPES from 'constants/notification_types'; import { handleActions } from 'util/redux-utils'; export type Subscription = { @@ -8,10 +9,23 @@ export type Subscription = { latest: ?string, }; +export type NotificationType = + | NOTIFICATION_TYPES.DOWNLOADING + | NOTIFICATION_TYPES.DOWNLOADED + | NOTIFICATION_TYPES.NOTIFY_ONLY; + +export type SubscriptionNotifications = { + [string]: { + subscription: Subscription, + type: NotificationType, + }, +}; + // Subscription redux types export type SubscriptionState = { subscriptions: Array, hasFetchedSubscriptions: boolean, + notifications: SubscriptionNotifications, }; // Subscription action types @@ -37,6 +51,22 @@ type setSubscriptionLatest = { }, }; +type setSubscriptionNotification = { + type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION, + data: { + subscription: Subscription, + uri: string, + type: NotificationType, + }, +}; + +type setSubscriptionNotifications = { + type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS, + data: { + notifications: SubscriptionNotifications, + }, +}; + type CheckSubscriptionStarted = { type: ACTIONS.CHECK_SUBSCRIPTION_STARTED, }; @@ -50,6 +80,7 @@ export type Action = | doChannelUnsubscribe | HasFetchedSubscriptions | setSubscriptionLatest + | setSubscriptionNotification | CheckSubscriptionStarted | CheckSubscriptionCompleted | Function; @@ -58,6 +89,7 @@ export type Dispatch = (action: Action) => any; const defaultState = { subscriptions: [], hasFetchedSubscriptions: false, + notifications: {}, }; export default handleActions( @@ -106,6 +138,23 @@ export default handleActions( : subscription ), }), + [ACTIONS.SET_SUBSCRIPTION_NOTIFICATION]: ( + state: SubscriptionState, + action: setSubscriptionNotification + ): SubscriptionState => ({ + ...state, + notifications: { + ...state.notifications, + [action.data.uri]: { subscription: action.data.subscription, type: action.data.type }, + }, + }), + [ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS]: ( + state: SubscriptionState, + action: setSubscriptionNotifications + ): SubscriptionState => ({ + ...state, + notifications: action.data.notifications, + }), }, defaultState ); diff --git a/src/renderer/redux/selectors/subscriptions.js b/src/renderer/redux/selectors/subscriptions.js index 50bedf3ef..30efb5dc4 100644 --- a/src/renderer/redux/selectors/subscriptions.js +++ b/src/renderer/redux/selectors/subscriptions.js @@ -4,6 +4,8 @@ import { selectAllClaimsByChannel, selectClaimsById } from './claims'; // get the entire subscriptions state const selectState = state => state.subscriptions || {}; +export const selectNotifications = createSelector(selectState, state => state.notifications); + // list of saved channel names and uris export const selectSubscriptions = createSelector(selectState, state => state.subscriptions); @@ -23,7 +25,7 @@ export const selectSubscriptionsFromClaims = createSelector( let channelClaims = []; // if subscribed channel has content - if (channelIds[subscription.uri]) { + if (channelIds[subscription.uri] && channelIds[subscription.uri]['1']) { // This will need to be more robust, we will want to be able to load more than the first page const pageOneChannelIds = channelIds[subscription.uri]['1']; -- 2.45.3 From d1bb7a9c2a72e9008d5c88e9302db2fe33298b76 Mon Sep 17 00:00:00 2001 From: liamcardenas Date: Mon, 26 Mar 2018 01:12:02 -0700 Subject: [PATCH 2/2] Authenticate as early as possible --- src/renderer/index.js | 4 +++- src/renderer/redux/actions/app.js | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/index.js b/src/renderer/index.js index b81edfd2c..631e42af1 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -15,7 +15,7 @@ import { } from 'redux/actions/app'; import { doNavigate } from 'redux/actions/navigation'; import { doDownloadLanguages, doUpdateIsNightAsync } from 'redux/actions/settings'; -import { doUserEmailVerify } from 'redux/actions/user'; +import { doUserEmailVerify, doAuthenticate } from 'redux/actions/user'; import 'scss/all.scss'; import store from 'store'; import app from './app'; @@ -95,6 +95,8 @@ document.addEventListener('click', event => { }); const init = () => { + app.store.dispatch(doAuthenticate()); + autoUpdater.on('update-downloaded', () => { app.store.dispatch(doAutoUpdate()); }); diff --git a/src/renderer/redux/actions/app.js b/src/renderer/redux/actions/app.js index f9c868baf..fa1483c8f 100644 --- a/src/renderer/redux/actions/app.js +++ b/src/renderer/redux/actions/app.js @@ -9,7 +9,6 @@ import { doFetchRewardedContent } from 'redux/actions/content'; import { doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info'; import { doAuthNavigate } from 'redux/actions/navigation'; import { doFetchDaemonSettings } from 'redux/actions/settings'; -import { doAuthenticate } from 'redux/actions/user'; import { doBalanceSubscribe } from 'redux/actions/wallet'; import { doPause } from 'redux/actions/media'; import { doCheckSubscriptions } from 'redux/actions/subscriptions'; @@ -289,7 +288,6 @@ export function doDaemonReady() { return (dispatch, getState) => { const state = getState(); - dispatch(doAuthenticate()); dispatch({ type: ACTIONS.DAEMON_READY }); dispatch(doFetchDaemonSettings()); dispatch(doBalanceSubscribe()); -- 2.45.3