diff --git a/qortal-ui-core/font/switch-theme.css b/qortal-ui-core/font/switch-theme.css index 52a6748f..b9eeee64 100644 --- a/qortal-ui-core/font/switch-theme.css +++ b/qortal-ui-core/font/switch-theme.css @@ -47,6 +47,7 @@ html { --chatHeadBgActive: #ebebeb; --chatHeadText: #080808; --chatHeadTextActive: #080808; + --lightChatHeadHover: #1e1f201a; } html[theme="dark"] { @@ -98,4 +99,5 @@ html[theme="dark"] { --chatHeadBgActive: #0f1a2e; --chatHeadText: #ffffff; --chatHeadTextActive: #ffffff; + --lightChatHeadHover: #e0e1e31a; } \ No newline at end of file diff --git a/qortal-ui-core/src/components/app-view.js b/qortal-ui-core/src/components/app-view.js index 4010f1b0..51325e36 100644 --- a/qortal-ui-core/src/components/app-view.js +++ b/qortal-ui-core/src/components/app-view.js @@ -55,6 +55,8 @@ class AppView extends connect(store)(LitElement) { background: var(--sidetopbar); color: var(--black); border-top: var(--border); + height: 48px; + padding: 3px; } #sideBar { diff --git a/qortal-ui-plugins/package.json b/qortal-ui-plugins/package.json index b1694c27..e7b41de8 100644 --- a/qortal-ui-plugins/package.json +++ b/qortal-ui-plugins/package.json @@ -17,6 +17,7 @@ "author": "QORTAL ", "license": "GPL-3.0", "dependencies": { + "@lit-labs/motion": "^1.0.3", "@material/mwc-list": "0.27.0", "@material/mwc-select": "0.27.0", "asmcrypto.js": "2.3.2", diff --git a/qortal-ui-plugins/plugins/core/components/ChatLeaveGroup.js b/qortal-ui-plugins/plugins/core/components/ChatLeaveGroup.js new file mode 100644 index 00000000..85bdb53b --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatLeaveGroup.js @@ -0,0 +1,247 @@ +import { LitElement, html, css } from 'lit'; +import { render } from 'lit/html.js'; +import { get, translate } from 'lit-translate'; +import { Epml } from '../../../epml'; +import snackbar from './snackbar.js' +import '@material/mwc-button'; +import '@material/mwc-dialog'; +import '@polymer/paper-spinner/paper-spinner-lite.js' +import '@material/mwc-icon'; +import './WrapperModal'; +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +class ChatLeaveGroup extends LitElement { + static get properties() { + return { + isLoading: { type: Boolean }, + isOpenLeaveModal: {type: Boolean}, + leaveGroupObj: { type: Object }, + error: {type: Boolean}, + message: {type: String}, + chatHeads: {type: Array}, + setActiveChatHeadUrl: {attribute: false} + } + } + + constructor() { + super(); + this.isLoading = false; + this.isOpenLeaveModal = false + this.leaveGroupObj = {} + this.leaveFee = 0.001 + this.error = false + this.message = '' + this.chatHeads = [] + } + + static get styles() { + return css` + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: .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; + } + ` + } + + firstUpdated() { + + } + + timeIsoString(timestamp) { + let myTimestamp = timestamp === undefined ? 1587560082346 : timestamp + let time = new Date(myTimestamp) + return time.toISOString() + } + + resetDefaultSettings() { + this.error = false + this.message = '' + this.isLoading = false + } + + renderErr9Text() { + return html`${translate("grouppage.gchange49")}` + } + + async confirmRelationship() { + + + let interval = null + let stop = false + const getAnswer = async () => { + const currentChats = this.chatHeads + + if (!stop) { + stop = true; + try { + const findGroup = currentChats.find((item)=> item.groupId === this.leaveGroupObj.groupId) + if (!findGroup) { + clearInterval(interval) + this.isLoading = false + this.isOpenLeaveModal= false + this.setActiveChatHeadUrl('') + } + + } catch (error) { + } + stop = false + } + }; + interval = setInterval(getAnswer, 5000); + } + + async _leaveGroup(groupId, groupName) { + // Reset Default Settings... + this.resetDefaultSettings() + const leaveFeeInput = this.leaveFee + + this.isLoading = true + + // Get Last Ref + const getLastRef = async () => { + let myRef = await parentEpml.request('apiCall', { + type: 'api', + url: `/addresses/lastreference/${this.selectedAddress.address}` + }) + return myRef + }; + + const validateReceiver = async () => { + let lastRef = await getLastRef(); + let myTransaction = await makeTransactionRequest(lastRef) + getTxnRequestResponse(myTransaction) + + } + + // Make Transaction Request + const makeTransactionRequest = async (lastRef) => { + let groupdialog3 = get("transactions.groupdialog3") + let groupdialog4 = get("transactions.groupdialog4") + let myTxnrequest = await parentEpml.request('transaction', { + type: 32, + nonce: this.selectedAddress.nonce, + params: { + fee: leaveFeeInput, + registrantAddress: this.selectedAddress.address, + rGroupName: groupName, + rGroupId: groupId, + lastReference: lastRef, + groupdialog3: groupdialog3, + groupdialog4: groupdialog4, + } + }) + return myTxnrequest + } + + const getTxnRequestResponse = (txnResponse) => { + + if (txnResponse.success === false && txnResponse.message) { + this.error = true + this.message = txnResponse.message + throw new Error(txnResponse) + } else if (txnResponse.success === true && !txnResponse.data.error) { + this.message = this.renderErr9Text() + this.error = false + this.confirmRelationship() + } else { + this.error = true + this.message = txnResponse.data.message + throw new Error(txnResponse) + } + } + validateReceiver() + } + + render() { + return html` + { + this.isOpenLeaveModal = true + }} class="top-bar-icon" style="margin: 0px 20px" icon="vaadin:exit" slot="icon"> + + { + if(this.isLoading) return + this.isOpenLeaveModal = false + } } + style=${(this.isOpenLeaveModal) ? "display: block" : "display: none"}> +
+

${translate("grouppage.gchange35")}

+
+
+ +
+ ${translate("grouppage.gchange4")} +
+
${this.leaveGroupObj.groupName}
+ + ${translate("grouppage.gchange5")} +
+
${this.leaveGroupObj.description}
+ + ${translate("grouppage.gchange10")} +
+
${this.leaveGroupObj.owner}
+ + ${translate("grouppage.gchange31")} +
+
+ + ${!this.leaveGroupObj.updated ? "" : html`${translate("grouppage.gchange32")} +
+
`} +
+ +
+ + + ${translate("grouppage.gchange36")}   + + + + + ${this.message} + +
+ + + +
+ `; + } +} + +customElements.define('chat-leave-group', ChatLeaveGroup); \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/core/components/ChatPage.js b/qortal-ui-plugins/plugins/core/components/ChatPage.js index fbb48b94..22cbd82e 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatPage.js +++ b/qortal-ui-plugins/plugins/core/components/ChatPage.js @@ -1,5 +1,6 @@ import { LitElement, html, css } from 'lit'; import { render } from 'lit/html.js'; +import {animate} from '@lit-labs/motion'; import { Epml } from '../../../epml.js'; import { use, get, translate, registerTranslateConfig } from 'lit-translate'; // import localForage from "localforage"; @@ -17,6 +18,8 @@ import './TimeAgo.js'; import './ChatTextEditor'; import './WrapperModal'; import './ChatSelect.js' +import './ChatSideNavHeads.js' +import './ChatLeaveGroup.js' import '@polymer/paper-spinner/paper-spinner-lite.js'; import '@material/mwc-button'; import '@material/mwc-dialog'; @@ -72,7 +75,12 @@ class ChatPage extends LitElement { webSocket: {attribute: false}, chatHeads: {type: Array}, forwardActiveChatHeadUrl: {type: String}, - openForwardOpen: {type: Boolean} + openForwardOpen: {type: Boolean}, + groupAdmin: {type: Array}, + groupMembers: {type: Array}, + shifted: {type: Boolean}, + groupInfo: {type: Object}, + setActiveChatHeadUrl: {attribute: false} } } @@ -81,7 +89,7 @@ class ChatPage extends LitElement { html { scroll-behavior: smooth; } - + .chat-head-container { display: flex; justify-content: flex-start; @@ -91,11 +99,7 @@ class ChatPage extends LitElement { width: 100%; } - .chat-container { - display: grid; - grid-template-rows: minmax(6%, 92vh) minmax(40px, auto); - max-height: 100%; - } + .chat-text-area { display: flex; @@ -524,6 +528,43 @@ class ChatPage extends LitElement { cursor: pointer; background-color: #03a8f475; } + .chat-container { + display: grid; + grid-template-rows: minmax(40px, auto) minmax(6%, 92vh) minmax(40px, auto); + max-height: 100%; + flex: 3; + } + .chat-right-panel { + flex: 0; + border-left: 3px solid rgb(221, 221, 221); + height: 100%; + overflow-y: auto; + background: transparent; + } + .movedin { + flex: 1 !important; + background: transparent; + } + + .chat-right-panel-label { + color: white; + padding: 5px; + font-size: 16px; + user-select: none; + } + .main-container { + display: flex; + height: 100%; + } + .top-bar-icon { + cursor: pointer; + height: 18px; + width: 18px; + transition: .2s all; + } + .top-bar-icon:hover { + color: var(--black) + } ` } @@ -570,13 +611,33 @@ class ChatPage extends LitElement { boxShadow: 'rgba(4, 4, 5, 0.15) 0px 0px 0px 1px, rgba(0, 0, 0, 0.24) 0px 8px 16px 0px' }); this.openForwardOpen = false + this.groupAdmin = [] + this.groupMembers = [] + this.shifted = false + this.groupInfo = {} } - + _toggle() { + this.shifted = !this.shifted; + } render() { + console.log('this.chatHeads', this.chatHeads) return html` +
+
+
+

Name

+
+
+ + + + + this.setActiveChatHeadUrl(val)}> +
+
${this.isLoadingMessages ? html` @@ -595,6 +656,7 @@ class ChatPage extends LitElement { this.renderChatScroller()}
+
@@ -661,6 +723,7 @@ class ChatPage extends LitElement {
+ ${(this.isUploadingImage || this.isDeletingImage) ? html`
@@ -766,6 +829,33 @@ class ChatPage extends LitElement {
+
+

Exit Icon

+ Group Avatar

Group Name

+

Group owner

+

100 Members

+

Description

+

date created

+

private / public

+

approvalThreshold

+

"minimumBlockDelay": 0, "maximumBlockDelay": 0

+

Admins

+ ${this.groupAdmin.map((item)=> { + return html` { + + }} chatInfo=${JSON.stringify(item)}>` + })} + +

Members

+ ${this.groupAdmin.map((item)=> { + return html` { + + }} chatInfo=${JSON.stringify(item)}>` + })} +
+ + +
` } @@ -908,6 +998,63 @@ class ChatPage extends LitElement { // this.initChatEditor(); }, 100) + + const isRecipient = this.chatId.includes('direct') === true ? true : false; + const groupId = this.chatId.split('/')[1]; + if(!isRecipient && groupId !== 0){ + + try { + const getMembers = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/members/${groupId}?onlyAdmins=false&limit=20`, + }); + const getMembersAdmins = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/members/${groupId}?onlyAdmins=true&limit=20`, + }); + const getGroupInfo = await parentEpml.request("apiCall", { + type: "api", + url: `/groups/${groupId}`, + }); + const getMembersAdminsWithName = (getMembersAdmins.members || []).map(async (member) => { + let memberItem = member + try { + const name = await this.getName(member.member) + memberItem = { + address: member.member, + name: name ? name : undefined + } + } catch (error) { + console.log(error) + } + + return memberItem + }) + const membersAdminsWithName = await Promise.all(getMembersAdminsWithName) + const getMembersWithName = (getMembers.members || []).map(async (member) => { + let memberItem = member + try { + const name = await this.getName(member.member) + memberItem = { + address: member.member, + name: name ? name : undefined + } + } catch (error) { + console.log(error) + } + + return memberItem + }) + const membersWithName = await Promise.all(getMembersWithName) + this.groupAdmin = membersAdminsWithName + this.groupMembers = membersWithName + this.groupInfo = getGroupInfo + console.log({membersAdminsWithName}) + } catch (error) { + console.error(error) + } + } + } @@ -961,6 +1108,24 @@ class ChatPage extends LitElement { } +async getName (recipient) { + try { + const getNames = await parentEpml.request("apiCall", { + type: "api", + url: `/names/address/${recipient}`, + }); + + if (Array.isArray(getNames) && getNames.length > 0 ) { + return getNames[0].name + } else { + return '' + } + + } catch (error) { + return "" + } +} + async renderPlaceholder() { const getName = async (recipient)=> { try { diff --git a/qortal-ui-plugins/plugins/core/components/ChatSelect.js b/qortal-ui-plugins/plugins/core/components/ChatSelect.js index be069a2b..62da5901 100644 --- a/qortal-ui-plugins/plugins/core/components/ChatSelect.js +++ b/qortal-ui-plugins/plugins/core/components/ChatSelect.js @@ -151,6 +151,8 @@ class ChatSelect extends LitElement { }) }) parentEpml.imReady() + + } shouldUpdate(changedProperties) { diff --git a/qortal-ui-plugins/plugins/core/components/ChatSideNavHeads.js b/qortal-ui-plugins/plugins/core/components/ChatSideNavHeads.js new file mode 100644 index 00000000..9a3a6d39 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/ChatSideNavHeads.js @@ -0,0 +1,180 @@ +import { LitElement, html, css } from 'lit' +import { Epml } from '../../../epml.js' + +import '@material/mwc-icon' + +const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }) + +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} + } + } + + static get styles() { + return css` + 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); + } + + .about { + margin-top: 8px; + } + + .about { + padding-left: 8px; + } + + .status { + color: #92959e; + } + + .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 = ''; + let backupAvatarImg = '' + 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` +
  • this.getUrl(this.chatInfo.url)} class="clearfix"> + ${this.isImageLoaded ? html`${avatarImg}` : html`` } + ${!this.isImageLoaded && !this.chatInfo.name && !this.chatInfo.groupName ? html`account_circle` : html`` } + ${!this.isImageLoaded && this.chatInfo.name ? html`
    ${this.chatInfo.name.charAt(0)}
    `: ''} + ${!this.isImageLoaded && this.chatInfo.groupName ? html`
    ${this.chatInfo.groupName.charAt(0)}
    `: ''} +
    +
    ${this.chatInfo.groupName ? this.chatInfo.groupName : this.chatInfo.name !== undefined ? this.chatInfo.name : this.chatInfo.address.substr(0, 15)}
    +
    +
  • + ` + } + + firstUpdated() { + let configLoaded = false + parentEpml.ready().then(() => { + parentEpml.subscribe('selected_address', async selectedAddress => { + this.selectedAddress = {} + selectedAddress = JSON.parse(selectedAddress) + if (!selectedAddress || Object.entries(selectedAddress).length === 0) return + this.selectedAddress = selectedAddress + }) + parentEpml.subscribe('config', c => { + if (!configLoaded) { + configLoaded = true + } + this.config = JSON.parse(c) + }) + }) + parentEpml.imReady() + + + } + + shouldUpdate(changedProperties) { + if(changedProperties.has('activeChatHeadUrl')){ + return true + } + if(changedProperties.has('chatInfo')){ + return true + } + + return false + } + + getUrl(chatUrl) { + this.setActiveChatHeadUrl(chatUrl) + } + + onPageNavigation(pageUrl) { + parentEpml.request('setPageUrl', pageUrl) + } +} + +window.customElements.define('chat-side-nav-heads', ChatSideNavHeads) diff --git a/qortal-ui-plugins/plugins/core/messaging/q-chat/q-chat.src.js b/qortal-ui-plugins/plugins/core/messaging/q-chat/q-chat.src.js index 9858766d..5f5cbfb4 100644 --- a/qortal-ui-plugins/plugins/core/messaging/q-chat/q-chat.src.js +++ b/qortal-ui-plugins/plugins/core/messaging/q-chat/q-chat.src.js @@ -702,7 +702,7 @@ class Chat extends LitElement { // Else render Welcome to Q-CHat // TODO: DONE: Do the above in the ChatPage - return html`` + return html` this.setActiveChatHeadUrl(val)}>` } setChatHeads(chatObj) {