Merge pull request #292 from nagisa77/n67uug-codex/add-user-mention-feature

Add mention notification feature
This commit is contained in:
Tim
2025-07-31 20:24:23 +08:00
committed by GitHub
13 changed files with 187 additions and 12 deletions

View File

@@ -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 || '')
}

View File

@@ -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 []
}
}

View File

@@ -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,42 @@ 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: {
extend: [
{
key: '@',
hint: async (key) => {
const list = await fetchMentions(key)
return list.map(u => ({
value: `@[${u.username}]`,
html: `<img src="${u.avatar}" /> @${u.username}`
}))
},
},
],
},
cdn: 'https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/vditor',
toolbar: [
'emoji',

View File

@@ -147,6 +147,29 @@
</router-link>
</NotificationContainer>
</template>
<template v-else-if="item.type === 'MENTION' && item.comment">
<NotificationContainer :item="item" :markRead="markRead">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
{{ item.fromUser.username }}
</router-link>
在评论中提到了你
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}#comment-${item.comment.id}`">
{{ sanitizeDescription(item.comment.content) }}
</router-link>
</NotificationContainer>
</template>
<template v-else-if="item.type === 'MENTION'">
<NotificationContainer :item="item" :markRead="markRead">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
{{ item.fromUser.username }}
</router-link>
在帖子
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/posts/${item.post.id}`">
{{ sanitizeDescription(item.post.title) }}
</router-link>
中提到了你
</NotificationContainer>
</template>
<template v-else-if="item.type === 'USER_FOLLOWED'">
<NotificationContainer :item="item" :markRead="markRead">
<router-link class="notif-content-text" @click="markRead(item.id)" :to="`/users/${item.fromUser.id}`">
@@ -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 = {
@@ -416,6 +440,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,
@@ -535,6 +570,8 @@ export default {
return '有人取消关注你'
case 'USER_ACTIVITY':
return '关注的用户有新动态'
case 'MENTION':
return '有人提到了你'
default:
return t
}