add comments behind a flag
This commit is contained in:
parent
631d7495c7
commit
90327a72ed
39 changed files with 559 additions and 76 deletions
|
@ -8,6 +8,10 @@
|
||||||
node_modules/lbry-redux/flow-typed/
|
node_modules/lbry-redux/flow-typed/
|
||||||
node_modules/lbryinc/flow-typed/
|
node_modules/lbryinc/flow-typed/
|
||||||
|
|
||||||
|
[untyped]
|
||||||
|
.*/node_modules/lbry-redux
|
||||||
|
.*/node_modules/lbryinc
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// On Web, this will find .env.defaults and optional .env in web/
|
// On Web, this will find .env.defaults and optional .env in web/
|
||||||
// On Desktop App, this will find .env.defaults and optional .env in root dir
|
// On Desktop App, this will find .env.defaults and optional .env in root dir
|
||||||
require('dotenv-defaults').config({silent: false});
|
require('dotenv-defaults').config({ silent: false });
|
||||||
const config = {
|
const config = {
|
||||||
MATOMO_URL: process.env.MATOMO_URL,
|
MATOMO_URL: process.env.MATOMO_URL,
|
||||||
MATOMO_ID: process.env.MATOMO_ID,
|
MATOMO_ID: process.env.MATOMO_ID,
|
||||||
|
|
31
flow-typed/notification.js
vendored
Normal file
31
flow-typed/notification.js
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// @flow
|
||||||
|
declare type WebNotification = {
|
||||||
|
active_at: string,
|
||||||
|
created_at: string,
|
||||||
|
id: number,
|
||||||
|
is_app_readable: boolean,
|
||||||
|
is_device_notified: boolean,
|
||||||
|
is_emailed: boolean,
|
||||||
|
is_read: boolean,
|
||||||
|
notification_parameters: {
|
||||||
|
device: {
|
||||||
|
analytics_label: string,
|
||||||
|
image_url: string,
|
||||||
|
is_data_only: boolean,
|
||||||
|
name: string,
|
||||||
|
placeholders: ?string,
|
||||||
|
target: string,
|
||||||
|
text: string,
|
||||||
|
title: string,
|
||||||
|
type: string,
|
||||||
|
},
|
||||||
|
dynamic: {
|
||||||
|
comment_author: string,
|
||||||
|
},
|
||||||
|
email: {},
|
||||||
|
},
|
||||||
|
notification_rule: string,
|
||||||
|
type: string,
|
||||||
|
updated_at: string,
|
||||||
|
user_id: number,
|
||||||
|
};
|
1
flow-typed/user.js
vendored
1
flow-typed/user.js
vendored
|
@ -27,4 +27,5 @@ declare type User = {
|
||||||
youtube_channels: ?Array<string>,
|
youtube_channels: ?Array<string>,
|
||||||
device_types: Array<DeviceType>,
|
device_types: Array<DeviceType>,
|
||||||
lbry_first_approved: boolean,
|
lbry_first_approved: boolean,
|
||||||
|
experimental_ui: boolean,
|
||||||
};
|
};
|
||||||
|
|
|
@ -149,7 +149,8 @@ const analytics: Analytics = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
apiLogPublish: (claimResult: ChannelClaim | StreamClaim) => {
|
apiLogPublish: (claimResult: ChannelClaim | StreamClaim) => {
|
||||||
if (internalAnalyticsEnabled && isProduction) {
|
// Don't check if this is production so channels created on localhost are still linked to user
|
||||||
|
if (internalAnalyticsEnabled) {
|
||||||
const { permanent_url: uri, claim_id: claimId, txid, nout, signing_channel: signingChannel } = claimResult;
|
const { permanent_url: uri, claim_id: claimId, txid, nout, signing_channel: signingChannel } = claimResult;
|
||||||
let channelClaimId;
|
let channelClaimId;
|
||||||
if (signingChannel) {
|
if (signingChannel) {
|
||||||
|
|
|
@ -77,6 +77,7 @@ type Props = {
|
||||||
setReferrer: (string, boolean) => void,
|
setReferrer: (string, boolean) => void,
|
||||||
analyticsTagSync: () => void,
|
analyticsTagSync: () => void,
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
|
socketConnect: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function App(props: Props) {
|
function App(props: Props) {
|
||||||
|
@ -194,10 +195,11 @@ function App(props: Props) {
|
||||||
if (wrapperElement) {
|
if (wrapperElement) {
|
||||||
ReactModal.setAppElement(wrapperElement);
|
ReactModal.setAppElement(wrapperElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAccessToken();
|
fetchAccessToken();
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
fetchChannelListMine(); // This needs to be done for web too...
|
fetchChannelListMine(); // This is fetched after a user is signed in on web
|
||||||
// @endif
|
// @endif
|
||||||
}, [appRef, fetchAccessToken, fetchChannelListMine]);
|
}, [appRef, fetchAccessToken, fetchChannelListMine]);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectThumbnailForUri } from 'lbry-redux';
|
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import ChannelThumbnail from './view';
|
import ChannelThumbnail from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(select, {
|
||||||
select,
|
doResolveUri,
|
||||||
null
|
})(ChannelThumbnail);
|
||||||
)(ChannelThumbnail);
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ type Props = {
|
||||||
obscure?: boolean,
|
obscure?: boolean,
|
||||||
small?: boolean,
|
small?: boolean,
|
||||||
allowGifs?: boolean,
|
allowGifs?: boolean,
|
||||||
|
claim: ?ChannelClaim,
|
||||||
|
doResolveUri: string => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelThumbnail(props: Props) {
|
function ChannelThumbnail(props: Props) {
|
||||||
|
@ -24,8 +26,11 @@ function ChannelThumbnail(props: Props) {
|
||||||
obscure,
|
obscure,
|
||||||
small = false,
|
small = false,
|
||||||
allowGifs = false,
|
allowGifs = false,
|
||||||
|
claim,
|
||||||
|
doResolveUri,
|
||||||
} = props;
|
} = props;
|
||||||
const [thumbError, setThumbError] = React.useState(false);
|
const [thumbError, setThumbError] = React.useState(false);
|
||||||
|
const shouldResolve = claim === undefined;
|
||||||
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
||||||
const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://');
|
const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://');
|
||||||
const channelThumbnail = thumbnail || thumbnailPreview;
|
const channelThumbnail = thumbnail || thumbnailPreview;
|
||||||
|
@ -41,6 +46,12 @@ function ChannelThumbnail(props: Props) {
|
||||||
colorClassName = `channel-thumbnail__default--4`;
|
colorClassName = `channel-thumbnail__default--4`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (shouldResolve && uri) {
|
||||||
|
doResolveUri(uri);
|
||||||
|
}
|
||||||
|
}, [doResolveUri, shouldResolve, uri]);
|
||||||
|
|
||||||
if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) {
|
if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) {
|
||||||
return <FreezeframeWrapper src={channelThumbnail} className="channel-thumbnail" />;
|
return <FreezeframeWrapper src={channelThumbnail} className="channel-thumbnail" />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -701,4 +701,10 @@ export const icons = {
|
||||||
<line x1="6" y1="20" x2="6" y2="14" />
|
<line x1="6" y1="20" x2="6" y2="14" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
|
[ICONS.NOTIFICATION]: 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>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Icon from 'component/common/icon';
|
||||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import NavigationButton from 'component/navigationButton';
|
import NavigationButton from 'component/navigationButton';
|
||||||
|
import NotificationHeaderButton from 'component/notificationHeaderButton';
|
||||||
import { LOGO_TITLE } from 'config';
|
import { LOGO_TITLE } from 'config';
|
||||||
import useIsMobile from 'effects/use-is-mobile';
|
import useIsMobile from 'effects/use-is-mobile';
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
@ -239,6 +240,8 @@ const Header = (props: Props) => {
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
|
<NotificationHeaderButton />
|
||||||
|
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
aria-label={__('Your account')}
|
aria-label={__('Your account')}
|
||||||
|
@ -298,6 +301,7 @@ const Header = (props: Props) => {
|
||||||
</Menu>
|
</Menu>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
aria-label={__('Settings')}
|
aria-label={__('Settings')}
|
||||||
|
|
4
ui/component/notification/index.js
Normal file
4
ui/component/notification/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Notification from './view';
|
||||||
|
|
||||||
|
export default connect()(Notification);
|
64
ui/component/notification/view.jsx
Normal file
64
ui/component/notification/view.jsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import DateTime from 'component/dateTime';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import { MenuItem } from '@reach/menu-button';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
notification: WebNotification,
|
||||||
|
menuButton: boolean,
|
||||||
|
children: any,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NOTIFICATION_CREATOR_SUBSCRIBER = 'creator_subscriber';
|
||||||
|
const NOTIFICATION_COMMENT = 'comment';
|
||||||
|
|
||||||
|
export default function Notification(props: Props) {
|
||||||
|
const { notification, menuButton = false } = props;
|
||||||
|
const notificationTarget = notification && notification.notification_parameters.device.target;
|
||||||
|
const notificationLink = formatLbryUrlForWeb(notificationTarget);
|
||||||
|
const { push } = useHistory();
|
||||||
|
|
||||||
|
let icon;
|
||||||
|
switch (notification.notification_rule) {
|
||||||
|
case NOTIFICATION_CREATOR_SUBSCRIBER:
|
||||||
|
icon = <Icon icon={ICONS.SUBSCRIBE} sectionIcon className="notification__icon" />;
|
||||||
|
break;
|
||||||
|
case NOTIFICATION_COMMENT:
|
||||||
|
icon = <ChannelThumbnail uri={notification.notification_parameters.dynamic.comment_author} />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icon = <Icon icon={ICONS.NOTIFICATION} sectionIcon className="notification__icon" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = menuButton
|
||||||
|
? (props: { children: any }) => (
|
||||||
|
<MenuItem className="menu__link--notification" onSelect={() => push(notificationLink)}>
|
||||||
|
{props.children}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
: (props: { children: any }) => (
|
||||||
|
<a className="menu__link--notification" onClick={() => push(notificationLink)}>
|
||||||
|
{props.children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<div className="notification__wrapper">
|
||||||
|
<div className="notification__icon">{icon}</div>
|
||||||
|
<div className="notification__content">
|
||||||
|
<div className="notification__title">{notification.notification_parameters.device.title}</div>
|
||||||
|
<div className="notification__text">{notification.notification_parameters.device.text}</div>
|
||||||
|
<div className="notification__time">
|
||||||
|
<DateTime timeAgo date={notification.created_at} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
20
ui/component/notificationHeaderButton/index.js
Normal file
20
ui/component/notificationHeaderButton/index.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectNotifications,
|
||||||
|
selectIsFetchingNotifications,
|
||||||
|
selectUnreadNotificationCount,
|
||||||
|
} from 'redux/selectors/notifications';
|
||||||
|
import { doReadNotifications } from 'redux/actions/notifications';
|
||||||
|
import { selectUser } from 'redux/selectors/user';
|
||||||
|
import NotificationHeaderButton from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
notifications: selectNotifications(state),
|
||||||
|
fetching: selectIsFetchingNotifications(state),
|
||||||
|
unreadCount: selectUnreadNotificationCount(state),
|
||||||
|
user: selectUser(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doReadNotifications,
|
||||||
|
})(NotificationHeaderButton);
|
91
ui/component/notificationHeaderButton/view.jsx
Normal file
91
ui/component/notificationHeaderButton/view.jsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// @flow
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import Notification from 'component/notification';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
// import { Menu, MenuList, MenuButton, MenuPopover, MenuItems, MenuItem } from '@reach/menu-button';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
unreadCount: number,
|
||||||
|
fetching: boolean,
|
||||||
|
notifications: ?Array<Notification>,
|
||||||
|
doReadNotifications: () => void,
|
||||||
|
user: ?User,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotificationHeaderButton(props: Props) {
|
||||||
|
const {
|
||||||
|
unreadCount,
|
||||||
|
// notifications,
|
||||||
|
fetching,
|
||||||
|
doReadNotifications,
|
||||||
|
user,
|
||||||
|
} = props;
|
||||||
|
const notificationsEnabled = user && user.experimental_ui;
|
||||||
|
const { push } = useHistory();
|
||||||
|
|
||||||
|
function handleMenuClick() {
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
doReadNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
push(`/$/${PAGES.NOTIFICATIONS}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notificationsEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
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>}
|
||||||
|
</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>
|
||||||
|
// );
|
||||||
|
}
|
|
@ -43,6 +43,7 @@ import RewardsVerifyPage from 'page/rewardsVerify';
|
||||||
import CheckoutPage from 'page/checkoutPage';
|
import CheckoutPage from 'page/checkoutPage';
|
||||||
import ChannelNew from 'page/channelNew';
|
import ChannelNew from 'page/channelNew';
|
||||||
import BuyPage from 'page/buy';
|
import BuyPage from 'page/buy';
|
||||||
|
import NotificationsPage from 'page/notifications';
|
||||||
|
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import { SITE_TITLE, WELCOME_VERSION } from 'config';
|
import { SITE_TITLE, WELCOME_VERSION } from 'config';
|
||||||
|
@ -190,6 +191,12 @@ function AppRouter(props: Props) {
|
||||||
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
|
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
|
||||||
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
|
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
|
||||||
|
|
||||||
|
<PrivateRoute
|
||||||
|
{...props}
|
||||||
|
path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`}
|
||||||
|
exact
|
||||||
|
component={SettingsNotificationsPage}
|
||||||
|
/>
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNEL_NEW}`} component={ChannelNew} />
|
<PrivateRoute {...props} path={`/$/${PAGES.CHANNEL_NEW}`} component={ChannelNew} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} />
|
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} />
|
||||||
|
@ -204,6 +211,7 @@ function AppRouter(props: Props) {
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
||||||
|
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||||
|
|
||||||
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
|
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
|
||||||
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
|
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
|
||||||
|
|
|
@ -216,15 +216,17 @@ export const CLEAR_PUBLISH_ERROR = 'CLEAR_PUBLISH_ERROR';
|
||||||
export const REMOVE_PENDING_PUBLISH = 'REMOVE_PENDING_PUBLISH';
|
export const REMOVE_PENDING_PUBLISH = 'REMOVE_PENDING_PUBLISH';
|
||||||
export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT';
|
export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT';
|
||||||
|
|
||||||
// media
|
// Media
|
||||||
export const MEDIA_PLAY = 'MEDIA_PLAY';
|
export const MEDIA_PLAY = 'MEDIA_PLAY';
|
||||||
export const MEDIA_PAUSE = 'MEDIA_PAUSE';
|
export const MEDIA_PAUSE = 'MEDIA_PAUSE';
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
export const CREATE_NOTIFICATION = 'CREATE_NOTIFICATION';
|
export const NOTIFICATION_LIST_STARTED = 'NOTIFICATION_LIST_STARTED';
|
||||||
export const EDIT_NOTIFICATION = 'EDIT_NOTIFICATION';
|
export const NOTIFICATION_LIST_COMPLETED = 'NOTIFICATION_LIST_COMPLETED';
|
||||||
export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION';
|
export const NOTIFICATION_LIST_FAILED = 'NOTIFICATION_LIST_FAILED';
|
||||||
export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION';
|
export const NOTIFICATION_READ_STARTED = 'NOTIFICATION_READ_STARTED';
|
||||||
|
export const NOTIFICATION_READ_COMPLETED = 'NOTIFICATION_READ_COMPLETED';
|
||||||
|
export const NOTIFICATION_READ_FAILED = 'NOTIFICATION_READ_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';
|
||||||
|
@ -249,3 +251,7 @@ export const COMMENT_HIDE_FAILED = 'COMMENT_HIDE_FAILED';
|
||||||
|
|
||||||
// Blocked Channels
|
// Blocked Channels
|
||||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
export const WS_CONNECT = 'WS_CONNECT';
|
||||||
|
export const WS_DISCONNECT = 'WS_DISCONNECT';
|
||||||
|
|
|
@ -110,3 +110,4 @@ export const CAMERA = 'Camera';
|
||||||
export const OPEN_LOG = 'FilePlus';
|
export const OPEN_LOG = 'FilePlus';
|
||||||
export const OPEN_LOG_FOLDER = 'Folder';
|
export const OPEN_LOG_FOLDER = 'Folder';
|
||||||
export const LBRY_STATUS = 'BarChart';
|
export const LBRY_STATUS = 'BarChart';
|
||||||
|
export const NOTIFICATION = 'Bell';
|
||||||
|
|
|
@ -40,4 +40,5 @@ exports.CREATOR_DASHBOARD = 'dashboard';
|
||||||
exports.CHECKOUT = 'checkout';
|
exports.CHECKOUT = 'checkout';
|
||||||
exports.CODE_2257 = '2257';
|
exports.CODE_2257 = '2257';
|
||||||
exports.BUY = 'buy';
|
exports.BUY = 'buy';
|
||||||
exports.CHANNEL_NEW = 'channelnew';
|
exports.CHANNEL_NEW = 'channel/new';
|
||||||
|
exports.NOTIFICATIONS = 'notifications';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doHideModal } from 'redux/actions/app';
|
import { doHideModal } from 'redux/actions/app';
|
||||||
import { doAbandonTxo, doAbandonClaim, selectTransactionItems } from 'lbry-redux';
|
import { doAbandonTxo, doAbandonClaim, selectTransactionItems } from 'lbry-redux';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import ModalRevokeClaim from './view';
|
import ModalRevokeClaim from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
|
@ -8,6 +9,7 @@ const select = state => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
toast: (message, isError) => dispatch(doToast({ message, isError })),
|
||||||
closeModal: () => dispatch(doHideModal()),
|
closeModal: () => dispatch(doHideModal()),
|
||||||
abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)),
|
abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)),
|
||||||
abandonClaim: (txid, nout, cb) => dispatch(doAbandonClaim(txid, nout, cb)),
|
abandonClaim: (txid, nout, cb) => dispatch(doAbandonClaim(txid, nout, cb)),
|
||||||
|
|
|
@ -6,9 +6,9 @@ import {
|
||||||
selectFetchingMyClaimsPageError,
|
selectFetchingMyClaimsPageError,
|
||||||
doClearPublish,
|
doClearPublish,
|
||||||
doFetchClaimListMine,
|
doFetchClaimListMine,
|
||||||
|
doCheckPendingClaims,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectUploadCount } from 'lbryinc';
|
import { selectUploadCount } from 'lbryinc';
|
||||||
import { doCheckPendingPublishesApp } from 'redux/actions/publish';
|
|
||||||
import FileListPublished from './view';
|
import FileListPublished from './view';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { MY_CLAIMS_PAGE_SIZE, PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim';
|
import { MY_CLAIMS_PAGE_SIZE, PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim';
|
||||||
|
@ -31,7 +31,7 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
checkPendingPublishes: () => dispatch(doCheckPendingPublishesApp()),
|
checkPendingPublishes: () => dispatch(doCheckPendingClaims()),
|
||||||
fetchClaimListMine: (page, pageSize) => dispatch(doFetchClaimListMine(page, pageSize)),
|
fetchClaimListMine: (page, pageSize) => dispatch(doFetchClaimListMine(page, pageSize)),
|
||||||
clearPublish: () => dispatch(doClearPublish()),
|
clearPublish: () => dispatch(doClearPublish()),
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,8 +6,8 @@ import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
|
||||||
import getHomepage from 'homepage';
|
import getHomepage from 'homepage';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
followedTags: Array<Tag>,
|
followedTags: Array<Tag>,
|
||||||
|
|
15
ui/page/notifications/index.js
Normal file
15
ui/page/notifications/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectNotifications,
|
||||||
|
selectIsFetchingNotifications,
|
||||||
|
selectUnreadNotificationCount,
|
||||||
|
} from 'redux/selectors/notifications';
|
||||||
|
import NotificationsPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
notifications: selectNotifications(state),
|
||||||
|
fetching: selectIsFetchingNotifications(state),
|
||||||
|
unreadCount: selectUnreadNotificationCount(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(NotificationsPage);
|
40
ui/page/notifications/view.jsx
Normal file
40
ui/page/notifications/view.jsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import Notification from 'component/notification';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
notifications: ?Array<Notification>,
|
||||||
|
fetching: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotificationsPage(props: Props) {
|
||||||
|
const { notifications, fetching } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
{fetching && (
|
||||||
|
<div className="main--empty">
|
||||||
|
<Spinner delayed />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notifications && notifications.length > 0 ? (
|
||||||
|
<Card
|
||||||
|
isBodyList
|
||||||
|
title={__('Notifications')}
|
||||||
|
body={
|
||||||
|
<div className="notification_list">
|
||||||
|
{notifications.map((notification, index) => {
|
||||||
|
return <Notification key={notification.id} notification={notification} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>{__('No notifications')}</div>
|
||||||
|
)}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ import {
|
||||||
selectFollowedTagsList,
|
selectFollowedTagsList,
|
||||||
// SHARED_PREFERENCES,
|
// SHARED_PREFERENCES,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doToast, doError } from 'redux/actions/notifications';
|
import { doToast, doError, doNotificationList } from 'redux/actions/notifications';
|
||||||
import Native from 'native';
|
import Native from 'native';
|
||||||
import {
|
import {
|
||||||
doFetchDaemonSettings,
|
doFetchDaemonSettings,
|
||||||
|
@ -49,6 +49,7 @@ import { doAuthenticate } from 'redux/actions/user';
|
||||||
import { lbrySettings as config, version as appVersion } from 'package.json';
|
import { lbrySettings as config, version as appVersion } from 'package.json';
|
||||||
import analytics, { SHARE_INTERNAL } from 'analytics';
|
import analytics, { SHARE_INTERNAL } from 'analytics';
|
||||||
import { doSignOutCleanup, deleteSavedPassword, getSavedPassword } from 'util/saved-passwords';
|
import { doSignOutCleanup, deleteSavedPassword, getSavedPassword } from 'util/saved-passwords';
|
||||||
|
import { doSocketConnect } from 'redux/actions/websocket';
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
const { autoUpdater } = remote.require('electron-updater');
|
const { autoUpdater } = remote.require('electron-updater');
|
||||||
|
@ -489,13 +490,18 @@ export function doSignIn() {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const user = selectUser(state);
|
const user = selectUser(state);
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
|
const notificationsEnabled = user.experimental_ui;
|
||||||
|
|
||||||
analytics.setUser(userId);
|
analytics.setUser(userId);
|
||||||
|
|
||||||
|
if (notificationsEnabled) {
|
||||||
|
dispatch(doSocketConnect());
|
||||||
|
dispatch(doNotificationList());
|
||||||
|
}
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
dispatch(doBalanceSubscribe());
|
dispatch(doBalanceSubscribe());
|
||||||
dispatch(doFetchChannelListMine());
|
dispatch(doFetchChannelListMine());
|
||||||
|
|
||||||
// @endif
|
// @endif
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
import uuid from 'uuid/v4';
|
import uuid from 'uuid/v4';
|
||||||
|
import { selectNotifications } from 'redux/selectors/notifications';
|
||||||
|
|
||||||
export function doToast(params: ToastParams) {
|
export function doToast(params: ToastParams) {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
|
@ -36,3 +38,39 @@ export function doDismissError() {
|
||||||
type: ACTIONS.DISMISS_ERROR,
|
type: ACTIONS.DISMISS_ERROR,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doNotificationList() {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_LIST_STARTED });
|
||||||
|
return Lbryio.call('notification', 'list')
|
||||||
|
.then(response => {
|
||||||
|
const notifications = response || [];
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_LIST_COMPLETED, data: { notifications } });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_LIST_FAILED, data: { error } });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doReadNotifications() {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const notifications = selectNotifications(state);
|
||||||
|
const unreadNotifications =
|
||||||
|
notifications &&
|
||||||
|
notifications
|
||||||
|
.filter(notification => !notification.is_read)
|
||||||
|
.map(notification => notification.id)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_READ_STARTED });
|
||||||
|
return Lbryio.call('notification', 'edit', { notification_ids: unreadNotifications, is_read: true })
|
||||||
|
.then(() => {
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_READ_COMPLETED });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
dispatch({ type: ACTIONS.NOTIFICATION_READ_FAILED, data: { error } });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -11,10 +11,8 @@ import {
|
||||||
ACTIONS as LBRY_REDUX_ACTIONS,
|
ACTIONS as LBRY_REDUX_ACTIONS,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doError } from 'redux/actions/notifications';
|
import { doError } from 'redux/actions/notifications';
|
||||||
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'connected-react-router';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
import { doOpenModal } from './app';
|
import { doOpenModal } from './app';
|
||||||
|
|
||||||
export const doPublishDesktop = (filePath: string) => (dispatch: Dispatch, getState: () => {}) => {
|
export const doPublishDesktop = (filePath: string) => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
|
@ -61,7 +59,7 @@ export const doPublishDesktop = (filePath: string) => (dispatch: Dispatch, getSt
|
||||||
lbryFirstError,
|
lbryFirstError,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(doCheckPendingPublishesApp());
|
dispatch(doCheckPendingClaims());
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
dispatch(doCheckReflectingFiles());
|
dispatch(doCheckReflectingFiles());
|
||||||
// @endif
|
// @endif
|
||||||
|
@ -85,22 +83,3 @@ export const doPublishDesktop = (filePath: string) => (dispatch: Dispatch, getSt
|
||||||
|
|
||||||
dispatch(doPublish(publishSuccess, publishFail));
|
dispatch(doPublish(publishSuccess, publishFail));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calls claim_list_mine until any pending publishes are confirmed
|
|
||||||
export const doCheckPendingPublishesApp = () => (dispatch: Dispatch, getState: GetState) => {
|
|
||||||
const onConfirmed = claim => {
|
|
||||||
if (selectosNotificationsEnabled(getState())) {
|
|
||||||
const notif = new window.Notification('LBRY Publish Complete', {
|
|
||||||
body: __('%nameOrTitle% has been published to lbry://%name%. Click here to view it.', {
|
|
||||||
nameOrTitle: claim.value_type === 'channel' ? `@${claim.name}` : claim.value.title,
|
|
||||||
name: claim.name,
|
|
||||||
}),
|
|
||||||
silent: false,
|
|
||||||
});
|
|
||||||
notif.onclick = () => {
|
|
||||||
dispatch(push(formatLbryUrlForWeb(claim.permanent_url)));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return dispatch(doCheckPendingClaims(onConfirmed));
|
|
||||||
};
|
|
||||||
|
|
42
ui/redux/actions/websocket.js
Normal file
42
ui/redux/actions/websocket.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { getAuthToken } from 'util/saved-passwords';
|
||||||
|
import { doNotificationList } from 'redux/actions/notifications';
|
||||||
|
|
||||||
|
let socket = null;
|
||||||
|
export const doSocketConnect = () => dispatch => {
|
||||||
|
const authToken = getAuthToken();
|
||||||
|
if (!authToken) {
|
||||||
|
console.error('Unable to connect to web socket because auth token is missing'); // eslint-disable-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket !== null) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = new WebSocket(`wss://api.lbry.com/subscribe?auth_token=${authToken}`);
|
||||||
|
|
||||||
|
socket.onmessage = e => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (data.type === 'pending_notification') {
|
||||||
|
dispatch(doNotificationList());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = e => {
|
||||||
|
console.error('Error connecting to websocket', e); // eslint-disable-line
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = e => {
|
||||||
|
// Reconnect?
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onopen = e => {
|
||||||
|
console.log('\nConnected to WS \n\n'); // eslint-disable-line
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doSocketDisconnect = () => ({
|
||||||
|
type: ACTIONS.WS_DISCONNECT,
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ import { handleActions } from 'util/redux-utils';
|
||||||
|
|
||||||
const defaultState: NotificationState = {
|
const defaultState: NotificationState = {
|
||||||
notifications: [],
|
notifications: [],
|
||||||
|
fetchingNotifications: false,
|
||||||
toasts: [],
|
toasts: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
};
|
};
|
||||||
|
@ -32,40 +33,39 @@ export default handleActions(
|
||||||
},
|
},
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
[ACTIONS.CREATE_NOTIFICATION]: (state: NotificationState, action: DoNotification) => {
|
[ACTIONS.NOTIFICATION_LIST_STARTED]: (state, action) => {
|
||||||
const notification: Notification = action.data;
|
|
||||||
const newNotifications: Array<Notification> = state.notifications.slice();
|
|
||||||
newNotifications.push(notification);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
notifications: newNotifications,
|
fetchingNotifications: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Used to mark notifications as read/dismissed
|
[ACTIONS.NOTIFICATION_LIST_COMPLETED]: (state, action) => {
|
||||||
[ACTIONS.EDIT_NOTIFICATION]: (state: NotificationState, action: DoEditNotification) => {
|
const { notifications } = action.data;
|
||||||
const { notification } = action.data;
|
|
||||||
let notifications: Array<Notification> = state.notifications.slice();
|
|
||||||
|
|
||||||
notifications = notifications.map(pastNotification =>
|
|
||||||
pastNotification.id === notification.id ? notification : pastNotification
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
notifications,
|
notifications,
|
||||||
|
fetchingNotifications: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[ACTIONS.DELETE_NOTIFICATION]: (state: NotificationState, action: DoDeleteNotification) => {
|
[ACTIONS.NOTIFICATION_LIST_FAILED]: (state, action) => {
|
||||||
const { id } = action.data;
|
return {
|
||||||
let newNotifications: Array<Notification> = state.notifications.slice();
|
...state,
|
||||||
newNotifications = newNotifications.filter(notification => notification.id !== id);
|
fetchingNotifications: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ACTIONS.NOTIFICATION_READ_COMPLETED]: (state, action) => {
|
||||||
|
const { notifications } = state;
|
||||||
|
const newNotifications = notifications && notifications.map(notification => ({ ...notification, is_read: true }));
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
notifications: newNotifications,
|
notifications: newNotifications,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[ACTIONS.NOTIFICATION_READ_FAILED]: (state, action) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoError) => {
|
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoError) => {
|
||||||
|
|
|
@ -2,6 +2,14 @@ import { createSelector } from 'reselect';
|
||||||
|
|
||||||
export const selectState = state => state.notifications || {};
|
export const selectState = state => state.notifications || {};
|
||||||
|
|
||||||
|
export const selectNotifications = createSelector(selectState, state => state.notifications);
|
||||||
|
|
||||||
|
export const selectIsFetchingNotifications = createSelector(selectState, state => state.fetchingNotifications);
|
||||||
|
|
||||||
|
export const selectUnreadNotificationCount = createSelector(selectNotifications, notifications => {
|
||||||
|
return notifications ? notifications.filter(notification => !notification.is_read).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];
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
@import 'component/modal';
|
@import 'component/modal';
|
||||||
@import 'component/nag';
|
@import 'component/nag';
|
||||||
@import 'component/navigation';
|
@import 'component/navigation';
|
||||||
|
@import 'component/notification';
|
||||||
@import 'component/pagination';
|
@import 'component/pagination';
|
||||||
@import 'component/purchase';
|
@import 'component/purchase';
|
||||||
@import 'component/placeholder';
|
@import 'component/placeholder';
|
||||||
|
|
|
@ -103,3 +103,9 @@
|
||||||
.comment__menu-icon {
|
.comment__menu-icon {
|
||||||
stroke: var(--color-comment-menu);
|
stroke: var(--color-comment-menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment__menu-list {
|
||||||
|
box-shadow: var(--card-box-shadow);
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
stroke: var(--color-text);
|
stroke: var(--color-text);
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
height: 3.5rem;
|
||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
border-radius: calc(3.5rem / 2);
|
border-radius: calc(3.5rem / 2);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
37
ui/scss/component/_notification.scss
Normal file
37
ui/scss/component/_notification.scss
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
.notifications__empty {
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification_list {
|
||||||
|
> * {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__content {
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__title {
|
||||||
|
font-size: var(--font-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__text {
|
||||||
|
font-size: var(--font-body);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__time {
|
||||||
|
@extend .help;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
.snack-bar {
|
.snack-bar {
|
||||||
bottom: 2rem;
|
bottom: 2rem;
|
||||||
left: 2rem;
|
right: 2rem;
|
||||||
|
max-width: 20rem;
|
||||||
background-color: var(--color-snack-bg);
|
background-color: var(--color-snack-bg);
|
||||||
color: var(--color-snack);
|
color: var(--color-snack);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
@ -25,6 +26,12 @@
|
||||||
background-color: var(--color-snack-bg-error);
|
background-color: var(--color-snack-bg-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.snack-bar--notification {
|
||||||
|
@extend .card;
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
.snack-bar__action {
|
.snack-bar__action {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: var(--spacing-s) 0;
|
margin: var(--spacing-s) 0;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10000;
|
z-index: 2;
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,13 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: var(--color-menu-background);
|
background-color: var(--color-menu-background);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-menu-item] {
|
[data-reach-menu-item] {
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 10000;
|
z-index: 2;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -35,23 +36,21 @@
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-decoration: initial;
|
text-decoration: initial;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
.icon {
|
|
||||||
stroke: var(--color-menu-icon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-menu-item][data-selected] {
|
[data-reach-menu-item][data-selected] {
|
||||||
background-color: var(--color-menu-background--active);
|
background-color: var(--color-menu-background--active);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.menu__title {
|
||||||
stroke: var(--color-menu-icon-active);
|
&[aria-expanded='true'] {
|
||||||
|
background-color: var(--color-primary-alt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__list {
|
.menu__list {
|
||||||
margin-left: calc(var(--spacing-m) * -1);
|
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||||
border-bottom-left-radius: var(--border-radius);
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
@ -60,6 +59,7 @@
|
||||||
|
|
||||||
.menu__list--header {
|
.menu__list--header {
|
||||||
@extend .menu__list;
|
@extend .menu__list;
|
||||||
|
padding: var(--spacing-xs);
|
||||||
margin-top: 19px;
|
margin-top: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,8 +72,9 @@
|
||||||
.menu__link {
|
.menu__link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-m);
|
padding: var(--spacing-s);
|
||||||
padding-right: var(--spacing-l);
|
padding-right: var(--spacing-l);
|
||||||
|
height: var(--button-height);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
stroke: var(--color-menu-icon);
|
stroke: var(--color-menu-icon);
|
||||||
|
@ -91,3 +92,27 @@
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu__link--notification {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
.icon__wrapper {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu__link--all-notifications {
|
||||||
|
@extend .button--alt;
|
||||||
|
width: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-right: auto;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
|
@ -301,3 +301,17 @@ textarea {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification__bubble {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f02849;
|
||||||
|
position: absolute;
|
||||||
|
top: -0.5rem;
|
||||||
|
right: -0.5rem;
|
||||||
|
color: white;
|
||||||
|
font-size: var(--font-small);
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
// Menu
|
// Menu
|
||||||
--color-menu-background: var(--color-header-background);
|
--color-menu-background: var(--color-header-background);
|
||||||
--color-menu-background--selected: var(--color-secondary-alt);
|
--color-menu-background--selected: var(--color-secondary-alt);
|
||||||
--color-menu-background--active: var(--color-secondary-alt);
|
--color-menu-background--active: #f0f7ff;
|
||||||
--color-menu-icon: var(--color-navigation-link);
|
--color-menu-icon: var(--color-navigation-link);
|
||||||
--color-menu-icon-active: var(--color-navigation-link);
|
--color-menu-icon-active: var(--color-navigation-link);
|
||||||
|
|
||||||
|
@ -75,9 +75,9 @@
|
||||||
--color-editor-attr: #04b0f4;
|
--color-editor-attr: #04b0f4;
|
||||||
--color-editor-string: #ff7451;
|
--color-editor-string: #ff7451;
|
||||||
--color-editor-inline-code-fg: var(--color-text);
|
--color-editor-inline-code-fg: var(--color-text);
|
||||||
--color-editor-inline-code-fg-preview: #eeeeee;
|
--color-editor-inline-code-fg-preview: #2e3439;
|
||||||
--color-editor-inline-code-bg: rgba(157, 161, 165, 0.3);
|
--color-editor-inline-code-bg: rgba(157, 161, 165, 0.3);
|
||||||
--color-editor-inline-code-bg-preview: #8f9499;
|
--color-editor-inline-code-bg-preview: #d0e8ff;
|
||||||
--color-editor-selected: #add6ff;
|
--color-editor-selected: #add6ff;
|
||||||
--color-editor-link: var(--color-link);
|
--color-editor-link: var(--color-link);
|
||||||
--color-editor-url: var(--color-editor-string);
|
--color-editor-url: var(--color-editor-string);
|
||||||
|
|
|
@ -2,7 +2,13 @@
|
||||||
const PAGES = require('../constants/pages');
|
const PAGES = require('../constants/pages');
|
||||||
|
|
||||||
exports.formatLbryUrlForWeb = uri => {
|
exports.formatLbryUrlForWeb = uri => {
|
||||||
return uri.replace('lbry://', '/').replace(/#/g, ':');
|
let newUrl = uri.replace('lbry://', '/').replace(/#/g, ':');
|
||||||
|
if (newUrl.startsWith('/?')) {
|
||||||
|
// This is a lbry link to an internal page ex: lbry://?rewards
|
||||||
|
newUrl = newUrl.replace('/?', '/$/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.formatFileSystemPath = path => {
|
exports.formatFileSystemPath = path => {
|
||||||
|
|
Loading…
Reference in a new issue