From 8eacfca4d153ca22d8bde2869d056947c621533b Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 8 Oct 2023 23:17:21 -0500 Subject: [PATCH] started friends feed --- core/language/us.json | 4 +- .../friends-view/ChatSideNavHeads.js | 2 +- core/src/components/friends-view/feed-item.js | 341 ++++++++++++++++++ .../friends-view/friend-item-actions.js | 12 + .../components/friends-view/friends-feed.js | 115 +++++- .../components/friends-view/friends-view.js | 2 +- 6 files changed, 466 insertions(+), 10 deletions(-) create mode 100644 core/src/components/friends-view/feed-item.js diff --git a/core/language/us.json b/core/language/us.json index 257c770b..b99cb5df 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -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" } diff --git a/core/src/components/friends-view/ChatSideNavHeads.js b/core/src/components/friends-view/ChatSideNavHeads.js index f74536d0..534756ac 100644 --- a/core/src/components/friends-view/ChatSideNavHeads.js +++ b/core/src/components/friends-view/ChatSideNavHeads.js @@ -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)} diff --git a/core/src/components/friends-view/feed-item.js b/core/src/components/friends-view/feed-item.js new file mode 100644 index 00000000..d5f40fe2 --- /dev/null +++ b/core/src/components/friends-view/feed-item.js @@ -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` +
+ ${ + this.status.status !== 'READY' + ? html` +
+
+

${`${Math.round(this.status.percentLoaded || 0 + ).toFixed(0)}% `}${translate('chatpage.cchange94')}

+
+ ` + : '' + } + ${this.status.status === 'READY' && this.feedItem ? html` +
+ ready +
+ ` : ''} + +
+ + ` + + + } +} + +customElements.define('feed-item', FeedItem); diff --git a/core/src/components/friends-view/friend-item-actions.js b/core/src/components/friends-view/friend-item-actions.js index 6b0f55af..19ade862 100644 --- a/core/src/components/friends-view/friend-item-actions.js +++ b/core/src/components/friends-view/friend-item-actions.js @@ -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(); }}" + > + edit ${translate('friends.friend10')} @@ -186,6 +192,9 @@ export class FriendItemActions extends connect(store)(LitElement) { ); this.closePopover(); }}" + > + send ${translate('friends.friend8')} @@ -212,6 +221,9 @@ export class FriendItemActions extends connect(store)(LitElement) { ); this.closePopover(); }}" + > + mail ${translate('friends.friend9')} diff --git a/core/src/components/friends-view/friends-feed.js b/core/src/components/friends-view/friends-feed.js index a20d1733..2be72bf0 100644 --- a/core/src/components/friends-view/friends-feed.js +++ b/core/src/components/friends-view/friends-feed.js @@ -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`
- hi - ${this.feed.map((item) => { - return html`

hello

`; + return html``; })}
@@ -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)**" diff --git a/core/src/components/friends-view/friends-view.js b/core/src/components/friends-view/friends-view.js index c4f3c198..80b006ee 100644 --- a/core/src/components/friends-view/friends-view.js +++ b/core/src/components/friends-view/friends-view.js @@ -246,7 +246,7 @@ class FriendsView extends connect(store)(LitElement) {