Skip to content

Commit 077bcfe

Browse files
authored
Add support for private comments in comment widget (#191)
* Add support for private comments in comment widget * Update Signed-off-by: Ryan Wang <[email protected]> * Update Signed-off-by: Ryan Wang <[email protected]> * Add option to show private comment badge * Add tooltip for private comment option * Refactor private comment badge rendering logic * Hide private checkbox in reply form * Update biome.json includes and import base-tooltip * Update minimum required version in plugin.yaml --------- Signed-off-by: Ryan Wang <[email protected]>
1 parent 8e49b63 commit 077bcfe

File tree

19 files changed

+280
-12
lines changed

19 files changed

+280
-12
lines changed

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"files": {
99
"ignoreUnknown": false,
10-
"includes": ["**", "!**/generated/**"]
10+
"includes": ["**", "!**/generated"]
1111
},
1212
"formatter": {
1313
"enabled": true,

packages/comment-widget/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"dependencies": {
3636
"@emoji-mart/data": "^1.2.1",
3737
"@floating-ui/dom": "^1.7.3",
38-
"@halo-dev/api-client": "^2.21.1",
38+
"@halo-dev/api-client": "https://pkg.pr.new/@halo-dev/api-client@7679",
3939
"@lit/context": "^1.1.6",
4040
"@lit/localize": "^0.12.2",
4141
"@tiptap/core": "^3.2.0",

packages/comment-widget/src/base-comment-item.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ export class BaseCommentItem extends LitElement {
3535
content = '';
3636

3737
@property({ type: String })
38-
ua: string = '';
38+
ua: string | undefined;
39+
40+
@property({ type: Boolean })
41+
private: boolean | undefined;
3942

4043
@consume({ context: configMapDataContext })
4144
@state()
@@ -51,7 +54,7 @@ export class BaseCommentItem extends LitElement {
5154
></user-avatar>
5255
</div>
5356
<div class="item-main flex-[1_1_auto] min-w-0 w-full">
54-
<div class="item-meta flex items-center gap-3 flex-wrap">
57+
<div class="item-meta flex items-center gap-2 flex-wrap">
5558
${when(
5659
this.userWebsite,
5760
() => html`
@@ -69,6 +72,14 @@ export class BaseCommentItem extends LitElement {
6972
`
7073
)}
7174
75+
${when(
76+
this.private && this.configMapData?.basic.showPrivateCommentBadge,
77+
() => html`<div class="inline-flex items-center gap-1 bg-muted-3 rounded-base px-1.5 py-1">
78+
<i class="i-ri-git-repository-private-line opacity-90 size-3" aria-hidden="true"></i>
79+
<span class="text-xs text-text-2">${msg('Private')}</span>
80+
</div>`
81+
)}
82+
7283
${when(this.ua && this.configMapData?.basic.showCommenterDevice, () => html`<commenter-ua-bar .ua=${this.ua}></commenter-ua-bar>`)}
7384
7485
<time class="item-meta-info text-xs text-text-3" title=${formatDate(this.creationTime)}>

packages/comment-widget/src/base-form.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { when } from 'lit/directives/when.js';
2424
import { ofetch } from 'ofetch';
2525
import type { CommentEditor } from './comment-editor';
2626
import { cleanHtml } from './utils/html';
27+
import './base-tooltip';
2728

2829
export class BaseForm extends LitElement {
2930
@consume({ context: baseUrlContext })
@@ -65,6 +66,9 @@ export class BaseForm extends LitElement {
6566
@state()
6667
toastManager: ToastManager | undefined;
6768

69+
@property({ type: Boolean })
70+
hidePrivateCheckbox = false;
71+
6872
textareaRef: Ref<HTMLTextAreaElement> = createRef<HTMLTextAreaElement>();
6973

7074
editorRef: Ref<CommentEditor> = createRef<CommentEditor>();
@@ -151,9 +155,11 @@ export class BaseForm extends LitElement {
151155

152156
renderAccountInfo() {
153157
return html`<div class="form-account flex items-center gap-2">
154-
<div class="form-account-avatar avatar">
155-
${when(this.currentUser?.spec.avatar, () => html`<img src=${this.currentUser?.spec.avatar || ''} class="size-full object-cover" />`)}
156-
</div>
158+
${when(
159+
this.currentUser?.spec.avatar,
160+
() => html`<div class="form-account-avatar avatar"><img src=${this.currentUser?.spec.avatar || ''} class="size-full object-cover" /></div>
161+
`
162+
)}
157163
<span class="form-account-name text-base text-text-1 font-semibold">
158164
${this.currentUser?.spec.displayName || this.currentUser?.metadata.name}
159165
</span>
@@ -238,7 +244,19 @@ export class BaseForm extends LitElement {
238244
</button>
239245
`
240246
)}
241-
<div class="form-actions justify-end flex gap-2 flex-wrap items-center">
247+
<div class="form-actions justify-end flex gap-3 flex-wrap items-center">
248+
${when(
249+
!this.hidePrivateCheckbox &&
250+
this.configMapData?.basic.enablePrivateComment,
251+
() => html`<div class="flex items-center gap-2">
252+
<input id="hidden" name="hidden" type="checkbox" />
253+
<label for="hidden" class="text-xs select-none text-text-3 hover:text-text-1 transition-all">${msg('Private')}</label>
254+
<base-tooltip content=${this.currentUser ? msg('Currently logged in. After selecting the private option, comments will only be visible to yourself and the site administrator.') : msg('You are currently anonymous. After selecting the private option, the comment will only be visible to the site administrator.')}>
255+
<i class="i-mingcute:information-line size-3.5 text-text-3 block"></i>
256+
</base-tooltip>
257+
</div>`
258+
)}
259+
242260
${when(
243261
this.showCaptcha && this.captcha,
244262
() => html`
@@ -293,6 +311,7 @@ export class BaseForm extends LitElement {
293311
detail: {
294312
...data,
295313
content,
314+
hidden: data.hidden === 'on',
296315
},
297316
});
298317
this.dispatchEvent(event);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {
2+
autoUpdate,
3+
computePosition,
4+
flip,
5+
offset,
6+
shift,
7+
} from '@floating-ui/dom';
8+
import type { TemplateResult } from 'lit';
9+
import { css, html, LitElement } from 'lit';
10+
import { property } from 'lit/decorators.js';
11+
import baseStyles from './styles/base';
12+
13+
/**
14+
* A simple tooltip component based on Lit and floating-ui.
15+
*
16+
* @example
17+
* ```html
18+
* <base-tooltip content="This is a tooltip">
19+
* <button>Hover me</button>
20+
* </base-tooltip>
21+
* ```
22+
*/
23+
export class BaseTooltip extends LitElement {
24+
/**
25+
* The content to display in the tooltip.
26+
*/
27+
@property({ type: String })
28+
content: string | TemplateResult = '';
29+
30+
private tooltipEl?: HTMLElement;
31+
private cleanupFn?: () => void;
32+
33+
override connectedCallback(): void {
34+
super.connectedCallback();
35+
this.addEventListener('mouseenter', this._showTooltip);
36+
this.addEventListener('mouseleave', this._hideTooltip);
37+
}
38+
39+
override disconnectedCallback(): void {
40+
super.disconnectedCallback();
41+
this.removeEventListener('mouseenter', this._showTooltip);
42+
this.removeEventListener('mouseleave', this._hideTooltip);
43+
44+
// Clean up positioning if needed
45+
if (this.cleanupFn) {
46+
this.cleanupFn();
47+
this.cleanupFn = undefined;
48+
}
49+
}
50+
51+
override firstUpdated(): void {
52+
// Get tooltip element after the component is rendered
53+
this.tooltipEl = this.shadowRoot?.querySelector('.tooltip') as HTMLElement;
54+
}
55+
56+
private _showTooltip = (): void => {
57+
if (!this.tooltipEl) return;
58+
59+
this.tooltipEl.classList.add('show');
60+
this._updatePosition();
61+
};
62+
63+
private _hideTooltip = (): void => {
64+
if (!this.tooltipEl) return;
65+
66+
this.tooltipEl.classList.remove('show');
67+
68+
// Clean up positioning
69+
if (this.cleanupFn) {
70+
this.cleanupFn();
71+
this.cleanupFn = undefined;
72+
}
73+
};
74+
75+
private _updatePosition(): void {
76+
if (!this.tooltipEl) return;
77+
78+
const floating = this.tooltipEl;
79+
80+
// Clean up previous positioning if any
81+
if (this.cleanupFn) {
82+
this.cleanupFn();
83+
}
84+
85+
// Use autoUpdate to reposition on scroll/resize
86+
this.cleanupFn = autoUpdate(this, floating, async () => {
87+
// Calculate the position
88+
const middlewares = [
89+
offset(8), // 8px distance from trigger
90+
flip(), // flip to other side if needed
91+
shift({ padding: 5 }), // shift to keep in viewport
92+
];
93+
94+
// Calculate position
95+
const { x, y } = await computePosition(this, floating, {
96+
placement: 'top', // Default placement is top
97+
middleware: middlewares,
98+
});
99+
100+
// Position the tooltip
101+
Object.assign(floating.style, {
102+
left: `${x}px`,
103+
top: `${y}px`,
104+
});
105+
});
106+
}
107+
108+
override render() {
109+
return html`
110+
<slot></slot>
111+
<div class="tooltip">
112+
${this.content}
113+
</div>
114+
`;
115+
}
116+
117+
static override styles = [
118+
...baseStyles,
119+
css`
120+
:host {
121+
display: inline-block;
122+
}
123+
124+
::slotted(*) {
125+
display: inline-block;
126+
}
127+
128+
.tooltip {
129+
position: absolute;
130+
background: rgba(0, 0, 0, 0.8);
131+
color: white;
132+
padding: 5px 10px;
133+
border-radius: 4px;
134+
font-size: 0.875em;
135+
max-width: 300px;
136+
width: auto;
137+
z-index: 10;
138+
opacity: 0;
139+
pointer-events: none;
140+
transition: opacity 0.2s;
141+
top: 0;
142+
left: 0;
143+
white-space: normal;
144+
word-wrap: break-word;
145+
}
146+
147+
.tooltip.show {
148+
opacity: 1;
149+
}
150+
`,
151+
];
152+
}
153+
154+
customElements.get('base-tooltip') ||
155+
customElements.define('base-tooltip', BaseTooltip);
156+
157+
declare global {
158+
interface HTMLElementTagNameMap {
159+
'base-tooltip': BaseTooltip;
160+
}
161+
}

packages/comment-widget/src/comment-form.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,14 @@ export class CommentForm extends LitElement {
8282

8383
const data = e.detail;
8484

85-
const { displayName, email, website, content } = data || {};
85+
const { displayName, email, website, content, hidden } = data || {};
8686

8787
const commentRequest: CommentRequest = {
8888
raw: content,
8989
content: content,
9090
// TODO: support user input
9191
allowNotification: true,
92+
hidden: hidden || false,
9293
subjectRef: {
9394
group: this.group,
9495
kind: this.kind,

packages/comment-widget/src/comment-item.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export class CommentItem extends LitElement {
121121
.approved=${this.comment?.spec.approved}
122122
.userWebsite=${this.comment?.spec.owner.annotations?.website}
123123
.ua=${this.comment?.spec.userAgent}
124+
.private=${this.comment?.spec.hidden}
124125
>
125126
<button slot="action" class="icon-button group -ml-2" type="button" @click="${this.handleUpvote}" aria-label=${msg('Upvote')}>
126127
<div class="icon-button-icon">

packages/comment-widget/src/generated/locales/es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
's3643189d1abbb7f4': `Código`,
2323
's3fb33d17bad61aa9': `Comentario enviado con éxito, pendiente de revisión`,
2424
's4c0e15f9073382e6': `Error al obtener el código de verificación`,
25+
's5184a3f3e2f7b603': `Actualmente anónimo. Después de seleccionar la opción privada, el comentario solo será visible para el administrador del sitio.`,
2526
's523eb9043213ff0d': `Itálica`,
2627
's58a3c1ecd4dd06cf': `Tachado`,
2728
's67749057edb2586b': `Cerrar sesión`,
2829
's6cb61eeccda272d5': `Bloque de código`,
2930
's7437373e541a8037': `Apodo`,
3031
's7584ded3d749c75e': `Cargar más`,
3132
's82665b2ffabc9c0a': `Sitio web`,
33+
's838e512973be01d4': `Actualmente conectado. Después de seleccionar la opción privada, los comentarios solo serán visibles para usted y el administrador del sitio.`,
3234
's84b033b2f7360187': `Por favor, inicie sesión o complete la información primero`,
3335
's98a5f7789c49dd3f': `En revisión`,
3436
's9f2ed66340f019c6': `Escribir un comentario`,
@@ -41,6 +43,7 @@
4143
'sc8da3cc71de63832': `Iniciar sesión`,
4244
'sd1f44f1a8bc20e67': `Correo electrónico`,
4345
'sd5e242ab9574958a': `Error al comentar, por favor intente más tarde`,
46+
'se7bee6e9a9b5394c': `Íntimo`,
4447
'sea7e567ed89dc0d7': `Seleccionar emoticono`,
4548
'sf3ff78cc329d3528': `Anterior`,
4649
'sf77128b082955d42': `(O iniciar sesión)`,

packages/comment-widget/src/generated/locales/zh-CN.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
's3643189d1abbb7f4': `代码`,
2323
's3fb33d17bad61aa9': `评论成功,请等待审核`,
2424
's4c0e15f9073382e6': `获取验证码失败`,
25+
's5184a3f3e2f7b603': `当前是匿名状态,选择私密选项后,评论将仅对网站管理员可见。`,
2526
's523eb9043213ff0d': `斜体`,
2627
's58a3c1ecd4dd06cf': `删除线`,
2728
's67749057edb2586b': `退出登录`,
2829
's6cb61eeccda272d5': `代码块`,
2930
's7437373e541a8037': `昵称`,
3031
's7584ded3d749c75e': `加载更多`,
3132
's82665b2ffabc9c0a': `网站`,
33+
's838e512973be01d4': `当前已登录,选择私密选项后,评论将仅对您和网站管理员可见。`,
3234
's84b033b2f7360187': `请先登录或者完善信息`,
3335
's98a5f7789c49dd3f': `审核中`,
3436
's9f2ed66340f019c6': `编写评论`,
@@ -41,6 +43,7 @@
4143
'sc8da3cc71de63832': `登录`,
4244
'sd1f44f1a8bc20e67': `电子邮件`,
4345
'sd5e242ab9574958a': `评论失败,请稍后重试`,
46+
'se7bee6e9a9b5394c': `私密`,
4447
'sea7e567ed89dc0d7': `选择表情`,
4548
'sf3ff78cc329d3528': `上一页`,
4649
'sf77128b082955d42': `(或登录账号)`,

packages/comment-widget/src/generated/locales/zh-TW.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
's3643189d1abbb7f4': `程式碼`,
2323
's3fb33d17bad61aa9': `評論成功,請等待審核`,
2424
's4c0e15f9073382e6': `獲取驗證碼失敗`,
25+
's5184a3f3e2f7b603': `目前是匿名狀態,選擇私密選項後,評論將僅對網站管理員可見。`,
2526
's523eb9043213ff0d': `斜體`,
2627
's58a3c1ecd4dd06cf': `刪除線`,
2728
's67749057edb2586b': `登出`,
2829
's6cb61eeccda272d5': `程式碼區塊`,
2930
's7437373e541a8037': `暱稱`,
3031
's7584ded3d749c75e': `載入更多`,
3132
's82665b2ffabc9c0a': `網站`,
33+
's838e512973be01d4': `目前已登入,選擇私密選項後,評論將僅對您和網站管理員可見。`,
3234
's84b033b2f7360187': `請先登入或者完善資訊`,
3335
's98a5f7789c49dd3f': `審核中`,
3436
's9f2ed66340f019c6': `撰寫評論`,
@@ -41,6 +43,7 @@
4143
'sc8da3cc71de63832': `登入`,
4244
'sd1f44f1a8bc20e67': `電子郵件`,
4345
'sd5e242ab9574958a': `評論失敗,請稍後重試`,
46+
'se7bee6e9a9b5394c': `私密`,
4447
'sea7e567ed89dc0d7': `選擇表情`,
4548
'sf3ff78cc329d3528': `上一頁`,
4649
'sf77128b082955d42': `(或登入帳號)`,

0 commit comments

Comments
 (0)