Subscribe notify #1066

Merged
liamcardenas merged 9 commits from subscribe-notify into master 2018-03-08 06:13:26 +01:00
10 changed files with 218 additions and 12 deletions

View file

@ -15,8 +15,8 @@ class FileList extends React.PureComponent {
this._sortFunctions = { this._sortFunctions = {
dateNew(fileInfos) { dateNew(fileInfos) {
return fileInfos.slice().sort((fileInfo1, fileInfo2) => { return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = fileInfo1.height const height1 = fileInfo1.height;
const height2 = fileInfo2.height const height2 = fileInfo2.height;
if (height1 > height2) { if (height1 > height2) {
return -1; return -1;
} else if (height1 < height2) { } else if (height1 < height2) {
@ -27,8 +27,8 @@ class FileList extends React.PureComponent {
}, },
dateOld(fileInfos) { dateOld(fileInfos) {
return fileInfos.slice().sort((fileInfo1, fileInfo2) => { return fileInfos.slice().sort((fileInfo1, fileInfo2) => {
const height1 = fileInfo1.height const height1 = fileInfo1.height;
const height2 = fileInfo2.height const height2 = fileInfo2.height;
if (height1 < height2) { if (height1 < height2) {
return -1; return -1;
} else if (height1 > height2) { } else if (height1 > height2) {

View file

@ -15,9 +15,9 @@ const RewardSummary = (props: Props) => {
<div className="card__title-primary"> <div className="card__title-primary">
<h3>{__('Rewards')}</h3> <h3>{__('Rewards')}</h3>
<p className="help"> <p className="help">
{__('Read our')}{' '} {__('Read our')} <Link href="https://lbry.io/faq/rewards">{__('FAQ')}</Link>{' '}
<Link href="https://lbry.io/faq/rewards">{__('FAQ')}</Link>{' '}{__('to learn more about LBRY Rewards')}. {__('to learn more about LBRY Rewards')}.
</p> </p>
</div> </div>
<div className="card__content"> <div className="card__content">
{unclaimedRewardAmount > 0 ? ( {unclaimedRewardAmount > 0 ? (

View file

@ -164,6 +164,10 @@ export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT';
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE'; 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 CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
// Video controls // Video controls
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';

View file

@ -63,7 +63,7 @@ ipcRenderer.on('window-is-focused', () => {
document.addEventListener('dragover', event => { document.addEventListener('dragover', event => {
event.preventDefault(); event.preventDefault();
}) });
document.addEventListener('drop', event => { document.addEventListener('drop', event => {
event.preventDefault(); event.preventDefault();
}); });

View file

@ -4,6 +4,7 @@ import { doFetchFileInfo } from 'redux/actions/file_info';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import { selectRewardContentClaimIds } from 'redux/selectors/content'; import { selectRewardContentClaimIds } from 'redux/selectors/content';
import { doFetchCostInfoForUri } from 'redux/actions/cost_info'; import { doFetchCostInfoForUri } from 'redux/actions/cost_info';
import { checkSubscriptionLatest } from 'redux/actions/subscriptions';
import { import {
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectContentTypeForUri, makeSelectContentTypeForUri,
@ -13,6 +14,7 @@ import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info';
import { selectShowNsfw } from 'redux/selectors/settings'; import { selectShowNsfw } from 'redux/selectors/settings';
import FilePage from './view'; import FilePage from './view';
import { makeSelectCurrentParam } from 'redux/selectors/navigation'; import { makeSelectCurrentParam } from 'redux/selectors/navigation';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
@ -23,12 +25,15 @@ const select = (state, props) => ({
tab: makeSelectCurrentParam('tab')(state), tab: makeSelectCurrentParam('tab')(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
subscriptions: selectSubscriptions(state),
}); });
const perform = dispatch => ({ 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)),
checkSubscriptionLatest: (subscription, uri) =>
dispatch(checkSubscriptionLatest(subscription, uri)),
}); });
export default connect(select, perform)(FilePage); export default connect(select, perform)(FilePage);

View file

@ -17,6 +17,7 @@ class FilePage extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.fetchFileInfo(this.props); this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props); this.fetchCostInfo(this.props);
this.checkSubscriptionLatest(this.props);
neb-b commented 2018-03-06 19:17:38 +01:00 (Migrated from github.com)
Review

You can avoid a lot of unnecessary calls by only calling checkSubscriptionLatest if you are subscribed to that channel

You can avoid a lot of unnecessary calls by only calling `checkSubscriptionLatest` if you are subscribed to that channel
liamcardenas commented 2018-03-07 20:29:46 +01:00 (Migrated from github.com)
Review

good call

good call
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -35,6 +36,28 @@ class FilePage extends React.PureComponent {
} }
} }
checkSubscriptionLatest(props) {
if (
props.subscriptions
.map(subscription => subscription.channelName)
.indexOf(props.claim.channel_name) !== -1
) {
props.checkSubscriptionLatest(
{
channelName: props.claim.channel_name,
uri: buildURI(
{
contentName: props.claim.channel_name,
claimId: props.claim.value.publisherSignature.certificateId,
},
false
),
},
buildURI({ contentName: props.claim.name, claimId: props.claim.claim_id }, false)
);
}
}
render() { render() {
const { const {
claim, claim,

View file

@ -11,6 +11,8 @@ import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthenticate } from 'redux/actions/user'; import { doAuthenticate } from 'redux/actions/user';
import { doBalanceSubscribe } from 'redux/actions/wallet'; import { doBalanceSubscribe } from 'redux/actions/wallet';
import { doPause } from 'redux/actions/media'; import { doPause } from 'redux/actions/media';
import { doCheckSubscriptions } from 'redux/actions/subscriptions';
import { import {
selectCurrentModal, selectCurrentModal,
selectIsUpgradeSkipped, selectIsUpgradeSkipped,
@ -253,6 +255,7 @@ export function doDaemonReady() {
dispatch(doCheckUpgradeAvailable()); dispatch(doCheckUpgradeAvailable());
} }
dispatch(doCheckUpgradeSubscribe()); dispatch(doCheckUpgradeSubscribe());
dispatch(doCheckSubscriptions());
}; };
} }

View file

@ -7,6 +7,7 @@ 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 { 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';
@ -288,7 +289,7 @@ export function doLoadVideo(uri) {
}; };
} }
export function doPurchaseUri(uri) { export function doPurchaseUri(uri, specificCostInfo) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const balance = selectBalance(state); const balance = selectBalance(state);
@ -321,7 +322,7 @@ export function doPurchaseUri(uri) {
return; return;
} }
const costInfo = makeSelectCostInfoForUri(uri)(state); const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo;
const { cost } = costInfo; const { cost } = costInfo;
if (cost > balance) { if (cost > balance) {
@ -358,6 +359,25 @@ export function doFetchClaimsByChannel(uri, page) {
const claimResult = result[uri] || {}; const claimResult = result[uri] || {};
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult; const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult;
if (claimsInChannel && claimsInChannel.length) {
const latest = claimsInChannel[0];
dispatch(
setSubscriptionLatest(
{
channelName: latest.channel_name,
uri: buildURI(
{
contentName: latest.channel_name,
claimId: latest.value.publisherSignature.certificateId,
},
false
),
},
buildURI({ contentName: latest.name, claimId: latest.claim_id }, false)
)
);
}
dispatch({ dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED, type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: { data: {

View file

@ -1,6 +1,13 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import type { Subscription, Dispatch } from 'redux/reducers/subscriptions'; import type { Subscription, Dispatch, SubscriptionState } from 'redux/reducers/subscriptions';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import Lbry from 'lbry';
import { doPurchaseUri } from 'redux/actions/content';
import { doNavigate } from 'redux/actions/navigation';
import { buildURI } from 'lbryURI';
const CHECK_SUBSCRIPTIONS_INTERVAL = 10 * 60 * 1000;
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) =>
dispatch({ dispatch({
@ -14,5 +21,113 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D
data: subscription, data: subscription,
}); });
export const doCheckSubscriptions = () => (
dispatch: Dispatch,
getState: () => SubscriptionState
) => {
const checkSubscriptionsTimer = setInterval(
() =>
selectSubscriptions(getState()).map((subscription: Subscription) =>
dispatch(doCheckSubscription(subscription))
),
CHECK_SUBSCRIPTIONS_INTERVAL
);
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
data: { checkSubscriptionsTimer },
});
};
export const doCheckSubscription = (subscription: Subscription) => (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
data: subscription,
});
Lbry.claim_list_by_channel({ uri: subscription.uri, page: 1 }).then(result => {
const claimResult = result[subscription.uri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
const count = subscription.latest
? claimsInChannel.reduce(
(prev, cur, index) =>
buildURI({ contentName: cur.name, claimId: cur.claim_id }, false) ===
subscription.latest
? index
: prev,
-1
)
: 1;
if (count !== 0) {
if (!claimsInChannel[0].value.stream.metadata.fee) {
dispatch(
doPurchaseUri(
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
),
{ cost: 0 }
)
);
}
const notif = new window.Notification(subscription.channelName, {
body: `Posted ${claimsInChannel[0].value.stream.metadata.title}${
count > 1 ? ` and ${count - 1} other new items` : ''
}${count < 0 ? ' and 9+ other new items' : ''}`,
silent: false,
});
notif.onclick = () => {
dispatch(
doNavigate('/show', {
uri: buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
true
),
})
);
};
}
//$FlowIssue
neb-b commented 2018-03-06 18:40:52 +01:00 (Migrated from github.com)
Review

I think this should have a case for count === 1, which is probably the most likely scenario. I think in that case just showing the title would make the most sense.

I think this should have a case for `count === 1`, which is probably the most likely scenario. I think in that case just showing the title would make the most sense.
liamcardenas commented 2018-03-07 20:26:06 +01:00 (Migrated from github.com)
Review

it does, it just says

"Sean"
"Posted myVideo"

instead of, for example,

"Sean"
"Posted myVideo and 9+ other new items"
it does, it just says ``` "Sean" "Posted myVideo" ``` instead of, for example, ``` "Sean" "Posted myVideo and 9+ other new items" ```
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
data: subscription,
});
});
};
export const checkSubscriptionLatest = (channel: Subscription, uri: string) => (
dispatch: Dispatch
) => {
Lbry.claim_list_by_channel({ uri: channel.uri, page: 1 }).then(result => {
const claimResult = result[channel.uri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
if (
claimsInChannel &&
claimsInChannel.length &&
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
) === uri
) {
dispatch(setSubscriptionLatest(channel, uri));
kauffj commented 2018-03-06 21:53:36 +01:00 (Migrated from github.com)
Review

Is this function actually necessary? Could doCheckSubscription just call setSubscriptionLatest?

Is this function actually necessary? Could `doCheckSubscription` just call `setSubscriptionLatest`?
liamcardenas commented 2018-03-07 20:34:27 +01:00 (Migrated from github.com)
Review

check and set are different, set simply sets the value in the redux store. Check does an asynchronous call and sets the value accordingly by dispatching set, if that makes sense.

check and set are different, set simply sets the value in the redux store. Check does an asynchronous call and sets the value accordingly by dispatching set, if that makes sense.
kauffj commented 2018-03-07 22:58:38 +01:00 (Migrated from github.com)
Review

I'm asking two things in my question:

  1. If we're already fetching the latest subscription data at time interval n, is it actually necessary to check for the latest for this same information when accessing a file page?
  2. If it is necessary (for staleness reasons, presumably), why can't more of the code be shared between the two? Isn't doCheckSubscription a superset of the functionality provided by checkSubscriptionLatest? and why wouldn't you want checkSubscriptionLatest to also begin downloads? Furthermore, why do both of these call Lbry.claim_list_by_channel when they could call doFetchClaimsByChannel instead?
I'm asking two things in my question: 1) If we're already fetching the latest subscription data at time interval `n`, is it actually necessary to check for the latest for this same information when accessing a file page? 2) If it is necessary (for staleness reasons, presumably), why can't more of the code be shared between the two? Isn't `doCheckSubscription` a superset of the functionality provided by `checkSubscriptionLatest`? and why wouldn't you want `checkSubscriptionLatest` to also begin downloads? Furthermore, why do _both_ of these call `Lbry.claim_list_by_channel` when they could call `doFetchClaimsByChannel` instead?
kauffj commented 2018-03-09 00:04:47 +01:00 (Migrated from github.com)
Review

@liamcardenas did you see above?

@liamcardenas did you see above?
}
});
};
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
dispatch: Dispatch
) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription,
uri,
},
});
export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) =>
dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS });

View file

@ -5,6 +5,7 @@ import { handleActions } from 'util/redux-utils';
export type Subscription = { export type Subscription = {
channelName: string, channelName: string,
uri: string, uri: string,
latest: ?string,
}; };
// Subscription redux types // Subscription redux types
@ -28,7 +29,30 @@ type HasFetchedSubscriptions = {
type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS, type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS,
}; };
export type Action = doChannelSubscribe | doChannelUnsubscribe | HasFetchedSubscriptions; type setSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
export type Action =
| doChannelSubscribe
| doChannelUnsubscribe
| HasFetchedSubscriptions
| setSubscriptionLatest
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| Function;
export type Dispatch = (action: Action) => any; export type Dispatch = (action: Action) => any;
const defaultState = { const defaultState = {
@ -70,6 +94,18 @@ export default handleActions(
...state, ...state,
hasFetchedSubscriptions: true, hasFetchedSubscriptions: true,
}), }),
[ACTIONS.SET_SUBSCRIPTION_LATEST]: (
state: SubscriptionState,
action: setSubscriptionLatest
): SubscriptionState => ({
...state,
subscriptions: state.subscriptions.map(
subscription =>
subscription.channelName === action.data.subscription.channelName
? { ...subscription, latest: action.data.uri }
: subscription
),
}),
}, },
defaultState defaultState
); );