Merge pull request #208 from Philreact/feature/friends-list

Feature/friends list
This commit is contained in:
AlphaX-Projects 2023-10-21 16:14:04 +02:00 committed by GitHub
commit ac3a097b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 4004 additions and 63 deletions

31
blog-test.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "Q-Blog",
"defaultFeedIndex": 0,
"feed": [
{
"id": "post-creation",
"version": 1,
"updated": 1696646223261,
"title": "Q-Blog Post creations",
"description": "blablabla",
"search": {
"query": "-post-",
"identifier": "q-blog-",
"service": "BLOG_POST",
"exactmatchnames": true
},
"click": "qortal://APP/Q-Blog/$${resource.name}$$/$${customParams.blogId}$$/$${customParams.shortIdentifier}$$",
"display": {
"title": "$${rawdata.title}$$"
},
"customParams": {
"blogId": "**methods.getBlogId(resource)**",
"shortIdentifier": "**methods.getShortId(resource)**"
},
"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;"
}
}
]
}

View File

@ -1182,6 +1182,36 @@
"notifications": {
"notify1": "Confirming transaction",
"notify2": "Transaction confirmed",
"explanation": "Your transaction is getting confirmed. To track its progress, click on the bell icon."
"explanation": "Your transaction is getting confirmed. To track its progress, click on the bell icon.",
"status1": "Fully synced",
"status2": "Not synced",
"notify3": "No notifications",
"notify4": "Tx notifications"
},
"friends": {
"friend1": "Add name",
"friend2": "Add friend",
"friend3": "Adding a friend allows you to connect easily with that person. Be sure to also follow that user to support the hosting of their published resources.",
"friend4": "Notes",
"friend5": "Follow name",
"friend6": "Alias",
"friend7": "Add an alias to better remember your friend (Optional)",
"friend8": "Send Q-Chat",
"friend9": "Send Q-Mail",
"friend10": "Edit friend",
"friend11": "Following",
"friend12": "Friends",
"friend13": "Feed",
"friend14": "Remove friend",
"friend15": "Feed settings",
"friend16": "Select the Q-Apps you want updates from, especially those related to your friends.",
"friends17": "Friends",
"friends18": "No items in your feed"
},
"save": {
"saving1": "Unable to fetch saved settings",
"saving2": "Nothing to save",
"saving3": "Save unsaved changes",
"saving4": "Undo changes"
}
}

View File

