per channel notification settings

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

View file

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

View file

@ -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
View file

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

View file

@ -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,
};

View file

@ -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",

View file

@ -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}

View file

@ -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),
});

View file

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

View file

@ -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);

View file

@ -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;

View file

@ -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} />

View file

@ -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, {

View file

@ -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;
}

View file

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

View file

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

View file

@ -206,9 +206,6 @@ export const DOWNLOAD_LANGUAGE_FAILURE = 'DOWNLOAD_LANGUAGE_FAILURE';
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
export const CHANNEL_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';

View file

@ -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';

View file

@ -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';

View file

@ -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)),
});

View file

@ -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 (

View file

@ -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);

View file

@ -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,
});

View file

@ -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 =

View file

@ -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, {

View file

@ -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,
})
);
};

View file

@ -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,
};
},
},

View file

@ -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;
}
);

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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' },

View file

@ -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"