mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Changed code based on feedback
ref https://github.com/TryGhost/Ghost/pull/21861#discussion_r1881701985 - simplified disabled logic to be within the component instead of globally. - added state change in a single dispatch query that can handle like / unlike as well as fallback handling. - updated test
This commit is contained in:
parent
c0071db5b2
commit
76d08f1696
3 changed files with 66 additions and 128 deletions
|
@ -189,39 +189,17 @@ async function showComment({state, api, data: comment}: {state: EditableAppConte
|
|||
};
|
||||
}
|
||||
|
||||
async function sendLikeToApi({api, data: comment}: {api: GhostApi, data: {id: string, commentsState: Comment[]}}) {
|
||||
const commentsState = comment.commentsState;
|
||||
|
||||
try {
|
||||
await api.comments.like({comment: {id: comment.id}});
|
||||
return {
|
||||
commentLikeLoadingId: null
|
||||
};
|
||||
} catch (err) {
|
||||
// if error we revert the state
|
||||
return {
|
||||
comments: commentsState,
|
||||
commentLikeLoadingId: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function likeComment({state, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) {
|
||||
const exisitngState = state.comments;
|
||||
|
||||
dispatchAction('sendLikeToApi', {id: comment.id, commentsState: exisitngState});
|
||||
|
||||
// optimistic update
|
||||
async function updateCommentLikeState({state, data: comment}: {state: EditableAppContext, data: {id: string, liked: boolean, commentsState: Comment[]}}) {
|
||||
return {
|
||||
comments: state.comments.map((c) => {
|
||||
const replies = c.replies.map((r) => {
|
||||
if (r.id === comment.id) {
|
||||
return {
|
||||
...r,
|
||||
liked: true,
|
||||
liked: comment.liked,
|
||||
count: {
|
||||
...r.count,
|
||||
likes: r.count.likes + 1
|
||||
likes: comment.liked ? r.count.likes + 1 : r.count.likes - 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -232,11 +210,11 @@ async function likeComment({state, data: comment, dispatchAction}: {state: Edita
|
|||
if (c.id === comment.id) {
|
||||
return {
|
||||
...c,
|
||||
liked: true,
|
||||
liked: comment.liked,
|
||||
replies,
|
||||
count: {
|
||||
...c.count,
|
||||
likes: c.count.likes + 1
|
||||
likes: comment.liked ? c.count.likes + 1 : c.count.likes - 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -245,69 +223,29 @@ async function likeComment({state, data: comment, dispatchAction}: {state: Edita
|
|||
...c,
|
||||
replies
|
||||
};
|
||||
}),
|
||||
commentLikeLoadingId: comment.id
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
async function sendUnlikeToApi({api, data: comment}: {api: GhostApi, data: {id: string, commentsState: Comment[]}}) {
|
||||
const commentsState = comment.commentsState;
|
||||
|
||||
async function likeComment({state, api, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) {
|
||||
dispatchAction('updateCommentLikeState', {id: comment.id, liked: true, commentsState: state.comments});
|
||||
try {
|
||||
await api.comments.unlike({comment: {id: comment.id}});
|
||||
return {
|
||||
commentUnlikeLoading: null
|
||||
};
|
||||
await api.comments.like({comment});
|
||||
return {};
|
||||
} catch (err) {
|
||||
// If error, revert the state
|
||||
return {
|
||||
comments: commentsState,
|
||||
commentUnlikeLoading: null
|
||||
};
|
||||
dispatchAction('updateCommentLikeState', {id: comment.id, liked: false, commentsState: state.comments});
|
||||
}
|
||||
}
|
||||
|
||||
async function unlikeComment({state, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) {
|
||||
const existingState = state.comments;
|
||||
async function unlikeComment({state, api, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) {
|
||||
dispatchAction('updateCommentLikeState', {id: comment.id, liked: false, commentsState: state.comments});
|
||||
|
||||
dispatchAction('sendUnlikeToApi', {id: comment.id, commentsState: existingState});
|
||||
// optimistic update
|
||||
return {
|
||||
comments: state.comments.map((c) => {
|
||||
const replies = c.replies.map((r) => {
|
||||
if (r.id === comment.id) {
|
||||
return {
|
||||
...r,
|
||||
liked: false,
|
||||
count: {
|
||||
...r.count,
|
||||
likes: r.count.likes - 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return r;
|
||||
});
|
||||
|
||||
if (c.id === comment.id) {
|
||||
return {
|
||||
...c,
|
||||
liked: false,
|
||||
replies,
|
||||
count: {
|
||||
...c.count,
|
||||
likes: c.count.likes - 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...c,
|
||||
replies
|
||||
};
|
||||
}),
|
||||
commentUnlikeLoading: comment.id
|
||||
};
|
||||
try {
|
||||
await api.comments.unlike({comment});
|
||||
return {};
|
||||
} catch (err) {
|
||||
dispatchAction('updateCommentLikeState', {id: comment.id, liked: true, commentsState: state.comments});
|
||||
}
|
||||
}
|
||||
|
||||
async function reportComment({api, data: comment}: {api: GhostApi, data: {id: string}}) {
|
||||
|
@ -317,7 +255,6 @@ async function reportComment({api, data: comment}: {api: GhostApi, data: {id: st
|
|||
}
|
||||
|
||||
async function deleteComment({state, api, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) {
|
||||
|
||||
await api.comments.edit({
|
||||
comment: {
|
||||
id: comment.id,
|
||||
|
@ -542,8 +479,7 @@ export const Actions = {
|
|||
highlightComment,
|
||||
setHighlightComment,
|
||||
setCommentsIsLoading,
|
||||
sendLikeToApi,
|
||||
sendUnlikeToApi
|
||||
updateCommentLikeState
|
||||
};
|
||||
|
||||
export type ActionType = keyof typeof Actions;
|
||||
|
|
|
@ -6,14 +6,15 @@ type Props = {
|
|||
comment: Comment;
|
||||
};
|
||||
const LikeButton: React.FC<Props> = ({comment}) => {
|
||||
const {dispatchAction, member, commentsEnabled, commentLikeLoadingId} = useAppContext();
|
||||
const {dispatchAction, member, commentsEnabled} = useAppContext();
|
||||
const [animationClass, setAnimation] = useState('');
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const paidOnly = commentsEnabled === 'paid';
|
||||
const isPaidMember = member && !!member.paid;
|
||||
const canLike = member && (isPaidMember || !paidOnly);
|
||||
|
||||
const toggleLike = () => {
|
||||
const toggleLike = async () => {
|
||||
if (!canLike) {
|
||||
dispatchAction('openPopup', {
|
||||
type: 'ctaPopup'
|
||||
|
@ -22,52 +23,27 @@ const LikeButton: React.FC<Props> = ({comment}) => {
|
|||
}
|
||||
|
||||
if (!comment.liked) {
|
||||
dispatchAction('likeComment', comment);
|
||||
setDisabled(true);
|
||||
await dispatchAction('likeComment', comment);
|
||||
setAnimation('animate-heartbeat');
|
||||
setTimeout(() => {
|
||||
setAnimation('');
|
||||
}, 400);
|
||||
setDisabled(false);
|
||||
} else {
|
||||
dispatchAction('unlikeComment', comment);
|
||||
setDisabled(true);
|
||||
await dispatchAction('unlikeComment', comment);
|
||||
setDisabled(false);
|
||||
}
|
||||
};
|
||||
|
||||
// If can like: use <button> element, otherwise use a <span>
|
||||
const CustomTag = canLike ? `button` : `span`;
|
||||
|
||||
let likeCursor = 'cursor-pointer';
|
||||
if (!canLike) {
|
||||
likeCursor = 'cursor-text';
|
||||
}
|
||||
|
||||
if (labs && labs.commentImprovements) {
|
||||
const likeIsinLoadingState = commentLikeLoadingId === comment.id;
|
||||
return (
|
||||
<button
|
||||
className={`duration-50 group flex cursor-pointer items-center font-sans text-base outline-0 transition-all ease-linear sm:text-sm ${
|
||||
comment.liked ? 'text-black/90 dark:text-white/90' : 'text-black/50 hover:text-black/75 dark:text-white/60 dark:hover:text-white/75'
|
||||
}`}
|
||||
data-testid="like-button"
|
||||
disabled={likeIsinLoadingState}
|
||||
type="button"
|
||||
onClick={toggleLike}
|
||||
>
|
||||
<LikeIcon
|
||||
className={animationClass + ` mr-[6px] ${
|
||||
comment.liked ? 'fill-black dark:fill-white stroke-black dark:stroke-white' : 'stroke-black/50 group-hover:stroke-black/75 dark:stroke-white/60 dark:group-hover:stroke-white/75'
|
||||
} ${!comment.liked && canLike && 'group-hover:stroke-black/75 dark:group-hover:stroke-white/75'} transition duration-50 ease-linear`}
|
||||
/>
|
||||
{comment.count.likes}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`duration-50 group flex cursor-pointer items-center font-sans text-base outline-0 transition-all ease-linear sm:text-sm ${
|
||||
comment.liked ? 'text-black/90 dark:text-white/90' : 'text-black/50 hover:text-black/75 dark:text-white/60 dark:hover:text-white/75'
|
||||
}`}
|
||||
data-testid="like-button"
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
onClick={toggleLike}
|
||||
>
|
||||
|
|
|
@ -102,12 +102,9 @@ test.describe('Actions', async () => {
|
|||
html: '<p>This is comment 1</p>'
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 2</p>',
|
||||
liked: true,
|
||||
count: {
|
||||
likes: 52
|
||||
}
|
||||
html: '<p>This is comment 2</p>'
|
||||
});
|
||||
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 3</p>'
|
||||
});
|
||||
|
@ -136,11 +133,7 @@ test.describe('Actions', async () => {
|
|||
html: '<p>This is comment 1</p>'
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 2</p>',
|
||||
liked: true,
|
||||
count: {
|
||||
likes: 52
|
||||
}
|
||||
html: '<p>This is comment 2</p>'
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 3</p>'
|
||||
|
@ -246,6 +239,39 @@ test.describe('Actions', async () => {
|
|||
await expect(likeButton).toHaveText('52');
|
||||
});
|
||||
|
||||
test('Can revert state of a reply if its unsuccessful', async ({page}) => {
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>',
|
||||
replies: [
|
||||
mockedApi.buildReply({
|
||||
html: '<p>This is a reply to 1</p>',
|
||||
liked: true,
|
||||
count: {
|
||||
likes: 3
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const {frame} = await initializeTest(page);
|
||||
|
||||
// Check like button is not filled yet
|
||||
const comment = frame.getByTestId('comment-component').nth(0);
|
||||
const reply = comment.getByTestId('comment-component').nth(0);
|
||||
// check if reply contains text This is a reply to 1
|
||||
await expect(reply).toContainText('This is a reply to 1');
|
||||
|
||||
const likeButton = reply.getByTestId('like-button');
|
||||
|
||||
mockedApi.setFailure('likeComment', {
|
||||
status: 500,
|
||||
body: {error: 'Internal Server Error'}
|
||||
});
|
||||
await likeButton.click();
|
||||
mockedApi.setDelay(100); // give time for disabled state
|
||||
await expect(likeButton).toHaveText('3');
|
||||
});
|
||||
|
||||
test('Can reply to a comment', async ({page}) => {
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>'
|
||||
|
|
Loading…
Add table
Reference in a new issue