Subscriptions overhaul #1872

Merged
daovist merged 13 commits from subscriptions-overhaul into master 2018-08-20 18:36:23 +02:00
14 changed files with 168 additions and 116 deletions

View file

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

View file

@ -15,3 +15,4 @@ export const AUTOMATIC_DARK_MODE_ENABLED = 'automaticDarkModeEnabled';
export const AUTOPLAY = 'autoplay';
export const RESULT_COUNT = 'resultCount';
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)),
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
checkSubscription: subscription => dispatch(doCheckSubscription(subscription)),
checkSubscription: uri => dispatch(doCheckSubscription(uri)),
openModal: (modal, props) => dispatch(doNotify(modal, props)),
prepareEdit: (publishData, uri) => dispatch(doPrepareEdit(publishData, uri)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),

View file

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

View file

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

View file

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

View file

@ -25,6 +25,9 @@ export default class extends React.PureComponent<Props> {
const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props;
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 = {};
Object.keys(notifications).forEach(cur => {
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 { doAuthenticate } from 'redux/actions/user';
import { doPause } from 'redux/actions/media';
import { doCheckSubscriptions } from 'redux/actions/subscriptions';
import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions';
import {
selectIsUpgradeSkipped,
selectUpdateUrl,
@ -347,7 +347,7 @@ export function doDaemonReady() {
dispatch(doCheckUpgradeAvailable());
}
dispatch(doCheckUpgradeSubscribe());
dispatch(doCheckSubscriptions());
dispatch(doCheckSubscriptionsInit());
};
}

View file

@ -110,6 +110,7 @@ export function doUpdateLoadStatus(uri, outpoint) {
dispatch(doUpdateLoadStatus(uri, outpoint));
}, DOWNLOAD_POLL_INTERVAL);
} else if (fileInfo.completed) {
const state = getState();
// TODO this isn't going to get called if they reload the client before
// the download finished
dispatch({
@ -121,13 +122,13 @@ export function doUpdateLoadStatus(uri, outpoint) {
},
});
const badgeNumber = selectBadgeNumber(getState());
const badgeNumber = selectBadgeNumber(state);
setBadge(badgeNumber === 0 ? '' : `${badgeNumber}`);
const totalProgress = selectTotalDownloadProgress(getState());
const totalProgress = selectTotalDownloadProgress(state);
setProgressBar(totalProgress);
const notifications = selectNotifications(getState());
const notifications = selectNotifications(state);
if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) {
const count = Object.keys(notifications).reduce(
(acc, cur) =>
@ -138,7 +139,7 @@ export function doUpdateLoadStatus(uri, outpoint) {
0
neb-b commented 2018-08-14 02:35:36 +02:00 (Migrated from github.com)
Review

I think the idea behind snackbars is that they are reserved for user actions, which wouldn't make sense for this use case. Maybe we should create our own notification component that mimics desktop notifications but has some additional style.

Not needed for this PR but I think we should stick with the OS notifications for now.

I think the idea behind snackbars is that they are reserved for user actions, which wouldn't make sense for this use case. Maybe we should create our own notification component that mimics desktop notifications but has some additional style. Not needed for this PR but I think we should stick with the OS notifications for now.
);
if (selectosNotificationsEnabled(getState())) {
if (selectosNotificationsEnabled(state)) {
const notif = new window.Notification(notifications[uri].subscription.channelName, {
body: `Posted ${fileInfo.metadata.title}${
count > 1 && count < 10 ? ` and ${count - 1} other new items` : ''
@ -153,13 +154,15 @@ export function doUpdateLoadStatus(uri, outpoint) {
);
};
}
dispatch(
setSubscriptionNotification(
notifications[uri].subscription,
uri,
NOTIFICATION_TYPES.DOWNLOADED
)
);
if (state.navigation.currentPath !== '/subscriptions') {
dispatch(
setSubscriptionNotification(
notifications[uri].subscription,
uri,
NOTIFICATION_TYPES.DOWNLOADED
)
);
}
} else {
// If notifications are disabled(false) just return
if (!selectosNotificationsEnabled(getState())) return;
@ -376,17 +379,18 @@ 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));
// commented out as a note for @sean, notification will be clared individually
// 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({

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ export const selectSubscriptionClaims = createSelector(
return [];
}
const fetchedSubscriptions = [];
let fetchedSubscriptions = [];
savedSubscriptions.forEach(subscription => {
let channelClaims = [];
@ -39,18 +39,20 @@ export const selectSubscriptionClaims = createSelector(
// loop over the list of ids and grab the claim
pageOneChannelIds.forEach(id => {
const grabbedClaim = allClaims[id];
channelClaims.push(grabbedClaim);
channelClaims = channelClaims.concat([grabbedClaim]);
});
}
fetchedSubscriptions.push({
claims: channelClaims,
channelName: subscription.channelName,
uri: subscription.uri,
});
fetchedSubscriptions = fetchedSubscriptions.concat([
{
claims: [...channelClaims],
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_REFERRAL = 'referral';
rewards.YOUTUBE_CREATOR = 'youtube_creator';
rewards.SUBSCRIPTION = 'subscription';
rewards.claimReward = type => {
function requestReward(resolve, reject, params) {