mirror of
https://github.com/Qortal/qortal-ui.git
synced 2025-03-13 11:12:32 +00:00
Merge pull request #208 from Philreact/feature/friends-list
Feature/friends list
This commit is contained in:
commit
ac3a097b29
31
blog-test.json
Normal file
31
blog-test.json
Normal 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;"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
|
224
core/src/components/friends-view/ChatSideNavHeads.js
Normal file
224
core/src/components/friends-view/ChatSideNavHeads.js
Normal 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)
|
496
core/src/components/friends-view/add-friends-modal.js
Normal file
496
core/src/components/friends-view/add-friends-modal.js
Normal 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);
|
92
core/src/components/friends-view/computePowWorkerFile.src.js
Normal file
92
core/src/components/friends-view/computePowWorkerFile.src.js
Normal 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
|
||||
}
|
78
core/src/components/friends-view/core-sync-status.js
Normal file
78
core/src/components/friends-view/core-sync-status.js
Normal 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);
|
516
core/src/components/friends-view/feed-item.js
Normal file
516
core/src/components/friends-view/feed-item.js
Normal 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);
|
237
core/src/components/friends-view/friend-item-actions.js
Normal file
237
core/src/components/friends-view/friend-item-actions.js
Normal 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);
|
471
core/src/components/friends-view/friends-feed.js
Normal file
471
core/src/components/friends-view/friends-feed.js
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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);
|
151
core/src/components/friends-view/friends-side-panel.js
Normal file
151
core/src/components/friends-view/friends-side-panel.js
Normal 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);
|
182
core/src/components/friends-view/friends-view-css.js
Normal file
182
core/src/components/friends-view/friends-view-css.js
Normal 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;
|
||||
}
|
||||
`
|
398
core/src/components/friends-view/friends-view.js
Normal file
398
core/src/components/friends-view/friends-view.js
Normal 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);
|
593
core/src/components/friends-view/save-settings-qdn.js
Normal file
593
core/src/components/friends-view/save-settings-qdn.js
Normal 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×tamp=${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);
|
@ -49,7 +49,7 @@ class WelcomePage extends LitElement {
|
||||
this.theme = localStorage.getItem('qortalTheme') ? localStorage.getItem('qortalTheme') : 'light'
|
||||
}
|
||||
|
||||
firstUpdate() {
|
||||
firstUpdated() {
|
||||
// ...
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,6 @@ class QortThemeToggle extends LitElement {
|
||||
} else {
|
||||
this.theme = 'light';
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('qort-theme-change', {
|
||||
bubbles: true,
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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=${() => {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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`
|
||||
|
@ -8,7 +8,6 @@ font-size: 28px;
|
||||
color: var(--chat-bubble-msg-color);
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
|
@ -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()
|
||||
|
@ -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 }) => {
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -281,10 +281,7 @@ class WebBrowser extends LitElement {
|
||||
else {
|
||||
identifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
const path = parts.join("/");
|
||||
|
||||
}extractComponents
|
||||
const components = {};
|
||||
components["service"] = service;
|
||||
components["name"] = name;
|
||||
|
Loading…
x
Reference in New Issue
Block a user