diff --git a/src/features/comment/Comment.tsx b/src/features/comment/Comment.tsx index 92be68dfb8..b0d19eec50 100644 --- a/src/features/comment/Comment.tsx +++ b/src/features/comment/Comment.tsx @@ -147,6 +147,7 @@ export default function Comment({ item={comment} showTouchFriendlyLinks={!context} mdClassName="collapse-md-margins" + canModerate={canModerate} /> )} {context} diff --git a/src/features/comment/CommentContent.tsx b/src/features/comment/CommentContent.tsx index d7dd45a950..de9758b8e7 100644 --- a/src/features/comment/CommentContent.tsx +++ b/src/features/comment/CommentContent.tsx @@ -1,34 +1,65 @@ +import { IonText } from "@ionic/react"; import { Comment, Post } from "lemmy-js-client"; +import { useEffect } from "react"; -import { useAppSelector } from "#/store"; +import { ModeratorRole } from "#/features/moderation/useCanModerate"; +import { useAppDispatch, useAppSelector } from "#/store"; import CommentLinks from "./CommentLinks"; import CommentMarkdown from "./CommentMarkdown"; +import { getCommentContent, LOADING_CONTENT } from "./commentSlice"; interface CommentContentProps { item: Comment | Post; showTouchFriendlyLinks?: boolean; mdClassName?: string; + canModerate?: ModeratorRole | undefined; } export default function CommentContent({ item, showTouchFriendlyLinks = true, mdClassName, + canModerate, }: CommentContentProps) { + const dispatch = useAppDispatch(); const touchFriendlyLinks = useAppSelector( (state) => state.settings.general.comments.touchFriendlyLinks, ); + const removedCommentContent = useAppSelector( + (state) => state.comment.commentContentById[item.id], + ); + + const content = (() => { + // is post + if (!("content" in item)) return item.body ?? item.name; + + if (item.content === "") { + return removedCommentContent ?? ""; + } + + return item.content; + })(); + + useEffect(() => { + if (!item.removed) return; + if (!("content" in item)) return; // only comments + if (content) return; + if (!canModerate) return; + + dispatch(getCommentContent(item.id)); + }, [item, content, dispatch, canModerate]); + + if (content === LOADING_CONTENT) + return Loading comment...; return ( <> - {"content" in item ? item.content : (item.body ?? item.name)} + {content} {showTouchFriendlyLinks && touchFriendlyLinks && ( - + )} ); diff --git a/src/features/comment/CommentHeader.tsx b/src/features/comment/CommentHeader.tsx index 8abb3dd773..bafbba0d69 100644 --- a/src/features/comment/CommentHeader.tsx +++ b/src/features/comment/CommentHeader.tsx @@ -50,7 +50,7 @@ export default function CommentHeader({ ); function renderActions() { - if (inModqueue) return ; + if (inModqueue) return ; if (canModerate) return ; diff --git a/src/features/comment/commentSlice.ts b/src/features/comment/commentSlice.ts index fc03b4463c..b5a4a883b6 100644 --- a/src/features/comment/commentSlice.ts +++ b/src/features/comment/commentSlice.ts @@ -1,4 +1,4 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Comment, CommentView } from "lemmy-js-client"; import { clientSelector } from "#/features/auth/authSelectors"; @@ -10,11 +10,14 @@ import { import { getRemoteHandle } from "#/helpers/lemmy"; import { AppDispatch, RootState } from "#/store"; +export const LOADING_CONTENT = -1; + interface CommentState { commentCollapsedById: Record; commentVotesById: Record; commentSavedById: Record; commentById: Record; + commentContentById: Record; } const initialState: CommentState = { @@ -25,6 +28,9 @@ const initialState: CommentState = { * surgical changes received after user edits or deletes comment */ commentById: {}, + + // https://github.com/LemmyNet/lemmy/issues/5230 + commentContentById: {}, }; export const commentSlice = createSlice({ @@ -76,8 +82,23 @@ export const commentSlice = createSlice({ ) => { state.commentSavedById[action.payload.commentId] = action.payload.saved; }, + setCommentContent: (state, action: PayloadAction) => { + state.commentContentById[action.payload.id] = action.payload.content; + }, resetComments: () => initialState, }, + extraReducers: (builder) => { + builder + .addCase(getCommentContent.fulfilled, (state, action) => { + state.commentContentById[action.meta.arg] = action.payload ?? ""; + }) + .addCase(getCommentContent.pending, (state, action) => { + state.commentContentById[action.meta.arg] = LOADING_CONTENT; + }) + .addCase(getCommentContent.rejected, (state, action) => { + state.commentContentById[action.meta.arg] = ""; + }); + }, }); // Action creators are generated for each case reducer function @@ -87,6 +108,7 @@ export const { updateCommentVote, updateCommentSaved, resetComments, + setCommentContent, } = commentSlice.actions; export default commentSlice.reducer; @@ -172,16 +194,17 @@ export const editComment = }; export const modRemoveComment = - (commentId: number, removed: boolean, reason?: string) => + (comment: Comment, removed: boolean, reason?: string) => async (dispatch: AppDispatch, getState: () => RootState) => { const response = await clientSelector(getState())?.removeComment({ - comment_id: commentId, + comment_id: comment.id, removed, reason, }); + dispatch(setCommentContent(comment)); dispatch(mutatedComment(response.comment_view)); - await dispatch(resolveCommentReport(commentId)); + await dispatch(resolveCommentReport(comment.id)); }; export const modNukeCommentChain = @@ -232,3 +255,25 @@ export const receivedComments = fetchTagsForHandles(comments.map((c) => getRemoteHandle(c.creator))), ); }; + +export const getCommentContent = createAsyncThunk( + "comment/getCommentContent", + async (commentId: number, thunkAPI) => { + const rootState = thunkAPI.getState() as RootState; + const client = clientSelector(rootState); + + const log = await client.getModlog({ comment_id: commentId }); + + return log.removed_comments[0]?.comment.content; + }, + { + condition: (commentId, { getState }) => { + const state = getState() as RootState; + + if (state.comment.commentContentById[commentId] === undefined) + return true; + + return false; + }, + }, +); diff --git a/src/features/moderation/ModqueueItemActions.tsx b/src/features/moderation/ModqueueItemActions.tsx index 9b329f2a10..fe4f489577 100644 --- a/src/features/moderation/ModqueueItemActions.tsx +++ b/src/features/moderation/ModqueueItemActions.tsx @@ -21,44 +21,43 @@ import { resolveCommentReport, resolvePostReport } from "./modSlice"; import useCanModerate, { getModColor } from "./useCanModerate"; interface ModqueueItemActionsProps { - item: PostView | CommentView; + itemView: PostView | CommentView; } export default function ModqueueItemActions({ - item, + itemView, }: ModqueueItemActionsProps) { const dispatch = useAppDispatch(); const presentToast = useAppToast(); - const canModerate = useCanModerate(item.community); + const canModerate = useCanModerate(itemView.community); async function modRemoveItem(remove: boolean) { - const id = isPost(item) ? item.post.id : item.comment.id; - const isAlreadyRemoved = isPost(item) - ? item.post.removed - : item.comment.removed; + const item = isPost(itemView) ? itemView.post : itemView.comment; + const isAlreadyRemoved = item.removed; // If removal status already in the state you want, just resolve reports if (remove === isAlreadyRemoved) { - const action = isPost(item) ? resolvePostReport : resolveCommentReport; - await dispatch(action(id)); + const action = isPost(itemView) + ? resolvePostReport + : resolveCommentReport; + await dispatch(action(item.id)); if (remove) - presentToast(isPost(item) ? postRemovedMod : commentRemovedMod); - else presentToast(isPost(item) ? postApproved : commentApproved); + presentToast(isPost(itemView) ? postRemovedMod : commentRemovedMod); + else presentToast(isPost(itemView) ? postApproved : commentApproved); return; } - const action = isPost(item) ? modRemovePost : modRemoveComment; - - await dispatch(action(id, remove)); + if (isPost(itemView)) await dispatch(modRemovePost(itemView.post, remove)); + else await dispatch(modRemoveComment(itemView.comment, remove)); const toastMessage = (() => { if (remove) { - if (isPost(item)) return postRemovedMod; + if (isPost(itemView)) return postRemovedMod; else return commentRemovedMod; } else { - if (isPost(item)) return postRestored; + if (isPost(itemView)) return postRestored; else return commentRestored; } })(); diff --git a/src/features/moderation/banner/RemovedBanner.tsx b/src/features/moderation/banner/RemovedBanner.tsx index f1f2c79ea0..fe21500892 100644 --- a/src/features/moderation/banner/RemovedBanner.tsx +++ b/src/features/moderation/banner/RemovedBanner.tsx @@ -27,10 +27,10 @@ export default function RemovedBanner({ itemView }: RemovedBannerProps) { handler: () => { (async () => { if (isPost(itemView)) { - dispatch(modRemovePost(itemView.post.id, false)); + dispatch(modRemovePost(itemView.post, false)); presentToast(postApproved); } else { - await dispatch(modRemoveComment(itemView.comment.id, false)); + await dispatch(modRemoveComment(itemView.comment, false)); presentToast(commentApproved); } })(); diff --git a/src/features/moderation/useCommentModActions.tsx b/src/features/moderation/useCommentModActions.tsx index b848bcb96b..58decec746 100644 --- a/src/features/moderation/useCommentModActions.tsx +++ b/src/features/moderation/useCommentModActions.tsx @@ -101,7 +101,7 @@ export default function useCommentModActions(commentView: CommentView) { icon: trashOutline, handler: () => { (async () => { - await dispatch(modRemoveComment(comment.id, true)); + await dispatch(modRemoveComment(comment, true)); presentToast(commentRemovedMod); })(); @@ -112,7 +112,7 @@ export default function useCommentModActions(commentView: CommentView) { icon: checkmarkCircleOutline, handler: () => { (async () => { - await dispatch(modRemoveComment(comment.id, false)); + await dispatch(modRemoveComment(comment, false)); presentToast(commentApproved); })(); @@ -130,9 +130,7 @@ export default function useCommentModActions(commentView: CommentView) { cssClass: "mod", handler: ({ reason }) => { (async () => { - await dispatch( - modRemoveComment(comment.id, true, reason), - ); + await dispatch(modRemoveComment(comment, true, reason)); presentToast(commentRemovedMod); })(); diff --git a/src/features/moderation/useModZoneActions.tsx b/src/features/moderation/useModZoneActions.tsx index 91396d6754..c401ffedc9 100644 --- a/src/features/moderation/useModZoneActions.tsx +++ b/src/features/moderation/useModZoneActions.tsx @@ -83,7 +83,7 @@ export default function useModZoneActions(props: UseModZoneActionsProps) { // icon: shieldCheckmarkOutline, // handler: () => { // (async () => { - // // await dispatch(modRemoveComment(comment.id, false)); + // // await dispatch(modRemoveComment(comment, false)); // // presentToast(commentApproved); // })(); // }, diff --git a/src/features/moderation/usePostModActions.tsx b/src/features/moderation/usePostModActions.tsx index 4cca8accca..062c34ceee 100644 --- a/src/features/moderation/usePostModActions.tsx +++ b/src/features/moderation/usePostModActions.tsx @@ -78,7 +78,7 @@ export default function usePostModActions(post: PostView) { icon: trashOutline, handler: () => { (async () => { - await dispatch(modRemovePost(post.post.id, true)); + await dispatch(modRemovePost(post.post, true)); presentToast(postRemovedMod); })(); @@ -89,7 +89,7 @@ export default function usePostModActions(post: PostView) { icon: checkmarkCircleOutline, handler: () => { (async () => { - await dispatch(modRemovePost(post.post.id, false)); + await dispatch(modRemovePost(post.post, false)); presentToast(postRestored); })(); @@ -107,7 +107,7 @@ export default function usePostModActions(post: PostView) { cssClass: "mod", handler: ({ reason }) => { (async () => { - await dispatch(modRemovePost(post.post.id, true, reason)); + await dispatch(modRemovePost(post.post, true, reason)); presentToast(postRemovedMod); })(); diff --git a/src/features/post/inFeed/compact/CompactPost.tsx b/src/features/post/inFeed/compact/CompactPost.tsx index bd903cd5e2..90a9ca0c33 100644 --- a/src/features/post/inFeed/compact/CompactPost.tsx +++ b/src/features/post/inFeed/compact/CompactPost.tsx @@ -123,7 +123,7 @@ export default function CompactPost({ post }: PostProps) { {inModqueue ? ( - + ) : ( {(showVotingButtons || inModqueue) && (
- {inModqueue && } + {inModqueue && } {!inModqueue && ( <> diff --git a/src/features/post/postSlice.ts b/src/features/post/postSlice.ts index fae7f5ab77..0245a29805 100644 --- a/src/features/post/postSlice.ts +++ b/src/features/post/postSlice.ts @@ -1,5 +1,5 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { PostView } from "lemmy-js-client"; +import { Post, PostView } from "lemmy-js-client"; import { clientSelector, @@ -410,16 +410,16 @@ export const postHiddenByIdSelector = (state: RootState) => { }; export const modRemovePost = - (postId: number, removed: boolean, reason?: string) => + (post: Post, removed: boolean, reason?: string) => async (dispatch: AppDispatch, getState: () => RootState) => { const response = await clientSelector(getState())?.removePost({ - post_id: postId, + post_id: post.id, removed, reason, }); dispatch(receivedPosts([response.post_view])); - await dispatch(resolvePostReport(postId)); + await dispatch(resolvePostReport(post.id)); }; export const modLockPost = diff --git a/src/features/shared/markdown/editing/modal/contents/ItemReplyingTo.tsx b/src/features/shared/markdown/editing/modal/contents/ItemReplyingTo.tsx index 972d9aac31..e1d2430fe5 100644 --- a/src/features/shared/markdown/editing/modal/contents/ItemReplyingTo.tsx +++ b/src/features/shared/markdown/editing/modal/contents/ItemReplyingTo.tsx @@ -6,6 +6,7 @@ import CommentContent from "#/features/comment/CommentContent"; import Ago from "#/features/labels/Ago"; import Edited from "#/features/labels/Edited"; import Vote from "#/features/labels/Vote"; +import useCanModerate from "#/features/moderation/useCanModerate"; import { preventModalSwipeOnTextSelection } from "#/helpers/ionic"; import { getHandle } from "#/helpers/lemmy"; @@ -16,6 +17,7 @@ interface ItemReplyingToProps { } export default function ItemReplyingTo({ item }: ItemReplyingToProps) { + const canModerate = useCanModerate(item.community); const payload = "comment" in item ? item.comment : item.post; return ( @@ -30,7 +32,11 @@ export default function ItemReplyingTo({ item }: ItemReplyingToProps) { {...preventModalSwipeOnTextSelection} className={styles.commentContentWrapper} > - +
);