per channel notification settings

This commit is contained in:
Sean Yesmunt 2020-11-02 11:51:08 -05:00
parent 07377a4e2a
commit 63f1fed33c
31 changed files with 295 additions and 660 deletions

View file

@ -1,6 +1,8 @@
[ignore] [ignore]
.*\.typeface\.json .*\.typeface\.json
.*/node_modules/findup/.* .*/node_modules/findup/.*
node_modules/lbryinc/flow-typed/Redux.js
node_modules/lbry-redux/flow-typed/Redux.js
[include] [include]

View file

@ -26,6 +26,7 @@ declare type WebNotification = {
hash: string, hash: string,
claim_title: string, claim_title: string,
comment?: string, comment?: string,
channel_url: string,
}, },
email: {}, email: {},
}, },

3
flow-typed/redux.js vendored Normal file
View file

@ -0,0 +1,3 @@
// @flow
declare type Dispatch = any;

View file

@ -14,99 +14,17 @@ import {
declare type Subscription = { declare type Subscription = {
channelName: string, // @CryptoCandor, channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6 uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
latest?: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620 notificationsDisabled?: boolean,
}; };
// Tracking for new content declare type Following = {
// i.e. If a subscription has a DOWNLOADING type, we will trigger an OS notification uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
// to tell users there is new content from their subscriptions notificationsDisabled: boolean,
declare type SubscriptionNotificationType = DOWNLOADED | DOWNLOADING | NOTIFY_ONLY;
declare type UnreadSubscription = {
type: SubscriptionNotificationType,
uris: Array<string>,
};
declare type UnreadSubscriptions = {
[string]: UnreadSubscription,
};
declare type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
declare type SuggestedType = SUGGESTED_TOP_BID | SUGGESTED_TOP_SUBSCRIBED | SUGGESTED_FEATURED;
declare type SuggestedSubscriptions = {
[SuggestedType]: string,
}; };
declare type SubscriptionState = { declare type SubscriptionState = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
unread: UnreadSubscriptions, following: Array<Following>,
loading: boolean, loading: boolean,
viewMode: ViewMode,
suggested: SuggestedSubscriptions,
loadingSuggested: boolean,
firstRunCompleted: boolean, firstRunCompleted: boolean,
showSuggestedSubs: boolean,
};
//
// Action types
//
declare type DoChannelSubscribe = {
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: Subscription,
};
declare type DoChannelUnsubscribe = {
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: Subscription,
};
declare type DoUpdateSubscriptionUnreads = {
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
type?: SubscriptionNotificationType,
},
};
declare type DoRemoveSubscriptionUnreads = {
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
},
};
declare type SetSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
declare type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
declare type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
declare type FetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
declare type SetViewMode = {
type: ACTIONS.SET_VIEW_MODE,
data: ViewMode,
};
declare type GetSuggestedSubscriptionsSuccess = {
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
data: SuggestedSubscriptions,
}; };

View file

@ -136,8 +136,8 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#04789190b060f27bf0c438a9af449b4a18ef4925", "lbry-redux": "lbryio/lbry-redux#4d11f319141ba49a26f06053c42c01f00dd4feaf",
"lbryinc": "lbryio/lbryinc#517c28a183d6ab69a357227809bc7c3c12d8411e", "lbryinc": "lbryio/lbryinc#9bdf18eef6a65aef7d92ce1040b6f8d3db1be671",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"lodash-es": "^4.17.14", "lodash-es": "^4.17.14",

View file

@ -860,6 +860,24 @@ export const icons = {
strokeMiterlimit="10" strokeMiterlimit="10"
/> />
), ),
[ICONS.BELL]: buildIcon(
<g>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</g>
),
[ICONS.BELL_ON]: buildIcon(
<g>
<path
d="M18 8C18 6.4087 17.3679 4.88258 16.2426 3.75736C15.1174 2.63214 13.5913 2 12 2C10.4087 2 8.88258 2.63214 7.75736 3.75736C6.63214 4.88258 6 6.4087 6 8C6 15 3 17 3 17H21C21 17 18 15 18 8Z"
fill="currentColor"
/>
<path
d="M13.73 21C13.5542 21.3031 13.3018 21.5547 12.9982 21.7295C12.6946 21.9044 12.3504 21.9965 12 21.9965C11.6496 21.9965 11.3054 21.9044 11.0018 21.7295C10.6982 21.5547 10.4458 21.3031 10.27 21"
strokeLinejoin="round"
/>
</g>
),
[ICONS.PIN]: (props: CustomProps) => ( [ICONS.PIN]: (props: CustomProps) => (
<svg <svg
{...props} {...props}

View file

@ -1,12 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux'; import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import FileProperties from './view'; import FileProperties from './view';
const select = (state, props) => ({ const select = (state, props) => ({
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state), downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state), isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
}); });

View file

@ -12,7 +12,6 @@ type Props = {
downloaded: boolean, downloaded: boolean,
claimIsMine: boolean, claimIsMine: boolean,
isSubscribed: boolean, isSubscribed: boolean,
isNew: boolean,
small: boolean, small: boolean,
}; };

View file

@ -1,3 +1,9 @@
import { connect } from 'react-redux';
import { doResolveUri, makeSelectClaimForUri } from 'lbry-redux';
import CardMedia from './view'; import CardMedia from './view';
export default CardMedia; const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
});
export default connect(select, { doResolveUri })(CardMedia);

View file