@ -43,8 +43,9 @@ import '../functional-components/side-menu-item.js'
import './start-minting.js'
import './notification-view/notification-bell.js'
import './notification-view/notification-bell-general.js'
import './friends-view/friends-side-panel-parent.js'
import './friends-view/save-settings-qdn.js'
import './friends-view/core-sync-status.js'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class AppView extends connect(store)(LitElement) {
@ -583,8 +584,10 @@ class AppView extends connect(store)(LitElement) {
</span>
</div>
<div style="display:flex;align-items:center;gap:20px">
<friends-side-panel-parent></friends-side-panel-parent>
<notification-bell></notification-bell>
<notification-bell-general></notification-bell-general>
<save-settings-qdn></save-settings-qdn>
</div>
<div style="display: inline;">
<span>
@ -671,6 +674,8 @@ class AppView extends connect(store)(LitElement) {
<mwc-button dense unelevated label="${translate("login.lp7")}" icon="lock_open" @click="${() => this.closeLockScreenActive()}"></mwc-button>
</div>
</paper-dialog>
<div id="portal-target"></div>
`
}

View File

@ -0,0 +1,224 @@
import { LitElement, html, css } from 'lit'
import { render } from 'lit/html.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import '@material/mwc-icon'
import '@vaadin/tooltip';
import './friend-item-actions'
class ChatSideNavHeads extends LitElement {
static get properties() {
return {
selectedAddress: { type: Object },
config: { type: Object },
chatInfo: { type: Object },
iconName: { type: String },
activeChatHeadUrl: { type: String },
isImageLoaded: { type: Boolean },
setActiveChatHeadUrl: {attribute: false},
openEditFriend: {attribute: false},
closeSidePanel: {attribute: false, type: Object}
}
}
static get styles() {
return css`
:host {
width: 100%;
}
ul {
list-style-type: none;
}
li {
padding: 10px 2px 10px 5px;
cursor: pointer;
width: 100%;
display: flex;
box-sizing: border-box;
font-size: 14px;
transition: 0.2s background-color;
}
li:hover {
background-color: var(--lightChatHeadHover);
}
.active {
background: var(--menuactive);
border-left: 4px solid #3498db;
}
.img-icon {
font-size:40px;
color: var(--chat-group);
}
.status {
color: #92959e;
}
.clearfix {
display: flex;
align-items: center;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
`
}
constructor() {
super()
this.selectedAddress = {}
this.config = {
user: {
node: {
}
}
}
this.chatInfo = {}
this.iconName = ''
this.activeChatHeadUrl = ''
this.isImageLoaded = false
this.imageFetches = 0
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "width:30px; height:30px; float: left; border-radius:50%; font-size:14px";
imageHTMLRes.onclick= () => {
this.openDialogImage = true;
}
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 4) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 500);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render() {
let avatarImg = ""
if (this.chatInfo.name) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.chatInfo.name}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = this.createImage(avatarUrl)
}
return html`
<li style="display:flex; justify-content: space-between; align-items: center" @click=${(e) => {
const target = e.target
const popover =
this.shadowRoot.querySelector('friend-item-actions');
if (popover) {
popover.openPopover(target);
}
}} class="clearfix" id=${`friend-item-parent-${this.chatInfo.name}`}>
<div style="display:flex; flex-grow: 1; align-items: center">
${this.isImageLoaded ? html`${avatarImg}` : html``}
${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName
? html`<mwc-icon class="img-icon">account_circle</mwc-icon>`
: html``}
${!this.isImageLoaded && this.chatInfo.name
? html`<div
style="width:30px; height:30px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadBgActive)"
: "var(--chatHeadBg)"}; color: ${this.activeChatHeadUrl ===
this.chatInfo.url
? "var(--chatHeadTextActive)"
: "var(--chatHeadText)"}; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize"
>
${this.chatInfo.name.charAt(0)}
</div>`
: ""}
${!this.isImageLoaded && this.chatInfo.groupName
? html`<div
style="width:30px; height:30px; float: left; border-radius:50%; background: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadBgActive)"
: "var(--chatHeadBg)"}; color: ${this.activeChatHeadUrl === this.chatInfo.url
? "var(--chatHeadTextActive)"
: "var(--chatHeadText)"}; font-weight:bold; display: flex; justify-content: center; align-items: center; text-transform: capitalize"
>
${this.chatInfo.groupName.charAt(0)}
</div>`
: ""}
<div>
<div class="name">
<span style="float:left; padding-left: 8px; color: var(--chat-group);">
${this.chatInfo.groupName
? this.chatInfo.groupName
: this.chatInfo.name !== undefined
? (this.chatInfo.alias || this.chatInfo.name)
: this.chatInfo.address.substr(0, 15)}
</span>
</div>
</div>
</div>
<div style="display:flex; align-items: center">
${this.chatInfo.willFollow ? html`
<mwc-icon id="willFollowIcon" style="color: var(--black)">connect_without_contact</mwc-icon>
<vaadin-tooltip
for="willFollowIcon"
position="top"
hover-delay=${200}
hide-delay=${1}
text=${get('friends.friend11')}>
</vaadin-tooltip>
` : ''}
</div>
</li>
<friend-item-actions
for=${`friend-item-parent-${this.chatInfo.name}`}
message=${get('notifications.explanation')}
.openEditFriend=${()=> {
this.openEditFriend(this.chatInfo)
}}
name=${this.chatInfo.name}
.closeSidePanel=${this.closeSidePanel}
></friend-item-actions>
`
}
shouldUpdate(changedProperties) {
if(changedProperties.has('activeChatHeadUrl')){
return true
}
if(changedProperties.has('chatInfo')){
return true
}
if(changedProperties.has('isImageLoaded')){
return true
}
return false
}
getUrl(chatUrl) {
this.setActiveChatHeadUrl(chatUrl)
}
}
window.customElements.define('chat-side-nav-heads', ChatSideNavHeads)

View File

@ -0,0 +1,496 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import {
use,
get,
translate,
translateUnsafeHTML,
registerTranslateConfig,
} from 'lit-translate';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@material/mwc-checkbox';
import { connect } from 'pwa-helpers';
import { store } from '../../store';
import '@polymer/paper-spinner/paper-spinner-lite.js'
class AddFriendsModal extends connect(store)(LitElement) {
static get properties() {
return {
isOpen: { type: Boolean },
setIsOpen: { attribute: false },
isLoading: { type: Boolean },
userSelected: { type: Object },
alias: { type: String },
willFollow: { type: Boolean },
notes: { type: String },
onSubmit: { attribute: false },
editContent: { type: Object },
onClose: { attribute: false },
mySelectedFeeds: { type: Array },
availableFeeedSchemas: {type: Array},
isLoadingSchemas: {type: Boolean}
};
}
constructor() {
super();
this.isOpen = false;
this.isLoading = false;
this.alias = '';
this.willFollow = true;
this.notes = '';
this.nodeUrl = this.getNodeUrl();
this.myNode = this.getMyNode();
this.mySelectedFeeds = [];
this.availableFeeedSchemas = [];
this.isLoadingSchemas= false;
}
static get styles() {
return css`
* {
--mdc-theme-primary: rgb(3, 169, 244);
--mdc-theme-secondary: var(--mdc-theme-primary);
--mdc-theme-surface: var(--white);
--mdc-dialog-content-ink-color: var(--black);
--mdc-dialog-min-width: 400px;
--mdc-dialog-max-width: 1024px;
box-sizing:border-box;
}
.input {
width: 90%;
outline: 0;
border-width: 0 0 2px;
border-color: var(--mdc-theme-primary);
background-color: transparent;
padding: 10px;
font-family: Roboto, sans-serif;
font-size: 15px;
color: var(--chat-bubble-msg-color);
box-sizing: border-box;
}
.input::selection {
background-color: var(--mdc-theme-primary);
color: white;
}
.input::placeholder {
opacity: 0.6;
color: var(--black);
}
.modal-button {
font-family: Roboto, sans-serif;
font-size: 16px;
color: var(--mdc-theme-primary);
background-color: transparent;
padding: 8px 10px;
border-radius: 5px;
border: none;
transition: all 0.3s ease-in-out;
}
.modal-button-red {
font-family: Roboto, sans-serif;
font-size: 16px;
color: #f44336;
background-color: transparent;
padding: 8px 10px;
border-radius: 5px;
border: none;
transition: all 0.3s ease-in-out;
}
.modal-button-red:hover {
cursor: pointer;
background-color: #f4433663;
}
.modal-button:hover {
cursor: pointer;
background-color: #03a8f475;
}
.checkbox-row {
position: relative;
display: flex;
align-items: center;
align-content: center;
font-family: Montserrat, sans-serif;
font-weight: 600;
color: var(--black);
}
.modal-overlay {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(
0,
0,
0,
0.5
); /* Semi-transparent backdrop */
z-index: 1000;
}
.modal-content {
position: fixed;
top: 50vh;
left: 50vw;
transform: translate(-50%, -50%);
background-color: var(--mdc-theme-surface);
width: 80vw;
max-width: 600px;
padding: 20px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;
z-index: 1001;
border-radius: 5px;
display: flex;
flex-direction:column;
}
.modal-overlay.hidden {
display: none;
}
.avatar {
width: 36px;
height: 36px;
display: flex;
align-items: center;
}
.app-name {
display: flex;
gap: 20px;
align-items: center;
width: 100%;
cursor: pointer;
padding: 5px;
border-radius: 5px;
margin-bottom: 10px;
}
.inner-content {
display: flex;
flex-direction: column;
max-height: 75vh;
flex-grow: 1;
overflow: auto;
}
.inner-content::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.inner-content::-webkit-scrollbar {
width: 12px;
border-radius: 7px;
background-color: whitesmoke;
}
.inner-content::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
`;
}
firstUpdated() {}
getNodeUrl() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
const nodeUrl =
myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
return nodeUrl;
}
getMyNode() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
return myNode;
}
clearFields() {
this.alias = '';
this.willFollow = true;
this.notes = '';
}
addFriend() {
this.onSubmit({
name: this.userSelected.name,
alias: this.alias,
notes: this.notes,
willFollow: this.willFollow,
mySelectedFeeds: this.mySelectedFeeds
});
this.clearFields();
this.onClose();
}
removeFriend() {
this.onSubmit(
{
name: this.userSelected.name,
alias: this.alias,
notes: this.notes,
willFollow: this.willFollow,
mySelectedFeeds: this.mySelectedFeeds
},
true
);
this.clearFields();
this.onClose();
}
async updated(changedProperties) {
if (
changedProperties &&
changedProperties.has('editContent') &&
this.editContent
) {
this.userSelected = {
name: this.editContent.name ?? '',
};
this.notes = this.editContent.notes ?? '';
this.willFollow = this.editContent.willFollow ?? true;
this.alias = this.editContent.alias ?? '';
this.requestUpdate()
}
if (
changedProperties &&
changedProperties.has('isOpen') && this.isOpen
) {
this.getAvailableFeedSchemas()
}
}
async getAvailableFeedSchemas() {
try {
this.isLoadingSchemas= true
const url = `${this.nodeUrl}/arbitrary/resources/search?service=DOCUMENT&identifier=ui_schema_feed&prefix=true`;
const res = await fetch(url);
const data = await res.json();
if (data.error === 401) {
this.availableFeeedSchemas = [];
} else {
const result = data.filter(
(item) => item.identifier === 'ui_schema_feed'
);
this.availableFeeedSchemas = result;
}
this.userFoundModalOpen = true;
} catch (error) {} finally {
this.isLoadingSchemas= false
}
}
render() {
return html`
<div class="modal-overlay ${this.isOpen ? '' : 'hidden'}">
<div class="modal-content">
<div class="inner-content">
<div style="text-align:center">
<h1>
${this.editContent
? translate('friends.friend10')
: translate('friends.friend2')}
</h1>
<hr />
</div>
<p>${translate('friends.friend3')}</p>
<div class="checkbox-row">
<label
for="willFollow"
id="willFollowLabel"
style="color: var(--black);"
>
${get('friends.friend5')}
</label>
<mwc-checkbox
style="margin-right: -15px;"
id="willFollow"
@change=${(e) => {
this.willFollow = e.target.checked;
}}
?checked=${this.willFollow}
></mwc-checkbox>
</div>
<div style="height:15px"></div>
<div style="display: flex;flex-direction: column;">
<label
for="name"
id="nameLabel"
style="color: var(--black);"
>
${get('login.name')}
</label>
<input
id="name"
class="input"
?disabled=${true}
value=${this.userSelected
? this.userSelected.name
: ''}
/>
</div>
<div style="height:15px"></div>
<div style="display: flex;flex-direction: column;">
<label
for="alias"
id="aliasLabel"
style="color: var(--black);"
>
${get('friends.friend6')}
</label>
<input
id="alias"
placeholder=${translate('friends.friend7')}
class="input"
.value=${this.alias}
@change=${(e) => {
this.alias = e.target.value
}}
/>
</div>
<div style="height:15px"></div>
<div style="margin-bottom:0;">
<textarea
class="input"
@change=${(e) => {
this.notes = e.target.value
}}
.value=${this.notes}
?disabled=${this.isLoading}
id="messageBoxAddFriend"
placeholder="${translate('friends.friend4')}"
rows="3"
></textarea>
</div>
<div style="height:15px"></div>
<h2>${translate('friends.friend15')}</h2>
<div style="margin-bottom:0;">
<p>${translate('friends.friend16')}</p>
</div>
<div>
${this.isLoadingSchemas ? html`
<div style="width:100%;display: flex; justify-content:center">
<paper-spinner-lite active></paper-spinner-lite>
</div>
` : ''}
${this.availableFeeedSchemas.map((schema) => {
const isAlreadySelected = this.mySelectedFeeds.find(
(item) => item.name === schema.name
);
let avatarImgApp;
const avatarUrl2 = `${this.nodeUrl}/arbitrary/THUMBNAIL/${schema.name}/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="app-name"
style="background:${isAlreadySelected ? 'lightblue': ''}"
@click=${() => {
const copymySelectedFeeds = [
...this.mySelectedFeeds,
];
const findIndex =
copymySelectedFeeds.findIndex(
(item) =>
item.name === schema.name
);
if (findIndex === -1) {
if(this.mySelectedFeeds.length > 4) return
copymySelectedFeeds.push({
name: schema.name,
identifier: schema.identifier,
service: schema.service,
});
this.mySelectedFeeds =
copymySelectedFeeds;
} else {
this.mySelectedFeeds =
copymySelectedFeeds.filter(
(item) =>
item.name !==
schema.name
);
}
}}
>
<div class="avatar">${avatarImgApp}</div>
<span
style="color:${isAlreadySelected ? 'var(--white)': 'var(--black)'};font-size:16px"
>${schema.name}</span
>
</div>
`;
})}
</div>
</div>
<div
style="display:flex;justify-content:space-between;align-items:center;margin-top:20px"
>
<button
class="modal-button-red"
?disabled="${this.isLoading}"
@click="${() => {
this.setIsOpen(false);
this.clearFields();
this.onClose();
}}"
>
${translate('general.close')}
</button>
${this.editContent
? html`
<button
?disabled="${this.isLoading}"
class="modal-button-red"
@click=${() => {
this.removeFriend();
}}
>
${translate('friends.friend14')}
</button>
`
: ''}
<button
?disabled="${this.isLoading}"
class="modal-button"
@click=${() => {
this.addFriend();
}}
>
${this.editContent
? translate('friends.friend10')
: translate('friends.friend2')}
</button>
</div>
</div>
</div>
`;
}
}
customElements.define('add-friends-modal', AddFriendsModal);

View File

@ -0,0 +1,92 @@
import { Sha256 } from 'asmcrypto.js'
function sbrk(size, heap){
let brk = 512 * 1024 // stack top
let old = brk
brk += size
if (brk > heap.length)
throw new Error('heap exhausted')
return old
}
self.addEventListener('message', async e => {
const response = await computePow(e.data.convertedBytes, e.data.path)
postMessage(response)
})
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 })
const heap = new Uint8Array(memory.buffer)
const computePow = async (convertedBytes, path) => {
let response = null
await new Promise((resolve, reject)=> {
const _convertedBytesArray = Object.keys(convertedBytes).map(
function (key) {
return convertedBytes[key]
}
)
const convertedBytesArray = new Uint8Array(_convertedBytesArray)
const convertedBytesHash = new Sha256()
.process(convertedBytesArray)
.finish().result
const hashPtr = sbrk(32, heap)
const hashAry = new Uint8Array(
memory.buffer,
hashPtr,
32
)
hashAry.set(convertedBytesHash)
const difficulty = 14
const workBufferLength = 8 * 1024 * 1024
const workBufferPtr = sbrk(
workBufferLength,
heap
)
const importObject = {
env: {
memory: memory
},
};
function loadWebAssembly(filename, imports) {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
.then(module => {
return new WebAssembly.Instance(module, importObject);
});
}
loadWebAssembly(path)
.then(wasmModule => {
response = {
nonce : wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty),
}
resolve()
});
})
return response
}

View File

@ -0,0 +1,78 @@
import { LitElement, html, css } from 'lit';
import '@material/mwc-icon';
import { store } from '../../store';
import { connect } from 'pwa-helpers';
import '@vaadin/tooltip';
import { get } from 'lit-translate';
class CoreSyncStatus extends connect(store)(LitElement) {
static get properties() {
return {
nodeStatus: {type: Object}
};
}
constructor() {
super();
this.nodeStatus = {
isMintingPossible:false,
isSynchronizing:true,
syncPercent:undefined,
numberOfConnections:undefined,
height:undefined,
}
}
static styles = css`
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.content {
padding: 16px;
}
.close {
visibility: hidden;
position: fixed;
z-index: -100;
right: -1000px;
}
.parent-side-panel {
transform: translateX(100%); /* start from outside the right edge */
transition: transform 0.3s ease-in-out;
}
.parent-side-panel.open {
transform: translateX(0); /* slide in to its original position */
}
`;
stateChanged(state) {
this.nodeStatus = state.app.nodeStatus
}
render() {
return html`
<mwc-icon id="icon" style="color: ${this.nodeStatus.syncPercent === 100 ? 'green': 'red'};user-select:none;margin-right:20px"
>lightbulb</mwc-icon
>
<vaadin-tooltip
for="icon"
position="bottom"
hover-delay=${400}
hide-delay=${1}
text=${this.nodeStatus.syncPercent === 100 ? get('notifications.status1'): get('notifications.status2')}>
</vaadin-tooltip>
`;
}
}
customElements.define('core-sync-status', CoreSyncStatus);

View File

@ -0,0 +1,516 @@
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';
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(3);
const requestQueueRawData = new RequestQueueWithPromise(3);
const requestQueueStatus = new RequestQueueWithPromise(3);
export class FeedItem extends connect(store)(LitElement) {
static get properties() {
return {
resource: { type: Object },
isReady: { type: Boolean},
status: {type: Object},
feedItem: {type: Object},
appName: {type: String},
link: {type: String}
};
}
static get styles() {
return css`
* {
--mdc-theme-text-primary-on-background: var(--black);
box-sizing: border-box;
}
:host {
width: 100%;
box-sizing: border-box;
}
img {
width:100%;
max-height:30vh;
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: 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;
font-size: 16px;
}
.avatar {
width: 36px;
height: 36px;
border-radius:50%;
overflow: hidden;
display:flex;
align-items:center;
}
.avatarApp {
width: 30px;
height: 30px;
border-radius:50%;
overflow: hidden;
display:flex;
align-items:center;
}
.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;
}
@-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.isReady = false
this.nodeUrl = this.getNodeUrl()
this.myNode = this.getMyNode()
this.hasCalledWhenDownloaded = false
this.isFetching = false
this.uid = new ShortUniqueId()
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()
}
async getRawData(){
const url = `${this.nodeUrl}/arbitrary/${this.resource.service}/${this.resource.name}/${this.resource.identifier}?apiKey=${this.myNode.apiKey}`
return await requestQueueRawData.enqueue(()=> {
return axios.get(url)
})
// 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 requestQueueStatus.enqueue(()=> {
return 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 = {
...this.resource.schema.display
}
this.updateDisplayWithPlaceholders(object, {},rawData.data)
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') {
const rawData = await this.getRawData()
const object = {
...this.resource.schema.display
}
this.updateDisplayWithPlaceholders(object, {},rawData.data)
this.feedItem = object
clearInterval(intervalId)
this.status = res
this.isReady = true
}
}, 5000) // 1 second interval
}
async _fetchImage() {
try {
this.fetchVideoUrl()
this.fetchStatus()
} catch (error) { /* empty */ }
}
firstUpdated(){
this.observer.observe(this);
}
async goToFeedLink(){
try {
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
}
render() {
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="width:100%; 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="width:100%; height:100%;"
onerror="this.onerror=null; this.src='/img/incognito.png';"
/>`;
return html`
<div
class=${[
`image-container`,
this.status.status !== 'READY'
? 'defaultSize'
: '',
this.status.status !== 'READY'
? '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; box-sizing: border-box;"
>
<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 class="parent-feed-item" style="position:relative" @click=${this.goToFeedLink}>
<div style="display:flex;gap:10px;margin-bottom:5px">
<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="avatarApp">
${avatarImgApp}
</div>
<message-time
timestamp=${this
.resource
.created}
></message-time>
</div>
</div>
` : ''}
</div>
`
}
}
customElements.define('feed-item', FeedItem);

View File

@ -0,0 +1,237 @@
// popover-component.js
import { LitElement, html, css } from 'lit';
import { createPopper } from '@popperjs/core';
import '@material/mwc-icon';
import { use, get, translate } from 'lit-translate';
import { store } from '../../store';
import { connect } from 'pwa-helpers';
import { setNewTab, setSideEffectAction } from '../../redux/app/app-actions';
import ShortUniqueId from 'short-unique-id';
export class FriendItemActions extends connect(store)(LitElement) {
static styles = css`
:host {
display: none;
position: absolute;
background-color: var(--white);
border: 1px solid #ddd;
padding: 8px;
z-index: 10;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
color: var(--black);
max-width: 250px;
}
.close-icon {
cursor: pointer;
float: right;
margin-left: 10px;
color: var(--black);
}
.send-message-button {
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
padding: 8px 5px;
border-radius: 3px;
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 {
cursor: pointer;
background-color: #03a8f485;
}
.action-parent {
display: flex;
flex-direction: column;
width: 100%;
}
div[tabindex='0']:focus {
outline: none;
}
`;
static get properties() {
return {
for: { type: String, reflect: true },
message: { type: String },
openEditFriend: { attribute: false },
name: { type: String },
closeSidePanel: {attribute: false, type: Object}
};
}
constructor() {
super();
this.message = '';
this.nodeUrl = this.getNodeUrl();
this.uid = new ShortUniqueId();
this.getUserAddress = this.getUserAddress.bind(this)
}
getNodeUrl() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
const nodeUrl =
myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
return nodeUrl;
}
firstUpdated() {
// We'll defer the popper attachment to the openPopover() method to ensure target availability
}
attachToTarget(target) {
if (!this.popperInstance && target) {
this.popperInstance = createPopper(target, this, {
placement: 'bottom'
});
}
}
openPopover(target) {
this.attachToTarget(target);
this.style.display = 'block';
setTimeout(() => {
this.shadowRoot.getElementById('parent-div').focus();
}, 50);
}
closePopover() {
this.style.display = 'none';
if (this.popperInstance) {
this.popperInstance.destroy();
this.popperInstance = null;
}
this.requestUpdate();
}
handleBlur() {
setTimeout(() => {
this.closePopover();
}, 0);
}
async getUserAddress() {
try {
const url = `${this.nodeUrl}/names/${this.name}`;
const res = await fetch(url);
const result = await res.json();
if (result.error === 401) {
return '';
} else {
return result.owner;
}
} catch (error) {
return '';
}
}
render() {
return html`
<div id="parent-div" tabindex="0" @blur=${this.handleBlur}>
<span class="close-icon" @click="${this.closePopover}"
><mwc-icon style="color: var(--black)"
>close</mwc-icon
></span
>
<div class="action-parent">
<div
class="send-message-button"
@click="${() => {
this.openEditFriend();
this.closePopover();
}}"
>
<mwc-icon style="color: var(--black)"
>edit</mwc-icon
>
${translate('friends.friend10')}
</div>
<div
class="send-message-button"
@click="${async () => {
const address = await this.getUserAddress();
if (!address) return;
store.dispatch(
setNewTab({
url: `q-chat`,
id: this.uid.rnd(),
myPlugObj: {
url: 'q-chat',
domain: 'core',
page: 'messaging/q-chat/index.html',
title: 'Q-Chat',
icon: 'vaadin:chat',
mwcicon: 'forum',
pluginNumber: 'plugin-qhsyOnpRhT',
menus: [],
parent: false,
},
openExisting: true,
})
);
store.dispatch(
setSideEffectAction({
type: 'openPrivateChat',
data: {
address,
name: this.name
},
})
);
this.closePopover();
this.closeSidePanel()
}}"
>
<mwc-icon style="color: var(--black)"
>send</mwc-icon
>
${translate('friends.friend8')}
</div>
<div
class="send-message-button"
@click="${() => {
const query = `?service=APP&name=Q-Mail/to/${this.name}`;
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: 'Q-Mail',
icon: 'vaadin:mailbox',
mwcicon: 'mail_outline',
menus: [],
parent: false,
},
openExisting: true,
})
);
this.closePopover();
this.closeSidePanel()
}}"
>
<mwc-icon style="color: var(--black)"
>mail</mwc-icon
>
${translate('friends.friend9')}
</div>
</div>
</div>
`;
}
}
customElements.define('friend-item-actions', FriendItemActions);

View File

@ -0,0 +1,471 @@
import { LitElement, html, css } from 'lit';
import '@material/mwc-icon';
import './friends-view'
import { friendsViewStyles } from './friends-view-css';
import { connect } from 'pwa-helpers';
import { store } from '../../store';
import './feed-item'
import { translate } from 'lit-translate';
import '@polymer/paper-spinner/paper-spinner-lite.js'
const perEndpointCount = 20;
const totalDesiredCount = 100;
const maxResultsInMemory = 300;
class FriendsFeed extends connect(store)(LitElement) {
static get properties() {
return {
feed: {type: Array},
setHasNewFeed: {attribute:false},
isLoading: {type: Boolean},
hasFetched: {type: Boolean},
mySelectedFeeds: {type: Array}
};
}
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)
this.mySelectedFeeds = []
this.getSchemas = this.getSchemas.bind(this)
this.hasFetched = false
this._updateFeeds = this._updateFeeds.bind(this)
}
static get styles() {
return [friendsViewStyles];
}
getNodeUrl() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
const nodeUrl =
myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
return nodeUrl;
}
getMyNode() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
return myNode;
}
_updateFeeds(event) {
const detail = event.detail
this.mySelectedFeeds = detail
this.reFetchFeedData()
this.requestUpdate()
}
connectedCallback() {
super.connectedCallback()
window.addEventListener('friends-my-selected-feeds-event', this._updateFeeds) }
disconnectedCallback() {
window.removeEventListener('friends-my-selected-feeds-event', this._updateFeeds)
super.disconnectedCallback()
}
async getSchemas(){
this.mySelectedFeeds = JSON.parse(localStorage.getItem('friends-my-selected-feeds') || "[]")
const schemas = this.mySelectedFeeds
const getAllSchemas = (schemas || []).map(
async (schema) => {
try {
const url = `${this.nodeUrl}/arbitrary/${schema.service}/${schema.name}/${schema.identifier}`;
const res = await fetch(url)
const data = await res.json()
if(data.error) return false
return data
} catch (error) {
console.log(error);
return false
}
}
);
const res = await Promise.all(getAllSchemas);
return res.filter((item)=> !!item)
}
getFeedOnInterval(){
let interval = null;
let stop = false;
const getAnswer = async () => {
if (!stop) {
stop = true;
try {
await this.reFetchFeedData()
} catch (error) {}
stop = false;
}
};
interval = setInterval(getAnswer, 900000);
}
async getEndpoints(){
const dynamicVars = {
}
const schemas = await this.getSchemas()
const friendList = JSON.parse(localStorage.getItem('friends-my-friend-list') || "[]")
const names = friendList.map(friend => `name=${friend.name}`).join('&');
if(names.length === 0){
this.endpoints= []
this.endpointOffsets = Array(this.endpoints.length).fill(0);
return
}
const baseurl = `${this.nodeUrl}/arbitrary/resources/search?reverse=true&exactmatchnames=true&${names}`
let formEndpoints = []
schemas.forEach((schema)=> {
const feedData = schema.feed[0]
if(feedData){
const copyFeedData = {...feedData}
const fullUrl = constructUrl(baseurl, copyFeedData.search, dynamicVars);
if(fullUrl){
formEndpoints.push({
url: fullUrl, schemaName: schema.name, schema: copyFeedData
})
}
}
})
this.endpoints= formEndpoints
this.endpointOffsets = Array(this.endpoints.length).fill(0);
}
async firstUpdated(){
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.downObserverElement =
this.shadowRoot.getElementById('downObserver');
this.elementObserver();
try {
await new Promise((res)=> {
setTimeout(() => {
res()
}, 5000);
})
if(this.mySelectedFeeds.length === 0){
await this.getEndpoints()
this.loadAndMergeData();
}
this.getFeedOnInterval()
} catch (error) {
console.log(error)
}
}
getMoreFeed(){
if(!this.hasInitialFetch) return
if(this.feedToRender.length === this.feed.length ) return
this.feedToRender = this.feed.slice(0, this.feedToRender.length + 20)
this.requestUpdate()
}
async refresh(){
try {
await this.getEndpoints()
this.reFetchFeedData()
} catch (error) {
}
}
elementObserver() {
const options = {
rootMargin: '0px',
threshold: 1,
};
// identify an element to observe
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) {
if (!entries[0].isIntersecting) {
return;
} else {
if (this.feedToRender.length < 20) {
return;
}
this.getMoreFeed();
}
}
async fetchDataFromEndpoint(endpointIndex, count) {
const offset = this.endpointOffsets[endpointIndex];
const url = `${this.endpoints[endpointIndex].url}&limit=${count}&offset=${offset}`;
const res = await fetch(url)
const data = await res.json()
return data.map((i)=> {
return {
...this.endpoints[endpointIndex],
...i
}
})
}
async initialLoad() {
let results = [];
let totalFetched = 0;
let i = 0;
let madeProgress = true;
let exhaustedEndpoints = new Set();
while (totalFetched < totalDesiredCount && madeProgress) {
madeProgress = false;
this.isLoading = true
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;
}
}
this.isLoading = false
this.hasFetched = true;
// 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 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
let clickValue1 = newItem.schema.click;
const resolvedClickValue1 = replacePlaceholders(clickValue1, resource, newItem.schema.customParams);
newItem.link = resolvedClickValue1
newData.push(newItem)
}
}
return newData
}
async reFetchFeedData() {
// Resetting offsets to start fresh.
this.endpointOffsets = Array(this.endpoints.length).fill(0);
await this.getEndpoints()
const oldIdentifiers = new Set(this.feed.map(item => item.identifier));
const newData = await this.initialLoad();
// Filter out items that are already in the feed
const trulyNewData = newData.filter(item => !oldIdentifiers.has(item.identifier));
if (trulyNewData.length > 0) {
// Adding extra data and merging with old data
const enhancedNewData = await this.addExtraData(trulyNewData);
// Merge new data with old data immutably
this.feed = [...enhancedNewData, ...this.feed];
this.feed.sort((a, b) => new Date(b.created) - new Date(a.created)); // Sort by timestamp, most recent first
this.feed = this.trimDataToLimit(this.feed, maxResultsInMemory); // Trim to the maximum allowed in memory
this.feedToRender = this.feed.slice(0, 20);
this.hasInitialFetch = true;
const created = trulyNewData[0].created;
let value = localStorage.getItem('lastSeenFeed');
if (((+value || 0) < created)) {
this.setHasNewFeed(true);
}
}
}
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
if(allData.length > 0){
const created = allData[0].created
let value = localStorage.getItem('lastSeenFeed')
if (((+value || 0) < created)) {
this.setHasNewFeed(true)
}
}
}
render() {
return html`
<div class="container">
<div id="viewElement" class="container-body" style=${"position: relative"}>
${this.isLoading ? html`
<div style="width:100%;display: flex; justify-content:center">
<paper-spinner-lite active></paper-spinner-lite>
</div>
` : ''}
${this.hasFetched && !this.isLoading && this.feed.length === 0 ? html`
<div style="width:100%;display: flex; justify-content:center">
<p>${translate('friends.friends18')}</p>
</div>
` : ''}
${this.feedToRender.map((item) => {
return html`<feed-item
.resource=${item}
appName=${'Q-Blog'}
link=${item.link}
></feed-item>`;
})}
<div id="downObserver"></div>
</div>
</div>
`;
}
}
customElements.define('friends-feed', FriendsFeed);
export function substituteDynamicVar(value, dynamicVars) {
if (typeof value !== 'string') return value;
const pattern = /\$\$\{([a-zA-Z0-9_]+)\}\$\$/g; // Adjusted pattern to capture $${name}$$ with curly braces
return value.replace(pattern, (match, p1) => {
return dynamicVars[p1] !== undefined ? dynamicVars[p1] : match;
});
}
export function constructUrl(base, search, dynamicVars) {
let queryStrings = [];
for (const [key, value] of Object.entries(search)) {
const substitutedValue = substituteDynamicVar(value, dynamicVars);
queryStrings.push(`${key}=${encodeURIComponent(substitutedValue)}`);
}
return queryStrings.length > 0 ? `${base}&${queryStrings.join('&')}` : base;
}
export function replacePlaceholders(template, resource, customParams) {
const dataSource = { resource, customParams };
return template.replace(/\$\$\{(.*?)\}\$\$/g, (match, p1) => {
const keys = p1.split('.');
let value = dataSource;
for (let key of keys) {
if (value[key] !== undefined) {
value = value[key];
} else {
return match; // Return placeholder unchanged
}
}
return value;
});
}

