2017-12-08 21:14:35 +01:00
|
|
|
// @flow
|
2017-12-21 18:32:51 +01:00
|
|
|
import * as ACTIONS from 'constants/action_types';
|
2018-03-26 09:31:52 +02:00
|
|
|
import * as NOTIFICATION_TYPES from 'constants/notification_types';
|
2018-08-10 16:51:51 +02:00
|
|
|
import * as SETTINGS from 'constants/settings';
|
2018-08-09 13:45:07 +02:00
|
|
|
import rewards from 'rewards';
|
2018-08-10 16:51:51 +02:00
|
|
|
import type { Dispatch, SubscriptionNotifications } from 'redux/reducers/subscriptions';
|
2018-05-07 06:50:55 +02:00
|
|
|
import type { Subscription } from 'types/subscription';
|
2018-08-14 00:04:49 +02:00
|
|
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
2018-08-10 16:51:51 +02:00
|
|
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
2018-08-28 06:19:26 +02:00
|
|
|
import { Lbry, buildURI, parseURI, selectCurrentPage } from 'lbry-redux';
|
2018-08-22 05:59:06 +02:00
|
|
|
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content';
|
2018-08-09 13:45:07 +02:00
|
|
|
import { doClaimRewardType } from 'redux/actions/rewards';
|
2018-05-07 06:50:55 +02:00
|
|
|
import Promise from 'bluebird';
|
|
|
|
import Lbryio from 'lbryio';
|
2018-03-06 08:44:36 +01:00
|
|
|
|
2018-08-08 20:23:26 +02:00
|
|
|
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
|
2018-08-14 00:04:49 +02:00
|
|
|
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
|
2017-12-08 21:14:35 +01:00
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
|
|
|
|
const {
|
|
|
|
subscriptions: subscriptionState,
|
|
|
|
settings: { daemonSettings },
|
|
|
|
} = getState();
|
|
|
|
const { subscriptions: reduxSubscriptions } = subscriptionState;
|
|
|
|
const { share_usage_data: isSharingData } = daemonSettings;
|
2017-12-08 21:14:35 +01:00
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
if (!isSharingData && isSharingData !== undefined) {
|
|
|
|
// They aren't sharing their data, subscriptions will be handled by persisted redux state
|
|
|
|
return;
|
|
|
|
}
|
2018-03-16 19:22:19 +01:00
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
// most of this logic comes from scenarios where the db isn't synced with redux
|
|
|
|
// this will happen if the user stops sharing data
|
|
|
|
dispatch({ type: ACTIONS.FETCH_SUBSCRIPTIONS_START });
|
|
|
|
|
|
|
|
Lbryio.call('subscription', 'list')
|
|
|
|
.then(dbSubscriptions => {
|
|
|
|
const storedSubscriptions = dbSubscriptions || [];
|
|
|
|
|
|
|
|
// User has no subscriptions in db or redux
|
|
|
|
if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// There is some mismatch between redux state and db state
|
|
|
|
// If something is in the db, but not in redux, add it to redux
|
|
|
|
// If something is in redux, but not in the db, add it to the db
|
|
|
|
if (storedSubscriptions.length !== reduxSubscriptions.length) {
|
|
|
|
const dbSubMap = {};
|
|
|
|
const reduxSubMap = {};
|
|
|
|
const subsNotInDB = [];
|
|
|
|
const subscriptionsToReturn = reduxSubscriptions.slice();
|
|
|
|
|
|
|
|
storedSubscriptions.forEach(sub => {
|
|
|
|
dbSubMap[sub.claim_id] = 1;
|
|
|
|
});
|
|
|
|
|
|
|
|
reduxSubscriptions.forEach(sub => {
|
|
|
|
const { claimId } = parseURI(sub.uri);
|
|
|
|
reduxSubMap[claimId] = 1;
|
|
|
|
|
|
|
|
if (!dbSubMap[claimId]) {
|
|
|
|
subsNotInDB.push({
|
|
|
|
claim_id: claimId,
|
|
|
|
channel_name: sub.channelName,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
storedSubscriptions.forEach(sub => {
|
|
|
|
if (!reduxSubMap[sub.claim_id]) {
|
|
|
|
const uri = `lbry://${sub.channel_name}#${sub.claim_id}`;
|
|
|
|
subscriptionsToReturn.push({ uri, channelName: sub.channel_name });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload)))
|
|
|
|
.then(() => subscriptionsToReturn)
|
|
|
|
.catch(
|
|
|
|
() =>
|
|
|
|
// let it fail, we will try again when the navigate to the subscriptions page
|
|
|
|
subscriptionsToReturn
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// DB is already synced, just return the subscriptions in redux
|
|
|
|
return reduxSubscriptions;
|
|
|
|
})
|
|
|
|
.then(subscriptions => {
|
|
|
|
dispatch({
|
|
|
|
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
|
|
|
|
data: subscriptions,
|
|
|
|
});
|
2018-08-22 05:59:06 +02:00
|
|
|
|
|
|
|
subscriptions.forEach(({ uri }) => dispatch(doFetchClaimsByChannel(uri)));
|
2018-05-07 06:50:55 +02:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
dispatch({
|
|
|
|
type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL,
|
|
|
|
});
|
|
|
|
});
|
2018-03-16 19:26:44 +01:00
|
|
|
};
|
2018-03-16 19:22:19 +01:00
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
|
|
|
|
dispatch: Dispatch
|
|
|
|
) =>
|
2017-12-13 22:36:30 +01:00
|
|
|
dispatch({
|
2018-05-07 06:50:55 +02:00
|
|
|
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
|
|
|
|
data: {
|
|
|
|
subscription,
|
|
|
|
uri,
|
|
|
|
},
|
2017-12-08 21:14:35 +01:00
|
|
|
});
|
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
export const setSubscriptionNotification = (
|
|
|
|
subscription: Subscription,
|
|
|
|
uri: string,
|
|
|
|
notificationType: string
|
|
|
|
) => (dispatch: Dispatch) =>
|
2018-03-06 08:44:36 +01:00
|
|
|
dispatch({
|
2018-05-07 06:50:55 +02:00
|
|
|
type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION,
|
|
|
|
data: {
|
|
|
|
subscription,
|
|
|
|
uri,
|
|
|
|
type: notificationType,
|
|
|
|
},
|
2018-03-06 08:44:36 +01:00
|
|
|
});
|
|
|
|
|
2018-08-16 18:41:50 +02:00
|
|
|
export const doCheckSubscription = (subscriptionUri: string, notify?: boolean) => (
|
2018-08-14 00:04:49 +02:00
|
|
|
dispatch: Dispatch,
|
|
|
|
getState: () => {}
|
|
|
|
) => {
|
2018-08-10 16:51:51 +02:00
|
|
|
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
|
2018-03-06 08:44:36 +01:00
|
|
|
|
2018-08-16 18:41:50 +02:00
|
|
|
const state = getState();
|
2018-08-28 06:19:26 +02:00
|
|
|
const currentPage = selectCurrentPage(state);
|
2018-08-20 16:01:32 +02:00
|
|
|
const savedSubscription = state.subscriptions.subscriptions.find(
|
|
|
|
sub => sub.uri === subscriptionUri
|
|
|
|
);
|
2018-08-16 18:41:50 +02:00
|
|
|
|
|
|
|
Lbry.claim_list_by_channel({ uri: subscriptionUri, page: 1 }).then(result => {
|
|
|
|
const claimResult = result[subscriptionUri] || {};
|
2018-03-06 08:44:36 +01:00
|
|
|
const { claims_in_channel: claimsInChannel } = claimResult;
|
|
|
|
|
2018-08-28 06:19:26 +02:00
|
|
|
// may happen if subscribed to an abandoned channel or an empty channel
|
|
|
|
if (!claimsInChannel) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-08 20:23:26 +02:00
|
|
|
const latestIndex = claimsInChannel.findIndex(
|
2018-08-16 18:41:50 +02:00
|
|
|
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
|
2018-08-08 20:23:26 +02:00
|
|
|
);
|
2018-03-06 08:44:36 +01:00
|
|
|
|
2018-08-16 18:41:50 +02:00
|
|
|
// 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) {
|
2018-08-14 00:04:49 +02:00
|
|
|
let downloadCount = 0;
|
2018-08-10 16:51:51 +02:00
|
|
|
claimsInChannel.slice(0, latestIndex === -1 ? 10 : latestIndex).forEach(claim => {
|
2018-08-08 20:23:26 +02:00
|
|
|
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, false);
|
|
|
|
const shouldDownload = Boolean(
|
2018-08-14 00:04:49 +02:00
|
|
|
downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT &&
|
2018-08-10 16:51:51 +02:00
|
|
|
!claim.value.stream.metadata.fee &&
|
|
|
|
makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state)
|
2018-08-08 20:23:26 +02:00
|
|
|
);
|
2018-08-28 06:19:26 +02:00
|
|
|
if (notify && currentPage !== 'subscriptions') {
|
2018-08-08 20:23:26 +02:00
|
|
|
dispatch(
|
|
|
|
setSubscriptionNotification(
|
2018-08-16 18:41:50 +02:00
|
|
|
savedSubscription,
|
2018-08-08 20:23:26 +02:00
|
|
|
uri,
|
|
|
|
shouldDownload ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2018-08-10 16:51:51 +02:00
|
|
|
if (shouldDownload) {
|
2018-08-14 00:04:49 +02:00
|
|
|
downloadCount += 1;
|
2018-09-12 21:50:04 +02:00
|
|
|
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
|
2018-08-08 20:23:26 +02:00
|
|
|
}
|
|
|
|
});
|
2018-08-16 18:41:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
},
|
2018-03-15 09:35:47 +01:00
|
|
|
false
|
2018-08-16 18:41:50 +02:00
|
|
|
),
|
|
|
|
},
|
|
|
|
buildURI(
|
|
|
|
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
|
|
|
|
false
|
2018-03-15 09:35:47 +01:00
|
|
|
)
|
2018-08-16 18:41:50 +02:00
|
|
|
)
|
|
|
|
);
|
2018-03-15 09:34:22 +01:00
|
|
|
|
2018-08-16 18:41:50 +02:00
|
|
|
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
|
|
|
|
// means it will delete a non-existant fetchingChannelClaims[uri]
|
|
|
|
dispatch({
|
|
|
|
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
|
|
|
|
data: {
|
|
|
|
uri: subscriptionUri,
|
|
|
|
claims: claimsInChannel || [],
|
|
|
|
page: 1,
|
|
|
|
},
|
|
|
|
});
|
2018-03-06 08:44:36 +01:00
|
|
|
});
|
2018-03-06 09:36:04 +01:00
|
|
|
};
|
2018-03-06 08:44:36 +01:00
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
export const setSubscriptionNotifications = (notifications: SubscriptionNotifications) => (
|
2018-03-06 09:36:04 +01:00
|
|
|
dispatch: Dispatch
|
|
|
|
) =>
|
|
|
|
dispatch({
|
2018-05-07 06:50:55 +02:00
|
|
|
type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS,
|
2018-03-06 01:28:11 +01:00
|
|
|
data: {
|
2018-05-07 06:50:55 +02:00
|
|
|
notifications,
|
2018-03-06 09:36:04 +01:00
|
|
|
},
|
2018-03-06 09:32:58 +01:00
|
|
|
});
|
2018-03-06 01:28:11 +01:00
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
export const doChannelSubscribe = (subscription: Subscription) => (
|
|
|
|
dispatch: Dispatch,
|
|
|
|
getState: () => any
|
|
|
|
) => {
|
|
|
|
const {
|
|
|
|
settings: { daemonSettings },
|
|
|
|
} = getState();
|
|
|
|
const { share_usage_data: isSharingData } = daemonSettings;
|
|
|
|
|
2018-03-26 09:31:52 +02:00
|
|
|
dispatch({
|
2018-05-07 06:50:55 +02:00
|
|
|
type: ACTIONS.CHANNEL_SUBSCRIBE,
|
|
|
|
data: subscription,
|
2018-03-26 09:31:52 +02:00
|
|
|
});
|
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
// if the user isn't sharing data, keep the subscriptions entirely in the app
|
|
|
|
if (isSharingData) {
|
|
|
|
const { claimId } = parseURI(subscription.uri);
|
|
|
|
// They are sharing data, we can store their subscriptions in our internal database
|
|
|
|
Lbryio.call('subscription', 'new', {
|
|
|
|
channel_name: subscription.channelName,
|
|
|
|
claim_id: claimId,
|
|
|
|
});
|
2018-08-09 13:45:07 +02:00
|
|
|
|
|
|
|
dispatch(doClaimRewardType(rewards.SUBSCRIPTION, { failSilently: true }));
|
2018-05-07 06:50:55 +02:00
|
|
|
}
|
|
|
|
|
2018-08-28 23:20:11 +02:00
|
|
|
dispatch(doCheckSubscription(subscription.uri, true));
|
2018-05-07 06:50:55 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
export const doChannelUnsubscribe = (subscription: Subscription) => (
|
|
|
|
dispatch: Dispatch,
|
|
|
|
getState: () => any
|
|
|
|
) => {
|
|
|
|
const {
|
|
|
|
settings: { daemonSettings },
|
|
|
|
} = getState();
|
|
|
|
const { share_usage_data: isSharingData } = daemonSettings;
|
|
|
|
|
2018-03-26 09:31:52 +02:00
|
|
|
dispatch({
|
2018-05-07 06:50:55 +02:00
|
|
|
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
|
|
|
|
data: subscription,
|
2018-03-26 09:31:52 +02:00
|
|
|
});
|
|
|
|
|
2018-05-07 06:50:55 +02:00
|
|
|
if (isSharingData) {
|
|
|
|
const { claimId } = parseURI(subscription.uri);
|
|
|
|
Lbryio.call('subscription', 'delete', {
|
|
|
|
claim_id: claimId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-08-10 16:51:51 +02:00
|
|
|
export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
|
|
|
|
const state = getState();
|
|
|
|
const subscriptions = selectSubscriptions(state);
|
|
|
|
subscriptions.forEach((sub: Subscription) => {
|
2018-08-20 16:01:32 +02:00
|
|
|
dispatch(doCheckSubscription(sub.uri, true));
|
2018-08-10 16:51:51 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
export const doCheckSubscriptionsInit = () => (dispatch: Dispatch) => {
|
2018-08-14 00:04:49 +02:00
|
|
|
// 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
|
2018-08-16 18:41:50 +02:00
|
|
|
setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
|
|
|
|
setTimeout(() => dispatch(doCheckSubscriptions()), 10000);
|
2018-08-10 16:51:51 +02:00
|
|
|
const checkSubscriptionsTimer = setInterval(
|
|
|
|
() => dispatch(doCheckSubscriptions()),
|
2018-08-20 18:20:53 +02:00
|
|
|
CHECK_SUBSCRIPTIONS_INTERVAL
|
2018-08-10 16:51:51 +02:00
|
|
|
);
|
2018-05-07 06:50:55 +02:00
|
|
|
dispatch({
|
|
|
|
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
|
|
|
|
data: { checkSubscriptionsTimer },
|
|
|
|
});
|
|
|
|
};
|