Add direct reacting from notifications

This commit is contained in:
saltrafael 2021-08-27 07:29:58 -03:00 committed by infinite-persistence
parent 6637c7b98b
commit d7344f5047
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
12 changed files with 226 additions and 63 deletions

View file

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add confirmation on comment removal _community pr!_ ([#6563](https://github.com/lbryio/lbry-desktop/pull/6563))
- Show on content page if a file is part of a playlist already _community pr!_([#6393](https://github.com/lbryio/lbry-desktop/pull/6393))
- Add filtering to playlists ([#6905](https://github.com/lbryio/lbry-desktop/pull/6905))
- Added direct replying to notifications _community pr!_ ([#6935](https://github.com/lbryio/lbry-desktop/pull/6935))
### Changed
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))

View file

@ -64,6 +64,9 @@ type Props = {
isModerator: boolean,
isGlobalMod: boolean,
isFiat: boolean,
supportDisabled: boolean,
setQuickReply: (any) => void,
quickReply: any,
};
const LENGTH_TO_COLLAPSE = 300;
@ -100,6 +103,9 @@ function Comment(props: Props) {
isModerator,
isGlobalMod,
isFiat,
supportDisabled,
setQuickReply,
quickReply,
} = props;
const {
@ -185,6 +191,7 @@ function Comment(props: Props) {
function handleSubmit() {
updateComment(commentId, editedMessage);
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
setEditing(false);
}
@ -294,6 +301,7 @@ function Comment(props: Props) {
commentIsMine={commentIsMine}
handleEditComment={handleEditComment}
supportAmount={supportAmount}
setQuickReply={setQuickReply}
/>
</Menu>
</div>
@ -403,6 +411,7 @@ function Comment(props: Props) {
onCancelReplying={() => {
setReplying(false);
}}
supportDisabled={supportDisabled}
/>
)}
</>

View file

@ -47,8 +47,10 @@ type Props = {
claimIsMine: boolean,
sendTip: ({}, (any) => void, (any) => void) => void,
doToast: ({ message: string }) => void,
supportDisabled: boolean,
doFetchCreatorSettings: (channelId: string) => Promise<any>,
settingsByChannelId: { [channelId: string]: PerChannelSettings },
setQuickReply: (any) => void,
};
export function CommentCreate(props: Props) {
@ -71,6 +73,8 @@ export function CommentCreate(props: Props) {
doToast,
doFetchCreatorSettings,
settingsByChannelId,
supportDisabled,
setQuickReply,
} = props;
const buttonRef: ElementRef<any> = React.useRef();
const {
@ -80,7 +84,7 @@ export function CommentCreate(props: Props) {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [commentFailure, setCommentFailure] = React.useState(false);
const [successTip, setSuccessTip] = React.useState({ txid: undefined, tipAmount: undefined });
const { claim_id: claimId } = claim;
const claimId = claim && claim.claim_id;
const [isSupportComment, setIsSupportComment] = React.useState();
const [isReviewingSupportComment, setIsReviewingSupportComment] = React.useState();
const [tipAmount, setTipAmount] = React.useState(1);
@ -322,6 +326,7 @@ export function CommentCreate(props: Props) {
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
.then((res) => {
setIsSubmitting(false);
if (setQuickReply) setQuickReply(res);
if (res && res.signature) {
setCommentValue('');
@ -522,32 +527,34 @@ export function CommentCreate(props: Props) {
requiresAuth={IS_WEB}
/>
)}
{!claimIsMine && (
<Button
disabled={disabled}
button="alt"
className="thatButton"
icon={ICONS.LBC}
onClick={() => {
setIsSupportComment(true);
setActiveTab(TAB_LBC);
}}
/>
{!supportDisabled && !claimIsMine && (
<>
<Button
disabled={disabled}
button="alt"
className="thatButton"
icon={ICONS.LBC}
onClick={() => {
setIsSupportComment(true);
setActiveTab(TAB_LBC);
}}
/>
{/* @if TARGET='web' */}
{stripeEnvironment && (
<Button
disabled={disabled}
button="alt"
className="thisButton"
icon={ICONS.FINANCE}
onClick={() => {
setIsSupportComment(true);
setActiveTab(TAB_FIAT);
}}
/>
)}
{/* @endif */}
</>
)}
{/* @if TARGET='web' */}
{!claimIsMine && stripeEnvironment && (
<Button
disabled={disabled}
button="alt"
className="thisButton"
icon={ICONS.FINANCE}
onClick={() => {
setIsSupportComment(true);
setActiveTab(TAB_FIAT);
}}
/>
)}
{/* @endif */}
{isReply && !minTip && (
<Button
button="link"

View file

@ -33,6 +33,7 @@ type Props = {
commentModBlockAsAdmin: (string, string) => void,
commentModBlockAsModerator: (string, string, string) => void,
commentModAddDelegate: (string, string, ChannelClaim) => void,
setQuickReply: (any) => void,
};
function CommentMenuList(props: Props) {
@ -59,6 +60,7 @@ function CommentMenuList(props: Props) {
moderationDelegatorsById,
openModal,
supportAmount,
setQuickReply,
} = props;
const contentChannelClaim = !claim
@ -86,7 +88,13 @@ function CommentMenuList(props: Props) {
if (playingUri && playingUri.source === 'comment') {
clearPlayingUri();
}
openModal(MODALS.CONFIRM_REMOVE_COMMENT, { commentId, commentIsMine, contentChannelPermanentUrl, supportAmount });
openModal(MODALS.CONFIRM_REMOVE_COMMENT, {
commentId,
commentIsMine,
contentChannelPermanentUrl,
supportAmount,
setQuickReply,
});
}
function handleCommentBlock() {

View file

@ -19,10 +19,21 @@ type Props = {
activeChannelId: ?string,
claim: ?ChannelClaim,
doToast: ({ message: string }) => void,
hideCreatorLike: boolean,
};
export default function CommentReactions(props: Props) {
const { myReacts, othersReacts, commentId, react, claimIsMine, claim, activeChannelId, doToast } = props;
const {
myReacts,
othersReacts,
commentId,
react,
claimIsMine,
claim,
activeChannelId,
doToast,
hideCreatorLike,
} = props;
const {
push,
location: { pathname },
@ -48,7 +59,7 @@ export default function CommentReactions(props: Props) {
}
return count;
};
const shouldHide = !canCreatorReact && hideCreatorLike;
const creatorLiked = getCountForReact(REACTION_TYPES.CREATOR_LIKE) > 0;
const likeIcon = SIMPLE_SITE
? myReacts.includes(REACTION_TYPES.LIKE)
@ -105,9 +116,8 @@ export default function CommentReactions(props: Props) {
label={<span className="comment__reaction-count">{getCountForReact(REACTION_TYPES.DISLIKE)}</span>}
/>
{ENABLE_CREATOR_REACTIONS && (canCreatorReact || creatorLiked) && (
{!shouldHide && ENABLE_CREATOR_REACTIONS && (canCreatorReact || creatorLiked) && (
<Button
iconOnly
disabled={!canCreatorReact || !claimIsMine}
requiresAuth={IS_WEB}
title={claimIsMine ? __('You loved this') : __('Creator loved this')}

View file

@ -18,6 +18,7 @@ type Props = {
isFetchingByParentId: { [string]: boolean },
onShowMore?: () => void,
hasMore: boolean,
supportDisabled: boolean,
};
function CommentsReplies(props: Props) {
@ -34,6 +35,7 @@ function CommentsReplies(props: Props) {
isFetchingByParentId,
onShowMore,
hasMore,
supportDisabled,
} = props;
const [isExpanded, setExpanded] = React.useState(true);
@ -98,6 +100,7 @@ function CommentsReplies(props: Props) {
numDirectReplies={comment.replies}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
supportDisabled={supportDisabled}
/>
);
})}

View file

@ -18,6 +18,9 @@ import NotificationContentChannelMenu from 'component/notificationContentChannel
import LbcMessage from 'component/common/lbc-message';
import UriIndicator from 'component/uriIndicator';
import { NavLink } from 'react-router-dom';
import CommentReactions from 'component/commentReactions';
import CommentCreate from 'component/commentCreate';
import CommentsReplies from 'component/commentsReplies';
type Props = {
notification: WebNotification,
@ -31,6 +34,8 @@ export default function Notification(props: Props) {
const { notification, menuButton = false, doReadNotifications, doDeleteNotification } = props;
const { push } = useHistory();
const { notification_rule, notification_parameters, is_read, id } = notification;
const [isReplying, setReplying] = React.useState(false);
const [quickReply, setQuickReply] = React.useState();
const isCommentNotification =
notification_rule === RULE.COMMENT ||
@ -119,7 +124,8 @@ export default function Notification(props: Props) {
fullTitle.push(message);
if (index === titleSplit.length - 1) {
return <LbcMessage>{fullTitle.join(' ')}</LbcMessage>;
const result = fullTitle.join(' ');
return <LbcMessage key={result}>{result}</LbcMessage>;
}
}
});
@ -147,11 +153,6 @@ export default function Notification(props: Props) {
}
}
function handleReadNotification(e) {
e.stopPropagation();
doReadNotifications([id]);
}
const Wrapper = menuButton
? (props: { children: any }) => (
<MenuItem className="menu__link--notification" onSelect={handleNotificationClick}>
@ -160,10 +161,8 @@ export default function Notification(props: Props) {
)
: notificationLink
? (props: { children: any }) => (
<NavLink {...navLinkProps} className="menu__link--notification">
<a className="menu__link--notification" onClick={handleNotificationClick}>
{props.children}
</a>
<NavLink {...navLinkProps} className="menu__link--notification" onClick={handleNotificationClick}>
{props.children}
</NavLink>
)
: (props: { children: any }) => (
@ -176,12 +175,12 @@ export default function Notification(props: Props) {
);
return (
<Wrapper>
<div
className={classnames('notification__wrapper', {
'notification__wrapper--unread': !is_read,
})}
>
<div
className={classnames('notification__wrapper', {
'notification__wrapper--unread': !is_read,
})}
>
<Wrapper>
<div className="notification__icon">{icon}</div>
<div className="notification__content-wrapper">
@ -220,7 +219,15 @@ export default function Notification(props: Props) {
</div>
<div className="notification__extra">
{!is_read && <Button className="notification__mark-seen" onClick={handleReadNotification} />}
{!is_read && (
<Button
className="notification__mark-seen"
onClick={(e) => {
e.stopPropagation();
doReadNotifications([id]);
}}
/>
)}
<div className="notification__time">
<DateTime timeAgo date={notification.active_at} />
</div>
@ -249,7 +256,46 @@ export default function Notification(props: Props) {
</MenuList>
</Menu>
</div>
</div>
</Wrapper>
</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 && (
<CommentCreate
isReply
uri={notificationTarget}
parentId={notification_parameters.dynamic.hash}
onDoneReplying={() => setReplying(false)}
onCancelReplying={() => setReplying(false)}
setQuickReply={setQuickReply}
supportDisabled
/>
)}
{quickReply && (
<CommentsReplies
uri={notificationTarget}
parentId={notification_parameters.dynamic.hash}
numDirectReplies={1}
supportDisabled
/>
)}
</div>
)}
</div>
);
}

View file

@ -11,10 +11,19 @@ type Props = {
closeModal: () => void,
deleteComment: (string, ?string) => void,
supportAmount?: any,
setQuickReply: (any) => void,
};
function ModalRemoveComment(props: Props) {
const { commentId, commentIsMine, contentChannelPermanentUrl, closeModal, deleteComment, supportAmount } = props;
const {
commentId,
commentIsMine,
contentChannelPermanentUrl,
closeModal,
deleteComment,
supportAmount,
setQuickReply,
} = props;
return (
<Modal isOpen contentLabel={__('Confirm Comment Deletion')} type="card" onAborted={closeModal}>
@ -24,7 +33,9 @@ function ModalRemoveComment(props: Props) {
<React.Fragment>
<p>{__('Are you sure you want to remove this comment?')}</p>
{Boolean(supportAmount) && (
<p className="help error__text"> {__('This comment has a tip associated with it which cannot be reverted.')}</p>
<p className="help error__text">
{__('This comment has a tip associated with it which cannot be reverted.')}
</p>
)}
</React.Fragment>
}
@ -35,8 +46,9 @@ function ModalRemoveComment(props: Props) {
button="primary"
label={__('Remove')}
onClick={() => {
deleteComment(commentId, commentIsMine ? undefined : contentChannelPermanentUrl);
closeModal();
deleteComment(commentId, commentIsMine ? undefined : contentChannelPermanentUrl);
if (setQuickReply) setQuickReply(undefined);
}}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} />

View file

@ -7,6 +7,8 @@ import {
selectUnseenNotificationCount,
selectNotificationCategories,
} from 'redux/selectors/notifications';
import { doCommentReactList } from 'redux/actions/comments';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doReadNotifications, doNotificationList, doSeeAllNotifications } from 'redux/actions/notifications';
import NotificationsPage from './view';
@ -17,10 +19,12 @@ const select = (state) => ({
fetching: selectIsFetchingNotifications(state),
unreadCount: selectUnreadNotificationCount(state),
unseenCount: selectUnseenNotificationCount(state),
activeChannel: selectActiveChannelClaim(state),
});
export default connect(select, {
doReadNotifications,
doNotificationList,
doSeeAllNotifications,
doCommentReactList,
})(NotificationsPage);

View file

@ -10,6 +10,7 @@ import usePersistedState from 'effects/use-persisted-state';
import Yrbl from 'component/yrbl';
import * as NOTIFICATIONS from 'constants/notifications';
import useFetched from 'effects/use-fetched';
import { RULE } from 'constants/notifications';
type Props = {
notifications: Array<Notification>,
@ -21,6 +22,8 @@ type Props = {
doSeeAllNotifications: () => void,
doReadNotifications: () => void,
doNotificationList: (?Array<string>) => void,
activeChannel: ?ChannelClaim,
doCommentReactList: (Array<string>) => Promise<any>,
};
export default function NotificationsPage(props: Props) {
@ -34,12 +37,41 @@ export default function NotificationsPage(props: Props) {
doReadNotifications,
doNotificationList,
notificationCategories,
activeChannel,
doCommentReactList,
} = props;
const initialFetchDone = useFetched(fetching);
const [name, setName] = usePersistedState('notifications--rule', NOTIFICATIONS.NOTIFICATION_NAME_ALL);
const isFiltered = name !== NOTIFICATIONS.NOTIFICATION_NAME_ALL;
const list = isFiltered ? notificationsFiltered : notifications;
// Fetch reacts
React.useEffect(() => {
if (initialFetchDone && activeChannel) {
let idsForReactionFetch = [];
list.map((notification) => {
const { notification_rule, notification_parameters } = notification;
const isComment =
notification_rule === RULE.COMMENT ||
notification_rule === RULE.COMMENT_REPLY ||
notification_rule === RULE.CREATOR_COMMENT;
const commentId =
isComment &&
notification_parameters &&
notification_parameters.dynamic &&
notification_parameters.dynamic.hash;
if (commentId) {
idsForReactionFetch.push(commentId);
}
});
if (idsForReactionFetch.length !== 0) {
doCommentReactList(idsForReactionFetch);
}
}
}, [initialFetchDone, doCommentReactList, list, activeChannel]);
React.useEffect(() => {
if (unseenCount > 0 || unreadCount > 0) {
// If there are unread notifications when entering the page, reset to All.
@ -106,7 +138,7 @@ export default function NotificationsPage(props: Props) {
{list && list.length > 0 && !(isFiltered && fetching) ? (
<div className="card">
<div className="notification_list">
{list.map((notification, index) => {
{list.map((notification) => {
return <Notification key={notification.id} notification={notification} />;
})}
</div>
@ -117,11 +149,9 @@ export default function NotificationsPage(props: Props) {
<Yrbl
title={__('No notifications')}
subtitle={
<p>
{isFiltered
? __('Try selecting another filter.')
: __("You don't have any notifications yet, but they will be here when you do!")}
</p>
isFiltered
? __('Try selecting another filter.')
: __("You don't have any notifications yet, but they will be here when you do!")
}
actions={
<div className="section__actions">

View file

@ -249,6 +249,7 @@ $thumbnailWidthSmall: 1rem;
.comment__message {
word-break: break-word;
max-width: 35rem;
color: var(--color-text);
ul li,
ol li {
@ -298,6 +299,7 @@ $thumbnailWidthSmall: 1rem;
.comment__char-count {
font-size: var(--font-xsmall);
color: var(--color-text);
}
.comment__char-count-mde {

View file

@ -7,18 +7,24 @@ $contentMaxWidth: 60rem;
}
.notification_list {
> * {
border-bottom: 1px solid var(--color-border);
.notification__wrapper {
border-top: 1px solid var(--color-border);
&:last-of-type {
border-bottom: none;
&:first-of-type {
border-top: none;
}
}
.comment__create,
.comment__content {
margin: var(--spacing-m);
}
}
.notification__icon {
display: flex;
align-items: flex-start;
margin-top: var(--spacing-xs);
.icon__wrapper {
width: 1rem;
@ -43,6 +49,7 @@ $contentMaxWidth: 60rem;
display: flex;
padding: var(--spacing-m) 0;
justify-content: space-between;
flex-direction: column;
.channel-thumbnail {
@include handleChannelGif(3rem);
@ -162,6 +169,30 @@ $contentMaxWidth: 60rem;
}
}
.notification__reactions {
display: flex;
margin: var(--spacing-m);
margin-bottom: 0;
> *:not(:last-of-type) {
margin-right: var(--spacing-m);
}
.button__label {
margin-left: var(--spacing-xs);
}
.comment__creator-like {
height: 0.8rem;
width: 0.8rem;
margin-left: 3px;
z-index: 3;
position: absolute;
top: 0.4rem;
left: 0.4rem;
}
}
.notification__bubble {
height: 1.5rem;
width: 1.5rem;