Subscription notifications implementation (#407)

* add notifications for unread subscriptions
This commit is contained in:
Akinwale Ariwodola 2019-01-21 17:11:31 +01:00 committed by GitHub
parent ec928c943d
commit 2c56c78467
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 389 additions and 68 deletions

View file

@ -6,7 +6,7 @@ import {
makeSelectLoadingForUri, makeSelectLoadingForUri,
makeSelectCostInfoForUri makeSelectCostInfoForUri
} from 'lbry-redux'; } from 'lbry-redux';
import { doPurchaseUri, doStartDownload } from '../../redux/actions/file'; import { doPurchaseUri, doStartDownload } from 'redux/actions/file';
import FileDownloadButton from './view'; import FileDownloadButton from './view';
const select = (state, props) => ({ const select = (state, props) => ({

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import {
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
selectEnabledChannelNotifications,
selectSubscriptions,
makeSelectIsSubscribed,
} from 'lbryinc';
import { doToast } from 'lbry-redux';
import SubscribeNotificationButton from './view';
const select = (state, props) => ({
enabledChannelNotifications: selectEnabledChannelNotifications(state),
subscriptions: selectSubscriptions(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
});
export default connect(
select,
{
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
doToast,
}
)(SubscribeNotificationButton);

View file

@ -0,0 +1,55 @@
import React from 'react';
import { parseURI } from 'lbry-redux';
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import Button from 'component/button';
import Colors from 'styles/colors';
class SubscribeNotificationButton extends React.PureComponent {
render() {
const {
uri,
name,
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
doToast,
enabledChannelNotifications,
isSubscribed,
style
} = this.props;
if (!isSubscribed) {
return null;
}
let styles = [];
if (style) {
if (style.length) {
styles = styles.concat(style);
} else {
styles.push(style);
}
}
const shouldNotify = enabledChannelNotifications.indexOf(name) > -1;
const { claimName } = parseURI(uri);
return (
<Button
style={styles}
theme={"light"}
icon={shouldNotify ? "bell-slash" : "bell"}
solid={true}
onPress={() => {
if (shouldNotify) {
doChannelSubscriptionDisableNotifications(name);
doToast({ message: 'You will not receive notifications for new content.' });
} else {
doChannelSubscriptionEnableNotifications(name);
doToast({ message: 'You will receive all notifications for new content.' });
}
}} />
);
}
}
export default SubscribeNotificationButton;

View file

@ -114,7 +114,7 @@ window.store = store;
const compressor = createCompressor(); const compressor = createCompressor();
const authFilter = createFilter('auth', ['authToken']); const authFilter = createFilter('auth', ['authToken']);
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']); const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); const subscriptionsFilter = createFilter('subscriptions', ['enabledChannelNotifications', 'subscriptions']);
const settingsFilter = createFilter('settings', ['clientSettings']); const settingsFilter = createFilter('settings', ['clientSettings']);
const walletFilter = createFilter('wallet', ['receiveAddress']); const walletFilter = createFilter('wallet', ['receiveAddress']);

View file

@ -3,20 +3,32 @@ import {
doFetchFeaturedUris, doFetchFeaturedUris,
selectBalance, selectBalance,
selectFeaturedUris, selectFeaturedUris,
selectFetchingFeaturedUris selectFetchingFeaturedUris,
} from 'lbry-redux'; } from 'lbry-redux';
import { doFetchRewardedContent } from 'lbryinc'; import {
doFetchRewardedContent,
doFetchMySubscriptions,
doRemoveUnreadSubscriptions,
selectEnabledChannelNotifications,
selectSubscriptionClaims,
selectUnreadSubscriptions,
} from 'lbryinc';
import DiscoverPage from './view'; import DiscoverPage from './view';
const select = state => ({ const select = state => ({
allSubscriptions: selectSubscriptionClaims(state),
balance: selectBalance(state), balance: selectBalance(state),
enabledChannelNotifications: selectEnabledChannelNotifications(state),
featuredUris: selectFeaturedUris(state), featuredUris: selectFeaturedUris(state),
fetchingFeaturedUris: selectFetchingFeaturedUris(state), fetchingFeaturedUris: selectFetchingFeaturedUris(state),
unreadSubscriptions: selectUnreadSubscriptions(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()), fetchFeaturedUris: () => dispatch(doFetchFeaturedUris()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchSubscriptions: () => dispatch(doFetchMySubscriptions()),
removeUnreadSubscriptions: () => dispatch(doRemoveUnreadSubscriptions()),
}); });
export default connect(select, perform)(DiscoverPage); export default connect(select, perform)(DiscoverPage);

View file

@ -8,7 +8,7 @@ import {
Text, Text,
View View
} from 'react-native'; } from 'react-native';
import { normalizeURI } from 'lbry-redux'; import { normalizeURI, parseURI } from 'lbry-redux';
import moment from 'moment'; import moment from 'moment';
import Colors from '../../styles/colors'; import Colors from '../../styles/colors';
import discoverStyle from '../../styles/discover'; import discoverStyle from '../../styles/discover';
@ -41,9 +41,66 @@ class DiscoverPage extends React.PureComponent {
} }
}); });
const { fetchFeaturedUris, fetchRewardedContent } = this.props; const {
fetchFeaturedUris,
fetchRewardedContent,
fetchSubscriptions
} = this.props;
fetchFeaturedUris(); fetchFeaturedUris();
fetchRewardedContent(); fetchRewardedContent();
fetchSubscriptions();
}
subscriptionForUri = (uri, channelName) => {
const { allSubscriptions } = this.props;
const { claimId, claimName } = parseURI(uri);
if (allSubscriptions) {
for (let i = 0; i < allSubscriptions.length; i++) {
const sub = allSubscriptions[i];
if (sub.claim_id === claimId && sub.name === claimName && sub.channel_name === channelName) {
return sub;
}
}
}
return null;
}
componentDidUpdate(prevProps, prevState) {
const { allSubscriptions, unreadSubscriptions, enabledChannelNotifications } = this.props;
const utility = NativeModules.UtilityModule;
if (utility) {
const hasUnread = prevProps.unreadSubscriptions &&
prevProps.unreadSubscriptions.length !== unreadSubscriptions.length &&
unreadSubscriptions.length > 0;
if (hasUnread) {
unreadSubscriptions.map(({ channel, uris }) => {
const { claimName: channelName } = parseURI(channel);
// check if notifications are enabled for the channel
if (enabledChannelNotifications.indexOf(channelName) > -1) {
uris.forEach(uri => {
const sub = this.subscriptionForUri(uri, channelName);
if (sub && sub.value && sub.value.stream) {
let isPlayable = false;
const source = sub.value.stream.source;
const metadata = sub.value.stream.metadata;
if (source) {
isPlayable = source.contentType && ['audio', 'video'].indexOf(source.contentType.substring(0, 5)) > -1;
}
if (metadata) {
utility.showNotificationForContent(uri, metadata.title, channelName, metadata.thumbnail, isPlayable);
}
}
});
}
});
}
}
} }
render() { render() {

View file

@ -16,7 +16,7 @@ import {
selectBlackListedOutpoints, selectBlackListedOutpoints,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc'; import { selectRewardContentClaimIds } from 'lbryinc';
import { doDeleteFile, doStopDownloadingFile } from '../../redux/actions/file'; import { doDeleteFile, doPurchaseUri, doStopDownloadingFile } from 'redux/actions/file';
import FilePage from './view'; import FilePage from './view';
const select = (state, props) => { const select = (state, props) => {
@ -44,6 +44,7 @@ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
purchaseUri: (uri, failureCallback) => dispatch(doPurchaseUri(uri, null, failureCallback)),
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
sendTip: (amount, claimId, uri, successCallback, errorCallback) => dispatch(doSendTip(amount, claimId, uri, successCallback, errorCallback)), sendTip: (amount, claimId, uri, successCallback, errorCallback) => dispatch(doSendTip(amount, claimId, uri, successCallback, errorCallback)),
stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)), stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)),

View file

@ -15,23 +15,24 @@ import {
View, View,
WebView WebView
} from 'react-native'; } from 'react-native';
import { navigateToUri } from '../../utils/helper'; import { navigateToUri } from 'utils/helper';
import ImageViewer from 'react-native-image-zoom-viewer'; import ImageViewer from 'react-native-image-zoom-viewer';
import Button from '../../component/button'; import Button from 'component/button';
import Colors from '../../styles/colors'; import Colors from 'styles/colors';
import ChannelPage from '../channel'; import ChannelPage from 'page/channel';
import FileDownloadButton from '../../component/fileDownloadButton'; import FileDownloadButton from 'component/fileDownloadButton';
import FileItemMedia from '../../component/fileItemMedia'; import FileItemMedia from 'component/fileItemMedia';
import FilePrice from '../../component/filePrice'; import FilePrice from 'component/filePrice';
import FloatingWalletBalance from '../../component/floatingWalletBalance'; import FloatingWalletBalance from 'component/floatingWalletBalance';
import Link from '../../component/link'; import Link from 'component/link';
import MediaPlayer from '../../component/mediaPlayer'; import MediaPlayer from 'component/mediaPlayer';
import RelatedContent from '../../component/relatedContent'; import RelatedContent from 'component/relatedContent';
import SubscribeButton from '../../component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import UriBar from '../../component/uriBar'; import SubscribeNotificationButton from 'component/subscribeNotificationButton';
import UriBar from 'component/uriBar';
import Video from 'react-native-video'; import Video from 'react-native-video';
import filePageStyle from '../../styles/filePage'; import filePageStyle from 'styles/filePage';
import uriBarStyle from '../../styles/uriBar'; import uriBarStyle from 'styles/uriBar';
class FilePage extends React.PureComponent { class FilePage extends React.PureComponent {
static navigationOptions = { static navigationOptions = {
@ -48,6 +49,7 @@ class FilePage extends React.PureComponent {
super(props); super(props);
this.state = { this.state = {
autoPlayMedia: false, autoPlayMedia: false,
autoDownloadStarted: false,
downloadButtonShown: false, downloadButtonShown: false,
downloadPressed: false, downloadPressed: false,
fileViewLogged: false, fileViewLogged: false,
@ -63,6 +65,7 @@ class FilePage extends React.PureComponent {
playerHeight: 0, playerHeight: 0,
tipAmount: null, tipAmount: null,
uri: null, uri: null,
uriVars: null,
stopDownloadConfirmed: false stopDownloadConfirmed: false
}; };
} }
@ -71,8 +74,8 @@ class FilePage extends React.PureComponent {
StatusBar.setHidden(false); StatusBar.setHidden(false);
const { isResolvingUri, resolveUri, navigation } = this.props; const { isResolvingUri, resolveUri, navigation } = this.props;
const { uri } = navigation.state.params; const { uri, uriVars } = navigation.state.params;
this.setState({ uri }); this.setState({ uri, uriVars });
if (!isResolvingUri) resolveUri(uri); if (!isResolvingUri) resolveUri(uri);
@ -330,6 +333,13 @@ class FilePage extends React.PureComponent {
sendTip(tipAmount, claim.claim_id, uri, () => { this.setState({ tipAmount: 0, showTipView: false }) }); sendTip(tipAmount, claim.claim_id, uri, () => { this.setState({ tipAmount: 0, showTipView: false }) });
} }
startDownloadFailed = () => {
this.startTime = null;
setTimeout(() => {
this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false });
}, 500);
}
render() { render() {
const { const {
claim, claim,
@ -341,7 +351,8 @@ class FilePage extends React.PureComponent {
rewardedContentClaimIds, rewardedContentClaimIds,
isResolvingUri, isResolvingUri,
blackListedOutpoints, blackListedOutpoints,
navigation navigation,
purchaseUri
} = this.props; } = this.props;
const { uri, autoplay } = navigation.state.params; const { uri, autoplay } = navigation.state.params;
@ -435,6 +446,12 @@ class FilePage extends React.PureComponent {
} }
} }
if (fileInfo && !this.state.autoDownloadStarted && this.state.uriVars && 'true' === this.state.uriVars.download) {
this.setState({ autoDownloadStarted: true }, () => {
purchaseUri(uri, this.startDownloadFailed);
});
}
innerContent = ( innerContent = (
<View style={filePageStyle.pageContainer}> <View style={filePageStyle.pageContainer}>
{this.state.showWebView && isWebViewable && <WebView source={{ uri: localFileUri }} {this.state.showWebView && isWebViewable && <WebView source={{ uri: localFileUri }}
@ -462,12 +479,7 @@ class FilePage extends React.PureComponent {
this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false }); this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false });
}} }}
onButtonLayout={() => this.setState({ downloadButtonShown: true })} onButtonLayout={() => this.setState({ downloadButtonShown: true })}
onStartDownloadFailed={() => { onStartDownloadFailed={this.startDownloadFailed} />}
this.startTime = null;
setTimeout(() => {
this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false });
}, 500);
}} />}
{!fileInfo && <FilePrice uri={uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} />} {!fileInfo && <FilePrice uri={uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} />}
</View> </View>
{canLoadMedia && fileInfo && <View style={playerBgStyle} {canLoadMedia && fileInfo && <View style={playerBgStyle}
@ -496,14 +508,11 @@ class FilePage extends React.PureComponent {
{showActions && {showActions &&
<View style={filePageStyle.actions}> <View style={filePageStyle.actions}>
<View style={filePageStyle.socialActions}> <View style={filePageStyle.socialActions}>
{channelName && <SubscribeButton <Button style={filePageStyle.actionButton}
style={[filePageStyle.actionButton, filePageStyle.subscribeButton]}
uri={channelUri} name={channelName} />}
{<Button style={filePageStyle.actionButton}
theme={"light"} theme={"light"}
icon={"gift"} icon={"gift"}
text={"Send a tip"} text={"Send a tip"}
onPress={() => this.setState({ showTipView: true })} />} onPress={() => this.setState({ showTipView: true })} />
</View> </View>
{showFileActions && {showFileActions &&
<View style={filePageStyle.fileActions}> <View style={filePageStyle.fileActions}>
@ -526,13 +535,27 @@ class FilePage extends React.PureComponent {
style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer} style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}
contentContainerstyle={showActions ? null : filePageStyle.scrollContent}> contentContainerstyle={showActions ? null : filePageStyle.scrollContent}>
<Text style={filePageStyle.title} selectable={true}>{title}</Text> <Text style={filePageStyle.title} selectable={true}>{title}</Text>
{channelName && <Link style={filePageStyle.channelName} {channelName &&
<View style={filePageStyle.channelRow}>
<Link style={filePageStyle.channelName}
selectable={true} selectable={true}
text={channelName} text={channelName}
onPress={() => { onPress={() => {
const channelUri = normalizeURI(channelName); const channelUri = normalizeURI(channelName);
navigateToUri(navigation, channelUri); navigateToUri(navigation, channelUri);
}} />} }} />
<View style={filePageStyle.subscriptionRow}>
<SubscribeButton
style={filePageStyle.actionButton}
uri={channelUri}
name={channelName} />
<SubscribeNotificationButton
style={[filePageStyle.actionButton, filePageStyle.bellButton]}
uri={channelUri}
name={channelName} />
</View>
</View>
}
{description && description.length > 0 && <View style={filePageStyle.divider} />} {description && description.length > 0 && <View style={filePageStyle.divider} />}

View file

@ -3,6 +3,7 @@ import { doBalanceSubscribe, doBlackListedOutpointsSubscribe, doToast } from 'lb
import { import {
doAuthenticate, doAuthenticate,
doCheckSubscriptionsInit, doCheckSubscriptionsInit,
doFetchMySubscriptions,
doFetchRewardedContent, doFetchRewardedContent,
doUserEmailToVerify, doUserEmailToVerify,
doUserEmailVerify, doUserEmailVerify,
@ -25,10 +26,11 @@ const perform = dispatch => ({
checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()), checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()),
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()), deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)), setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)), verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error)), verifyUserEmailFailure: error => dispatch(doUserEmailVerifyFailure(error))
}); });
export default connect(select, perform)(SplashScreen); export default connect(select, perform)(SplashScreen);

