From 63f1fed33c5a75ab7ab4a1e4eb2586f4f8a4ba41 Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Mon, 2 Nov 2020 11:51:08 -0500 Subject: [PATCH] per channel notification settings --- .flowconfig | 2 + flow-typed/notification.js | 1 + flow-typed/redux.js | 3 + flow-typed/subscription.js | 92 +--------- package.json | 4 +- ui/component/common/icon-custom.jsx | 18 ++ ui/component/fileProperties/index.js | 3 +- ui/component/fileProperties/view.jsx | 1 - ui/component/fileThumbnail/index.js | 8 +- ui/component/fileThumbnail/view.jsx | 60 ++++--- ui/component/notification/view.jsx | 10 ++ ui/component/subscribeButton/index.js | 7 +- ui/component/subscribeButton/view.jsx | 67 ++++--- ui/component/subscribeMarkAsRead/index.js | 10 -- ui/component/subscribeMarkAsRead/view.jsx | 34 ---- ui/constants/action_types.js | 6 - ui/constants/icons.js | 2 + ui/constants/notifications.js | 1 + ui/page/file/index.js | 15 +- ui/page/file/view.jsx | 14 -- ui/page/tagsFollowingManage/index.js | 6 +- ui/redux/actions/comments.js | 18 +- ui/redux/actions/notifications.js | 23 ++- ui/redux/actions/publish.js | 5 +- ui/redux/actions/subscriptions.js | 141 +-------------- ui/redux/reducers/subscriptions.js | 145 ++++++--------- ui/redux/selectors/subscriptions.js | 209 ++++------------------ ui/scss/component/_channel.scss | 9 +- ui/scss/component/_notification.scss | 27 ++- ui/store.js | 4 + yarn.lock | 10 +- 31 files changed, 295 insertions(+), 660 deletions(-) create mode 100644 flow-typed/redux.js delete mode 100644 ui/component/subscribeMarkAsRead/index.js delete mode 100644 ui/component/subscribeMarkAsRead/view.jsx diff --git a/.flowconfig b/.flowconfig index 4870244e1..bcc5a4557 100644 --- a/.flowconfig +++ b/.flowconfig @@ -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] diff --git a/flow-typed/notification.js b/flow-typed/notification.js index 04b0eef22..bf9c0c358 100644 --- a/flow-typed/notification.js +++ b/flow-typed/notification.js @@ -26,6 +26,7 @@ declare type WebNotification = { hash: string, claim_title: string, comment?: string, + channel_url: string, }, email: {}, }, diff --git a/flow-typed/redux.js b/flow-typed/redux.js new file mode 100644 index 000000000..e159c168e --- /dev/null +++ b/flow-typed/redux.js @@ -0,0 +1,3 @@ +// @flow + +declare type Dispatch = any; diff --git a/flow-typed/subscription.js b/flow-typed/subscription.js index ccdfc4ed9..53956b014 100644 --- a/flow-typed/subscription.js +++ b/flow-typed/subscription.js @@ -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, -}; - -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, - unread: UnreadSubscriptions, + following: Array, 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, - type?: SubscriptionNotificationType, - }, -}; - -declare type DoRemoveSubscriptionUnreads = { - type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS, - data: { - channel: string, - uris: Array, - }, -}; - -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, -}; - -declare type SetViewMode = { - type: ACTIONS.SET_VIEW_MODE, - data: ViewMode, -}; - -declare type GetSuggestedSubscriptionsSuccess = { - type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START, - data: SuggestedSubscriptions, }; diff --git a/package.json b/package.json index a48b615cc..825d30df4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index 45ff5efef..f3e47407b 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -860,6 +860,24 @@ export const icons = { strokeMiterlimit="10" /> ), + [ICONS.BELL]: buildIcon( + + + + + ), + [ICONS.BELL_ON]: buildIcon( + + + + + ), [ICONS.PIN]: (props: CustomProps) => ( ({ downloaded: makeSelectFilePartlyDownloaded(props.uri)(state), isSubscribed: makeSelectIsSubscribed(props.uri)(state), - isNew: makeSelectIsNew(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state), }); diff --git a/ui/component/fileProperties/view.jsx b/ui/component/fileProperties/view.jsx index 53d2713ce..418888cc9 100644 --- a/ui/component/fileProperties/view.jsx +++ b/ui/component/fileProperties/view.jsx @@ -12,7 +12,6 @@ type Props = { downloaded: boolean, claimIsMine: boolean, isSubscribed: boolean, - isNew: boolean, small: boolean, }; diff --git a/ui/component/fileThumbnail/index.js b/ui/component/fileThumbnail/index.js index 22c6c0488..a5a169060 100644 --- a/ui/component/fileThumbnail/index.js +++ b/ui/component/fileThumbnail/index.js @@ -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); diff --git a/ui/component/fileThumbnail/view.jsx b/ui/component/fileThumbnail/view.jsx index 1004fefee..c8567482f 100644 --- a/ui/component/fileThumbnail/view.jsx +++ b/ui/component/fileThumbnail/view.jsx @@ -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 { - 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 ( - - {children} - - ); + 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 ( -
+ {children} -
+ ); } + + const url = passedThumbnail || uri ? thumbnailFromClaim : Placeholder; + + return ( +
+ {children} +
+ ); } export default FileThumbnail; diff --git a/ui/component/notification/view.jsx b/ui/component/notification/view.jsx index 3a2ff36a1..5722d9720 100644 --- a/ui/component/notification/view.jsx +++ b/ui/component/notification/view.jsx @@ -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 = ; break; + case NOTIFICATIONS.NEW_CONTENT: + icon = ; + break; case NOTIFICATIONS.DAILY_WATCH_AVAILABLE: case NOTIFICATIONS.DAILY_WATCH_REMIND: icon = ; @@ -136,6 +140,12 @@ export default function Notification(props: Props) { )} + {notification_rule === NOTIFICATIONS.NEW_CONTENT && ( + <> + + + )} +
diff --git a/ui/component/subscribeButton/index.js b/ui/component/subscribeButton/index.js index ba8eb841f..2460858eb 100644 --- a/ui/component/subscribeButton/index.js +++ b/ui/component/subscribeButton/index.js @@ -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, { diff --git a/ui/component/subscribeButton/view.jsx b/ui/component/subscribeButton/view.jsx index 664e5da00..ebd29f26c 100644 --- a/ui/component/subscribeButton/view.jsx +++ b/ui/component/subscribeButton/view.jsx @@ -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 ? ( -
) : null; } diff --git a/ui/component/subscribeMarkAsRead/index.js b/ui/component/subscribeMarkAsRead/index.js deleted file mode 100644 index 81487e7b8..000000000 --- a/ui/component/subscribeMarkAsRead/index.js +++ /dev/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); diff --git a/ui/component/subscribeMarkAsRead/view.jsx b/ui/component/subscribeMarkAsRead/view.jsx deleted file mode 100644 index 85ff48a8b..000000000 --- a/ui/component/subscribeMarkAsRead/view.jsx +++ /dev/null @@ -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 { - 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