View File

@ -0,0 +1,81 @@
import { LitElement, html, css } from 'lit';
import '@material/mwc-icon';
import './friends-side-panel.js';
import '@vaadin/tooltip';
import { get } from 'lit-translate';
class FriendsSidePanelParent extends LitElement {
static get properties() {
return {
isOpen: {type: Boolean},
hasNewFeed: {type: Boolean}
};
}
constructor() {
super();
this.isOpen = false
this.hasNewFeed = false
}
static styles = css`
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.content {
padding: 16px;
}
.close {
visibility: hidden;
position: fixed;
z-index: -100;
right: -1000px;
}
.parent-side-panel {
transform: translateX(100%); /* start from outside the right edge */
transition: transform 0.3s ease-in-out;
}
.parent-side-panel.open {
transform: translateX(0); /* slide in to its original position */
}
`;
setHasNewFeed(val){
this.hasNewFeed = val
}
render() {
return html`
<mwc-icon id="friends-icon" @click=${()=> {
this.isOpen = !this.isOpen
if(this.isOpen && this.hasNewFeed){
localStorage.setItem('lastSeenFeed', Date.now());
this.hasNewFeed = false
this.shadowRoot.querySelector("friends-side-panel").selected = 'feed'
}
}} style="color: ${this.hasNewFeed ? 'green' : 'var(--black)'}; cursor:pointer;user-select:none"
>group</mwc-icon
>
<vaadin-tooltip
for="friends-icon"
position="bottom"
hover-delay=${400}
hide-delay=${1}
text=${get('friends.friends17')}>
</vaadin-tooltip>
<friends-side-panel .setHasNewFeed=${(val)=> this.setHasNewFeed(val)} ?isOpen=${this.isOpen} .setIsOpen=${(val)=> this.isOpen = val}></friends-side-panel>
`;
}
}
customElements.define('friends-side-panel-parent', FriendsSidePanelParent);

