Direct reacting from notifications #6935
15 changed files with 264 additions and 67 deletions
|
@ -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))
|
||||
![]() ![]() @infinite-persistence if this looks good on another review, please merge @infinite-persistence if this looks good on another review, please merge
|
||||
|
||||
### Changed
|
||||
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
doSendTip,
|
||||
} from 'lbry-redux';
|
||||
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
||||
import { doCommentCreate, doFetchCreatorSettings } from 'redux/actions/comments';
|
||||
import { doCommentCreate, doFetchCreatorSettings, doCommentById } from 'redux/actions/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectSettingsByChannelId } from 'redux/selectors/comments';
|
||||
|
@ -43,6 +43,7 @@ const perform = (dispatch, ownProps) => ({
|
|||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId, false)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentCreate);
|
||||
|
|
|
@ -47,8 +47,12 @@ 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,
|
||||
fetchComment: (commentId: string) => Promise<any>,
|
||||
shouldFetchComment: boolean,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
|
@ -71,6 +75,10 @@ export function CommentCreate(props: Props) {
|
|||
doToast,
|
||||
doFetchCreatorSettings,
|
||||
settingsByChannelId,
|
||||
supportDisabled,
|
||||
setQuickReply,
|
||||
fetchComment,
|
||||
shouldFetchComment,
|
||||
} = props;
|
||||
const buttonRef: ElementRef<any> = React.useRef();
|
||||
const {
|
||||
|
@ -80,7 +88,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);
|
||||
|
@ -90,7 +98,8 @@ export function CommentCreate(props: Props) {
|
|||
const charCount = commentValue.length;
|
||||
const [activeTab, setActiveTab] = React.useState('');
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const disabled = isSubmitting || isFetchingChannels || !commentValue.length;
|
||||
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||
const disabled = deletedComment || isSubmitting || isFetchingChannels || !commentValue.length;
|
||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
|
@ -99,6 +108,15 @@ export function CommentCreate(props: Props) {
|
|||
const minAmount = minTip || minSuper || 0;
|
||||
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
||||
|
||||
// Fetch top-level comments to identify if it has been deleted and can reply to it
|
||||
React.useEffect(() => {
|
||||
if (shouldFetchComment && fetchComment) {
|
||||
fetchComment(parentId).then((result) => {
|
||||
setDeletedComment(String(result).includes('Error'));
|
||||
});
|
||||
}
|
||||
}, [fetchComment, shouldFetchComment, parentId]);
|
||||
|
||||
const minAmountRef = React.useRef(minAmount);
|
||||
minAmountRef.current = minAmount;
|
||||
|
||||
|
@ -322,6 +340,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 +541,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"
|
||||
|
@ -561,6 +582,7 @@ export function CommentCreate(props: Props) {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{MinAmountNotice}
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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')}
|
||||
|
@ -116,7 +126,7 @@ export default function CommentReactions(props: Props) {
|
|||
onClick={() => react(commentId, REACTION_TYPES.CREATOR_LIKE)}
|
||||
>
|
||||
{creatorLiked && (
|
||||
<ChannelThumbnail xsmall uri={authorUri} hideStakedIndicator className="comment__creator-like" />
|
||||
<ChannelThumbnail xsmall uri={authorUri} hideStakedIndicator className="comment__creator-like" allowGifs />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -170,6 +170,8 @@ export function doCommentById(commentId: string, toastIfNotFound: boolean = true
|
|||
} else {
|
||||
devToast(dispatch, error.message);
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,18 +7,25 @@ $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);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.notification__icon {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: auto;
|
||||
|
||||
.icon__wrapper {
|
||||
width: 1rem;
|
||||
|
@ -36,6 +43,10 @@ $contentMaxWidth: 60rem;
|
|||
align-items: center;
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.notification__wrapper {
|
||||
|
@ -43,6 +54,7 @@ $contentMaxWidth: 60rem;
|
|||
display: flex;
|
||||
padding: var(--spacing-m) 0;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(3rem);
|
||||
|
@ -57,6 +69,16 @@ $contentMaxWidth: 60rem;
|
|||
@media (max-width: $breakpoint-small) {
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__creator-like {
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
margin-left: 3px;
|
||||
z-index: 3;
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
left: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notification__wrapper--unread {
|
||||
|
@ -162,6 +184,29 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.notification__reactions {
|
||||
display: flex;
|
||||
margin: var(--spacing-m);
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: 5rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin-left: 3rem;
|
||||
}
|
||||
|
||||
|
||||
> *:not(:last-of-type) {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.button__label {
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.notification__bubble {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
|
|
|
@ -119,6 +119,7 @@
|
|||
}
|
||||
|
||||
.menu__link--notification {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue
File thumbnail flickers a lot
Probably a non-blocker, but a bit annoying. Was present in the past, but now super obvious.
It flickers when:
[Non-blocker] Unknown error
Not sure how to reproduce this. Happens only on a few notifications when replying.
Ah, it happens when the actual comment has been deleted. Not sure how to handle this gracefully. Probably can ignore for now, unless you have some ideas.