diff --git a/blivedm b/blivedm index efc7f3b..87651f0 160000 --- a/blivedm +++ b/blivedm @@ -1 +1 @@ -Subproject commit efc7f3b3f8175ad05135b9819f86ed6fd561c674 +Subproject commit 87651f044c5605fff08d0caffa012b4f89259353 diff --git a/frontend/src/components/ChatRenderer/constants.js b/frontend/src/components/ChatRenderer/constants.js index 6fac931..86a7028 100644 --- a/frontend/src/components/ChatRenderer/constants.js +++ b/frontend/src/components/ChatRenderer/constants.js @@ -20,6 +20,7 @@ export const GUARD_LEVEL_TO_TEXT = [ export const MESSAGE_TYPE_TEXT = 0 export const MESSAGE_TYPE_MEMBER = 1 export const MESSAGE_TYPE_SUPER_CHAT = 2 +export const MESSAGE_TYPE_DEL = 3 // 美元 -> 人民币 汇率 const EXCHANGE_RATE = 7 diff --git a/frontend/src/components/ChatRenderer/index.vue b/frontend/src/components/ChatRenderer/index.vue index 47a9b43..3006545 100644 --- a/frontend/src/components/ChatRenderer/index.vue +++ b/frontend/src/components/ChatRenderer/index.vue @@ -17,7 +17,7 @@ :avatarUrl="message.avatarUrl" :title="message.title" :content="message.content" :time="message.time" > - this.maxNumber) { - // 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857 - this.$nextTick(() => this.messages.splice(0, this.messages.length - this.maxNumber)) - } - - this.$nextTick(() => { - if (this.canAutoScroll) { - this.scrollToBottom() - } - }) + this.enqueueMessages(messages) }, mergeSimilar(content) { for (let i = this.messages.length - 1; i >= 0 && i >= this.messages.length - 5; i--) { @@ -129,6 +126,141 @@ export default { return false }, delMessage(id) { + this.delMessages([id]) + }, + delMessages(ids) { + this.enqueueMessages(ids.map(id => { + return { + id: id, + type: constants.MESSAGE_TYPE_DEL + } + })) + }, + clearMessages() { + this.messages = [] + this.paidMessages = [] + }, + + enqueueMessages(messages) { + if (this.lastEnqueueTime) { + let interval = new Date() - this.lastEnqueueTime + if (interval > 0) { + this.enqueueIntervals.push(interval) + if (this.enqueueIntervals.length > 5) { + this.enqueueIntervals.splice(0, this.enqueueIntervals.length - 5) + } + this.estimatedEnqueueInterval = Math.max(...this.enqueueIntervals) + } + } + this.lastEnqueueTime = new Date() + + // 只有要显示的消息需要平滑 + let messageGroup = [] + for (let message of messages) { + messageGroup.push(message) + if (message.type !== constants.MESSAGE_TYPE_DEL) { + this.smoothedMessageQueue.push(messageGroup) + messageGroup = [] + } + } + if (messageGroup.length > 0) { + this.smoothedMessageQueue.push(messageGroup) + } + + if (!this.emitSmoothedMessageTimerId) { + this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages) + } + }, + emitSmoothedMessages() { + this.emitSmoothedMessageTimerId = null + if (this.smoothedMessageQueue.length <= 0) { + return + } + + // 估计的下次进队列剩余时间 + let estimatedNextEnqueueRemainTime = 10 * 1000 + if (this.estimatedEnqueueInterval) { + estimatedNextEnqueueRemainTime = Math.max(this.lastEnqueueTime - new Date() + this.estimatedEnqueueInterval, 1) + } + // 最快80ms/条,计算发送的消息数,保证在下次进队列之前消费完队列 + let groupNumToEmit + if (this.smoothedMessageQueue.length < estimatedNextEnqueueRemainTime / 80) { + // 队列中消息数很少,每次发1条也能发完 + groupNumToEmit = 1 + } else { + // 每次发1条以上,保证按最快速度能发完 + groupNumToEmit = Math.ceil(this.smoothedMessageQueue.length / (estimatedNextEnqueueRemainTime / 80)) + } + + let messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit) + let mergedGroup = [] + for (let messageGroup of messageGroups) { + for (let message of messageGroup) { + mergedGroup.push(message) + } + } + this.handleMessageGroup(mergedGroup) + + if (this.smoothedMessageQueue.length <= 0) { + return + } + let sleepTime + if (groupNumToEmit == 1) { + // 队列中消息数很少,随便定个80-1000ms的时间 + sleepTime = estimatedNextEnqueueRemainTime / this.smoothedMessageQueue.length + sleepTime *= 0.5 + Math.random() + if (sleepTime > 1000) { + sleepTime = 1000 + } else if (sleepTime < 80) { + sleepTime = 80 + } + } else { + // 按最快速度发 + sleepTime = 80 + } + this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages, sleepTime) + }, + + handleMessageGroup(messageGroup) { + if (messageGroup.length <= 0) { + return + } + + for (let message of messageGroup) { + switch (message.type) { + case constants.MESSAGE_TYPE_TEXT: + case constants.MESSAGE_TYPE_MEMBER: + case constants.MESSAGE_TYPE_SUPER_CHAT: + this.handleAddMessage(message) + break + case constants.MESSAGE_TYPE_DEL: + this.handleDelMessage(message.id) + break + } + } + + if (this.messages.length > this.maxNumber) { + // 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857 + this.$nextTick(() => this.messages.splice(0, this.messages.length - this.maxNumber)) + } + this.$nextTick(() => { + if (this.canScrollToBottom) { + this.scrollToBottom() + } + }) + }, + handleAddMessage(message) { + message = { + ...message, + addTime: new Date() // 添加一个本地时间给Ticker用,防止本地时间和服务器时间相差很大的情况 + } + this.messages.push(message) + if (message.type !== constants.MESSAGE_TYPE_TEXT) { + this.paidMessages.push(message) + } + }, + handleDelMessage(message) { + let id = message.id for (let arr of [this.messages, this.paidMessages]) { for (let i = 0; i < arr.length; i++) { if (arr[i].id === id) { @@ -138,16 +270,20 @@ export default { } } }, - clearMessages() { - this.messages = [] - this.paidMessages = [] + + maybeScrollToBottom() { + if (this.canScrollToBottom) { + this.scrollToBottom() + } }, scrollToBottom() { this.$refs.scroller.scrollTop = this.$refs.scroller.scrollHeight + this.atBottom = true }, onScroll() { let scroller = this.$refs.scroller - this.canAutoScroll = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < SCROLLED_TO_BOTTOM_EPSILON + this.atBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < SCROLLED_TO_BOTTOM_EPSILON + // this.flushActiveItems() } } }