@ -3,48 +3,52 @@ import type { Node } from 'react';
import React from 'react'; import React from 'react';
import FreezeframeWrapper from './FreezeframeWrapper'; import FreezeframeWrapper from './FreezeframeWrapper';
import Placeholder from './placeholder.png'; import Placeholder from './placeholder.png';
import classnames from 'classnames';
type Props = { type Props = {
uri: string,
thumbnail: ?string, // externally sourced image thumbnail: ?string, // externally sourced image
children?: Node, children?: Node,
allowGifs: boolean, allowGifs: boolean,
claim: ?StreamClaim,
doResolveUri: string => void,
className?: string,
}; };
const className = 'media__thumb'; function FileThumbnail(props: Props) {
const { claim, uri, doResolveUri, thumbnail: rawThumbnail, children, allowGifs = false, className } = props;
const passedThumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
const thumbnailFromClaim =
uri && claim && claim.value && claim.value.thumbnail ? claim.value.thumbnail.url : undefined;
const thumbnail = passedThumbnail || thumbnailFromClaim;
const hasResolvedClaim = claim !== undefined;
class FileThumbnail extends React.PureComponent<Props> { React.useEffect(() => {
render() { if (!hasResolvedClaim) {
const { thumbnail: rawThumbnail, children, allowGifs = false } = this.props; doResolveUri(uri);
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
if (!allowGifs && thumbnail && thumbnail.endsWith('gif')) {
return (
<FreezeframeWrapper src={thumbnail} className={className}>
{children}
</FreezeframeWrapper>
);
} }
}, [hasResolvedClaim, uri, doResolveUri]);
let url; if (!allowGifs && thumbnail && thumbnail.endsWith('gif')) {
// @if TARGET='web'
// Pass image urls through a compression proxy
url = thumbnail || Placeholder;
// url = thumbnail
// ? 'https://ext.thumbnails.lbry.com/400x,q55/' +
// // The image server will redirect if we don't remove the double slashes after http(s)
// thumbnail.replace('https://', 'https:/').replace('http://', 'http:/')
// : Placeholder;
// @endif
// @if TARGET='app'
url = thumbnail || Placeholder;
// @endif
return ( return (
<div style={{ backgroundImage: `url('${url.replace(/'/g, "\\'")}')` }} className={className}> <FreezeframeWrapper src={thumbnail} className={classnames('media__thumb', className)}>
{children} {children}
</div> </FreezeframeWrapper>
); );
} }
const url = passedThumbnail || uri ? thumbnailFromClaim : Placeholder;
return (
<div
style={{ backgroundImage: `url('${url ? url.replace(/'/g, "\\'") : ''}')` }}
className={classnames('media__thumb', className, {
'media__thumb--resolving': !hasResolvedClaim,
})}
>
{children}
</div>
);
} }
export default FileThumbnail; export default FileThumbnail;

View file

@ -14,6 +14,7 @@ import { formatLbryUrlForWeb } from 'util/url';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view'; import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
import FileThumbnail from 'component/fileThumbnail';
type Props = { type Props = {
notification: WebNotification, notification: WebNotification,
@ -66,6 +67,9 @@ export default function Notification(props: Props) {
case NOTIFICATIONS.NOTIFICATION_REPLY: case NOTIFICATIONS.NOTIFICATION_REPLY:
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.reply_author} />; icon = <ChannelThumbnail small uri={notification_parameters.dynamic.reply_author} />;
break; break;
case NOTIFICATIONS.NEW_CONTENT:
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.channel_url} />;
break;
case NOTIFICATIONS.DAILY_WATCH_AVAILABLE: case NOTIFICATIONS.DAILY_WATCH_AVAILABLE:
case NOTIFICATIONS.DAILY_WATCH_REMIND: case NOTIFICATIONS.DAILY_WATCH_REMIND:
icon = <Icon icon={ICONS.LBC} sectionIcon className="notification__icon" />; icon = <Icon icon={ICONS.LBC} sectionIcon className="notification__icon" />;
@ -136,6 +140,12 @@ export default function Notification(props: Props) {
)} )}
</div> </div>
{notification_rule === NOTIFICATIONS.NEW_CONTENT && (
<>
<FileThumbnail uri={notification_parameters.device.target} className="notification__content-thumbnail" />
</>
)}
<div className="notification__extra"> <div className="notification__extra">
<div className="notification__time"> <div className="notification__time">
<DateTime timeAgo date={notification.active_at} /> <DateTime timeAgo date={notification.active_at} />

View file

