Skip to content

Commit

Permalink
✨ 添加链接解析与预览 (#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
A-kirami authored Dec 2, 2024
1 parent 9340e25 commit 4cba5bd
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 6 deletions.
5 changes: 5 additions & 0 deletions src/adapter/onebot/v11/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
ImageContent,
AudioContent,
VideoContent,
LinkContent,
ForwardContent,
ForwardContentNode,
ContentMapping,
Expand Down Expand Up @@ -346,6 +347,10 @@ const messageBuildStrategy: MessageBuildStrategy<ContentMapping> = {
})
},

link: (content: LinkContent): TextMessage => {
return createMessage('text', { text: content.data.url })
},

forward: (): ForwardMessage => {
throw new Error('未实现消息段, 跳过')
},
Expand Down
5 changes: 5 additions & 0 deletions src/adapter/onebot/v12/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ImageContent,
AudioContent,
VideoContent,
LinkContent,
ContentMapping,
} from '~/adapter/content'
import type { Message as AdapterMessage, MessageBuildStrategy, MessageParseStrategy } from '~/adapter/message'
Expand Down Expand Up @@ -133,6 +134,10 @@ const messageBuildStrategy: MessageBuildStrategy<ContentMapping> = {
video: (content: VideoContent): VideoMessage => {
return createMessage('video', { file_id: content.data.id })
},

link: (content: LinkContent): TextMessage => {
return createMessage('text', { text: content.data.url })
},
}

