diff --git a/CHANGELOG.md b/CHANGELOG.md index e53c5892c..309650b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). * Do not kill an existing daemon, instead check if one exists ([#973](https://github.com/lbryio/lbry-app/pull/973)) * Enable play button immediately after user clicks download ([#987](https://github.com/lbryio/lbry-app/pull/987)) * Significantly improved search performance ([#1032](https://github.com/lbryio/lbry-app/pull/1032)) + * Allow editing of claims when bid is greater than current balance ([1105](https://github.com/lbryio/lbry-app/pull/1105)) ### Fixed * Fixed sort by date of published content ([#986](https://github.com/lbryio/lbry-app/issues/986)) @@ -35,6 +36,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). * App will no longer reset when minimizing to tray ([#1042](https://github.com/lbryio/lbry-app/pull/1042)) * Error when clicking LBRY URLs when app is closed on macOS ([#1119](https://github.com/lbryio/lbry-app/issues/1119)) * LBRY URLs not working on Linux ([#1120](https://github.com/lbryio/lbry-app/issues/1120)) + * Fix Windows notifications not showing ([1145](https://github.com/lbryio/lbry-app/pull/1145)) ### Deprecated * diff --git a/package.json b/package.json index 63463db7f..042a3175e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "keywords": [ "lbry" ], + "build": { + "appId": "io.lbry.LBRY" + }, "license": "MIT", "homepage": "https://lbry.io/", "bugs": { diff --git a/src/main/index.js b/src/main/index.js index a456fcd5a..efd917060 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -50,6 +50,7 @@ const installExtensions = async () => { app.setAsDefaultProtocolClient('lbry'); app.setName('LBRY'); +app.setAppUserModelId('io.lbry.LBRY'); app.on('ready', async () => { const processList = await findProcess('name', 'lbrynet-daemon'); diff --git a/src/renderer/analytics.js b/src/renderer/analytics.js index 57d053fc9..2a03b7428 100644 --- a/src/renderer/analytics.js +++ b/src/renderer/analytics.js @@ -2,6 +2,7 @@ import mixpanel from 'mixpanel-browser'; import Lbryio from 'lbryio'; import isDev from 'electron-is-dev'; +import type { Subscription } from 'redux/reducers/subscriptions'; if (isDev) { mixpanel.init('691723e855cabb9d27a7a79002216967'); @@ -13,7 +14,9 @@ type Analytics = { track: (string, ?Object) => void, setUser: Object => void, toggle: (boolean, ?boolean) => void, - apiLog: (string, string, string) => void, + apiLogView: (string, string, string) => void, + apiLogSubscribe: Subscription => void, + apiLogUnsubscribe: Subscription => void, }; let analyticsEnabled: boolean = false; @@ -44,7 +47,7 @@ const analytics: Analytics = { } analyticsEnabled = enabled; }, - apiLog: (uri: string, outpoint: string, claimId: string): void => { + apiLogView: (uri: string, outpoint: string, claimId: string): void => { if (analyticsEnabled) { Lbryio.call('file', 'view', { uri, @@ -53,6 +56,20 @@ const analytics: Analytics = { }).catch(() => {}); } }, + apiLogSubscribe: (subscription: Subscription): void => { + if (analyticsEnabled) { + Lbryio.call('subscription', 'new', { + subscription, + }).catch(() => {}); + } + }, + apiLogUnsubscribe: (subscription: Subscription): void => { + if (analyticsEnabled) { + Lbryio.call('subscription', 'delete', { + subscription, + }).catch(() => {}); + } + }, }; export default analytics; diff --git a/src/renderer/component/publishForm/internal/channelSection.jsx b/src/renderer/component/publishForm/internal/channelSection.jsx index b36c1b317..db0b040b1 100644 --- a/src/renderer/component/publishForm/internal/channelSection.jsx +++ b/src/renderer/component/publishForm/internal/channelSection.jsx @@ -57,6 +57,16 @@ class ChannelSection extends React.PureComponent { return; } + if (newChannelBid === 0) { + this.refs.newChannelName.showError(__('Bid value must be greater than 0.')); + + return; + } + if (newChannelBid === balance) { + this.refs.newChannelName.showError(__('Please decrease your bid to account for transaction fees.')); + + return; + } this.setState({ creatingChannel: true, diff --git a/src/renderer/component/publishForm/view.jsx b/src/renderer/component/publishForm/view.jsx index 4f770a803..b07f6083f 100644 --- a/src/renderer/component/publishForm/view.jsx +++ b/src/renderer/component/publishForm/view.jsx @@ -61,15 +61,6 @@ class PublishForm extends React.PureComponent { } handleSubmit() { - const { balance } = this.props; - const { bid } = this.state; - - if (bid > balance) { - this.handlePublishError({ message: 'insufficient funds' }); - - return; - } - this.setState({ submitting: true, }); diff --git a/src/renderer/component/subscribeButton/index.js b/src/renderer/component/subscribeButton/index.js index 2f5dceb30..6c93bbb65 100644 --- a/src/renderer/component/subscribeButton/index.js +++ b/src/renderer/component/subscribeButton/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; +import { doOpenModal } from 'redux/actions/app'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import SubscribeButton from './view'; @@ -11,4 +12,5 @@ const select = (state, props) => ({ export default connect(select, { doChannelSubscribe, doChannelUnsubscribe, + doOpenModal, })(SubscribeButton); diff --git a/src/renderer/component/subscribeButton/view.jsx b/src/renderer/component/subscribeButton/view.jsx index 216b0bf72..bb0d0c146 100644 --- a/src/renderer/component/subscribeButton/view.jsx +++ b/src/renderer/component/subscribeButton/view.jsx @@ -1,7 +1,15 @@ import React from 'react'; import Link from 'component/link'; +import * as modals from 'constants/modal_types'; -export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannelUnsubscribe }) => { +export default ({ + channelName, + uri, + subscriptions, + doChannelSubscribe, + doChannelUnsubscribe, + doOpenModal, +}) => { const isSubscribed = subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1; @@ -15,12 +23,15 @@ export default ({ channelName, uri, subscriptions, doChannelSubscribe, doChannel iconRight={isSubscribed ? '' : 'at'} button={isSubscribed ? 'alt' : 'primary'} label={subscriptionLabel} - onClick={() => + onClick={() => { + if (!subscriptions.length) { + doOpenModal(modals.FIRST_SUBSCRIPTION); + } subscriptionHandler({ channelName, uri, - }) - } + }); + }} /> ) : null; diff --git a/src/renderer/constants/modal_types.js b/src/renderer/constants/modal_types.js index a0272b708..0c2e03da1 100644 --- a/src/renderer/constants/modal_types.js +++ b/src/renderer/constants/modal_types.js @@ -16,3 +16,4 @@ export const TRANSACTION_FAILED = 'transaction_failed'; export const REWARD_APPROVAL_REQUIRED = 'reward_approval_required'; export const AFFIRM_PURCHASE = 'affirm_purchase'; export const CONFIRM_CLAIM_REVOKE = 'confirmClaimRevoke'; +export const FIRST_SUBSCRIPTION = 'firstSubscription'; diff --git a/src/renderer/modal/modalFirstSubscription/index.js b/src/renderer/modal/modalFirstSubscription/index.js new file mode 100644 index 000000000..6c669f79b --- /dev/null +++ b/src/renderer/modal/modalFirstSubscription/index.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux'; +import { doCloseModal } from 'redux/actions/app'; +import { doNavigate } from 'redux/actions/navigation'; +import ModalFirstSubscription from './view'; + +const perform = dispatch => () => ({ + closeModal: () => dispatch(doCloseModal()), + navigate: path => dispatch(doNavigate(path)), +}); + +export default connect(null, perform)(ModalFirstSubscription); diff --git a/src/renderer/modal/modalFirstSubscription/view.jsx b/src/renderer/modal/modalFirstSubscription/view.jsx new file mode 100644 index 000000000..546819fbc --- /dev/null +++ b/src/renderer/modal/modalFirstSubscription/view.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Modal } from 'modal/modal'; +import Link from 'component/link'; + +const ModalFirstSubscription = props => { + const { closeModal, navigate } = props; + + return ( + +
+

{__('Subscriptions 101')}

+

{__('You just subscribed to your first channel. Awesome!')}

+

{__('A few quick things to know:')}

+

+ {__('1) You can use the')}{' '} + { + navigate('/subscriptions'); + closeModal(); + }} + />{' '} + {__('to view content across all of your subscribed channels.')} +

+

+ {__( + '2) This app will automatically download new free content from channels you are subscribed to.' + )} +

+

+ {__( + '3) If we have your email address, we may send you notifications and rewards related to new content.' + )} +

+
+ +
+
+
+ ); +}; + +export default ModalFirstSubscription; diff --git a/src/renderer/modal/modalRouter/view.jsx b/src/renderer/modal/modalRouter/view.jsx index 341883171..510d49b9d 100644 --- a/src/renderer/modal/modalRouter/view.jsx +++ b/src/renderer/modal/modalRouter/view.jsx @@ -14,8 +14,9 @@ import ModalTransactionFailed from 'modal/modalTransactionFailed'; import ModalFileTimeout from 'modal/modalFileTimeout'; import ModalAffirmPurchase from 'modal/modalAffirmPurchase'; import ModalRevokeClaim from 'modal/modalRevokeClaim'; -import ModalEmailCollection from '../modalEmailCollection'; -import ModalPhoneCollection from '../modalPhoneCollection'; +import ModalEmailCollection from 'modal/modalEmailCollection'; +import ModalPhoneCollection from 'modal/modalPhoneCollection'; +import ModalFirstSubscription from 'modal/modalFirstSubscription'; import * as modals from 'constants/modal_types'; class ModalRouter extends React.PureComponent { @@ -135,6 +136,8 @@ class ModalRouter extends React.PureComponent { return ; case modals.EMAIL_COLLECTION: return ; + case modals.FIRST_SUBSCRIPTION: + return ; default: return null; } diff --git a/src/renderer/page/rewards/index.js b/src/renderer/page/rewards/index.js index e456a1ca2..c0c7c6b64 100644 --- a/src/renderer/page/rewards/index.js +++ b/src/renderer/page/rewards/index.js @@ -4,9 +4,11 @@ import { selectFetchingRewards, selectUnclaimedRewards } from 'redux/selectors/r import { selectUser } from 'redux/selectors/user'; import { doAuthNavigate, doNavigate } from 'redux/actions/navigation'; import { doRewardList } from 'redux/actions/rewards'; +import { selectDaemonSettings } from 'redux/selectors/settings'; import RewardsPage from './view'; const select = (state, props) => ({ + daemonSettings: selectDaemonSettings(state), fetching: selectFetchingRewards(state), rewards: selectUnclaimedRewards(state), user: selectUser(state), diff --git a/src/renderer/page/rewards/view.jsx b/src/renderer/page/rewards/view.jsx index 258d76052..99375faee 100644 --- a/src/renderer/page/rewards/view.jsx +++ b/src/renderer/page/rewards/view.jsx @@ -29,9 +29,9 @@ class RewardsPage extends React.PureComponent { // } renderPageHeader() { - const { doAuth, navigate, user } = this.props; + const { doAuth, navigate, user, daemonSettings } = this.props; - if (user && !user.is_reward_approved) { + if (user && !user.is_reward_approved && daemonSettings.share_usage_data) { if (!user.primary_email || !user.has_verified_email || !user.is_identity_verified) { return (
@@ -78,9 +78,21 @@ class RewardsPage extends React.PureComponent { } renderUnclaimedRewards() { - const { fetching, rewards, user } = this.props; + const { fetching, rewards, user, daemonSettings, navigate } = this.props; - if (fetching) { + if (!daemonSettings.share_usage_data) { + return ( +
+

+ {__( + 'Rewards are currently disabled for your account. Turn on diagnostic data sharing, in' + )}{' '} + navigate('/settings')} label="Settings" /> + {__(', in order to re-enable them.')} +

+
+ ); + } else if (fetching) { return (
diff --git a/src/renderer/page/settings/view.jsx b/src/renderer/page/settings/view.jsx index fe704b906..f83fc9bc9 100644 --- a/src/renderer/page/settings/view.jsx +++ b/src/renderer/page/settings/view.jsx @@ -309,7 +309,10 @@ class SettingsPage extends React.PureComponent { onChange={this.onShareDataChange.bind(this)} defaultChecked={daemonSettings.share_usage_data} label={__( - 'Help make LBRY better by contributing analytics and diagnostic data and about my usage' + 'Help make LBRY better by contributing analytics and diagnostic data about my usage.' + )} + helper={__( + 'You will be ineligible to earn rewards while diagnostics are not being shared.' )} />
diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index 5fd99c23e..146b9b6d5 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -228,7 +228,7 @@ export function doDownloadFile(uri, streamInfo) { return dispatch => { dispatch(doStartDownload(uri, streamInfo.outpoint)); - analytics.apiLog(uri, streamInfo.output, streamInfo.claim_id); + analytics.apiLogView(uri, streamInfo.output, streamInfo.claim_id); dispatch(doClaimEligiblePurchaseRewards()); }; diff --git a/src/renderer/redux/actions/subscriptions.js b/src/renderer/redux/actions/subscriptions.js index 8a390987b..58269ab40 100644 --- a/src/renderer/redux/actions/subscriptions.js +++ b/src/renderer/redux/actions/subscriptions.js @@ -6,21 +6,30 @@ import Lbry from 'lbry'; import { doPurchaseUri } from 'redux/actions/content'; import { doNavigate } from 'redux/actions/navigation'; import { buildURI } from 'lbryURI'; +import analytics from 'analytics'; const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000; -export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => +export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { dispatch({ type: ACTIONS.CHANNEL_SUBSCRIBE, data: subscription, }); -export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch) => + analytics.apiLogSubscribe(subscription); + + dispatch(doCheckSubscription(subscription, true)); +}; + +export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { dispatch({ type: ACTIONS.CHANNEL_UNSUBSCRIBE, data: subscription, }); + analytics.apiLogUnsubscribe(subscription); +}; + export const doCheckSubscriptions = () => ( dispatch: Dispatch, getState: () => SubscriptionState diff --git a/src/renderer/redux/reducers/claims.js b/src/renderer/redux/reducers/claims.js index 5078bfccc..a96cec487 100644 --- a/src/renderer/redux/reducers/claims.js +++ b/src/renderer/redux/reducers/claims.js @@ -43,7 +43,7 @@ reducers[ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED] = (state, action) => { const byId = Object.assign({}, state.byId); const pendingById = Object.assign({}, state.pendingById); - claims.filter(claim => claim.category && claim.category.match(/claim/)).forEach(claim => { + claims.filter(claim => claim.category && (claim.category.match(/claim/) || claim.category.match(/update/))).forEach(claim => { byId[claim.claim_id] = claim; const pending = Object.values(pendingById).find(