Merge pull request #1872 from lbryio/subscriptions-overhaul

Subscriptions overhaul
This commit is contained in:
Sean Yesmunt 2018-08-20 12:36:22 -04:00 committed by GitHub
commit a5bc6adaff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 168 additions and 116 deletions

View file

@ -25,10 +25,7 @@ type Props = {
const SideBar = (props: Props) => { const SideBar = (props: Props) => {
const { navLinks, notifications } = props; const { navLinks, notifications } = props;
const badges = Object.keys(notifications).reduce( const badges = Object.keys(notifications).length;
(acc, cur) => (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING ? acc : acc + 1),
0
);
return ( return (
<nav className="nav"> <nav className="nav">

View file

@ -15,3 +15,4 @@ export const AUTOMATIC_DARK_MODE_ENABLED = 'automaticDarkModeEnabled';
export const AUTOPLAY = 'autoplay'; export const AUTOPLAY = 'autoplay';
export const RESULT_COUNT = 'resultCount'; export const RESULT_COUNT = 'resultCount';
export const OS_NOTIFICATIONS_ENABLED = 'osNotificationsEnabled'; export const OS_NOTIFICATIONS_ENABLED = 'osNotificationsEnabled';
export const AUTO_DOWNLOAD = 'autoDownload';

View file

@ -40,7 +40,7 @@ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
checkSubscription: subscription => dispatch(doCheckSubscription(subscription)), checkSubscription: uri => dispatch(doCheckSubscription(uri)),
openModal: (modal, props) => dispatch(doNotify(modal, props)), openModal: (modal, props) => dispatch(doNotify(modal, props)),
prepareEdit: (publishData, uri) => dispatch(doPrepareEdit(publishData, uri)), prepareEdit: (publishData, uri) => dispatch(doPrepareEdit(publishData, uri)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),

View file

@ -44,7 +44,7 @@ type Props = {
fetchFileInfo: string => void, fetchFileInfo: string => void,
fetchCostInfo: string => void, fetchCostInfo: string => void,
prepareEdit: ({}, string) => void, prepareEdit: ({}, string) => void,
checkSubscription: ({ channelName: string, uri: string }) => void, checkSubscription: (uri: string) => void,
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
setClientSetting: (string, boolean | string) => void, setClientSetting: (string, boolean | string) => void,
autoplay: boolean, autoplay: boolean,
@ -96,16 +96,12 @@ class FilePage extends React.Component<Props> {
checkSubscription = (props: Props) => { checkSubscription = (props: Props) => {
if (props.subscriptions.find(sub => sub.channelName === props.claim.channel_name)) { if (props.subscriptions.find(sub => sub.channelName === props.claim.channel_name)) {
props.checkSubscription({ props.checkSubscription(
channelName: props.claim.channel_name, buildURI({
uri: buildURI( contentName: props.claim.channel_name,
{ claimId: props.claim.value.publisherSignature.certificateId,
contentName: props.claim.channel_name, }, false)
claimId: props.claim.value.publisherSignature.certificateId, );
},
false
),
});
} }
}; };

View file

@ -30,6 +30,7 @@ const select = state => ({
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state), autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
walletEncrypted: selectWalletIsEncrypted(state), walletEncrypted: selectWalletIsEncrypted(state),
osNotificationsEnabled: selectosNotificationsEnabled(state), osNotificationsEnabled: selectosNotificationsEnabled(state),
autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -31,6 +31,7 @@ type Props = {
themes: Array<string>, themes: Array<string>,
automaticDarkModeEnabled: boolean, automaticDarkModeEnabled: boolean,
autoplay: boolean, autoplay: boolean,
autoDownload: boolean,
encryptWallet: () => void, encryptWallet: () => void,
decryptWallet: () => void, decryptWallet: () => void,
walletEncrypted: boolean, walletEncrypted: boolean,
@ -59,6 +60,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
(this: any).onAutoplayChange = this.onAutoplayChange.bind(this); (this: any).onAutoplayChange = this.onAutoplayChange.bind(this);
(this: any).clearCache = this.clearCache.bind(this); (this: any).clearCache = this.clearCache.bind(this);
(this: any).onDesktopNotificationsChange = this.onDesktopNotificationsChange.bind(this); (this: any).onDesktopNotificationsChange = this.onDesktopNotificationsChange.bind(this);
(this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this);
// (this: any).onLanguageChange = this.onLanguageChange.bind(this) // (this: any).onLanguageChange = this.onLanguageChange.bind(this)
} }
@ -119,6 +121,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
this.props.setClientSetting(settings.SHOW_NSFW, event.target.checked); this.props.setClientSetting(settings.SHOW_NSFW, event.target.checked);
} }
onAutoDownloadChange(event: SyntheticInputEvent<*>) {
this.props.setClientSetting(settings.AUTO_DOWNLOAD, event.target.checked);
}
onChangeEncryptWallet() { onChangeEncryptWallet() {
const { props } = this; const { props } = this;
props.walletEncrypted ? props.decryptWallet() : props.encryptWallet(); props.walletEncrypted ? props.decryptWallet() : props.encryptWallet();
@ -157,6 +163,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
autoplay, autoplay,
walletEncrypted, walletEncrypted,
osNotificationsEnabled, osNotificationsEnabled,
autoDownload,
} = this.props; } = this.props;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -265,6 +272,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
checked={autoplay} checked={autoplay}
postfix={__('Autoplay media files')} postfix={__('Autoplay media files')}
/> />
<FormField
type="checkbox"
name="auto_download"
onChange={this.onAutoDownloadChange}
checked={autoDownload}
postfix={__('Automatically download new content from your subscriptions')}
/>
<FormField <FormField
type="checkbox" type="checkbox"
name="show_nsfw" name="show_nsfw"

View file

@ -25,6 +25,9 @@ export default class extends React.PureComponent<Props> {
const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props; const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props;
doFetchMySubscriptions(); doFetchMySubscriptions();
// @sean will change this behavior when implementing new content labeling
// notifications should be cleared individually
// do we want a way to clear individual claims without viewing?
const newNotifications = {}; const newNotifications = {};
Object.keys(notifications).forEach(cur => { Object.keys(notifications).forEach(cur => {
if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) { if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) {

View file

@ -18,7 +18,7 @@ import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthNavigate } from 'redux/actions/navigation'; import { doAuthNavigate } from 'redux/actions/navigation';
import { doAuthenticate } from 'redux/actions/user'; import { doAuthenticate } from 'redux/actions/user';
import { doPause } from 'redux/actions/media'; import { doPause } from 'redux/actions/media';
import { doCheckSubscriptions } from 'redux/actions/subscriptions'; import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions';
import { import {
selectIsUpgradeSkipped, selectIsUpgradeSkipped,
selectUpdateUrl, selectUpdateUrl,
@ -347,7 +347,7 @@ export function doDaemonReady() {
dispatch(doCheckUpgradeAvailable()); dispatch(doCheckUpgradeAvailable());
} }
dispatch(doCheckUpgradeSubscribe()); dispatch(doCheckUpgradeSubscribe());
dispatch(doCheckSubscriptions()); dispatch(doCheckSubscriptionsInit());
}; };
} }

View file

@ -110,6 +110,7 @@ export function doUpdateLoadStatus(uri, outpoint) {
dispatch(doUpdateLoadStatus(uri, outpoint)); dispatch(doUpdateLoadStatus(uri, outpoint));
}, DOWNLOAD_POLL_INTERVAL); }, DOWNLOAD_POLL_INTERVAL);
} else if (fileInfo.completed) { } else if (fileInfo.completed) {
const state = getState();
// TODO this isn't going to get called if they reload the client before // TODO this isn't going to get called if they reload the client before
// the download finished // the download finished
dispatch({ dispatch({
@ -121,13 +122,13 @@ export function doUpdateLoadStatus(uri, outpoint) {
}, },
}); });
const badgeNumber = selectBadgeNumber(getState()); const badgeNumber = selectBadgeNumber(state);
setBadge(badgeNumber === 0 ? '' : `${badgeNumber}`); setBadge(badgeNumber === 0 ? '' : `${badgeNumber}`);
const totalProgress = selectTotalDownloadProgress(getState()); const totalProgress = selectTotalDownloadProgress(state);
setProgressBar(totalProgress); setProgressBar(totalProgress);
const notifications = selectNotifications(getState()); const notifications = selectNotifications(state);
if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) { if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) {
const count = Object.keys(notifications).reduce( const count = Object.keys(notifications).reduce(
(acc, cur) => (acc, cur) =>
@ -138,7 +139,7 @@ export function doUpdateLoadStatus(uri, outpoint) {
0 0
); );
if (selectosNotificationsEnabled(getState())) { if (selectosNotificationsEnabled(state)) {
const notif = new window.Notification(notifications[uri].subscription.channelName, { const notif = new window.Notification(notifications[uri].subscription.channelName, {
body: `Posted ${fileInfo.metadata.title}${ body: `Posted ${fileInfo.metadata.title}${
count > 1 && count < 10 ? ` and ${count - 1} other new items` : '' count > 1 && count < 10 ? ` and ${count - 1} other new items` : ''
@ -153,13 +154,15 @@ export function doUpdateLoadStatus(uri, outpoint) {
); );
}; };
} }
dispatch( if (state.navigation.currentPath !== '/subscriptions') {
setSubscriptionNotification( dispatch(
notifications[uri].subscription, setSubscriptionNotification(
uri, notifications[uri].subscription,
NOTIFICATION_TYPES.DOWNLOADED uri,
) NOTIFICATION_TYPES.DOWNLOADED
); )
);
}
} else { } else {
// If notifications are disabled(false) just return // If notifications are disabled(false) just return
if (!selectosNotificationsEnabled(getState())) return; if (!selectosNotificationsEnabled(getState())) return;
@ -376,17 +379,18 @@ 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()); // commented out as a note for @sean, notification will be clared individually
const newNotifications = {}; // const notifications = selectNotifications(getState());
Object.keys(notifications).forEach(cur => { // const newNotifications = {};
if ( // Object.keys(notifications).forEach(cur => {
notifications[cur].subscription.channelName !== latest.channel_name || // if (
notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING // notifications[cur].subscription.channelName !== latest.channel_name ||
) { // notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING
newNotifications[cur] = { ...notifications[cur] }; // ) {
} // newNotifications[cur] = { ...notifications[cur] };
}); // }
dispatch(setSubscriptionNotifications(newNotifications)); // });
// dispatch(setSubscriptionNotifications(newNotifications));
} }
dispatch({ dispatch({

View file

@ -40,11 +40,13 @@ export function doClaimRewardType(rewardType, options) {
} }
if (!userIsRewardApproved && rewardType !== rewards.TYPE_CONFIRM_EMAIL) { if (!userIsRewardApproved && rewardType !== rewards.TYPE_CONFIRM_EMAIL) {
const action = doNotify({ if (!options || !options.failSilently) {
id: MODALS.REWARD_APPROVAL_REQUIRED, const action = doNotify({
isError: false, id: MODALS.REWARD_APPROVAL_REQUIRED,
}); isError: false,
dispatch(action); });
dispatch(action);
}
return; return;
} }

View file

@ -1,19 +1,19 @@
// @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 * as NOTIFICATION_TYPES from 'constants/notification_types';
import type { import * as SETTINGS from 'constants/settings';
Dispatch, import rewards from 'rewards';
SubscriptionState, import type { Dispatch, SubscriptionNotifications } from 'redux/reducers/subscriptions';
SubscriptionNotifications,
} from 'redux/reducers/subscriptions';
import type { Subscription } from 'types/subscription'; import type { Subscription } from 'types/subscription';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, buildURI, parseURI } from 'lbry-redux'; import { Lbry, buildURI, parseURI } from 'lbry-redux';
import { doPurchaseUri } from 'redux/actions/content'; import { doPurchaseUri } from 'redux/actions/content';
import { doClaimRewardType } from 'redux/actions/rewards';
import Promise from 'bluebird'; import Promise from 'bluebird';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000; const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () => any) => { export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
@ -124,63 +124,83 @@ export const setSubscriptionNotification = (
}, },
}); });
export const doCheckSubscription = (subscription: Subscription, notify?: boolean) => ( export const doCheckSubscription = (subscriptionUri: string, notify?: boolean) => (
dispatch: Dispatch dispatch: Dispatch,
getState: () => {}
) => { ) => {
dispatch({ // no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
data: subscription,
});
Lbry.claim_list_by_channel({ uri: subscription.uri, page: 1 }).then(result => { const state = getState();
const claimResult = result[subscription.uri] || {}; const savedSubscription = state.subscriptions.subscriptions.find(
sub => sub.uri === subscriptionUri
);
Lbry.claim_list_by_channel({ uri: subscriptionUri, page: 1 }).then(result => {
const claimResult = result[subscriptionUri] || {};
const { claims_in_channel: claimsInChannel } = claimResult; const { claims_in_channel: claimsInChannel } = claimResult;
if (claimsInChannel) { const latestIndex = claimsInChannel.findIndex(
if (notify) { claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
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);
}
dispatch( // if latest is 0, nothing has changed
setSubscriptionLatest( // when there is no subscription latest, it is either a newly subscriubed channel or
{ // the user has cleared their cache. Either way, do not download or notify about new content
channelName: claimsInChannel[0].channel_name, // as that would download/notify 10 claims per channel
uri: buildURI( if (claimsInChannel.length && latestIndex !== 0 && savedSubscription.latest) {
{ let downloadCount = 0;
channelName: claimsInChannel[0].channel_name, claimsInChannel.slice(0, latestIndex === -1 ? 10 : latestIndex).forEach(claim => {
claimId: claimsInChannel[0].claim_id, const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, false);
}, const shouldDownload = Boolean(
false downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT &&
), !claim.value.stream.metadata.fee &&
}, makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state)
buildURI( );
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, if (notify) {
false dispatch(
) setSubscriptionNotification(
) savedSubscription,
); uri,
shouldDownload ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
)
);
}
if (shouldDownload) {
downloadCount += 1;
dispatch(doPurchaseUri(uri, { cost: 0 }));
}
});
} }
// always setLatest; important for newly subscribed channels
dispatch(
setSubscriptionLatest(
{
channelName: claimsInChannel[0].channel_name,
uri: buildURI(
{
channelName: claimsInChannel[0].channel_name,
claimId: claimsInChannel[0].claim_id,
},
false
),
},
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
)
)
);
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
// means it will delete a non-existant fetchingChannelClaims[uri]
dispatch({ dispatch({
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED, type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: subscription, data: {
uri: subscriptionUri,
claims: claimsInChannel || [],
page: 1,
},
}); });
}); });
}; };
@ -217,8 +237,11 @@ export const doChannelSubscribe = (subscription: Subscription) => (
channel_name: subscription.channelName, channel_name: subscription.channelName,
claim_id: claimId, claim_id: claimId,
}); });
dispatch(doClaimRewardType(rewards.SUBSCRIPTION, { failSilently: true }));
} }
// should be subUri
dispatch(doCheckSubscription(subscription, true)); dispatch(doCheckSubscription(subscription, true));
}; };
@ -244,15 +267,22 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (
} }
}; };
export const doCheckSubscriptions = () => ( export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
dispatch: Dispatch, const state = getState();
getState: () => SubscriptionState 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 <PersistGate> which reqiures a package upgrade
setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
setTimeout(() => dispatch(doCheckSubscriptions()), 10000);
const checkSubscriptionsTimer = setInterval( const checkSubscriptionsTimer = setInterval(
() => () => dispatch(doCheckSubscriptions()),
selectSubscriptions(getState()).map((subscription: Subscription) =>
dispatch(doCheckSubscription(subscription, true))
),
CHECK_SUBSCRIPTIONS_INTERVAL CHECK_SUBSCRIPTIONS_INTERVAL
); );
dispatch({ dispatch({

View file

@ -26,6 +26,7 @@ const defaultState = {
automaticDarkModeEnabled: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false), automaticDarkModeEnabled: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
autoplay: getLocalStorageSetting(SETTINGS.AUTOPLAY, false), autoplay: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
resultCount: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)), resultCount: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)),
autoDownload: getLocalStorageSetting(SETTINGS.AUTO_DOWNLOAD, true),
osNotificationsEnabled: Boolean( osNotificationsEnabled: Boolean(
getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true) getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true)
), ),

