Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions src/hooks/accounts/accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "../..";
import commentsStore from "../../stores/comments";
import * as accountsActions from "../../stores/accounts/accounts-actions";
import { COMMENT_MODERATION_AUTHOR_SUMMARY_KEY } from "../../stores/accounts/utils";
import PkcJsMock, {
PKC,
Comment,
Expand Down Expand Up @@ -3398,6 +3399,139 @@ describe("accounts", () => {
expect(Object.keys(rendered.result.current.editedComment.failedEdits).length).toBe(0);
});

test("comment moderation author ban is reflected by useEditedComment", async () => {
const commentCid = rendered.result.current.accountComments[0].cid;
const communityAddress = rendered.result.current.accountComments[0].communityAddress;
const commentModerationTimestamp = Math.ceil(Date.now() / 1000);
const banExpiresAt = commentModerationTimestamp + 60 * 60;

rendered.rerender(commentCid);
await waitFor(
() =>
rendered.result.current.comment?.cid &&
rendered.result.current.comment.index === undefined,
);

await act(async () => {
await accountsActions.publishCommentModeration({
timestamp: commentModerationTimestamp,
commentCid,
communityAddress,
commentModeration: { author: { banExpiresAt } },
onChallenge: (challenge: any, comment: any) => comment.publishChallengeAnswers(),
onChallengeVerification: () => {},
});
});

await waitFor(() => rendered.result.current.editedComment.editedComment);
expect(rendered.result.current.editedComment.state).toBe("pending");
expect(
rendered.result.current.editedComment.pendingEdits[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY],
).toEqual({ banExpiresAt });
expect(
rendered.result.current.editedComment.editedComment.commentModeration?.author?.banExpiresAt,
).toBe(banExpiresAt);
expect(
rendered.result.current.editedComment.editedComment.author?.community?.banExpiresAt,
).toBe(banExpiresAt);

const updatedComment = { ...commentsStore.getState().comments[commentCid] };
updatedComment.author = {
...updatedComment.author,
community: { ...updatedComment.author?.community, banExpiresAt },
};
updatedComment.updatedAt = commentModerationTimestamp + 1;
commentsStore.setState(({ comments }) => ({
comments: { ...comments, [commentCid]: updatedComment },
}));

await waitFor(() => rendered.result.current.editedComment.state === "succeeded");
expect(
rendered.result.current.editedComment.succeededEdits[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY],
).toEqual({ banExpiresAt });
});

test("comment moderation author unban stays pending while refreshed data still has stale ban", async () => {
const commentCid = rendered.result.current.accountComments[0].cid;
const communityAddress = rendered.result.current.accountComments[0].communityAddress;
const commentModerationTimestamp = Math.ceil(Date.now() / 1000);
const banExpiresAt = commentModerationTimestamp + 60 * 60;

rendered.rerender(commentCid);
await waitFor(
() =>
rendered.result.current.comment?.cid &&
rendered.result.current.comment.index === undefined,
);

const staleBannedComment = { ...commentsStore.getState().comments[commentCid] };
staleBannedComment.commentModeration = {
...staleBannedComment.commentModeration,
author: { banExpiresAt },
};
staleBannedComment.author = {
...staleBannedComment.author,
community: { ...staleBannedComment.author?.community, banExpiresAt },
};
staleBannedComment.updatedAt = commentModerationTimestamp + 1;
commentsStore.setState(({ comments }) => ({
comments: { ...comments, [commentCid]: staleBannedComment },
}));

await act(async () => {
await accountsActions.publishCommentModeration({
timestamp: commentModerationTimestamp,
commentCid,
communityAddress,
commentModeration: { author: undefined },
onChallenge: (challenge: any, comment: any) => comment.publishChallengeAnswers(),
onChallengeVerification: () => {},
});
});

await waitFor(() => rendered.result.current.editedComment.editedComment);
expect(rendered.result.current.editedComment.state).toBe("pending");
expect(
Object.prototype.hasOwnProperty.call(
rendered.result.current.editedComment.pendingEdits,
COMMENT_MODERATION_AUTHOR_SUMMARY_KEY,
),
).toBe(true);
expect(
rendered.result.current.editedComment.pendingEdits[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY],
).toBeUndefined();
expect(
rendered.result.current.editedComment.editedComment.commentModeration?.author,
).toBeUndefined();
expect(
rendered.result.current.editedComment.editedComment.author?.community?.banExpiresAt,
).toBeUndefined();

const refreshedUnbannedComment = { ...staleBannedComment };
refreshedUnbannedComment.commentModeration = {
...refreshedUnbannedComment.commentModeration,
author: undefined,
};
refreshedUnbannedComment.updatedAt = commentModerationTimestamp + 2;
commentsStore.setState(({ comments }) => ({
comments: { ...comments, [commentCid]: refreshedUnbannedComment },
}));

await waitFor(() => rendered.result.current.editedComment.state === "succeeded");
expect(
Object.prototype.hasOwnProperty.call(
rendered.result.current.editedComment.succeededEdits,
COMMENT_MODERATION_AUTHOR_SUMMARY_KEY,
),
).toBe(true);
expect(
rendered.result.current.editedComment.succeededEdits[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY],
).toBeUndefined();
expect(
rendered.result.current.editedComment.editedComment.author?.community?.banExpiresAt,
).toBeUndefined();
});

test("edited comment failed", async () => {
const commentCid = rendered.result.current.accountComments[0].cid;
expect(commentCid).not.toBe(undefined);
Expand Down
62 changes: 57 additions & 5 deletions src/hooks/accounts/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ import {
useAccountWithCalculatedProperties,
useCalculatedNotifications,
} from "./utils";
import { getAccountEditPropertySummary } from "../../stores/accounts/utils";
import {
COMMENT_MODERATION_AUTHOR_SUMMARY_KEY,
getAccountEditPropertySummary,
} from "../../stores/accounts/utils";
import {
getCanonicalCommunityAddress,
getEquivalentCommunityAddressGroupKey,
Expand All @@ -49,6 +52,47 @@ import {
import { addCommentModeration } from "../../lib/utils/comment-moderation";
import useInterval from "../utils/use-interval";

const getCommentEditPropertyValue = (comment: any, propertyName: string) => {
if (propertyName !== COMMENT_MODERATION_AUTHOR_SUMMARY_KEY) {
return comment?.[propertyName];
}

const commentModeration = comment?.commentModeration;
if (
commentModeration &&
typeof commentModeration === "object" &&
Object.prototype.hasOwnProperty.call(commentModeration, "author")
) {
return commentModeration.author;
}

return comment?.author?.community?.banExpiresAt !== undefined
? { banExpiresAt: comment.author.community.banExpiresAt }
: undefined;
};

const applyEditedCommentProperty = (comment: any, propertyName: string, value: any) => {
if (propertyName !== COMMENT_MODERATION_AUTHOR_SUMMARY_KEY) {
comment[propertyName] = value;
return;
}

comment.commentModeration = comment.commentModeration ? { ...comment.commentModeration } : {};
if (value === undefined) {
delete comment.commentModeration.author;
} else {
comment.commentModeration.author = value;
}

comment.author = comment.author ? { ...comment.author } : {};
comment.author.community = comment.author.community ? { ...comment.author.community } : {};
if (value?.banExpiresAt === undefined) {
delete comment.author.community.banExpiresAt;
} else {
comment.author.community.banExpiresAt = value.banExpiresAt;
}
};

/**
* @param accountName - The nickname of the account, e.g. 'Account 1'. If no accountName is provided, return
* the active account id.
Expand Down Expand Up @@ -857,7 +901,7 @@ export function useEditedComment(options?: UseEditedCommentOptions): UseEditedCo
// Without a newer update we can only treat recent edits as pending. Older edits that never
// produced any update are effectively stale and should stop shadowing the live comment.
if (!comment?.updatedAt) {
if (isEqual(comment?.[propertyName], propertyNameEdit.value)) {
if (isEqual(getCommentEditPropertyValue(comment, propertyName), propertyNameEdit.value)) {
setPropertyNameEditState("succeeded");
} else if (propertyNameEdit.timestamp > now - expiryTime) {
setPropertyNameEditState("pending");
Expand All @@ -878,7 +922,7 @@ export function useEditedComment(options?: UseEditedCommentOptions): UseEditedCo
// has been received after the edit was published so we can evaluate
else {
// comment has propertyNameEdit, propertyNameEdit succeeded
if (isEqual(comment[propertyName], propertyNameEdit.value)) {
if (isEqual(getCommentEditPropertyValue(comment, propertyName), propertyNameEdit.value)) {
setPropertyNameEditState("succeeded");
continue;
}
Expand Down Expand Up @@ -918,10 +962,18 @@ export function useEditedComment(options?: UseEditedCommentOptions): UseEditedCo
// add pending and succeeded props so the editor can see his changes right away
// don't add failed edits to reflect the current state of the edited comment
for (const propertyName in editedResult.pendingEdits) {
editedResult.editedComment[propertyName] = editedResult.pendingEdits[propertyName];
applyEditedCommentProperty(
editedResult.editedComment,
propertyName,
editedResult.pendingEdits[propertyName],
);
}
for (const propertyName in editedResult.succeededEdits) {
editedResult.editedComment[propertyName] = editedResult.succeededEdits[propertyName];
applyEditedCommentProperty(
editedResult.editedComment,
propertyName,
editedResult.succeededEdits[propertyName],
);
}
editedResult.editedComment = addCommentModeration(editedResult.editedComment);

Expand Down
14 changes: 14 additions & 0 deletions src/stores/accounts/accounts-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,20 @@ describe("accounts-actions", () => {
expect(stale.accountsEditsSummaries.acc1["cid-1"].spoiler.value).toBe(true);
});

test("addStoredAccountEditSummaryToState preserves comment moderation author unban edits", () => {
const result = accountsActions.addStoredAccountEditSummaryToState({} as any, "acc1", {
commentCid: "cid-1",
timestamp: 10,
commentModeration: { author: undefined },
});

expect(result.accountsEditsSummaries.acc1["cid-1"]["commentModeration.author"]).toEqual({
timestamp: 10,
value: undefined,
});
expect(result.accountsEditsSummaries.acc1["cid-1"].author).toBeUndefined();
});

test("addStoredAccountEditSummaryToState is a no-op when edit has no target", () => {
const summaries = { acc1: { existing: { spoiler: { timestamp: 1, value: true } } } };
expect(
Expand Down
17 changes: 13 additions & 4 deletions src/stores/accounts/accounts-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
getAccountCommunities,
getCommentCidsToAccountsComments,
getAccountsCommentsIndexes,
COMMENT_MODERATION_AUTHOR_SUMMARY_KEY,
getAccountEditPropertySummary,
fetchCommentLinkDimensions,
getAccountCommentDepth,
Expand Down Expand Up @@ -247,9 +248,16 @@ const accountEditNonPropertyNames = new Set([
]);

const normalizeStoredAccountEditForSummary = (storedAccountEdit: any) => {
const normalizedEdit = storedAccountEdit.commentModeration
? { ...storedAccountEdit, ...storedAccountEdit.commentModeration, commentModeration: undefined }
: { ...storedAccountEdit };
const normalizedEdit = { ...storedAccountEdit };
const commentModeration = normalizedEdit.commentModeration;
if (commentModeration && typeof commentModeration === "object") {
const { author, ...commentModerationProperties } = commentModeration;
Object.assign(normalizedEdit, commentModerationProperties);
if (Object.prototype.hasOwnProperty.call(commentModeration, "author")) {
normalizedEdit[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY] = author;
}
normalizedEdit.commentModeration = undefined;
}
const communityEdit = normalizedEdit.communityEdit ?? normalizedEdit.communityEdit;
if (communityEdit && typeof communityEdit === "object") {
Object.assign(normalizedEdit, communityEdit);
Expand Down Expand Up @@ -281,7 +289,8 @@ export const addStoredAccountEditSummaryToState = (

for (const propertyName in normalizedEdit) {
if (
normalizedEdit[propertyName] === undefined ||
(normalizedEdit[propertyName] === undefined &&
propertyName !== COMMENT_MODERATION_AUTHOR_SUMMARY_KEY) ||
accountEditNonPropertyNames.has(propertyName)
) {
continue;
Expand Down
22 changes: 22 additions & 0 deletions src/stores/accounts/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import utils from "./utils";
import { COMMENT_MODERATION_AUTHOR_SUMMARY_KEY } from "./utils";
import { Role } from "../../types";
import commentsStore from "../comments";
import repliesPagesStore from "../replies-pages";
Expand Down Expand Up @@ -336,6 +337,27 @@ describe("accountsStore utils", () => {
expect((summary as any).author).toBeUndefined();
});

test("getAccountEditPropertySummary preserves comment moderation author ban edits", () => {
const summary = utils.getAccountEditPropertySummary([
{
commentCid: "cid-1",
timestamp: 10,
commentModeration: { author: { banExpiresAt: 20 } },
},
{
commentCid: "cid-1",
timestamp: 15,
commentModeration: { author: undefined },
},
] as any);

expect(summary[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY]).toEqual({
timestamp: 15,
value: undefined,
});
expect((summary as any).author).toBeUndefined();
});

test("getAccountEditPropertySummary handles undefined input", () => {
expect(utils.getAccountEditPropertySummary(undefined as any)).toEqual({});
});
Expand Down
14 changes: 11 additions & 3 deletions src/stores/accounts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,17 @@ const accountEditNonPropertyNames = new Set([
"timestamp",
]);

export const COMMENT_MODERATION_AUTHOR_SUMMARY_KEY = "commentModeration.author";

const normalizeAccountEditForSummary = (accountEdit: AccountEdit) => {
const normalizedAccountEdit = { ...accountEdit };
if (normalizedAccountEdit.commentModeration) {
Object.assign(normalizedAccountEdit, normalizedAccountEdit.commentModeration);
const commentModeration = normalizedAccountEdit.commentModeration;
if (commentModeration && typeof commentModeration === "object") {
const { author, ...commentModerationProperties } = commentModeration;
Object.assign(normalizedAccountEdit, commentModerationProperties);
if (Object.prototype.hasOwnProperty.call(commentModeration, "author")) {
normalizedAccountEdit[COMMENT_MODERATION_AUTHOR_SUMMARY_KEY] = author;
}
delete normalizedAccountEdit.commentModeration;
}
const communityEdit = normalizedAccountEdit.communityEdit ?? normalizedAccountEdit.communityEdit;
Expand All @@ -262,7 +269,8 @@ export const getAccountEditPropertySummary = (accountEdits: AccountEdit[] | unde
const normalizedAccountEdit = normalizeAccountEditForSummary(accountEdit);
for (const propertyName in normalizedAccountEdit) {
if (
normalizedAccountEdit[propertyName] === undefined ||
(normalizedAccountEdit[propertyName] === undefined &&
propertyName !== COMMENT_MODERATION_AUTHOR_SUMMARY_KEY) ||
accountEditNonPropertyNames.has(propertyName)
) {
continue;
Expand Down
Loading