mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2024-12-22 13:22:58 -05:00
Add share feature
This commit is contained in:
parent
62f3d8b195
commit
ee3d2ba346
7 changed files with 194 additions and 46 deletions
|
@ -8,12 +8,12 @@
|
||||||
import videojs from 'video.js'
|
import videojs from 'video.js'
|
||||||
import qualityLevels from 'videojs-contrib-quality-levels'
|
import qualityLevels from 'videojs-contrib-quality-levels'
|
||||||
import { createQualitySelector } from '@/assets/qualitySelector'
|
import { createQualitySelector } from '@/assets/qualitySelector'
|
||||||
|
import { getTimeFromQuery } from '@/mixins'
|
||||||
import 'video.js/dist/video-js.css'
|
import 'video.js/dist/video-js.css'
|
||||||
|
|
||||||
videojs.registerPlugin('qualityLevels', qualityLevels)
|
videojs.registerPlugin('qualityLevels', qualityLevels)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VideoJsPlayer',
|
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -23,7 +23,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['PlayerTimeUpdate'],
|
emits: ['PlayerTimeUpdate'],
|
||||||
data() {
|
setup() {
|
||||||
let player: any
|
let player: any
|
||||||
return {
|
return {
|
||||||
player
|
player
|
||||||
|
@ -36,6 +36,12 @@ export default {
|
||||||
this.player = videojs('video-player', this.options, () => {
|
this.player = videojs('video-player', this.options, () => {
|
||||||
createQualitySelector(this.player)
|
createQualitySelector(this.player)
|
||||||
|
|
||||||
|
if (this.$route.query['t']) {
|
||||||
|
const timeQuery = this.$route.query['t'].toString() || ""
|
||||||
|
const time = getTimeFromQuery(timeQuery)
|
||||||
|
this.player.currentTime(time)
|
||||||
|
}
|
||||||
|
|
||||||
this.player.on('timeupdate', () => {
|
this.player.on('timeupdate', () => {
|
||||||
emit('PlayerTimeUpdate', this.player.currentTime())
|
emit('PlayerTimeUpdate', this.player.currentTime())
|
||||||
})
|
})
|
||||||
|
|
107
src/components/popups/ShareButtonPopup.vue
Normal file
107
src/components/popups/ShareButtonPopup.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
const route = useRoute()
|
||||||
|
const instanceUrl = location.protocol + '//' + location.host;
|
||||||
|
let currentUrl = ref(instanceUrl)
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: route.fullPath,
|
||||||
|
usingTwitchUrl: ref(false),
|
||||||
|
usingTime: ref(false),
|
||||||
|
query: ref(""),
|
||||||
|
instanceUrl,
|
||||||
|
currentUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
props: {
|
||||||
|
time: {
|
||||||
|
type: Number,
|
||||||
|
default() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
useTime: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatSecondsToQuery(sec: number) {
|
||||||
|
const date = new Date(sec * 1000)
|
||||||
|
const hour = date.getHours()
|
||||||
|
const minutes = date.getMinutes()
|
||||||
|
const seconds = date.getSeconds()
|
||||||
|
|
||||||
|
return `${hour}h${minutes}m${seconds}s`
|
||||||
|
},
|
||||||
|
toggleTwitchUrl() {
|
||||||
|
if (this.usingTwitchUrl) {
|
||||||
|
this.currentUrl = "https://twitch.tv"
|
||||||
|
} else {
|
||||||
|
this.currentUrl = this.instanceUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async copyUrl() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(this.currentUrl + this.path + this.query);
|
||||||
|
console.log('Content copied to clipboard');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleTime() {
|
||||||
|
if (this.usingTime) {
|
||||||
|
const timestamp = this.formatSecondsToQuery(this.$props.time)
|
||||||
|
this.query = "?t=" + timestamp
|
||||||
|
} else {
|
||||||
|
this.query = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoUrl() {
|
||||||
|
location.href = this.currentUrl + this.path + this.query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed top-0 bottom-0 left-0 right-0 flex w-full z-50 h-[100vh] bg-opacity-50 bg-black">
|
||||||
|
<div class="bg-ctp-crust my-auto h-min mx-auto w-[35rem] max-w-[95vw] p-5 rounded-md relative z-50 text-white">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<h1 class="text-3xl font-bold">Share</h1>
|
||||||
|
<button @click="$emit('close')">
|
||||||
|
<v-icon name="io-close-sharp" scale="1.8"></v-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr class="my-2" />
|
||||||
|
<div class="flex bg-ctp-surface0 p-3 rounded-md h-12 overflow-x-scroll whitespace-nowrap">
|
||||||
|
<p class="" ref="urlPreview">
|
||||||
|
{{ currentUrl + path + query }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul class="mt-1">
|
||||||
|
<li>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input :disabled="!useTime" class="align-middle w-4 h-4 mr-1 disabled:opacity-50" type="checkbox" @change="toggleTime()" v-model="usingTime" /> Add Timestamp
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input class="align-middle w-4 h-4 mr-1" @change="toggleTwitchUrl()" v-model="usingTwitchUrl" type="checkbox" /> Twitch URL
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="space-x-2 mt-1">
|
||||||
|
<button class="p-2 py-1.5 bg-ctp-surface0 rounded-md" @click="copyUrl()">Copy Link</button>
|
||||||
|
<button class="p-2 py-1.5 bg-ctp-surface0 rounded-md" @click="gotoUrl()" >Go to</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -40,3 +40,15 @@ export async function getEndpoint(endpoint: string) {
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTimeFromQuery(query: string) {
|
||||||
|
// H, M, S
|
||||||
|
const x = query.split(/[^0-9.]/g);
|
||||||
|
const times = x.map(Number)
|
||||||
|
|
||||||
|
let time = 0
|
||||||
|
time += times[0] * 3600
|
||||||
|
time += times[1] * 60
|
||||||
|
time += times[2]
|
||||||
|
return time
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ export function getDefaultSettings() {
|
||||||
|
|
||||||
export function syncUserSettings() {
|
export function syncUserSettings() {
|
||||||
const defaultSettings = getDefaultSettings()
|
const defaultSettings = getDefaultSettings()
|
||||||
let userSettings = localStorage.getItem('settings')
|
const userSettings = localStorage.getItem('settings')
|
||||||
if (!userSettings) return
|
if (!userSettings) return
|
||||||
const parsedUserSettings = JSON.parse(userSettings)
|
const parsedUserSettings = JSON.parse(userSettings)
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,12 @@ import ErrorMessage from '@/components/ErrorMessage.vue'
|
||||||
import FollowButton from '@/components/FollowButton.vue'
|
import FollowButton from '@/components/FollowButton.vue'
|
||||||
import LoadingScreen from '@/components/LoadingScreen.vue'
|
import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||||
import AboutTab from '@/components/user/AboutTab.vue'
|
import AboutTab from '@/components/user/AboutTab.vue'
|
||||||
|
import ShareModal from '@/components/popups/ShareButtonPopup.vue'
|
||||||
|
|
||||||
import type { Video } from '@/types'
|
import type { Video } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
import { chatVisible } from '@/settingsManager'
|
import { chatVisible } from '@/settingsManager'
|
||||||
|
|
||||||
interface ChatComponent {
|
|
||||||
updateVodComments: (time: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['rootBackendUrl'],
|
inject: ['rootBackendUrl'],
|
||||||
async setup() {
|
async setup() {
|
||||||
|
@ -50,7 +47,9 @@ export default {
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
status,
|
status,
|
||||||
videoOptions
|
videoOptions,
|
||||||
|
time: ref(0),
|
||||||
|
shareModalVisible: ref(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
@ -70,22 +69,22 @@ export default {
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
FollowButton,
|
FollowButton,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
AboutTab
|
AboutTab,
|
||||||
|
ShareModal
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate,
|
truncate,
|
||||||
abbreviate,
|
abbreviate,
|
||||||
handlePlayerTimeUpdate(time: number) {
|
chatVisible,
|
||||||
if (!chatVisible()) return
|
toggleShareModal() {
|
||||||
const chat = this.$refs.chat as ChatComponent
|
this.shareModalVisible = !this.shareModalVisible
|
||||||
chat.updateVodComments(time)
|
}
|
||||||
},
|
|
||||||
chatVisible
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<share-modal v-if="shareModalVisible" :useTime="false" @close="toggleShareModal"></share-modal>
|
||||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||||
<error-message v-else-if="status == 'error'"></error-message>
|
<error-message v-else-if="status == 'error'"></error-message>
|
||||||
|
|
||||||
|
@ -97,7 +96,7 @@ export default {
|
||||||
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
||||||
>
|
>
|
||||||
<div class="w-full mx-auto rounded-lg mb-5">
|
<div class="w-full mx-auto rounded-lg mb-5">
|
||||||
<video-player :options="videoOptions" @PlayerTimeUpdate="handlePlayerTimeUpdate">
|
<video-player :options="videoOptions">
|
||||||
</video-player>
|
</video-player>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -121,25 +120,22 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-2 inline-flex">
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="pt-2 inline-flex">
|
||||||
<follow-button :username="data.streamer.username"></follow-button>
|
<follow-button :username="data.streamer.username"></follow-button>
|
||||||
<p class="align-baseline font-bold ml-3">
|
<p class="align-baseline font-bold ml-3">
|
||||||
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple-600">
|
||||||
|
<v-icon name="fa-share-alt"></v-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ABOUT TAB -->
|
<!-- ABOUT TAB -->
|
||||||
<about-tab :socials="data.streamer.socials" :about="data.streamer.about"></about-tab>
|
<about-tab :socials="data.streamer.socials" :about="data.streamer.about"></about-tab>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
|
||||||
<twitch-chat
|
|
||||||
v-if="chatVisible()"
|
|
||||||
:isVod="true"
|
|
||||||
:channelName="data.streamer.login"
|
|
||||||
ref="chat"
|
|
||||||
></twitch-chat>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||||
import VideoTab from '@/components/user/VideoTab.vue'
|
import VideoTab from '@/components/user/VideoTab.vue'
|
||||||
import AudioPlayer from '@/components/AudioPlayer.vue'
|
import AudioPlayer from '@/components/AudioPlayer.vue'
|
||||||
import AboutTab from '@/components/user/AboutTab.vue'
|
import AboutTab from '@/components/user/AboutTab.vue'
|
||||||
|
import ShareModal from '@/components/popups/ShareButtonPopup.vue'
|
||||||
|
|
||||||
|
|
||||||
import type { StreamerData } from '@/types'
|
import type { StreamerData } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
|
@ -40,7 +42,8 @@ export default {
|
||||||
data,
|
data,
|
||||||
status,
|
status,
|
||||||
videoOptions,
|
videoOptions,
|
||||||
audioOptions
|
audioOptions,
|
||||||
|
shareModalVisible: ref(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
@ -68,18 +71,23 @@ export default {
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
VideoTab,
|
VideoTab,
|
||||||
AudioPlayer,
|
AudioPlayer,
|
||||||
AboutTab
|
AboutTab,
|
||||||
|
ShareModal
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate,
|
truncate,
|
||||||
abbreviate,
|
abbreviate,
|
||||||
chatVisible,
|
chatVisible,
|
||||||
getSetting
|
getSetting,
|
||||||
|
toggleShareModal() {
|
||||||
|
this.shareModalVisible = !this.shareModalVisible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<share-modal v-if="shareModalVisible" :time="0" :useTime="false" @close="toggleShareModal"></share-modal>
|
||||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||||
<error-message v-else-if="status == 'error'"></error-message>
|
<error-message v-else-if="status == 'error'"></error-message>
|
||||||
|
|
||||||
|
@ -160,11 +168,16 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-2 inline-flex">
|
<div class="pt-2 space-x-2 items-center inline-flex">
|
||||||
<follow-button :username="data.login"></follow-button>
|
<follow-button :username="data.login"></follow-button>
|
||||||
|
|
||||||
<p class="align-baseline font-bold ml-3">
|
<p class="align-baseline font-bold ml-3">
|
||||||
{{ abbreviate(data.followers) }} {{ $t('main.followers') }}
|
{{ abbreviate(data.followers) }} {{ $t('main.followers') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple-600">
|
||||||
|
<v-icon name="fa-share-alt"></v-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import ErrorMessage from '@/components/ErrorMessage.vue'
|
||||||
import FollowButton from '@/components/FollowButton.vue'
|
import FollowButton from '@/components/FollowButton.vue'
|
||||||
import LoadingScreen from '@/components/LoadingScreen.vue'
|
import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||||
import AboutTab from '@/components/user/AboutTab.vue'
|
import AboutTab from '@/components/user/AboutTab.vue'
|
||||||
|
import ShareModal from '@/components/popups/ShareButtonPopup.vue'
|
||||||
|
|
||||||
|
|
||||||
import type { Video } from '@/types'
|
import type { Video } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
|
@ -22,11 +24,9 @@ export default {
|
||||||
async setup() {
|
async setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const vodID = route.params.vodID
|
const vodID = route.params.vodID
|
||||||
const data = ref<Video>()
|
|
||||||
const status = ref<'ok' | 'error'>()
|
|
||||||
const rootBackendUrl = inject('rootBackendUrl')
|
const rootBackendUrl = inject('rootBackendUrl')
|
||||||
const videoOptions = {
|
const videoOptions = {
|
||||||
autoplay: true,
|
autoplay: getSetting('autoplay'),
|
||||||
controls: true,
|
controls: true,
|
||||||
sources: [
|
sources: [
|
||||||
{
|
{
|
||||||
|
@ -36,11 +36,12 @@ export default {
|
||||||
],
|
],
|
||||||
fluid: true
|
fluid: true
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data: ref<Video>(),
|
||||||
status,
|
status: ref<'ok' | 'error'>(),
|
||||||
videoOptions
|
videoOptions,
|
||||||
|
time: ref(0),
|
||||||
|
shareModalVisible: ref(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
@ -60,7 +61,8 @@ export default {
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
FollowButton,
|
FollowButton,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
AboutTab
|
AboutTab,
|
||||||
|
ShareModal
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate,
|
truncate,
|
||||||
|
@ -68,15 +70,20 @@ export default {
|
||||||
handlePlayerTimeUpdate(time: number) {
|
handlePlayerTimeUpdate(time: number) {
|
||||||
if (!chatVisible()) return
|
if (!chatVisible()) return
|
||||||
const chat = this.$refs.chat as ChatComponent
|
const chat = this.$refs.chat as ChatComponent
|
||||||
|
this.time = time
|
||||||
chat.updateVodComments(time)
|
chat.updateVodComments(time)
|
||||||
},
|
},
|
||||||
chatVisible,
|
chatVisible,
|
||||||
getSetting
|
getSetting,
|
||||||
|
toggleShareModal() {
|
||||||
|
this.shareModalVisible = !this.shareModalVisible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<share-modal v-if="shareModalVisible" :time="time" :useTime="true" @close="toggleShareModal"></share-modal>
|
||||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||||
<error-message v-else-if="status == 'error'"></error-message>
|
<error-message v-else-if="status == 'error'"></error-message>
|
||||||
|
|
||||||
|
@ -102,7 +109,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="ml-3 content-between">
|
<div class="ml-3 content-between w-5/6">
|
||||||
<router-link :to="'/' + data.streamer.login">
|
<router-link :to="'/' + data.streamer.login">
|
||||||
<h1 class="text-2xl md:text-4xl font-bold">{{ data.streamer.username }}</h1>
|
<h1 class="text-2xl md:text-4xl font-bold">{{ data.streamer.username }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -112,11 +119,18 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-2 inline-flex">
|
<div class="flex justify-between items-center">
|
||||||
<follow-button :username="data.streamer.login"></follow-button>
|
<div class="pt-2 inline-flex">
|
||||||
<p class="align-baseline font-bold ml-3">
|
<follow-button :username="data.streamer.login"></follow-button>
|
||||||
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
<p class="align-baseline font-bold ml-3">
|
||||||
</p>
|
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple-600">
|
||||||
|
<v-icon name="fa-share-alt"></v-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -125,7 +139,7 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<twitch-chat
|
<twitch-chat
|
||||||
v-if="getSetting('chatVisible')"
|
v-if="!getSetting('chatVisible')"
|
||||||
:isVod="true"
|
:isVod="true"
|
||||||
:channelName="data.streamer.login"
|
:channelName="data.streamer.login"
|
||||||
ref="chat"
|
ref="chat"
|
||||||
|
|
Loading…
Reference in a new issue