diff --git a/CHANGELOG.md b/CHANGELOG.md
index d735f3f69..ebb8217e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.132.0] - Not released
+### Added
+- Ability to "unlock" and preview comments from the banned users and with reply
+ to the banned users.
### Fixed
- Fixed a bug where the first partial opening of comments ("show last N") would
open one extra comment.
diff --git a/src/components/alternative-text.jsx b/src/components/alternative-text.jsx
new file mode 100644
index 000000000..1606a1305
--- /dev/null
+++ b/src/components/alternative-text.jsx
@@ -0,0 +1,29 @@
+import cn from 'classnames';
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { Icon } from './fontawesome-icons';
+import { ButtonLink } from './button-link';
+import style from './alternative-text.module.scss';
+
+export function AlternativeText({
+ status,
+ icon,
+ isError = false,
+ inComment = false,
+ close,
+ children,
+}) {
+ return (
+
+
+
+
+
+ {status}
+
+
+
+
+
{children}
+
+ );
+}
diff --git a/src/components/translated-text.module.scss b/src/components/alternative-text.module.scss
similarity index 100%
rename from src/components/translated-text.module.scss
rename to src/components/alternative-text.module.scss
diff --git a/src/components/comment-likers.js b/src/components/comment-likers.js
index a85be299f..51a5d7a32 100644
--- a/src/components/comment-likers.js
+++ b/src/components/comment-likers.js
@@ -8,10 +8,11 @@ import { initialAsyncState } from '../redux/async-helpers';
* Load and return all likers of the given comment
*
* @param {string} id
+ * @param {boolean} suppress do not load likes (when we know they aren't available)
*/
-export function useCommentLikers(id) {
+export function useCommentLikers(id, suppress = false) {
const dispatch = useDispatch();
- useEffect(() => void dispatch(getCommentLikes(id)), [dispatch, id]);
+ useEffect(() => void (!suppress && dispatch(getCommentLikes(id))), [suppress, dispatch, id]);
return useSelector((state) => {
const { status = initialAsyncState, likes = [] } = state.commentLikes[id] || {};
return {
diff --git a/src/components/post/post-comment-more-menu.jsx b/src/components/post/post-comment-more-menu.jsx
index 94103bc29..2e9b80027 100644
--- a/src/components/post/post-comment-more-menu.jsx
+++ b/src/components/post/post-comment-more-menu.jsx
@@ -9,6 +9,7 @@ import {
faEdit,
faHeartBroken,
faLink,
+ faLockOpen,
faUserFriends,
} from '@fortawesome/free-solid-svg-icons';
import { faHeart, faClock, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
@@ -35,6 +36,7 @@ export const PostCommentMoreMenu = forwardRef(function PostCommentMore(
doLike,
doUnlike,
doShowLikes,
+ doUnlock,
getBackwardIdx,
createdAt,
updatedAt,
@@ -46,7 +48,7 @@ export const PostCommentMoreMenu = forwardRef(function PostCommentMore(
},
menuRef,
) {
- const { status, likers } = useCommentLikers(id);
+ const { status, likers } = useCommentLikers(id, isHidden);
const myUsername = useSelector((state) => state.user.username);
const bIdx = getBackwardIdx();
const arrows = bIdx <= 4 ? '^'.repeat(bIdx) : `^${bIdx}`;
@@ -113,6 +115,13 @@ export const PostCommentMoreMenu = forwardRef(function PostCommentMore(
!isHidden && (
),
+ doUnlock && (
+
+
+ Show comment
+
+
+ ),
],
[
doDelete && authorUsername !== myUsername && (
diff --git a/src/components/post/post-comment.jsx b/src/components/post/post-comment.jsx
index 344d3b8e9..4cc1d5955 100644
--- a/src/components/post/post-comment.jsx
+++ b/src/components/post/post-comment.jsx
@@ -5,7 +5,10 @@ import classnames from 'classnames';
import { connect } from 'react-redux';
import { preventDefault, confirmFirst } from '../../utils';
-import { READMORE_STYLE_COMPACT } from '../../utils/frontend-preferences-options';
+import {
+ HIDDEN_AUTHOR_BANNED,
+ READMORE_STYLE_COMPACT,
+} from '../../utils/frontend-preferences-options';
import { commentReadmoreConfig } from '../../utils/readmore-config';
import { defaultCommentState } from '../../redux/reducers/comment-edit';
@@ -22,6 +25,7 @@ import { Separated } from '../separated';
import { TranslatedText } from '../translated-text';
import { initialAsyncState } from '../../redux/async-helpers';
import { existingCommentURI, newCommentURI } from '../../services/drafts';
+import { UnlockedHiddenComment } from '../unlocked-hidden-comment';
import { PostCommentMore } from './post-comment-more';
import { PostCommentPreview } from './post-comment-preview';
import { CommentProvider } from './post-comment-provider';
@@ -36,6 +40,7 @@ class PostComment extends Component {
previewSeqNumber: 0,
previewLeft: 0,
previewTop: 0,
+ unlockHidden: false,
};
scrollToComment = () => {
@@ -158,9 +163,13 @@ class PostComment extends Component {
canLike: !ownComment && !this.isHidden(),
canReply: !this.isHidden() && this.props.canAddComment,
canDelete: this.props.isEditable || this.props.isDeletable,
+ canUnlock: this.props.hideType === HIDDEN_AUTHOR_BANNED || this.props.isReplyToBanned,
};
}
+ unlockHidden = () => this.setState({ unlockHidden: true });
+ lockHidden = () => this.setState({ unlockHidden: false });
+
// We use this strange data structure because there can be more than one
// PostCommentMore element created in the comment (see Expandable/bonusInfo).
_moreMenuOpeners = [];
@@ -171,7 +180,7 @@ class PostComment extends Component {
onMoreMenuOpened = (moreMenuOpened) => this.setState({ moreMenuOpened });
commentTail() {
- const { canLike, canReply, canDelete } = this.possibleActions();
+ const { canLike, canReply, canDelete, canUnlock } = this.possibleActions();
return (
+ {this.state.unlockHidden && (
+
+ )}
{this.hiddenBody()}
{commentTail}
diff --git a/src/components/translated-text.jsx b/src/components/translated-text.jsx
index 1a2738a6d..b4da4f0a1 100644
--- a/src/components/translated-text.jsx
+++ b/src/components/translated-text.jsx
@@ -1,16 +1,12 @@
import ISO6391 from 'iso-639-1';
import { useDispatch, useSelector } from 'react-redux';
-import cn from 'classnames';
import { useCallback } from 'react';
-import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { initialAsyncState } from '../redux/async-helpers';
import { resetTranslation } from '../redux/action-creators';
import { useServerValue } from './hooks/server-info';
import PieceOfText from './piece-of-text';
-import { Icon } from './fontawesome-icons';
import { faTranslate } from './fontawesome-custom-icons';
-import { ButtonLink } from './button-link';
-import style from './translated-text.module.scss';
+import { AlternativeText } from './alternative-text';
const selectTranslationService = (serverInfo) => serverInfo.textTranslation.serviceTitle;
@@ -27,29 +23,31 @@ export function TranslatedText({ type, id, userHover, arrowHover, arrowClick })
}
if (status.loading) {
return (
-
);
}
if (status.error) {
return (
-
);
}
return (
-
-
- );
-}
-
-function Layout({ status, isError = false, inComment = false, reset, children }) {
- return (
-
-
-
-
-
- {status}
-
-
-
-
-
{children}
-
+
);
}
diff --git a/src/components/unlocked-hidden-comment.jsx b/src/components/unlocked-hidden-comment.jsx
new file mode 100644
index 000000000..4a581d7b7
--- /dev/null
+++ b/src/components/unlocked-hidden-comment.jsx
@@ -0,0 +1,87 @@
+import { useDispatch, useSelector } from 'react-redux';
+import { faLockOpen } from '@fortawesome/free-solid-svg-icons';
+import { useEffect } from 'react';
+import { COMMENT_VISIBLE, HIDDEN_AUTHOR_BANNED } from '../utils/frontend-preferences-options';
+import { initialAsyncState } from '../redux/async-helpers';
+import { unlockComment } from '../redux/action-creators';
+import PieceOfText from './piece-of-text';
+import { AlternativeText } from './alternative-text';
+import UserName from './user-name';
+
+export function UnlockedHiddenComment({ id, userHover, arrowHover, arrowClick, close }) {
+ const dispatch = useDispatch();
+ const comment = useSelector((state) => state.comments[id]);
+
+ const status = useSelector((store) => store.unlockedCommentStates[id] ?? initialAsyncState);
+ const unlockedComment = useSelector((store) => store.unlockedComments[id]);
+
+ useEffect(() => {
+ if (comment.hideType === HIDDEN_AUTHOR_BANNED && status.initial) {
+ dispatch(unlockComment(id));
+ }
+ }, [comment.hideType, dispatch, id, status.initial]);
+
+ let title;
+ if (comment.hideType === COMMENT_VISIBLE) {
+ title = `Comment with reply to blocked user:`;
+ } else if (comment.hideType === HIDDEN_AUTHOR_BANNED) {
+ if (status.loading) {
+ title = `Loading comment from a blocked user...`;
+ } else if (status.error) {
+ title = `Error loading comment: ${status.errorText}`;
+ } else {
+ title = `Comment from a blocked user:`;
+ }
+ }
+
+ let content = null;
+ if (comment.hideType === COMMENT_VISIBLE) {
+ content = (
+
+ );
+ } else if (comment.hideType === HIDDEN_AUTHOR_BANNED) {
+ content = (
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+}
+
+function CommentContent({ comment, userHover, arrowHover, arrowClick }) {
+ const allUsers = useSelector((state) => state.users);
+ const author = allUsers[comment?.createdBy];
+ return (
+ comment && (
+ <>
+
+ {author && (
+ <>
+ {' '}
+ -
+ >
+ )}
+ >
+ )
+ );
+}
diff --git a/src/redux/action-creators.js b/src/redux/action-creators.js
index 78dfa5200..165bf557e 100644
--- a/src/redux/action-creators.js
+++ b/src/redux/action-creators.js
@@ -1387,3 +1387,11 @@ export function notifyOfAllComments(postId, enabled) {
payload: { postId, enabled },
};
}
+
+export function unlockComment(id) {
+ return {
+ type: ActionTypes.UNLOCK_COMMENT,
+ apiRequest: Api.unlockComment,
+ payload: { id },
+ };
+}
diff --git a/src/redux/action-types.js b/src/redux/action-types.js
index 18466a850..945763000 100644
--- a/src/redux/action-types.js
+++ b/src/redux/action-types.js
@@ -179,3 +179,4 @@ export const TRANSLATE_TEXT = 'TRANSLATE_TEXT';
export const GET_BACKLINKS = 'GET_BACKLINKS';
export const NOTIFY_OF_ALL_COMMENTS = 'NOTIFY_OF_ALL_COMMENTS';
export const SET_ORBIT = 'SET_ORBIT';
+export const UNLOCK_COMMENT = 'UNLOCK_COMMENT';
diff --git a/src/redux/reducers.js b/src/redux/reducers.js
index 6133dba0b..17c9715e3 100644
--- a/src/redux/reducers.js
+++ b/src/redux/reducers.js
@@ -2181,3 +2181,5 @@ export const calendarMonthDays = fromResponse(
export { userStatsStatus, userStats } from './reducers/dynamic-user-stats';
export { translationStates, translationResults } from './reducers/translation';
+
+export { unlockedCommentStates, unlockedComments } from './reducers/unlocked-comments';
diff --git a/src/redux/reducers/unlocked-comments.js b/src/redux/reducers/unlocked-comments.js
new file mode 100644
index 000000000..f80785f2c
--- /dev/null
+++ b/src/redux/reducers/unlocked-comments.js
@@ -0,0 +1,14 @@
+import { UNLOCK_COMMENT } from '../action-types';
+import { asyncResultsMap, asyncStatesMap } from '../async-helpers';
+import { setOnLocationChange } from './helpers';
+
+const resetOnLocationChange = setOnLocationChange({});
+
+export const unlockedCommentStates = asyncStatesMap(UNLOCK_COMMENT, {}, resetOnLocationChange);
+export const unlockedComments = asyncResultsMap(
+ UNLOCK_COMMENT,
+ {
+ transformer: (action) => action.payload.comments,
+ },
+ resetOnLocationChange,
+);
diff --git a/src/services/api.js b/src/services/api.js
index 43069d75d..449ac312b 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -820,6 +820,10 @@ export function getCommentsByIds({ commentIds }) {
return fetch(`${apiRoot}/v2/comments/byIds`, postRequestOptions('POST', { commentIds }));
}
+export function unlockComment({ id }) {
+ return fetch(`${apiRoot}/v2/comments/${id}?unlock-banned`, getRequestOptions());
+}
+
export function translateText({ type, id, lang }) {
const part = type === 'post' ? 'posts' : 'comments';
const qs = lang ? `?lang=${lang}` : '';