diff --git a/src/renderer/component/fileList/view.jsx b/src/renderer/component/fileList/view.jsx index f803ee141..eef0993c6 100644 --- a/src/renderer/component/fileList/view.jsx +++ b/src/renderer/component/fileList/view.jsx @@ -15,8 +15,8 @@ class FileList extends React.PureComponent { this._sortFunctions = { dateNew(fileInfos) { return fileInfos.slice().sort((fileInfo1, fileInfo2) => { - const height1 = fileInfo1.height - const height2 = fileInfo2.height + const height1 = fileInfo1.height; + const height2 = fileInfo2.height; if (height1 > height2) { return -1; } else if (height1 < height2) { @@ -27,8 +27,8 @@ class FileList extends React.PureComponent { }, dateOld(fileInfos) { return fileInfos.slice().sort((fileInfo1, fileInfo2) => { - const height1 = fileInfo1.height - const height2 = fileInfo2.height + const height1 = fileInfo1.height; + const height2 = fileInfo2.height; if (height1 < height2) { return -1; } else if (height1 > height2) { diff --git a/src/renderer/component/rewardSummary/view.jsx b/src/renderer/component/rewardSummary/view.jsx index 36507d535..9fa611902 100644 --- a/src/renderer/component/rewardSummary/view.jsx +++ b/src/renderer/component/rewardSummary/view.jsx @@ -15,9 +15,9 @@ const RewardSummary = (props: Props) => {

{__('Rewards')}

- {__('Read our')}{' '} - {__('FAQ')}{' '}{__('to learn more about LBRY Rewards')}. -

+ {__('Read our')} {__('FAQ')}{' '} + {__('to learn more about LBRY Rewards')}. +

{unclaimedRewardAmount > 0 ? ( diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index df8cf1407..584fa7a47 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -164,6 +164,10 @@ export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT'; export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE'; export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE'; 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 export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; diff --git a/src/renderer/index.js b/src/renderer/index.js index a5eadda32..b4225577a 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -63,7 +63,7 @@ ipcRenderer.on('window-is-focused', () => { document.addEventListener('dragover', event => { event.preventDefault(); -}) +}); document.addEventListener('drop', event => { event.preventDefault(); }); diff --git a/src/renderer/page/file/index.js b/src/renderer/page/file/index.js index 4984ed812..37fb13c7f 100644 --- a/src/renderer/page/file/index.js +++ b/src/renderer/page/file/index.js @@ -4,6 +4,7 @@ import { doFetchFileInfo } from 'redux/actions/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { selectRewardContentClaimIds } from 'redux/selectors/content'; import { doFetchCostInfoForUri } from 'redux/actions/cost_info'; +import { checkSubscriptionLatest } from 'redux/actions/subscriptions'; import { makeSelectClaimForUri, makeSelectContentTypeForUri, @@ -13,6 +14,7 @@ import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info'; import { selectShowNsfw } from 'redux/selectors/settings'; import FilePage from './view'; import { makeSelectCurrentParam } from 'redux/selectors/navigation'; +import { selectSubscriptions } from 'redux/selectors/subscriptions'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), @@ -23,12 +25,15 @@ const select = (state, props) => ({ tab: makeSelectCurrentParam('tab')(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), rewardedContentClaimIds: selectRewardContentClaimIds(state, props), + subscriptions: selectSubscriptions(state), }); const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), + checkSubscriptionLatest: (subscription, uri) => + dispatch(checkSubscriptionLatest(subscription, uri)), }); export default connect(select, perform)(FilePage); diff --git a/src/renderer/page/file/view.jsx b/src/renderer/page/file/view.jsx index e248ee5d6..069a0dfdc 100644 --- a/src/renderer/page/file/view.jsx +++ b/src/renderer/page/file/view.jsx @@ -17,6 +17,7 @@ class FilePage extends React.PureComponent { componentDidMount() { this.fetchFileInfo(this.props); this.fetchCostInfo(this.props); + this.checkSubscriptionLatest(this.props); } 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() { const { claim, diff --git a/src/renderer/redux/actions/app.js b/src/renderer/redux/actions/app.js index 1d9b55f8e..e67a5827e 100644 --- a/src/renderer/redux/actions/app.js +++ b/src/renderer/redux/actions/app.js @@ -11,6 +11,8 @@ import { doFetchDaemonSettings } from 'redux/actions/settings'; import { doAuthenticate } from 'redux/actions/user'; import { doBalanceSubscribe } from 'redux/actions/wallet'; import { doPause } from 'redux/actions/media'; +import { doCheckSubscriptions } from 'redux/actions/subscriptions'; + import { selectCurrentModal, selectIsUpgradeSkipped, @@ -253,6 +255,7 @@ export function doDaemonReady() { dispatch(doCheckUpgradeAvailable()); } dispatch(doCheckUpgradeSubscribe()); + dispatch(doCheckSubscriptions()); }; } diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index 2ca2c4dbb..bdf831a03 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -7,6 +7,7 @@ import Lbryio from 'lbryio'; import { normalizeURI, buildURI } from 'lbryURI'; import { doAlertError, doOpenModal } from 'redux/actions/app'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; +import { setSubscriptionLatest } from 'redux/actions/subscriptions'; import { selectBadgeNumber } from 'redux/selectors/app'; import { selectMyClaimsRaw } from 'redux/selectors/claims'; 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) => { const state = getState(); const balance = selectBalance(state); @@ -321,7 +322,7 @@ export function doPurchaseUri(uri) { return; } - const costInfo = makeSelectCostInfoForUri(uri)(state); + const costInfo = makeSelectCostInfoForUri(uri)(state) || specificCostInfo; const { cost } = costInfo; if (cost > balance) { @@ -358,6 +359,25 @@ export function doFetchClaimsByChannel(uri, page) { const claimResult = result[uri] || {}; 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({ type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED, data: { diff --git a/src/renderer/redux/actions/subscriptions.js b/src/renderer/redux/actions/subscriptions.js index e3eb939ed..650f98a9b 100644 --- a/src/renderer/redux/actions/subscriptions.js +++ b/src/renderer/redux/actions/subscriptions.js @@ -1,6 +1,13 @@ // @flow 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) => dispatch({ @@ -14,5 +21,113 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D 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 + 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)); + } + }); +}; + +export const setSubscriptionLatest = (subscription: Subscription, uri: string) => ( + dispatch: Dispatch +) => + dispatch({ + type: ACTIONS.SET_SUBSCRIPTION_LATEST, + data: { + subscription, + uri, + }, + }); + export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); diff --git a/src/renderer/redux/reducers/subscriptions.js b/src/renderer/redux/reducers/subscriptions.js index f7a846ffc..a8f40746f 100644 --- a/src/renderer/redux/reducers/subscriptions.js +++ b/src/renderer/redux/reducers/subscriptions.js @@ -5,6 +5,7 @@ import { handleActions } from 'util/redux-utils'; export type Subscription = { channelName: string, uri: string, + latest: ?string, }; // Subscription redux types @@ -28,7 +29,30 @@ type HasFetchedSubscriptions = { 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; const defaultState = { @@ -70,6 +94,18 @@ export default handleActions( ...state, 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 );