add mark as seen to notifications
This commit is contained in:
parent
2ae3484363
commit
9ee4b256fb
15 changed files with 236 additions and 152 deletions
2
flow-typed/notification.js
vendored
2
flow-typed/notification.js
vendored
|
@ -7,6 +7,7 @@ declare type WebNotification = {
|
||||||
is_device_notified: boolean,
|
is_device_notified: boolean,
|
||||||
is_emailed: boolean,
|
is_emailed: boolean,
|
||||||
is_read: boolean,
|
is_read: boolean,
|
||||||
|
is_seen: boolean,
|
||||||
notification_parameters: {
|
notification_parameters: {
|
||||||
device: {
|
device: {
|
||||||
analytics_label: string,
|
analytics_label: string,
|
||||||
|
@ -23,6 +24,7 @@ declare type WebNotification = {
|
||||||
comment_author: string,
|
comment_author: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
claim_title: string,
|
claim_title: string,
|
||||||
|
comment?: string,
|
||||||
},
|
},
|
||||||
email: {},
|
email: {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { doSeeNotifications } from 'redux/actions/notifications';
|
||||||
import Notification from './view';
|
import Notification from './view';
|
||||||
|
|
||||||
export default connect()(Notification);
|
export default connect(null, {
|
||||||
|
doSeeNotifications,
|
||||||
|
})(Notification);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { NOTIFICATION_CREATOR_SUBSCRIBER, NOTIFICATION_COMMENT } from 'constants/notifications';
|
import { NOTIFICATION_CREATOR_SUBSCRIBER, NOTIFICATION_COMMENT } from 'constants/notifications';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
@ -14,58 +15,87 @@ type Props = {
|
||||||
notification: WebNotification,
|
notification: WebNotification,
|
||||||
menuButton: boolean,
|
menuButton: boolean,
|
||||||
children: any,
|
children: any,
|
||||||
|
doSeeNotifications: ([number]) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Notification(props: Props) {
|
export default function Notification(props: Props) {
|
||||||
const { notification, menuButton = false } = props;
|
const { notification, menuButton = false, doSeeNotifications } = props;
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const notificationTarget = notification && notification.notification_parameters.device.target;
|
const { notification_rule, notification_parameters, is_seen, id } = notification;
|
||||||
|
const notificationTarget = notification && notification_parameters.device.target;
|
||||||
|
const commentText = notification_rule === NOTIFICATION_COMMENT && notification_parameters.dynamic.comment;
|
||||||
let notificationLink = formatLbryUrlForWeb(notificationTarget);
|
let notificationLink = formatLbryUrlForWeb(notificationTarget);
|
||||||
if (notification.notification_rule === NOTIFICATION_COMMENT && notification.notification_parameters.dynamic.hash) {
|
if (notification_rule === NOTIFICATION_COMMENT && notification_parameters.dynamic.hash) {
|
||||||
notificationLink += `?lc=${notification.notification_parameters.dynamic.hash}`;
|
notificationLink += `?lc=${notification_parameters.dynamic.hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon;
|
let icon;
|
||||||
switch (notification.notification_rule) {
|
switch (notification_rule) {
|
||||||
case NOTIFICATION_CREATOR_SUBSCRIBER:
|
case NOTIFICATION_CREATOR_SUBSCRIBER:
|
||||||
icon = <Icon icon={ICONS.SUBSCRIBE} sectionIcon className="notification__icon" />;
|
icon = <Icon icon={ICONS.SUBSCRIBE} sectionIcon className="notification__icon" />;
|
||||||
break;
|
break;
|
||||||
case NOTIFICATION_COMMENT:
|
case NOTIFICATION_COMMENT:
|
||||||
icon = <ChannelThumbnail small uri={notification.notification_parameters.dynamic.comment_author} />;
|
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.comment_author} />;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
icon = <Icon icon={ICONS.NOTIFICATION} sectionIcon className="notification__icon" />;
|
icon = <Icon icon={ICONS.NOTIFICATION} sectionIcon className="notification__icon" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNotificationClick() {
|
||||||
|
if (!is_seen) {
|
||||||
|
doSeeNotifications([id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationLink) {
|
||||||
|
push(notificationLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Wrapper = menuButton
|
const Wrapper = menuButton
|
||||||
? (props: { children: any }) => (
|
? (props: { children: any }) => (
|
||||||
<MenuItem className="menu__link--notification" onSelect={() => push(notificationLink)}>
|
<MenuItem className="menu__link--notification" onSelect={handleNotificationClick}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
: (props: { children: any }) => (
|
: notificationLink
|
||||||
<a className="menu__link--notification" onClick={() => push(notificationLink)}>
|
? (props: { children: any }) => (
|
||||||
|
<a className="menu__link--notification" onClick={handleNotificationClick}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
|
)
|
||||||
|
: (props: { children: any }) => (
|
||||||
|
<span
|
||||||
|
className={is_seen ? 'menu__link--notification-nolink' : 'menu__link--notification'}
|
||||||
|
onClick={handleNotificationClick}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<div className="notification__wrapper">
|
<div
|
||||||
|
className={classnames('notification__wrapper', {
|
||||||
|
'notification__wrapper--unseen': !is_seen,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="notification__icon">{icon}</div>
|
<div className="notification__icon">{icon}</div>
|
||||||
<div className="notification__content">
|
<div className="notification__content">
|
||||||
<div>
|
<div>
|
||||||
{notification.notification_rule !== NOTIFICATION_COMMENT && (
|
{notification_rule !== NOTIFICATION_COMMENT && (
|
||||||
<div className="notification__title">{notification.notification_parameters.device.title}</div>
|
<div className="notification__title">{notification_parameters.device.title}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="notification__text">
|
{notification_rule === NOTIFICATION_COMMENT && commentText ? (
|
||||||
{notification.notification_parameters.device.text.replace(
|
<>
|
||||||
// This is terrible and will be replaced when I make the comment channel clickable
|
<div className="notification__title">{notification_parameters.device.title}</div>
|
||||||
'commented on',
|
<div className="notification__text mobile-hidden">{commentText}</div>
|
||||||
notification.group_count ? `left ${notification.group_count} comments on` : 'commented on'
|
</>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<>
|
||||||
|
<div className="notification__text">{notification_parameters.device.text}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="notification__time">
|
<div className="notification__time">
|
||||||
|
|
|
@ -3,28 +3,18 @@ import * as PAGES from 'constants/pages';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Notification from 'component/notification';
|
|
||||||
import NotificationBubble from 'component/notificationBubble';
|
import NotificationBubble from 'component/notificationBubble';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
// import { Menu, MenuList, MenuButton, MenuPopover, MenuItems, MenuItem } from '@reach/menu-button';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
unreadCount: number,
|
unreadCount: number,
|
||||||
fetching: boolean,
|
|
||||||
notifications: ?Array<Notification>,
|
|
||||||
doReadNotifications: () => void,
|
doReadNotifications: () => void,
|
||||||
user: ?User,
|
user: ?User,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NotificationHeaderButton(props: Props) {
|
export default function NotificationHeaderButton(props: Props) {
|
||||||
const {
|
const { unreadCount, doReadNotifications, user } = props;
|
||||||
unreadCount,
|
|
||||||
// notifications,
|
|
||||||
fetching,
|
|
||||||
doReadNotifications,
|
|
||||||
user,
|
|
||||||
} = props;
|
|
||||||
const notificationsEnabled = user && user.experimental_ui;
|
const notificationsEnabled = user && user.experimental_ui;
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
|
|
||||||
|
@ -43,7 +33,6 @@ export default function NotificationHeaderButton(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
disabled={fetching}
|
|
||||||
aria-label={__('Notifications')}
|
aria-label={__('Notifications')}
|
||||||
title={__('Notifications')}
|
title={__('Notifications')}
|
||||||
className="header__navigation-item menu__title header__navigation-item--icon"
|
className="header__navigation-item menu__title header__navigation-item--icon"
|
||||||
|
@ -52,41 +41,4 @@ export default function NotificationHeaderButton(props: Props) {
|
||||||
<NotificationBubble />
|
<NotificationBubble />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Below is disabled until scroll style issues are resolved
|
|
||||||
// return (
|
|
||||||
// <Menu>
|
|
||||||
// <MenuButton
|
|
||||||
// onClick={handleMenuClick}
|
|
||||||
// disabled={fetching}
|
|
||||||
// aria-label={__('Notifications')}
|
|
||||||
// title={__('Notifications')}
|
|
||||||
// className="header__navigation-item menu__title header__navigation-item--icon"
|
|
||||||
// >
|
|
||||||
// <Icon size={18} icon={ICONS.NOTIFICATION} aria-hidden />
|
|
||||||
// {unreadCount > 0 && <span className="notification__bubble">{unreadCount}</span>}
|
|
||||||
// </MenuButton>
|
|
||||||
|
|
||||||
// {notifications && notifications.length > 0 ? (
|
|
||||||
// <MenuList className="menu__list--header">
|
|
||||||
// {notifications.slice(0, 7).map((notification, index) => (
|
|
||||||
// <Notification menuButton key={notification.id} id={notification.id} notification={notification} />
|
|
||||||
// ))}
|
|
||||||
|
|
||||||
// <MenuItem className="menu__link" onSelect={() => push(`/$/${PAGES.NOTIFICATIONS}`)}>
|
|
||||||
// <Icon aria-hidden icon={ICONS.NOTIFICATION} />
|
|
||||||
// {__('View All')}
|
|
||||||
// </MenuItem>
|
|
||||||
// </MenuList>
|
|
||||||
// ) : (
|
|
||||||
// <MenuPopover>
|
|
||||||
// <div className="menu__list--header notifications__empty">No notifications yet.</div>
|
|
||||||
// {/* Below is needed because MenuPopover isn't meant to be used this way */}
|
|
||||||
// <MenuItems>
|
|
||||||
// <MenuItem disabled onSelect={() => {}} />
|
|
||||||
// </MenuItems>
|
|
||||||
// </MenuPopover>
|
|
||||||
// )}
|
|
||||||
// </Menu>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,6 +239,9 @@ export const NOTIFICATION_LIST_FAILED = 'NOTIFICATION_LIST_FAILED';
|
||||||
export const NOTIFICATION_READ_STARTED = 'NOTIFICATION_READ_STARTED';
|
export const NOTIFICATION_READ_STARTED = 'NOTIFICATION_READ_STARTED';
|
||||||
export const NOTIFICATION_READ_COMPLETED = 'NOTIFICATION_READ_COMPLETED';
|
export const NOTIFICATION_READ_COMPLETED = 'NOTIFICATION_READ_COMPLETED';
|
||||||
export const NOTIFICATION_READ_FAILED = 'NOTIFICATION_READ_FAILED';
|
export const NOTIFICATION_READ_FAILED = 'NOTIFICATION_READ_FAILED';
|
||||||
|
export const NOTIFICATION_SEEN_STARTED = 'NOTIFICATION_SEEN_STARTED';
|
||||||
|
export const NOTIFICATION_SEEN_COMPLETED = 'NOTIFICATION_SEEN_COMPLETED';
|
||||||
|
export const NOTIFICATION_SEEN_FAILED = 'NOTIFICATION_SEEN_FAILED';
|
||||||
export const CREATE_TOAST = 'CREATE_TOAST';
|
export const CREATE_TOAST = 'CREATE_TOAST';
|
||||||
export const DISMISS_TOAST = 'DISMISS_TOAST';
|
export const DISMISS_TOAST = 'DISMISS_TOAST';
|
||||||
export const CREATE_ERROR = 'CREATE_ERROR';
|
export const CREATE_ERROR = 'CREATE_ERROR';
|
||||||
|
|
|
@ -3,16 +3,20 @@ import {
|
||||||
selectNotifications,
|
selectNotifications,
|
||||||
selectIsFetchingNotifications,
|
selectIsFetchingNotifications,
|
||||||
selectUnreadNotificationCount,
|
selectUnreadNotificationCount,
|
||||||
|
selectUnseenNotificationCount,
|
||||||
} from 'redux/selectors/notifications';
|
} from 'redux/selectors/notifications';
|
||||||
import { doReadNotifications } from 'redux/actions/notifications';
|
import { doReadNotifications, doNotificationList, doSeeAllNotifications } from 'redux/actions/notifications';
|
||||||
import NotificationsPage from './view';
|
import NotificationsPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
notifications: selectNotifications(state),
|
notifications: selectNotifications(state),
|
||||||
fetching: selectIsFetchingNotifications(state),
|
fetching: selectIsFetchingNotifications(state),
|
||||||
unreadCount: selectUnreadNotificationCount(state),
|
unreadCount: selectUnreadNotificationCount(state),
|
||||||
|
unseenCount: selectUnseenNotificationCount(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
doReadNotifications,
|
doReadNotifications,
|
||||||
|
doNotificationList,
|
||||||
|
doSeeAllNotifications,
|
||||||
})(NotificationsPage);
|
})(NotificationsPage);
|
||||||
|
|
|
@ -1,68 +1,28 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import { NOTIFICATION_COMMENT } from 'constants/notifications';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import Notification from 'component/notification';
|
import Notification from 'component/notification';
|
||||||
import Yrbl from 'component/yrbl';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import Yrbl from 'component/yrbl';
|
||||||
|
import usePrevious from 'effects/use-previous';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notifications: ?Array<Notification>,
|
notifications: Array<Notification>,
|
||||||
fetching: boolean,
|
fetching: boolean,
|
||||||
unreadCount: number,
|
unreadCount: number,
|
||||||
|
unseenCount: number,
|
||||||
|
doSeeAllNotifications: () => void,
|
||||||
doReadNotifications: () => void,
|
doReadNotifications: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NotificationsPage(props: Props) {
|
export default function NotificationsPage(props: Props) {
|
||||||
const { notifications, fetching, unreadCount, doReadNotifications } = props;
|
const { notifications, fetching, unreadCount, unseenCount, doSeeAllNotifications, doReadNotifications } = props;
|
||||||
|
const [hasFetched, setHasFetched] = React.useState(false);
|
||||||
// Group sequential comment notifications if they are by the same author
|
const previousFetching = usePrevious(fetching);
|
||||||
let groupedCount = 1;
|
const hasNotifications = notifications.length > 0;
|
||||||
const groupedNotifications =
|
|
||||||
notifications &&
|
|
||||||
notifications.reduce((list, notification, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return [notification];
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousNotification = notifications[index - 1];
|
|
||||||
const isCommentNotification = notification.notification_rule === NOTIFICATION_COMMENT;
|
|
||||||
const previousIsCommentNotification = previousNotification.notification_rule === NOTIFICATION_COMMENT;
|
|
||||||
if (isCommentNotification && previousIsCommentNotification) {
|
|
||||||
const notificationTarget = notification.notification_parameters.device.target;
|
|
||||||
const previousTarget = previousNotification && previousNotification.notification_parameters.device.target;
|
|
||||||
const author = notification.notification_parameters.dynamic.comment_author;
|
|
||||||
const previousAuthor = previousNotification.notification_parameters.dynamic.comment_author;
|
|
||||||
|
|
||||||
if (author === previousAuthor && notificationTarget === previousTarget) {
|
|
||||||
const newList = [...list];
|
|
||||||
newList.pop();
|
|
||||||
groupedCount += 1;
|
|
||||||
const newNotification = {
|
|
||||||
...previousNotification,
|
|
||||||
group_count: groupedCount,
|
|
||||||
};
|
|
||||||
|
|
||||||
newList[index - groupedCount] = newNotification;
|
|
||||||
return newList;
|
|
||||||
} else {
|
|
||||||
if (groupedCount > 1) {
|
|
||||||
groupedCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...list, notification];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (groupedCount > 1) {
|
|
||||||
groupedCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...list, notification];
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (unreadCount > 0) {
|
if (unreadCount > 0) {
|
||||||
|
@ -70,43 +30,64 @@ export default function NotificationsPage(props: Props) {
|
||||||
}
|
}
|
||||||
}, [unreadCount, doReadNotifications]);
|
}, [unreadCount, doReadNotifications]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if ((fetching === false && previousFetching === true) || hasNotifications) {
|
||||||
|
setHasFetched(true);
|
||||||
|
}
|
||||||
|
}, [fetching, previousFetching, setHasFetched, hasNotifications]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{fetching && (
|
{fetching && !hasNotifications && (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<Spinner delayed />
|
<Spinner delayed />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{groupedNotifications && groupedNotifications.length > 0 ? (
|
{hasFetched && (
|
||||||
<Card
|
<>
|
||||||
isBodyList
|
{notifications && notifications.length > 0 ? (
|
||||||
title={__('Notifications')}
|
<Card
|
||||||
body={
|
isBodyList
|
||||||
<div className="notification_list">
|
title={
|
||||||
{groupedNotifications.map((notification, index) => {
|
<span>
|
||||||
if (!notification) {
|
{__('Notifications')}
|
||||||
return null;
|
{fetching && <Spinner type="small" />}
|
||||||
}
|
</span>
|
||||||
|
}
|
||||||
return <Notification key={notification.id} notification={notification} />;
|
titleActions={
|
||||||
})}
|
unseenCount > 0 && (
|
||||||
</div>
|
<Button
|
||||||
}
|
icon={ICONS.EYE}
|
||||||
/>
|
onClick={doSeeAllNotifications}
|
||||||
) : (
|
button="secondary"
|
||||||
<div className="main--empty">
|
label={__('Mark all as read')}
|
||||||
<Yrbl
|
/>
|
||||||
title={__('No Notifications')}
|
)
|
||||||
subtitle={
|
}
|
||||||
<div>
|
body={
|
||||||
<p>{__("You don't have any notifications yet, but they will be here when you do!")}</p>
|
<div className="notification_list">
|
||||||
<div className="section__actions">
|
{notifications.map((notification, index) => {
|
||||||
<Button button="primary" icon={ICONS.HOME} label={__('Go Home')} navigate="/" />
|
return <Notification key={notification.id} notification={notification} />;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
) : (
|
||||||
</div>
|
<div className="main--empty">
|
||||||
|
<Yrbl
|
||||||
|
title={__('No Notifications')}
|
||||||
|
subtitle={
|
||||||
|
<div>
|
||||||
|
<p>{__("You don't have any notifications yet, but they will be here when you do!")}</p>
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button button="primary" icon={ICONS.HOME} label={__('Go Home')} navigate="/" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import uuid from 'uuid/v4';
|
import uuid from 'uuid/v4';
|
||||||
import { selectNotifications } from 'redux/selectors/notifications';
|
import { selectNotifications } from 'redux/selectors/notifications';
|
||||||
|
import { doResolveUris } from 'lbry-redux';
|
||||||
|
|
||||||
export function doToast(params: ToastParams) {
|
export function doToast(params: ToastParams) {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
|
@ -45,6 +46,20 @@ export function doNotificationList() {
|
||||||
return Lbryio.call('notification', 'list')
|
return Lbryio.call('notification', 'list')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const notifications = response || [];
|
const notifications = response || [];
|
||||||
|
const channelsToResolve = notifications
|
||||||
|
.filter((notification: WebNotification) => {
|
||||||
|
if (
|
||||||
|
notification.notification_parameters.dynamic &&
|
||||||
|
notification.notification_parameters.dynamic.comment_author
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(notification => notification.notification_parameters.dynamic.comment_author);
|
||||||
|
|
||||||
|
dispatch(doResolveUris(channelsToResolve));
|
||||||
dispatch({ type: ACTIONS.NOTIFICATION_LIST_COMPLETED, data: { notifications } });
|
dispatch({ type: ACTIONS.NOTIFICATION_LIST_COMPLETED, data: { notifications } });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -74,3 +89,32 @@ export function doReadNotifications() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doSeeNotifications(notificationIds: Array<string>) {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_SEEN_STARTED });
|
||||||
|
return Lbryio.call('notification', 'edit', { notification_ids: notificationIds.join(','), is_seen: true })
|
||||||
|
.then(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.NOTIFICATION_SEEN_COMPLETED,
|
||||||
|
data: {
|
||||||
|
notificationIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_SEEN_FAILED, data: { error } });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doSeeAllNotifications() {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const notifications = selectNotifications(state);
|
||||||
|
const unSeenNotifications =
|
||||||
|
notifications && notifications.filter(notification => !notification.is_seen).map(notification => notification.id);
|
||||||
|
|
||||||
|
dispatch(doSeeNotifications(unSeenNotifications));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,22 @@ export default handleActions(
|
||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[ACTIONS.NOTIFICATION_SEEN_COMPLETED]: (state, action) => {
|
||||||
|
const { notifications } = state;
|
||||||
|
const { notificationIds } = action.data;
|
||||||
|
const newNotifications = notifications.map(notification => {
|
||||||
|
if (notificationIds.includes(notification.id)) {
|
||||||
|
return { ...notification, is_seen: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
notifications: newNotifications,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoError) => {
|
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoError) => {
|
||||||
|
|
|
@ -10,6 +10,10 @@ export const selectUnreadNotificationCount = createSelector(selectNotifications,
|
||||||
return notifications ? notifications.filter(notification => !notification.is_read).length : 0;
|
return notifications ? notifications.filter(notification => !notification.is_read).length : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const selectUnseenNotificationCount = createSelector(selectNotifications, notifications => {
|
||||||
|
return notifications ? notifications.filter(notification => !notification.is_seen).length : 0;
|
||||||
|
});
|
||||||
|
|
||||||
export const selectToast = createSelector(selectState, state => {
|
export const selectToast = createSelector(selectState, state => {
|
||||||
if (state.toasts.length) {
|
if (state.toasts.length) {
|
||||||
const { id, params } = state.toasts[0];
|
const { id, params } = state.toasts[0];
|
||||||
|
|
|
@ -213,6 +213,10 @@
|
||||||
& > *:not(:last-child) {
|
& > *:not(:last-child) {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card__media--nsfw {
|
.card__media--nsfw {
|
||||||
|
|
|
@ -26,6 +26,26 @@
|
||||||
.channel-thumbnail {
|
.channel-thumbnail {
|
||||||
@include handleChannelGif(3rem);
|
@include handleChannelGif(3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
.channel-thumbnail {
|
||||||
|
@include handleChannelGif(2rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__wrapper--unseen {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-button-secondary-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__content {
|
.notification__content {
|
||||||
|
@ -33,23 +53,36 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__title {
|
.notification__title {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__text {
|
.notification__text {
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-body);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__time {
|
.notification__time {
|
||||||
@extend .help;
|
@extend .help;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__bubble {
|
.notification__bubble {
|
||||||
|
|
|
@ -108,6 +108,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu__link--notification-nolink {
|
||||||
|
@extend .menu__link--notification;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menu__link--all-notifications {
|
.menu__link--all-notifications {
|
||||||
@extend .button--alt;
|
@extend .button--alt;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
|
@ -97,6 +97,6 @@ $breakpoint-large: 1600px;
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
:root {
|
:root {
|
||||||
--font-base: 16px;
|
--font-body: 0.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
--color-placeholder-background: #f0f0f0;
|
--color-placeholder-background: #f0f0f0;
|
||||||
--color-header-background: #ffffff;
|
--color-header-background: #ffffff;
|
||||||
--color-card-background: #ffffff;
|
--color-card-background: #ffffff;
|
||||||
--color-card-background-highlighted: #f6faff;
|
--color-card-background-highlighted: #f0f7ff;
|
||||||
--color-list-header: #fff;
|
--color-list-header: #fff;
|
||||||
--color-file-viewer-background: var(--color-card-background);
|
--color-file-viewer-background: var(--color-card-background);
|
||||||
--color-tabs-background: var(--color-card-background);
|
--color-tabs-background: var(--color-card-background);
|
||||||
|
|
Loading…
Add table
Reference in a new issue