View File

@ -0,0 +1,151 @@
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},
selected: {type: String},
setHasNewFeed: {attribute: false},
closeSidePanel: {attribute: false, type: Object}
};
}
constructor(){
super()
this.selected = 'friends'
this.closeSidePanel = this.closeSidePanel.bind(this)
}
static styles = css`
:host {
display: block;
position: fixed;
top: 55px;
right: 0px;
width: 420px;
max-width: 95%;
height: calc(100vh - 55px);
background-color: var(--white);
border-left: 1px solid rgb(224, 224, 224);
z-index: 1;
transform: translateX(100%); /* start from outside the right edge */
transition: transform 0.3s ease-in-out;
}
:host([isOpen]) {
transform: unset; /* slide in to its original position */
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.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;
}
`;
refreshFeed(){
this.shadowRoot.querySelector('friends-feed').refresh()
}
closeSidePanel(){
this.setIsOpen(false)
}
render() {
return html`
<div class="parent">
<div class="header">
<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>
<div style="display:flex;gap:15px;align-items:center">
<mwc-icon @click=${()=> {
this.refreshFeed()
}} style="color: var(--black); cursor:pointer;">refresh</mwc-icon>
<mwc-icon style="cursor:pointer" @click=${()=> {
this.setIsOpen(false)
}}>close</mwc-icon>
</div>
</div>
<div class="content">
<div class="${this.selected === 'friends' ? 'active-content' : 'default-content'}">
<friends-view .closeSidePanel=${this.closeSidePanel} .refreshFeed=${()=>this.refreshFeed()}></friends-view>
</div>
<div class="${this.selected === 'feed' ? 'active-content' : 'default-content'}">
<friends-feed .setHasNewFeed=${(val)=> this.setHasNewFeed(val)}></friends-feed>
</div>
</div>
</div>
</div>
`;
}
}
customElements.define('friends-side-panel', FriendsSidePanel);

View File

@ -0,0 +1,182 @@
import { css } from 'lit'
export const friendsViewStyles = css`
* {
box-sizing: border-box;
}
.top-bar-icon {
cursor: pointer;
height: 18px;
width: 18px;
transition: 0.2s all;
}
.top-bar-icon:hover {
color: var(--black);
}
.modal-button {
font-family: Roboto, sans-serif;
font-size: 16px;
color: var(--mdc-theme-primary);
background-color: transparent;
padding: 8px 10px;
border-radius: 5px;
border: none;
transition: all 0.3s ease-in-out;
}
.close-row {
width: 100%;
display: flex;
justify-content: flex-end;
height: 50px;
flex:0
}
.container-body {
width: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
margin-top: 5px;
padding: 0px 6px;
box-sizing: border-box;
align-items: center;
gap: 10px;
}
.container-body::-webkit-scrollbar-track {
background-color: whitesmoke;
border-radius: 7px;
}
.container-body::-webkit-scrollbar {
width: 6px;
border-radius: 7px;
background-color: whitesmoke;
}
.container-body::-webkit-scrollbar-thumb {
background-color: rgb(180, 176, 176);
border-radius: 7px;
transition: all 0.3s ease-in-out;
}
.container-body::-webkit-scrollbar-thumb:hover {
background-color: rgb(148, 146, 146);
cursor: pointer;
}
p {
color: var(--black);
margin: 0px;
padding: 0px;
word-break: break-all;
}
.container {
display: flex;
width: 100%;
flex-direction: column;
height: 100%;
}
.chat-right-panel-label {
font-family: Montserrat, sans-serif;
color: var(--group-header);
padding: 5px;
font-size: 13px;
user-select: none;
}
.group-info {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 10px;
}
.group-name {
font-family: Raleway, sans-serif;
font-size: 20px;
color: var(--chat-bubble-msg-color);
text-align: center;
user-select: none;
}
.group-description {
font-family: Roboto, sans-serif;
color: var(--chat-bubble-msg-color);
letter-spacing: 0.3px;
font-weight: 300;
font-size: 14px;
margin-top: 15px;
word-break: break-word;
user-select: none;
}
.group-subheader {
font-family: Montserrat, sans-serif;
font-size: 14px;
color: var(--chat-bubble-msg-color);
}
.group-data {
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
font-size: 14px;
color: var(--chat-bubble-msg-color);
}
.search-results-div {
position: absolute;
top: 25px;
right: 25px;
}
.name-input {
width: 100%;
outline: 0;
border-width: 0 0 2px;
border-color: var(--mdc-theme-primary);
background-color: transparent;
padding: 10px;
font-family: Roboto, sans-serif;
font-size: 15px;
color: var(--chat-bubble-msg-color);
box-sizing: border-box;
}
.name-input::selection {
background-color: var(--mdc-theme-primary);
color: white;
}
.name-input::placeholder {
opacity: 0.9;
color: var(--black);
}
.search-field {
width: 100%;
position: relative;
}
.search-icon {
position: absolute;
right: 3px;
color: var(--chat-bubble-msg-color);
transition: hover 0.3s ease-in-out;
background: none;
border-radius: 50%;
padding: 6px 3px;
font-size: 21px;
}
.search-icon:hover {
cursor: pointer;
background: #d7d7d75c;
}
`

View File