const messageParseStrategy: MessageParseStrategy<MessageMapping> = {
Expand Down
3 changes: 3 additions & 0 deletions src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ declare global {
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const isWindows11: typeof import('./utils/utils')['isWindows11']
const linkMessageParse: typeof import('./utils/chat')['linkMessageParse']
const linkParse: typeof import('./utils/chat')['linkParse']
const logger: typeof import('@tauri-apps/plugin-log')
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
Expand Down Expand Up @@ -427,6 +429,7 @@ declare module 'vue' {
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isWindows11: UnwrapRef<typeof import('./utils/utils')['isWindows11']>
readonly linkMessageParse: UnwrapRef<typeof import('./utils/chat')['linkMessageParse']>
readonly logger: UnwrapRef<typeof import('@tauri-apps/plugin-log')>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
Expand Down
8 changes: 5 additions & 3 deletions src/components/chat/ChatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ const behav = new Behav()
const state = useStateStore()
const general = useGeneralSettingsStore()
const scene = $computed(() => message.scene)
const msgElements = $computed(() => general.enableLinkPreview ? linkMessageParse(scene.message) : scene.message)
const isUserMsg = $computed(() => scene.sender_id === state.user?.id)
const userName = scene.detail_type === 'group' ? scene.member.card || scene.user_name : scene.user_name
Expand Down Expand Up @@ -44,8 +48,6 @@ async function resendMessage(): Promise<void> {
async function pokeUser(): Promise<void> {
await behav.pokeUser(state.user!.id, scene.user_id, 'group_id' in scene ? scene.group_id : undefined)
}
const general = useGeneralSettingsStore()
</script>

<template>
Expand Down Expand Up @@ -94,7 +96,7 @@ const general = useGeneralSettingsStore()
:class="[$style.messageBubble, isUserMsg ? 'rounded-tr-none bg-blue-50' : 'rounded-tl-none bg-white']"
>
<!-- eslint-disable-next-line vue/valid-v-for -->
<ChatMessageElement v-for="msg in scene.message" :type="msg.type" :data="msg.data" />
<ChatMessageElement v-for="msg in msgElements" :type="msg.type" :data="msg.data" />
</div>
</ChatMessageMenu>
<div class="h-5 flex items-center gap-1 px-1">
Expand Down
4 changes: 2 additions & 2 deletions src/components/chat/ChatMessageElementLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ onBeforeMount(async () => {

<template>
<div data-type="link">
<a :href="data.url" class="break-all text-blue-400 font-medium underline hover:no-underline">{{ data.url }}</a>
<a v-if="metadata?.image" :href="metadata.url">
<a :href="data.url" target="_blank" class="break-all text-blue-400 font-medium no-underline hover:underline">{{ data.url }}</a>
<a v-if="metadata?.image" :href="metadata.url" target="_blank">
<div
class="mt-2 border border-gray-200 rounded-xl p-3 transition-colors dark:bg-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500"
>
Expand Down
9 changes: 8 additions & 1 deletion src/components/chat/ChatMessageElementText.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<script setup lang="ts">
import linkifyStr from 'linkify-string'
import type { TextContent } from '~/adapter/content'
const { data } = $defineProps<{
data: TextContent['data']
}>()
function getTextMessage(text: string): string {
return linkifyStr(text, { target: '_blank', className: 'text-blue-400 hover:underline', validate: { email: false } })
}
</script>

<template>
<span data-type="text" class="whitespace-pre-wrap break-all text-gray-800">{{ data.text }}</span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span data-type="text" class="whitespace-pre-wrap break-all text-gray-800" v-html="getTextMessage(data.text)" />
</template>
3 changes: 3 additions & 0 deletions src/stores/general-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface GeneralSettings {
enbaleSuperUser: boolean
showRecallMessage: boolean
applyAcrylicWindowEffects: boolean
enableLinkPreview: boolean
}

export const useGeneralSettingsStore = defineStore(
Expand All @@ -19,6 +20,7 @@ export const useGeneralSettingsStore = defineStore(
const enbaleSuperUser = $ref<GeneralSettings['enbaleSuperUser']>(false)
const showRecallMessage = $ref<GeneralSettings['showRecallMessage']>(true)
const applyAcrylicWindowEffects = $ref<GeneralSettings['applyAcrylicWindowEffects']>(false)
const enableLinkPreview = $ref<GeneralSettings['enableLinkPreview']>(true)

watch($$(applyAcrylicWindowEffects), async (enable) => {
await setAcrylicWindowEffect(enable)
Expand All @@ -31,6 +33,7 @@ export const useGeneralSettingsStore = defineStore(
enbaleSuperUser,
showRecallMessage,
applyAcrylicWindowEffects,
enableLinkPreview,
})
},

Expand Down
60 changes: 60 additions & 0 deletions src/utils/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,63 @@ export async function getPreviewMessage(scene: Scenes, uid?: string): Promise<st
const nickname = await getUserNickname(user_id, group_id)
return `${nickname}: ${message}`
}

/**
* 解析消息组中的链接,并将链接转换为 `link` 元素。
*
* @param contents 消息组
*
* @returns 解析后的消息组,其中文本消息中的链接被转换为 `link` 消息。
*/
export function linkMessageParse(contents: Contents[]): Contents[] {
const result: Contents[] = []

for (const element of contents) {
if (element.type === 'text') {
const { text } = element.data
const urlRegex = /https?:\/\/[^\s]+/g
let lastIndex = 0

for (const match of text.matchAll(urlRegex)) {
const url = match[0]
const startIndex = match.index
const endIndex = startIndex + url.length

// 添加链接之前的文本部分
if (startIndex > lastIndex) {
result.push({
type: 'text',
data: {
text: text.slice(lastIndex, startIndex),
},
})
}

// 添加链接
result.push({
type: 'link',
data: {
url,
},
})

lastIndex = endIndex
}

// 添加剩余的文本部分
if (lastIndex < text.length) {
result.push({
type: 'text',
data: {
text: text.slice(lastIndex),
},
})
}
} else {
// 直接添加非文本元素
result.push(element)
}
}

return result
}
12 changes: 12 additions & 0 deletions src/views/settings/general.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const generalSettingsSchema = toTypedSchema(
enbaleSuperUser: z.boolean(),
showRecallMessage: z.boolean(),
applyAcrylicWindowEffects: z.boolean(),
enableLinkPreview: z.boolean(),
}),
)
Expand Down Expand Up @@ -153,5 +154,16 @@ const osType = getOsType()
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="enableLinkPreview">
<FormItem class="max-w-120 flex flex-row items-center justify-between rounded-lg">
<div class="space-y-0.5">
<FormLabel>启用链接预览</FormLabel>
<FormDescription>预览消息中的链接内容</FormDescription>
</div>
<FormControl>
<Switch :checked="value" aria-readonly @update:checked="handleChange" />
</FormControl>
</FormItem>
</FormField>
</form>
</template>

0 comments on commit 4cba5bd

Please sign in to comment.