View file

@ -33,7 +33,8 @@ class SplashScreen extends React.PureComponent {
launchUrl: null, launchUrl: null,
isDownloadingHeaders: false, isDownloadingHeaders: false,
headersDownloadProgress: 0, headersDownloadProgress: 0,
shouldAuthenticate: false shouldAuthenticate: false,
subscriptionsFetched: false
}); });
if (NativeModules.DaemonServiceControl) { if (NativeModules.DaemonServiceControl) {
@ -119,7 +120,7 @@ class SplashScreen extends React.PureComponent {
} }
_updateStatusCallback(status) { _updateStatusCallback(status) {
const { deleteCompleteBlobs } = this.props; const { deleteCompleteBlobs, fetchSubscriptions } = this.props;
const startupStatus = status.startup_status; const startupStatus = status.startup_status;
// At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve // At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve
const hasStarted = startupStatus.file_manager && startupStatus.wallet && status.wallet.blocks_behind <= 0; const hasStarted = startupStatus.file_manager && startupStatus.wallet && status.wallet.blocks_behind <= 0;
@ -138,6 +139,7 @@ class SplashScreen extends React.PureComponent {
isRunning: true, isRunning: true,
}); });
// fetch subscriptions, so that we can check for new content after resolve
Lbry.resolve({ uri: 'lbry://one' }).then(() => { Lbry.resolve({ uri: 'lbry://one' }).then(() => {
// Leave the splash screen // Leave the splash screen
const { const {

View file

@ -30,7 +30,7 @@ const downloadsStyle = StyleSheet.create({
noDownloadsText: { noDownloadsText: {
textAlign: 'center', textAlign: 'center',
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
fontSize: 14, fontSize: 16,
position: 'absolute' position: 'absolute'
}, },
loading: { loading: {

View file

@ -57,14 +57,23 @@ const filePageStyle = StyleSheet.create({
marginTop: 12, marginTop: 12,
marginLeft: 20, marginLeft: 20,
marginRight: 20, marginRight: 20,
marginBottom: 12 marginBottom: 8
},
channelRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginLeft: 20,
marginRight: 20,
marginBottom: 16,
},
subscriptionRow: {
flexDirection: 'row',
alignSelf: 'flex-end'
}, },
channelName: { channelName: {
fontFamily: 'Inter-UI-SemiBold', fontFamily: 'Inter-UI-SemiBold',
fontSize: 20, fontSize: 20,
marginLeft: 20, marginTop: 6,
marginRight: 20,
marginBottom: 20,
color: Colors.LbryGreen color: Colors.LbryGreen
}, },
description: { description: {
@ -164,8 +173,8 @@ const filePageStyle = StyleSheet.create({
paddingLeft: 16, paddingLeft: 16,
paddingRight: 16 paddingRight: 16
}, },
subscribeButton: { bellButton: {
marginRight: 8 marginLeft: 8
}, },
loading: { loading: {
position: 'absolute', position: 'absolute',

View file

@ -24,7 +24,14 @@ export function dispatchNavigateToUri(dispatch, nav, uri) {
return; return;
} }
const params = { uri }; let uriVars = {};
if (uri.indexOf('?') > -1) {
uriVarsStr = uri.substring(uri.indexOf('?') + 1);
uri = uri.substring(0, uri.indexOf('?'));
uriVars = parseUriVars(uriVarsStr);
}
const params = { uri, uriVars };
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) { if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
const mainRoute = nav.routes[0]; const mainRoute = nav.routes[0];
const discoverRoute = mainRoute.routes[0]; const discoverRoute = mainRoute.routes[0];
@ -62,6 +69,23 @@ export function formatBytes(bytes, decimalPoints = 0) {
return `${value} GB`; return `${value} GB`;
} }
function parseUriVars(vars) {
const uriVars = {};
const parts = vars.split('&');
for (let i = 0; i < parts.length; i++) {
const str = parts[i];
if (str.indexOf('=') > -1) {
const key = str.substring(0, str.indexOf('='));
const value = str.substring(str.indexOf('=') + 1);
uriVars[key] = value;
} else {
uriVars[str] = null;
}
}
return uriVars;
}
export function navigateToUri(navigation, uri, additionalParams) { export function navigateToUri(navigation, uri, additionalParams) {
if (!navigation) { if (!navigation) {
return; return;
@ -76,7 +100,14 @@ export function navigateToUri(navigation, uri, additionalParams) {
return; return;
} }
const params = Object.assign({ uri }, additionalParams); let uriVars = {};
if (uri.indexOf('?') > -1) {
uriVarsStr = uri.substring(uri.indexOf('?') + 1);
uri = uri.substring(0, uri.indexOf('?'));
uriVars = parseUriVars(uriVarsStr);
}
const params = Object.assign({ uri, uriVars }, additionalParams);
if ('File' === navigation.state.routeName) { if ('File' === navigation.state.routeName) {
const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params }); const stackAction = StackActions.replace({ routeName: 'File', newKey: uri, params });
navigation.dispatch(stackAction); navigation.dispatch(stackAction);

View file

@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -57,7 +57,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
private static final int RECEIVE_SMS_PERMISSION_REQ_CODE = 203; private static final int RECEIVE_SMS_PERMISSION_REQ_CODE = 203;
private BroadcastReceiver backgroundMediaReceiver; private BroadcastReceiver notificationsReceiver;
private BroadcastReceiver smsReceiver; private BroadcastReceiver smsReceiver;
@ -73,6 +73,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
public static final String DEVICE_ID_KEY = "deviceId"; public static final String DEVICE_ID_KEY = "deviceId";
public static final String SOURCE_NOTIFICATION_ID_KEY = "sourceNotificationId";
public static final String SETTING_KEEP_DAEMON_RUNNING = "keepDaemonRunning"; public static final String SETTING_KEEP_DAEMON_RUNNING = "keepDaemonRunning";
public static List<Integer> downloadNotificationIds = new ArrayList<Integer>(); public static List<Integer> downloadNotificationIds = new ArrayList<Integer>();
@ -129,7 +131,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.build(); .build();
mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null); mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null);
registerBackgroundMediaReceiver(); registerNotificationsReceiver();
setContentView(mReactRootView); setContentView(mReactRootView);
} }
@ -147,12 +149,12 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
registerReceiver(stopServiceReceiver, intentFilter); registerReceiver(stopServiceReceiver, intentFilter);
} }
private void registerBackgroundMediaReceiver() { private void registerNotificationsReceiver() {
// Background media receiver // Background media receiver
IntentFilter backgroundMediaFilter = new IntentFilter(); IntentFilter filter = new IntentFilter();
backgroundMediaFilter.addAction(BackgroundMediaModule.ACTION_PLAY); filter.addAction(BackgroundMediaModule.ACTION_PLAY);
backgroundMediaFilter.addAction(BackgroundMediaModule.ACTION_PAUSE); filter.addAction(BackgroundMediaModule.ACTION_PAUSE);
backgroundMediaReceiver = new BroadcastReceiver() { notificationsReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
String action = intent.getAction(); String action = intent.getAction();
@ -169,7 +171,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
} }
} }
}; };
registerReceiver(backgroundMediaReceiver, backgroundMediaFilter); registerReceiver(notificationsReceiver, filter);
} }
public void registerSmsReceiver() { public void registerSmsReceiver() {
@ -363,9 +365,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
} }
} }
if (backgroundMediaReceiver != null) { if (notificationsReceiver != null) {
unregisterReceiver(backgroundMediaReceiver); unregisterReceiver(notificationsReceiver);
backgroundMediaReceiver = null; notificationsReceiver = null;
} }
if (smsReceiver != null) { if (smsReceiver != null) {
@ -410,6 +412,15 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
if (mReactInstanceManager != null) { if (mReactInstanceManager != null) {
mReactInstanceManager.onNewIntent(intent); mReactInstanceManager.onNewIntent(intent);
} }
if (intent != null) {
int sourceNotificationId = intent.getIntExtra(SOURCE_NOTIFICATION_ID_KEY, -1);
if (sourceNotificationId > -1) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(sourceNotificationId);
}
}
super.onNewIntent(intent); super.onNewIntent(intent);
} }

