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

started friends feed

This commit is contained in:
PhilReact 2023-10-08 23:17:21 -05:00
parent a20cd240ef
commit 8eacfca4d1
6 changed files with 466 additions and 10 deletions

View File

@ -1185,8 +1185,8 @@
"friend5": "Follow name",
"friend6": "Alias",
"friend7": "Add an alias to better remember your friend (Optional)",
"friend8": "Send a Q-Chat message",
"friend9": "Send a Q-Mail",
"friend8": "Send Q-Chat",
"friend9": "Send Q-Mail",
"friend10": "Edit friend",
"friend11": "Following"
}

View File

@ -164,7 +164,7 @@ class ChatSideNavHeads extends LitElement {
${this.chatInfo.groupName
? this.chatInfo.groupName
: this.chatInfo.name !== undefined
? this.chatInfo.name
? (this.chatInfo.alias || this.chatInfo.name)
: this.chatInfo.address.substr(0, 15)}
</span>
</div>

View File

@ -0,0 +1,341 @@
import { LitElement, html, css } from 'lit';
import {
get,
translate,
} from 'lit-translate';
import axios from 'axios'
import '@material/mwc-menu';
import '@material/mwc-list/mwc-list-item.js'
import { RequestQueueWithPromise } from '../../../../plugins/plugins/utils/queue';
const requestQueue = new RequestQueueWithPromise(5);
export class FeedItem extends LitElement {
static get properties() {
return {
resource: { type: Object },
isReady: { type: Boolean},
status: {type: Object},
feedItem: {type: Object}
};
}
static get styles() {
return css`
* {
--mdc-theme-text-primary-on-background: var(--black);
}
img {
max-width:45vh;
max-height:40vh;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.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;
}
mwc-menu {
position: absolute;
}
@-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.isFetching = 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);
}
}
});
this.feedItem = null
}
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 {
if(this.isFetching) return
this.isFetching = true
await axios.get(`${this.nodeUrl}/arbitrary/resource/properties/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`)
this.isFetching = false
} catch (error) {
this.isFetching = false
}
}
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 getRawData(){
const url = `${this.nodeUrl}/arbitrary/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`
const response2 = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const responseData2 = await response2.json()
return responseData2
}
updateDisplayWithPlaceholders(display, resource, rawdata) {
const pattern = /\$\$\{([a-zA-Z0-9_\.]+)\}\$\$/g;
for (const key in display) {
const value = display[key];
display[key] = value.replace(pattern, (match, p1) => {
if (p1.startsWith('rawdata.')) {
const dataKey = p1.split('.')[1];
if (rawdata[dataKey] === undefined) {
console.error("rawdata key not found:", dataKey);
}
return rawdata[dataKey] || match;
} else if (p1.startsWith('resource.')) {
const resourceKey = p1.split('.')[1];
if (resource[resourceKey] === undefined) {
console.error("resource key not found:", resourceKey);
}
return resource[resourceKey] || match;
}
return match;
});
}
}
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'){
const rawData = await this.getRawData()
const object = {
title: "$${rawdata.title}$$",
}
this.updateDisplayWithPlaceholders(object, {},rawData)
this.feedItem = object
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()
}, 25000)
return
}
percentLoaded = res.percentLoaded
}
this.status = res
if(this.status.status === 'DOWNLOADED'){
this.fetchResource()
}
}
// check if progress is 100% and clear interval if true
if (res.status === 'READY') {
this.feedItem = await this.getRawData()
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) { /* empty */ }
}
firstUpdated(){
this.observer.observe(this);
}
render() {
console.log('this.feedItem', this.feedItem)
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;"
>
<div
class=${`smallLoading`}
></div>
<p style="color: var(--black)">${`${Math.round(this.status.percentLoaded || 0
).toFixed(0)}% `}${translate('chatpage.cchange94')}</p>
</div>
`
: ''
}
${this.status.status === 'READY' && this.feedItem ? html`
<div style="position:relative">
ready
</div>
` : ''}
</div>
`
}
}
customElements.define('feed-item', FeedItem);

View File

@ -38,6 +38,9 @@ export class FriendItemActions extends connect(store)(LitElement) {
text-align: center;
color: var(--mdc-theme-primary);
transition: all 0.3s ease-in-out;
display: flex;
align-items: center;
gap: 10px
}
.send-message-button:hover {
@ -148,6 +151,9 @@ export class FriendItemActions extends connect(store)(LitElement) {
this.openEditFriend();
this.closePopover();
}}"
>
<mwc-icon style="color: var(--black)"
>edit</mwc-icon
>
${translate('friends.friend10')}
</div>
@ -186,6 +192,9 @@ export class FriendItemActions extends connect(store)(LitElement) {
);
this.closePopover();
}}"
>
<mwc-icon style="color: var(--black)"
>send</mwc-icon
>
${translate('friends.friend8')}
</div>
@ -212,6 +221,9 @@ export class FriendItemActions extends connect(store)(LitElement) {
);
this.closePopover();
}}"
>
<mwc-icon style="color: var(--black)"
>mail</mwc-icon
>
${translate('friends.friend9')}
</div>

View File

@ -4,6 +4,10 @@ import './friends-view'
import { friendsViewStyles } from './friends-view-css';
import { connect } from 'pwa-helpers';
import { store } from '../../store';
import './feed-item'
const perEndpointCount = 20;
const totalDesiredCount = 100;
const maxResultsInMemory = 300;
class FriendsFeed extends connect(store)(LitElement) {
static get properties() {
return {
@ -15,6 +19,10 @@ class FriendsFeed extends connect(store)(LitElement) {
this.feed = []
this.nodeUrl = this.getNodeUrl();
this.myNode = this.getMyNode();
this.endpoints = []
this.endpointOffsets = [] // Initialize offsets for each endpoint to 0
this.loadAndMergeData = this.loadAndMergeData.bind(this)
}
static get styles() {
@ -51,7 +59,10 @@ class FriendsFeed extends connect(store)(LitElement) {
const baseurl = `${this.nodeUrl}/arbitrary/resources/search?reverse=true`
const fullUrl = constructUrl(baseurl, feedData.search, dynamicVars);
console.log({fullUrl})
this.endpoints= ['http://127.0.0.1:12391/arbitrary/resources/search?reverse=true&query=-post-&identifier=q-blog-&service=BLOG_POST&exactmatchnames=true&limit=20']
this.endpointOffsets = Array(this.endpoints.length).fill(0); // Initialize offsets for each endpoint to 0
console.log('this.endpoints', this.endpoints)
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
@ -77,22 +88,109 @@ let clickValue1 = schemaObj.feed[0].click;
const resolvedClickValue1 = replacePlaceholders(clickValue1, resource, schemaObj.feed[0].customParams);
console.log(resolvedClickValue1);
this.loadAndMergeData();
} catch (error) {
console.log(error)
}
}
async fetchDataFromEndpoint(endpointIndex, count) {
const offset = this.endpointOffsets[endpointIndex];
const url = `${this.endpoints[endpointIndex]}&limit=${count}&offset=${offset}`;
return fetch(url).then(res => res.json());
}
async initialLoad() {
let results = [];
let totalFetched = 0;
let i = 0;
let madeProgress = true;
let exhaustedEndpoints = new Set();
while (totalFetched < totalDesiredCount && madeProgress) {
madeProgress = false;
for (i = 0; i < this.endpoints.length; i++) {
if (exhaustedEndpoints.has(i)) {
continue;
}
const remainingCount = totalDesiredCount - totalFetched;
// If we've already reached the desired count, break
if (remainingCount <= 0) {
break;
}
let fetchCount = Math.min(perEndpointCount, remainingCount);
let data = await this.fetchDataFromEndpoint(i, fetchCount);
// Increment the offset for this endpoint by the number of items fetched
this.endpointOffsets[i] += data.length;
if (data.length > 0) {
madeProgress = true;
}
if (data.length < fetchCount) {
exhaustedEndpoints.add(i);
}
results = results.concat(data);
totalFetched += data.length;
}
if (exhaustedEndpoints.size === this.endpoints.length) {
break;
}
}
// Trim the results if somehow they are over the totalDesiredCount
return results.slice(0, totalDesiredCount);
}
trimDataToLimit(data, limit) {
return data.slice(0, limit);
}
mergeData(newData, existingData) {
const existingIds = new Set(existingData.map(item => item.identifier)); // Assume each item has a unique 'id'
const uniqueNewData = newData.filter(item => !existingIds.has(item.identifier));
return uniqueNewData.concat(existingData);
}
async loadAndMergeData() {
let allData = this.feed
const newData = await this.initialLoad();
allData = this.mergeData(newData, allData);
allData.sort((a, b) => new Date(b.created) - new Date(a.created)); // Sort by timestamp, most recent first
allData = this.trimDataToLimit(allData, maxResultsInMemory); // Trim to the maximum allowed in memory
this.feed = [...allData]
}
render() {
console.log('ron')
console.log('ron', this.feed)
return html`
<div class="container">
<div id="viewElement" class="container-body" style=${"position: relative"}>
hi
${this.feed.map((item) => {
return html`<p>hello</p>`;
return html`<feed-item
.resource=${{
name: item.name,
service: item.service,
identifier: item.identifier,
}}
></feed-item>`;
})}
<div id="downObserver"></div>
</div>
@ -198,6 +296,7 @@ export function replacePlaceholders(template, resource, customParams) {
export const schema = {
name: "Q-Blog",
defaultFeedIndex: 0,
feed: [
{
id:"post-creation",
@ -213,7 +312,11 @@ export const schema = {
exactmatchnames: true
},
click: "qortal://APP/Q-Blog/$${resource.name}$$/$${customParams.blogId}$$/$${customParams.shortIdentifier}$$",
display: "",
display: {
title: "$${rawdata.title}$$",
description: "$${rawdata.description}$$",
coverImage: "$${rawdata.image}$$"
},
customParams: {
blogId: "**methods.getBlogId(resource)**",
shortIdentifier: "**methods.getShortId(resource)**"

View File

@ -246,7 +246,7 @@ class FriendsView extends connect(store)(LitElement) {
<vaadin-icon
@click=${this.userSearch}
slot="icon"
icon="vaadin:open-book"
icon="vaadin:search"
class="search-icon">
</vaadin-icon>