per channel notification settings
This commit is contained in:
parent
07377a4e2a
commit
63f1fed33c
31 changed files with 295 additions and 660 deletions
|
@ -1,6 +1,8 @@
|
|||
[ignore]
|
||||
.*\.typeface\.json
|
||||
.*/node_modules/findup/.*
|
||||
node_modules/lbryinc/flow-typed/Redux.js
|
||||
node_modules/lbry-redux/flow-typed/Redux.js
|
||||
|
||||
[include]
|
||||
|
||||
|
|
1
flow-typed/notification.js
vendored
1
flow-typed/notification.js
vendored
|
@ -26,6 +26,7 @@ declare type WebNotification = {
|
|||
hash: string,
|
||||
claim_title: string,
|
||||
comment?: string,
|
||||
channel_url: string,
|
||||
},
|
||||
email: {},
|
||||
},
|
||||
|
|
3
flow-typed/redux.js
vendored
Normal file
3
flow-typed/redux.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
// @flow
|
||||
|
||||
declare type Dispatch = any;
|
92
flow-typed/subscription.js
vendored
92
flow-typed/subscription.js
vendored
|
@ -14,99 +14,17 @@ import {
|
|||
declare type Subscription = {
|
||||
channelName: string, // @CryptoCandor,
|
||||
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
|
||||
latest?: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620
|
||||
notificationsDisabled?: boolean,
|
||||
};
|
||||
|
||||
// Tracking for new content
|
||||
// i.e. If a subscription has a DOWNLOADING type, we will trigger an OS notification
|
||||
// to tell users there is new content from their subscriptions
|
||||
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 Following = {
|
||||
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
|
||||
notificationsDisabled: boolean,
|
||||
};
|
||||
|
||||
declare type SubscriptionState = {
|
||||
subscriptions: Array<Subscription>,
|
||||
unread: UnreadSubscriptions,
|
||||
following: Array<Following>,
|
||||
loading: boolean,
|
||||
viewMode: ViewMode,
|
||||
suggested: SuggestedSubscriptions,
|
||||
loadingSuggested: 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,
|
||||
};
|
||||
|
|
|
@ -136,8 +136,8 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#04789190b060f27bf0c438a9af449b4a18ef4925",
|
||||
"lbryinc": "lbryio/lbryinc#517c28a183d6ab69a357227809bc7c3c12d8411e",
|
||||
"lbry-redux": "lbryio/lbry-redux#4d11f319141ba49a26f06053c42c01f00dd4feaf",
|
||||
"lbryinc": "lbryio/lbryinc#9bdf18eef6a65aef7d92ce1040b6f8d3db1be671",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
"lodash-es": "^4.17.14",
|
||||
|
|
|
@ -860,6 +860,24 @@ export const icons = {
|
|||
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) => (
|
||||
<svg
|
||||
{...props}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux';
|
||||
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import FileProperties from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
downloaded: makeSelectFilePartlyDownloaded(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
isNew: makeSelectIsNew(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ type Props = {
|
|||
downloaded: boolean,
|
||||
claimIsMine: boolean,
|
||||
isSubscribed: boolean,
|
||||
isNew: boolean,
|
||||
small: boolean,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doResolveUri, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import CardMedia from './view';
|
||||
|
||||
export default CardMedia;
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select, { doResolveUri })(CardMedia);
|
||||
|
|
|
@ -3,48 +3,52 @@ import type { Node } from 'react';
|
|||
import React from 'react';
|
||||
import FreezeframeWrapper from './FreezeframeWrapper';
|
||||
import Placeholder from './placeholder.png';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
thumbnail: ?string, // externally sourced image
|
||||
children?: Node,
|
||||
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> {
|
||||
render() {
|
||||
const { thumbnail: rawThumbnail, children, allowGifs = false } = this.props;
|
||||
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
||||
|
||||
if (!allowGifs && thumbnail && thumbnail.endsWith('gif')) {
|
||||
return (
|
||||
<FreezeframeWrapper src={thumbnail} className={className}>
|
||||
{children}
|
||||
</FreezeframeWrapper>
|
||||
);
|
||||
React.useEffect(() => {
|
||||
if (!hasResolvedClaim) {
|
||||
doResolveUri(uri);
|
||||
}
|
||||
}, [hasResolvedClaim, uri, doResolveUri]);
|
||||
|
||||
let url;
|
||||
// @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
|
||||
|
||||
if (!allowGifs && thumbnail && thumbnail.endsWith('gif')) {
|
||||
return (
|
||||
<div style={{ backgroundImage: `url('${url.replace(/'/g, "\\'")}')` }} className={className}>
|
||||
<FreezeframeWrapper src={thumbnail} className={classnames('media__thumb', className)}>
|
||||
{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;
|
||||
|
|
|
@ -14,6 +14,7 @@ import { formatLbryUrlForWeb } from 'util/url';
|
|||
import { useHistory } from 'react-router';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
|
||||
type Props = {
|
||||
notification: WebNotification,
|
||||
|
@ -66,6 +67,9 @@ export default function Notification(props: Props) {
|
|||
case NOTIFICATIONS.NOTIFICATION_REPLY:
|
||||
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.reply_author} />;
|
||||
break;
|
||||
case NOTIFICATIONS.NEW_CONTENT:
|
||||
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.channel_url} />;
|
||||
break;
|
||||
case NOTIFICATIONS.DAILY_WATCH_AVAILABLE:
|
||||
case NOTIFICATIONS.DAILY_WATCH_REMIND:
|
||||
icon = <Icon icon={ICONS.LBC} sectionIcon className="notification__icon" />;
|
||||
|
@ -136,6 +140,12 @@ export default function Notification(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{notification_rule === NOTIFICATIONS.NEW_CONTENT && (
|
||||
<>
|
||||
<FileThumbnail uri={notification_parameters.device.target} className="notification__content-thumbnail" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="notification__extra">
|
||||
<div className="notification__time">
|
||||
<DateTime timeAgo date={notification.active_at} />
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
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 { doToast } from 'redux/actions/notifications';
|
||||
import SubscribeButton from './view';
|
||||
|
@ -9,6 +13,7 @@ const select = (state, props) => ({
|
|||
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
|
||||
firstRunCompleted: selectFirstRunCompleted(state),
|
||||
permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state),
|
||||
notificationsDisabled: makeSelectNotificationsDisabled(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
|
|
|
@ -9,16 +9,18 @@ import { useIsMobile } from 'effects/use-screensize';
|
|||
type SubscriptionArgs = {
|
||||
channelName: string,
|
||||
uri: string,
|
||||
notificationsDisabled?: boolean,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
permanentUrl: ?string,
|
||||
isSubscribed: boolean,
|
||||
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
|
||||
doChannelSubscribe: SubscriptionArgs => void,
|
||||
doChannelUnsubscribe: SubscriptionArgs => void,
|
||||
showSnackBarOnSubscribe: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
shrinkOnMobile: boolean,
|
||||
notificationsDisabled: boolean,
|
||||
};
|
||||
|
||||
export default function SubscribeButton(props: Props) {
|
||||
|
@ -30,6 +32,7 @@ export default function SubscribeButton(props: Props) {
|
|||
showSnackBarOnSubscribe,
|
||||
doToast,
|
||||
shrinkOnMobile = false,
|
||||
notificationsDisabled,
|
||||
} = props;
|
||||
|
||||
const buttonRef = useRef();
|
||||
|
@ -50,27 +53,49 @@ export default function SubscribeButton(props: Props) {
|
|||
const titlePrefix = isSubscribed ? __('Unfollow this channel') : __('Follow this channel');
|
||||
|
||||
return permanentUrl ? (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
iconColor="red"
|
||||
largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
|
||||
icon={unfollowOverride ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIBE}
|
||||
button={'alt'}
|
||||
requiresAuth={IS_WEB}
|
||||
label={label}
|
||||
title={titlePrefix}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
<div className="button-group">
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
iconColor="red"
|
||||
largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
|
||||
icon={unfollowOverride ? ICONS.UNSUBSCRIBE : ICONS.SUBSCRIBE}
|
||||
button={'alt'}
|
||||
requiresAuth={IS_WEB}
|
||||
label={label}
|
||||
title={titlePrefix}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
subscriptionHandler({
|
||||
channelName: claimName,
|
||||
uri: permanentUrl,
|
||||
});
|
||||
subscriptionHandler({
|
||||
channelName: claimName,
|
||||
uri: permanentUrl,
|
||||
notificationsDisabled: true,
|
||||
});
|
||||
|
||||
if (showSnackBarOnSubscribe) {
|
||||
doToast({ message: `${__('Now following ')} ${claimName}!` });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
if (showSnackBarOnSubscribe) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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} />;
|
||||
}
|
||||
}
|
|
@ -206,9 +206,6 @@ export const DOWNLOAD_LANGUAGE_FAILURE = 'DOWNLOAD_LANGUAGE_FAILURE';
|
|||
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
|
||||
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
|
||||
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
|
||||
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST';
|
||||
export const 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_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
|
||||
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_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
|
||||
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
|
||||
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
|
||||
|
|
|
@ -33,6 +33,8 @@ export const COMPLETED = 'CheckCircle';
|
|||
export const NOT_COMPLETED = 'Circle';
|
||||
export const SUBSCRIBE = 'Heart';
|
||||
export const UNSUBSCRIBE = 'BrokenHeart';
|
||||
export const BELL = 'Bell';
|
||||
export const BELL_ON = 'BellOn';
|
||||
export const UNLOCK = 'Unlock';
|
||||
export const LOCK = 'Lock';
|
||||
export const WEB = 'Globe';
|
||||
|
|
|
@ -3,3 +3,4 @@ export const NOTIFICATION_COMMENT = 'comment';
|
|||
export const NOTIFICATION_REPLY = 'comment-reply';
|
||||
export const DAILY_WATCH_AVAILABLE = 'daily_watch_available';
|
||||
export const DAILY_WATCH_REMIND = 'daily_watch_remind';
|
||||
export const NEW_CONTENT = 'new_content';
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
||||
import { doSetContentHistoryItem, doSetPrimaryUri } from 'redux/actions/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
doFetchFileInfo,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectChannelForClaimUri,
|
||||
makeSelectClaimIsNsfw,
|
||||
} from 'lbry-redux';
|
||||
import { doFetchFileInfo, makeSelectFileInfoForUri, makeSelectMetadataForUri, makeSelectClaimIsNsfw } from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import FilePage from './view';
|
||||
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
||||
import FilePage from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
|
@ -28,8 +20,6 @@ const select = (state, props) => {
|
|||
obscureNsfw: !selectShowMatureContent(state),
|
||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
};
|
||||
};
|
||||
|
@ -38,7 +28,6 @@ const perform = dispatch => ({
|
|||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
|
||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||
setPrimaryUri: uri => dispatch(doSetPrimaryUri(uri)),
|
||||
});
|
||||
|
||||
|
|
|
@ -19,10 +19,7 @@ type Props = {
|
|||
fetchFileInfo: string => void,
|
||||
fetchCostInfo: string => void,
|
||||
setViewed: string => void,
|
||||
isSubscribed: boolean,
|
||||
channelUri: string,
|
||||
renderMode: string,
|
||||
markSubscriptionRead: (string, string) => void,
|
||||
obscureNsfw: boolean,
|
||||
isMature: boolean,
|
||||
linkedComment: any,
|
||||
|
@ -32,14 +29,11 @@ type Props = {
|
|||
function FilePage(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
channelUri,
|
||||
renderMode,
|
||||
fetchFileInfo,
|
||||
fetchCostInfo,
|
||||
setViewed,
|
||||
isSubscribed,
|
||||
fileInfo,
|
||||
markSubscriptionRead,
|
||||
obscureNsfw,
|
||||
isMature,
|
||||
costInfo,
|
||||
|
@ -68,14 +62,6 @@ function FilePage(props: Props) {
|
|||
};
|
||||
}, [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() {
|
||||
if (RENDER_MODES.FLOATING_MODES.includes(renderMode)) {
|
||||
return (
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions';
|
||||
import { doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
|
||||
|
||||
import TagsEdit from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
@ -11,6 +9,4 @@ const select = state => ({
|
|||
suggestedSubscriptions: selectSuggestedChannels(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doFetchRecommendedSubscriptions,
|
||||
})(TagsEdit);
|
||||
export default connect(select)(TagsEdit);
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { makeSelectNotificationForCommentId } from 'redux/selectors/notifications';
|
||||
|
||||
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 claim = selectClaimsByUri(state)[uri];
|
||||
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) {
|
||||
return (dispatch: Dispatch) => {
|
||||
return (dispatch: Dispatch<*>) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_SET_CHANNEL,
|
||||
data: channelName,
|
||||
|
@ -59,7 +59,7 @@ export function doSetCommentChannel(channelName: string) {
|
|||
}
|
||||
|
||||
export function doCommentReactList(uri: string | null, commentId?: string) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return (dispatch: Dispatch<*>, getState: GetState) => {
|
||||
const state = getState();
|
||||
const channel = selectCommentChannel(state);
|
||||
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) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return (dispatch: Dispatch<*>, getState: GetState) => {
|
||||
const state = getState();
|
||||
const channel = selectCommentChannel(state);
|
||||
const pendingReacts = selectPendingCommentReacts(state);
|
||||
|
@ -204,7 +204,7 @@ export function doCommentCreate(
|
|||
parent_id?: string,
|
||||
uri: string
|
||||
) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return (dispatch: Dispatch<*>, getState: GetState) => {
|
||||
const state = getState();
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_CREATE_STARTED,
|
||||
|
@ -268,7 +268,7 @@ export function doCommentCreate(
|
|||
}
|
||||
|
||||
export function doCommentHide(comment_id: string) {
|
||||
return (dispatch: Dispatch) => {
|
||||
return (dispatch: Dispatch<*>) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_HIDE_STARTED,
|
||||
});
|
||||
|
@ -297,7 +297,7 @@ export function doCommentHide(comment_id: string) {
|
|||
}
|
||||
|
||||
export function doCommentPin(commentId: string, remove: boolean) {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return (dispatch: Dispatch<*>, getState: GetState) => {
|
||||
const state = getState();
|
||||
// const channel = localStorage.getItem('comment-channel');
|
||||
const channel = selectCommentChannel(state);
|
||||
|
@ -347,7 +347,7 @@ export function doCommentPin(commentId: string, remove: boolean) {
|
|||
}
|
||||
|
||||
export function doCommentAbandon(comment_id: string) {
|
||||
return (dispatch: Dispatch) => {
|
||||
return (dispatch: Dispatch<*>) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_ABANDON_STARTED,
|
||||
});
|
||||
|
@ -396,7 +396,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
|||
if (comment === '') {
|
||||
return doCommentAbandon(comment_id);
|
||||
} else {
|
||||
return (dispatch: Dispatch) => {
|
||||
return (dispatch: Dispatch<*>) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_UPDATE_STARTED,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as NOTIFICATIONS from 'constants/notifications';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
// $FlowFixMe
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { selectNotifications } from 'redux/selectors/notifications';
|
||||
import { doResolveUris } from 'lbry-redux';
|
||||
|
@ -42,7 +42,7 @@ export function doDismissError() {
|
|||
}
|
||||
|
||||
export function doNotificationList() {
|
||||
return (dispatch: Dispatch) => {
|
||||
return (dispatch: Dispatch<*>) => {
|
||||
dispatch({ type: ACTIONS.NOTIFICATION_LIST_STARTED });
|
||||
return Lbryio.call('notification', 'list', { is_app_readable: true })
|
||||
.then(response => {
|
||||
|
@ -50,15 +50,22 @@ export function doNotificationList() {
|
|||
const channelsToResolve = notifications
|
||||
.filter((notification: WebNotification) => {
|
||||
if (
|
||||
notification.notification_parameters.dynamic &&
|
||||
notification.notification_parameters.dynamic.comment_author
|
||||
(notification.notification_parameters.dynamic &&
|
||||
notification.notification_parameters.dynamic.comment_author) ||
|
||||
notification.notification_rule === NOTIFICATIONS.NEW_CONTENT
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
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({ type: ACTIONS.NOTIFICATION_LIST_COMPLETED, data: { notifications } });
|
||||
|
@ -70,7 +77,7 @@ export function doNotificationList() {
|
|||
}
|
||||
|
||||
export function doReadNotifications() {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return (dispatch: Dispatch<*>, getState: GetState) => {
|
||||
const state = getState();
|
||||
const notifications = selectNotifications(state);
|
||||
const unreadNotifications =
|
||||
|
@ -92,7 +99,7 @@ export function doReadNotifications() {
|
|||
}
|
||||
|
||||
export function doSeeNotifications(notificationIds: Array<string>) {
|
||||
return (dispatch: Dispatch) => {
|
||||
return (dispatch: Dispatch<*>) => {
|
||||
dispatch({ type: ACTIONS.NOTIFICATION_SEEN_STARTED });
|
||||
return Lbryio.call('notification', 'edit', { notification_ids: notificationIds.join(','), is_seen: true })
|
||||
.then(() => {
|
||||
|
@ -110,7 +117,7 @@ export function doSeeNotifications(notificationIds: Array<string>) {
|
|||
}
|
||||
|
||||
export function doSeeAllNotifications() {
|
||||
return (dispatch: Dispatch, getState: GetState) => {
|
||||
return (dispatch: Dispatch<*>, getState: GetState) => {
|
||||
const state = getState();
|
||||
const notifications = selectNotifications(state);
|
||||
const unSeenNotifications =
|
||||
|
|
|
@ -15,7 +15,10 @@ import { push } from 'connected-react-router';
|
|||
import analytics from 'analytics';
|
||||
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 => {
|
||||
dispatch(
|
||||
doOpenModal(MODALS.PUBLISH_PREVIEW, {
|
||||
|
|
|
@ -1,127 +1,11 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import REWARDS from 'rewards';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { doClaimRewardType } from 'redux/actions/rewards';
|
||||
import { selectUnreadByChannel } from 'redux/selectors/subscriptions';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import { doAlertWaitingForSync } from 'redux/actions/app';
|
||||
|
||||
export const doSetViewMode = (viewMode: ViewMode) => (dispatch: Dispatch) =>
|
||||
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) => {
|
||||
export const doChannelSubscribe = subscription => (dispatch, getState) => {
|
||||
const {
|
||||
settings: { daemonSettings },
|
||||
sync: { prefsReady: ready },
|
||||
|
@ -151,13 +35,14 @@ export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dis
|
|||
Lbryio.call('subscription', 'new', {
|
||||
channel_name: subscription.channelName,
|
||||
claim_id: channelClaimId,
|
||||
notifications_disabled: subscription.notificationsDisabled,
|
||||
});
|
||||
|
||||
dispatch(doClaimRewardType(REWARDS.TYPE_SUBSCRIPTION, { failSilently: true }));
|
||||
}
|
||||
};
|
||||
|
||||
export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch, getState: GetState) => {
|
||||
export const doChannelUnsubscribe = subscription => (dispatch, getState) => {
|
||||
const {
|
||||
settings: { daemonSettings },
|
||||
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,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,106 +1,64 @@
|
|||
// @flow
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
|
||||
import { VIEW_ALL } from 'constants/subscriptions';
|
||||
import { handleActions } from 'util/redux-utils';
|
||||
|
||||
const defaultState: SubscriptionState = {
|
||||
subscriptions: [],
|
||||
unread: {},
|
||||
suggested: {},
|
||||
subscriptions: [], // Deprecated
|
||||
following: [],
|
||||
loading: false,
|
||||
viewMode: VIEW_ALL,
|
||||
loadingSuggested: false,
|
||||
firstRunCompleted: false,
|
||||
showSuggestedSubs: false,
|
||||
enabledChannelNotifications: [],
|
||||
};
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[ACTIONS.CHANNEL_SUBSCRIBE]: (state: SubscriptionState, action: DoChannelSubscribe): SubscriptionState => {
|
||||
const newSubscription: Subscription = action.data;
|
||||
[ACTIONS.CHANNEL_SUBSCRIBE]: (state: SubscriptionState, action): SubscriptionState => {
|
||||
const newSubscription: { uri: string, channelName: string, notificationsDisabled: boolean } = action.data;
|
||||
const newSubscriptions: Array<Subscription> = state.subscriptions.slice();
|
||||
let newFollowing: Array<Following> = state.following.slice();
|
||||
// prevent duplicates in the sidebar
|
||||
if (!newSubscriptions.some(sub => sub.uri === newSubscription.uri)) {
|
||||
// $FlowFixMe
|
||||
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 {
|
||||
...state,
|
||||
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 newSubscriptions = state.subscriptions
|
||||
.slice()
|
||||
.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 {
|
||||
...state,
|
||||
unread: { ...unread },
|
||||
subscriptions: newSubscriptions,
|
||||
};
|
||||
},
|
||||
[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,
|
||||
},
|
||||
following: newFollowing,
|
||||
};
|
||||
},
|
||||
[ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
|
||||
|
@ -111,39 +69,20 @@ export default handleActions(
|
|||
...state,
|
||||
loading: false,
|
||||
}),
|
||||
[ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: (
|
||||
state: SubscriptionState,
|
||||
action: FetchedSubscriptionsSucess
|
||||
): SubscriptionState => ({
|
||||
[ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: (state: SubscriptionState, action): SubscriptionState => ({
|
||||
...state,
|
||||
loading: false,
|
||||
subscriptions: action.data,
|
||||
}),
|
||||
[ACTIONS.SET_VIEW_MODE]: (state: SubscriptionState, action: SetViewMode): SubscriptionState => ({
|
||||
[ACTIONS.SET_VIEW_MODE]: (state: SubscriptionState, action): SubscriptionState => ({
|
||||
...state,
|
||||
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]: (
|
||||
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;
|
||||
if (!incomingSubscriptions) {
|
||||
return {
|
||||
|
@ -151,6 +90,7 @@ export default handleActions(
|
|||
};
|
||||
}
|
||||
let newSubscriptions;
|
||||
let newFollowing;
|
||||
|
||||
if (!subscriptions) {
|
||||
newSubscriptions = state.subscriptions;
|
||||
|
@ -166,9 +106,24 @@ export default handleActions(
|
|||
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 {
|
||||
...state,
|
||||
subscriptions: newSubscriptions,
|
||||
following: newFollowing,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
selectAllClaimsByChannel,
|
||||
selectClaimsById,
|
||||
selectAllFetchingChannelClaims,
|
||||
makeSelectChannelForClaimUri,
|
||||
selectClaimsByUri,
|
||||
parseURI,
|
||||
makeSelectClaimForUri,
|
||||
} from 'lbry-redux';
|
||||
|
@ -20,27 +17,17 @@ export const selectSubscriptions = createSelector(
|
|||
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
|
||||
export const selectIsFetchingSubscriptions = createSelector(
|
||||
selectState,
|
||||
state => state.loading
|
||||
);
|
||||
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
|
||||
|
||||
// The current view mode on the subscriptions page
|
||||
export const selectViewMode = createSelector(
|
||||
selectState,
|
||||
state => state.viewMode
|
||||
);
|
||||
export const selectViewMode = createSelector(selectState, state => state.viewMode);
|
||||
|
||||
// Suggested subscriptions from internal apis
|
||||
export const selectSuggested = createSelector(
|
||||
selectState,
|
||||
state => state.suggested
|
||||
);
|
||||
export const selectIsFetchingSuggested = createSelector(
|
||||
selectState,
|
||||
state => state.loadingSuggested
|
||||
);
|
||||
export const selectSuggested = createSelector(selectState, state => state.suggested);
|
||||
export const selectIsFetchingSuggested = createSelector(selectState, state => state.loadingSuggested);
|
||||
export const selectSuggestedChannels = createSelector(
|
||||
selectSubscriptions,
|
||||
selectSuggested,
|
||||
|
@ -93,14 +80,8 @@ export const selectSuggestedChannels = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const selectFirstRunCompleted = createSelector(
|
||||
selectState,
|
||||
state => state.firstRunCompleted
|
||||
);
|
||||
export const selectshowSuggestedSubs = createSelector(
|
||||
selectState,
|
||||
state => state.showSuggestedSubs
|
||||
);
|
||||
export const selectFirstRunCompleted = createSelector(selectState, state => state.firstRunCompleted);
|
||||
export const selectshowSuggestedSubs = createSelector(selectState, state => state.showSuggestedSubs);
|
||||
|
||||
// Fetching any claims that are a part of a users subscriptions
|
||||
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
|
||||
// Accepts content or channel uris
|
||||
export const makeSelectChannelInSubscriptions = uri =>
|
||||
createSelector(
|
||||
selectSubscriptions,
|
||||
subscriptions => subscriptions.some(sub => sub.uri === uri)
|
||||
);
|
||||
createSelector(selectSubscriptions, subscriptions => subscriptions.some(sub => sub.uri === uri));
|
||||
|
||||
export const makeSelectIsSubscribed = uri =>
|
||||
createSelector(
|
||||
|
@ -288,22 +130,31 @@ export const makeSelectIsSubscribed = uri =>
|
|||
}
|
||||
);
|
||||
|
||||
export const makeSelectIsNew = uri =>
|
||||
export const makeSelectNotificationsDisabled = uri =>
|
||||
createSelector(
|
||||
makeSelectIsSubscribed(uri),
|
||||
makeSelectChannelForClaimUri(uri),
|
||||
selectUnreadByChannel,
|
||||
(isSubscribed, channel, unreadByChannel) => {
|
||||
if (!isSubscribed) {
|
||||
return false;
|
||||
selectFollowing,
|
||||
makeSelectChannelForClaimUri(uri, true),
|
||||
makeSelectClaimForUri(uri),
|
||||
(following, channelUri, claim) => {
|
||||
if (channelUri) {
|
||||
return following.some(following => following.uri === channelUri && following.notificationsDisabled);
|
||||
}
|
||||
|
||||
const unreadForChannel = unreadByChannel[`lbry://${channel}`];
|
||||
if (unreadForChannel) {
|
||||
return unreadForChannel.uris.includes(uri);
|
||||
// If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
|
||||
let isChannel;
|
||||
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;
|
||||
// If they are subscribed, check to see if this uri is in the list of unreads
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -185,7 +185,8 @@ $metadata-z-index: 1;
|
|||
flex-wrap: wrap;
|
||||
font-size: var(--font-base);
|
||||
|
||||
> * {
|
||||
> .button,
|
||||
> .button-group .button {
|
||||
padding: 0 var(--spacing-xs);
|
||||
|
||||
&: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) {
|
||||
flex-direction: column;
|
||||
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
}
|
||||
|
||||
.notification__icon {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.icon__wrapper {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
@ -25,6 +28,10 @@
|
|||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.notification__wrapper {
|
||||
|
@ -72,7 +79,7 @@
|
|||
color: var(--color-text);
|
||||
margin-bottom: var(--spacing-s);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
|
||||
|
@ -84,7 +91,7 @@
|
|||
.notification__text {
|
||||
font-size: var(--font-body);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -146,3 +153,19 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,6 +147,10 @@ const sharedStateFilters = {
|
|||
return value.map(({ uri }) => uri);
|
||||
},
|
||||
},
|
||||
following: {
|
||||
source: 'subscriptions',
|
||||
property: 'following',
|
||||
},
|
||||
blocked: { source: 'blocked', property: 'blockedChannels' },
|
||||
settings: { source: 'settings', property: 'sharedPreferences' },
|
||||
app_welcome_version: { source: 'app', property: 'welcomeVersion' },
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -7411,17 +7411,23 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
<<<<<<< HEAD
|
||||
lbry-redux@lbryio/lbry-redux#04789190b060f27bf0c438a9af449b4a18ef4925:
|
||||
version "0.0.1"
|
||||
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:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
uuid "^8.3.1"
|
||||
|
||||
lbryinc@lbryio/lbryinc#517c28a183d6ab69a357227809bc7c3c12d8411e:
|
||||
lbryinc@lbryio/lbryinc#9bdf18eef6a65aef7d92ce1040b6f8d3db1be671:
|
||||
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:
|
||||
reselect "^3.0.0"
|
||||
|
||||
|
|
Loading…
Reference in a new issue