Merge pull request #48 from KevinMidboe/refactor/image-loading
Refactor/image loading
This commit is contained in:
@@ -2,28 +2,32 @@
|
||||
<section class="movie">
|
||||
|
||||
<!-- HEADER w/ POSTER -->
|
||||
<header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }" :class="compact ? 'compact' : ''" @click="compact=!compact">
|
||||
<div class="movie__wrap movie__wrap--header">
|
||||
<figure class="movie__poster">
|
||||
<header ref="header" :class="compact ? 'compact' : ''" @click="compact=!compact">
|
||||
<figure class="movie__poster">
|
||||
<img class="movie-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
src="~assets/placeholder.png">
|
||||
|
||||
<!-- -->
|
||||
<!-- <img v-else class="movie-item__img is-loaded" src="~assets/no-image.png" ref="image" alt="No image - linked image unavailable"> -->
|
||||
</figure>
|
||||
<!-- <figure class="movie__poster">
|
||||
<img v-if="movie && poster === null"
|
||||
class="movies-item__img is-loaded"
|
||||
alt="movie poster image"
|
||||
src="~assets/no-image.png">
|
||||
<img v-else-if="poster === undefined"
|
||||
class="movies-item__img grey"
|
||||
alt="movie poster image">
|
||||
<!-- src="~assets/placeholder.png"> -->
|
||||
alt="movie poster image"
|
||||
src="~assets/placeholder.png">
|
||||
<img v-else
|
||||
class="movies-item__img is-loaded"
|
||||
alt="movie poster image"
|
||||
:src="ASSET_URL + ASSET_SIZES[0] + poster">
|
||||
</figure>
|
||||
</figure> -->
|
||||
|
||||
<div class="movie__title">
|
||||
<h1 v-if="movie">{{ movie.title }}</h1>
|
||||
<loading-placeholder v-else :count="1" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="movie__title" v-if="movie">{{ movie.title }}</h1>
|
||||
<loading-placeholder v-else :count="1" />
|
||||
</header>
|
||||
|
||||
<!-- Siderbar and movie info -->
|
||||
@@ -64,32 +68,48 @@
|
||||
|
||||
<!-- MOVIE INFO -->
|
||||
<div class="movie__info">
|
||||
<div class="movie__description" v-if="movie"> {{ movie.overview }}</div>
|
||||
|
||||
<!-- Loading placeholder -->
|
||||
<div class="movie__description noselect"
|
||||
@click="truncatedDescription=!truncatedDescription"
|
||||
v-if="!loading">
|
||||
<span :class="truncatedDescription ? 'truncated':null">{{ movie.overview }}</span>
|
||||
<button class="truncate-toggle"><i>⬆</i></button>
|
||||
</div>
|
||||
<div v-else class="movie__description">
|
||||
<loading-placeholder :count="12" />
|
||||
<loading-placeholder :count="5" />
|
||||
</div>
|
||||
|
||||
<div class="movie__details" v-if="movie">
|
||||
<div v-if="movie.year" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Release Date</h2>
|
||||
<div class="movie__details-text">{{ movie.year }}</div>
|
||||
<div v-if="movie.year">
|
||||
<h2 class="title">Release Date</h2>
|
||||
<div class="text">{{ movie.year }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.rank" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Rating</h2>
|
||||
<div class="movie__details-text">{{ movie.rank }}</div>
|
||||
<div v-if="movie.rating">
|
||||
<h2 class="title">Rating</h2>
|
||||
<div class="text">{{ movie.rating }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Seasons</h2>
|
||||
<div class="movie__details-text">{{ movie.seasons }}</div>
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Seasons</h2>
|
||||
<div class="text">{{ movie.seasons }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.genres" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Genres</h2>
|
||||
<div class="movie__details-text">{{ nestedDataToString(movie.genres) }}</div>
|
||||
<div v-if="movie.genres">
|
||||
<h2 class="title">Genres</h2>
|
||||
<div class="text">{{ movie.genres.join(', ') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Production status</h2>
|
||||
<div class="text">{{ movie.production_status }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Runtime</h2>
|
||||
<div class="text">{{ movie.runtime[0] }} minutes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,7 +146,17 @@ import LoadingPlaceholder from './ui/LoadingPlaceholder'
|
||||
import { getMovie, getPerson, getShow, request, getRequestStatus } from '@/api'
|
||||
|
||||
export default {
|
||||
props: ['id', 'type'],
|
||||
// props: ['id', 'type'],
|
||||
props: {
|
||||
id: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
type: {
|
||||
required: false,
|
||||
type: String
|
||||
}
|
||||
},
|
||||
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
|
||||
directives: { img: img }, // TODO decide to remove or use
|
||||
data(){
|
||||
@@ -142,11 +172,40 @@ export default {
|
||||
requested: false,
|
||||
admin: localStorage.getItem('admin') == "true" ? true : false,
|
||||
showTorrents: false,
|
||||
compact: false
|
||||
compact: false,
|
||||
loading: true,
|
||||
truncatedDescription: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id: function(val){
|
||||
if (this.type === 'movie') {
|
||||
this.fetchMovie(val);
|
||||
} else {
|
||||
this.fetchShow(val)
|
||||
}
|
||||
},
|
||||
backdrop: function(backdrop) {
|
||||
if (backdrop != null) {
|
||||
const style = {
|
||||
backgroundImage: 'url(' + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ')'
|
||||
}
|
||||
|
||||
Object.assign(this.$refs.header.style, style)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
numberOfTorrentResults: () => {
|
||||
let numTorrents = store.getters['torrentModule/resultCount']
|
||||
return numTorrents !== null ? numTorrents + ' results' : null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseResponse(movie) {
|
||||
setTimeout(() => {
|
||||
|
||||
this.loading = false
|
||||
this.movie = { ...movie }
|
||||
this.title = movie.title
|
||||
this.poster = movie.poster
|
||||
@@ -155,13 +214,22 @@ export default {
|
||||
this.checkIfRequested(movie)
|
||||
.then(status => this.requested = status)
|
||||
|
||||
|
||||
store.dispatch('documentTitle/updateTitle', movie.title)
|
||||
this.setPosterSrc()
|
||||
}, 1000)
|
||||
},
|
||||
async checkIfRequested(movie) {
|
||||
return await getRequestStatus(movie.id, movie.type)
|
||||
},
|
||||
nestedDataToString(data) {
|
||||
return data.join(', ')
|
||||
setPosterSrc() {
|
||||
const poster = this.$refs['poster-image']
|
||||
if (this.poster == null) {
|
||||
poster.src = '/dist/no-image.png'
|
||||
return
|
||||
}
|
||||
|
||||
poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`
|
||||
},
|
||||
sendRequest(){
|
||||
request(this.id, this.type, storage.token)
|
||||
@@ -176,25 +244,7 @@ export default {
|
||||
window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id: function(val){
|
||||
if (this.type === 'movie') {
|
||||
this.fetchMovie(val);
|
||||
} else {
|
||||
this.fetchShow(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
numberOfTorrentResults: () => {
|
||||
let numTorrents = store.getters['torrentModule/resultCount']
|
||||
return numTorrents !== null ? numTorrents + ' results' : null
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
|
||||
},
|
||||
created(){
|
||||
created() {
|
||||
this.prevDocumentTitle = store.getters['documentTitle/title']
|
||||
|
||||
if (this.type === 'movie') {
|
||||
@@ -216,8 +266,9 @@ export default {
|
||||
this.$router.push({ name: '404' });
|
||||
})
|
||||
}
|
||||
|
||||
console.log('admin: ', this.admin)
|
||||
},
|
||||
beforeDestroy() {
|
||||
store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -226,6 +277,89 @@ export default {
|
||||
@import "./src/scss/loading-placeholder";
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/main";
|
||||
|
||||
header {
|
||||
$duration: 0.2s;
|
||||
height: 250px;
|
||||
transform: scaleY(1);
|
||||
transition: height $duration ease;
|
||||
transform-origin: top;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: $background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@include tablet-min {
|
||||
height: 350px;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $background-dark-85;
|
||||
}
|
||||
@include mobile {
|
||||
&.compact {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.movie__poster {
|
||||
display: none;
|
||||
|
||||
@include desktop {
|
||||
background: $background-color;
|
||||
height: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: calc(45% - 40px);
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.truncate-toggle {
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: $text-color;
|
||||
|
||||
> i {
|
||||
font-style: unset;
|
||||
font-size: 0.7rem;
|
||||
transition: 0.3s ease all;
|
||||
transform: rotateY(180deg)
|
||||
}
|
||||
|
||||
&::before, &::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
}
|
||||
&::before {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&::after {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
&__wrap {
|
||||
@@ -246,49 +380,6 @@ export default {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
&__header {
|
||||
$duration: 0.2s;
|
||||
height: 250px;
|
||||
transform: scaleY(1);
|
||||
transition: height $duration ease;
|
||||
transform-origin: top;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: $background-color;
|
||||
@include tablet-min {
|
||||
height: 350px;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $background-dark-85;
|
||||
}
|
||||
&.compact {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
&__poster {
|
||||
display: none;
|
||||
@include tablet-min {
|
||||
background: $background-color;
|
||||
height: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: calc(45% - 40px);
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
display: block;
|
||||
@@ -364,37 +455,49 @@ export default {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
|
||||
& .truncated {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
& + .truncate-toggle > i {
|
||||
transform: rotateY(0deg) rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
&__details {
|
||||
&-block {
|
||||
float: left;
|
||||
}
|
||||
&-block:not(:last-child) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: $green;
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
& .title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: $green;
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
& .text {
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
&-text {
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
&__admin {
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<template>
|
||||
<li class="movies-item" :class="{'shortList': shortList}">
|
||||
<a class="movies-item__link" :class="{'no-image': !movie}" @click.prevent="openMoviePopup(movie.id, movie.type)">
|
||||
<li class="movie-item" :class="{'shortList': shortList}">
|
||||
<figure class="movie-item__poster">
|
||||
<img class="movie-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
@click="openMoviePopup(movie.id, movie.type)"
|
||||
:alt="posterAltText"
|
||||
:data-src="poster"
|
||||
src="~assets/placeholder.png">
|
||||
|
||||
<!-- TODO change to picture element -->
|
||||
<figure class="movies-item__poster">
|
||||
<img v-if="movie.poster" class="movies-item__img is-loaded" ref="image" src="~assets/placeholder.png" :alt="`${movie.title} poster image`">
|
||||
|
||||
<div v-if="movie.download" class="progress">
|
||||
<progress :value="movie.download.progress" max="100"></progress>
|
||||
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="movies-item__content">
|
||||
<p class="movies-item__title">{{ movie.title || movie.name }}</p>
|
||||
<p class="movies-item__title">{{ movie.year }}</p>
|
||||
<div v-if="movie.download" class="progress">
|
||||
<progress :value="movie.download.progress" max="100"></progress>
|
||||
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
|
||||
</div>
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<div class="movie-item__info">
|
||||
<p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p>
|
||||
<p v-if="movie.year">{{ movie.year }}</p>
|
||||
<p v-if="movie.type == 'person'">Known for: {{ movie.known_for_department }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -38,6 +41,8 @@ export default {
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
poster: undefined,
|
||||
observed: false,
|
||||
posterSizes: [{
|
||||
id: 'w500',
|
||||
minWidth: 500
|
||||
@@ -54,37 +59,35 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
posterUrl: function() {
|
||||
if (this.movie.poster == null)
|
||||
return "~assets/no-image.png"
|
||||
|
||||
const correctWidth = this.posterQualityIdentifierFromPosterWidth
|
||||
|
||||
return `https://image.tmdb.org/t/p/${correctWidth}${this.movie.poster}`
|
||||
},
|
||||
posterQualityIdentifierFromPosterWidth: function() {
|
||||
const posterWidth = this.$refs.image.clientHeight
|
||||
if (posterWidth > this.posterSizes[0].minWidth)
|
||||
return this.posterSizes[0].id
|
||||
|
||||
const widthCandidates = this.posterSizes.filter(size => posterWidth < size.minWidth ? size.id : null)
|
||||
return widthCandidates[widthCandidates.length - 1].id
|
||||
posterAltText: function() {
|
||||
const type = this.movie.type || ''
|
||||
const title = this.movie.title || this.movie.name
|
||||
return this.movie.poster ? `Poster for ${type} ${title}` : `Missing image for ${type} ${title}`
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.movie.poster != null) {
|
||||
this.poster = 'https://image.tmdb.org/t/p/w500' + this.movie.poster
|
||||
} else {
|
||||
this.poster = '/dist/no-image.png'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.image == undefined)
|
||||
const poster = this.$refs['poster-image']
|
||||
if (poster == null)
|
||||
return
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry.isIntersecting && this.observed == false) {
|
||||
const lazyImage = entry.target
|
||||
lazyImage.src = this.posterUrl
|
||||
lazyImage.class
|
||||
lazyImage.src = lazyImage.dataset.src
|
||||
this.observed = true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
imageObserver.observe(this.$refs.image);
|
||||
imageObserver.observe(poster);
|
||||
},
|
||||
methods: {
|
||||
openMoviePopup(id, type) {
|
||||
@@ -94,74 +97,103 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/main";
|
||||
|
||||
.movies-item {
|
||||
.movie-item {
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
background-color: $background-color;
|
||||
transition: background-color 0.5s ease;
|
||||
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
padding: 15px;
|
||||
width: 33%;
|
||||
}
|
||||
@include tablet-landscape-min{
|
||||
@include tablet-landscape-min {
|
||||
padding: 15px;
|
||||
width: 25%;
|
||||
}
|
||||
@include desktop-min{
|
||||
@include desktop-min {
|
||||
padding: 15px;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
@include desktop-lg-min{
|
||||
@include desktop-lg-min {
|
||||
padding: 15px;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
&__link{
|
||||
&:hover &__info > p {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&__poster {
|
||||
text-decoration: none;
|
||||
color: $text-color-70;
|
||||
font-weight: 300;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
&.is-loaded{
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 10px rgba($dark, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__content{
|
||||
|
||||
&__info {
|
||||
padding-top: 15px;
|
||||
}
|
||||
&__poster{
|
||||
transition: transform 0.5s ease, box-shadow 0.3s ease;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
&__img{
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
&.is-loaded{
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
font-weight: 300;
|
||||
|
||||
> p {
|
||||
color: $text-color-70;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
@include mobile-ls-min{
|
||||
font-size: 12px;
|
||||
}
|
||||
@include tablet-min{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__link:not(.no-image):hover &__poster{
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 10px rgba($dark, 0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.no-image {
|
||||
background-color: var(--text-color);
|
||||
color: var(--background-color);
|
||||
width: 100%;
|
||||
height: 383px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 1.5rem;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&__title{
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
@include mobile-ls-min{
|
||||
font-size: 12px;
|
||||
}
|
||||
@include tablet-min{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
&__link:hover &__title{
|
||||
color: $text-color;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user