@ -1,6 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
import { makeSelectIsSubscribed, selectFirstRunCompleted } from 'redux/selectors/subscriptions'; import {
makeSelectIsSubscribed,
selectFirstRunCompleted,
makeSelectNotificationsDisabled,
} from 'redux/selectors/subscriptions';
import { makeSelectPermanentUrlForUri } from 'lbry-redux'; import { makeSelectPermanentUrlForUri } from 'lbry-redux';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import SubscribeButton from './view'; import SubscribeButton from './view';
@ -9,6 +13,7 @@ const select = (state, props) => ({
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state), isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
firstRunCompleted: selectFirstRunCompleted(state), firstRunCompleted: selectFirstRunCompleted(state),
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state), permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
notificationsDisabled: makeSelectNotificationsDisabled(props.uri)(state),
}); });
export default connect(select, { export default connect(select, {

View file

@ -9,16 +9,18 @@ import { useIsMobile } from 'effects/use-screensize';
type SubscriptionArgs = { type SubscriptionArgs = {
channelName: string, channelName: string,
uri: string, uri: string,
notificationsDisabled?: boolean,
}; };
type Props = { type Props = {
permanentUrl: ?string, permanentUrl: ?string,
isSubscribed: boolean, isSubscribed: boolean,
doChannelSubscribe: ({ channelName: string, uri: string }) => void, doChannelSubscribe: SubscriptionArgs => void,
doChannelUnsubscribe: SubscriptionArgs => void, doChannelUnsubscribe: SubscriptionArgs => void,
showSnackBarOnSubscribe: boolean, showSnackBarOnSubscribe: boolean,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
shrinkOnMobile: boolean, shrinkOnMobile: boolean,
notificationsDisabled: boolean,
}; };
export default function SubscribeButton(props: Props) { export default function SubscribeButton(props: Props) {
@ -30,6 +32,7 @@ export default function SubscribeButton(props: Props) {
showSnackBarOnSubscribe, showSnackBarOnSubscribe,
doToast, doToast,
shrinkOnMobile = false, shrinkOnMobile = false,
notificationsDisabled,
} = props; } = props;
const buttonRef = useRef(); const buttonRef = useRef();
@ -50,27 +53,49 @@ export default function SubscribeButton(props: Props) {
const titlePrefix = isSubscribed ? __('Unfollow this channel') : __('Follow this channel'); const titlePrefix = isSubscribed ? __('Unfollow this channel') : __('Follow this channel');
return permanentUrl ? ( return permanentUrl ? (
<Button <div className="button-group">
ref={buttonRef} <Button
iconColor="red" ref={buttonRef}
largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel} iconColor="red"
icon={unfollowOverride ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIBE} largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
button={'alt'} icon={unfollowOverride ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIBE}
requiresAuth={IS_WEB} button={'alt'}
label={label} requiresAuth={IS_WEB}
title={titlePrefix} label={label}
onClick={e => { title={titlePrefix}
e.stopPropagation(); onClick={e => {
e.stopPropagation();
subscriptionHandler({ subscriptionHandler({
channelName: claimName, channelName: claimName,
uri: permanentUrl, uri: permanentUrl,
}); notificationsDisabled: true,
});
if (showSnackBarOnSubscribe) { if (showSnackBarOnSubscribe) {
doToast({ message: `${__('Now following ')} ${claimName}!` }); doToast({ message: __('Now following %channel%!', { channel: claimName }) });
} }
}} }}
/> />
{isSubscribed && (
<Button
button="alt"
icon={notificationsDisabled ? ICONS.BELL : ICONS.BELL_ON}
onClick={() => {
const newNotificationsDisabled = !notificationsDisabled;
doChannelSubscribe({
channelName: claimName,
uri: permanentUrl,
notificationsDisabled: newNotificationsDisabled,
});
if (newNotificationsDisabled === false) {
doToast({ message: __('Notifications enabled for %channel%!', { channel: claimName }) });
}
}}
/>
)}
</div>
) : null; ) : null;
} }

View file

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import { doRemoveUnreadSubscriptions } from 'redux/actions/subscriptions';
import MarkAsRead from './view';
export default connect(
null,
{
doRemoveUnreadSubscriptions,
}
)(MarkAsRead);

View file

@ -1,34 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import React, { PureComponent } from 'react';
import Button from 'component/button';
type Props = {
channel: ?string,
doRemoveUnreadSubscriptions: (?string) => void,
};
export default class MarkAsRead extends PureComponent<Props> {
constructor() {
super();
(this: any).handleClick = this.handleClick.bind(this);
}
handleClick() {
const { channel, doRemoveUnreadSubscriptions } = this.props;
// If there is no channel, mark all as read
// If there is a channel, only mark that channel as read
if (channel) {
doRemoveUnreadSubscriptions(channel);
} else {
doRemoveUnreadSubscriptions();
}
}
render() {
const { channel } = this.props;
const label = channel ? __('Mark as read') : __('Mark all as read');
return <Button button="inverse" icon={ICONS.COMPLETE} label={label} onClick={this.handleClick} />;
}
}

View file

@ -206,9 +206,6 @@ export const DOWNLOAD_LANGUAGE_FAILURE = 'DOWNLOAD_LANGUAGE_FAILURE';
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 UPDATE_SUBSCRIPTION_UNREADS = 'UPDATE_SUBSCRIPTION_UNREADS';
export const REMOVE_SUBSCRIPTION_UNREADS = 'REMOVE_SUBSCRIPTION_UNREADS';
export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED'; export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED'; export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE'; export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
@ -216,9 +213,6 @@ export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START';
export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL'; export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS'; export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
export const SET_VIEW_MODE = 'SET_VIEW_MODE'; export const SET_VIEW_MODE = 'SET_VIEW_MODE';
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
// Publishing // Publishing
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH'; export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';

View file

@ -33,6 +33,8 @@ export const COMPLETED = 'CheckCircle';
export const NOT_COMPLETED = 'Circle'; export const NOT_COMPLETED = 'Circle';
export const SUBSCRIBE = 'Heart'; export const SUBSCRIBE = 'Heart';
export const UNSUBSCRIBE = 'BrokenHeart'; export const UNSUBSCRIBE = 'BrokenHeart';
export const BELL = 'Bell';
export const BELL_ON = 'BellOn';
export const UNLOCK = 'Unlock'; export const UNLOCK = 'Unlock';
export const LOCK = 'Lock'; export const LOCK = 'Lock';
export const WEB = 'Globe'; export const WEB = 'Globe';

View file

@ -3,3 +3,4 @@ export const NOTIFICATION_COMMENT = 'comment';
export const NOTIFICATION_REPLY = 'comment-reply'; export const NOTIFICATION_REPLY = 'comment-reply';
export const DAILY_WATCH_AVAILABLE = 'daily_watch_available'; export const DAILY_WATCH_AVAILABLE = 'daily_watch_available';
export const DAILY_WATCH_REMIND = 'daily_watch_remind'; export const DAILY_WATCH_REMIND = 'daily_watch_remind';
export const NEW_CONTENT = 'new_content';

View file

@ -1,20 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content'; import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { import { doFetchFileInfo, makeSelectFileInfoForUri, makeSelectMetadataForUri, makeSelectClaimIsNsfw } from 'lbry-redux';
doFetchFileInfo,
makeSelectFileInfoForUri,
makeSelectMetadataForUri,
makeSelectChannelForClaimUri,
makeSelectClaimIsNsfw,
} from 'lbry-redux';
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc'; import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content'; import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import FilePage from './view';
import { makeSelectCommentForCommentId } from 'redux/selectors/comments'; import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
import FilePage from './view';
const select = (state, props) => { const select = (state, props) => {
const { search } = props.location; const { search } = props.location;
@ -28,8 +20,6 @@ const select = (state, props) => {
obscureNsfw: !selectShowMatureContent(state), obscureNsfw: !selectShowMatureContent(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state), isMature: makeSelectClaimIsNsfw(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
renderMode: makeSelectFileRenderModeForUri(props.uri)(state), renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
}; };
}; };
@ -38,7 +28,6 @@ const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)), setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
setPrimaryUri: uri => dispatch(doSetPrimaryUri(uri)), setPrimaryUri: uri => dispatch(doSetPrimaryUri(uri)),
}); });

