From c5413064942bef116d0a9407cca0f9f1f08da9a2 Mon Sep 17 00:00:00 2001
From: Tim <135014430+nagisa77@users.noreply.github.com>
Date: Thu, 31 Jul 2025 17:57:25 +0800
Subject: [PATCH 1/4] Add mention suggestions and admin list
---
open-isle-cli/src/utils/user.js | 30 ++++++++++++++
open-isle-cli/src/utils/vditor.js | 30 +++++++++++++-
open-isle-cli/src/views/MessagePageView.vue | 39 ++++++++++++++++++-
.../openisle/controller/UserController.java | 10 +++++
.../com/openisle/model/NotificationType.java | 4 +-
.../com/openisle/service/CommentService.java | 2 +
.../openisle/service/NotificationService.java | 25 ++++++++++++
.../com/openisle/service/PostService.java | 2 +
.../com/openisle/service/UserService.java | 7 ++++
9 files changed, 146 insertions(+), 3 deletions(-)
create mode 100644 open-isle-cli/src/utils/user.js
diff --git a/open-isle-cli/src/utils/user.js b/open-isle-cli/src/utils/user.js
new file mode 100644
index 000000000..a7469067e
--- /dev/null
+++ b/open-isle-cli/src/utils/user.js
@@ -0,0 +1,30 @@
+import { API_BASE_URL } from '../main'
+
+export async function fetchFollowings(username) {
+ if (!username) return []
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/users/${username}/following`)
+ return res.ok ? await res.json() : []
+ } catch (e) {
+ return []
+ }
+}
+
+export async function fetchAdmins() {
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/users/admins`)
+ return res.ok ? await res.json() : []
+ } catch (e) {
+ return []
+ }
+}
+
+export async function searchUsers(keyword) {
+ if (!keyword) return []
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/search/users?keyword=${encodeURIComponent(keyword)}`)
+ return res.ok ? await res.json() : []
+ } catch (e) {
+ return []
+ }
+}
diff --git a/open-isle-cli/src/utils/vditor.js b/open-isle-cli/src/utils/vditor.js
index 2a46a0a52..3b1a5b755 100644
--- a/open-isle-cli/src/utils/vditor.js
+++ b/open-isle-cli/src/utils/vditor.js
@@ -1,7 +1,8 @@
import Vditor from 'vditor'
import 'vditor/dist/index.css'
import { API_BASE_URL } from '../main'
-import { getToken } from './auth'
+import { getToken, authState } from './auth'
+import { searchUsers, fetchFollowings, fetchAdmins } from './user'
export function getEditorTheme() {
return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'classic'
@@ -19,11 +20,38 @@ export function createVditor(editorId, options = {}) {
after
} = options
+ const fetchMentions = async (value) => {
+ if (!value) {
+ const [followings, admins] = await Promise.all([
+ fetchFollowings(authState.username),
+ fetchAdmins()
+ ])
+ const combined = [...followings, ...admins]
+ const seen = new Set()
+ return combined.filter(u => {
+ if (seen.has(u.id)) return false
+ seen.add(u.id)
+ return true
+ })
+ }
+ return searchUsers(value)
+ }
+
return new Vditor(editorId, {
placeholder,
height: 'auto',
theme: getEditorTheme(),
preview: Object.assign({ theme: { current: getPreviewTheme() } }, preview),
+ hint: {
+ delay: 200,
+ at: async (value) => {
+ const list = await fetchMentions(value)
+ return list.map(u => ({
+ value: u.username,
+ html: `@${u.username}`
+ }))
+ }
+ },
cdn: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/vditor',
toolbar: [
'emoji',
diff --git a/open-isle-cli/src/views/MessagePageView.vue b/open-isle-cli/src/views/MessagePageView.vue
index 93b7c64d2..675fbfc6f 100644
--- a/open-isle-cli/src/views/MessagePageView.vue
+++ b/open-isle-cli/src/views/MessagePageView.vue
@@ -147,6 +147,29 @@
+
+
+
+ {{ item.fromUser.username }}
+
+ 在评论中提到了你:
+
+ {{ sanitizeDescription(item.comment.content) }}
+
+
+
+
+
+
+ {{ item.fromUser.username }}
+
+ 在帖子
+
+ {{ sanitizeDescription(item.post.title) }}
+
+ 中提到了你
+
+
@@ -327,7 +350,8 @@ export default {
POST_SUBSCRIBED: 'fas fa-bookmark',
POST_UNSUBSCRIBED: 'fas fa-bookmark',
REGISTER_REQUEST: 'fas fa-user-clock',
- ACTIVITY_REDEEM: 'fas fa-coffee'
+ ACTIVITY_REDEEM: 'fas fa-coffee',
+ MENTION: 'fas fa-at'
}
const reactionEmojiMap = {
@@ -420,6 +444,17 @@ export default {
router.push(`/users/${n.comment.author.id}`)
}
})
+ } else if (n.type === 'MENTION') {
+ notifications.value.push({
+ ...n,
+ icon: iconMap[n.type],
+ iconClick: () => {
+ if (n.fromUser) {
+ markRead(n.id)
+ router.push(`/users/${n.fromUser.id}`)
+ }
+ }
+ })
} else if (n.type === 'USER_FOLLOWED' || n.type === 'USER_UNFOLLOWED') {
notifications.value.push({
...n,
@@ -539,6 +574,8 @@ export default {
return '有人取消关注你'
case 'USER_ACTIVITY':
return '关注的用户有新动态'
+ case 'MENTION':
+ return '有人提到了你'
default:
return t
}
diff --git a/src/main/java/com/openisle/controller/UserController.java b/src/main/java/com/openisle/controller/UserController.java
index 4809e0e67..1aef3a650 100644
--- a/src/main/java/com/openisle/controller/UserController.java
+++ b/src/main/java/com/openisle/controller/UserController.java
@@ -196,6 +196,16 @@ public class UserController {
.collect(java.util.stream.Collectors.toList());
}
+ /**
+ * List all administrator users.
+ */
+ @GetMapping("/admins")
+ public java.util.List admins() {
+ return userService.getAdmins().stream()
+ .map(this::toDto)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
@GetMapping("/{identifier}/all")
public ResponseEntity userAggregate(@PathVariable("identifier") String identifier,
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
diff --git a/src/main/java/com/openisle/model/NotificationType.java b/src/main/java/com/openisle/model/NotificationType.java
index fb5672af3..8679530ff 100644
--- a/src/main/java/com/openisle/model/NotificationType.java
+++ b/src/main/java/com/openisle/model/NotificationType.java
@@ -31,5 +31,7 @@ public enum NotificationType {
/** A user requested registration approval */
REGISTER_REQUEST,
/** A user redeemed an activity reward */
- ACTIVITY_REDEEM
+ ACTIVITY_REDEEM,
+ /** You were mentioned in a post or comment */
+ MENTION
}
diff --git a/src/main/java/com/openisle/service/CommentService.java b/src/main/java/com/openisle/service/CommentService.java
index 49d11c037..ce74c7d75 100644
--- a/src/main/java/com/openisle/service/CommentService.java
+++ b/src/main/java/com/openisle/service/CommentService.java
@@ -64,6 +64,7 @@ public class CommentService {
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null, null, null);
}
}
+ notificationService.notifyMentions(content, author, post, comment);
return comment;
}
@@ -102,6 +103,7 @@ public class CommentService {
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment, null, null, null, null);
}
}
+ notificationService.notifyMentions(content, author, parent.getPost(), comment);
return comment;
}
diff --git a/src/main/java/com/openisle/service/NotificationService.java b/src/main/java/com/openisle/service/NotificationService.java
index 2e5af02b4..a1bf8e5bc 100644
--- a/src/main/java/com/openisle/service/NotificationService.java
+++ b/src/main/java/com/openisle/service/NotificationService.java
@@ -11,6 +11,11 @@ import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.util.Set;
+import java.util.HashSet;
+
import java.util.List;
import java.util.concurrent.Executor;
@@ -28,6 +33,8 @@ public class NotificationService {
@Value("${app.website-url}")
private String websiteUrl;
+ private static final Pattern MENTION_PATTERN = Pattern.compile("@([A-Za-z0-9_]+)");
+
private String buildPayload(String body, String url) {
// try {
// return new ObjectMapper().writeValueAsString(Map.of(
@@ -144,4 +151,22 @@ public class NotificationService {
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
return notificationRepository.countByUserAndRead(user, false);
}
+
+ public void notifyMentions(String content, User fromUser, Post post, Comment comment) {
+ if (content == null || fromUser == null) {
+ return;
+ }
+ Matcher matcher = MENTION_PATTERN.matcher(content);
+ Set names = new HashSet<>();
+ while (matcher.find()) {
+ names.add(matcher.group(1));
+ }
+ for (String name : names) {
+ userRepository.findByUsername(name).ifPresent(target -> {
+ if (!target.getId().equals(fromUser.getId())) {
+ createNotification(target, NotificationType.MENTION, post, comment, null, fromUser, null, null);
+ }
+ });
+ }
+ }
}
diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java
index e1a05b34e..2e124f3f4 100644
--- a/src/main/java/com/openisle/service/PostService.java
+++ b/src/main/java/com/openisle/service/PostService.java
@@ -139,6 +139,7 @@ public class PostService {
null);
}
}
+ notificationService.notifyMentions(content, author, post, null);
return post;
}
@@ -407,6 +408,7 @@ public class PostService {
post.setTags(new java.util.HashSet<>(tags));
Post updated = postRepository.save(post);
imageUploader.adjustReferences(oldContent, content);
+ notificationService.notifyMentions(content, user, updated, null);
return updated;
}
diff --git a/src/main/java/com/openisle/service/UserService.java b/src/main/java/com/openisle/service/UserService.java
index 5872791c8..8e6a1d5ed 100644
--- a/src/main/java/com/openisle/service/UserService.java
+++ b/src/main/java/com/openisle/service/UserService.java
@@ -185,4 +185,11 @@ public class UserService {
user.setPassword(passwordEncoder.encode(newPassword));
return userRepository.save(user);
}
+
+ /**
+ * Get all administrator accounts.
+ */
+ public java.util.List getAdmins() {
+ return userRepository.findByRole(Role.ADMIN);
+ }
}
From 4fcc47aa405d825be76515fb2a97c670285066d3 Mon Sep 17 00:00:00 2001
From: Tim
Date: Thu, 31 Jul 2025 19:53:21 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20@=20=E6=A0=B7?=
=?UTF-8?q?=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
open-isle-cli/package-lock.json | 8 ++++----
open-isle-cli/package.json | 10 +++++-----
open-isle-cli/src/utils/vditor.js | 20 +++++++++++--------
.../openisle/controller/SearchController.java | 2 ++
4 files changed, 23 insertions(+), 17 deletions(-)
diff --git a/open-isle-cli/package-lock.json b/open-isle-cli/package-lock.json
index e2e900c42..53f445be1 100644
--- a/open-isle-cli/package-lock.json
+++ b/open-isle-cli/package-lock.json
@@ -12,7 +12,7 @@
"echarts": "^5.6.0",
"ldrs": "^1.1.7",
"markdown-it": "^14.1.0",
- "vditor": "^3.8.7",
+ "vditor": "^3.11.1",
"vue": "^3.2.13",
"vue-easy-lightbox": "^1.19.0",
"vue-echarts": "^7.0.3",
@@ -11330,9 +11330,9 @@
}
},
"node_modules/vditor": {
- "version": "3.8.7",
- "resolved": "https://registry.npmjs.org/vditor/-/vditor-3.8.7.tgz",
- "integrity": "sha512-7loYmcj1TCkJV86qwstxzAy33GweFKdJnpiSzGGPqUzDaOcUAiaiFAjIQjxwJ3/sx7pNZf1NWLhXKabXEN7xzQ==",
+ "version": "3.11.1",
+ "resolved": "https://registry.npmjs.org/vditor/-/vditor-3.11.1.tgz",
+ "integrity": "sha512-7rjNSXYVyZG0mVZpUG2tfxwnoNtkcRCnwdSju+Zvpjf/r72iQa6kLpeThFMIKPuQ5CRnQQv6gnR3eNU6UGbC2Q==",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5"
diff --git a/open-isle-cli/package.json b/open-isle-cli/package.json
index 979bcfd71..ed41c14b4 100644
--- a/open-isle-cli/package.json
+++ b/open-isle-cli/package.json
@@ -9,15 +9,15 @@
},
"dependencies": {
"core-js": "^3.8.3",
+ "echarts": "^5.6.0",
"ldrs": "^1.1.7",
"markdown-it": "^14.1.0",
- "vditor": "^3.8.7",
+ "vditor": "^3.11.1",
"vue": "^3.2.13",
- "vue-router": "^4.5.1",
- "vue-toastification": "^2.0.0-rc.5",
"vue-easy-lightbox": "^1.19.0",
- "echarts": "^5.6.0",
- "vue-echarts": "^7.0.3"
+ "vue-echarts": "^7.0.3",
+ "vue-router": "^4.5.1",
+ "vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@babel/core": "^7.12.16",
diff --git a/open-isle-cli/src/utils/vditor.js b/open-isle-cli/src/utils/vditor.js
index 3b1a5b755..5592d017b 100644
--- a/open-isle-cli/src/utils/vditor.js
+++ b/open-isle-cli/src/utils/vditor.js
@@ -43,14 +43,18 @@ export function createVditor(editorId, options = {}) {
theme: getEditorTheme(),
preview: Object.assign({ theme: { current: getPreviewTheme() } }, preview),
hint: {
- delay: 200,
- at: async (value) => {
- const list = await fetchMentions(value)
- return list.map(u => ({
- value: u.username,
- html: `@${u.username}`
- }))
- }
+ extend: [
+ {
+ key: '@',
+ hint: async (key) => {
+ const list = await fetchMentions(key)
+ return list.map(u => ({
+ value: `@[${u.username}]`,
+ html: `
@${u.username}`
+ }))
+ },
+ },
+ ],
},
cdn: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/vditor',
toolbar: [
diff --git a/src/main/java/com/openisle/controller/SearchController.java b/src/main/java/com/openisle/controller/SearchController.java
index dbc2d16f9..067789d83 100644
--- a/src/main/java/com/openisle/controller/SearchController.java
+++ b/src/main/java/com/openisle/controller/SearchController.java
@@ -68,6 +68,7 @@ public class SearchController {
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
+ dto.setAvatar(user.getAvatar());
return dto;
}
@@ -82,6 +83,7 @@ public class SearchController {
private static class UserDto {
private Long id;
private String username;
+ private String avatar;
}
@Data
From d4c1ad54fc84571bf514949653b5d7215710a682 Mon Sep 17 00:00:00 2001
From: Tim <135014430+nagisa77@users.noreply.github.com>
Date: Thu, 31 Jul 2025 20:03:23 +0800
Subject: [PATCH 3/4] Add mention link rendering in Markdown
---
open-isle-cli/src/utils/markdown.js | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/open-isle-cli/src/utils/markdown.js b/open-isle-cli/src/utils/markdown.js
index 227c751ff..a3ed32ec7 100644
--- a/open-isle-cli/src/utils/markdown.js
+++ b/open-isle-cli/src/utils/markdown.js
@@ -3,6 +3,30 @@ import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
import { toast } from '../main'
+function mentionPlugin(md) {
+ const mentionReg = /^@\[([^\]]+)\]/
+ function mention(state, silent) {
+ const pos = state.pos
+ if (state.src.charCodeAt(pos) !== 0x40) return false
+ const match = mentionReg.exec(state.src.slice(pos))
+ if (!match) return false
+ if (!silent) {
+ const tokenOpen = state.push('link_open', 'a', 1)
+ tokenOpen.attrs = [
+ ['href', `/users/${match[1]}`],
+ ['target', '_blank'],
+ ['class', 'mention-link']
+ ]
+ const text = state.push('text', '', 0)
+ text.content = `@${match[1]}`
+ state.push('link_close', 'a', -1)
+ }
+ state.pos += match[0].length
+ return true
+ }
+ md.inline.ruler.before('emphasis', 'mention', mention)
+}
+
const md = new MarkdownIt({
html: false,
linkify: true,
@@ -18,6 +42,8 @@ const md = new MarkdownIt({
}
})
+md.use(mentionPlugin)
+
export function renderMarkdown(text) {
return md.render(text || '')
}
From 8d3bc728c5c66de1beb576bd1271fc701616cc47 Mon Sep 17 00:00:00 2001
From: Tim
Date: Thu, 31 Jul 2025 20:22:44 +0800
Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E8=87=AA?=
=?UTF-8?q?=E6=B5=8B=E9=80=9A=E8=BF=87?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/main/java/com/openisle/service/NotificationService.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/openisle/service/NotificationService.java b/src/main/java/com/openisle/service/NotificationService.java
index a1bf8e5bc..561b59b28 100644
--- a/src/main/java/com/openisle/service/NotificationService.java
+++ b/src/main/java/com/openisle/service/NotificationService.java
@@ -33,7 +33,7 @@ public class NotificationService {
@Value("${app.website-url}")
private String websiteUrl;
- private static final Pattern MENTION_PATTERN = Pattern.compile("@([A-Za-z0-9_]+)");
+ private static final Pattern MENTION_PATTERN = Pattern.compile("@\\[([^\\]]+)\\]");
private String buildPayload(String body, String url) {
// try {