View file

@ -62,7 +62,7 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
Random random = new Random(); Random random = new Random();
do { do {
id = random.nextInt(); id = random.nextInt();
} while (id < 100); } while (id < 1000);
return id; return id;
} }
@ -106,7 +106,7 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
} }
} }
private PendingIntent getLaunchPendingIntent(String uri) { public static PendingIntent getLaunchPendingIntent(String uri, Context context) {
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0); PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0);
@ -121,7 +121,7 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// The file URI is used as the unique ID // The file URI is used as the unique ID
builder.setContentIntent(getLaunchPendingIntent(id)) builder.setContentIntent(getLaunchPendingIntent(id, context))
.setContentTitle(String.format("Downloading %s", truncateFilename(filename))) .setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
.setGroup(GROUP_DOWNLOADS) .setGroup(GROUP_DOWNLOADS)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
@ -160,7 +160,7 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
builders.put(notificationId, builder); builders.put(notificationId, builder);
} }
builder.setContentIntent(getLaunchPendingIntent(id)) builder.setContentIntent(getLaunchPendingIntent(id, context))
.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes))) .setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS) .setGroup(GROUP_DOWNLOADS)
.setProgress(MAX_PROGRESS, new Double(progress).intValue(), false) .setProgress(MAX_PROGRESS, new Double(progress).intValue(), false)

