mirror of
https://github.com/Qortal/qortal-ui.git
synced 2025-03-27 15:55:55 +00:00
improve loading of img in chat
This commit is contained in:
parent
2de6f4d25b
commit
5c6529d269
289
plugins/plugins/core/components/ChatImage.js
Normal file
289
plugins/plugins/core/components/ChatImage.js
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import { LitElement, html, css } from 'lit';
|
||||||
|
import { render } from 'lit/html.js';
|
||||||
|
import {
|
||||||
|
use,
|
||||||
|
get,
|
||||||
|
translate,
|
||||||
|
translateUnsafeHTML,
|
||||||
|
registerTranslateConfig,
|
||||||
|
} from 'lit-translate';
|
||||||
|
import axios from 'axios'
|
||||||
|
import { RequestQueueWithPromise } from '../../utils/queue';
|
||||||
|
|
||||||
|
const requestQueue = new RequestQueueWithPromise(5);
|
||||||
|
|
||||||
|
export class ChatImage extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
resource: { type: Object },
|
||||||
|
isReady: { type: Boolean},
|
||||||
|
status: {type: Object},
|
||||||
|
setOpenDialogImage: { attribute: false}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
img {
|
||||||
|
max-width:45vh;
|
||||||
|
max-height:40vh;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.smallLoading,
|
||||||
|
.smallLoading:after {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallLoading {
|
||||||
|
border-width: 0.8em;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(3, 169, 244, 0.2) rgba(3, 169, 244, 0.2)
|
||||||
|
rgba(3, 169, 244, 0.2) rgb(3, 169, 244);
|
||||||
|
font-size: 30px;
|
||||||
|
position: relative;
|
||||||
|
text-indent: -9999em;
|
||||||
|
transform: translateZ(0px);
|
||||||
|
animation: 1.1s linear 0s infinite normal none running loadingAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defaultSize {
|
||||||
|
width: 45vh;
|
||||||
|
height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@-webkit-keyframes loadingAnimation {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loadingAnimation {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.resource = {
|
||||||
|
identifier: "",
|
||||||
|
name: "",
|
||||||
|
service: ""
|
||||||
|
}
|
||||||
|
this.status = {
|
||||||
|
status: ''
|
||||||
|
}
|
||||||
|
this.url = ""
|
||||||
|
this.isReady = false
|
||||||
|
this.nodeUrl = this.getNodeUrl()
|
||||||
|
this.myNode = this.getMyNode()
|
||||||
|
this.hasCalledWhenDownloaded = false
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting && this.status.status !== 'READY') {
|
||||||
|
this._fetchImage();
|
||||||
|
// Stop observing after the image has started loading
|
||||||
|
this.observer.unobserve(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getNodeUrl(){
|
||||||
|
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
|
||||||
|
|
||||||
|
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port
|
||||||
|
return nodeUrl
|
||||||
|
}
|
||||||
|
getMyNode(){
|
||||||
|
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
|
||||||
|
|
||||||
|
return myNode
|
||||||
|
}
|
||||||
|
|
||||||
|
getApiKey() {
|
||||||
|
const myNode =
|
||||||
|
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
|
||||||
|
window.parent.reduxStore.getState().app.nodeConfig.node
|
||||||
|
];
|
||||||
|
let apiKey = myNode.apiKey;
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchResource() {
|
||||||
|
try {
|
||||||
|
// await qortalRequest({
|
||||||
|
// action: 'GET_QDN_RESOURCE_PROPERTIES',
|
||||||
|
// name,
|
||||||
|
// service,
|
||||||
|
// identifier
|
||||||
|
// })
|
||||||
|
await axios.get(`${this.nodeUrl}/arbitrary/resource/properties/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchVideoUrl() {
|
||||||
|
|
||||||
|
this.fetchResource()
|
||||||
|
this.url = `${this.nodeUrl}/arbitrary/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?async=true&apiKey=${this.myNode.apiKey}`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchStatus(){
|
||||||
|
let isCalling = false
|
||||||
|
let percentLoaded = 0
|
||||||
|
let timer = 24
|
||||||
|
const response = await axios.get(`${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
|
||||||
|
if(response && response.data && response.data.status === 'READY'){
|
||||||
|
this.status = response.data
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
if (isCalling) return
|
||||||
|
isCalling = true
|
||||||
|
|
||||||
|
const data = await requestQueue.enqueue(() => {
|
||||||
|
return axios.get(`${this.nodeUrl}/arbitrary/resource/status/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
|
||||||
|
});
|
||||||
|
const res = data.data
|
||||||
|
|
||||||
|
isCalling = false
|
||||||
|
if (res.localChunkCount) {
|
||||||
|
if (res.percentLoaded) {
|
||||||
|
if (
|
||||||
|
res.percentLoaded === percentLoaded &&
|
||||||
|
res.percentLoaded !== 100
|
||||||
|
) {
|
||||||
|
timer = timer - 5
|
||||||
|
} else {
|
||||||
|
timer = 24
|
||||||
|
}
|
||||||
|
if (timer < 0) {
|
||||||
|
timer = 24
|
||||||
|
isCalling = true
|
||||||
|
this.status = {
|
||||||
|
...res,
|
||||||
|
status: 'REFETCHING'
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isCalling = false
|
||||||
|
this.fetchResource({
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
identifier
|
||||||
|
})
|
||||||
|
}, 25000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
percentLoaded = res.percentLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
this.status = res
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if progress is 100% and clear interval if true
|
||||||
|
if (res?.status === 'READY') {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
this.status = res
|
||||||
|
this.isReady = true
|
||||||
|
}
|
||||||
|
}, 5000) // 1 second interval
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchImage() {
|
||||||
|
try {
|
||||||
|
this.fetchVideoUrl({
|
||||||
|
name: this.resource.name,
|
||||||
|
service: this.resource.service,
|
||||||
|
identifier: this.resource.identifier
|
||||||
|
})
|
||||||
|
this.fetchStatus()
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(){
|
||||||
|
this.observer.observe(this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldUpdate(changedProperties) {
|
||||||
|
if (changedProperties.has('setOpenDialogImage') && changedProperties.size === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
async updated(changedProperties) {
|
||||||
|
if (changedProperties && changedProperties.has('status')) {
|
||||||
|
if(this.hasCalledWhenDownloaded === false && this.status.status === 'DOWNLOADED'){
|
||||||
|
this.fetchResource()
|
||||||
|
this.hasCalledWhenDownloaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class=${[
|
||||||
|
`image-container`,
|
||||||
|
this.status.status !== 'READY'
|
||||||
|
? 'defaultSize'
|
||||||
|
: '',
|
||||||
|
this.status.status !== 'READY'
|
||||||
|
? 'hideImg'
|
||||||
|
: '',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
${
|
||||||
|
this.status.status !== 'READY'
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:center;align-items:center;position:absolute;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=${`smallLoading`}
|
||||||
|
></div>
|
||||||
|
<p>${`${Math.round(this.status.percentLoaded || 0
|
||||||
|
).toFixed(0)}% loaded`}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
${this.status.status === 'READY' ? html`
|
||||||
|
<img @click=${()=> this.setOpenDialogImage(true)} src=${this.url} />
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('chat-image', ChatImage);
|
@ -3895,9 +3895,10 @@ class ChatPage extends LitElement {
|
|||||||
new Compressor(image, {
|
new Compressor(image, {
|
||||||
quality: .6,
|
quality: .6,
|
||||||
maxWidth: 1200,
|
maxWidth: 1200,
|
||||||
|
mimeType: 'image/webp',
|
||||||
success(result) {
|
success(result) {
|
||||||
const file = new File([result], "name", {
|
const file = new File([result], "name", {
|
||||||
type: image.type
|
type: 'image/webp'
|
||||||
})
|
})
|
||||||
compressedFile = file
|
compressedFile = file
|
||||||
resolve()
|
resolve()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,126 +1,133 @@
|
|||||||
import { LitElement, html, css } from 'lit'
|
import { LitElement, html, css } from 'lit';
|
||||||
import { render } from 'lit/html.js'
|
import { render } from 'lit/html.js';
|
||||||
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
|
import {
|
||||||
|
use,
|
||||||
|
get,
|
||||||
|
translate,
|
||||||
|
translateUnsafeHTML,
|
||||||
|
registerTranslateConfig,
|
||||||
|
} from 'lit-translate';
|
||||||
|
|
||||||
export class ImageComponent extends LitElement {
|
export class ImageComponent extends LitElement {
|
||||||
|
static get properties() {
|
||||||
static get properties() {
|
return {
|
||||||
return {
|
class: { type: String },
|
||||||
class: { type: String },
|
gif: { type: Object },
|
||||||
gif: { type: Object },
|
alt: { type: String },
|
||||||
alt: { type: String },
|
attempts: { type: Number },
|
||||||
attempts: { type: Number },
|
maxAttempts: { type: Number },
|
||||||
maxAttempts: { type: Number },
|
error: { type: Boolean },
|
||||||
error: { type: Boolean },
|
sendMessage: { attribute: false },
|
||||||
sendMessage: { attribute: false },
|
setOpenGifModal: { attribute: false },
|
||||||
setOpenGifModal: { attribute: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
.gif-error-msg {
|
|
||||||
margin: 0;
|
|
||||||
font-family: Roboto, sans-serif;
|
|
||||||
font-size: 17px;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
color: var(--chat-bubble-msg-color);
|
|
||||||
font-weight: 300;
|
|
||||||
padding: 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gif-image {
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
height: 150px;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition: all 0.2s cubic-bezier(0, 0.55, 0.45, 1);
|
|
||||||
box-shadow: rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gif-image:hover {
|
|
||||||
border: 1px solid var(--mdc-theme-primary );
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.attempts = 0
|
|
||||||
this.maxAttempts = 5
|
|
||||||
}
|
|
||||||
getApiKey() {
|
|
||||||
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
|
|
||||||
let apiKey = myNode.apiKey
|
|
||||||
return apiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
async _fetchImage() {
|
|
||||||
this.attempts++;
|
|
||||||
if (this.attempts > this.maxAttempts) return
|
|
||||||
await new Promise((res) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
res()
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.gif.url)
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.ok) {
|
|
||||||
this.error = false
|
|
||||||
this.gif = {
|
|
||||||
...this.gif,
|
|
||||||
url: data.src
|
|
||||||
};
|
};
|
||||||
this.requestUpdate();
|
}
|
||||||
} else if (!data.ok || data.error) {
|
|
||||||
this.error = true
|
static get styles() {
|
||||||
} else {
|
return css`
|
||||||
this.error = false
|
.gif-error-msg {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
|
font-size: 17px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: var(--chat-bubble-msg-color);
|
||||||
|
font-weight: 300;
|
||||||
|
padding: 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gif-image {
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s cubic-bezier(0, 0.55, 0.45, 1);
|
||||||
|
box-shadow: rgb(50 50 93 / 25%) 0px 6px 12px -2px,
|
||||||
|
rgb(0 0 0 / 30%) 0px 3px 7px -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gif-image:hover {
|
||||||
|
border: 1px solid var(--mdc-theme-primary);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attempts = 0;
|
||||||
|
this.maxAttempts = 5;
|
||||||
|
}
|
||||||
|
getApiKey() {
|
||||||
|
const myNode =
|
||||||
|
window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
|
||||||
|
window.parent.reduxStore.getState().app.nodeConfig.node
|
||||||
|
];
|
||||||
|
let apiKey = myNode.apiKey;
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchImage() {
|
||||||
|
this.attempts++;
|
||||||
|
if (this.attempts > this.maxAttempts) return;
|
||||||
|
await new Promise((res) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.gif.url);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.ok) {
|
||||||
|
this.error = false;
|
||||||
|
this.gif = {
|
||||||
|
...this.gif,
|
||||||
|
url: data.src,
|
||||||
|
};
|
||||||
|
this.requestUpdate();
|
||||||
|
} else if (!data.ok || data.error) {
|
||||||
|
this.error = true;
|
||||||
|
} else {
|
||||||
|
this.error = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.error = true;
|
||||||
|
console.error(error);
|
||||||
|
this._fetchImage();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
this.error = true
|
|
||||||
console.error(error)
|
render() {
|
||||||
this._fetchImage()
|
if (this.error && this.attempts <= this.maxAttempts) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._fetchImage();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return html` ${this.gif && !this.error
|
||||||
|
? html` <img
|
||||||
|
class=${this.class}
|
||||||
|
src=${this.gif.url}
|
||||||
|
alt=${this.alt}
|
||||||
|
@click=${() => {
|
||||||
|
this.sendMessage({
|
||||||
|
type: 'gif',
|
||||||
|
identifier: this.gif.identifier,
|
||||||
|
name: this.gif.name,
|
||||||
|
filePath: this.gif.filePath,
|
||||||
|
service: 'GIF_REPOSITORY',
|
||||||
|
});
|
||||||
|
this.setOpenGifModal(false);
|
||||||
|
}}
|
||||||
|
@error=${this._fetchImage}
|
||||||
|
/>`
|
||||||
|
: this.error && this.attempts <= this.maxAttempts
|
||||||
|
? html`
|
||||||
|
<p class="gif-error-msg">${translate('gifs.gchange15')}</p>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<p class="gif-error-msg">${translate('gifs.gchange16')}</p>
|
||||||
|
`}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
customElements.define('image-component', ImageComponent);
|
||||||
if (this.error && this.attempts <= this.maxAttempts) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this._fetchImage()
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
${this.gif && !this.error
|
|
||||||
? html`
|
|
||||||
<img
|
|
||||||
class=${this.class}
|
|
||||||
src=${this.gif.url}
|
|
||||||
alt=${this.alt}
|
|
||||||
@click=${() => {
|
|
||||||
this.sendMessage({
|
|
||||||
type: 'gif',
|
|
||||||
identifier: this.gif.identifier,
|
|
||||||
name: this.gif.name,
|
|
||||||
filePath: this.gif.filePath,
|
|
||||||
service: "GIF_REPOSITORY"
|
|
||||||
})
|
|
||||||
this.setOpenGifModal(false);
|
|
||||||
}}
|
|
||||||
@error=${this._fetchImage}
|
|
||||||
/>`
|
|
||||||
: this.error && this.attempts <= this.maxAttempts ? html`
|
|
||||||
<p class='gif-error-msg'>${translate('gifs.gchange15')}</p>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<p class='gif-error-msg'>${translate('gifs.gchange16')}</p>
|
|
||||||
`
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('image-component', ImageComponent)
|
|
||||||
|
@ -32,3 +32,40 @@ export class RequestQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RequestQueueWithPromise {
|
||||||
|
constructor(maxConcurrent = 5) {
|
||||||
|
this.queue = [];
|
||||||
|
this.maxConcurrent = maxConcurrent;
|
||||||
|
this.currentlyProcessing = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a request to the queue and return a promise
|
||||||
|
enqueue(request) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Push the request and its resolve and reject callbacks to the queue
|
||||||
|
this.queue.push({ request, resolve, reject });
|
||||||
|
this.process();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process requests in the queue
|
||||||
|
async process() {
|
||||||
|
while (this.queue.length > 0 && this.currentlyProcessing < this.maxConcurrent) {
|
||||||
|
this.currentlyProcessing++;
|
||||||
|
const { request, resolve, reject } = this.queue.shift();
|
||||||
|
try {
|
||||||
|
const response = await request();
|
||||||
|
resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
this.currentlyProcessing--;
|
||||||
|
this.process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user