View file

@ -19,10 +19,7 @@ type Props = {
fetchFileInfo: string => void, fetchFileInfo: string => void,
fetchCostInfo: string => void, fetchCostInfo: string => void,
setViewed: string => void, setViewed: string => void,
isSubscribed: boolean,
channelUri: string,
renderMode: string, renderMode: string,
markSubscriptionRead: (string, string) => void,
obscureNsfw: boolean, obscureNsfw: boolean,
isMature: boolean, isMature: boolean,
linkedComment: any, linkedComment: any,
@ -32,14 +29,11 @@ type Props = {
function FilePage(props: Props) { function FilePage(props: Props) {
const { const {
uri, uri,
channelUri,
renderMode, renderMode,
fetchFileInfo, fetchFileInfo,
fetchCostInfo, fetchCostInfo,
setViewed, setViewed,
isSubscribed,
fileInfo, fileInfo,
markSubscriptionRead,
obscureNsfw, obscureNsfw,
isMature, isMature,
costInfo, costInfo,
@ -68,14 +62,6 @@ function FilePage(props: Props) {
}; };
}, [uri, hasFileInfo, fetchFileInfo, fetchCostInfo, setViewed, setPrimaryUri]); }, [uri, hasFileInfo, fetchFileInfo, fetchCostInfo, setViewed, setPrimaryUri]);
React.useEffect(() => {
// Always try to remove
// If it doesn't exist, nothing will happen
if (isSubscribed) {
markSubscriptionRead(channelUri, uri);
}
}, [isSubscribed, markSubscriptionRead, uri, channelUri]);
function renderFilePageLayout() { function renderFilePageLayout() {
if (RENDER_MODES.FLOATING_MODES.includes(renderMode)) { if (RENDER_MODES.FLOATING_MODES.includes(renderMode)) {
return ( return (

View file

@ -1,8 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectFollowedTags } from 'redux/selectors/tags'; import { selectFollowedTags } from 'redux/selectors/tags';
import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions'; import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions';
import { doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
import TagsEdit from './view'; import TagsEdit from './view';
const select = state => ({ const select = state => ({
@ -11,6 +9,4 @@ const select = state => ({
suggestedSubscriptions: selectSuggestedChannels(state), suggestedSubscriptions: selectSuggestedChannels(state),
}); });
export default connect(select, { export default connect(select)(TagsEdit);
doFetchRecommendedSubscriptions,
})(TagsEdit);

View file

@ -13,7 +13,7 @@ import {
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications'; import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) { export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
const claim = selectClaimsByUri(state)[uri]; const claim = selectClaimsByUri(state)[uri];
const claimId = claim ? claim.claim_id : null; const claimId = claim ? claim.claim_id : null;
@ -50,7 +50,7 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
} }
export function doSetCommentChannel(channelName: string) { export function doSetCommentChannel(channelName: string) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch<*>) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_SET_CHANNEL, type: ACTIONS.COMMENT_SET_CHANNEL,
data: channelName, data: channelName,
@ -59,7 +59,7 @@ export function doSetCommentChannel(channelName: string) {
} }
export function doCommentReactList(uri: string | null, commentId?: string) { export function doCommentReactList(uri: string | null, commentId?: string) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
const channel = selectCommentChannel(state); const channel = selectCommentChannel(state);
const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId]; const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId];
@ -100,7 +100,7 @@ export function doCommentReactList(uri: string | null, commentId?: string) {
} }
export function doCommentReact(commentId: string, type: string) { export function doCommentReact(commentId: string, type: string) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
const channel = selectCommentChannel(state); const channel = selectCommentChannel(state);
const pendingReacts = selectPendingCommentReacts(state); const pendingReacts = selectPendingCommentReacts(state);
@ -204,7 +204,7 @@ export function doCommentCreate(
parent_id?: string, parent_id?: string,
uri: string uri: string
) { ) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
dispatch({ dispatch({
type: ACTIONS.COMMENT_CREATE_STARTED, type: ACTIONS.COMMENT_CREATE_STARTED,
@ -268,7 +268,7 @@ export function doCommentCreate(
} }
export function doCommentHide(comment_id: string) { export function doCommentHide(comment_id: string) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch<*>) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_HIDE_STARTED, type: ACTIONS.COMMENT_HIDE_STARTED,
}); });
@ -297,7 +297,7 @@ export function doCommentHide(comment_id: string) {
} }
export function doCommentPin(commentId: string, remove: boolean) { export function doCommentPin(commentId: string, remove: boolean) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
// const channel = localStorage.getItem('comment-channel'); // const channel = localStorage.getItem('comment-channel');
const channel = selectCommentChannel(state); const channel = selectCommentChannel(state);
@ -347,7 +347,7 @@ export function doCommentPin(commentId: string, remove: boolean) {
} }
export function doCommentAbandon(comment_id: string) { export function doCommentAbandon(comment_id: string) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch<*>) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_ABANDON_STARTED, type: ACTIONS.COMMENT_ABANDON_STARTED,
}); });
@ -396,7 +396,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
if (comment === '') { if (comment === '') {
return doCommentAbandon(comment_id); return doCommentAbandon(comment_id);
} else { } else {
return (dispatch: Dispatch) => { return (dispatch: Dispatch<*>) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_UPDATE_STARTED, type: ACTIONS.COMMENT_UPDATE_STARTED,
}); });

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATIONS from 'constants/notifications';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
// $FlowFixMe
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { selectNotifications } from 'redux/selectors/notifications'; import { selectNotifications } from 'redux/selectors/notifications';
import { doResolveUris } from 'lbry-redux'; import { doResolveUris } from 'lbry-redux';
@ -42,7 +42,7 @@ export function doDismissError() {
} }
export function doNotificationList() { export function doNotificationList() {
return (dispatch: Dispatch) => { return (dispatch: Dispatch<*>) => {
dispatch({ type: ACTIONS.NOTIFICATION_LIST_STARTED }); dispatch({ type: ACTIONS.NOTIFICATION_LIST_STARTED });
return Lbryio.call('notification', 'list', { is_app_readable: true }) return Lbryio.call('notification', 'list', { is_app_readable: true })
.then(response => { .then(response => {
@ -50,15 +50,22 @@ export function doNotificationList() {
const channelsToResolve = notifications const channelsToResolve = notifications
.filter((notification: WebNotification) => { .filter((notification: WebNotification) => {
if ( if (
notification.notification_parameters.dynamic && (notification.notification_parameters.dynamic &&
notification.notification_parameters.dynamic.comment_author notification.notification_parameters.dynamic.comment_author) ||
notification.notification_rule === NOTIFICATIONS.NEW_CONTENT
) { ) {
return true; return true;
} else { } else {
return false; return false;
} }
}) })
.map(notification => notification.notification_parameters.dynamic.comment_author); .map(notification => {
if (notification.notification_rule === NOTIFICATIONS.NEW_CONTENT) {
return notification.notification_parameters.device.target;
} else {
return notification.notification_parameters.dynamic.comment_author;
}
});
dispatch(doResolveUris(channelsToResolve)); dispatch(doResolveUris(channelsToResolve));
dispatch({ type: ACTIONS.NOTIFICATION_LIST_COMPLETED, data: { notifications } }); dispatch({ type: ACTIONS.NOTIFICATION_LIST_COMPLETED, data: { notifications } });
@ -70,7 +77,7 @@ export function doNotificationList() {
} }
export function doReadNotifications() { export function doReadNotifications() {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
const notifications = selectNotifications(state); const notifications = selectNotifications(state);
const unreadNotifications = const unreadNotifications =
@ -92,7 +99,7 @@ export function doReadNotifications() {
} }
export function doSeeNotifications(notificationIds: Array<string>) { export function doSeeNotifications(notificationIds: Array<string>) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch<*>) => {
dispatch({ type: ACTIONS.NOTIFICATION_SEEN_STARTED }); dispatch({ type: ACTIONS.NOTIFICATION_SEEN_STARTED });
return Lbryio.call('notification', 'edit', { notification_ids: notificationIds.join(','), is_seen: true }) return Lbryio.call('notification', 'edit', { notification_ids: notificationIds.join(','), is_seen: true })
.then(() => { .then(() => {
@ -110,7 +117,7 @@ export function doSeeNotifications(notificationIds: Array<string>) {
} }
export function doSeeAllNotifications() { export function doSeeAllNotifications() {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch<*>, getState: GetState) => {
const state = getState(); const state = getState();
const notifications = selectNotifications(state); const notifications = selectNotifications(state);
const unSeenNotifications = const unSeenNotifications =

View file

@ -15,7 +15,10 @@ import { push } from 'connected-react-router';
import analytics from 'analytics'; import analytics from 'analytics';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => { export const doPublishDesktop = (filePath: string, preview?: boolean) => (
dispatch: Dispatch<*>,
getState: () => {}
) => {
const publishPreview = previewResponse => { const publishPreview = previewResponse => {
dispatch( dispatch(
doOpenModal(MODALS.PUBLISH_PREVIEW, { doOpenModal(MODALS.PUBLISH_PREVIEW, {

View file

@ -1,127 +1,11 @@
// @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import REWARDS from 'rewards'; import REWARDS from 'rewards';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import { doClaimRewardType } from 'redux/actions/rewards'; import { doClaimRewardType } from 'redux/actions/rewards';
import { selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import { doAlertWaitingForSync } from 'redux/actions/app'; import { doAlertWaitingForSync } from 'redux/actions/app';
export const doSetViewMode = (viewMode: ViewMode) => (dispatch: Dispatch) => export const doChannelSubscribe = subscription => (dispatch, getState) => {
dispatch({
type: ACTIONS.SET_VIEW_MODE,
data: viewMode,
});
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription,
uri,
},
});
// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
channelUri: string,
uris: ?Array<string>,
type: ?SubscriptionNotificationType
) => (dispatch: Dispatch, getState: GetState) => {
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({
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
type: newType,
},
});
};
// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: ?string, readUris: ?Array<string>) => (
dispatch: Dispatch,
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 },
});
}
const currentChannelUnread = unreadByChannel[channelUri];
if (!currentChannelUnread || !currentChannelUnread.uris) {
// Channel passed in doesn't have any unreads
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;
}
dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
},
});
};
// Remove a single file from a channels unread subscriptions
export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) => (dispatch: Dispatch) => {
dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
const { const {
settings: { daemonSettings }, settings: { daemonSettings },
sync: { prefsReady: ready }, sync: { prefsReady: ready },
@ -151,13 +35,14 @@ export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dis
Lbryio.call('subscription', 'new', { Lbryio.call('subscription', 'new', {
channel_name: subscription.channelName, channel_name: subscription.channelName,
claim_id: channelClaimId, claim_id: channelClaimId,
notifications_disabled: subscription.notificationsDisabled,
}); });
dispatch(doClaimRewardType(REWARDS.TYPE_SUBSCRIPTION, { failSilently: true })); dispatch(doClaimRewardType(REWARDS.TYPE_SUBSCRIPTION, { failSilently: true }));
} }
}; };
export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => { export const doChannelUnsubscribe = subscription => (dispatch, getState) => {
const { const {
settings: { daemonSettings }, settings: { daemonSettings },
sync: { prefsReady: ready }, sync: { prefsReady: ready },
@ -182,23 +67,3 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: D
}); });
} }
}; };
export const doFetchRecommendedSubscriptions = () => (dispatch: Dispatch) => {
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,
})
);
};

View file

@ -1,106 +1,64 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { VIEW_ALL } from 'constants/subscriptions';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
const defaultState: SubscriptionState = { const defaultState: SubscriptionState = {
subscriptions: [], subscriptions: [], // Deprecated
unread: {}, following: [],
suggested: {},
loading: false, loading: false,
viewMode: VIEW_ALL,
loadingSuggested: false,
firstRunCompleted: false, firstRunCompleted: false,
showSuggestedSubs: false,
enabledChannelNotifications: [],
}; };
export default handleActions( export default handleActions(
{ {
[ACTIONS.CHANNEL_SUBSCRIBE]: (state: SubscriptionState, action: DoChannelSubscribe): SubscriptionState => { [ACTIONS.CHANNEL_SUBSCRIBE]: (state: SubscriptionState, action): SubscriptionState => {
const newSubscription: Subscription = action.data; const newSubscription: { uri: string, channelName: string, notificationsDisabled: boolean } = action.data;
const newSubscriptions: Array<Subscription> = state.subscriptions.slice(); const newSubscriptions: Array<Subscription> = state.subscriptions.slice();
let newFollowing: Array<Following> = state.following.slice();
// prevent duplicates in the sidebar // prevent duplicates in the sidebar
if (!newSubscriptions.some(sub => sub.uri === newSubscription.uri)) { if (!newSubscriptions.some(sub => sub.uri === newSubscription.uri)) {
// $FlowFixMe
newSubscriptions.unshift(newSubscription); newSubscriptions.unshift(newSubscription);
} }
if (!newFollowing.some(sub => sub.uri === newSubscription.uri)) {
newFollowing.unshift({
uri: newSubscription.uri,
notificationsDisabled: newSubscription.notificationsDisabled,
});
} else {
newFollowing = newFollowing.map(following => {
if (following.uri === newSubscription.uri) {
return {
uri: newSubscription.uri,
notificationsDisabled: newSubscription.notificationsDisabled,
};
} else {
return following;
}
});
}
return { return {
...state, ...state,
subscriptions: newSubscriptions, subscriptions: newSubscriptions,
following: newFollowing,
}; };
}, },
[ACTIONS.CHANNEL_UNSUBSCRIBE]: (state: SubscriptionState, action: DoChannelUnsubscribe): SubscriptionState => { [ACTIONS.CHANNEL_UNSUBSCRIBE]: (state: SubscriptionState, action): SubscriptionState => {
const subscriptionToRemove: Subscription = action.data; const subscriptionToRemove: Subscription = action.data;
const newSubscriptions = state.subscriptions const newSubscriptions = state.subscriptions
.slice() .slice()
.filter(subscription => subscription.channelName !== subscriptionToRemove.channelName); .filter(subscription => subscription.channelName !== subscriptionToRemove.channelName);
const newFollowing = state.following
.slice()
.filter(subscription => subscription.uri !== subscriptionToRemove.uri);
// Check if we need to remove it from the 'unread' state
const { unread } = state;
if (unread[subscriptionToRemove.uri]) {
delete unread[subscriptionToRemove.uri];
}
return { return {
...state, ...state,
unread: { ...unread },
subscriptions: newSubscriptions, subscriptions: newSubscriptions,
}; following: newFollowing,
},
[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
),
}),
[ACTIONS.UPDATE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: DoUpdateSubscriptionUnreads
): SubscriptionState => {
const { channel, uris, type } = action.data;
return {
...state,
unread: {
...state.unread,
[channel]: {
uris,
type,
},
},
};
},
[ACTIONS.REMOVE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: DoRemoveSubscriptionUnreads
): SubscriptionState => {
const { channel, uris } = action.data;
// If no channel is passed in, remove all unreads
let newUnread;
if (channel) {
newUnread = { ...state.unread };
if (!uris) {
delete newUnread[channel];
} else {
newUnread[channel].uris = uris;
}
} else {
newUnread = {};
}
return {
...state,
unread: {
...newUnread,
},
}; };
}, },
[ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({ [ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
@ -111,39 +69,20 @@ export default handleActions(
...state, ...state,
loading: false, loading: false,
}), }),
[ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: ( [ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: (state: SubscriptionState, action): SubscriptionState => ({
state: SubscriptionState,
action: FetchedSubscriptionsSucess
): SubscriptionState => ({
...state, ...state,
loading: false, loading: false,
subscriptions: action.data, subscriptions: action.data,
}), }),
[ACTIONS.SET_VIEW_MODE]: (state: SubscriptionState, action: SetViewMode): SubscriptionState => ({ [ACTIONS.SET_VIEW_MODE]: (state: SubscriptionState, action): SubscriptionState => ({
...state, ...state,
viewMode: action.data, viewMode: action.data,
}), }),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: true,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: GetSuggestedSubscriptionsSuccess
): SubscriptionState => ({
...state,
suggested: action.data,
loadingSuggested: false,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: false,
}),
[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: ( [LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: (
state: SubscriptionState, state: SubscriptionState,
action: { data: { subscriptions: ?Array<string> } } action: { data: { subscriptions: ?Array<string>, following: ?Array<Subscription> } }
) => { ) => {
const { subscriptions } = action.data; const { subscriptions, following } = action.data;
const incomingSubscriptions = Array.isArray(subscriptions) && subscriptions.length; const incomingSubscriptions = Array.isArray(subscriptions) && subscriptions.length;
if (!incomingSubscriptions) { if (!incomingSubscriptions) {
return { return {
@ -151,6 +90,7 @@ export default handleActions(
}; };
} }
let newSubscriptions; let newSubscriptions;
let newFollowing;
if (!subscriptions) { if (!subscriptions) {
newSubscriptions = state.subscriptions; newSubscriptions = state.subscriptions;
@ -166,9 +106,24 @@ export default handleActions(
newSubscriptions = parsedSubscriptions; newSubscriptions = parsedSubscriptions;
} }
if (!following) {
newFollowing = newSubscriptions.slice().map(({ uri }) => {
return {
uri,
// Default first time movers to notifications on
// This value is for email notifications too so we can't default off
// New subscriptions after population will default off
notificationsDisabled: false,
};
});
} else {
newFollowing = following;
}
return { return {
...state, ...state,
subscriptions: newSubscriptions, subscriptions: newSubscriptions,
following: newFollowing,
}; };
}, },
}, },

View file

@ -1,11 +1,8 @@
import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions'; import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { import {
selectAllClaimsByChannel,
selectClaimsById,
selectAllFetchingChannelClaims, selectAllFetchingChannelClaims,
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
selectClaimsByUri,
parseURI, parseURI,
makeSelectClaimForUri, makeSelectClaimForUri,
} from 'lbry-redux'; } from 'lbry-redux';
@ -20,27 +17,17 @@ export const selectSubscriptions = createSelector(
state => state.subscriptions && state.subscriptions.sort((a, b) => a.channelName.localeCompare(b.channelName)) state => state.subscriptions && state.subscriptions.sort((a, b) => a.channelName.localeCompare(b.channelName))
); );
export const selectFollowing = createSelector(selectState, state => state.following && state.following);
// Fetching list of users subscriptions // Fetching list of users subscriptions
export const selectIsFetchingSubscriptions = createSelector( export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
selectState,
state => state.loading
);
// The current view mode on the subscriptions page // The current view mode on the subscriptions page
export const selectViewMode = createSelector( export const selectViewMode = createSelector(selectState, state => state.viewMode);
selectState,
state => state.viewMode
);
// Suggested subscriptions from internal apis // Suggested subscriptions from internal apis
export const selectSuggested = createSelector( export const selectSuggested = createSelector(selectState, state => state.suggested);
selectState, export const selectIsFetchingSuggested = createSelector(selectState, state => state.loadingSuggested);
state => state.suggested
);
export const selectIsFetchingSuggested = createSelector(
selectState,
state => state.loadingSuggested
);
export const selectSuggestedChannels = createSelector( export const selectSuggestedChannels = createSelector(
selectSubscriptions, selectSubscriptions,
selectSuggested, selectSuggested,
@ -93,14 +80,8 @@ export const selectSuggestedChannels = createSelector(
} }
); );
export const selectFirstRunCompleted = createSelector( export const selectFirstRunCompleted = createSelector(selectState, state => state.firstRunCompleted);
selectState, export const selectshowSuggestedSubs = createSelector(selectState, state => state.showSuggestedSubs);
state => state.firstRunCompleted
);
export const selectshowSuggestedSubs = createSelector(
selectState,
state => state.showSuggestedSubs
);
// Fetching any claims that are a part of a users subscriptions // Fetching any claims that are a part of a users subscriptions
export const selectSubscriptionsBeingFetched = createSelector( export const selectSubscriptionsBeingFetched = createSelector(
@ -119,149 +100,10 @@ export const selectSubscriptionsBeingFetched = createSelector(
} }
); );
export const selectUnreadByChannel = createSelector(
selectState,
state => state.unread
);
// Returns the current total of unread subscriptions
export const selectUnreadAmount = createSelector(
selectUnreadByChannel,
unreadByChannel => {
const unreadChannels = Object.keys(unreadByChannel);
let badges = 0;
if (!unreadChannels.length) {
return badges;
}
unreadChannels.forEach(channel => {
badges += unreadByChannel[channel].uris.length;
});
return badges;
}
);
// Returns the uris with channels as an array with the channel with the newest content first
// If you just want the `unread` state, use selectUnread
export const selectUnreadSubscriptions = createSelector(
selectUnreadAmount,
selectUnreadByChannel,
selectClaimsByUri,
(unreadAmount, unreadByChannel, claimsByUri) => {
// determine which channel has the newest content
const unreadList = [];
if (!unreadAmount) {
return unreadList;
}
const channelUriList = Object.keys(unreadByChannel);
// There is only one channel with unread notifications
if (unreadAmount === 1) {
channelUriList.forEach(channel => {
const unreadChannel = {
channel,
uris: unreadByChannel[channel].uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
channelUriList
.sort((channel1, channel2) => {
const latestUriFromChannel1 = unreadByChannel[channel1].uris[0];
const latestClaimFromChannel1 = claimsByUri[latestUriFromChannel1] || {};
const latestUriFromChannel2 = unreadByChannel[channel2].uris[0];
const latestClaimFromChannel2 = claimsByUri[latestUriFromChannel2] || {};
const latestHeightFromChannel1 = latestClaimFromChannel1.height || 0;
const latestHeightFromChannel2 = latestClaimFromChannel2.height || 0;
if (latestHeightFromChannel1 !== latestHeightFromChannel2) {
return latestHeightFromChannel2 - latestHeightFromChannel1;
}
return 0;
})
.forEach(channel => {
const unreadSubscription = unreadByChannel[channel];
const unreadChannel = {
channel,
uris: unreadSubscription.uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
);
// Returns all unread subscriptions for a uri passed in
export const makeSelectUnreadByChannel = uri =>
createSelector(
selectUnreadByChannel,
unread => unread[uri]
);
// Returns the first page of claims for every channel a user is subscribed to
export const selectSubscriptionClaims = createSelector(
selectAllClaimsByChannel,
selectClaimsById,
selectSubscriptions,
selectUnreadByChannel,
(channelIds, allClaims, savedSubscriptions, unreadByChannel) => {
// no claims loaded yet
if (!Object.keys(channelIds).length) {
return [];
}
let fetchedSubscriptions = [];
savedSubscriptions.forEach(subscription => {
let channelClaims = [];
// if subscribed channel has content
if (channelIds[subscription.uri] && channelIds[subscription.uri]['1']) {
// This will need to be more robust, we will want to be able to load more than the first page
// Strip out any ids that will be shown as notifications
const pageOneChannelIds = channelIds[subscription.uri]['1'];
// we have the channel ids and the corresponding claims
// loop over the list of ids and grab the claim
pageOneChannelIds.forEach(id => {
const grabbedClaim = allClaims[id];
if (
unreadByChannel[subscription.uri] &&
unreadByChannel[subscription.uri].uris.some(uri => uri.includes(id))
) {
grabbedClaim.isNew = true;
}
channelClaims = channelClaims.concat([grabbedClaim]);
});
}
fetchedSubscriptions = fetchedSubscriptions.concat(channelClaims);
});
return fetchedSubscriptions;
}
);
// Returns true if a user is subscribed to the channel associated with the uri passed in // Returns true if a user is subscribed to the channel associated with the uri passed in
// Accepts content or channel uris // Accepts content or channel uris
export const makeSelectChannelInSubscriptions = uri => export const makeSelectChannelInSubscriptions = uri =>
createSelector( createSelector(selectSubscriptions, subscriptions => subscriptions.some(sub => sub.uri === uri));
selectSubscriptions,
subscriptions => subscriptions.some(sub => sub.uri === uri)
);
export const makeSelectIsSubscribed = uri => export const makeSelectIsSubscribed = uri =>
createSelector( createSelector(
@ -288,22 +130,31 @@ export const makeSelectIsSubscribed = uri =>
} }
); );
export const makeSelectIsNew = uri => export const makeSelectNotificationsDisabled = uri =>
createSelector( createSelector(
makeSelectIsSubscribed(uri), selectFollowing,
makeSelectChannelForClaimUri(uri), makeSelectChannelForClaimUri(uri, true),
selectUnreadByChannel, makeSelectClaimForUri(uri),
(isSubscribed, channel, unreadByChannel) => { (following, channelUri, claim) => {
if (!isSubscribed) { if (channelUri) {
return false; return following.some(following => following.uri === channelUri && following.notificationsDisabled);
} }
const unreadForChannel = unreadByChannel[`lbry://${channel}`]; // If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
if (unreadForChannel) { let isChannel;
return unreadForChannel.uris.includes(uri); try {
({ isChannel } = parseURI(uri));
} catch (e) {}
if (isChannel && claim) {
const uri = claim.permanent_url;
const disabled = following.some(sub => {
return sub.uri === uri && sub.notificationsDisabled === true;
});
return disabled;
} }
return false; return true;
// If they are subscribed, check to see if this uri is in the list of unreads
} }
); );

View file

@ -185,7 +185,8 @@ $metadata-z-index: 1;
flex-wrap: wrap; flex-wrap: wrap;
font-size: var(--font-base); font-size: var(--font-base);
> * { > .button,
> .button-group .button {
padding: 0 var(--spacing-xs); padding: 0 var(--spacing-xs);
&:not(:last-child) { &:not(:last-child) {
@ -193,6 +194,12 @@ $metadata-z-index: 1;
} }
} }
.button-group .button {
&:not(:last-child) {
margin-right: 0;
}
}
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
flex-direction: column; flex-direction: column;

View file

@ -14,6 +14,9 @@
} }
.notification__icon { .notification__icon {
display: flex;
align-items: flex-start;
.icon__wrapper { .icon__wrapper {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@ -25,6 +28,10 @@
padding: 1.5rem; padding: 1.5rem;
} }
} }
@media (min-width: $breakpoint-small) {
align-items: center;
}
} }
.notification__wrapper { .notification__wrapper {
@ -72,7 +79,7 @@
color: var(--color-text); color: var(--color-text);
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
@ -84,7 +91,7 @@
.notification__text { .notification__text {
font-size: var(--font-body); font-size: var(--font-body);
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
@ -146,3 +153,19 @@
margin-top: 0; margin-top: 0;
} }
} }
.notification__content-thumbnail {
@include thumbnail;
position: relative;
margin-right: auto;
height: auto;
width: 100%;
margin-top: var(--spacing-s);
@media (min-width: $breakpoint-small) {
height: 4rem;
width: calc(4rem * 16 / 9);
margin-left: var(--spacing-s);
margin-top: 0;
}
}

View file

@ -147,6 +147,10 @@ const sharedStateFilters = {
return value.map(({ uri }) => uri); return value.map(({ uri }) => uri);
}, },
}, },
following: {
source: 'subscriptions',
property: 'following',
},
blocked: { source: 'blocked', property: 'blockedChannels' }, blocked: { source: 'blocked', property: 'blockedChannels' },
settings: { source: 'settings', property: 'sharedPreferences' }, settings: { source: 'settings', property: 'sharedPreferences' },
app_welcome_version: { source: 'app', property: 'welcomeVersion' }, app_welcome_version: { source: 'app', property: 'welcomeVersion' },

View file

@ -7411,17 +7411,23 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
<<<<<<< HEAD
lbry-redux@lbryio/lbry-redux#04789190b060f27bf0c438a9af449b4a18ef4925: lbry-redux@lbryio/lbry-redux#04789190b060f27bf0c438a9af449b4a18ef4925:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/04789190b060f27bf0c438a9af449b4a18ef4925" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/04789190b060f27bf0c438a9af449b4a18ef4925"
=======
lbry-redux@lbryio/lbry-redux#4d11f319141ba49a26f06053c42c01f00dd4feaf:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/4d11f319141ba49a26f06053c42c01f00dd4feaf"
>>>>>>> per channel notification settings
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
uuid "^8.3.1" uuid "^8.3.1"
lbryinc@lbryio/lbryinc#517c28a183d6ab69a357227809bc7c3c12d8411e: lbryinc@lbryio/lbryinc#9bdf18eef6a65aef7d92ce1040b6f8d3db1be671:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/517c28a183d6ab69a357227809bc7c3c12d8411e" resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/9bdf18eef6a65aef7d92ce1040b6f8d3db1be671"
dependencies: dependencies:
reselect "^3.0.0" reselect "^3.0.0"