mirror of https://github.com/halo-dev/halo
Add support for hidden comments (#7679)
* Add 'hidden' field to comment and reply requests Signed-off-by: Ryan Wang <i@ryanc.cc> * Add support for filtering comments with hidden * Specify hidden=false and approved=true for anonymous users * Set default hidden flag only if null in comments * Add 'private reply' option to comment modals * Add private tag for hidden comments and replies * Allow hiding comments only * Enhance comment visibility logic to allow owners to view hidden comments * Remove hidden input for reply form Signed-off-by: Ryan Wang <i@ryanc.cc> * Refine i18n Signed-off-by: Ryan Wang <i@ryanc.cc> --------- Signed-off-by: Ryan Wang <i@ryanc.cc> Co-authored-by: John Niang <johnniang@foxmail.com>pull/7711/head
parent
3f5b69d5d0
commit
3487132154
|
@ -3597,7 +3597,7 @@
|
||||||
},
|
},
|
||||||
"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish": {
|
"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish": {
|
||||||
"put": {
|
"put": {
|
||||||
"description": "Publish a post.",
|
"description": "UnPublish a post.",
|
||||||
"operationId": "UnpublishPost",
|
"operationId": "UnpublishPost",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
@ -16462,6 +16462,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/CommentEmailOwner"
|
"$ref": "#/components/schemas/CommentEmailOwner"
|
||||||
},
|
},
|
||||||
|
@ -21328,6 +21332,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/CommentEmailOwner"
|
"$ref": "#/components/schemas/CommentEmailOwner"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1464,7 +1464,7 @@
|
||||||
},
|
},
|
||||||
"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish": {
|
"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish": {
|
||||||
"put": {
|
"put": {
|
||||||
"description": "Publish a post.",
|
"description": "UnPublish a post.",
|
||||||
"operationId": "UnpublishPost",
|
"operationId": "UnpublishPost",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
@ -3868,6 +3868,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/CommentEmailOwner"
|
"$ref": "#/components/schemas/CommentEmailOwner"
|
||||||
},
|
},
|
||||||
|
@ -5643,6 +5647,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/CommentEmailOwner"
|
"$ref": "#/components/schemas/CommentEmailOwner"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1455,6 +1455,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/CommentEmailOwner"
|
"$ref": "#/components/schemas/CommentEmailOwner"
|
||||||
},
|
},
|
||||||
|
@ -2826,6 +2830,10 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/CommentEmailOwner"
|
"$ref": "#/components/schemas/CommentEmailOwner"
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,14 @@ public interface UserService {
|
||||||
|
|
||||||
Mono<User> grantRoles(String username, Set<String> roles);
|
Mono<User> grantRoles(String username, Set<String> roles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has sufficient roles.
|
||||||
|
*
|
||||||
|
* @param roles roles to check
|
||||||
|
* @return a Mono that emits true if the user has all the roles, false otherwise
|
||||||
|
*/
|
||||||
|
Mono<Boolean> hasSufficientRoles(Collection<String> roles);
|
||||||
|
|
||||||
Mono<User> signUp(SignUpData signUpData);
|
Mono<User> signUp(SignUpData signUpData);
|
||||||
|
|
||||||
Mono<User> createUser(User user, Set<String> roles);
|
Mono<User> createUser(User user, Set<String> roles);
|
||||||
|
|
|
@ -32,6 +32,9 @@ public class CommentRequest {
|
||||||
@Schema(defaultValue = "false")
|
@Schema(defaultValue = "false")
|
||||||
private Boolean allowNotification;
|
private Boolean allowNotification;
|
||||||
|
|
||||||
|
@Schema(defaultValue = "false")
|
||||||
|
private Boolean hidden;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts {@link CommentRequest} to {@link Comment}.
|
* Converts {@link CommentRequest} to {@link Comment}.
|
||||||
*
|
*
|
||||||
|
@ -48,6 +51,7 @@ public class CommentRequest {
|
||||||
spec.setRaw(raw);
|
spec.setRaw(raw);
|
||||||
spec.setContent(content);
|
spec.setContent(content);
|
||||||
spec.setAllowNotification(allowNotification);
|
spec.setAllowNotification(allowNotification);
|
||||||
|
spec.setHidden(hidden);
|
||||||
|
|
||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
spec.setOwner(owner.toCommentOwner());
|
spec.setOwner(owner.toCommentOwner());
|
||||||
|
|
|
@ -107,7 +107,10 @@ public class CommentServiceImpl extends AbstractCommentService implements Commen
|
||||||
comment.getSpec().setCreationTime(Instant.now());
|
comment.getSpec().setCreationTime(Instant.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
comment.getSpec().setHidden(false);
|
if (comment.getSpec().getHidden() == null) {
|
||||||
|
comment.getSpec().setHidden(false);
|
||||||
|
}
|
||||||
|
|
||||||
return Mono.just(comment);
|
return Mono.just(comment);
|
||||||
})
|
})
|
||||||
.flatMap(populatedComment -> Mono.when(populateOwner(populatedComment),
|
.flatMap(populatedComment -> Mono.when(populateOwner(populatedComment),
|
||||||
|
|
|
@ -26,6 +26,9 @@ public class ReplyRequest {
|
||||||
@Schema(defaultValue = "false")
|
@Schema(defaultValue = "false")
|
||||||
private Boolean allowNotification;
|
private Boolean allowNotification;
|
||||||
|
|
||||||
|
@Schema(defaultValue = "false")
|
||||||
|
private Boolean hidden;
|
||||||
|
|
||||||
private CommentEmailOwner owner;
|
private CommentEmailOwner owner;
|
||||||
|
|
||||||
private String quoteReply;
|
private String quoteReply;
|
||||||
|
@ -45,6 +48,7 @@ public class ReplyRequest {
|
||||||
spec.setRaw(raw);
|
spec.setRaw(raw);
|
||||||
spec.setContent(content);
|
spec.setContent(content);
|
||||||
spec.setAllowNotification(allowNotification);
|
spec.setAllowNotification(allowNotification);
|
||||||
|
spec.setHidden(hidden);
|
||||||
spec.setQuoteReply(quoteReply);
|
spec.setQuoteReply(quoteReply);
|
||||||
|
|
||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
|
|
|
@ -64,7 +64,7 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
.flatMap(this::approveComment)
|
.flatMap(this::approveComment)
|
||||||
.filter(comment -> isTrue(comment.getSpec().getApproved()))
|
.filter(comment -> isTrue(comment.getSpec().getApproved()))
|
||||||
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
|
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
|
||||||
.flatMap(comment -> prepareReply(commentName, reply))
|
.flatMap(comment -> prepareReply(comment, reply))
|
||||||
.flatMap(this::doCreateReply);
|
.flatMap(this::doCreateReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +76,9 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
return approveReply(quotedReply)
|
return approveReply(quotedReply)
|
||||||
.filter(reply -> isTrue(reply.getSpec().getApproved()))
|
.filter(reply -> isTrue(reply.getSpec().getApproved()))
|
||||||
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
|
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
|
||||||
|
.doOnNext(approvedQuoteReply -> prepared.getSpec()
|
||||||
|
.setHidden(approvedQuoteReply.getSpec().getHidden())
|
||||||
|
)
|
||||||
.flatMap(approvedQuoteReply -> client.create(prepared));
|
.flatMap(approvedQuoteReply -> client.create(prepared));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +134,9 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Reply> prepareReply(String commentName, Reply reply) {
|
private Mono<Reply> prepareReply(Comment comment, Reply reply) {
|
||||||
reply.getSpec().setCommentName(commentName);
|
reply.getSpec().setCommentName(comment.getMetadata().getName());
|
||||||
|
reply.getSpec().setHidden(comment.getSpec().getHidden());
|
||||||
if (reply.getSpec().getTop() == null) {
|
if (reply.getSpec().getTop() == null) {
|
||||||
reply.getSpec().setTop(false);
|
reply.getSpec().setTop(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,8 +176,6 @@ public class CommentFinderEndpoint implements CustomEndpoint {
|
||||||
Reply reply = replyRequest.toReply();
|
Reply reply = replyRequest.toReply();
|
||||||
reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||||
reply.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));
|
reply.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));
|
||||||
// fix gh-2951
|
|
||||||
reply.getSpec().setHidden(false);
|
|
||||||
return environmentFetcher.fetchComment()
|
return environmentFetcher.fetchComment()
|
||||||
.map(commentSetting -> {
|
.map(commentSetting -> {
|
||||||
if (isFalse(commentSetting.getEnable())) {
|
if (isFalse(commentSetting.getEnable())) {
|
||||||
|
@ -191,6 +189,11 @@ public class CommentFinderEndpoint implements CustomEndpoint {
|
||||||
}
|
}
|
||||||
reply.getSpec()
|
reply.getSpec()
|
||||||
.setApproved(isFalse(commentSetting.getRequireReviewForNew()));
|
.setApproved(isFalse(commentSetting.getRequireReviewForNew()));
|
||||||
|
|
||||||
|
if (reply.getSpec().getHidden() == null) {
|
||||||
|
reply.getSpec().setHidden(false);
|
||||||
|
}
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
})
|
})
|
||||||
.defaultIfEmpty(reply);
|
.defaultIfEmpty(reply);
|
||||||
|
|
|
@ -14,6 +14,8 @@ import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.ReactiveTransactionManager;
|
import org.springframework.transaction.ReactiveTransactionManager;
|
||||||
|
@ -50,6 +52,7 @@ import run.halo.app.infra.exception.EmailVerificationFailed;
|
||||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
||||||
import run.halo.app.infra.exception.UserNotFoundException;
|
import run.halo.app.infra.exception.UserNotFoundException;
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
import run.halo.app.security.device.DeviceService;
|
import run.halo.app.security.device.DeviceService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -183,6 +186,15 @@ public class UserServiceImpl implements UserService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Boolean> hasSufficientRoles(Collection<String> roles) {
|
||||||
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
|
.map(SecurityContext::getAuthentication)
|
||||||
|
.map(a -> AuthorityUtils.authoritiesToRoles(a.getAuthorities()))
|
||||||
|
.flatMap(userRoles -> roleService.contains(userRoles, roles))
|
||||||
|
.defaultIfEmpty(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<User> signUp(SignUpData signUpData) {
|
public Mono<User> signUp(SignUpData signUpData) {
|
||||||
return environmentFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class)
|
return environmentFetcher.fetch(SystemSetting.User.GROUP, SystemSetting.User.class)
|
||||||
|
|
|
@ -3,25 +3,28 @@ package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||||
|
import static run.halo.app.core.extension.content.Comment.CommentOwner.ownerIdentity;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.or;
|
import static run.halo.app.extension.index.query.QueryFactory.or;
|
||||||
|
|
||||||
import com.google.common.hash.Hashing;
|
import com.google.common.hash.Hashing;
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.content.comment.OwnerInfo;
|
import run.halo.app.content.comment.OwnerInfo;
|
||||||
|
@ -32,14 +35,13 @@ import run.halo.app.core.extension.content.Comment;
|
||||||
import run.halo.app.core.extension.content.Reply;
|
import run.halo.app.core.extension.content.Reply;
|
||||||
import run.halo.app.core.user.service.UserService;
|
import run.halo.app.core.user.service.UserService;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
|
import run.halo.app.extension.ExtensionUtil;
|
||||||
import run.halo.app.extension.ListOptions;
|
import run.halo.app.extension.ListOptions;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.extension.PageRequest;
|
import run.halo.app.extension.PageRequest;
|
||||||
import run.halo.app.extension.PageRequestImpl;
|
import run.halo.app.extension.PageRequestImpl;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Ref;
|
import run.halo.app.extension.Ref;
|
||||||
import run.halo.app.extension.index.query.Query;
|
|
||||||
import run.halo.app.extension.router.selector.FieldSelector;
|
|
||||||
import run.halo.app.infra.AnonymousUserConst;
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
import run.halo.app.theme.finders.CommentPublicQueryService;
|
import run.halo.app.theme.finders.CommentPublicQueryService;
|
||||||
import run.halo.app.theme.finders.vo.CommentStatsVo;
|
import run.halo.app.theme.finders.vo.CommentStatsVo;
|
||||||
|
@ -59,6 +61,8 @@ import run.halo.app.theme.finders.vo.ReplyVo;
|
||||||
public class CommentPublicQueryServiceImpl implements CommentPublicQueryService {
|
public class CommentPublicQueryServiceImpl implements CommentPublicQueryService {
|
||||||
private static final int DEFAULT_SIZE = 10;
|
private static final int DEFAULT_SIZE = 10;
|
||||||
|
|
||||||
|
private static final String COMMENT_VIEW_PERMISSION = "role-template-view-comments";
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final CounterService counterService;
|
private final CounterService counterService;
|
||||||
|
@ -80,22 +84,18 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
||||||
var pageRequest = Optional.ofNullable(pageParam)
|
var pageRequest = Optional.ofNullable(pageParam)
|
||||||
.map(page -> page.withSort(page.getSort().and(defaultCommentSort())))
|
.map(page -> page.withSort(page.getSort().and(defaultCommentSort())))
|
||||||
.orElse(PageRequestImpl.ofSize(0));
|
.orElse(PageRequestImpl.ofSize(0));
|
||||||
return fixedCommentFieldSelector(ref)
|
return populateCommentListOptions(ref)
|
||||||
.flatMap(fieldSelector -> {
|
.flatMap(listOptions -> client.listBy(Comment.class, listOptions, pageRequest))
|
||||||
var listOptions = new ListOptions();
|
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
||||||
listOptions.setFieldSelector(fieldSelector);
|
.map(this::toCommentVo)
|
||||||
return client.listBy(Comment.class, listOptions, pageRequest)
|
.flatMapSequential(Function.identity())
|
||||||
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
.collectList()
|
||||||
.map(this::toCommentVo)
|
.map(commentVos -> new ListResult<>(listResult.getPage(),
|
||||||
.flatMapSequential(Function.identity())
|
listResult.getSize(),
|
||||||
.collectList()
|
listResult.getTotal(),
|
||||||
.map(commentVos -> new ListResult<>(listResult.getPage(),
|
commentVos)
|
||||||
listResult.getSize(),
|
)
|
||||||
listResult.getTotal(),
|
)
|
||||||
commentVos)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.defaultIfEmpty(ListResult.emptyResult());
|
.defaultIfEmpty(ListResult.emptyResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,10 +127,10 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageParam) {
|
public Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageParam) {
|
||||||
return fixedReplyFieldSelector(commentName)
|
// check comment
|
||||||
.flatMap(fieldSelector -> {
|
return client.get(Comment.class, commentName)
|
||||||
var listOptions = new ListOptions();
|
.flatMap(this::populateReplyListOptions)
|
||||||
listOptions.setFieldSelector(fieldSelector);
|
.flatMap(listOptions -> {
|
||||||
var pageRequest = Optional.ofNullable(pageParam)
|
var pageRequest = Optional.ofNullable(pageParam)
|
||||||
.map(page -> page.withSort(page.getSort().and(defaultReplySort())))
|
.map(page -> page.withSort(page.getSort().and(defaultReplySort())))
|
||||||
.orElse(PageRequestImpl.ofSize(0));
|
.orElse(PageRequestImpl.ofSize(0));
|
||||||
|
@ -250,53 +250,76 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
||||||
.map(OwnerInfo::from);
|
.map(OwnerInfo::from);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<FieldSelector> fixedCommentFieldSelector(@Nullable Ref ref) {
|
private Mono<ListOptions> populateCommentListOptions(@Nullable Ref ref) {
|
||||||
return Mono.fromSupplier(
|
return populateVisibleListOptions(null)
|
||||||
() -> {
|
.doOnNext(builder -> {
|
||||||
var baseQuery = isNull("metadata.deletionTimestamp");
|
if (ref != null) {
|
||||||
if (ref != null) {
|
builder.andQuery(
|
||||||
baseQuery =
|
equal("spec.subjectRef", Comment.toSubjectRefKey(ref)));
|
||||||
and(baseQuery,
|
}
|
||||||
equal("spec.subjectRef", Comment.toSubjectRefKey(ref)));
|
})
|
||||||
|
.map(ListOptions.ListOptionsBuilder::build);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ListOptions.ListOptionsBuilder> populateVisibleListOptions(
|
||||||
|
@Nullable Comment comment) {
|
||||||
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
|
.map(SecurityContext::getAuthentication)
|
||||||
|
.map(Authentication::getName)
|
||||||
|
.defaultIfEmpty(AnonymousUserConst.PRINCIPAL)
|
||||||
|
.zipWith(userService.hasSufficientRoles(Set.of(COMMENT_VIEW_PERMISSION))
|
||||||
|
.defaultIfEmpty(false))
|
||||||
|
.flatMap(tuple2 -> {
|
||||||
|
var username = tuple2.getT1();
|
||||||
|
var hasViewPermission = tuple2.getT2();
|
||||||
|
var commentHidden = false;
|
||||||
|
var isCommentOwner = false;
|
||||||
|
if (comment != null) {
|
||||||
|
commentHidden = Boolean.TRUE.equals(comment.getSpec().getHidden());
|
||||||
|
var owner = comment.getSpec().getOwner();
|
||||||
|
isCommentOwner = owner != null && Objects.equals(
|
||||||
|
ownerIdentity(owner.getKind(), owner.getName()),
|
||||||
|
ownerIdentity(User.KIND, username)
|
||||||
|
);
|
||||||
|
boolean hasPermission =
|
||||||
|
(!commentHidden) || (hasViewPermission || isCommentOwner);
|
||||||
|
if (ExtensionUtil.isDeleted(comment) || !hasPermission) {
|
||||||
|
return Mono.error(new ServerWebInputException(
|
||||||
|
"The comment was not found, hidden or deleted."
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return baseQuery;
|
}
|
||||||
})
|
|
||||||
.flatMap(this::concatVisibleQuery)
|
var builder = ListOptions.builder();
|
||||||
.map(FieldSelector::of);
|
builder.andQuery(isNull("metadata.deletionTimestamp"));
|
||||||
|
var visibleQuery = and(
|
||||||
|
equal("spec.hidden", BooleanUtils.FALSE),
|
||||||
|
equal("spec.approved", BooleanUtils.TRUE)
|
||||||
|
);
|
||||||
|
|
||||||
|
var isAnonymous = AnonymousUserConst.isAnonymousUser(username);
|
||||||
|
if (isAnonymous) {
|
||||||
|
builder.andQuery(visibleQuery);
|
||||||
|
} else if (!(hasViewPermission || (commentHidden && isCommentOwner))) {
|
||||||
|
builder.andQuery(or(
|
||||||
|
equal("spec.owner", ownerIdentity(User.KIND, username)),
|
||||||
|
visibleQuery
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// View all replies if the user is not an anonymous user, has view permission
|
||||||
|
// or is the comment owner.
|
||||||
|
return Mono.just(builder);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Query> concatVisibleQuery(Query query) {
|
private Mono<ListOptions> populateReplyListOptions(Comment comment) {
|
||||||
Assert.notNull(query, "The query must not be null");
|
|
||||||
var approvedQuery = and(
|
|
||||||
equal("spec.approved", BooleanUtils.TRUE),
|
|
||||||
equal("spec.hidden", BooleanUtils.FALSE)
|
|
||||||
);
|
|
||||||
// we should list all comments that the user owns
|
|
||||||
return getCurrentUserWithoutAnonymous()
|
|
||||||
.map(username -> or(approvedQuery, equal("spec.owner",
|
|
||||||
Comment.CommentOwner.ownerIdentity(User.KIND, username)))
|
|
||||||
)
|
|
||||||
.defaultIfEmpty(approvedQuery)
|
|
||||||
.map(compositeQuery -> and(query, compositeQuery));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<FieldSelector> fixedReplyFieldSelector(String commentName) {
|
|
||||||
Assert.notNull(commentName, "The commentName must not be null");
|
|
||||||
// The comment name must be equal to the comment name of the reply
|
// The comment name must be equal to the comment name of the reply
|
||||||
// is approved and not hidden
|
// is approved and not hidden
|
||||||
return Mono.fromSupplier(() -> and(
|
return populateVisibleListOptions(comment)
|
||||||
equal("spec.commentName", commentName),
|
.doOnNext(builder ->
|
||||||
isNull("metadata.deletionTimestamp")
|
builder.andQuery(equal("spec.commentName", comment.getMetadata().getName()))
|
||||||
))
|
)
|
||||||
.flatMap(this::concatVisibleQuery)
|
.map(ListOptions.ListOptionsBuilder::build);
|
||||||
.map(FieldSelector::of);
|
|
||||||
}
|
|
||||||
|
|
||||||
Mono<String> getCurrentUserWithoutAnonymous() {
|
|
||||||
return ReactiveSecurityContextHolder.getContext()
|
|
||||||
.mapNotNull(SecurityContext::getAuthentication)
|
|
||||||
.map(Principal::getName)
|
|
||||||
.filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Sort defaultCommentSort() {
|
static Sort defaultCommentSort() {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.extension.index.IndexerFactory;
|
import run.halo.app.extension.index.IndexerFactory;
|
||||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||||
import run.halo.app.infra.AnonymousUserConst;
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
|
import run.halo.app.infra.exception.DuplicateNameException;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
@DirtiesContext
|
@DirtiesContext
|
||||||
|
@ -288,11 +289,19 @@ class CommentPublicQueryServiceIntegrationTest {
|
||||||
@Nested
|
@Nested
|
||||||
class ListReplyTest {
|
class ListReplyTest {
|
||||||
private final List<Reply> storedReplies = mockRelies();
|
private final List<Reply> storedReplies = mockRelies();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CommentPublicQueryServiceImpl commentPublicQueryService;
|
private CommentPublicQueryServiceImpl commentPublicQueryService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
|
// create comment
|
||||||
|
var comment = createComment();
|
||||||
|
client.create(comment)
|
||||||
|
.onErrorResume(DuplicateNameException.class, e -> Mono.just(comment))
|
||||||
|
.as(StepVerifier::create)
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
Flux.fromIterable(storedReplies)
|
Flux.fromIterable(storedReplies)
|
||||||
.flatMap(reply -> client.create(reply))
|
.flatMap(reply -> client.create(reply))
|
||||||
.as(StepVerifier::create)
|
.as(StepVerifier::create)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
VModal,
|
VModal,
|
||||||
VSpace,
|
VSpace,
|
||||||
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQueryClient } from "@tanstack/vue-query";
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useUserAgent } from "@uc/modules/profile/tabs/composables/use-user-agent";
|
import { useUserAgent } from "@uc/modules/profile/tabs/composables/use-user-agent";
|
||||||
|
@ -177,6 +178,11 @@ const { data: contentProvider } = useContentProviderExtensionPoint();
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
:label="$t('core.comment.comment_detail_modal.fields.content')"
|
:label="$t('core.comment.comment_detail_modal.fields.content')"
|
||||||
>
|
>
|
||||||
|
<div v-if="comment.comment.spec.hidden" class="mb-2">
|
||||||
|
<VTag>
|
||||||
|
{{ $t("core.comment.list.fields.private") }}
|
||||||
|
</VTag>
|
||||||
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="contentProvider?.component"
|
:is="contentProvider?.component"
|
||||||
:content="comment.comment.spec.content"
|
:content="comment.comment.spec.content"
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
VLoading,
|
VLoading,
|
||||||
VSpace,
|
VSpace,
|
||||||
VStatusDot,
|
VStatusDot,
|
||||||
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { OperationItem } from "@halo-dev/console-shared";
|
import type { OperationItem } from "@halo-dev/console-shared";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
|
@ -291,6 +292,9 @@ const { data: contentProvider } = useContentProviderExtensionPoint();
|
||||||
:owner="comment?.owner"
|
:owner="comment?.owner"
|
||||||
@click="detailModalVisible = true"
|
@click="detailModalVisible = true"
|
||||||
/>
|
/>
|
||||||
|
<VTag v-if="comment.comment.spec.hidden">
|
||||||
|
{{ $t("core.comment.list.fields.private") }}
|
||||||
|
</VTag>
|
||||||
<span class="whitespace-nowrap text-sm text-gray-900">
|
<span class="whitespace-nowrap text-sm text-gray-900">
|
||||||
{{ $t("core.comment.text.commented_on") }}
|
{{ $t("core.comment.text.commented_on") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
VDescriptionItem,
|
VDescriptionItem,
|
||||||
VModal,
|
VModal,
|
||||||
VSpace,
|
VSpace,
|
||||||
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQueryClient } from "@tanstack/vue-query";
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useUserAgent } from "@uc/modules/profile/tabs/composables/use-user-agent";
|
import { useUserAgent } from "@uc/modules/profile/tabs/composables/use-user-agent";
|
||||||
|
@ -185,17 +186,25 @@ const { data: contentProvider } = useContentProviderExtensionPoint();
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
:label="$t('core.comment.reply_detail_modal.fields.original_comment')"
|
:label="$t('core.comment.reply_detail_modal.fields.original_comment')"
|
||||||
>
|
>
|
||||||
<OwnerButton :owner="comment.owner" />
|
<div class="mb-2 flex items-center gap-2">
|
||||||
<div class="mt-2">
|
<OwnerButton :owner="comment.owner" />
|
||||||
<component
|
<VTag v-if="comment.comment.spec.hidden">
|
||||||
:is="contentProvider?.component"
|
{{ $t("core.comment.list.fields.private") }}
|
||||||
:content="comment.comment.spec.content"
|
</VTag>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<component
|
||||||
|
:is="contentProvider?.component"
|
||||||
|
:content="comment.comment.spec.content"
|
||||||
|
/>
|
||||||
</VDescriptionItem>
|
</VDescriptionItem>
|
||||||
<VDescriptionItem
|
<VDescriptionItem
|
||||||
:label="$t('core.comment.reply_detail_modal.fields.content')"
|
:label="$t('core.comment.reply_detail_modal.fields.content')"
|
||||||
>
|
>
|
||||||
|
<div v-if="reply.reply.spec.hidden" class="mb-2">
|
||||||
|
<VTag>
|
||||||
|
{{ $t("core.comment.list.fields.private") }}
|
||||||
|
</VTag>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
v-if="quoteReply"
|
v-if="quoteReply"
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
VEntity,
|
VEntity,
|
||||||
VEntityField,
|
VEntityField,
|
||||||
VStatusDot,
|
VStatusDot,
|
||||||
|
VTag,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import type { OperationItem } from "@halo-dev/console-shared";
|
import type { OperationItem } from "@halo-dev/console-shared";
|
||||||
import { useQueryClient } from "@tanstack/vue-query";
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
|
@ -226,6 +227,9 @@ const { data: contentProvider } = useContentProviderExtensionPoint();
|
||||||
:owner="reply?.owner"
|
:owner="reply?.owner"
|
||||||
@click="detailModalVisible = true"
|
@click="detailModalVisible = true"
|
||||||
/>
|
/>
|
||||||
|
<VTag v-if="comment.comment.spec.hidden">
|
||||||
|
{{ $t("core.comment.list.fields.private") }}
|
||||||
|
</VTag>
|
||||||
<span class="whitespace-nowrap text-sm text-gray-900">
|
<span class="whitespace-nowrap text-sm text-gray-900">
|
||||||
{{ $t("core.comment.text.replied_below") }}
|
{{ $t("core.comment.text.replied_below") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -520,7 +520,7 @@ export const PostV1alpha1ConsoleApiAxiosParamCreator = function (configuration?:
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Publish a post.
|
* UnPublish a post.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
|
@ -797,7 +797,7 @@ export const PostV1alpha1ConsoleApiFp = function(configuration?: Configuration)
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Publish a post.
|
* UnPublish a post.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
|
@ -935,7 +935,7 @@ export const PostV1alpha1ConsoleApiFactory = function (configuration?: Configura
|
||||||
return localVarFp.revertToSpecifiedSnapshotForPost(requestParameters.name, requestParameters.revertSnapshotForPostParam, options).then((request) => request(axios, basePath));
|
return localVarFp.revertToSpecifiedSnapshotForPost(requestParameters.name, requestParameters.revertSnapshotForPostParam, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Publish a post.
|
* UnPublish a post.
|
||||||
* @param {PostV1alpha1ConsoleApiUnpublishPostRequest} requestParameters Request parameters.
|
* @param {PostV1alpha1ConsoleApiUnpublishPostRequest} requestParameters Request parameters.
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
|
@ -1362,7 +1362,7 @@ export class PostV1alpha1ConsoleApi extends BaseAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish a post.
|
* UnPublish a post.
|
||||||
* @param {PostV1alpha1ConsoleApiUnpublishPostRequest} requestParameters Request parameters.
|
* @param {PostV1alpha1ConsoleApiUnpublishPostRequest} requestParameters Request parameters.
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
|
|
|
@ -38,6 +38,12 @@ export interface CommentRequest {
|
||||||
* @memberof CommentRequest
|
* @memberof CommentRequest
|
||||||
*/
|
*/
|
||||||
'content': string;
|
'content': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof CommentRequest
|
||||||
|
*/
|
||||||
|
'hidden'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {CommentEmailOwner}
|
* @type {CommentEmailOwner}
|
||||||
|
|
|
@ -35,6 +35,12 @@ export interface ReplyRequest {
|
||||||
* @memberof ReplyRequest
|
* @memberof ReplyRequest
|
||||||
*/
|
*/
|
||||||
'content': string;
|
'content': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof ReplyRequest
|
||||||
|
*/
|
||||||
|
'hidden'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {CommentEmailOwner}
|
* @type {CommentEmailOwner}
|
||||||
|
|
|
@ -197,6 +197,13 @@ core:
|
||||||
button: Reply and approve
|
button: Reply and approve
|
||||||
cancel_approve:
|
cancel_approve:
|
||||||
button: Cancel approve
|
button: Cancel approve
|
||||||
|
list:
|
||||||
|
fields:
|
||||||
|
private: Private
|
||||||
|
reply_modal:
|
||||||
|
fields:
|
||||||
|
hidden:
|
||||||
|
label: Private reply
|
||||||
detail_modal:
|
detail_modal:
|
||||||
fields:
|
fields:
|
||||||
owner: Commentator
|
owner: Commentator
|
||||||
|
|
|
@ -595,6 +595,7 @@ core:
|
||||||
reply_count: "{count} Replies"
|
reply_count: "{count} Replies"
|
||||||
has_new_replies: New replies
|
has_new_replies: New replies
|
||||||
pending_review: Pending review
|
pending_review: Pending review
|
||||||
|
private: Private
|
||||||
subject_refs:
|
subject_refs:
|
||||||
post: Post
|
post: Post
|
||||||
page: Page
|
page: Page
|
||||||
|
|
|
@ -561,6 +561,7 @@ core:
|
||||||
reply_count: "{count} 条回复"
|
reply_count: "{count} 条回复"
|
||||||
has_new_replies: 有新的回复
|
has_new_replies: 有新的回复
|
||||||
pending_review: 待审核
|
pending_review: 待审核
|
||||||
|
private: 私密
|
||||||
subject_refs:
|
subject_refs:
|
||||||
post: 文章
|
post: 文章
|
||||||
page: 页面
|
page: 页面
|
||||||
|
|
|
@ -546,6 +546,7 @@ core:
|
||||||
reply_count: "{count} 條回覆"
|
reply_count: "{count} 條回覆"
|
||||||
has_new_replies: 有新的回覆
|
has_new_replies: 有新的回覆
|
||||||
pending_review: 待審核
|
pending_review: 待審核
|
||||||
|
private: 私密
|
||||||
subject_refs:
|
subject_refs:
|
||||||
post: 文章
|
post: 文章
|
||||||
page: 頁面
|
page: 頁面
|
||||||
|
|
Loading…
Reference in New Issue