FreeTube/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js

334 lines
9.3 KiB
JavaScript

import { defineComponent } from 'vue'
import FtLoader from '../ft-loader/ft-loader.vue'
import FtCard from '../ft-card/ft-card.vue'
import FtButton from '../ft-button/ft-button.vue'
import autolinker from 'autolinker'
import { getRandomColorClass } from '../../helpers/colors'
import { getLocalVideoInfo, parseLocalTextRuns } from '../../helpers/api/local'
import { formatNumber } from '../../helpers/utils'
export default defineComponent({
name: 'WatchVideoLiveChat',
components: {
'ft-loader': FtLoader,
'ft-card': FtCard,
'ft-button': FtButton
},
props: {
liveChat: {
type: EventTarget,
default: null
},
videoId: {
type: String,
required: true
},
channelId: {
type: String,
required: true
}
},
data: function () {
return {
/** @type {import('youtubei.js').YT.LiveChat|null} */
liveChatInstance: null,
isLoading: true,
hasError: false,
hasEnded: false,
showEnableChat: false,
errorMessage: '',
stayAtBottom: true,
showSuperChat: false,
showScrollToBottom: false,
comments: [],
superChatComments: [],
superChat: {
id: '',
author: {
name: '',
thumbnailUrl: ''
},
message: '',
superChat: {
amount: '',
colorClass: ''
}
},
/** @type {number|null} */
watchingCount: null,
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
chatHeight: function () {
if (this.superChatComments.length > 0) {
return '390px'
} else {
return '445px'
}
},
scrollingBehaviour: function () {
return this.$store.getters.getDisableSmoothScrolling ? 'auto' : 'smooth'
},
hideVideoViews: function () {
return this.$store.getters.getHideVideoViews
},
formattedWatchingCount: function () {
return this.watchingCount !== null ? formatNumber(this.watchingCount) : '0'
}
},
beforeDestroy: function () {
this.hasEnded = true
this.liveChatInstance?.stop()
this.liveChatInstance = null
},
created: function () {
if (!process.env.IS_ELECTRON) {
this.hasError = true
this.errorMessage = this.$t('Video["Live Chat is currently not supported in this build."]')
this.isLoading = false
} else {
switch (this.backendPreference) {
case 'local':
if (this.liveChat) {
this.liveChatInstance = this.liveChat
this.startLiveChatLocal()
} else {
this.showLiveChatUnavailable()
}
break
case 'invidious':
if (this.backendFallback) {
this.getLiveChatLocal()
} else {
this.hasError = true
this.errorMessage = this.$t('Video["Live Chat is currently not supported with the Invidious API. A direct connection to YouTube is required."]')
this.showEnableChat = true
this.isLoading = false
}
break
}
}
},
methods: {
enableLiveChat: function () {
this.hasError = false
this.showEnableChat = false
this.isLoading = true
this.getLiveChatLocal()
},
getLiveChatLocal: async function () {
const videoInfo = await getLocalVideoInfo(this.videoId)
if (videoInfo.livechat) {
this.liveChatInstance = videoInfo.getLiveChat()
this.startLiveChatLocal()
} else {
this.showLiveChatUnavailable()
}
},
showLiveChatUnavailable: function () {
this.hasError = true
this.errorMessage = this.$t('Video["Live Chat is unavailable for this stream. It may have been disabled by the uploader."]')
this.isLoading = false
this.showEnableChat = false
},
startLiveChatLocal: function () {
this.liveChatInstance.once('start', initialData => {
/**
* @type {import ('youtubei.js/dist/src/parser/index').LiveChatContinuation}
*/
const liveChatContinuation = initialData
const actions = liveChatContinuation.actions.filter(action => action.type === 'AddChatItemAction')
for (const { item } of actions) {
switch (item.type) {
case 'LiveChatTextMessage':
this.parseLiveChatComment(item)
break
case 'LiveChatPaidMessage':
this.parseLiveChatSuperChat(item)
}
}
this.isLoading = false
setTimeout(() => {
this.$refs.liveChatComments?.scrollTo({
top: this.$refs.liveChatComments.scrollHeight,
behavior: 'instant'
})
})
})
this.liveChatInstance.on('chat-update', action => {
if (this.hasEnded) {
return
}
if (action.type === 'AddChatItemAction') {
switch (action.item.type) {
case 'LiveChatTextMessage':
this.parseLiveChatComment(action.item)
break
case 'LiveChatPaidMessage':
this.parseLiveChatSuperChat(action.item)
break
}
}
})
this.liveChatInstance.on('metadata-update', metadata => {
if (!this.hideVideoViews && metadata.views && !isNaN(metadata.views.original_view_count)) {
this.watchingCount = metadata.views.original_view_count
}
})
this.liveChatInstance.once('end', () => {
this.hasEnded = true
this.liveChatInstance = null
})
this.liveChatInstance.once('error', error => {
this.liveChatInstance.stop()
this.liveChatInstance = null
console.error(error)
this.errorMessage = error
this.hasError = true
this.isLoading = false
this.hasEnded = true
})
this.liveChatInstance.start()
},
/**
* @param {import('youtubei.js').YTNodes.LiveChatTextMessage} comment
*/
parseLiveChatComment: function (comment) {
/**
* can also be undefined if there is no badge
* @type {import('youtubei.js').YTNodes.LiveChatAuthorBadge}
*/
const badge = comment.author.badges.find(badge => badge.type === 'LiveChatAuthorBadge' && badge.custom_thumbnail)
const parsedComment = {
message: autolinker.link(parseLocalTextRuns(comment.message.runs, 20)),
author: {
name: comment.author.name,
thumbnailUrl: comment.author.thumbnails.at(-1).url,
isOwner: comment.author.id === this.channelId,
isModerator: comment.author.is_moderator,
isMember: !!badge
}
}
if (badge) {
parsedComment.badge = {
url: badge.custom_thumbnail.at(-1)?.url,
tooltip: badge.tooltip ?? ''
}
}
this.pushComment(parsedComment)
},
/**
* @param {import('youtubei.js').YTNodes.LiveChatPaidMessage} superChat
*/
parseLiveChatSuperChat: function (superChat) {
const parsedComment = {
id: superChat.id,
message: autolinker.link(parseLocalTextRuns(superChat.message.runs, 20)),
author: {
name: superChat.author.name.text,
thumbnailUrl: superChat.author.thumbnails[0].url
},
superChat: {
amount: superChat.purchase_amount,
colorClass: getRandomColorClass()
}
}
this.superChatComments.unshift(parsedComment)
setTimeout(() => {
this.removeFromSuperChat(parsedComment.id)
}, 120000)
this.pushComment(parsedComment)
},
pushComment: function (comment) {
this.comments.push(comment)
if (!this.isLoading && this.stayAtBottom) {
setTimeout(() => {
this.$refs.liveChatComments?.scrollTo({
top: this.$refs.liveChatComments.scrollHeight,
behavior: this.scrollingBehaviour
})
})
}
if (this.comments.length > 150 && this.stayAtBottom) {
this.comments = this.comments.splice(this.comments.length - 150, this.comments.length)
}
},
removeFromSuperChat: function (id) {
this.superChatComments = this.superChatComments.filter((comment) => {
return comment.id !== id
})
},
showSuperChatComment: function (comment) {
if (this.superChat.id === comment.id && this.showSuperChat) {
this.showSuperChat = false
} else {
this.superChat = comment
this.showSuperChat = true
}
},
onScroll: function (event) {
const liveChatComments = this.$refs.liveChatComments
if (event.wheelDelta >= 0 && this.stayAtBottom) {
this.stayAtBottom = false
if (liveChatComments.scrollHeight > liveChatComments.clientHeight) {
this.showScrollToBottom = true
}
} else if (event.wheelDelta < 0 && !this.stayAtBottom) {
if ((liveChatComments.scrollHeight - liveChatComments.scrollTop) === liveChatComments.clientHeight) {
this.scrollToBottom()
}
}
},
scrollToBottom: function () {
this.$refs.liveChatComments.scrollTo({
top: this.$refs.liveChatComments.scrollHeight,
behavior: this.scrollingBehaviour
})
this.stayAtBottom = true
this.showScrollToBottom = false
}
}
})