lbry-desktop/src/ui/redux/actions/subscriptions.js

385 lines
12 KiB
JavaScript
Raw Normal View History

2017-12-08 21:14:35 +01:00
// @flow
import { PAGE_SIZE } from 'constants/claim';
import * as ACTIONS from 'constants/action_types';
import * as SETTINGS from 'constants/settings';
2018-10-19 22:38:07 +02:00
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
2018-09-24 05:44:42 +02:00
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
2018-10-19 22:38:07 +02:00
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, parseURI, doResolveUris } from 'lbry-redux';
import { doPlayUri } from 'redux/actions/content';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
2017-12-08 21:14:35 +01:00
2019-03-18 06:09:50 +01:00
export const doSetViewMode = (viewMode: ViewMode) => (dispatch: Dispatch) =>
2018-10-19 22:38:07 +02:00
dispatch({
type: ACTIONS.SET_VIEW_MODE,
data: viewMode,
});
2019-03-18 06:09:50 +01:00
export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: GetState) => {
2018-10-19 22:38:07 +02:00
const state: { subscriptions: SubscriptionState, settings: any } = getState();
const { subscriptions: reduxSubscriptions } = state.subscriptions;
const { share_usage_data: shareSetting } = state.settings.daemonSettings;
const isSharingData = shareSetting || IS_WEB;
2017-12-08 21:14:35 +01:00
if (!isSharingData && isSharingData !== undefined) {
// They aren't sharing their data, subscriptions will be handled by persisted redux state
return;
}
// 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 || [];
2019-06-11 20:10:58 +02:00
// // 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 reduxSubMap = {};
const subscriptionsToReturn = reduxSubscriptions.slice();
reduxSubscriptions.forEach(sub => {
const { channelClaimId } = parseURI(sub.uri);
reduxSubMap[channelClaimId] = 1;
});
storedSubscriptions.forEach(sub => {
if (!reduxSubMap[sub.claim_id]) {
const uri = `lbry://${sub.channel_name}#${sub.claim_id}`;
subscriptionsToReturn.push({ uri, channelName: sub.channel_name });
}
});
2019-06-11 20:10:58 +02:00
return subscriptionsToReturn;
}
// DB is already synced, just return the subscriptions in redux
return reduxSubscriptions;
})
2018-10-19 22:38:07 +02:00
.then((subscriptions: Array<Subscription>) => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: subscriptions,
});
dispatch(doResolveUris(subscriptions.map(({ uri }) => uri)));
dispatch(doCheckSubscriptions());
})
.catch(() => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL,
});
});
2018-03-16 19:26:44 +01:00
};
2019-05-07 23:38:29 +02:00
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (dispatch: Dispatch) =>
2017-12-13 22:36:30 +01:00
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription,
uri,
},
2017-12-08 21:14:35 +01:00
});
2018-10-19 22:38:07 +02:00
// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
channelUri: string,
uris: ?Array<string>,
type: ?SubscriptionNotificationType
2019-03-18 06:09:50 +01:00
) => (dispatch: Dispatch, getState: GetState) => {
2018-10-19 22:38:07 +02:00
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
const currentUnreadForChannel: UnreadSubscription = unreadByChannel[channelUri];
let newUris;
let newType;
if (!currentUnreadForChannel) {
newUris = uris;
newType = type;
} else {
if (uris) {
// If a channel currently has no unread uris, just add them all
if (!currentUnreadForChannel.uris || !currentUnreadForChannel.uris.length) {
newUris = uris;
} else {
// They already have unreads and now there are new ones
// Add the new ones to the beginning of the list
// Make sure there are no duplicates
const currentUnreadUris = currentUnreadForChannel.uris;
newUris = uris.filter(uri => !currentUnreadUris.includes(uri)).concat(currentUnreadUris);
}
} else {
newUris = currentUnreadForChannel.uris;
}
newType = type || currentUnreadForChannel.type;
}
dispatch({
2018-10-19 22:38:07 +02:00
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
2018-10-19 22:38:07 +02:00
channel: channelUri,
uris: newUris,
type: newType,
},
});
2018-10-19 22:38:07 +02:00
};
// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: ?string, readUris: ?Array<string>) => (
2019-03-18 06:09:50 +01:00
dispatch: Dispatch,
2018-10-19 22:38:07 +02:00
getState: GetState
) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
// If no channel is passed in, remove all unread subscriptions from all channels
if (!channelUri) {
return dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: { channel: null },
});
}
2018-10-19 22:38:07 +02:00
const currentChannelUnread = unreadByChannel[channelUri];
if (!currentChannelUnread || !currentChannelUnread.uris) {
// Channel passed in doesn't have any unreads
2018-10-19 22:38:07 +02:00
return;
}
// For each uri passed in, remove it from the list of unread uris
// If no uris are passed in, remove them all
let newUris;
if (readUris) {
const urisToRemoveMap = readUris.reduce(
(acc, val) => ({
...acc,
[val]: true,
}),
{}
);
const filteredUris = currentChannelUnread.uris.filter(uri => !urisToRemoveMap[uri]);
newUris = filteredUris.length ? filteredUris : null;
} else {
newUris = null;
}
2018-10-19 22:38:07 +02:00
dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
},
});
};
// Remove a single file from a channels unread subscriptions
2019-05-07 23:38:29 +02:00
export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) => (dispatch: Dispatch) => {
2018-10-19 22:38:07 +02:00
dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};
2018-10-30 18:07:55 +01:00
export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => (
2019-03-18 06:09:50 +01:00
dispatch: Dispatch,
2018-10-19 22:38:07 +02:00
getState: GetState
) => {
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
2019-08-06 05:48:41 +02:00
const state = getState();
2018-10-19 22:38:07 +02:00
const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
2019-05-07 23:38:29 +02:00
const savedSubscription = state.subscriptions.subscriptions.find(sub => sub.uri === subscriptionUri);
2019-08-06 05:48:41 +02:00
2018-10-19 22:38:07 +02:00
if (!savedSubscription) {
2019-05-07 23:38:29 +02:00
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
2018-10-19 22:38:07 +02:00
}
2019-08-30 21:11:04 +02:00
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED,
data: {
uri: subscriptionUri,
page: 1,
},
});
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
2019-06-11 20:15:36 +02:00
Lbry.claim_search({
channel: subscriptionUri,
valid_channel_signature: true,
2019-06-11 20:15:36 +02:00
order_by: ['release_time'],
page: 1,
page_size: PAGE_SIZE,
}).then(claimListByChannel => {
const { items: claimsInChannel } = claimListByChannel;
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
// may happen if subscribed to an abandoned channel or an empty channel
if (!claimsInChannel || !claimsInChannel.length) {
return;
}
2019-08-06 05:48:41 +02:00
2019-08-30 21:11:04 +02:00
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: {
uri: subscriptionUri,
claims: claimsInChannel || [],
page: 1,
},
});
2019-06-11 20:15:36 +02:00
// Determine if the latest subscription currently saved is actually the latest subscription
const latestIndex = claimsInChannel.findIndex(claim => claim.permanent_url === savedSubscription.latest);
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
// If latest is 0, nothing has changed
// Do not download/notify about new content, it would download/notify 10 claims per channel
if (latestIndex !== 0 && savedSubscription.latest) {
let downloadCount = 0;
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
const newUnread = [];
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
2019-08-30 21:11:04 +02:00
const uri = claim.canonical_url;
2019-06-11 20:15:36 +02:00
const shouldDownload =
shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
// Add the new content to the list of "un-read" subscriptions
if (shouldNotify) {
newUnread.push(uri);
}
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
if (shouldDownload) {
downloadCount += 1;
// this fails since something is not resolved/saved somewhere...
dispatch(doPlayUri(uri, true, true));
2019-06-11 20:15:36 +02:00
}
});
2019-08-06 05:48:41 +02:00
2018-10-30 18:07:55 +01:00
dispatch(
2019-06-11 20:15:36 +02:00
doUpdateUnreadSubscriptions(
subscriptionUri,
newUnread,
downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
2018-10-30 18:07:55 +01:00
)
);
2019-06-11 20:15:36 +02:00
}
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
// Set the latest piece of content for a channel
// This allows the app to know if there has been new content since it was last set
2019-08-28 01:36:41 +02:00
const latest = claimsInChannel[0];
2019-06-11 20:15:36 +02:00
dispatch(
setSubscriptionLatest(
{
channelName: latest.signing_channel.name,
uri: latest.signing_channel.permanent_url,
2018-10-30 18:07:55 +01:00
},
latest.permanent_url
2019-06-11 20:15:36 +02:00
)
);
2019-08-06 05:48:41 +02:00
2019-06-11 20:15:36 +02:00
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
2019-10-13 19:41:51 +02:00
// means it will delete a non-existent fetchingChannelClaims[uri]
2019-06-11 20:15:36 +02:00
});
2018-10-19 22:38:07 +02:00
};
2019-05-07 23:38:29 +02:00
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
const {
settings: { daemonSettings },
} = getState();
const { share_usage_data: shareSetting } = daemonSettings;
const isSharingData = shareSetting || IS_WEB;
2018-10-31 18:08:30 +01:00
const subscriptionUri = subscription.uri;
if (!subscriptionUri.startsWith('lbry://')) {
2019-10-13 19:41:51 +02:00
throw Error(`Subscription uris must include the "lbry://" prefix.\nTried to subscribe to ${subscriptionUri}`);
2018-10-31 18:08:30 +01:00
}
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: subscription,
});
// if the user isn't sharing data, keep the subscriptions entirely in the app
if (isSharingData || IS_WEB) {
const { channelClaimId } = 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: channelClaimId,
});
2018-09-24 05:44:42 +02:00
dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true }));
}
2018-08-28 23:20:11 +02:00
dispatch(doCheckSubscription(subscription.uri, true));
};
2019-05-07 23:38:29 +02:00
export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
const {
settings: { daemonSettings },
} = getState();
const { share_usage_data: shareSetting } = daemonSettings;
const isSharingData = shareSetting || IS_WEB;
dispatch({
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: subscription,
});
if (isSharingData) {
const { channelClaimId } = parseURI(subscription.uri);
Lbryio.call('subscription', 'delete', {
claim_id: channelClaimId,
});
}
};
2019-03-18 06:09:50 +01:00
export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const subscriptions = selectSubscriptions(state);
2018-10-19 22:38:07 +02:00
subscriptions.forEach((sub: Subscription) => {
2018-08-20 16:01:32 +02:00
dispatch(doCheckSubscription(sub.uri, true));
});
};
2019-03-18 06:09:50 +01:00
export const doCheckSubscriptionsInit = () => (dispatch: Dispatch) => {
// doCheckSubscriptionsInit is called by doDaemonReady
// setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked
2019-10-13 19:41:51 +02:00
// this will be replaced with <PersistGate> which requires a package upgrade
setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
2019-05-07 23:38:29 +02:00
const checkSubscriptionsTimer = setInterval(() => dispatch(doCheckSubscriptions()), CHECK_SUBSCRIPTIONS_INTERVAL);
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
data: { checkSubscriptionsTimer },
});
};
2018-11-21 22:20:55 +01:00
2019-03-18 06:09:50 +01:00
export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => {
2018-11-21 22:20:55 +01:00
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
});
return Lbryio.call('subscription', 'suggest')
.then(suggested =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS,
data: suggested,
})
)
.catch(error =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL,
error,
})
);
};