@@ -289,7 +338,18 @@ class ChatPage extends LitElement {
`
}
+
+
+ insertImage(file){
+ this.imageFile = file
+
+
+ }
+
+
+
async firstUpdated() {
+
// TODO: Load and fetch messages from localstorage (maybe save messages to localstorage...)
// this.changeLanguage();
this.emojiPickerHandler = this.shadowRoot.querySelector('.emoji-button');
@@ -638,11 +698,10 @@ class ChatPage extends LitElement {
}
-
const stringified = JSON.stringify(messageObject)
const size = new Blob([stringified]).size;
this.chatMessageSize = size
-
+
} catch (error) {
console.error(error)
@@ -1001,7 +1060,7 @@ class ChatPage extends LitElement {
// Add to the messages... TODO: Save messages to localstorage and fetch from it to make it persistent...
}
- _sendMessage(outSideMsg) {
+ async _sendMessage(outSideMsg) {
// have params to determine if it's a reply or not
// have variable to determine if it's a response, holds signature in constructor
// need original message signature
@@ -1016,8 +1075,161 @@ class ChatPage extends LitElement {
// Format and Sanitize Message
const sanitizedMessage = messageText.replace(/ /gi, ' ').replace(/
/gi, '\n');
const trimmedMessage = sanitizedMessage.trim();
+
+ const getName = async (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 ""
+ }
+ }
- if(outSideMsg && outSideMsg.type === 'reaction'){
+ if(outSideMsg && outSideMsg.type === 'delete'){
+ const userName = outSideMsg.name
+ const identifier = outSideMsg.identifier
+ let compressedFile = ''
+ var str =
+ "iVBORw0KGgoAAAANSUhEUgAAAsAAAAGMAQMAAADuk4YmAAAAA1BMVEX///+nxBvIAAAAAXRSTlMAQObYZgAAADlJREFUeF7twDEBAAAAwiD7p7bGDlgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAGJrAABgPqdWQAAAABJRU5ErkJggg==";
+
+ const b64toBlob = (b64Data, contentType='', sliceSize=512) => {
+ const byteCharacters = atob(b64Data);
+ const byteArrays = [];
+
+ for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
+ const slice = byteCharacters.slice(offset, offset + sliceSize);
+
+ const byteNumbers = new Array(slice.length);
+ for (let i = 0; i < slice.length; i++) {
+ byteNumbers[i] = slice.charCodeAt(i);
+ }
+
+ const byteArray = new Uint8Array(byteNumbers);
+ byteArrays.push(byteArray);
+ }
+
+ const blob = new Blob(byteArrays, {type: contentType});
+ return blob;
+ }
+ const blob = b64toBlob(str, 'image/png');
+
+ await new Promise(resolve =>{
+ new Compressor( blob, {
+ quality: 0.6,
+ maxWidth: 500,
+ success(result){
+ console.log({result})
+ const file = new File([result], "name", {
+ type: 'image/png'
+ });
+ console.log({file})
+ compressedFile = file
+ resolve()
+ },
+ error(err) {
+ console.log(err.message);
+ },
+ })
+ })
+ try {
+ console.log({userName, compressedFile, identifier, selectedAddress: this.selectedAddress})
+ await publishData({
+ registeredName: userName ,
+ file : compressedFile ,
+ service: 'IMAGE',
+ identifier : identifier,
+ parentEpml,
+ metaData: undefined,
+ uploadType: 'file',
+ selectedAddress: this.selectedAddress
+ })
+ } catch (error) {
+ console.error(error)
+ }
+
+
+
+ typeMessage = 'edit'
+ let chatReference = outSideMsg.editedMessageObj.reference
+
+ if(outSideMsg.editedMessageObj.chatReference){
+ chatReference = outSideMsg.editedMessageObj.chatReference
+ }
+
+ let message = ""
+ try {
+ const parsedMessageObj = JSON.parse(outSideMsg.editedMessageObj.decodedMessage)
+ message = parsedMessageObj
+
+ } catch (error) {
+ message = outSideMsg.editedMessageObj.decodedMessage
+ }
+ const messageObject = {
+ ...message,
+ isImageDeleted: true
+ }
+ const stringifyMessageObject = JSON.stringify(messageObject)
+ this.sendMessage(stringifyMessageObject, typeMessage, chatReference);
+
+
+ }
+ else if(outSideMsg && outSideMsg.type === 'image'){
+ const userName = await getName(this.selectedAddress.address)
+ const id = this.uid();
+ const identifier = `qchat_${id}`
+ let compressedFile = ''
+ await new Promise(resolve =>{
+ new Compressor( outSideMsg.imageFile, {
+ quality: 0.6,
+ maxWidth: 500,
+ success(result){
+ const file = new File([result], "name", {
+ type: outSideMsg.imageFile.type
+ });
+ compressedFile = file
+ resolve()
+ },
+ error(err) {
+ console.log(err.message);
+ },
+ })
+ })
+
+ await publishData({
+ registeredName: userName ,
+ file : compressedFile ,
+ service: 'IMAGE',
+ identifier : identifier,
+ parentEpml,
+ metaData: undefined,
+ uploadType: 'file',
+ selectedAddress: this.selectedAddress
+ })
+ const messageObject = {
+ messageText: outSideMsg.caption,
+ images: [{
+ service: "IMAGE",
+ name: userName,
+ identifier: identifier
+ }],
+ isImageDeleted: false,
+ repliedTo: '',
+ version: 1
+ }
+ const stringifyMessageObject = JSON.stringify(messageObject)
+ this.sendMessage(stringifyMessageObject, typeMessage);
+
+
+
+ } else if(outSideMsg && outSideMsg.type === 'reaction'){
typeMessage = 'edit'
let chatReference = outSideMsg.editedMessageObj.reference
@@ -1271,10 +1483,12 @@ class ChatPage extends LitElement {
return arr.length === 0
}
+
+
initChatEditor() {
const ChatEditor = function (editorConfig) {
-
+
const ChatEditor = function () {
const editor = this;
editor.init();
@@ -1421,11 +1635,14 @@ class ChatPage extends LitElement {
editor.mirror.value = unescapedValue;
};
- ChatEditor.prototype.listenChanges = function () {
+ ChatEditor.prototype.listenChanges = function () {
const editor = this;
- ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste'].map(function (event) {
- editor.content.body.addEventListener(event, function (e) {
+ const events = ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste']
+
+ for (let i = 0; i < events.length; i++) {
+ const event = events[i]
+ editor.content.body.addEventListener(event, async function (e) {
editorConfig.getMessageSize(editorConfig.mirrorElement.value)
if (e.type === 'click') {
@@ -1435,10 +1652,29 @@ class ChatPage extends LitElement {
}
if (e.type === 'paste') {
+
e.preventDefault();
-
+ const item_list = await navigator.clipboard.read();
+ let image_type; // we will feed this later
+ const item = item_list.find( item => // choose the one item holding our image
+ item.types.some( type => { // does this item have our type
+ if( type.startsWith( 'image/' ) ) {
+ image_type = type; // store which kind of image type it is
+ return true;
+ }
+ } )
+ );
+ const blob = item && await item.getType( image_type );
+ var file = new File([blob], "name", {
+ type: image_type
+ });
+
+ editorConfig.insertImage(file)
+
+
+
navigator.clipboard.readText().then(clipboardText => {
-
+
let escapedText = editorConfig.escape(clipboardText);
editor.insertText(escapedText);
@@ -1508,7 +1744,7 @@ class ChatPage extends LitElement {
editor.updateMirror();
});
- });
+ }
editor.content.addEventListener('click', function (event) {
@@ -1548,7 +1784,10 @@ class ChatPage extends LitElement {
emojiPicker: this.emojiPicker,
escape: escape,
unescape: unescape,
- placeholder: this.chatEditorPlaceholder
+ placeholder: this.chatEditorPlaceholder,
+ imageFile: this.imageFile,
+ requestUpdate: this.requestUpdate,
+ insertImage: this.insertImage
};
this.chatEditor = new ChatEditor(editorConfig);
}
diff --git a/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js b/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js
index 65ea615a..4609de6c 100644
--- a/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js
+++ b/qortal-ui-plugins/plugins/core/components/ChatScroller-css.js
@@ -329,4 +329,22 @@ export const chatStyles = css`
margin-right: 10px;
cursor: pointer
}
+
+ .image-container {
+ display: flex;
+ }
+ .image-delete-icon {
+ margin-left: 5px;
+ height: 20px;
+ cursor: pointer;
+ visibility: hidden;
+ transition: .2s all;
+ opacity: .8
+ }
+ .image-delete-icon:hover {
+ opacity: 1
+ }
+ .message-parent:hover .image-delete-icon {
+ visibility: visible;
+ }
`
diff --git a/qortal-ui-plugins/plugins/core/components/ChatScroller.js b/qortal-ui-plugins/plugins/core/components/ChatScroller.js
index 5fe748a8..be2c9acb 100644
--- a/qortal-ui-plugins/plugins/core/components/ChatScroller.js
+++ b/qortal-ui-plugins/plugins/core/components/ChatScroller.js
@@ -210,19 +210,26 @@ class MessageTemplate extends LitElement {
let message = ""
let reactions = []
let repliedToData = null
+ let image = null
+ let isImageDeleted = false
try {
const parsedMessageObj = JSON.parse(this.messageObj.decodedMessage)
message = parsedMessageObj.messageText
repliedToData = this.messageObj.repliedToData
+ isImageDeleted = parsedMessageObj.isImageDeleted
reactions = parsedMessageObj.reactions || []
-
+ if(parsedMessageObj.images && Array.isArray(parsedMessageObj.images) && parsedMessageObj.images.length > 0){
+ image = parsedMessageObj.images[0]
+ }
} catch (error) {
message = this.messageObj.decodedMessage
}
let avatarImg = ''
+ let imageHTML = ''
let nameMenu = ''
let levelFounder = ''
let hideit = hidemsg.includes(this.messageObj.sender)
+
levelFounder = html`
`
@@ -234,6 +241,13 @@ class MessageTemplate extends LitElement {
} else {
avatarImg = html`
![](/img/incognito.png)
`
}
+
+ if(image){
+ 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 imageUrl = `${nodeUrl}/arbitrary/${image.service}/${image.name}/${image.identifier}?async=true&apiKey=${myNode.apiKey}`
+ imageHTML = html`
![](${imageUrl})
`
+ }
if (this.messageObj.sender === this.myAddress) {
@@ -268,9 +282,21 @@ class MessageTemplate extends LitElement {
${repliedToData.decodedMessage.messageText}
`}
+ ${image && !isImageDeleted ? html`
+
+ ${imageHTML} this.sendMessage({
+ type: 'delete',
+ name: image.name,
+ identifier: image.identifier,
+ editedMessageObj: this.messageObj,
+
+ })}
+ class="image-delete-icon" icon="vaadin:close" slot="icon">
+
+ ` : html``}
${unsafeHTML(this.emojiPicker.parse(this.escapeHTML(message)))}
-
${reactions.map((reaction)=> {
@@ -387,6 +413,15 @@ class ChatMenu extends LitElement {
render() {
return html`
+
@@ -402,15 +437,7 @@ class ChatMenu extends LitElement {
}}">
-
+
${this.myAddress === this.originalMessage.sender ? (
html`
{
+ const myNode =
+ window.parent.reduxStore.getState().app.nodeConfig.knownNodes[
+ window.parent.reduxStore.getState().app.nodeConfig.node
+ ]
+ let apiKey = myNode.apiKey
+ return apiKey
+}
+
+export const publishData = async ({
+ registeredName,
+ path,
+ file,
+ service,
+ identifier,
+ parentEpml,
+ metaData,
+ uploadType,
+ selectedAddress,
+}) => {
+ const validateName = async (receiverName) => {
+ let nameRes = await parentEpml.request("apiCall", {
+ type: "api",
+ url: `/names/${receiverName}`,
+ })
+
+ return nameRes
+ }
+
+ const convertBytesForSigning = async (transactionBytesBase58) => {
+ let convertedBytes = await parentEpml.request("apiCall", {
+ type: "api",
+ method: "POST",
+ url: `/transactions/convert`,
+ body: `${transactionBytesBase58}`,
+ })
+ return convertedBytes
+ }
+
+ const signAndProcess = async (transactionBytesBase58) => {
+ let convertedBytesBase58 = await convertBytesForSigning(
+ transactionBytesBase58
+ )
+ if (convertedBytesBase58.error) {
+ return
+ }
+
+ const convertedBytes =
+ window.parent.Base58.decode(convertedBytesBase58)
+ const _convertedBytesArray = Object.keys(convertedBytes).map(
+ function (key) {
+ return convertedBytes[key]
+ }
+ )
+ const convertedBytesArray = new Uint8Array(_convertedBytesArray)
+ const convertedBytesHash = new window.parent.Sha256()
+ .process(convertedBytesArray)
+ .finish().result
+ const hashPtr = window.parent.sbrk(32, window.parent.heap)
+ const hashAry = new Uint8Array(
+ window.parent.memory.buffer,
+ hashPtr,
+ 32
+ )
+
+ hashAry.set(convertedBytesHash)
+ const difficulty = 14
+ const workBufferLength = 8 * 1024 * 1024
+ const workBufferPtr = window.parent.sbrk(
+ workBufferLength,
+ window.parent.heap
+ )
+ let nonce = window.parent.computePow(
+ hashPtr,
+ workBufferPtr,
+ workBufferLength,
+ difficulty
+ )
+ let response = await parentEpml.request("sign_arbitrary", {
+ nonce: selectedAddress.nonce,
+ arbitraryBytesBase58: transactionBytesBase58,
+ arbitraryBytesForSigningBase58: convertedBytesBase58,
+ arbitraryNonce: nonce,
+ })
+ let myResponse = { error: "" }
+ if (response === false) {
+ return
+ } else {
+ myResponse = response
+ }
+
+ return myResponse
+ }
+
+ const validate = async () => {
+ let validNameRes = await validateName(registeredName)
+ if (validNameRes.error) {
+ return
+ }
+ let transactionBytes = await uploadData(registeredName, path, file)
+ if (transactionBytes.error) {
+ return
+ } else if (
+ transactionBytes.includes("Error 500 Internal Server Error")
+ ) {
+ return
+ }
+
+ let signAndProcessRes = await signAndProcess(transactionBytes)
+ if (signAndProcessRes.error) {
+ return
+ }
+ }
+
+ const uploadData = async (registeredName, path, file) => {
+ if (identifier != null && identifier.trim().length > 0) {
+ let postBody = path
+ let urlSuffix = ""
+ if (file != null) {
+ // If we're sending zipped data, make sure to use the /zip version of the POST /arbitrary/* API
+ if (uploadType === "zip") {
+ urlSuffix = "/zip"
+ }
+ // If we're sending file data, use the /base64 version of the POST /arbitrary/* API
+ else if (uploadType === "file") {
+ urlSuffix = "/base64"
+ }
+
+ // Base64 encode the file to work around compatibility issues between javascript and java byte arrays
+ let fileBuffer = new Uint8Array(await file.arrayBuffer())
+ postBody = Buffer.from(fileBuffer).toString("base64")
+ }
+
+ // Optional metadata
+
+ // let title = encodeURIComponent(metaData.title || "")
+ // let description = encodeURIComponent(metaData.description || "")
+ // let category = encodeURIComponent(metaData.category || "")
+ // let tag1 = encodeURIComponent(metaData.tag1 || "")
+ // let tag2 = encodeURIComponent(metaData.tag2 || "")
+ // let tag3 = encodeURIComponent(metaData.tag3 || "")
+ // let tag4 = encodeURIComponent(metaData.tag4 || "")
+ // let tag5 = encodeURIComponent(metaData.tag5 || "")
+
+ // let metadataQueryString = `title=${title}&description=${description}&category=${category}&tags=${tag1}&tags=${tag2}&tags=${tag3}&tags=${tag4}&tags=${tag5}`
+
+ let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}?apiKey=${getApiKey()}`
+ if (identifier != null && identifier.trim().length > 0) {
+ uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}?apiKey=${getApiKey()}`
+ }
+ let uploadDataRes = await parentEpml.request("apiCall", {
+ type: "api",
+ method: "POST",
+ url: `${uploadDataUrl}`,
+ body: `${postBody}`,
+ })
+ return uploadDataRes
+ }
+
+
+
+
+
+ }
+ await validate()
+}