diff --git a/core/language/us.json b/core/language/us.json index b99cb5df..370137da 100644 --- a/core/language/us.json +++ b/core/language/us.json @@ -1188,6 +1188,8 @@ "friend8": "Send Q-Chat", "friend9": "Send Q-Mail", "friend10": "Edit friend", - "friend11": "Following" + "friend11": "Following", + "friend12": "Friends", + "friend13": "Feed" } } \ No newline at end of file diff --git a/core/src/components/friends-view/feed-item.js b/core/src/components/friends-view/feed-item.js index d5f40fe2..ab67d17e 100644 --- a/core/src/components/friends-view/feed-item.js +++ b/core/src/components/friends-view/feed-item.js @@ -7,15 +7,23 @@ import axios from 'axios' import '@material/mwc-menu'; import '@material/mwc-list/mwc-list-item.js' import { RequestQueueWithPromise } from '../../../../plugins/plugins/utils/queue'; +import '../../../../plugins/plugins/core/components/TimeAgo' +import { connect } from 'pwa-helpers'; +import { store } from '../../store'; +import { setNewTab } from '../../redux/app/app-actions'; +import ShortUniqueId from 'short-unique-id'; + const requestQueue = new RequestQueueWithPromise(5); -export class FeedItem extends LitElement { +export class FeedItem extends connect(store)(LitElement) { static get properties() { return { resource: { type: Object }, isReady: { type: Boolean}, status: {type: Object}, - feedItem: {type: Object} + feedItem: {type: Object}, + appName: {type: String}, + link: {type: String} }; } @@ -23,10 +31,15 @@ export class FeedItem extends LitElement { return css` * { --mdc-theme-text-primary-on-background: var(--black); + box-sizing: border-box; + } + :host { + width: 100%; + box-sizing: border-box; } img { - max-width:45vh; - max-height:40vh; + width:100%; + max-height:30vh; border-radius: 5px; cursor: pointer; position: relative; @@ -51,9 +64,40 @@ export class FeedItem extends LitElement { } .defaultSize { - width: 45vh; - height: 40vh; + width: 100%; + height: 160px; } + .parent-feed-item { + position: relative; + display: flex; + background-color: var(--chat-bubble-bg); + flex-grow: 0; + flex-direction: column; + align-items: flex-start; + justify-content: center; + border-radius: 5px; + padding: 12px 15px 4px 15px; + min-width: 150px; + width: 100%; + box-sizing: border-box; + cursor: pointer; + } + .avatar { + width: 42px; + height: 42px; + } + .feed-item-name { + user-select: none; + color: #03a9f4; + margin-bottom: 5px; + } + + .app-name { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } mwc-menu { position: absolute; @@ -102,6 +146,7 @@ export class FeedItem extends LitElement { this.myNode = this.getMyNode() this.hasCalledWhenDownloaded = false this.isFetching = false + this.uid = new ShortUniqueId() this.observer = new IntersectionObserver(entries => { for (const entry of entries) { @@ -283,11 +328,93 @@ getMyNode(){ - + async goToFeedLink(){ + try { + console.log('this.link', this.link) + let newQuery = this.link + if (newQuery.endsWith('/')) { + newQuery = newQuery.slice(0, -1) + } + const res = await this.extractComponents(newQuery) + if (!res) return + const { service, name, identifier, path } = res + let query = `?service=${service}` + if (name) { + query = query + `&name=${name}` + } + if (identifier) { + query = query + `&identifier=${identifier}` + } + if (path) { + query = query + `&path=${path}` + } + + store.dispatch(setNewTab({ + url: `qdn/browser/index.html${query}`, + id: this.uid.rnd(), + myPlugObj: { + "url": "myapp", + "domain": "core", + "page": `qdn/browser/index.html${query}`, + "title": name, + "icon": 'vaadin:external-browser', + "mwcicon": 'open_in_browser', + "menus": [], + "parent": false + }, + openExisting: true + })) + } catch (error) { + console.log({error}) + } + } - + async extractComponents(url) { + if (!url.startsWith("qortal://")) { + return null + } + + url = url.replace(/^(qortal\:\/\/)/, "") + if (url.includes("/")) { + let parts = url.split("/") + const service = parts[0].toUpperCase() + parts.shift() + const name = parts[0] + parts.shift() + let identifier + + if (parts.length > 0) { + identifier = parts[0] // Do not shift yet + // Check if a resource exists with this service, name and identifier combination + const myNode = store.getState().app.nodeConfig.knownNodes[store.getState().app.nodeConfig.node] + const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port + const url = `${nodeUrl}/arbitrary/resource/status/${service}/${name}/${identifier}?apiKey=${myNode.apiKey}}` + + const res = await fetch(url); + const data = await res.json(); + if (data.totalChunkCount > 0) { + // Identifier exists, so don't include it in the path + parts.shift() + } + else { + identifier = null + } + } + + const path = parts.join("/") + + const components = {} + components["service"] = service + components["name"] = name + components["identifier"] = identifier + components["path"] = path + return components + } + return null +} + @@ -297,6 +424,20 @@ getMyNode(){ render() { console.log('this.feedItem', this.feedItem) + let avatarImg + const avatarUrl = `${this.nodeUrl}/arbitrary/THUMBNAIL/${this.resource.name}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`; + avatarImg = html``; + let avatarImgApp + const avatarUrl2 = `${this.nodeUrl}/arbitrary/THUMBNAIL/${this.appName}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`; + avatarImgApp = html``; return html`
${ this.status.status !== 'READY' ? html`
- ready +
+
+
+ ${avatarImg}
${this.resource.name} +
+
+

${this.feedItem.title}

+
+
+
+ ${avatarImgApp} +
+ +
` : ''} diff --git a/core/src/components/friends-view/friends-feed.js b/core/src/components/friends-view/friends-feed.js index 2be72bf0..9a9b33f5 100644 --- a/core/src/components/friends-view/friends-feed.js +++ b/core/src/components/friends-view/friends-feed.js @@ -5,6 +5,7 @@ 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; @@ -17,18 +18,25 @@ class FriendsFeed extends connect(store)(LitElement) { constructor(){ super() this.feed = [] + this.feedToRender = [] 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) + this.hasInitialFetch = false + this.observerHandler = this.observerHandler.bind(this); + this.elementObserver = this.elementObserver.bind(this) + } static get styles() { return [friendsViewStyles]; } + + getNodeUrl() { const myNode = store.getState().app.nodeConfig.knownNodes[ @@ -49,45 +57,27 @@ class FriendsFeed extends connect(store)(LitElement) { } async firstUpdated(){ - console.log('sup') + this.viewElement = this.shadowRoot.getElementById('viewElement'); + this.downObserverElement = + this.shadowRoot.getElementById('downObserver'); + this.elementObserver(); const feedData = schema.feed[0] let schemaObj = {...schema} const dynamicVars = { - name: 'Phil' + } - const getMail = async () => { + const getEndpoints = async () => { const baseurl = `${this.nodeUrl}/arbitrary/resources/search?reverse=true` const fullUrl = constructUrl(baseurl, feedData.search, dynamicVars); - 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 + this.endpoints= [{url: fullUrl, schemaName: schema.name, schema: feedData }] + this.endpointOffsets = Array(this.endpoints.length).fill(0); - console.log('this.endpoints', this.endpoints) - const response = await fetch(fullUrl, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - - const data = await response.json() - return data; + } try { - getMail() - const resource = { - name: 'Phil', - identifier: 'q-blog-Mugician-post-Love-Explosion-Festival--yJ8kuo' - } - // First, evaluate methods to get values for customParams -await updateCustomParamsWithMethods(schemaObj, resource); -console.log({schemaObj}) -// Now, generate your final URLs -let clickValue1 = schemaObj.feed[0].click; + getEndpoints() -const resolvedClickValue1 = replacePlaceholders(clickValue1, resource, schemaObj.feed[0].customParams); - -console.log(resolvedClickValue1); this.loadAndMergeData(); @@ -96,10 +86,59 @@ this.loadAndMergeData(); } } + getMoreFeed(){ + if(!this.hasInitialFetch) return + console.log('getting more feed') + if(this.feedToRender.length === this.feed.length ) return + this.feedToRender = this.feed.slice(0, this.feedToRender.length + 20) + this.requestUpdate() + } + + + elementObserver() { + const options = { + rootMargin: '0px', + threshold: 1, + }; + // identify an element to observe + console.log('this', this.viewElement, this.downObserverElement) + const elementToObserve = this.downObserverElement; + // passing it a callback function + const observer = new IntersectionObserver( + this.observerHandler, + options + ); + // call `observe()` on that MutationObserver instance, + // passing it the element to observe, and the options object + observer.observe(elementToObserve); + } + + observerHandler(entries) { + console.log({entries}) + if (!entries[0].isIntersecting) { + return; + } else { + console.log('this.feedToRender', this.feedToRender) + if (this.feedToRender.length < 20) { + return; + } + this.getMoreFeed(); + } + } + 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()); + const url = `${this.endpoints[endpointIndex].url}&limit=${count}&offset=${offset}`; + const res = await fetch(url) + const data = await res.json() + console.log({data}) + return data.map((i)=> { + return { + ...this.endpoints[endpointIndex], + ...i + } + }) + } @@ -165,17 +204,55 @@ this.loadAndMergeData(); const uniqueNewData = newData.filter(item => !existingIds.has(item.identifier)); return uniqueNewData.concat(existingData); } - + + + async addExtraData(data){ + let newData = [] + for (let item of data) { + let newItem = { + ...item, + schema: { + ...item.schema, + customParams: {...item.schema.customParams} + + } + } + let newResource = { + identifier: newItem.identifier, + service: newItem.service, + name: newItem.name + } + if(newItem.schema){ + const resource = newItem + // First, evaluate methods to get values for customParams + await updateCustomParamsWithMethods(newItem.schema, newResource); + // Now, generate your final URLs + let clickValue1 = newItem.schema.click; + + const resolvedClickValue1 = replacePlaceholders(clickValue1, resource, newItem.schema.customParams); + newItem.link = resolvedClickValue1 + newData.push(newItem) + } + } + return newData + + } async loadAndMergeData() { let allData = this.feed const newData = await this.initialLoad(); + allData = await this.addExtraData(newData) 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] + this.feedToRender = this.feed.slice(0,20) + this.hasInitialFetch = true } - + + + + render() { console.log('ron', this.feed) @@ -183,13 +260,11 @@ this.loadAndMergeData();
- ${this.feed.map((item) => { + ${this.feedToRender.map((item) => { return html``; })}
@@ -223,13 +298,43 @@ export function constructUrl(base, search, dynamicVars) { return queryStrings.length > 0 ? `${base}&${queryStrings.join('&')}` : base; } +function validateMethodString(methodString) { + // Check for IIFE + const iifePattern = /^\(.*\)\s*\(\)/; + if (iifePattern.test(methodString)) { + throw new Error("IIFE detected!"); + } + + // Check for disallowed keywords + const disallowed = ["eval", "Function", "fetch", "XMLHttpRequest"]; + for (const keyword of disallowed) { + if (methodString.includes(keyword)) { + throw new Error(`Disallowed keyword detected: ${keyword}`); + } + } + + // ... Add more validation steps here ... + + return true; +} + function executeMethodInWorker(methodString, externalArgs) { return new Promise((resolve, reject) => { + if (!validateMethodString(methodString)) { + reject(new Error("Invalid method string provided.")); + return; + } + const workerFunction = ` self.onmessage = function(event) { - const method = ${methodString}; - const result = method(event.data.externalArgs); - self.postMessage(result); + const methodFunction = new Function("resource", "${methodString}"); + const result = methodFunction(event.data.externalArgs); + + if (typeof result === 'string' || typeof result === 'number') { + self.postMessage(result); + } else { + self.postMessage(''); + } } `; @@ -238,9 +343,16 @@ function executeMethodInWorker(methodString, externalArgs) { const worker = new Worker(blobURL); worker.onmessage = function(event) { - resolve(event.data); - worker.terminate(); - URL.revokeObjectURL(blobURL); + if (typeof event.data === 'string' || typeof event.data === 'number') { + resolve(event.data); + worker.terminate(); + URL.revokeObjectURL(blobURL); + } else { + resolve(""); + worker.terminate(); + URL.revokeObjectURL(blobURL); + } + }; worker.onerror = function(error) { @@ -256,17 +368,24 @@ function executeMethodInWorker(methodString, externalArgs) { export async function updateCustomParamsWithMethods(schema,resource) { - for (const key in schema.feed[0].customParams) { - const value = schema.feed[0].customParams[key]; - + console.log({schema, resource}) + for (const key in schema.customParams) { + const value = schema.customParams[key]; + console.log({value}) if (value.startsWith("**methods.") && value.endsWith("**")) { const methodInvocation = value.slice(10, -2).split('('); const methodName = methodInvocation[0]; - if (schema.feed[0].methods[methodName]) { - const methodResult = await executeMethodInWorker(schema.feed[0].methods[methodName].toString(), resource); + if (schema.methods[methodName]) { + const newResource = { + identifier: resource.identifier, + name: resource.name, + service: resource.service + } + console.log({newResource}) + const methodResult = await executeMethodInWorker(schema.methods[methodName], newResource); console.log({methodResult}) - schema.feed[0].customParams[key] = methodResult; + schema.customParams[key] = methodResult; } } } @@ -292,9 +411,9 @@ export function replacePlaceholders(template, resource, customParams) { +// export const schemaList = [schema] - -export const schema = { + const schema = { name: "Q-Blog", defaultFeedIndex: 0, feed: [ @@ -305,7 +424,6 @@ export const schema = { title: "Q-Blog Post creations", description: "blablabla", search: { - name:"$${name}$$", query: "-post-", identifier: "q-blog-", service: "BLOG_POST", @@ -321,27 +439,35 @@ export const schema = { blogId: "**methods.getBlogId(resource)**", shortIdentifier: "**methods.getShortId(resource)**" }, - methods: { - getShortId: function(resource) { - const str = resource.identifier - const arr = str.split('-post-') - const shortIdentifier = arr[1] - - return shortIdentifier - }, - getBlogId: function(resource) { - const str = resource.identifier - const arr = str.split('-post-') - const id = arr[0] - let blogId = "" - if (id.startsWith('q-blog-')) { - blogId = id.substring(7); - } else { - blogId= id; - } - return blogId - } + "methods": { + "getShortId": "return resource.identifier.split('-post-')[1];", + "getBlogId": "const arr = resource.identifier.split('-post-'); const id = arr[0]; return id.startsWith('q-blog-') ? id.substring(7) : id;" } + // methods: { + // getShortId: function(resource) { + // console.log({resource}) + // const str = resource.identifier + // const arr = str.split('-post-') + // const shortIdentifier = arr[1] + + // return shortIdentifier + // }, + // getBlogId: function(resource) { + // console.log({resource}) + // const str = resource.identifier + // const arr = str.split('-post-') + // const id = arr[0] + // let blogId = "" + // if (id.startsWith('q-blog-')) { + // blogId = id.substring(7); + // } else { + // blogId= id; + // } + // return blogId + // } + // } } ] -} \ No newline at end of file +} + +// export const schema = JSON.stringify(schema2, null, 2); // 2 spaces indentation diff --git a/core/src/components/friends-view/friends-side-panel.js b/core/src/components/friends-view/friends-side-panel.js index 96c2be4f..76740d5c 100644 --- a/core/src/components/friends-view/friends-side-panel.js +++ b/core/src/components/friends-view/friends-side-panel.js @@ -2,13 +2,20 @@ import { LitElement, html, css } from 'lit'; import '@material/mwc-icon'; import './friends-view' import './friends-feed' +import { translate } from 'lit-translate'; class FriendsSidePanel extends LitElement { static get properties() { return { setIsOpen: { attribute: false}, - isOpen: {type: Boolean} + isOpen: {type: Boolean}, + selected: {type: String} }; } + + constructor(){ + super() + this.selected = 'friends' + } static styles = css` :host { @@ -21,7 +28,6 @@ class FriendsSidePanel extends LitElement { height: calc(100vh - 55px); background-color: var(--white); border-left: 1px solid rgb(224, 224, 224); - overflow-y: auto; z-index: 1; transform: translateX(100%); /* start from outside the right edge */ transition: transform 0.3s ease-in-out; @@ -40,23 +46,85 @@ class FriendsSidePanel extends LitElement { .content { padding: 16px; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: auto; } + .content::-webkit-scrollbar-track { + background-color: whitesmoke; + border-radius: 7px; + } + + .content::-webkit-scrollbar { + width: 12px; + border-radius: 7px; + background-color: whitesmoke; + } + + .content::-webkit-scrollbar-thumb { + background-color: rgb(180, 176, 176); + border-radius: 7px; + transition: all 0.3s ease-in-out; + } + .parent { + display: flex; + flex-direction: column; + height: 100%; + } + + .active { + font-size: 16px; + background: var(--black); + color: var(--white); + padding: 5px; + border-radius: 2px; + cursor: pointer; + } + + .default { + font-size: 16px; + color: var(--black); + padding: 5px; + border-radius: 2px; + cursor: pointer; + } + + .default-content { + visibility: hidden; + position: absolute; + z-index: -50; + } + `; render() { return html` +
- Panel Title +
+ this.selected = 'friends'} class="${this.selected === 'friends' ? 'active' : 'default'}">${translate('friends.friend12')} + this.selected = 'feed'} class="${this.selected === 'feed' ? 'active' : 'default'}">${translate('friends.friend13')} +
{ this.setIsOpen(false) }}>close
- - +
+ +
+
+ +
+ +
+
`; } diff --git a/core/src/components/friends-view/friends-view-css.js b/core/src/components/friends-view/friends-view-css.js index c1d2be4b..303d510d 100644 --- a/core/src/components/friends-view/friends-view-css.js +++ b/core/src/components/friends-view/friends-view-css.js @@ -1,6 +1,9 @@ import { css } from 'lit' export const friendsViewStyles = css` +* { + box-sizing: border-box; +} .top-bar-icon { cursor: pointer; height: 18px; @@ -41,6 +44,7 @@ export const friendsViewStyles = css` padding: 0px 6px; box-sizing: border-box; align-items: center; + gap: 10px; } .container-body::-webkit-scrollbar-track { @@ -164,7 +168,7 @@ export const friendsViewStyles = css` position: absolute; right: 3px; color: var(--chat-bubble-msg-color); - transition: all 0.3s ease-in-out; + transition: hover 0.3s ease-in-out; background: none; border-radius: 50%; padding: 6px 3px; diff --git a/core/src/components/friends-view/friends-view.js b/core/src/components/friends-view/friends-view.js index 80b006ee..f591348f 100644 --- a/core/src/components/friends-view/friends-view.js +++ b/core/src/components/friends-view/friends-view.js @@ -270,7 +270,7 @@ class FriendsView extends connect(store)(LitElement) { ?loading=${this.isLoading}>
-
+ ${this.friendList.map((item) => { return html`