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

feed in separate palce

This commit is contained in:
PhilReact 2023-10-10 18:49:18 -05:00
parent 8eacfca4d1
commit fac7ae9004
8 changed files with 453 additions and 98 deletions

View File

@ -1188,6 +1188,8 @@
"friend8": "Send Q-Chat",
"friend9": "Send Q-Mail",
"friend10": "Edit friend",
"friend11": "Following"
"friend11": "Following",
"friend12": "Friends",
"friend13": "Feed"
}
}

View File

@ -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`<img
src="${avatarUrl}"
style="max-width:100%; max-height:100%;"
onerror="this.onerror=null; this.src='/img/incognito.png';"
/>`;
let avatarImgApp
const avatarUrl2 = `${this.nodeUrl}/arbitrary/THUMBNAIL/${this.appName}/qortal_avatar?async=true&apiKey=${this.myNode.apiKey}`;
avatarImgApp = html`<img
src="${avatarUrl2}"
style="max-width:100%; max-height:100%;"
onerror="this.onerror=null; this.src='/img/incognito.png';"
/>`;
return html`
<div
class=${[
@ -308,12 +449,13 @@ getMyNode(){
? 'hideImg'
: '',
].join(' ')}
style=" box-sizing: border-box;"
>
${
this.status.status !== 'READY'
? html`
<div
style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:center;align-items:center;"
style="display:flex;flex-direction:column;width:100%;height:100%;justify-content:center;align-items:center; box-sizing: border-box;"
>
<div
class=${`smallLoading`}
@ -325,8 +467,24 @@ getMyNode(){
: ''
}
${this.status.status === 'READY' && this.feedItem ? html`
<div style="position:relative">
ready
<div class="parent-feed-item" style="position:relative" @click=${this.goToFeedLink}>
<div style="display:flex;gap:10px;margin-bottom:20px">
<div class="avatar">
${avatarImg}</div> <span class="feed-item-name">${this.resource.name}</span>
</div>
<div>
<p>${this.feedItem.title}</p>
</div>
<div class="app-name">
<div class="avatar">
${avatarImgApp}
</div>
<message-time
timestamp=${this
.resource
.created}
></message-time>
</div>
</div>
` : ''}

View File

@ -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();
<div class="container">
<div id="viewElement" class="container-body" style=${"position: relative"}>
${this.feed.map((item) => {
${this.feedToRender.map((item) => {
return html`<feed-item
.resource=${{
name: item.name,
service: item.service,
identifier: item.identifier,
}}
.resource=${item}
appName=${'Q-Blog'}
link=${item.link}
></feed-item>`;
})}
<div id="downObserver"></div>
@ -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
// }
// }
}
]
}
}
// export const schema = JSON.stringify(schema2, null, 2); // 2 spaces indentation

View File

@ -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`
<div class="parent">
<div class="header">
<span>Panel Title</span>
<div style="display:flex;align-items:center;gap:10px">
<span @click=${()=> this.selected = 'friends'} class="${this.selected === 'friends' ? 'active' : 'default'}">${translate('friends.friend12')}</span>
<span @click=${()=> this.selected = 'feed'} class="${this.selected === 'feed' ? 'active' : 'default'}">${translate('friends.friend13')}</span>
</div>
<mwc-icon style="cursor:pointer" @click=${()=> {
this.setIsOpen(false)
}}>close</mwc-icon>
</div>
<div class="content">
<friends-view></friends-view>
<friends-feed></friends-feed>
<div class="${this.selected === 'friends' ? 'active-content' : 'default-content'}">
<friends-view></friends-view>
</div>
<div class="${this.selected === 'feed' ? 'active-content' : 'default-content'}">
<friends-feed></friends-feed>
</div>
<!-- ${this.selected === 'friends' ? html`<friends-view></friends-view>` : ''}
${this.selected === 'feed' ? html`<friends-feed></friends-feed>` : ''} -->
</div>
</div>
</div>
`;
}

View File

@ -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;

View File

@ -270,7 +270,7 @@ class FriendsView extends connect(store)(LitElement) {
?loading=${this.isLoading}>
</chat-search-results>
</div>
<br />
${this.friendList.map((item) => {
return html`<chat-side-nav-heads

View File

@ -281,10 +281,7 @@ class WebBrowser extends LitElement {
else {
identifier = null;
}
}
const path = parts.join("/");
}extractComponents
const components = {};
components["service"] = service;
components["name"] = name;