4
1
mirror of https://github.com/Qortal/qortal-ui.git synced 2025-02-11 17:55:51 +00:00

improve loading of img in chat

This commit is contained in:
PhilReact 2023-09-20 00:30:12 -05:00
parent 2de6f4d25b
commit 5c6529d269
5 changed files with 2899 additions and 1966 deletions

View 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);

View File

@ -3895,9 +3895,10 @@ class ChatPage extends LitElement {
new Compressor(image, {
quality: .6,
maxWidth: 1200,
mimeType: 'image/webp',
success(result) {
const file = new File([result], "name", {
type: image.type
type: 'image/webp'
})
compressedFile = file
resolve()

File diff suppressed because it is too large Load Diff

View File

@ -1,126 +1,133 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import {
use,
get,
translate,
translateUnsafeHTML,
registerTranslateConfig,
} from 'lit-translate';
export class ImageComponent extends LitElement {
static get properties() {
return {
class: { type: String },
gif: { type: Object },
alt: { type: String },
attempts: { type: Number },
maxAttempts: { type: Number },
error: { type: Boolean },
sendMessage: { 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
static get properties() {
return {
class: { type: String },
gif: { type: Object },
alt: { type: String },
attempts: { type: Number },
maxAttempts: { type: Number },
error: { type: Boolean },
sendMessage: { attribute: false },
setOpenGifModal: { attribute: false },
};
this.requestUpdate();
} else if (!data.ok || data.error) {
this.error = true
} else {
this.error = 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;
} else {
this.error = false;
}
} catch (error) {
this.error = true;
console.error(error);
this._fetchImage();
}
} catch (error) {
this.error = true
console.error(error)
this._fetchImage()
}
render() {
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() {
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)
customElements.define('image-component', ImageComponent);

View File

@ -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();
}
}
}
}