feat: add Stream component for message rendering and update dependencies

This commit is contained in:
tt
2025-02-23 15:30:18 +08:00
parent 4bf4f80ef9
commit 9aee552c13
12 changed files with 201 additions and 224 deletions

View File

@@ -5,8 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover,shrink-to-fit=no">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<title>悟空IM演示程序</title>
</head>

View File

@@ -11,10 +11,13 @@
"dependencies": {
"axios": "^1.4.0",
"buffer": "^6.0.3",
"highlight.js": "^11.11.1",
"marked": "^15.0.7",
"marked-highlight": "^2.2.1",
"process": "^0.11.10",
"vue": "^3.2.45",
"vue-router": "^4.0.13",
"wukongimjssdk": "^1.2.10"
"wukongimjssdk": "^1.3.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",

View File

@@ -68,6 +68,16 @@ export class ConversationWrap {
return this.conversation.simpleReminders
}
public get conversationDigest() {
if(!this.lastMessage) {
return ""
}
if(this.lastMessage.streamOn) {
return "[流消息]"
}
return this.lastMessage.content.conversationDigest
}
reloadIsMentionMe(): void {
return this.conversation.reloadIsMentionMe()
}

View File

@@ -163,7 +163,7 @@ const fetchChannelInfoIfNeed = (channel: Channel) => {
</div>
<div class="right-item2">
<div class="last-msg">
{{ conversationWrap.lastMessage?.content.conversationDigest }}
{{ conversationWrap.conversationDigest }}
</div>
<div v-if="conversationWrap.unread > 0" className="reddot">
{{ conversationWrap.unread }}

View File

@@ -4,19 +4,23 @@ import { Message, MessageContentType } from 'wukongimjssdk';
import Text from './Text.vue'
import CustomMessage from './CustomMessage.vue'
import { orderMessage } from './CustomMessage'
import Stream from './Stream.vue';
const props = defineProps<{
message: any
}>()
const contentType = props.message.content.contentType
const streamOn = props.message.streamOn
</script>
<template>
<div>
<Text :message="$props.message" v-if="contentType === MessageContentType.text"></Text>
<CustomMessage :message="$props.message" v-if="contentType === orderMessage" ></CustomMessage>
<Stream :message="$props.message" v-if="streamOn"></Stream>
<Text :message="$props.message" v-else-if="contentType === MessageContentType.text"></Text>
<CustomMessage :message="$props.message" v-else-if="contentType === orderMessage" ></CustomMessage>
</div>
</template>

View File

@@ -0,0 +1,27 @@
<template>
<div class="wk-markdown markdown-body" v-html="props.message.content?.text"></div>
</template>
<script setup lang="ts">
const props = defineProps<{
message: any
}>()
</script>
<style scoped>
.wk-markdown {
display: block;
text-align: left;
font-size: 14px;
max-width: 320px;
word-break: break-all;
overflow-y: auto;
}
</style>

View File

@@ -1,5 +1,5 @@
import { Conversation, MessageContentType, Setting } from "wukongimjssdk";
import { WKSDK, Message, StreamItem, Channel, ChannelTypePerson, ChannelTypeGroup, MessageStatus, SyncOptions, MessageExtra, MessageContent } from "wukongimjssdk";
import { WKSDK, Message, Stream, Channel, ChannelTypePerson, ChannelTypeGroup, MessageStatus, SyncOptions, MessageExtra, MessageContent } from "wukongimjssdk";
import BigNumber from "bignumber.js";
import { Buffer } from 'buffer';
export class Convert {
@@ -36,44 +36,48 @@ export class Convert {
let contentType = 0
try {
const decodedBuffer = Buffer.from(msgMap["payload"], 'base64')
const contentObj = JSON.parse(decodedBuffer.toString('utf8'))
if (contentObj) {
contentType = contentObj.type
let contentObj = null
const payload = msgMap["payload"]
if(payload && payload!=="") {
const decodedBuffer = Buffer.from(payload, 'base64')
contentObj = JSON.parse(decodedBuffer.toString('utf8'))
if (contentObj) {
contentType = contentObj.type
}
}
const messageContent = WKSDK.shared().getMessageContent(contentType)
if (contentObj) {
messageContent.decode(this.stringToUint8Array(JSON.stringify(contentObj)))
}
message.content = messageContent
}catch (error) {
console.log(error)
// 如果报错直接设置为unknown
const messageContent = WKSDK.shared().getMessageContent(MessageContentType.unknown)
message.content = messageContent
}catch(e) {
console.log(e)
// 如果报错直接设置为unknown
const messageContent = WKSDK.shared().getMessageContent(MessageContentType.unknown)
message.content = messageContent
}
message.isDeleted = msgMap["is_deleted"] === 1
const streamMaps = msgMap["streams"]
if(streamMaps && streamMaps.length>0) {
const streams = new Array<StreamItem>()
const streams = new Array<Stream>()
for (const streamMap of streamMaps) {
const streamItem = new StreamItem()
streamItem.clientMsgNo = streamMap["client_msg_no"]
streamItem.streamSeq = streamMap["stream_seq"]
if(streamMap["blob"] && streamMap["blob"].length>0) {
const blob = Buffer.from(streamMap["blob"], 'base64')
const blobObj = JSON.parse(blob.toString('utf8'))
const blobType = blobObj.type
const blobContent = WKSDK.shared().getMessageContent(contentType)
if (blobObj) {
blobContent.decode(this.stringToUint8Array(JSON.stringify(blobObj)))
const streamItem = new Stream()
streamItem.streamNo = streamMap["stream_no"]
streamItem.streamId = streamMap["stream_idstr"]
if(streamMap["payload"] && streamMap["payload"].length>0) {
const payload = Buffer.from(streamMap["payload"], 'base64')
const payloadObj = JSON.parse(payload.toString('utf8'))
const payloadType = payloadObj.type
const payloadContent = WKSDK.shared().getMessageContent(payloadType)
if (payloadObj) {
payloadContent.decode(this.stringToUint8Array(JSON.stringify(payloadObj)))
}
streamItem.clientMsgNo = streamMap["client_msg_no"]
streamItem.streamSeq = streamMap["stream_seq"]
streamItem.content = blobContent
streamItem.content = payloadContent
}
streams.push(streamItem)
}

View File

@@ -2,7 +2,7 @@
import { nextTick, onMounted, onUnmounted, ref, toRaw, toRefs, unref } from 'vue';
import APIClient from '../services/APIClient'
import { useRouter } from "vue-router";
import { WKSDK, Message, StreamItem, MessageText, Channel, ChannelTypePerson, ChannelTypeGroup, MessageStatus, SyncOptions, PullMode, MessageContent, MessageContentType, ConnectionInfo } from "wukongimjssdk";
import { WKSDK, Message, MessageText, Channel, ChannelTypePerson, ChannelTypeGroup, MessageStatus, PullMode, MessageContent, MessageContentType, ConnectionInfo, Stream, StreamListener } from "wukongimjssdk";
import { ConnectStatus, ConnectStatusListener } from 'wukongimjssdk';
import { SendackPacket, Setting } from 'wukongimjssdk';
import { Buffer } from 'buffer';
@@ -10,12 +10,33 @@ import { MessageListener, MessageStatusListener } from 'wukongimjssdk';
import Conversation from '../components/Conversation/index.vue'
import { CustomMessage, orderMessage } from '../customessage';
import MessageUI from '../messages/Message.vue';
import { Marked } from 'marked';
import { markedHighlight } from "marked-highlight";
import hljs from 'highlight.js';
const marked = new Marked(markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
}));
marked.use({
gfm: true,
});
const router = useRouter();
const chatRef = ref<HTMLElement | null>(null)
const chatRef = ref<HTMLElement | null>(null)
const showSettingPanel = ref(false)
const title = ref("")
const text = ref("")
const isComposing = ref(false) // 是否正在输入中,防中文干扰
const hasHandled = ref(false) // 是否已经处理过,防中文干扰
let msgCount = 0
const channelID = ref("") // 设置聊天的频道ID
@@ -39,6 +60,7 @@ title.value = `${uid || ""}(未连接)`
let connectStatusListener!: ConnectStatusListener
let messageListener!: MessageListener
let messageStatusListener!: MessageStatusListener
let streamListener!: StreamListener // 流监听
onMounted(() => {
@@ -95,36 +117,47 @@ const connectIM = (addr: string) => {
if (!to.value.isEqual(msg.channel)) {
return
}
if (msg.streamOn) {
let exist = false
for (const message of messages.value) {
if (message.streamNo === msg.streamNo) {
let streams = message.streams;
const newStream = new StreamItem()
newStream.clientMsgNo = msg.clientMsgNo
newStream.streamSeq = msg.streamSeq || 0
newStream.content = msg.content
if (streams && streams.length > 0) {
streams.push(newStream)
} else {
streams = [newStream]
}
message.streams = streams
exist = true
break
}
}
if (!exist) {
messages.value.push(msg)
}
} else {
messages.value.push(msg)
}
messages.value.push(msg)
scrollBottom()
}
WKSDK.shared().chatManager.addMessageListener(messageListener)
// 流监听
streamListener = (stream: Stream) => {
if (!to.value.isEqual(stream.channel)) {
return
}
for (const message of messages.value) {
if (message.streamNo === stream.streamNo) {
let streams = message.streams;
if (streams && streams.length > 0) {
streams.push(stream)
} else {
streams = [stream]
}
streams.sort((a, b) => {
if (!a.streamId || !b.streamId) {
return 0
}
if (a.streamId > b.streamId) {
return 1
} else {
return -1
}
})
message.streams = streams
message.content = streamsToMessageText(streams)
break
}
// 刷新ui
messages.value = [...messages.value]
scrollBottom()
}
}
WKSDK.shared().streamManager.addStreamListener(streamListener)
messageStatusListener = (ack: SendackPacket) => {
console.log(ack)
messages.value.forEach((m) => {
@@ -143,6 +176,7 @@ onUnmounted(() => {
WKSDK.shared().connectManager.removeConnectStatusListener(connectStatusListener)
WKSDK.shared().chatManager.removeMessageListener(messageListener)
WKSDK.shared().chatManager.removeMessageStatusListener(messageStatusListener)
WKSDK.shared().streamManager.removeStreamListener(streamListener)
WKSDK.shared().disconnect()
})
@@ -179,6 +213,14 @@ const pullLast = async () => {
limit: 15, startMessageSeq: 0, endMessageSeq: 0,
pullMode: PullMode.Up
})
// 渲染流消息
for (const m of msgs) {
if (m.streamOn && m.streams) {
m.content = streamsToMessageText(m.streams)
}
}
pulldowning.value = false
if (msgs && msgs.length > 0) {
msgs.forEach((m) => {
@@ -202,6 +244,14 @@ const pullDown = async () => {
limit: limit, startMessageSeq: firstMsg.messageSeq - 1, endMessageSeq: 0,
pullMode: PullMode.Down
})
// 渲染流消息
for (const m of msgs) {
if (m.streamOn && m.streams) {
m.content = streamsToMessageText(m.streams)
}
}
if (msgs.length < limit) {
pulldownFinished.value = true
}
@@ -298,103 +348,28 @@ const onCustomMessageSend = () => {
scrollBottom()
}
const onMessageStream = async () => {
if (!to.value || to.value.channelID === "") {
showSettingPanel.value = true
return
}
const start = !startStreamMessage.value
if (start) {
const content = JSON.stringify({
type: MessageContentType.text,
content: "我是流消息"
})
const result = await APIClient.shared.messageStreamStart({
header: {
red_dot: 1,
},
from_uid: WKSDK.shared().config.uid || "",
channel_id: to.value.channelID,
channel_type: to.value.channelType,
payload: Buffer.from(content).toString('base64')
}).catch((err) => {
alert(err.msg)
})
if (result) {
messageStreamStart(result.stream_no)
}
} else {
messageStreamEnd()
}
startStreamMessage.value = start
msgInputPlaceholder.value = start ? "现在开始你输入的消息是流式消息,多次输入试试。" : "请输入消息"
}
const messageStreamStart = (streamNoStr: string) => {
streamNo.value = streamNoStr
}
const messageStreamEnd = async () => {
await APIClient.shared.messageStreamEnd({
"stream_no": streamNo.value || "",
"channel_id": channelID.value,
"channel_type": p2p.value ? ChannelTypePerson : ChannelTypeGroup,
})
streamNo.value = undefined
}
const logout = () => {
WKSDK.shared().connectManager.disconnect()
router.push({ path: '/' })
}
const getMessageItemUI = (m: any) => {
const streams = m.streams
// 流转文本消息
const streamsToMessageText = (streams: Stream[]) => {
let text = ""
const contentType = m.contentType
// 文本消息
if (contentType == MessageContentType.text) {
const messageText = m.content as MessageText
text = messageText.text || ""
return text
}
// 自定义消息
if (contentType == orderMessage) {
const customMessage = m.content as CustomMessage
text = customMessage.title || ""
return text
}
// 流消息
if (streams && streams.length > 0) { // 流式消息拼接
for (const stream of streams) {
if (stream.content instanceof MessageText) {
const messageText = stream.content as MessageText
text = text + (messageText.text || "")
}
for (const stream of streams) {
if (stream.content instanceof MessageText) {
const messageText = stream.content as MessageText
text = text + (messageText.text || "")
}
return text
}
return "未知消息"
const htmlText = marked.parse(text)
if (htmlText) {
return new MessageText(htmlText as string)
}
}
const handleScroll = (e: any) => {
const targetScrollTop = e.target.scrollTop;
const scrollOffsetTop = e.target.scrollHeight - (targetScrollTop + e.target.clientHeight);
@@ -414,24 +389,19 @@ const handleScroll = (e: any) => {
}
const onEnter = () => {
// 中文输入法正在组合输入时,不触发
if (hasHandled.value || isComposing.value) return
onSend()
}
// const sendInputFocus = () => {
// document.body.addEventListener("touchmove", stop, {
// passive: false,
// }); // passive 参数不能省略用来兼容ios和android
// }
// const sendInputBlur = () => {
// document.body.removeEventListener("touchmove", stop);
// }
// const stop = (e:any) => {
// e.preventDefault(); // 阻止默认的处理方式(阻止下拉滑动的效果)
// };
const onKeydown = (e: any) => {
if (!isComposing.value) {
hasHandled.value = false
return
}
// 中文输入法状态下回车确认输入
hasHandled.value = true
}
</script>
<template>
@@ -490,7 +460,8 @@ const onEnter = () => {
</div>
<div class="footer">
<input :placeholder="msgInputPlaceholder" v-model="text" style="height: 40px;"
@keydown.enter="onEnter" />
@keyup.enter="onEnter" @keydown.enter="onKeydown" @compositionstart="isComposing = true"
@compositionend="isComposing = false" />
<!-- <button class="message-stream" v-on:click="onMessageStream">{{ startStreamMessage ? '停止流消息' : '开启流消息'
}}</button> -->
<button class="message-custom" v-on:click="onCustomMessageSend">自定义消息</button>

View File

@@ -411,6 +411,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
highlight.js@^11.11.1:
version "11.11.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.11.1.tgz#fca06fa0e5aeecf6c4d437239135fabc15213585"
integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -430,6 +435,16 @@ magic-string@^0.30.0:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13"
marked-highlight@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/marked-highlight/-/marked-highlight-2.2.1.tgz#33b7fcac24088c0e0213229b7d01044a2974670d"
integrity sha512-SiCIeEiQbs9TxGwle9/OwbOejHCZsohQRaNTY2u8euEXYt2rYUFoiImUirThU3Gd/o6Q1gHGtH9qloHlbJpNIA==
marked@^15.0.7:
version "15.0.7"
resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.7.tgz#f67d7e34d202ce087e6b879107b5efb04e743314"
integrity sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==
md5-typescript@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/md5-typescript/-/md5-typescript-1.0.5.tgz#68c0b24dff8e5d3162e498fa9893b63be72e038f"
@@ -558,10 +573,10 @@ vue@^3.2.45:
"@vue/server-renderer" "3.3.4"
"@vue/shared" "3.3.4"
wukongimjssdk@^1.2.10:
version "1.2.10"
resolved "https://registry.yarnpkg.com/wukongimjssdk/-/wukongimjssdk-1.2.10.tgz#2aad714d9b04ca4ceae11bb527328bbeb715cb64"
integrity sha512-MX4NJoXGV+KnxZ6kK8UwsjLWEewGQudmCGV2d4/vrtI99Z78EkfWARPyVGX3jkqX0vwDzxid2JrcQewuo3vXGA==
wukongimjssdk@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/wukongimjssdk/-/wukongimjssdk-1.3.1.tgz#fde5deb57bfb6ab3c981f9d876b03adb6340611f"
integrity sha512-ahUl5DqIRCCTTeW8qLxGYS4cmN6e3sx8Q6sVtdphLbpwvLhnmMZUf9lV9Z5tvGZ+2G41QcP4mG10z+W/derZLg==
dependencies:
"@types/bignumber.js" "^5.0.0"
"@types/crypto-js" "^4.0.2"

View File

@@ -1039,7 +1039,6 @@ func (ch *channel) syncMessages(c *wkhttp.Context) {
return
}
fmt.Println("streamItemsMap--->", streamItemsMap)
for _, message := range messageResps {
if strings.TrimSpace(message.StreamNo) == "" {
continue

View File

@@ -442,13 +442,13 @@ type taskCfg struct {
ChannelPrefix string `json:"channel_prefix"` // 频道前缀
}
func (t *taskCfg) check() error {
if t.Online <= 0 {
return errors.New("online must be greater than 0")
}
// func (t *taskCfg) check() error {
// if t.Online <= 0 {
// return errors.New("online must be greater than 0")
// }
return nil
}
// return nil
// }
type channelCfg struct {
Count int `json:"count"` // 创建频道数

View File

@@ -1,55 +0,0 @@
package api
import (
"github.com/WuKongIM/WuKongIM/pkg/wklog"
)
type system struct {
s *Server
wklog.Log
}
func newSystem(s *Server) *system {
return &system{
s: s,
Log: wklog.NewWKLog("system"),
}
}
// // Route route
// func (s *system) route(r *wkhttp.WKHttp) {
// r.GET("/system/ping", s.ping)
// }
// func (s *system) ping(c *wkhttp.Context) {
// pongs, err := service.Cluster.TestPing()
// if err != nil {
// c.ResponseError(err)
// return
// }
// results := make([]pingResult, 0, len(pongs))
// for _, pong := range pongs {
// errStr := ""
// if pong.Err != nil {
// errStr = pong.Err.Error()
// }
// results = append(results, pingResult{
// NodeId: pong.NodeId,
// Err: errStr,
// Millisecond: pong.Millisecond,
// })
// }
// sort.Slice(results, func(i, j int) bool {
// return results[i].NodeId < results[j].NodeId
// })
// c.JSON(http.StatusOK, results)
// }
type pingResult struct {
NodeId uint64 `json:"node_id"`
Err string `json:"err"`
Millisecond int64 `json:"millisecond"`
}