mirror of
https://github.com/mmjee/Piped-Material.git
synced 2024-12-06 19:26:36 +01:00
Reformatted, fixed playlist page grid, added skipping SponsorBlock if such data is unavailable for any reason, fixed errors popping up during logging in
This commit is contained in:
+64
-65
@@ -1,71 +1,71 @@
|
||||
<template>
|
||||
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error"/>
|
||||
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
|
||||
|
||||
<v-container fluid v-else-if="playlist">
|
||||
<v-row>
|
||||
<v-col md="8" offset-md="2">
|
||||
<v-card>
|
||||
<v-img v-if="playlist.avatarUrl" :src="playlist.avatarUrl"/>
|
||||
<v-card-title class="text-h4">
|
||||
{{ playlist.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<router-link v-if="playlist.uploaderUrl" :to="playlist.uploaderUrl" custom
|
||||
v-slot="{ navigate }">
|
||||
<div style="justify-items: center; align-items: center; vertical-align: center; display: flex; cursor: pointer" @click="navigate" @keypress.enter="navigate" role="link">
|
||||
<img :src="playlist.uploaderAvatar" height="48" width="48" class="rounded-circle" />
|
||||
<div class="text-h5 ml-4">
|
||||
{{ playlist.uploader }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<h5 class="text-h5 ml-16">{{ $tc('counts.videos', playlist.videos, { n: $store.getters['i18n/fmtFullNumber'](playlist.videos) }) }}</h5>
|
||||
<br />
|
||||
<div v-html="renderedDescription" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn icon x-large link :href="getRssUrl">
|
||||
<v-icon x-large>{{ mdiRssBox }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon x-large link :href="youtubeURL">
|
||||
<v-icon x-large>{{ mdiYoutube }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="isAdmin" @click="deleteSelf" outlined>
|
||||
{{ $t('playlists.delete_self') }}
|
||||
</v-btn>
|
||||
<v-btn v-if="isAdmin" @click="renameSelf" outlined>
|
||||
{{ $t('playlists.rename_self') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<v-row v-for="(chunk, chunkId) in chunkedByFour" :key="chunkId" class="mt-4">
|
||||
<v-col md="3" v-for="video in chunk" :key="video.url">
|
||||
<VideoItem :video="video" max-height>
|
||||
<template v-slot:bottom>
|
||||
<v-card-actions v-if="isAdmin">
|
||||
<v-btn
|
||||
outlined
|
||||
color="secondary"
|
||||
@click.prevent="removeVideo(video)"
|
||||
class="pm-breakable-button"
|
||||
>
|
||||
{{ $t('playlists.remove_video')}}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</VideoItem>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-container fluid v-else-if="playlist">
|
||||
<v-row>
|
||||
<v-col md="8" offset-md="2">
|
||||
<v-card>
|
||||
<v-img v-if="playlist.avatarUrl" :src="playlist.avatarUrl" />
|
||||
<v-card-title class="text-h4">
|
||||
{{ playlist.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<router-link v-if="playlist.uploaderUrl" :to="playlist.uploaderUrl" custom v-slot="{ navigate }">
|
||||
<div style="justify-items: center; align-items: center; vertical-align: center; display: flex; cursor: pointer"
|
||||
@click="navigate" @keypress.enter="navigate" role="link">
|
||||
<img :src="playlist.uploaderAvatar" height="48" width="48" class="rounded-circle" />
|
||||
<div class="text-h5 ml-4">
|
||||
{{ playlist.uploader }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<h5 class="text-h5 ml-16">{{ $tc('counts.videos', playlist.videos, {
|
||||
n:
|
||||
$store.getters['i18n/fmtFullNumber'](playlist.videos)
|
||||
}) }}</h5>
|
||||
<br />
|
||||
<div v-html="renderedDescription" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn icon x-large link :href="getRssUrl">
|
||||
<v-icon x-large>{{ mdiRssBox }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon x-large link :href="youtubeURL">
|
||||
<v-icon x-large>{{ mdiYoutube }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="isAdmin" @click="deleteSelf" outlined>
|
||||
{{ $t('playlists.delete_self') }}
|
||||
</v-btn>
|
||||
<v-btn v-if="isAdmin" @click="renameSelf" outlined>
|
||||
{{ $t('playlists.rename_self') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-divider class="my-4" />
|
||||
<GridRow>
|
||||
<GridCol v-for="video in playlist.relatedStreams" :key="video.url">
|
||||
<VideoItem :video="video" max-height>
|
||||
<template v-slot:bottom>
|
||||
<v-card-actions v-if="isAdmin">
|
||||
<v-btn outlined color="secondary" @click.prevent="removeVideo(video)"
|
||||
class="pm-breakable-button">
|
||||
{{ $t('playlists.remove_video') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</VideoItem>
|
||||
</GridCol>
|
||||
</GridRow>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { chunk as _chunk } from 'lodash-es'
|
||||
import { mdiRssBox, mdiYoutube } from '@mdi/js'
|
||||
|
||||
import GridRow from '@/components/Grid/GridRow.vue'
|
||||
import GridCol from '@/components/Grid/GridCol.vue'
|
||||
import ErrorHandler from '@/components/ErrorHandler.vue'
|
||||
import VideoItem from '@/components/Video/VideoItem.vue'
|
||||
|
||||
@@ -114,9 +114,6 @@ export default {
|
||||
const url = new URL('https://youtube.com/playlist')
|
||||
url.searchParams.set('list', this.playlistID)
|
||||
return url.href
|
||||
},
|
||||
chunkedByFour () {
|
||||
return _chunk(this.playlist.relatedStreams, 4)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -168,7 +165,9 @@ export default {
|
||||
},
|
||||
components: {
|
||||
ErrorHandler,
|
||||
VideoItem
|
||||
VideoItem,
|
||||
GridRow,
|
||||
GridCol
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+141
-144
@@ -1,148 +1,136 @@
|
||||
<template>
|
||||
<NGErrorHandler :error="error" v-if="error != null" />
|
||||
<v-progress-linear indeterminate v-else-if="!loaded" />
|
||||
<div v-else>
|
||||
<Player
|
||||
ref="player"
|
||||
:video="video"
|
||||
:initial-skip="initialSkip"
|
||||
:sponsors="sponsors"
|
||||
:selectedAutoLoop="selectedAutoLoop"
|
||||
:audio-only="($store.getters['prefs/getPreferenceBoolean']('listen', false) || $route.query.listen === '1') && !video.livestream"
|
||||
@videoEnded="videoEnded"
|
||||
@timeupdate="onTimeUpdate"
|
||||
/>
|
||||
<NGErrorHandler :error="error" v-if="error != null" />
|
||||
<v-progress-linear indeterminate v-else-if="!loaded" />
|
||||
<div v-else>
|
||||
<Player ref="player" :video="video" :initial-skip="initialSkip" :sponsors="sponsors"
|
||||
:selectedAutoLoop="selectedAutoLoop"
|
||||
:audio-only="($store.getters['prefs/getPreferenceBoolean']('listen', false) || $route.query.listen === '1') && !video.livestream"
|
||||
@videoEnded="videoEnded" @timeupdate="onTimeUpdate" />
|
||||
|
||||
<v-dialog max-width="960" v-model="sharingPanelOpen">
|
||||
<VideoSharingPanel @input="sharingPanelOpen = $event" :current-time="currentTime" :video-id="videoId" />
|
||||
</v-dialog>
|
||||
<VideoPlaylistOperationsA
|
||||
:dialog-open="playlistAddDialogOpen"
|
||||
@input="playlistAddDialogOpen = $event"
|
||||
:video-id="videoId"
|
||||
/>
|
||||
<!-- <v-dialog max-width="720" v-model="playlistRemoveDialogOpen">
|
||||
<v-dialog max-width="960" v-model="sharingPanelOpen">
|
||||
<VideoSharingPanel @input="sharingPanelOpen = $event" :current-time="currentTime" :video-id="videoId" />
|
||||
</v-dialog>
|
||||
<VideoPlaylistOperationsA :dialog-open="playlistAddDialogOpen" @input="playlistAddDialogOpen = $event"
|
||||
:video-id="videoId" />
|
||||
<!-- <v-dialog max-width="720" v-model="playlistRemoveDialogOpen">
|
||||
<VideoSharingPanel @input="sharingPanelOpen = $event" :current-time="currentTime" :video-id="videoId" />
|
||||
</v-dialog> -->
|
||||
|
||||
<v-row dense class="mb-2">
|
||||
<v-col md="10" offset-md="1">
|
||||
<v-card outlined color="bgTwo">
|
||||
<v-card-title class="text-h5">{{ video.title }}</v-card-title>
|
||||
<v-card-subtitle class="text-subtitle-1">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<ExpandableNumber :num="video.views" message-key="counts.views" />
|
||||
•
|
||||
{{ $store.getters['i18n/fmtDate'](new Date(video.uploadDate)) }}
|
||||
<span v-if="lastWatchFound">
|
||||
•
|
||||
{{ $t('misc.lastWatchedTill', { t: lastWatchDurationH }) }}
|
||||
<ExpandableDate :date="lastWatch.timestamp" />
|
||||
</span>
|
||||
<span v-if="video.visibility !== 'public'">• {{ $t('visibilities.' + video.visibility) }}</span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" :style="$vuetify.breakpoint.mdAndUp ? { textAlign: 'right' } : {}">
|
||||
<span style="vertical-align: middle">
|
||||
<v-icon>{{ mdiThumbUp }}</v-icon>
|
||||
<b class="ml-1">{{ $store.getters['i18n/fmtFullNumber'](video.likes) }}</b>
|
||||
</span>
|
||||
<v-btn outlined class="mt-1 ml-1" @click.stop="onShareClick">
|
||||
<v-icon class="mr-1">
|
||||
{{ mdiShareVariant }}
|
||||
</v-icon>
|
||||
{{ $t('video_sharing_panel.share') }}
|
||||
</v-btn>
|
||||
<v-btn outlined class="mt-1 ml-1" @click.stop="playlistAddDialogOpen = true" v-if="$store.getters['auth/isCurrentlyAuthenticated']">
|
||||
<v-icon class="mr-1">
|
||||
{{ mdiLinkPlus }}
|
||||
</v-icon>
|
||||
{{ $t('playlists.add_to_playlist') }}
|
||||
</v-btn>
|
||||
<v-btn class="mt-1 ml-1" link :href="'https://odysee.com/' + video.lbryId"
|
||||
v-if="video.lbryId" target="_blank" outlined>
|
||||
LBRY
|
||||
</v-btn>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
class="mt-1 ml-1" @click="downloadAccess"
|
||||
outlined
|
||||
v-bind="attrs" v-on="on"
|
||||
>
|
||||
M3U8/DASH/Stream
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>
|
||||
With this you can play the video in your own media player.
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-subtitle>
|
||||
<v-divider class="my-1" v-if="Array.isArray(video.chapters) && video.chapters.length !== 0" />
|
||||
<VideoChapters :chapters="video.chapters" @seek="$refs.player.skipToTime($event)" v-if="Array.isArray(video.chapters) && video.chapters.length !== 0" />
|
||||
<v-divider class="my-1" />
|
||||
<v-card-text>
|
||||
<router-link v-if="video.uploaderUrl" :to="video.uploaderUrl" custom v-slot="{ navigate }">
|
||||
<div
|
||||
style="justify-items: center; align-items: center; vertical-align: center; display: flex; cursor: pointer"
|
||||
@click="navigate" @keypress.enter="navigate" role="link"
|
||||
>
|
||||
<div>
|
||||
<v-img :src="video.uploaderAvatar" height="48" width="48" class="rounded-circle" />
|
||||
</div>
|
||||
<a :href="video.uploaderUrl" class="text-h5 ml-4 text-decoration-none">
|
||||
{{ video.uploader }}
|
||||
<br />
|
||||
<span class="text-subtitle-1">
|
||||
<ExpandableNumber :num="video.uploaderSubscriberCount" message-key="counts.subscribers" />
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-4">
|
||||
<SubscriptionButton :channel-id="channelId" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="mt-4" dir="auto" v-html="video.description" />
|
||||
<v-divider class="my-4" />
|
||||
<div class="mt-4" v-if="showDesc && sponsors && sponsors.segments">
|
||||
Sponsors Segments: {{ sponsors.segments.length }}
|
||||
</div>
|
||||
<div>
|
||||
<!-- TODO translate -->
|
||||
<v-checkbox dense :input-value="isAutoplayEnabled" @change="onAutoplayChg"
|
||||
:label="$t('actions.autoplay_next_video')" hide-details />
|
||||
<v-checkbox dense
|
||||
:input-value="$store.getters['prefs/getPreferenceBoolean']('listen', false)"
|
||||
@change="onListenChg"
|
||||
:label="$t('preferences.listen')"
|
||||
hide-details
|
||||
/>
|
||||
<v-checkbox dense v-model="selectedAutoLoop" :label="$t('actions.loop_this_video')" hide-details />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense class="mb-2">
|
||||
<v-col md="10" offset-md="1">
|
||||
<v-card outlined color="bgTwo">
|
||||
<v-card-title class="text-h5">{{ video.title }}</v-card-title>
|
||||
<v-card-subtitle class="text-subtitle-1">
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<ExpandableNumber :num="video.views" message-key="counts.views" />
|
||||
•
|
||||
{{ $store.getters['i18n/fmtDate'](new Date(video.uploadDate)) }}
|
||||
<span v-if="lastWatchFound">
|
||||
•
|
||||
{{ $t('misc.lastWatchedTill', { t: lastWatchDurationH }) }}
|
||||
<ExpandableDate :date="lastWatch.timestamp" />
|
||||
</span>
|
||||
<span v-if="video.visibility !== 'public'">• {{ $t('visibilities.' + video.visibility)
|
||||
}}</span>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" :style="$vuetify.breakpoint.mdAndUp ? { textAlign: 'right' } : {}">
|
||||
<span style="vertical-align: middle">
|
||||
<v-icon>{{ mdiThumbUp }}</v-icon>
|
||||
<b class="ml-1">{{ $store.getters['i18n/fmtFullNumber'](video.likes) }}</b>
|
||||
</span>
|
||||
<v-btn outlined class="mt-1 ml-1" @click.stop="onShareClick">
|
||||
<v-icon class="mr-1">
|
||||
{{ mdiShareVariant }}
|
||||
</v-icon>
|
||||
{{ $t('video_sharing_panel.share') }}
|
||||
</v-btn>
|
||||
<v-btn outlined class="mt-1 ml-1" @click.stop="playlistAddDialogOpen = true"
|
||||
v-if="$store.getters['auth/isCurrentlyAuthenticated']">
|
||||
<v-icon class="mr-1">
|
||||
{{ mdiLinkPlus }}
|
||||
</v-icon>
|
||||
{{ $t('playlists.add_to_playlist') }}
|
||||
</v-btn>
|
||||
<v-btn class="mt-1 ml-1" link :href="'https://odysee.com/' + video.lbryId"
|
||||
v-if="video.lbryId" target="_blank" outlined>
|
||||
LBRY
|
||||
</v-btn>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn class="mt-1 ml-1" @click="downloadAccess" outlined v-bind="attrs" v-on="on">
|
||||
M3U8/DASH/Stream
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>
|
||||
With this you can play the video in your own media player.
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-subtitle>
|
||||
<v-divider class="my-1" v-if="Array.isArray(video.chapters) && video.chapters.length !== 0" />
|
||||
<VideoChapters :chapters="video.chapters" @seek="$refs.player.skipToTime($event)"
|
||||
v-if="Array.isArray(video.chapters) && video.chapters.length !== 0" />
|
||||
<v-divider class="my-1" />
|
||||
<v-card-text>
|
||||
<router-link v-if="video.uploaderUrl" :to="video.uploaderUrl" custom v-slot="{ navigate }">
|
||||
<div style="justify-items: center; align-items: center; vertical-align: center; display: flex; cursor: pointer"
|
||||
@click="navigate" @keypress.enter="navigate" role="link">
|
||||
<div>
|
||||
<v-img :src="video.uploaderAvatar" height="48" width="48" class="rounded-circle" />
|
||||
</div>
|
||||
<a :href="video.uploaderUrl" class="text-h5 ml-4 text-decoration-none">
|
||||
{{ video.uploader }}
|
||||
<br />
|
||||
<span class="text-subtitle-1">
|
||||
<ExpandableNumber :num="video.uploaderSubscriberCount"
|
||||
message-key="counts.subscribers" />
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-4">
|
||||
<SubscriptionButton :channel-id="channelId" />
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="mt-4" dir="auto" v-html="video.description" />
|
||||
<v-divider class="my-4" />
|
||||
<div class="mt-4" v-if="showDesc && sponsors && sponsors.segments">
|
||||
Sponsors Segments: {{ sponsors.segments.length }}
|
||||
</div>
|
||||
<div>
|
||||
<!-- TODO translate -->
|
||||
<v-checkbox dense :input-value="isAutoplayEnabled" @change="onAutoplayChg"
|
||||
:label="$t('actions.autoplay_next_video')" hide-details />
|
||||
<v-checkbox dense :input-value="$store.getters['prefs/getPreferenceBoolean']('listen', false)"
|
||||
@change="onListenChg" :label="$t('preferences.listen')" hide-details />
|
||||
<v-checkbox dense v-model="selectedAutoLoop" :label="$t('actions.loop_this_video')"
|
||||
hide-details />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="8" offset-md="1" v-if="commentsLoaded">
|
||||
<h5 class="text-h4 text-center my-4">{{ $t('actions.related_comments') }}</h5>
|
||||
<VideoComment v-for="comment in comments.comments" :key="comment.commentId" :comment="comment"
|
||||
:video="video" />
|
||||
<v-progress-linear indeterminate v-intersect="onCommentsProgressIntersect"
|
||||
v-if="comments.comments.length !== 0 && comments.nextpage != null" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" offset-md="1" style="display: flex; justify-content: center" v-else-if="$store.getters['prefs/getPreferenceBoolean']('disableCommentsByDefault')">
|
||||
<v-btn x-large @click="fetchComments">{{ $t('actions.loadComments') }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" v-if="video && video.relatedStreams && $store.getters['prefs/getPreferenceBoolean']('showRelatedVideos')">
|
||||
<h5 class="text-h4 text-center my-4">{{ $t('actions.related_videos') }}</h5>
|
||||
<VideoItem class="my-4" v-for="related in video.relatedStreams" :video="related" :key="related.url" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col cols="12" md="8" offset-md="1" v-if="commentsLoaded">
|
||||
<h5 class="text-h4 text-center my-4">{{ $t('actions.related_comments') }}</h5>
|
||||
<VideoComment v-for="comment in comments.comments" :key="comment.commentId" :comment="comment"
|
||||
:video="video" />
|
||||
<v-progress-linear indeterminate v-intersect="onCommentsProgressIntersect"
|
||||
v-if="comments.comments.length !== 0 && comments.nextpage != null" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" offset-md="1" style="display: flex; justify-content: center"
|
||||
v-else-if="$store.getters['prefs/getPreferenceBoolean']('disableCommentsByDefault')">
|
||||
<v-btn x-large @click="fetchComments">{{ $t('actions.loadComments') }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2"
|
||||
v-if="video && video.relatedStreams && $store.getters['prefs/getPreferenceBoolean']('showRelatedVideos')">
|
||||
<h5 class="text-h4 text-center my-4">{{ $t('actions.related_videos') }}</h5>
|
||||
<VideoItem class="my-4" v-for="related in video.relatedStreams" :video="related" :key="related.url" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -288,12 +276,21 @@ export default {
|
||||
if (selectedSkip.length === 0) {
|
||||
return
|
||||
}
|
||||
this.sponsors = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/sponsors/' + this.videoId,
|
||||
params: {
|
||||
category: JSON.stringify(selectedSkip)
|
||||
try {
|
||||
this.sponsors = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/sponsors/' + this.videoId,
|
||||
params: {
|
||||
category: JSON.stringify(selectedSkip)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
// Just skip if sponsors are not available for some reason
|
||||
if (e.response?.status === 500) {
|
||||
this.sponsors = null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async fetchComments () {
|
||||
this.comments = await this.$store.dispatch('auth/makeRequest', {
|
||||
|
||||
@@ -74,7 +74,7 @@ const AuthenticationStore = {
|
||||
})
|
||||
resp = data.data
|
||||
} catch (e) {
|
||||
if (e.response.status === 400) {
|
||||
if (e.response.status === 400 || e.response.status === 401) {
|
||||
const errorData = e.response.data
|
||||
if ('error' in errorData) {
|
||||
throw new AuthenticationError(errorData.error)
|
||||
|
||||
Reference in New Issue
Block a user