View file

@ -25,7 +25,7 @@ export const selectSubscriptionClaims = createSelector(
return []; return [];
} }
const fetchedSubscriptions = []; let fetchedSubscriptions = [];
savedSubscriptions.forEach(subscription => { savedSubscriptions.forEach(subscription => {
let channelClaims = []; let channelClaims = [];
@ -39,18 +39,20 @@ export const selectSubscriptionClaims = createSelector(
// loop over the list of ids and grab the claim // loop over the list of ids and grab the claim
pageOneChannelIds.forEach(id => { pageOneChannelIds.forEach(id => {
const grabbedClaim = allClaims[id]; const grabbedClaim = allClaims[id];
channelClaims.push(grabbedClaim); channelClaims = channelClaims.concat([grabbedClaim]);
}); });
} }
fetchedSubscriptions.push({ fetchedSubscriptions = fetchedSubscriptions.concat([
claims: channelClaims, {
channelName: subscription.channelName, claims: [...channelClaims],
uri: subscription.uri, channelName: subscription.channelName,
}); uri: subscription.uri,
},
]);
}); });
return fetchedSubscriptions; return [...fetchedSubscriptions];
} }
); );

View file

@ -13,6 +13,7 @@ rewards.TYPE_FIRST_PUBLISH = 'first_publish';
rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download'; rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download';
rewards.TYPE_REFERRAL = 'referral'; rewards.TYPE_REFERRAL = 'referral';
rewards.YOUTUBE_CREATOR = 'youtube_creator'; rewards.YOUTUBE_CREATOR = 'youtube_creator';
rewards.SUBSCRIPTION = 'subscription';
rewards.claimReward = type => { rewards.claimReward = type => {
function requestReward(resolve, reject, params) { function requestReward(resolve, reject, params) {