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"