@ -0,0 +1,398 @@
import { LitElement, html, css } from 'lit';
import { render } from 'lit/html.js';
import { connect } from 'pwa-helpers';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@polymer/paper-spinner/paper-spinner-lite.js';
import '@polymer/paper-progress/paper-progress.js';
import '@material/mwc-icon';
import '@vaadin/icon'
import '@vaadin/icons'
import '@vaadin/button';
import './ChatSideNavHeads';
import '../../../../plugins/plugins/core/components/ChatSearchResults'
import './add-friends-modal'
import {
use,
get,
translate,
translateUnsafeHTML,
registerTranslateConfig,
} from 'lit-translate';
import { store } from '../../store';
import { friendsViewStyles } from './friends-view-css';
import { parentEpml } from '../show-plugin';
class FriendsView extends connect(store)(LitElement) {
static get properties() {
return {
error: { type: Boolean },
toggle: { attribute: false },
userName: { type: String },
errorMessage: { type: String },
successMessage: { type: String },
setUserName: { attribute: false },
friendList: { type: Array },
userSelected: { type: Object },
isLoading: {type: Boolean},
userFoundModalOpen: {type: Boolean},
userFound: { type: Array},
isOpenAddFriendsModal: {type: Boolean},
editContent: {type: Object},
mySelectedFeeds: {type: Array},
refreshFeed: {attribute: false},
closeSidePanel: {attribute: false, type: Object}
};
}
static get styles() {
return [friendsViewStyles];
}
constructor() {
super();
this.error = false;
this.observerHandler = this.observerHandler.bind(this);
this.viewElement = '';
this.downObserverElement = '';
this.myAddress =
window.parent.reduxStore.getState().app.selectedAddress.address;
this.errorMessage = '';
this.successMessage = '';
this.friendList = [];
this.userSelected = {};
this.isLoading = false;
this.userFoundModalOpen = false
this.userFound = [];
this.nodeUrl = this.getNodeUrl();
this.myNode = this.getMyNode();
this.isOpenAddFriendsModal = false
this.editContent = null
this.addToFriendList = this.addToFriendList.bind(this)
this._updateFriends = this._updateFriends.bind(this)
this._updateFeed = this._updateFeed.bind(this)
}
getNodeUrl() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
const nodeUrl =
myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
return nodeUrl;
}
getMyNode() {
const myNode =
store.getState().app.nodeConfig.knownNodes[
window.parent.reduxStore.getState().app.nodeConfig.node
];
return myNode;
}
getMoreFriends() {}
firstUpdated() {
this.viewElement = this.shadowRoot.getElementById('viewElement');
this.downObserverElement =
this.shadowRoot.getElementById('downObserver');
this.elementObserver();
this.mySelectedFeeds = JSON.parse(localStorage.getItem('friends-my-selected-feeds') || "[]")
this.friendList = JSON.parse(localStorage.getItem('friends-my-friend-list') || "[]")
}
_updateFriends(event) {
const detail = event.detail
this.friendList = detail
}
_updateFeed(event) {
console.log({event})
const detail = event.detail
console.log({detail})
this.mySelectedFeeds = detail
this.requestUpdate()
}
connectedCallback() {
super.connectedCallback()
console.log('callback')
window.addEventListener('friends-my-friend-list-event', this._updateFriends)
window.addEventListener('friends-my-selected-feeds-event', this._updateFeed)
}
disconnectedCallback() {
window.removeEventListener('friends-my-friend-list-event', this._updateFriends)
window.addEventListener('friends-my-selected-feeds-event', this._updateFeed)
super.disconnectedCallback()
}
elementObserver() {
const options = {
root: this.viewElement,
rootMargin: '0px',
threshold: 1,
};
// identify an element to observe
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) {
if (!entries[0].isIntersecting) {
return;
} else {
if (this.friendList.length < 20) {
return;
}
this.getMoreFriends();
}
}
async userSearch() {
const nameValue = this.shadowRoot.getElementById('sendTo').value
if(!nameValue) {
this.userFound = []
this.userFoundModalOpen = true
return;
}
try {
const url = `${this.nodeUrl}/names/${nameValue}`
const res = await fetch(url)
const result = await res.json()
if (result.error === 401) {
this.userFound = []
} else {
this.userFound = [
result
];
}
this.userFoundModalOpen = true;
} catch (error) {
// let err4string = get("chatpage.cchange35");
// parentEpml.request('showSnackBar', `${err4string}`)
}
}
getApiKey() {
const apiNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node]
let apiKey = apiNode.apiKey
return apiKey
}
async myFollowName(name) {
let items = [
name
]
let namesJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/followedNames?apiKey=${this.getApiKey()}`,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: `${namesJsonString}`
})
return ret
}
async unFollowName(name) {
let items = [
name
]
let namesJsonString = JSON.stringify({ "items": items })
let ret = await parentEpml.request('apiCall', {
url: `/lists/followedNames?apiKey=${this.getApiKey()}`,
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: `${namesJsonString}`
})
return ret
}
async addToFriendList(val, isRemove){
const copyVal = {...val}
delete copyVal.mySelectedFeeds
if(isRemove){
this.friendList = this.friendList.filter((item)=> item.name !== copyVal.name)
}else if(this.editContent){
const findFriend = this.friendList.findIndex(item=> item.name === copyVal.name)
if(findFriend !== -1){
const copyList = [...this.friendList]
copyList[findFriend] = copyVal
this.friendList = copyList
}
} else {
this.friendList = [...this.friendList, copyVal]
}
if(!copyVal.willFollow || isRemove) {
this.unFollowName(copyVal.name)
} else if(copyVal.willFollow){
this.myFollowName(copyVal.name)
}
this.setMySelectedFeeds(val.mySelectedFeeds)
await new Promise((res)=> {
setTimeout(()=> {
res()
},50)
})
this.userSelected = {};
this.shadowRoot.getElementById('sendTo').value = ''
this.isLoading = false;
this.isOpenAddFriendsModal = false
this.editContent = null
this.setMyFriends(this.friendList)
if(!isRemove && this.friendList.length === 1){
this.refreshFeed()
}
}
setMyFriends(friendList){
localStorage.setItem('friends-my-friend-list', JSON.stringify(friendList));
const tempSettingsData= JSON.parse(localStorage.getItem('temp-settings-data') || "{}")
const newTemp = {
...tempSettingsData,
userLists: {
data: [friendList],
timestamp: Date.now()
}
}
localStorage.setItem('temp-settings-data', JSON.stringify(newTemp));
this.dispatchEvent(
new CustomEvent('temp-settings-data-event', {
bubbles: true,
composed: true
}),
);
}
setMySelectedFeeds(mySelectedFeeds){
this.mySelectedFeeds = mySelectedFeeds
const tempSettingsData= JSON.parse(localStorage.getItem('temp-settings-data') || "{}")
const newTemp = {
...tempSettingsData,
friendsFeed: {
data: mySelectedFeeds,
timestamp: Date.now()
}
}
localStorage.setItem('temp-settings-data', JSON.stringify(newTemp));
localStorage.setItem('friends-my-selected-feeds', JSON.stringify(mySelectedFeeds));
}
openEditFriend(val){
this.isOpenAddFriendsModal = true
this.userSelected = val
this.editContent = {...val, mySelectedFeeds: this.mySelectedFeeds}
}
onClose(){
this.isLoading = false;
this.isOpenAddFriendsModal = false
this.editContent = null
this.userSelected = {}
}
render() {
console.log('rendered1')
return html`
<div class="container">
<div id="viewElement" class="container-body" style=${"position: relative"}>
<p class="group-name">My Friends</p>
<div class="search-field">
<input
type="text"
class="name-input"
?disabled=${this.isLoading}
id="sendTo"
placeholder="${translate("friends.friend1")}"
value=${this.userSelected.name ? this.userSelected.name: ''}
@keypress=${(e) => {
if(e.key === 'Enter'){
this.userSearch()
}
}}
/>
<vaadin-icon
@click=${this.userSearch}
slot="icon"
icon="vaadin:search"
class="search-icon">
</vaadin-icon>
</div>
<div class="search-results-div">
<chat-search-results
.onClickFunc=${(result) => {
this.userSelected = result;
this.isOpenAddFriendsModal = true
this.userFound = [];
this.userFoundModalOpen = false;
}}
.closeFunc=${() => {
this.userFoundModalOpen = false;
this.userFound = [];
}}
.searchResults=${this.userFound}
?isOpen=${this.userFoundModalOpen}
?loading=${this.isLoading}>
</chat-search-results>
</div>
${this.friendList.map((item) => {
return html`<chat-side-nav-heads
activeChatHeadUrl=""
.setActiveChatHeadUrl=${(val) => {
}}
.chatInfo=${item}
.openEditFriend=${(val)=> this.openEditFriend(val)}
.closeSidePanel=${this.closeSidePanel}
></chat-side-nav-heads>`;
})}
<div id="downObserver"></div>
</div>
</div>
<add-friends-modal
?isOpen=${this.isOpenAddFriendsModal}
.setIsOpen=${(val)=> {
this.isOpenAddFriendsModal = val
}}
.userSelected=${this.userSelected}
.onSubmit=${(val, isRemove)=> this.addToFriendList(val, isRemove)}
.editContent=${this.editContent}
.onClose=${()=> this.onClose()}
.mySelectedFeeds=${this.mySelectedFeeds}
>
</add-friends-modal>
`;
}
}
customElements.define('friends-view', FriendsView);

View File

@ -0,0 +1,593 @@
import { LitElement, html, css } from 'lit';
import '@material/mwc-icon';
import './friends-side-panel.js';
import { connect } from 'pwa-helpers';
import { store } from '../../store.js';
import WebWorker from 'web-worker:./computePowWorkerFile.src.js';
import '@polymer/paper-spinner/paper-spinner-lite.js';
import '@vaadin/tooltip';
import { get, translate } from 'lit-translate';
import {
decryptGroupData,
encryptDataGroup,
objectToBase64,
uint8ArrayToBase64,
uint8ArrayToObject,
} from '../../../../plugins/plugins/core/components/qdn-action-encryption.js';
import { publishData } from '../../../../plugins/plugins/utils/publish-image.js';
import { parentEpml } from '../show-plugin.js';
import '../notification-view/popover.js';
class SaveSettingsQdn extends connect(store)(LitElement) {
static get properties() {
return {
isOpen: { type: Boolean },
syncPercentage: { type: Number },
settingsRawData: { type: Object },
valuesToBeSavedOnQdn: { type: Object },
resourceExists: { type: Boolean },
isSaving: { type: Boolean },
fee: { type: Object },
};
}
constructor() {
super();
this.isOpen = false;
this.getGeneralSettingsQdn = this.getGeneralSettingsQdn.bind(this);
this._updateTempSettingsData = this._updateTempSettingsData.bind(this);
this.setValues = this.setValues.bind(this);
this.saveToQdn = this.saveToQdn.bind(this);
this.syncPercentage = 0;
this.hasRetrievedResource = false;
this.hasAttemptedToFetchResource = false;
this.resourceExists = undefined;
this.settingsRawData = null;
this.nodeUrl = this.getNodeUrl();
this.myNode = this.getMyNode();
this.valuesToBeSavedOnQdn = {};
this.isSaving = false;
this.fee = null;
}
static styles = css`
:host {
margin-right: 20px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
}
.content {
padding: 16px;
}
.close {
visibility: hidden;
position: fixed;
z-index: -100;
right: -1000px;
}
.parent-side-panel {
transform: translateX(100%); /* start from outside the right edge */
transition: transform 0.3s ease-in-out;
}
.parent-side-panel.open {
transform: translateX(0); /* slide in to its original position */
}
.notActive {
opacity: 0.5;
cursor: default;
color: var(--black);
}
.active {
opacity: 1;
cursor: pointer;
color: green;
}
.accept-button {
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
padding: 8px 5px;
border-radius: 3px;
text-align: center;
color: var(--mdc-theme-primary);
transition: all 0.3s ease-in-out;
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
}
.accept-button:hover {
cursor: pointer;
background-color: #03a8f485;
}
.undo-button {
font-family: Roboto, sans-serif;
letter-spacing: 0.3px;
font-weight: 300;
padding: 8px 5px;
border-radius: 3px;
text-align: center;
color: #f44336;
transition: all 0.3s ease-in-out;
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
}
.undo-button:hover {
cursor: pointer;
background-color: #f4433663;
}
`;
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;
}
async getRawData(dataItem) {
const url = `${this.nodeUrl}/arbitrary/${dataItem.service}/${dataItem.name}/${dataItem.identifier}?encoding=base64`;
const res = await fetch(url);
const data = await res.text();
if (data.error) throw new Error('Cannot retrieve your data from qdn');
const decryptedData = decryptGroupData(data);
const decryptedDataToBase64 = uint8ArrayToObject(decryptedData);
return decryptedDataToBase64;
}
async setValues(response, resource) {
this.settingsRawData = response;
const rawDataTimestamp = resource.updated;
const tempSettingsData = JSON.parse(
localStorage.getItem('temp-settings-data') || '{}'
);
const userLists = response.userLists || [];
const friendsFeed = response.friendsFeed;
const myMenuPlugs = response.myMenuPlugs;
this.valuesToBeSavedOnQdn = {};
if (
userLists.length > 0 &&
(!tempSettingsData.userLists ||
(tempSettingsData.userLists &&
tempSettingsData.userLists.timestamp < rawDataTimestamp))
) {
const friendList = userLists[0];
const copyPayload = [...friendList];
localStorage.setItem(
'friends-my-friend-list',
JSON.stringify(friendList)
);
this.dispatchEvent(
new CustomEvent('friends-my-friend-list-event', {
bubbles: true,
composed: true,
detail: copyPayload,
})
);
} else if (
tempSettingsData.userLists &&
tempSettingsData.userLists.timestamp > rawDataTimestamp
) {
this.valuesToBeSavedOnQdn = {
...this.valuesToBeSavedOnQdn,
userLists: {
data: tempSettingsData.userLists.data,
},
};
}
if (
friendsFeed &&
(!tempSettingsData.friendsFeed ||
(tempSettingsData.friendsFeed &&
tempSettingsData.friendsFeed.timestamp < rawDataTimestamp))
) {
const copyPayload = [...friendsFeed];
localStorage.setItem(
'friends-my-selected-feeds',
JSON.stringify(friendsFeed)
);
this.dispatchEvent(
new CustomEvent('friends-my-selected-feeds-event', {
bubbles: true,
composed: true,
detail: copyPayload,
})
);
} else if (
tempSettingsData.friendsFeed &&
tempSettingsData.friendsFeed.timestamp > rawDataTimestamp
) {
this.valuesToBeSavedOnQdn = {
...this.valuesToBeSavedOnQdn,
friendsFeed: {
data: tempSettingsData.friendsFeed.data,
},
};
}
if (
myMenuPlugs &&
(!tempSettingsData.myMenuPlugs ||
(tempSettingsData.myMenuPlugs &&
tempSettingsData.myMenuPlugs.timestamp < rawDataTimestamp))
) {
if (Array.isArray(myMenuPlugs)) {
const copyPayload = [...myMenuPlugs];
localStorage.setItem(
'myMenuPlugs',
JSON.stringify(myMenuPlugs)
);
this.dispatchEvent(
new CustomEvent('myMenuPlugs-event', {
bubbles: true,
composed: true,
detail: copyPayload,
})
);
}
} else if (
tempSettingsData.myMenuPlugs &&
tempSettingsData.myMenuPlugs.timestamp > rawDataTimestamp
) {
this.valuesToBeSavedOnQdn = {
...this.valuesToBeSavedOnQdn,
myMenuPlugs: {
data: tempSettingsData.myMenuPlugs.data,
},
};
}
}
async getGeneralSettingsQdn() {
try {
const arbFee = await this.getArbitraryFee();
this.fee = arbFee;
this.hasAttemptedToFetchResource = true;
let resource;
const nameObject = store.getState().app.accountInfo.names[0];
if (!nameObject) throw new Error('no name');
const name = nameObject.name;
this.error = '';
const url = `${this.nodeUrl}/arbitrary/resources/search?service=DOCUMENT_PRIVATE&identifier=qortal_general_settings&name=${name}&prefix=true&exactmatchnames=true&excludeblocked=true&limit=20`;
const res = await fetch(url);
let data = '';
try {
data = await res.json();
if (Array.isArray(data)) {
data = data.filter(
(item) => item.identifier === 'qortal_general_settings'
);
if (data.length > 0) {
this.resourceExists = true;
const dataItem = data[0];
try {
const response = await this.getRawData(dataItem);
if (response.version) {
this.setValues(response, dataItem);
} else {
this.error = 'Cannot get saved user settings';
}
} catch (error) {
console.log({ error });
this.error = 'Cannot get saved user settings';
}
} else {
this.resourceExists = false;
}
} else {
this.error = 'Unable to perform query';
}
} catch (error) {
data = {
error: 'No resource found',
};
}
if (resource) {
this.hasRetrievedResource = true;
}
} catch (error) {
console.log({ error });
}
}
stateChanged(state) {
if (
state.app.accountInfo &&
state.app.accountInfo.names.length &&
state.app.nodeStatus &&
state.app.nodeStatus.syncPercent !== this.syncPercentage
) {
this.syncPercentage = state.app.nodeStatus.syncPercent;
if (
!this.hasAttemptedToFetchResource &&
state.app.nodeStatus.syncPercent === 100
) {
this.getGeneralSettingsQdn();
}
}
}
async getArbitraryFee() {
const timestamp = Date.now();
const url = `${this.nodeUrl}/transactions/unitfee?txType=ARBITRARY&timestamp=${timestamp}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Error when fetching arbitrary fee');
}
const data = await response.json();
const arbitraryFee = (Number(data) / 1e8).toFixed(8);
return {
timestamp,
fee: Number(data),
feeToShow: arbitraryFee,
};
}
async saveToQdn() {
try {
this.isSaving = true;
if (this.resourceExists === true && this.error)
throw new Error('Unable to save');
const nameObject = store.getState().app.accountInfo.names[0];
if (!nameObject) throw new Error('no name');
const name = nameObject.name;
const identifer = 'qortal_general_settings';
const filename = 'qortal_general_settings.json';
const selectedAddress = store.getState().app.selectedAddress;
const getArbitraryFee = await this.getArbitraryFee();
const feeAmount = getArbitraryFee.fee;
const friendsList = JSON.parse(
localStorage.getItem('friends-my-friend-list') || '[]'
);
const friendsFeed = JSON.parse(
localStorage.getItem('friends-my-selected-feeds') || '[]'
);
const myMenuPlugs = JSON.parse(
localStorage.getItem('myMenuPlugs') || '[]'
);
let newObject;
if (this.resourceExists === false) {
newObject = {
version: 1,
userLists: [friendsList],
friendsFeed,
myMenuPlugs,
};
} else if (this.settingsRawData) {
const tempSettingsData = JSON.parse(
localStorage.getItem('temp-settings-data') || '{}'
);
newObject = {
...this.settingsRawData,
};
for (const key in tempSettingsData) {
if (tempSettingsData[key].hasOwnProperty('data')) {
if (
key === 'userLists' &&
!Array.isArray(tempSettingsData[key].data)
)
continue;
if (
key === 'friendsFeed' &&
!Array.isArray(tempSettingsData[key].data)
)
continue;
if (
key === 'myMenuPlugs' &&
!Array.isArray(tempSettingsData[key].data)
)
continue;
newObject[key] = tempSettingsData[key].data;
}
}
}
const newObjectToBase64 = await objectToBase64(newObject);
const encryptedData = encryptDataGroup({
data64: newObjectToBase64,
publicKeys: [],
});
const worker = new WebWorker();
try {
const resPublish = await publishData({
registeredName: encodeURIComponent(name),
file: encryptedData,
service: 'DOCUMENT_PRIVATE',
identifier: encodeURIComponent(identifer),
parentEpml: parentEpml,
uploadType: 'file',
selectedAddress: selectedAddress,
worker: worker,
isBase64: true,
filename: filename,
apiVersion: 2,
withFee: true,
feeAmount: feeAmount,
});
this.resourceExists = true;
this.setValues(newObject, {
updated: Date.now(),
});
localStorage.setItem('temp-settings-data', JSON.stringify({}));
this.valuesToBeSavedOnQdn = {};
worker.terminate();
} catch (error) {
worker.terminate();
}
} catch (error) {
console.log({ error });
} finally {
this.isSaving = false;
}
}
_updateTempSettingsData() {
this.valuesToBeSavedOnQdn = JSON.parse(
localStorage.getItem('temp-settings-data') || '{}'
);
}
connectedCallback() {
super.connectedCallback();
window.addEventListener(
'temp-settings-data-event',
this._updateTempSettingsData
);
}
disconnectedCallback() {
window.removeEventListener(
'temp-settings-data-event',
this._updateTempSettingsData
);
super.disconnectedCallback();
}
render() {
return html`
${this.isSaving ||
(!this.error && this.resourceExists === undefined)
? html`
<paper-spinner-lite
active
style="display: block; margin: 0 auto;"
></paper-spinner-lite>
`
: html`
<mwc-icon
id="save-icon"
class=${Object.values(this.valuesToBeSavedOnQdn)
.length > 0 || this.resourceExists === false
? 'active'
: 'notActive'}
@click=${() => {
if (
Object.values(this.valuesToBeSavedOnQdn)
.length > 0 ||
this.resourceExists === false
) {
if (!this.fee) return;
// this.saveToQdn()
const target =
this.shadowRoot.getElementById(
'popover-notification'
);
const popover =
this.shadowRoot.querySelector(
'popover-component'
);
if (popover) {
popover.openPopover(target);
}
}
// this.isOpen = !this.isOpen
}}
style="user-select:none"
>save</mwc-icon
>
<vaadin-tooltip
for="save-icon"
position="bottom"
hover-delay=${300}
hide-delay=${1}
text=${this.error
? get('save.saving1')
: Object.values(this.valuesToBeSavedOnQdn)
.length > 0 ||
this.resourceExists === false
? get('save.saving3')
: get('save.saving2')}
>
</vaadin-tooltip>
<popover-component for="save-icon" message="">
<div style="margin-bottom:20px">
<p style="margin:10px 0px; font-size:16px">
${`${get('walletpage.wchange12')}: ${
this.fee ? this.fee.feeToShow : ''
}`}
</p>
</div>
<div
style="display:flex;justify-content:space-between;gap:10px"
>
<div
class="undo-button"
@click="${() => {
localStorage.setItem(
'temp-settings-data',
JSON.stringify({})
);
this.valuesToBeSavedOnQdn = {};
const popover =
this.shadowRoot.querySelector(
'popover-component'
);
if (popover) {
popover.closePopover();
}
this.getGeneralSettingsQdn();
}}"
>
${translate('save.saving4')}
</div>
<div
class="accept-button"
@click="${() => {
this.saveToQdn();
const popover =
this.shadowRoot.querySelector(
'popover-component'
);
if (popover) {
popover.closePopover();
}
}}"
>
${translate('browserpage.bchange28')}
</div>
</div>
</popover-component>
`}
`;
}
}
customElements.define('save-settings-qdn', SaveSettingsQdn);

