Skip to content

Commit ccae3d2

Browse files
authored
feat: use gravatar source as comment's avatar (#111)
后端改动:halo-dev/halo#5642 Fixes #97 Fixes #77 两个不同邮箱,相同头像 效果: ![image](https://github.com/halo-dev/plugin-comment-widget/assets/52254895/3b61eaec-4cca-4425-8735-827d3f06ba72) ```release-note 评论头像支持使用 Gravatar 源 ```
1 parent 3965522 commit ccae3d2

12 files changed

Lines changed: 291 additions & 8 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { CommentVo, ReplyVo } from '@halo-dev/api-client';
2+
import { getAvatarProvider } from './providers';
3+
4+
abstract class AvatarPolicy {
5+
abstract applyCommentPolicy(comment: CommentVo | undefined): string | undefined;
6+
abstract applyReplyPolicy(reply: ReplyVo | undefined): string | undefined;
7+
}
8+
9+
let policyInstance: AvatarPolicy | undefined;
10+
const emailKind = 'Email';
11+
const emailHash = 'email-hash';
12+
13+
class AnonymousUserPolicy extends AvatarPolicy {
14+
applyCommentPolicy(comment: CommentVo | undefined): string | undefined {
15+
const avatarProvider = getAvatarProvider();
16+
const isAnonymous = comment?.owner.kind === emailKind;
17+
if (isAnonymous) {
18+
return avatarProvider?.getAvatarSrc(comment?.spec.owner.annotations?.[emailHash]);
19+
}
20+
return comment?.owner.avatar;
21+
}
22+
applyReplyPolicy(reply: ReplyVo | undefined): string | undefined {
23+
const avatarProvider = getAvatarProvider();
24+
const isAnonymous = reply?.owner.kind === emailKind;
25+
if (isAnonymous) {
26+
return avatarProvider?.getAvatarSrc(reply?.spec.owner.annotations?.[emailHash]);
27+
}
28+
return reply?.owner.avatar;
29+
}
30+
}
31+
32+
class AllUserPolicy extends AvatarPolicy {
33+
applyCommentPolicy(comment: CommentVo | undefined): string | undefined {
34+
const avatarProvider = getAvatarProvider();
35+
return avatarProvider?.getAvatarSrc(comment?.spec.owner.annotations?.[emailHash]);
36+
}
37+
applyReplyPolicy(reply: ReplyVo | undefined): string | undefined {
38+
const avatarProvider = getAvatarProvider();
39+
return avatarProvider?.getAvatarSrc(reply?.spec.owner.annotations?.[emailHash]);
40+
}
41+
}
42+
43+
class NoAvatarUserPolicy extends AvatarPolicy {
44+
applyCommentPolicy(comment: CommentVo | undefined): string | undefined {
45+
const avatarProvider = getAvatarProvider();
46+
const isAnonymous = comment?.owner.kind === emailKind;
47+
const avatar = comment?.owner.avatar;
48+
if (isAnonymous || !avatar) {
49+
return avatarProvider?.getAvatarSrc(comment?.spec.owner.annotations?.[emailHash]);
50+
}
51+
return avatar;
52+
}
53+
applyReplyPolicy(reply: ReplyVo | undefined): string | undefined {
54+
const avatarProvider = getAvatarProvider();
55+
const isAnonymous = reply?.owner.kind === emailKind;
56+
const avatar = reply?.owner.avatar;
57+
if (isAnonymous || !avatar) {
58+
return avatarProvider?.getAvatarSrc(reply?.spec.owner.annotations?.[emailHash]);
59+
}
60+
return avatar;
61+
}
62+
}
63+
64+
enum AvatarPolicyEnum {
65+
ANONYMOUS_USER_POLICY = 'anonymousUser',
66+
ALL_USER_POLICY = 'allUser',
67+
NO_AVATAR_USER_POLICY = 'noAvatarUser',
68+
}
69+
70+
function setPolicyInstance(nPolicyInstance: AvatarPolicy | undefined) {
71+
policyInstance = nPolicyInstance;
72+
}
73+
74+
function getPolicyInstance(): AvatarPolicy | undefined {
75+
return policyInstance;
76+
}
77+
78+
export {
79+
AnonymousUserPolicy,
80+
AllUserPolicy,
81+
NoAvatarUserPolicy,
82+
AvatarPolicyEnum,
83+
setPolicyInstance,
84+
getPolicyInstance,
85+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export default abstract class AvatarProvider {
2+
private readonly _name: string;
3+
private _url: string;
4+
5+
constructor(name: string, url: string) {
6+
this._name = name;
7+
this._url = url;
8+
}
9+
10+
get url(): string {
11+
return this._url;
12+
}
13+
14+
set url(value: string) {
15+
this._url = value;
16+
}
17+
18+
get name(): string {
19+
return this._name;
20+
}
21+
22+
abstract getAvatarSrc(emailHash: string | undefined): string;
23+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import AvatarProvider from './avatar-provider';
2+
3+
class Gravatar extends AvatarProvider {
4+
override getAvatarSrc(emailHash: string | undefined): string {
5+
return `${this.url}/avatar/${emailHash}`;
6+
}
7+
}
8+
9+
export default new Gravatar('Gravatar', 'https://gravatar.com');
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Gravatar from './gravatar';
2+
import AvatarProvider from './avatar-provider';
3+
4+
let avatarProvider: AvatarProvider | undefined;
5+
6+
enum AvatarProviderEnum {
7+
GRAVATAR = 'gravatar',
8+
}
9+
10+
export function setAvatarProvider(provider: string, mirrorUrl?: string) {
11+
switch (provider) {
12+
case AvatarProviderEnum.GRAVATAR:
13+
if (mirrorUrl) {
14+
Gravatar.url = mirrorUrl;
15+
}
16+
avatarProvider = Gravatar;
17+
break;
18+
default:
19+
}
20+
}
21+
22+
export function getAvatarProvider() {
23+
return avatarProvider;
24+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LS_UPVOTED_COMMENTS_KEY } from './constant';
1212
import varStyles from './styles/var';
1313
import { Ref, createRef, ref } from 'lit/directives/ref.js';
1414
import { CommentReplies } from './comment-replies';
15+
import { getPolicyInstance } from './avatar/avatar-policy';
1516

1617
export class CommentItem extends LitElement {
1718
@consume({ context: baseUrlContext })
@@ -103,7 +104,7 @@ export class CommentItem extends LitElement {
103104

104105
override render() {
105106
return html`<base-comment-item
106-
.userAvatar="${this.comment?.owner.avatar}"
107+
.userAvatar="${handleCommentAvatar(this.comment)}"
107108
.userDisplayName="${this.comment?.owner.displayName}"
108109
.content="${this.comment?.spec.content || ''}"
109110
.creationTime="${this.comment?.spec.creationTime}"
@@ -233,6 +234,14 @@ export class CommentItem extends LitElement {
233234

234235
customElements.get('comment-item') || customElements.define('comment-item', CommentItem);
235236

237+
function handleCommentAvatar(comment: CommentVo | undefined): string | undefined {
238+
const avatarPolicy = getPolicyInstance();
239+
if (avatarPolicy === undefined) {
240+
return comment?.owner.avatar;
241+
}
242+
return avatarPolicy.applyCommentPolicy(comment);
243+
}
244+
236245
declare global {
237246
interface HTMLElementTagNameMap {
238247
'comment-item': CommentItem;

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { repeat } from 'lit/directives/repeat.js';
55
import baseStyles from './styles/base';
66
import { provide } from '@lit/context';
77
import {
8-
allowAnonymousCommentsContext,
98
baseUrlContext,
109
currentUserContext,
1110
emojiDataUrlContext,
@@ -16,12 +15,25 @@ import {
1615
toastContext,
1716
versionContext,
1817
withRepliesContext,
18+
allowAnonymousCommentsContext,
19+
useAvatarProviderContext,
20+
avatarPolicyContext,
21+
avatarProviderContext,
22+
avatarProviderMirrorContext,
1923
} from './context';
2024
import './comment-form';
2125
import './comment-item';
2226
import './comment-pagination';
2327
import varStyles from './styles/var';
2428
import { ToastManager } from './lit-toast';
29+
import {
30+
AnonymousUserPolicy,
31+
AllUserPolicy,
32+
NoAvatarUserPolicy,
33+
AvatarPolicyEnum,
34+
setPolicyInstance,
35+
} from './avatar/avatar-policy';
36+
import { setAvatarProvider } from './avatar/providers';
2537

2638
export class CommentWidget extends LitElement {
2739
@provide({ context: baseUrlContext })
@@ -58,6 +70,22 @@ export class CommentWidget extends LitElement {
5870
@property({ type: Number, attribute: 'with-reply-size' })
5971
withReplySize = 10;
6072

73+
@provide({ context: useAvatarProviderContext })
74+
@property({ type: Boolean, attribute: 'use-avatar-provider' })
75+
useAvatarProvider = false;
76+
77+
@provide({ context: avatarProviderContext })
78+
@property({ type: String, attribute: 'avatar-provider' })
79+
avatarProvider = '';
80+
81+
@provide({ context: avatarProviderMirrorContext })
82+
@property({ type: String, attribute: 'avatar-provider-mirror' })
83+
avatarProviderMirror = '';
84+
85+
@provide({ context: avatarPolicyContext })
86+
@property({ type: String, attribute: 'avatar-policy' })
87+
avatarPolicy = '';
88+
6189
@provide({ context: emojiDataUrlContext })
6290
@property({ type: String, attribute: 'emoji-data-url' })
6391
emojiDataUrl = 'https://unpkg.com/@emoji-mart/data';
@@ -203,12 +231,42 @@ export class CommentWidget extends LitElement {
203231
await this.fetchComments({ scrollIntoView: true });
204232
}
205233

234+
initAvatarProvider() {
235+
if (!this.useAvatarProvider) {
236+
return;
237+
}
238+
setAvatarProvider(this.avatarProvider, this.avatarProviderMirror);
239+
}
240+
241+
initAvatarPolicy() {
242+
if (!this.useAvatarProvider) {
243+
console.log(this.useAvatarProvider);
244+
setPolicyInstance(undefined);
245+
return;
246+
}
247+
switch (this.avatarPolicy) {
248+
case AvatarPolicyEnum.ALL_USER_POLICY: {
249+
setPolicyInstance(new AllUserPolicy());
250+
break;
251+
}
252+
case AvatarPolicyEnum.NO_AVATAR_USER_POLICY: {
253+
setPolicyInstance(new NoAvatarUserPolicy());
254+
break;
255+
}
256+
case AvatarPolicyEnum.ANONYMOUS_USER_POLICY:
257+
default:
258+
setPolicyInstance(new AnonymousUserPolicy());
259+
}
260+
}
261+
206262
override connectedCallback(): void {
207263
super.connectedCallback();
208264
this.toastManager = new ToastManager();
209265
this.fetchCurrentUser();
210266
this.fetchComments();
211267
this.fetchGlobalInfo();
268+
this.initAvatarProvider();
269+
this.initAvatarPolicy();
212270
}
213271

214272
static override styles = [

packages/comment-widget/src/context/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export const nameContext = createContext<string>(Symbol('name'));
99
export const versionContext = createContext<string>(Symbol('version'));
1010
export const replySizeContext = createContext<number>(Symbol('replySize'));
1111
export const withRepliesContext = createContext<boolean>(Symbol('withReplies'));
12+
export const useAvatarProviderContext = createContext<boolean>(Symbol('useAvatarProvider'));
13+
export const avatarProviderContext = createContext<string>(Symbol('avatarProvider'));
14+
export const avatarProviderMirrorContext = createContext<string>(Symbol('avatarProviderMirror'));
15+
export const avatarPolicyContext = createContext<string>(Symbol('avatarPolicy'));
1216

1317
export const allowAnonymousCommentsContext = createContext<boolean>(
1418
Symbol('allowAnonymousComments')

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { LS_UPVOTED_REPLIES_KEY } from './constant';
1010
import { consume } from '@lit/context';
1111
import { baseUrlContext } from './context';
1212
import varStyles from './styles/var';
13+
import { getPolicyInstance } from './avatar/avatar-policy';
1314

1415
export class ReplyItem extends LitElement {
1516
@consume({ context: baseUrlContext })
@@ -106,7 +107,7 @@ export class ReplyItem extends LitElement {
106107
override render() {
107108
return html`
108109
<base-comment-item
109-
.userAvatar="${this.reply?.owner.avatar}"
110+
.userAvatar="${handleReplyAvatar(this.reply)}"
110111
.userDisplayName="${this.reply?.owner.displayName}"
111112
.content="${this.reply?.spec.content || ''}"
112113
.creationTime="${this.reply?.metadata.creationTimestamp ?? undefined}"
@@ -248,6 +249,14 @@ export class ReplyItem extends LitElement {
248249

249250
customElements.get('reply-item') || customElements.define('reply-item', ReplyItem);
250251

252+
function handleReplyAvatar(reply: ReplyVo | undefined): string | undefined {
253+
const avatarPolicy = getPolicyInstance();
254+
if (avatarPolicy === undefined) {
255+
return reply?.owner.avatar;
256+
}
257+
return avatarPolicy.applyReplyPolicy(reply);
258+
}
259+
251260
declare global {
252261
interface HTMLElementTagNameMap {
253262
'reply-item': ReplyItem;

packages/widget/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ interface Props {
1111
replySize?: number;
1212
withReplies?: boolean;
1313
withReplySize?: number;
14+
useAvatarProvider: boolean;
15+
avatarProvider?: string;
16+
avatarProviderMirror?: string;
17+
avatarPolicy?: string;
1418
}
1519

1620
export function init(el: string, props: Props) {
@@ -34,6 +38,10 @@ export function init(el: string, props: Props) {
3438
commentWidget.withReplySize = props.withReplySize || 10;
3539
commentWidget.emojiDataUrl =
3640
'/plugins/PluginCommentWidget/assets/static/emoji/native.json';
41+
commentWidget.useAvatarProvider = props.useAvatarProvider || false;
42+
commentWidget.avatarProvider = props.avatarProvider || '';
43+
commentWidget.avatarProviderMirror = props.avatarProviderMirror || '';
44+
commentWidget.avatarPolicy = props.avatarPolicy || '';
3745

3846
const observer = new IntersectionObserver((entries) => {
3947
entries.forEach((entry) => {

src/main/java/run/halo/comment/widget/CommentWidgetPlugin.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import org.pf4j.PluginWrapper;
44
import org.springframework.stereotype.Component;
5-
import run.halo.app.extension.SchemeManager;
65
import run.halo.app.plugin.BasePlugin;
76

87
/**
@@ -11,10 +10,10 @@
1110
*/
1211
@Component
1312
public class CommentWidgetPlugin extends BasePlugin {
14-
1513
public CommentWidgetPlugin(PluginWrapper wrapper) {
1614
super(wrapper);
1715
}
16+
1817
@Override
1918
public void start() {
2019
}

0 commit comments

Comments
 (0)