Notify after download, badges and fix error

This commit is contained in:
liamcardenas 2018-03-26 00:31:52 -07:00
parent 87cb8731c8
commit 2251fe5832
10 changed files with 217 additions and 66 deletions
src/renderer

View file

@ -2,11 +2,13 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectCurrentPage, selectHeaderLinks } from 'redux/selectors/navigation'; import { selectCurrentPage, selectHeaderLinks } from 'redux/selectors/navigation';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { selectNotifications } from 'redux/selectors/subscriptions';
import SubHeader from './view'; import SubHeader from './view';
const select = (state, props) => ({ const select = (state, props) => ({
currentPage: selectCurrentPage(state), currentPage: selectCurrentPage(state),
subLinks: selectHeaderLinks(state), subLinks: selectHeaderLinks(state),
notifications: selectNotifications(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -1,9 +1,15 @@
import React from 'react'; import React from 'react';
import Link from 'component/link'; import Link from 'component/link';
import classnames from 'classnames'; import classnames from 'classnames';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
const SubHeader = props => { 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 = []; const links = [];
@ -14,7 +20,9 @@ const SubHeader = props => {
key={link} key={link}
className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected'} className={link == currentPage ? 'sub-header-selected' : 'sub-header-unselected'}
> >
{subLinks[link]} {subLinks[link] === 'Subscriptions' && badges
? `Subscriptions (${badges})`
: subLinks[link]}
</Link> </Link>
); );
} }

View file

@ -165,6 +165,8 @@ export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE'; export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS'; export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST'; 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_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED'; export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE'; export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';

View file

@ -0,0 +1,3 @@
export const DOWNLOADING = 'DOWNLOADING';
export const DOWNLOADED = 'DOWNLOADED';
export const NOTIFY_ONLY = 'NOTIFY_ONLY;';

View file

@ -4,18 +4,24 @@ import {
selectSubscriptionsFromClaims, selectSubscriptionsFromClaims,
selectSubscriptions, selectSubscriptions,
selectHasFetchedSubscriptions, selectHasFetchedSubscriptions,
selectNotifications,
} from 'redux/selectors/subscriptions'; } from 'redux/selectors/subscriptions';
import { doFetchClaimsByChannel } from 'redux/actions/content'; import { doFetchClaimsByChannel } from 'redux/actions/content';
import { setHasFetchedSubscriptions } from 'redux/actions/subscriptions'; import {
setHasFetchedSubscriptions,
setSubscriptionNotifications,
} from 'redux/actions/subscriptions';
import SubscriptionsPage from './view'; import SubscriptionsPage from './view';
const select = state => ({ const select = state => ({
hasFetchedSubscriptions: state.subscriptions.hasFetchedSubscriptions, hasFetchedSubscriptions: state.subscriptions.hasFetchedSubscriptions,
savedSubscriptions: selectSubscriptions(state), savedSubscriptions: selectSubscriptions(state),
subscriptions: selectSubscriptionsFromClaims(state), subscriptions: selectSubscriptionsFromClaims(state),
notifications: selectNotifications(state),
}); });
export default connect(select, { export default connect(select, {
doFetchClaimsByChannel, doFetchClaimsByChannel,
setHasFetchedSubscriptions, setHasFetchedSubscriptions,
setSubscriptionNotifications,
})(SubscriptionsPage); })(SubscriptionsPage);

View file

@ -4,6 +4,7 @@ import SubHeader from 'component/subHeader';
import { BusyMessage } from 'component/common.js'; import { BusyMessage } from 'component/common.js';
import { FeaturedCategory } from 'page/discover/view'; import { FeaturedCategory } from 'page/discover/view';
import type { Subscription } from 'redux/reducers/subscriptions'; import type { Subscription } from 'redux/reducers/subscriptions';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
type SavedSubscriptions = Array<Subscription>; type SavedSubscriptions = Array<Subscription>;
@ -23,11 +24,23 @@ export default class extends React.PureComponent<Props> {
// that causes this component to be rendered with zero savedSubscriptions // that causes this component to be rendered with zero savedSubscriptions
// we need to wait until persist/REHYDRATE has fired before rendering the page // we need to wait until persist/REHYDRATE has fired before rendering the page
componentDidMount() { componentDidMount() {
const { savedSubscriptions, setHasFetchedSubscriptions } = this.props; const {
savedSubscriptions,
setHasFetchedSubscriptions,
notifications,
setSubscriptionNotifications,
} = this.props;
if (savedSubscriptions.length) { if (savedSubscriptions.length) {
this.fetchSubscriptions(savedSubscriptions); this.fetchSubscriptions(savedSubscriptions);
setHasFetchedSubscriptions(); setHasFetchedSubscriptions();
} }
const newNotifications = {};
Object.keys(notifications).forEach(cur => {
if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) {
newNotifications[cur] = { ...notifications[cur] };
}
});
setSubscriptionNotifications(newNotifications);
} }
componentWillReceiveProps(props: Props) { componentWillReceiveProps(props: Props) {
@ -52,6 +65,7 @@ export default class extends React.PureComponent<Props> {
render() { render() {
const { subscriptions, savedSubscriptions } = this.props; 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( const someClaimsNotLoaded = Boolean(
subscriptions.find(subscription => !subscription.claims.length) subscriptions.find(subscription => !subscription.claims.length)
); );

View file

@ -1,13 +1,20 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import Lbry from 'lbry'; import Lbry from 'lbry';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
import { normalizeURI, buildURI } from 'lbryURI'; import { normalizeURI, buildURI } from 'lbryURI';
import { doAlertError, doOpenModal } from 'redux/actions/app'; import { doAlertError, doOpenModal } from 'redux/actions/app';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; 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 { selectBadgeNumber } from 'redux/selectors/app';
import { selectMyClaimsRaw } from 'redux/selectors/claims'; import { selectMyClaimsRaw } from 'redux/selectors/claims';
import { selectResolvingUris } from 'redux/selectors/content'; import { selectResolvingUris } from 'redux/selectors/content';
@ -164,13 +171,45 @@ export function doUpdateLoadStatus(uri, outpoint) {
const totalProgress = selectTotalDownloadProgress(getState()); const totalProgress = selectTotalDownloadProgress(getState());
setProgressBar(totalProgress); setProgressBar(totalProgress);
const notif = new window.Notification('LBRY Download Complete', { const notifications = selectNotifications(getState());
body: fileInfo.metadata.title, if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) {
silent: false, const count = Object.keys(notifications).reduce(
}); (acc, cur) =>
notif.onclick = () => { notifications[cur].subscription.channelName ===
ipcRenderer.send('focusWindow', 'main'); 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 { } else {
// ready to play // ready to play
const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo; const { total_bytes: totalBytes, written_bytes: writtenBytes } = fileInfo;
@ -344,7 +383,7 @@ export function doPurchaseUri(uri, specificCostInfo) {
} }
export function doFetchClaimsByChannel(uri, page) { export function doFetchClaimsByChannel(uri, page) {
return dispatch => { return (dispatch, getState) => {
dispatch({ dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED, type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri, page }, data: { uri, page },
@ -371,6 +410,17 @@ export function doFetchClaimsByChannel(uri, page) {
buildURI({ contentName: latest.name, claimId: latest.claim_id }, false) 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({ dispatch({

View file

@ -1,6 +1,12 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; 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 { selectSubscriptions } from 'redux/selectors/subscriptions';
import Lbry from 'lbry'; import Lbry from 'lbry';
import { doPurchaseUri } from 'redux/actions/content'; import { doPurchaseUri } from 'redux/actions/content';
@ -9,6 +15,7 @@ import { buildURI } from 'lbryURI';
import analytics from 'analytics'; import analytics from 'analytics';
const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000; const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => {
dispatch({ dispatch({
@ -59,63 +66,47 @@ export const doCheckSubscription = (subscription: Subscription, notify?: boolean
const claimResult = result[subscription.uri] || {}; const claimResult = result[subscription.uri] || {};
const { claims_in_channel: claimsInChannel } = claimResult; const { claims_in_channel: claimsInChannel } = claimResult;
const count = subscription.latest if (claimsInChannel) {
? claimsInChannel.reduce( if (notify) {
(prev, cur, index) => claimsInChannel.reduce((prev, cur, index) => {
buildURI({ contentName: cur.name, claimId: cur.claim_id }, false) === const uri = buildURI({ contentName: cur.name, claimId: cur.claim_id }, false);
subscription.latest if (prev === -1 && uri !== subscription.latest) {
? index dispatch(
: prev, setSubscriptionNotification(
-1 subscription,
) uri,
: 1; index < SUBSCRIPTION_DOWNLOAD_LIMIT && !cur.value.stream.metadata.fee
? NOTIFICATION_TYPES.DOWNLOADING
if (count !== 0 && notify) { : NOTIFICATION_TYPES.NOTIFY_ONLY
if (!claimsInChannel[0].value.stream.metadata.fee) { )
dispatch( );
doPurchaseUri( if (index < SUBSCRIPTION_DOWNLOAD_LIMIT && !cur.value.stream.metadata.fee) {
buildURI( dispatch(doPurchaseUri(uri, { cost: 0 }));
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, }
false }
), return uri === subscription.latest || !subscription.latest ? index : prev;
{ cost: 0 } }, -1);
)
);
} }
const notif = new window.Notification(subscription.channelName, { dispatch(
body: `Posted ${claimsInChannel[0].value.stream.metadata.title}${ setSubscriptionLatest(
count > 1 ? ` and ${count - 1} other new items` : '' {
}${count < 0 ? ' and 9+ other new items' : ''}`, channelName: claimsInChannel[0].channel_name,
silent: false,
});
notif.onclick = () => {
dispatch(
doNavigate('/show', {
uri: buildURI( uri: buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, {
true channelName: claimsInChannel[0].channel_name,
claimId: claimsInChannel[0].claim_id,
},
false
), ),
}) },
); buildURI(
}; { contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
}
dispatch(
setSubscriptionLatest(
{
channelName: claimsInChannel[0].channel_name,
uri: buildURI(
{ channelName: claimsInChannel[0].channel_name, claimId: claimsInChannel[0].claim_id },
false false
), )
},
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
) )
) );
); }
dispatch({ dispatch({
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED, 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) => export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) =>
dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS });

View file

@ -1,5 +1,6 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
export type Subscription = { export type Subscription = {
@ -8,10 +9,23 @@ export type Subscription = {
latest: ?string, 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 // Subscription redux types
export type SubscriptionState = { export type SubscriptionState = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
hasFetchedSubscriptions: boolean, hasFetchedSubscriptions: boolean,
notifications: SubscriptionNotifications,
}; };
// Subscription action types // 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 CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED, type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
}; };
@ -50,6 +80,7 @@ export type Action =
| doChannelUnsubscribe | doChannelUnsubscribe
| HasFetchedSubscriptions | HasFetchedSubscriptions
| setSubscriptionLatest | setSubscriptionLatest
| setSubscriptionNotification
| CheckSubscriptionStarted | CheckSubscriptionStarted
| CheckSubscriptionCompleted | CheckSubscriptionCompleted
| Function; | Function;
@ -58,6 +89,7 @@ export type Dispatch = (action: Action) => any;
const defaultState = { const defaultState = {
subscriptions: [], subscriptions: [],
hasFetchedSubscriptions: false, hasFetchedSubscriptions: false,
notifications: {},
}; };
export default handleActions( export default handleActions(
@ -106,6 +138,23 @@ export default handleActions(
: subscription : 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 defaultState
); );

View file

@ -4,6 +4,8 @@ import { selectAllClaimsByChannel, selectClaimsById } from './claims';
// get the entire subscriptions state // get the entire subscriptions state
const selectState = state => state.subscriptions || {}; const selectState = state => state.subscriptions || {};
export const selectNotifications = createSelector(selectState, state => state.notifications);
// list of saved channel names and uris // list of saved channel names and uris
export const selectSubscriptions = createSelector(selectState, state => state.subscriptions); export const selectSubscriptions = createSelector(selectState, state => state.subscriptions);
@ -23,7 +25,7 @@ export const selectSubscriptionsFromClaims = createSelector(
let channelClaims = []; let channelClaims = [];
// if subscribed channel has content // 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 // 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']; const pageOneChannelIds = channelIds[subscription.uri]['1'];