From 5e0db9b308d48ff28caa500da95c1577e9bd4839 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 14 Mar 2024 10:54:07 +0800 Subject: [PATCH 1/4] Fix the issue of scrolling to the comment view after the initial loading of comments (#98) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整滚动到评论组件视图的时机,改为仅在分页的时候才滚动到顶部。在此之前,如果文章比较短,可以在屏幕中显示评论组件时,页面可能会自动滚动到评论组件的部分。 /kind bug ```release-note 优化加载数据之后滚动到评论组件视图的时机 ``` --- packages/comment-widget/src/comment-widget.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/comment-widget/src/comment-widget.ts b/packages/comment-widget/src/comment-widget.ts index 90a9be3..fd16389 100644 --- a/packages/comment-widget/src/comment-widget.ts +++ b/packages/comment-widget/src/comment-widget.ts @@ -84,7 +84,9 @@ export class CommentWidget extends LitElement { override render() { return html`
- + ${this.loading ? html`` : html` @@ -135,7 +137,8 @@ export class CommentWidget extends LitElement { this.currentUser = data.user.metadata.name === 'anonymousUser' ? undefined : data.user; } - async fetchComments(page?: number) { + async fetchComments(options?: { page?: number; scrollIntoView?: boolean }) { + const { page, scrollIntoView } = options || {}; try { if (this.comments.items.length === 0) { this.loading = true; @@ -170,14 +173,17 @@ export class CommentWidget extends LitElement { } } finally { this.loading = false; - this.scrollIntoView({ block: 'start', inline: 'start', behavior: 'smooth' }); + + if (scrollIntoView) { + this.scrollIntoView({ block: 'start', inline: 'start', behavior: 'smooth' }); + } } } async onPageChange(e: CustomEvent) { const data = e.detail; this.comments.page = data.page; - await this.fetchComments(); + await this.fetchComments({ scrollIntoView: true }); } override connectedCallback(): void { From db8b8001839e5751f25b64f4fa5acaf1ca9ac2e4 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 14 Mar 2024 10:56:07 +0800 Subject: [PATCH 2/4] fix: issue of the emoji picker being semi-transparent (#103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复鼠标移动到 Emoji 区域背景变半透明的问题。 Fixes https://github.com/halo-dev/plugin-comment-widget/issues/96 ```release-note 修复鼠标移动到 Emoji 区域背景变半透明的问题。 ``` --- packages/comment-widget/src/emoji-button.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/comment-widget/src/emoji-button.ts b/packages/comment-widget/src/emoji-button.ts index d31dc52..104ea0e 100644 --- a/packages/comment-widget/src/emoji-button.ts +++ b/packages/comment-widget/src/emoji-button.ts @@ -118,7 +118,7 @@ export class EmojiButton extends LitElement { position: relative; } - .emoji-button:hover { + .emoji-button:hover icon-emoji { opacity: 0.8; } From d29197eee2cbdc17c2326893fd9edd4b91523a05 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Thu, 14 Mar 2024 11:06:06 +0800 Subject: [PATCH 3/4] feat: make the content editor focused when the reply form is opened (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持打开评论回复时,自动聚焦输入框。 /kind feature ```release-note 支持打开评论回复时,自动聚焦输入框。 ``` --- packages/comment-widget/src/base-form.ts | 4 ++++ packages/comment-widget/src/reply-form.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/comment-widget/src/base-form.ts b/packages/comment-widget/src/base-form.ts index 41ffbb2..9ae96dd 100644 --- a/packages/comment-widget/src/base-form.ts +++ b/packages/comment-widget/src/base-form.ts @@ -223,6 +223,10 @@ export class BaseForm extends LitElement { form?.reset(); } + setFocus() { + this.textareaRef.value?.focus(); + } + static override styles = [ varStyles, baseStyles, diff --git a/packages/comment-widget/src/reply-form.ts b/packages/comment-widget/src/reply-form.ts index 3c0ce18..be1e416 100644 --- a/packages/comment-widget/src/reply-form.ts +++ b/packages/comment-widget/src/reply-form.ts @@ -38,6 +38,15 @@ export class ReplyForm extends LitElement { baseFormRef: Ref = createRef(); + override connectedCallback(): void { + super.connectedCallback(); + + setTimeout(() => { + this.scrollIntoView({ block: 'center', inline: 'start', behavior: 'smooth' }); + this.baseFormRef.value?.setFocus(); + }, 0); + } + override render() { return html``; } From 4c3da7efb6d23ce0d7d0f88a64cdf91ad06d0f9e Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Tue, 19 Mar 2024 18:36:07 +0800 Subject: [PATCH 4/4] feat: fetch comments with related replies (#106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持加载评论列表时同时加载对应回复。 image TODO: - [x] 在插件设置中提供设置,是否启用自动加载回复,以及默认加载条数 ```release-note 支持加载评论列表时同时加载对应回复。 ``` --- build.gradle | 2 +- .../src/base-comment-item-action.ts | 3 +- .../comment-widget/src/base-comment-item.ts | 1 + packages/comment-widget/src/comment-item.ts | 116 ++++++++++++++---- .../comment-widget/src/comment-replies.ts | 115 ++++++++++++++--- packages/comment-widget/src/comment-widget.ts | 16 ++- packages/comment-widget/src/context/index.ts | 2 + packages/comment-widget/src/reply-item.ts | 3 +- packages/example/index.html | 9 +- packages/widget/src/index.ts | 18 ++- .../comment/widget/DefaultCommentWidget.java | 26 +++- src/main/resources/extensions/settings.yaml | 22 ++++ src/main/resources/plugin.yaml | 4 +- 13 files changed, 274 insertions(+), 63 deletions(-) create mode 100644 src/main/resources/extensions/settings.yaml diff --git a/build.gradle b/build.gradle index 31021d7..d31f0ae 100644 --- a/build.gradle +++ b/build.gradle @@ -42,4 +42,4 @@ build { halo { version = "2.13.0" -} \ No newline at end of file +} diff --git a/packages/comment-widget/src/base-comment-item-action.ts b/packages/comment-widget/src/base-comment-item-action.ts index ad58046..f84584a 100644 --- a/packages/comment-widget/src/base-comment-item-action.ts +++ b/packages/comment-widget/src/base-comment-item-action.ts @@ -21,7 +21,7 @@ export class BaseCommentItemAction extends LitElement { display: inline-flex; align-items: center; cursor: pointer; - margin-right: 0.5em; + gap: 0.1em; } .item-action__icon { @@ -44,6 +44,7 @@ export class BaseCommentItemAction extends LitElement { .item-action__text { color: var(--base-info-color); user-select: none; + font-size: 0.75em; } .item-action:hover .item-action__icon { diff --git a/packages/comment-widget/src/base-comment-item.ts b/packages/comment-widget/src/base-comment-item.ts index 5ef358d..0a01e09 100644 --- a/packages/comment-widget/src/base-comment-item.ts +++ b/packages/comment-widget/src/base-comment-item.ts @@ -108,6 +108,7 @@ export class BaseCommentItem extends LitElement { margin-top: 0.5em; display: flex; align-items: center; + gap: 0.7em; } .item--animate-breath { diff --git a/packages/comment-widget/src/comment-item.ts b/packages/comment-widget/src/comment-item.ts index 8ac6c5c..5404505 100644 --- a/packages/comment-widget/src/comment-item.ts +++ b/packages/comment-widget/src/comment-item.ts @@ -7,9 +7,11 @@ import './user-avatar'; import './base-comment-item'; import './base-comment-item-action'; import { consume } from '@lit/context'; -import { baseUrlContext } from './context'; +import { baseUrlContext, withRepliesContext } from './context'; import { LS_UPVOTED_COMMENTS_KEY } from './constant'; import varStyles from './styles/var'; +import { Ref, createRef, ref } from 'lit/directives/ref.js'; +import { CommentReplies } from './comment-replies'; export class CommentItem extends LitElement { @consume({ context: baseUrlContext }) @@ -19,19 +21,33 @@ export class CommentItem extends LitElement { @property({ type: Object }) comment: CommentVo | undefined; + @consume({ context: withRepliesContext, subscribe: true }) + @state() + withReplies = false; + @state() showReplies = false; + @state() + showReplyForm = false; + @state() upvoted = false; @state() upvoteCount = 0; + commentRepliesRef: Ref = createRef(); + override connectedCallback(): void { super.connectedCallback(); this.checkUpvotedStatus(); + if (this.withReplies) { + this.showReplies = true; + this.showReplyForm = false; + } + this.upvoteCount = this.comment?.stats.upvote || 0; } @@ -72,6 +88,19 @@ export class CommentItem extends LitElement { this.checkUpvotedStatus(); } + handleShowReplies() { + this.showReplies = !this.showReplies; + + if (!this.withReplies) { + this.showReplyForm = !this.showReplyForm; + } + } + + onReplyCreated() { + this.commentRepliesRef.value?.fetchReplies(); + this.showReplies = true; + } + override render() { return html``} - - - - - + ${this.withReplies && this.comment?.status?.visibleReplyCount === 0 + ? '' + : html` + + + + `} + ${this.withReplies + ? html` + + + + ` + : ''}
+ ${this.showReplyForm + ? html`
+ +
` + : ''} ${this.showReplies - ? html`` + ? html`` : ``}
`; @@ -159,6 +223,10 @@ export class CommentItem extends LitElement { .item__action--upvote { margin-left: -0.5em; } + + .item__reply-form { + margin-top: 0.5em; + } `, ]; } diff --git a/packages/comment-widget/src/comment-replies.ts b/packages/comment-widget/src/comment-replies.ts index 28d223c..db8b81c 100644 --- a/packages/comment-widget/src/comment-replies.ts +++ b/packages/comment-widget/src/comment-replies.ts @@ -1,9 +1,9 @@ -import { CommentVo, ReplyVo } from '@halo-dev/api-client'; +import { CommentVo, ReplyVo, ReplyVoList } from '@halo-dev/api-client'; import { LitElement, css, html } from 'lit'; import { property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { consume } from '@lit/context'; -import { baseUrlContext, toastContext } from './context'; +import { baseUrlContext, replySizeContext, toastContext, withRepliesContext } from './context'; import './reply-item'; import './loading-block'; import './reply-form'; @@ -16,12 +16,29 @@ export class CommentReplies extends LitElement { @property({ attribute: false }) baseUrl = ''; + @consume({ context: withRepliesContext, subscribe: true }) + @state() + withReplies = false; + + @consume({ context: replySizeContext, subscribe: true }) + @state() + replySize = 10; + @property({ type: Object }) comment: CommentVo | undefined; + @property({ type: Boolean }) + showReplyForm = false; + @state() replies: ReplyVo[] = []; + @state() + page = 1; + + @state() + hasNext = false; + @state() loading = false; @@ -34,10 +51,8 @@ export class CommentReplies extends LitElement { override render() { return html`
- - ${this.loading - ? html`` - : html` + ${this.replies.length + ? html`
${repeat( this.replies, @@ -53,7 +68,14 @@ export class CommentReplies extends LitElement { >` )}
- `} + ` + : ''} + ${this.loading ? html`` : ''} + ${this.hasNext && !this.loading + ? html`
+ +
` + : ''}
`; } @@ -61,47 +83,100 @@ export class CommentReplies extends LitElement { this.activeQuoteReply = event.detail.quoteReply; } - async fetchReplies() { - if (this.replies.length === 0) { + async fetchReplies(options?: { append: boolean }) { + try { this.loading = true; - } - try { + // Reload replies list + if (!options?.append) { + this.page = 1; + } + + const queryParams = [`page=${this.page || 0}`, `size=${this.replySize}`]; + const response = await fetch( - `${this.baseUrl}/apis/api.halo.run/v1alpha1/comments/${this.comment?.metadata.name}/reply` + `${this.baseUrl}/apis/api.halo.run/v1alpha1/comments/${this.comment?.metadata.name}/reply?${queryParams.join('&')}` ); if (!response.ok) { throw new Error('加载回复列表失败,请稍后重试'); } - const data = await response.json(); - this.replies = data.items; + const data = (await response.json()) as ReplyVoList; + + if (options?.append) { + this.replies = this.replies.concat(data.items); + } else { + this.replies = data.items; + } + + this.hasNext = data.hasNext; + this.page = data.page; } catch (error) { if (error instanceof Error) { this.toastManager?.error(error.message); } + } finally { + this.loading = false; } + } - this.loading = false; + async fetchNext() { + this.page++; + this.fetchReplies({ append: true }); } override connectedCallback(): void { super.connectedCallback(); - this.fetchReplies(); + + if (this.withReplies) { + // TODO: Fix ts error + // Needs @halo-dev/api-client@2.14.0 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.replies = this.comment?.replies.items; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.page = this.comment?.replies.page; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.hasNext = this.comment?.replies.hasNext; + } else { + this.fetchReplies(); + } } static override styles = [ varStyles, baseStyles, css` - .replies__wrapper { - margin-top: 0.5em; - } - .replies__list { margin-top: 0.875em; } + + .replies__next-wrapper { + display: flex; + justify-content: center; + margin: 0.5em 0; + } + + .replies__next-wrapper button { + border-radius: var(--base-border-radius); + color: var(--base-color); + font-size: 0.875em; + display: inline-flex; + align-items: center; + font-weight: 600; + padding: 0.4em 0.875em; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 0.15s; + border: 1px solid transparent; + } + + .replies__next-wrapper button:hover { + background-color: var(--component-pagination-button-bg-color-hover); + } `, ]; } diff --git a/packages/comment-widget/src/comment-widget.ts b/packages/comment-widget/src/comment-widget.ts index fd16389..862930c 100644 --- a/packages/comment-widget/src/comment-widget.ts +++ b/packages/comment-widget/src/comment-widget.ts @@ -12,8 +12,10 @@ import { groupContext, kindContext, nameContext, + replySizeContext, toastContext, versionContext, + withRepliesContext, } from './context'; import './comment-form'; import './comment-item'; @@ -23,7 +25,7 @@ import { ToastManager } from './lit-toast'; export class CommentWidget extends LitElement { @provide({ context: baseUrlContext }) - @property({ type: String }) + @property({ type: String, attribute: 'base-url' }) baseUrl = ''; @provide({ context: kindContext }) @@ -42,8 +44,16 @@ export class CommentWidget extends LitElement { @property({ type: String }) name = ''; + @provide({ context: withRepliesContext }) + @property({ type: Boolean, attribute: 'with-replies' }) + withReplies = false; + + @provide({ context: replySizeContext }) + @property({ type: Number, attribute: 'reply-size' }) + replySize = 10; + @provide({ context: emojiDataUrlContext }) - @property({ type: String }) + @property({ type: String, attribute: 'emoji-data-url' }) emojiDataUrl = 'https://unpkg.com/@emoji-mart/data'; @provide({ context: currentUserContext }) @@ -155,6 +165,8 @@ export class CommentWidget extends LitElement { `page=${this.comments.page}`, `size=${this.comments.size}`, `version=${this.version}`, + `withReplies=${this.withReplies}`, + `replySize=${this.replySize}`, ]; const response = await fetch( diff --git a/packages/comment-widget/src/context/index.ts b/packages/comment-widget/src/context/index.ts index c5b6b7f..e660c92 100644 --- a/packages/comment-widget/src/context/index.ts +++ b/packages/comment-widget/src/context/index.ts @@ -7,6 +7,8 @@ export const kindContext = createContext(Symbol('kind')); export const groupContext = createContext(Symbol('group')); export const nameContext = createContext(Symbol('name')); export const versionContext = createContext(Symbol('version')); +export const withRepliesContext = createContext(Symbol('withReplies')); +export const replySizeContext = createContext(Symbol('replySize')); export const allowAnonymousCommentsContext = createContext( Symbol('allowAnonymousComments') diff --git a/packages/comment-widget/src/reply-item.ts b/packages/comment-widget/src/reply-item.ts index c7a4f49..ef93c56 100644 --- a/packages/comment-widget/src/reply-item.ts +++ b/packages/comment-widget/src/reply-item.ts @@ -157,6 +157,7 @@ export class ReplyItem extends LitElement { diff --git a/packages/example/index.html b/packages/example/index.html index 8f69b53..65f518a 100644 --- a/packages/example/index.html +++ b/packages/example/index.html @@ -20,8 +20,8 @@ --halo-comment-widget-base-border-radius: 0.5em; /* Color */ - --halo-comment-widget-base-color: #ffffff; - --halo-comment-widget-base-info-color: #64748b; + --halo-comment-widget-base-color: #ffffff; + --halo-comment-widget-base-info-color: #64748b; /* Component */ --halo-comment-widget-component-avatar-rounded: 9999px; @@ -67,11 +67,12 @@ diff --git a/packages/widget/src/index.ts b/packages/widget/src/index.ts index ed07d37..68a0e2c 100644 --- a/packages/widget/src/index.ts +++ b/packages/widget/src/index.ts @@ -3,7 +3,15 @@ import '@halo-dev/comment-widget/var.css'; export { CommentWidget }; -export function init(el: string, props: Record) { +interface Props { + group: string; + kind: string; + name: string; + withReplies?: boolean; + replySize?: number; +} + +export function init(el: string, props: Props) { const parent = document.querySelector(el) as HTMLElement; if (!parent) { @@ -14,10 +22,12 @@ export function init(el: string, props: Record) { 'comment-widget' ) as CommentWidget; - commentWidget.kind = props.kind as string; - commentWidget.group = props.group as string; + commentWidget.kind = props.kind; + commentWidget.group = props.group; commentWidget.version = 'v1alpha1'; - commentWidget.name = props.name as string; + commentWidget.name = props.name; + commentWidget.withReplies = props.withReplies || false; + commentWidget.replySize = props.replySize || 10; commentWidget.emojiDataUrl = '/plugins/PluginCommentWidget/assets/static/emoji/native.json'; diff --git a/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java b/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java index 5eb0f5e..762be51 100644 --- a/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java +++ b/src/main/java/run/halo/comment/widget/DefaultCommentWidget.java @@ -1,21 +1,21 @@ package run.halo.comment.widget; +import java.util.Properties; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.pf4j.PluginWrapper; import org.springframework.stereotype.Component; -import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.Assert; import org.springframework.util.PropertyPlaceholderHelper; import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.model.IAttribute; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.processor.element.IElementTagStructureHandler; +import run.halo.app.plugin.SettingFetcher; import run.halo.app.theme.dialect.CommentWidget; -import java.util.Properties; - /** * A default implementation of {@link CommentWidget}. * @@ -29,6 +29,7 @@ public class DefaultCommentWidget implements CommentWidget { static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER = new PropertyPlaceholderHelper("${", "}"); private final PluginWrapper pluginWrapper; + private final SettingFetcher settingFetcher; @Override public void render(ITemplateContext context, @@ -63,6 +64,12 @@ private String commentHtml(IAttribute groupAttribute, IAttribute kindAttribute, properties.setProperty("name", nameAttribute.getValue()); properties.setProperty("domId", domIdFrom(group, kindAttribute.getValue(), nameAttribute.getValue())); + var basicConfig = settingFetcher.fetch(BasicConfig.GROUP, BasicConfig.class) + .orElse(new BasicConfig()); + // placeholderHelper only support string, so we need to convert boolean to string + properties.setProperty("withReplies", String.valueOf(basicConfig.isWithReplies())); + properties.setProperty("replySize", String.valueOf(basicConfig.getReplySize())); + return PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders("""
""", properties); } + @Data + static class BasicConfig { + public static final String GROUP = "basic"; + private boolean withReplies; + private int replySize; + } + private String domIdFrom(String group, String kind, String name) { Assert.notNull(name, "The name must not be null."); Assert.notNull(kind, "The kind must not be null."); String groupKindNameAsDomId = String.join("-", group, kind, name); return "comment-" + groupKindNameAsDomId.replaceAll("[^\\-_a-zA-Z0-9\\s]", "-") - .replaceAll("(-)+", "-"); + .replaceAll("(-)+", "-"); } private String getGroup(IAttribute groupAttribute) { diff --git a/src/main/resources/extensions/settings.yaml b/src/main/resources/extensions/settings.yaml new file mode 100644 index 0000000..cec26b0 --- /dev/null +++ b/src/main/resources/extensions/settings.yaml @@ -0,0 +1,22 @@ +apiVersion: v1alpha1 +kind: Setting +metadata: + name: plugin-comment-widget-settings +spec: + forms: + - group: basic + label: 基本设置 + formSchema: + - $formkit: checkbox + label: 同时加载评论的回复 + name: withReplies + id: withReplies + key: withReplies + value: false + - $formkit: number + label: 默认加载回复条数 + name: replySize + id: replySize + key: replySize + validation: required + value: 20 diff --git a/src/main/resources/plugin.yaml b/src/main/resources/plugin.yaml index f220053..3ef7c03 100644 --- a/src/main/resources/plugin.yaml +++ b/src/main/resources/plugin.yaml @@ -8,7 +8,7 @@ metadata: "store.halo.run/app-id": "app-YXyaD" spec: enabled: true - requires: ">=2.6.0" + requires: ">=2.14.0" author: name: Halo OSS Team website: https://github.com/halo-dev @@ -16,6 +16,8 @@ spec: homepage: https://github.com/halo-dev/plugin-comment-widget displayName: "评论组件" description: "为用户前台提供完整的评论解决方案" + configMapName: plugin-comment-widget-configmap + settingName: plugin-comment-widget-settings license: - name: "GPL-3.0" url: "https://github.com/halo-dev/plugin-comment-widget/blob/main/LICENSE"