View File

@ -49,7 +49,7 @@ class WelcomePage extends LitElement {
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
}
firstUpdate() {
firstUpdated() {
// ...
}

View File

@ -111,15 +111,30 @@ class NotificationBellGeneral extends connect(store)(LitElement) {
>
${hasOngoing
? html`
<mwc-icon style="color: green;cursor:pointer"
<mwc-icon id="notification-general-icon" style="color: green;cursor:pointer;user-select:none"
>notifications</mwc-icon
>
<vaadin-tooltip
for="notification-general-icon"
position="bottom"
hover-delay=${400}
hide-delay=${1}
text=${get('notifications.notify4')}>
</vaadin-tooltip>
`
: html`
<mwc-icon
style="color: var(--black); cursor:pointer"
id="notification-general-icon"
style="color: var(--black); cursor:pointer;user-select:none"
>notifications</mwc-icon
>
<vaadin-tooltip
for="notification-general-icon"
position="bottom"
hover-delay=${400}
hide-delay=${1}
text=${get('notifications.notify4')}>
</vaadin-tooltip>
`}
</div>
${hasOngoing
@ -147,6 +162,9 @@ class NotificationBellGeneral extends connect(store)(LitElement) {
@blur=${this.handleBlur}
>
<div class="notifications-list">
${this.notifications.length === 0 ? html`
<p style="font-size: 16px; width: 100%; text-align:center;margin-top:20px;">${translate('notifications.notify3')}</p>
` : ''}
${repeat(
this.notifications,
(notification) => notification.reference.signature, // key function
@ -169,7 +187,6 @@ class NotificationBellGeneral extends connect(store)(LitElement) {
}
_toggleNotifications() {
if (this.notifications.length === 0) return;
this.showNotifications = !this.showNotifications;
if (this.showNotifications) {
requestAnimationFrame(() => {
@ -184,7 +201,6 @@ class NotificationBellGeneral extends connect(store)(LitElement) {
flex-direction: column;
align-items: center;
position: relative;
margin-right: 20px;
}
.count {

View File

@ -8,6 +8,8 @@ import '@polymer/iron-icons/iron-icons.js'
import { store } from '../../store.js'
import { setNewTab } from '../../redux/app/app-actions.js'
import { routes } from '../../plugins/routes.js'
import '@material/mwc-icon';
import config from '../../notifications/config.js'
import '../../../../plugins/plugins/core/components/TimeAgo.js'
@ -138,11 +140,29 @@ class NotificationBell extends connect(store)(LitElement) {
return html`
<div class="layout">
${this.notificationCount ? html`
<paper-icon-button style="color: green;" icon="icons:mail" @click=${() => this._toggleNotifications()} title="Q-Mail"></paper-icon-button>
<mwc-icon @click=${() => this._toggleNotifications()} id="notification-mail-icon" style="color: green;cursor:pointer;user-select:none"
>mail</mwc-icon
>
<vaadin-tooltip
for="notification-mail-icon"
position="bottom"
hover-delay=${400}
hide-delay=${1}
text="Q-Mail">
</vaadin-tooltip>
` : html`
<paper-icon-button icon="icons:mail" @click=${() => {
this._openTabQmail()
}} title="Q-Mail"></paper-icon-button>
<mwc-icon @click=${() => this._openTabQmail()} id="notification-mail-icon" style="color: var(--black); cursor:pointer;user-select:none"
>mail</mwc-icon
>
<vaadin-tooltip
for="notification-mail-icon"
position="bottom"
hover-delay=${400}
hide-delay=${1}
text="Q-Mail">
</vaadin-tooltip>
`}
${this.notificationCount ? html`
@ -218,8 +238,8 @@ class NotificationBell extends connect(store)(LitElement) {
.count {
position: absolute;
top: 2px;
right: 0px;
top: -5px;
right: -5px;
font-size: 12px;
background-color: red;
color: white;
@ -229,6 +249,7 @@ class NotificationBell extends connect(store)(LitElement) {
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.nocount {

View File

@ -23,6 +23,8 @@ export class PopoverComponent extends LitElement {
margin-left: 10px;
color: var(--black)
}
`;
static properties = {
@ -40,7 +42,6 @@ export class PopoverComponent extends LitElement {
}
attachToTarget(target) {
console.log({target})
if (!this.popperInstance && target) {
this.popperInstance = createPopper(target, this, {
placement: 'bottom',
@ -66,7 +67,8 @@ export class PopoverComponent extends LitElement {
render() {
return html`
<span class="close-icon" @click="${this.closePopover}"><mwc-icon style="color: var(--black)">close</mwc-icon></span>
<div><mwc-icon style="color: var(--black)">info</mwc-icon> ${this.message}</div>
<div><mwc-icon style="color: var(--black)">info</mwc-icon> ${this.message} <slot></slot>
</div>
`;
}
}

View File

@ -114,7 +114,6 @@ class QortThemeToggle extends LitElement {
} else {
this.theme = 'light';
}
this.dispatchEvent(
new CustomEvent('qort-theme-change', {
bubbles: true,

View File

@ -26,11 +26,9 @@ import '@vaadin/grid'
import '@vaadin/text-field'
import '../custom-elements/frag-file-input.js'
const chatLastSeen = localForage.createInstance({
name: "chat-last-seen",
})
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
export const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
class ShowPlugin extends connect(store)(LitElement) {
static get properties() {
@ -435,9 +433,21 @@ class ShowPlugin extends connect(store)(LitElement) {
@click="${() => {
this.currentTab = index
}}"
@mousedown="${(event) => {
if (event.button === 1) {
event.preventDefault();
this.removeTab(index, tab.id);
}
}}"
>
<div id="icon-${tab.id}" class="${this.currentTab === index ? "iconActive" : "iconInactive"}">
<mwc-icon>${icon}</mwc-icon>
${tab.myPlugObj && tab.myPlugObj.url === "myapp" ? html`
<tab-avatar appname=${title} appicon=${icon}></tab-avatar>
` : html`
<mwc-icon>${icon}</mwc-icon>
`}
</div>
<div class="tabCard">
${count ? html`
@ -996,6 +1006,7 @@ class NavBar extends connect(store)(LitElement) {
border-top-right-radius: 20px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 10px;
position: relative;
}
.app-list .app-icon:hover .removeIcon {
@ -1009,14 +1020,14 @@ class NavBar extends connect(store)(LitElement) {
}
.menuIconPos {
position: relative;
right: -2px;
}
.removeIconPos {
position: absolute;
top: -36px;
left: 0;
top: -10px;
right: -10px;
z-index: 1;
}
.menuIconPos:hover .removeIcon {
@ -1028,9 +1039,7 @@ class NavBar extends connect(store)(LitElement) {
color: var(--black);
--mdc-icon-size: 28px;
cursor: pointer;
position: absolute;
top: 30px;
left: 123px;
position: relative;
z-index: 1;
}
@ -1192,6 +1201,7 @@ class NavBar extends connect(store)(LitElement) {
this.myFollowedNamesList = []
this.searchContentString = ''
this.searchNameResources = []
this._updateMyMenuPlugins = this._updateMyMenuPlugins.bind(this)
}
render() {
@ -1458,6 +1468,49 @@ class NavBar extends connect(store)(LitElement) {
await this.getMyFollowedNamesList()
}
async _updateMyMenuPlugins(event) {
await new Promise((res)=> {
setTimeout(() => {
res()
}, 1000);
})
const detail = event.detail
this.myMenuPlugins = detail
const addressInfo = this.addressInfo
const isMinter = addressInfo?.error !== 124 && +addressInfo?.level > 0
const isSponsor = +addressInfo?.level >= 5
if (!isMinter) {
this.newMenuList = this.myMenuPlugins.filter((minter) => {
return minter.url !== 'minting'
})
} else {
this.newMenuList = this.myMenuPlugins.filter((minter) => {
return minter.url !== 'become-minter'
})
}
if (!isSponsor) {
this.myMenuList = this.newMenuList.filter((sponsor) => {
return sponsor.url !== 'sponsorship-list'
})
} else {
this.myMenuList = this.newMenuList
}
this.requestUpdate()
}
connectedCallback() {
super.connectedCallback()
window.addEventListener('myMenuPlugs-event', this._updateMyMenuPlugins) }
disconnectedCallback() {
window.removeEventListener('myMenuPlugs-event', this._updateMyMenuPlugins)
super.disconnectedCallback()
}
openImportDialog() {
this.shadowRoot.getElementById('importTabMenutDialog').show()
}
@ -1468,7 +1521,9 @@ class NavBar extends connect(store)(LitElement) {
localStorage.removeItem("myMenuPlugs")
myFile = file
const newTabMenu = JSON.parse((myFile) || "[]")
const copyPayload = [...newTabMenu]
localStorage.setItem("myMenuPlugs", JSON.stringify(newTabMenu))
this.saveSettingToTemp(copyPayload)
this.shadowRoot.getElementById('importTabMenutDialog').close()
this.myMenuPlugins = JSON.parse(localStorage.getItem("myMenuPlugs") || "[]")
this.firstUpdated()
@ -1951,8 +2006,10 @@ class NavBar extends connect(store)(LitElement) {
if (myNameRes !== false) {
oldMenuPlugs.push(newMenuPlugsItem)
const copyPayload = [...oldMenuPlugs]
localStorage.setItem("myMenuPlugs", JSON.stringify(oldMenuPlugs))
this.saveSettingToTemp(copyPayload)
let myplugstring2 = get("walletpage.wchange52")
parentEpml.request('showSnackBar', `${myplugstring2}`)
@ -2014,9 +2071,10 @@ class NavBar extends connect(store)(LitElement) {
if (myNameRes !== false) {
oldMenuPlugs.push(newMenuPlugsItem)
const copyPayload = [...oldMenuPlugs]
localStorage.setItem("myMenuPlugs", JSON.stringify(oldMenuPlugs))
this.saveSettingToTemp(copyPayload)
let myplugstring2 = get("walletpage.wchange52")
parentEpml.request('showSnackBar', `${myplugstring2}`)
@ -2084,9 +2142,10 @@ class NavBar extends connect(store)(LitElement) {
}
oldMenuPlugs.push(newMenuPlugsItem)
const copyPayload = [...oldMenuPlugs]
localStorage.setItem("myMenuPlugs", JSON.stringify(oldMenuPlugs))
this.saveSettingToTemp(copyPayload)
let myplugstring2 = get("walletpage.wchange52")
parentEpml.request('showSnackBar', `${myplugstring2}`)
@ -2149,11 +2208,20 @@ class NavBar extends connect(store)(LitElement) {
renderRemoveIcon(appurl, appicon, appname, appid, appplugin) {
return html`
<div class="removeIconPos" title="${translate('tabmenu.tm22')}" @click="${() => this.openRemoveApp(appname, appid, appurl)}">
<div class="menuIconPos" @click="${() => this.changePage(appplugin)}">
<div class="removeIconPos" title="${translate('tabmenu.tm22')}" @click="${(event) => {
event.stopPropagation();
this.openRemoveApp(appname, appid, appurl)
} }">
<mwc-icon class="removeIcon">backspace</mwc-icon>
</div>
<div class="menuIconPos" @click="${() => this.changePage(appplugin)}">
${appurl === 'myapp' ? html`
<app-avatar appicon=${appicon} appname=${appname}></app-avatar>
` : html`
<mwc-icon class="menuIcon">${appicon}</mwc-icon>
`}
</div>
`
}
@ -2210,14 +2278,36 @@ class NavBar extends connect(store)(LitElement) {
const pluginToRemove = this.pluginNumberToDelete
this.newMenuFilter = []
this.newMenuFilter = this.myMenuList.filter((item) => item.pluginNumber !== pluginToRemove)
const copyPayload = [...this.newMenuFilter]
const myNewObj = JSON.stringify(this.newMenuFilter)
localStorage.removeItem("myMenuPlugs")
localStorage.setItem("myMenuPlugs", myNewObj)
this.saveSettingToTemp(copyPayload)
this.myMenuPlugins = JSON.parse(localStorage.getItem("myMenuPlugs") || "[]")
this.firstUpdated()
this.closeRemoveApp()
}
saveSettingToTemp(data){
const tempSettingsData= JSON.parse(localStorage.getItem('temp-settings-data') || "{}")
const newTemp = {
...tempSettingsData,
myMenuPlugs: {
data: data,
timestamp: Date.now()
}
}
localStorage.setItem('temp-settings-data', JSON.stringify(newTemp));
this.dispatchEvent(
new CustomEvent('temp-settings-data-event', {
bubbles: true,
composed: true
}),
);
}
closeRemoveApp() {
this.shadowRoot.querySelector('#removePlugin').close()
this.pluginNameToDelete = ''
@ -2348,3 +2438,148 @@ class NavBar extends connect(store)(LitElement) {
}
customElements.define('nav-bar', NavBar)
class AppAvatar extends LitElement {
static get properties() {
return {
hasAvatar: { type: Boolean },
isImageLoaded: {type: Boolean},
appicon: {type: String},
appname: {type: String}
}
}
constructor() {
super()
this.hasAvatar = false
this.isImageLoaded = false
this.imageFetches = 0
}
static get styles() {
return css`
:host {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
}
.menuIcon {
color: var(--app-icon);
--mdc-icon-size: 64px;
cursor: pointer;
}
`
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "border-radius:10px; font-size:14px; object-fit: fill;height:60px;width:60px";
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 1) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 5000);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render(){
let avatarImg = ""
if (this.appname) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.appname}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = this.createImage(avatarUrl)
}
return html`
${this.isImageLoaded ? html`
${avatarImg}
` : html`
<mwc-icon class="menuIcon">${this.appicon}</mwc-icon>
`}
`
}
}
customElements.define('app-avatar', AppAvatar)
class TabAvatar extends LitElement {
static get properties() {
return {
hasAvatar: { type: Boolean },
isImageLoaded: {type: Boolean},
appicon: {type: String},
appname: {type: String}
}
}
constructor() {
super()
this.hasAvatar = false
this.isImageLoaded = false
this.imageFetches = 0
}
createImage(imageUrl) {
const imageHTMLRes = new Image();
imageHTMLRes.src = imageUrl;
imageHTMLRes.style= "border-radius:4px; font-size:14px; object-fit: fill;height:24px;width:24px";
imageHTMLRes.onload = () => {
this.isImageLoaded = true;
}
imageHTMLRes.onerror = () => {
if (this.imageFetches < 1) {
setTimeout(() => {
this.imageFetches = this.imageFetches + 1;
imageHTMLRes.src = imageUrl;
}, 5000);
} else {
this.isImageLoaded = false
}
};
return imageHTMLRes;
}
render(){
let avatarImg = ""
if (this.appname) {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
const nodeUrl = myNode.protocol + '://' + myNode.domain + ':' + myNode.port;
const avatarUrl = `${nodeUrl}/arbitrary/THUMBNAIL/${this.appname}/qortal_avatar?async=true&apiKey=${myNode.apiKey}`;
avatarImg = this.createImage(avatarUrl)
}
return html`
${this.isImageLoaded ? html`
${avatarImg}
` : html`
<mwc-icon>${this.appicon}</mwc-icon>
`}
`
}
}
customElements.define('tab-avatar', TabAvatar)

View File

@ -68,7 +68,9 @@ export default (state = INITIAL_STATE, action) => {
loggedIn: false,
loggingIn: false,
wallet: INITIAL_STATE.wallet,
selectedAddress: INITIAL_STATE.selectedAddress
selectedAddress: INITIAL_STATE.selectedAddress,
accountInfo: INITIAL_STATE.accountInfo
}
case ADD_PLUGIN:
return {

View File

@ -218,7 +218,6 @@ class ChatGroupsManagement extends LitElement {
}
nameRenderer(person){
console.log({person})
return html`
<vaadin-horizontal-layout style="align-items: center;display:flex" theme="spacing">
<vaadin-avatar style="margin-right:5px" img="${person.pictureUrl}" .name="${person.displayName}"></vaadin-avatar>
@ -229,9 +228,7 @@ class ChatGroupsManagement extends LitElement {
render() {
return html`
<!-- <vaadin-icon @click=${()=> {
this.isOpenLeaveModal = true
}} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:exit" slot="icon"></vaadin-icon> -->
<!-- Leave Group Dialog -->
<wrapper-modal
.removeImage=${() => {

View File

@ -1199,6 +1199,11 @@ class ChatPage extends LitElement {
}, 2000)
}
const chatScrollerElement = this.shadowRoot.querySelector('chat-scroller');
if (chatScrollerElement && chatScrollerElement.disableFetching) {
chatScrollerElement.disableFetching = false
}
return
}
this.isLoadingGoToRepliedMessage = {
@ -1668,12 +1673,20 @@ class ChatPage extends LitElement {
}
})
this.messagesRendered = {
messages: list,
type: 'inBetween',
message: messageToGoTo
}
const lastMsg = list.at(-1)
if(lastMsg){
const count = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages/count?after=${lastMsg.timestamp}&involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=false`
})
this.messagesRendered = {
messages: list,
type: 'inBetween',
message: messageToGoTo,
count
}
}
this.isLoadingOldMessages = false
@ -1727,11 +1740,20 @@ class ChatPage extends LitElement {
}
})
this.messagesRendered = {
messages: list,
type: 'inBetween',
signature: messageToGoTo.signature
}
const lastMsg = list.at(-1)
if(lastMsg){
const count = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages/count?after=${lastMsg.timestamp}&txGroupId=${Number(this._chatId)}&limit=20&reverse=false`
})
this.messagesRendered = {
messages: list,
type: 'inBetween',
signature: messageToGoTo.signature,
count
}
}
this.isLoadingOldMessages = false

View File

@ -345,7 +345,7 @@ class ChatScroller extends LitElement {
this.requestUpdate();
}
async newListMessages(newMessages) {
async newListMessages(newMessages, count) {
let data = [];
const copy = [...newMessages];
copy.forEach((newMessage) => {
@ -368,6 +368,9 @@ class ChatScroller extends LitElement {
// url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=${chatLimit}&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false&encoding=BASE64`
// })
this.messagesToRender = data;
if (count > 0) {
this.disableAddingNewMessages = true;
}
this.clearLoaders();
this.requestUpdate();
await this.updateComplete;
@ -645,7 +648,7 @@ class ChatScroller extends LitElement {
else if (this.messages.type === 'inBetween')
this.newListMessages(
this.messages.messages,
this.messages.signature
this.messages.count
);
else if (this.messages.type === 'update')
this.replaceMessagesWithUpdateByArray(this.messages.messages);

View File

@ -1,12 +1,10 @@
import { LitElement, html } from 'lit'
import { render } from 'lit/html.js'
import { Epml } from '../../../epml.js'
import { chatSearchResultsStyles } from './ChatSearchResults-css.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import '@vaadin/icon'
import '@vaadin/icons'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
export class ChatSearchResults extends LitElement {
static get properties() {
@ -19,7 +17,10 @@ export class ChatSearchResults extends LitElement {
}
}
static styles = [chatSearchResultsStyles]
static get styles() {
return [chatSearchResultsStyles];
}
render() {
return html`

View File

@ -8,7 +8,6 @@ font-size: 28px;
color: var(--chat-bubble-msg-color);
margin-bottom: 10px;
padding: 10px 0;
user-select: none;
}
.avatar-container {

View File

@ -1,14 +1,11 @@
import { LitElement, html } from 'lit'
import { render } from 'lit/html.js'
import { use, get, translate, translateUnsafeHTML, registerTranslateConfig } from 'lit-translate'
import { translate } from 'lit-translate'
import { userInfoStyles } from './UserInfo-css.js'
import { Epml } from '../../../../epml'
import { cropAddress } from '../../../utils/cropAddress.js'
import '@polymer/paper-progress/paper-progress.js'
import '@vaadin/button'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
export class UserInfo extends LitElement {
static get properties() {
@ -29,7 +26,10 @@ export class UserInfo extends LitElement {
this.imageFetches = 0
}
static styles = [userInfoStyles]
static get styles() {
return [userInfoStyles];
}
createImage(imageUrl) {
const imageHTMLRes = new Image()

View File

@ -89,6 +89,47 @@ export function base64ToUint8Array(base64) {
return bytes
}
export function uint8ArrayToObject(uint8Array) {
// Decode the byte array using TextDecoder
const decoder = new TextDecoder()
const jsonString = decoder.decode(uint8Array)
// Convert the JSON string back into an object
const obj = JSON.parse(jsonString)
return obj
}
export function objectToBase64(obj) {
// Step 1: Convert the object to a JSON string
const jsonString = JSON.stringify(obj);
// Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: 'application/json' });
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === 'string') {
// Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace(
'data:application/json;base64,',
''
);
resolve(base64);
} else {
reject(new Error('Failed to read the Blob as a base64-encoded string'));
}
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsDataURL(blob);
});
}
export const encryptData = ({ data64, recipientPublicKey }) => {

View File

@ -516,6 +516,27 @@ class Chat extends LitElement {
chatHeads = JSON.parse(chatHeads)
this.getChatHeadFromState(chatHeads)
})
parentEpml.subscribe('side_effect_action', async sideEffectActionParam => {
const sideEffectAction = JSON.parse(sideEffectActionParam)
if(sideEffectAction && sideEffectAction.type === 'openPrivateChat'){
const name = sideEffectAction.data.name
const address = sideEffectAction.data.address
if(this.chatHeadsObj.direct && this.chatHeadsObj.direct.find(item=> item.address === address)){
this.setActiveChatHeadUrl(`direct/${address}`)
window.parent.reduxStore.dispatch(
window.parent.reduxAction.setSideEffectAction(null))
} else {
this.setOpenPrivateMessage({
open: true,
name: name
})
window.parent.reduxStore.dispatch(
window.parent.reduxAction.setSideEffectAction(null))
}
}
})
parentEpml.request('apiCall', {
url: `/addresses/balance/${window.parent.reduxStore.getState().app.selectedAddress.address}`
}).then(res => {

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;