View file

@ -1,13 +1,21 @@
package io.lbry.browser.reactmodules; package io.lbry.browser.reactmodules;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.Manifest; import android.Manifest;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.support.v4.content.FileProvider; import android.support.v4.content.FileProvider;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.ContextCompat;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
@ -18,14 +26,30 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.squareup.picasso.Picasso;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import io.lbry.browser.MainActivity; import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.Utils; import io.lbry.browser.Utils;
import io.lbry.browser.reactmodules.DownloadManagerModule;
public class UtilityModule extends ReactContextBaseJavaModule { public class UtilityModule extends ReactContextBaseJavaModule {
private static final Map<String, Integer> activeNotifications = new HashMap<String, Integer>();
private static final String FILE_PROVIDER = "io.lbry.browser.fileprovider"; private static final String FILE_PROVIDER = "io.lbry.browser.fileprovider";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.SUBSCRIPTIONS_NOTIFICATION_CHANNEL";
public static final String ACTION_NOTIFICATION_PLAY = "io.lbry.browser.ACTION_NOTIFICATION_PLAY";
public static final String ACTION_NOTIFICATION_LATER = "io.lbry.browser.ACTION_NOTIFICATION_LATER";
private Context context; private Context context;
public UtilityModule(ReactApplicationContext reactContext) { public UtilityModule(ReactApplicationContext reactContext) {
@ -172,6 +196,75 @@ public class UtilityModule extends ReactContextBaseJavaModule {
} }
} }
@ReactMethod
public void showNotificationForContent(final String uri, String title, String publisher, final String thumbnail, boolean isPlayable) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Subscriptions", NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("LBRY subscription notifications");
notificationManager.createNotificationChannel(channel);
}
if (activeNotifications.containsKey(uri)) {
// the notification for the specified uri is already present, don't try to create another one
return;
}
int id = 0;
Random random = new Random();
do {
id = random.nextInt();
} while (id < 100);
final int notificationId = id;
String uriWithParam = String.format("%s?download=true", uri);
Intent playIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uriWithParam));
playIntent.putExtra(MainActivity.SOURCE_NOTIFICATION_ID_KEY, notificationId);
playIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent playPendingIntent = PendingIntent.getActivity(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT);
boolean hasThumbnail = false;
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.lbrygreen))
.setContentIntent(DownloadManagerModule.getLaunchPendingIntent(uri, context))
.setContentTitle(publisher)
.setContentText(title)
.setSmallIcon(R.drawable.ic_lbry)
.addAction(android.R.drawable.ic_media_play, (isPlayable ? "Play" : "Open"), playPendingIntent);
activeNotifications.put(uri, notificationId);
if (thumbnail != null) {
// attempt to load the thumbnail Bitmap before displaying the notification
final Uri thumbnailUri = Uri.parse(thumbnail);
if (thumbnailUri != null) {
hasThumbnail = true;
(new AsyncTask<Void, Void, Bitmap>() {
protected Bitmap doInBackground(Void... params) {
try {
return Picasso.get().load(thumbnailUri).get();
} catch (IOException e) {
return null;
}
}
protected void onPostExecute(Bitmap result) {
if (result != null) {
builder.setLargeIcon(result)
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(result).bigLargeIcon(null));
}
notificationManager.notify(notificationId, builder.build());
}
}).execute();
}
}
if (!hasThumbnail) {
notificationManager.notify(notificationId, builder.build());
}
}
private static boolean isEmulator() { private static boolean isEmulator() {
String buildModel = Build.MODEL.toLowerCase(); String buildModel = Build.MODEL.toLowerCase();
return (// Check FINGERPRINT return (// Check FINGERPRINT