lbry-desktop/ui/component/notification/view.jsx
saltrafael b75a4014b6
Re-design comment threads (#1489)
* Redesign threadline and fetching state

- threadline goes right below channel avatar, mimicking reddits implementation, has a increase effect on hover and is slimmer, creating more space for comments on screen
- fetching state now replaces show/hide button, also mimicking reddit, and now says that it is loading, instead of a blank spinner, and also improves space a bit

* Redesign comment threads

- Allow for infinite comment chains
- Can go back and forth between the pages
- Can go back to all comments or to the first comment in the chain
- Some other improvements, which include:
- add title on non-drawer comment sections (couldn't see amount of comments)
- fix Expandable component (would begin expanded and collapse after the effect runs, which looked bad and shifted the layout, now each comments greater than the set length begins collapsed)
- used constants for consistency

* Fix replying to last thread comment

* Fix buttons condition (only on fetched comment to avoid deleted case)

* Fix auto-scroll

* Bring back instant feedback for Show More replies

* Improve thread back links

- Now going back to all comments links the top-level comment for easier navigation
- Going back to ~ previous ~ now goes back into the chain instead of topmost level

* Clear timeouts due to unrelated issue

* Fix deep thread linked comment case and more scroll improvements

* More minor changes

* Flow

* Fix commentList tile style

* Fix long channel names overflowing on small screens

* More scroll changes

* Fix threadline

* Revert "Fix long channel names overflowing on small screens"

This reverts commit e4d2dc7da5861ed8136a60f3352e41a690cd4d33.

* Fix replies fetch

* Revert "Fix replies fetch"

This reverts commit ec70054675a604a7a5f3764ba07c36bf7b0f49c8.

* Cleanup and make smooth

* Always use linked comment on threads

* Cleanup

* Higlight thread comment

* Fix comment body styles
2022-05-16 06:22:13 -04:00

267 lines
9.5 KiB
JavaScript

// @flow
import { lazyImport } from 'util/lazyImport';
import { formatLbryUrlForWeb } from 'util/url';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import { NavLink } from 'react-router-dom';
import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view';
import { parseURI } from 'util/lbryURI';
import { RULE } from 'constants/notifications';
import { useHistory } from 'react-router';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames';
import DateTime from 'component/dateTime';
import FileThumbnail from 'component/fileThumbnail';
import Icon from 'component/common/icon';
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
import React from 'react';
import UriIndicator from 'component/uriIndicator';
import { generateNotificationTitle } from './helpers/title';
import { generateNotificationText } from './helpers/text';
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
const CommentCreate = lazyImport(() => import('component/commentCreate' /* webpackChunkName: "comments" */));
const CommentReactions = lazyImport(() => import('component/commentReactions' /* webpackChunkName: "comments" */));
const CommentsReplies = lazyImport(() => import('component/commentsReplies' /* webpackChunkName: "comments" */));
type Props = {
menuButton: boolean,
notification: WebNotification,
deleteNotification: () => void,
readNotification: () => void,
};
export default function Notification(props: Props) {
const { menuButton = false, notification, readNotification, deleteNotification } = props;
const { notification_rule, notification_parameters, is_read } = notification;
const { push } = useHistory();
const [isReplying, setReplying] = React.useState(false);
const [quickReply, setQuickReply] = React.useState();
const isCommentNotification =
notification_rule === RULE.COMMENT ||
notification_rule === RULE.COMMENT_REPLY ||
notification_rule === RULE.CREATOR_COMMENT;
const notificationTarget = getNotificationTarget();
const creatorIcon = (channelUrl, channelThumbnail) => (
<UriIndicator uri={channelUrl} link showAtSign channelInfo={{ uri: channelUrl, name: '' }}>
<ChannelThumbnail small thumbnailPreview={channelThumbnail} uri={channelThumbnail ? undefined : channelUrl} />
</UriIndicator>
);
let channelUrl;
let icon;
switch (notification_rule) {
case RULE.CREATOR_SUBSCRIBER:
icon = <Icon icon={ICONS.SUBSCRIBE} sectionIcon />;
break;
case RULE.COMMENT:
case RULE.CREATOR_COMMENT:
channelUrl = notification_parameters.dynamic.comment_author;
icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.comment_author_thumbnail);
break;
case RULE.COMMENT_REPLY:
channelUrl = notification_parameters.dynamic.reply_author;
icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.comment_author_thumbnail);
break;
case RULE.NEW_CONTENT:
channelUrl = notification_parameters.dynamic.channel_url;
icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.channel_thumbnail);
break;
case RULE.NEW_LIVESTREAM:
channelUrl = notification_parameters.dynamic.channel_url;
icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.channel_thumbnail);
break;
case RULE.WEEKLY_WATCH_REMINDER:
case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND:
case RULE.MISSED_OUT:
case RULE.REWARDS_APPROVAL_PROMPT:
icon = <Icon icon={ICONS.LBC} sectionIcon />;
break;
case RULE.FIAT_TIP:
icon = <Icon icon={ICONS.FINANCE} sectionIcon />;
break;
default:
icon = <Icon icon={ICONS.NOTIFICATION} sectionIcon />;
}
let notificationLink = formatLbryUrlForWeb(notificationTarget);
let urlParams = new URLSearchParams();
if (isCommentNotification && notification_parameters.dynamic.hash) {
urlParams.append(LINKED_COMMENT_QUERY_PARAM, notification_parameters.dynamic.hash);
}
let channelName;
if (channelUrl) {
try {
({ claimName: channelName } = parseURI(channelUrl));
} catch (e) {}
}
try {
const { isChannel } = parseURI(notificationTarget);
if (isChannel) urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
} catch (e) {}
notificationLink += `?${urlParams.toString()}`;
const navLinkProps = { to: notificationLink, onClick: (e) => e.stopPropagation() };
function getNotificationTarget() {
switch (notification_rule) {
case RULE.WEEKLY_WATCH_REMINDER:
case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND:
return `/$/${PAGES.CHANNELS_FOLLOWING}`;
case RULE.MISSED_OUT:
case RULE.REWARDS_APPROVAL_PROMPT:
return `/$/${PAGES.REWARDS_VERIFY}?redirect=/$/${PAGES.REWARDS}`;
default:
return notification_parameters.device.target;
}
}
function handleNotificationClick() {
if (!is_read) readNotification();
if (menuButton && notificationLink) push(notificationLink);
}
const Wrapper = menuButton
? (props: { children: any }) => (
<MenuItem className="menu__link--notification" onSelect={handleNotificationClick}>
{props.children}
</MenuItem>
)
: notificationLink
? (props: { children: any }) => (
<NavLink {...navLinkProps} className="menu__link--notification" onClick={handleNotificationClick}>
{props.children}
</NavLink>
)
: (props: { children: any }) => (
<span
className={is_read ? 'menu__link--notification-nolink' : 'menu__link--notification'}
onClick={handleNotificationClick}
>
{props.children}
</span>
);
return (
<div className={classnames('notification__wrapper', { 'notification__wrapper--unread': !is_read })}>
<Wrapper>
<div className="notification__icon">{icon}</div>
<div className="notificationContent__wrapper">
<div className="notification__content">
<div className="notificationText__wrapper">
<div className="notification__title">
{generateNotificationTitle(notification_rule, notification_parameters, channelName)}
</div>
{generateNotificationText(notification_rule, notification_parameters)}
</div>
{notification_rule === RULE.NEW_CONTENT && (
<FileThumbnail
uri={notification_parameters.device.target}
thumbnail={notification_parameters?.dynamic?.claim_thumbnail}
className="notificationContent__thumbnail"
/>
)}
{notification_rule === RULE.NEW_LIVESTREAM && (
<FileThumbnail
thumbnail={notification_parameters.device.image_url}
className="notificationContent__thumbnail"
/>
)}
</div>
<div className="notification__extra">
{!is_read && (
<Button
className="notification__markSeen"
onClick={(e) => {
e.stopPropagation();
readNotification();
}}
/>
)}
<div className="notification__time">
<DateTime timeAgo date={notification.active_at} />
</div>
</div>
</div>
<div className="notification__menu">
<Menu>
<MenuButton
className="menu__button notification__menuButton"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
</MenuButton>
<MenuList className="menu__list">
<MenuItem className="menu__link" onSelect={() => deleteNotification()}>
<Icon aria-hidden icon={ICONS.DELETE} />
{__('Delete')}
</MenuItem>
{notification_rule === RULE.NEW_CONTENT && channelUrl ? (
<NotificationContentChannelMenu uri={channelUrl} />
) : null}
</MenuList>
</Menu>
</div>
</Wrapper>
{isCommentNotification && (
<div>
<div className="notification__reactions">
<Button
label={__('Reply')}
className="comment__action"
onClick={() => setReplying(!isReplying)}
icon={ICONS.REPLY}
/>
<CommentReactions
uri={notificationTarget}
commentId={notification_parameters.dynamic.hash}
hideCreatorLike
/>
</div>
{isReplying && (
<React.Suspense fallback={null}>
<CommentCreate
isReply
uri={notificationTarget}
parentId={notification_parameters.dynamic.hash}
onDoneReplying={() => setReplying(false)}
onCancelReplying={() => setReplying(false)}
setQuickReply={setQuickReply}
supportDisabled
shouldFetchComment
/>
</React.Suspense>
)}
{quickReply && (
<CommentsReplies
uri={notificationTarget}
parentId={notification_parameters.dynamic.hash}
numDirectReplies={1}
supportDisabled
/>
)}
</div>
)}
</div>
);
}