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]
|
[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]
|
||||||
|
|
||||||
|
|
1
flow-typed/notification.js
vendored
1
flow-typed/notification.js
vendored
|
@ -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
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 = {
|
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,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ type Props = {
|
||||||
downloaded: boolean,
|
downloaded: boolean,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
isSubscribed: boolean,
|
isSubscribed: boolean,
|
||||||
isNew: boolean,
|
|
||||||
small: boolean,
|
small: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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://');
|
}
|
||||||
|
}, [hasResolvedClaim, uri, doResolveUri]);
|
||||||
|
|
||||||
if (!allowGifs && thumbnail && thumbnail.endsWith('gif')) {
|
if (!allowGifs && thumbnail && thumbnail.endsWith('gif')) {
|
||||||
return (
|
return (
|
||||||
<FreezeframeWrapper src={thumbnail} className={className}>
|
<FreezeframeWrapper src={thumbnail} className={classnames('media__thumb', className)}>
|
||||||
{children}
|
{children}
|
||||||
</FreezeframeWrapper>
|
</FreezeframeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url;
|
const url = passedThumbnail || uri ? thumbnailFromClaim : Placeholder;
|
||||||
// @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}>
|
<div
|
||||||
|
style={{ backgroundImage: `url('${url ? url.replace(/'/g, "\\'") : ''}')` }}
|
||||||
|
className={classnames('media__thumb', className, {
|
||||||
|
'media__thumb--resolving': !hasResolvedClaim,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default FileThumbnail;
|
export default FileThumbnail;
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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,6 +53,7 @@ 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 ? (
|
||||||
|
<div className="button-group">
|
||||||
<Button
|
<Button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
iconColor="red"
|
iconColor="red"
|
||||||
|
@ -65,12 +69,33 @@ export default function SubscribeButton(props: Props) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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' },
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue