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

image functionality rough draft

This commit is contained in:
Phillip Lang Martinez 2022-10-31 18:56:45 +02:00
parent b73b9a0ab2
commit af71ec3508
6 changed files with 479 additions and 27 deletions

View File

@ -509,7 +509,8 @@
"bcchange9": "Private Message", "bcchange9": "Private Message",
"bcchange10": "More", "bcchange10": "More",
"bcchange11": "Reply", "bcchange11": "Reply",
"bcchange12": "Edit" "bcchange12": "Edit",
"bcchange13": "Reaction"
}, },
"grouppage": { "grouppage": {
"gchange1": "Qortal Groups", "gchange1": "Qortal Groups",

View File

@ -19,8 +19,10 @@
"dependencies": { "dependencies": {
"@material/mwc-list": "0.27.0", "@material/mwc-list": "0.27.0",
"@material/mwc-select": "0.27.0", "@material/mwc-select": "0.27.0",
"compressorjs": "^1.1.1",
"emoji-picker-js": "https://github.com/Qortal/emoji-picker-js", "emoji-picker-js": "https://github.com/Qortal/emoji-picker-js",
"localforage": "^1.10.0" "localforage": "^1.10.0",
"short-unique-id": "^4.4.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.19.3", "@babel/core": "7.19.3",

View File

@ -6,6 +6,8 @@ import localForage from "localforage";
registerTranslateConfig({ registerTranslateConfig({
loader: lang => fetch(`/language/${lang}.json`).then(res => res.json()) loader: lang => fetch(`/language/${lang}.json`).then(res => res.json())
}) })
import ShortUniqueId from 'short-unique-id';
import Compressor from 'compressorjs';
import { escape, unescape } from 'html-escaper'; import { escape, unescape } from 'html-escaper';
import { inputKeyCodes } from '../../utils/keyCodes.js' import { inputKeyCodes } from '../../utils/keyCodes.js'
@ -20,6 +22,7 @@ import '@material/mwc-button'
import '@material/mwc-dialog' import '@material/mwc-dialog'
import '@material/mwc-icon' import '@material/mwc-icon'
import { replaceMessagesEdited } from '../../utils/replace-messages-edited.js'; import { replaceMessagesEdited } from '../../utils/replace-messages-edited.js';
import { publishData } from '../../utils/publish-image.js';
const messagesCache = localForage.createInstance({ const messagesCache = localForage.createInstance({
name: "messages-cache", name: "messages-cache",
@ -53,7 +56,8 @@ class ChatPage extends LitElement {
messagesRendered: { type: Array }, messagesRendered: { type: Array },
repliedToMessageObj: { type: Object }, repliedToMessageObj: { type: Object },
editedMessageObj: { type: Object }, editedMessageObj: { type: Object },
chatMessageSize: { type: String} chatMessageSize: { type: Number},
imageFile: {type: Object}
} }
} }
@ -189,6 +193,8 @@ class ChatPage extends LitElement {
super() super()
this.getOldMessage = this.getOldMessage.bind(this) this.getOldMessage = this.getOldMessage.bind(this)
this._sendMessage = this._sendMessage.bind(this) this._sendMessage = this._sendMessage.bind(this)
this.insertImage = this.insertImage.bind(this)
this.getMessageSize = this.getMessageSize.bind(this)
this._downObserverhandler = this._downObserverhandler.bind(this) this._downObserverhandler = this._downObserverhandler.bind(this)
this.selectedAddress = {} this.selectedAddress = {}
this.chatId = '' this.chatId = ''
@ -210,11 +216,54 @@ class ChatPage extends LitElement {
this.messagesRendered = [] this.messagesRendered = []
this.repliedToMessageObj = null this.repliedToMessageObj = null
this.editedMessageObj = null this.editedMessageObj = null
this.chatMessageSize = 5
this.imageFile = null
this.uid = new ShortUniqueId();
} }
render() { render() {
return html` return html`
${this.isLoadingMessages ? html`<h1>${translate("chatpage.cchange22")}</h1>` : this.renderChatScroller(this._initialMessages)} ${this.isLoadingMessages ? html`<h1>${translate("chatpage.cchange22")}</h1>` : this.renderChatScroller(this._initialMessages)}
<mwc-dialog id="showDialogPublicKey" ?open=${this.imageFile}>
<div class="dialog-header" >
</div>
<div class="dialog-container">
hello
${this.imageFile && html`
<img src=${URL.createObjectURL(this.imageFile)} />
`}
</div>
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
@click=${()=>{
this._sendMessage({
type: 'image',
imageFile: this.imageFile,
caption: 'This is a caption'
})
}}
>
send
</mwc-button>
<mwc-button
slot="primaryAction"
dialogAction="cancel"
class="red"
@click=${()=>{
this.imageFile = null
}}
>
${translate("general.close")}
</mwc-button>
</mwc-dialog>
<div class="chat-text-area"> <div class="chat-text-area">
<div class="typing-area"> <div class="typing-area">
${this.repliedToMessageObj && html` ${this.repliedToMessageObj && html`
@ -274,8 +323,18 @@ class ChatPage extends LitElement {
` `
} }
async firstUpdated() {
insertImage(file){
this.imageFile = file
}
async firstUpdated() {
// TODO: Load and fetch messages from localstorage (maybe save messages to localstorage...) // TODO: Load and fetch messages from localstorage (maybe save messages to localstorage...)
@ -627,11 +686,10 @@ class ChatPage extends LitElement {
} }
const stringified = JSON.stringify(messageObject) const stringified = JSON.stringify(messageObject)
const size = new Blob([stringified]).size; const size = new Blob([stringified]).size;
this.chatMessageSize = size this.chatMessageSize = size
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -991,7 +1049,7 @@ class ChatPage extends LitElement {
// Add to the messages... TODO: Save messages to localstorage and fetch from it to make it persistent... // 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 params to determine if it's a reply or not
// have variable to determine if it's a response, holds signature in constructor // have variable to determine if it's a response, holds signature in constructor
// need original message signature // need original message signature
@ -1006,8 +1064,161 @@ class ChatPage extends LitElement {
// Format and Sanitize Message // Format and Sanitize Message
const sanitizedMessage = messageText.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n'); const sanitizedMessage = messageText.replace(/&nbsp;/gi, ' ').replace(/<br\s*[\/]?>/gi, '\n');
const trimmedMessage = sanitizedMessage.trim(); 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' typeMessage = 'edit'
let chatReference = outSideMsg.editedMessageObj.reference let chatReference = outSideMsg.editedMessageObj.reference
@ -1261,10 +1472,12 @@ class ChatPage extends LitElement {
return arr.length === 0 return arr.length === 0
} }
initChatEditor() { initChatEditor() {
const ChatEditor = function (editorConfig) { const ChatEditor = function (editorConfig) {
const ChatEditor = function () { const ChatEditor = function () {
const editor = this; const editor = this;
editor.init(); editor.init();
@ -1411,11 +1624,14 @@ class ChatPage extends LitElement {
editor.mirror.value = unescapedValue; editor.mirror.value = unescapedValue;
}; };
ChatEditor.prototype.listenChanges = function () { ChatEditor.prototype.listenChanges = function () {
const editor = this; const editor = this;
['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste'].map(function (event) { const events = ['drop', 'contextmenu', 'mouseup', 'click', 'touchend', 'keydown', 'blur', 'paste']
editor.content.body.addEventListener(event, function (e) {
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) editorConfig.getMessageSize(editorConfig.mirrorElement.value)
if (e.type === 'click') { if (e.type === 'click') {
@ -1425,10 +1641,29 @@ class ChatPage extends LitElement {
} }
if (e.type === 'paste') { if (e.type === 'paste') {
e.preventDefault(); 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 => { navigator.clipboard.readText().then(clipboardText => {
let escapedText = editorConfig.escape(clipboardText); let escapedText = editorConfig.escape(clipboardText);
editor.insertText(escapedText); editor.insertText(escapedText);
@ -1496,7 +1731,7 @@ class ChatPage extends LitElement {
editor.updateMirror(); editor.updateMirror();
}); });
}); }
editor.content.addEventListener('click', function (event) { editor.content.addEventListener('click', function (event) {
@ -1535,7 +1770,10 @@ class ChatPage extends LitElement {
emojiPicker: this.emojiPicker, emojiPicker: this.emojiPicker,
escape: escape, escape: escape,
unescape: unescape, unescape: unescape,
placeholder: this.chatEditorPlaceholder placeholder: this.chatEditorPlaceholder,
imageFile: this.imageFile,
requestUpdate: this.requestUpdate,
insertImage: this.insertImage
}; };
this.chatEditor = new ChatEditor(editorConfig); this.chatEditor = new ChatEditor(editorConfig);
} }

View File

@ -329,4 +329,22 @@ export const chatStyles = css`
margin-right: 10px; margin-right: 10px;
cursor: pointer 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;
}
` `

View File

@ -210,19 +210,26 @@ class MessageTemplate extends LitElement {
let message = "" let message = ""
let reactions = [] let reactions = []
let repliedToData = null let repliedToData = null
let image = null
let isImageDeleted = false
try { try {
const parsedMessageObj = JSON.parse(this.messageObj.decodedMessage) const parsedMessageObj = JSON.parse(this.messageObj.decodedMessage)
message = parsedMessageObj.messageText message = parsedMessageObj.messageText
repliedToData = this.messageObj.repliedToData repliedToData = this.messageObj.repliedToData
isImageDeleted = parsedMessageObj.isImageDeleted
reactions = parsedMessageObj.reactions || [] reactions = parsedMessageObj.reactions || []
if(parsedMessageObj.images && Array.isArray(parsedMessageObj.images) && parsedMessageObj.images.length > 0){
image = parsedMessageObj.images[0]
}
} catch (error) { } catch (error) {
message = this.messageObj.decodedMessage message = this.messageObj.decodedMessage
} }
let avatarImg = '' let avatarImg = ''
let imageHTML = ''
let nameMenu = '' let nameMenu = ''
let levelFounder = '' let levelFounder = ''
let hideit = hidemsg.includes(this.messageObj.sender) let hideit = hidemsg.includes(this.messageObj.sender)
levelFounder = html`<level-founder checkleveladdress="${this.messageObj.sender}"></level-founder>` levelFounder = html`<level-founder checkleveladdress="${this.messageObj.sender}"></level-founder>`
@ -234,6 +241,13 @@ class MessageTemplate extends LitElement {
} else { } else {
avatarImg = html`<img src='/img/incognito.png' style="max-width:100%; max-height:100%;" onerror="this.onerror=null;" />` avatarImg = html`<img src='/img/incognito.png' style="max-width:100%; max-height:100%;" onerror="this.onerror=null;" />`
} }
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`<img src="${imageUrl}" style="max-width:45vh; max-height:40vh; border-radius: 5px" onerror="this.onerror=null; this.src='/img/incognito.png';" />`
}
if (this.messageObj.sender === this.myAddress) { if (this.messageObj.sender === this.myAddress) {
@ -268,9 +282,21 @@ class MessageTemplate extends LitElement {
<p class="replied-message">${repliedToData.decodedMessage.messageText}</p> <p class="replied-message">${repliedToData.decodedMessage.messageText}</p>
</div> </div>
`} `}
${image && !isImageDeleted ? html`
<div class="image-container">
${imageHTML}<vaadin-icon
@click=${() => this.sendMessage({
type: 'delete',
name: image.name,
identifier: image.identifier,
editedMessageObj: this.messageObj,
})}
class="image-delete-icon" icon="vaadin:close" slot="icon"></vaadin-icon>
</div>
` : html``}
<div id="messageContent" class="message"> <div id="messageContent" class="message">
${unsafeHTML(this.emojiPicker.parse(this.escapeHTML(message)))} ${unsafeHTML(this.emojiPicker.parse(this.escapeHTML(message)))}
</div> </div>
<div> <div>
${reactions.map((reaction)=> { ${reactions.map((reaction)=> {
@ -387,6 +413,15 @@ class ChatMenu extends LitElement {
render() { render() {
return html` return html`
<div class="container"> <div class="container">
<div
class="menu-icon tooltip reaction"
data-text="${translate("blockpage.bcchange13")}"
@click=${(e) => {
this.emojiPicker.togglePicker(e.target)
}}
>
<vaadin-icon icon="vaadin:smiley-o" slot="icon"></vaadin-icon>
</div>
<div class="menu-icon tooltip" data-text="${translate("blockpage.bcchange9")}" @click="${() => this.showPrivateMessageModal()}"> <div class="menu-icon tooltip" data-text="${translate("blockpage.bcchange9")}" @click="${() => this.showPrivateMessageModal()}">
<vaadin-icon icon="vaadin:paperplane" slot="icon"></vaadin-icon> <vaadin-icon icon="vaadin:paperplane" slot="icon"></vaadin-icon>
</div> </div>
@ -402,15 +437,7 @@ class ChatMenu extends LitElement {
}}"> }}">
<vaadin-icon icon="vaadin:reply" slot="icon"></vaadin-icon> <vaadin-icon icon="vaadin:reply" slot="icon"></vaadin-icon>
</div> </div>
<div
class="menu-icon tooltip reaction"
data-text="${translate("blockpage.bcchange13")}"
@click=${(e) => {
this.emojiPicker.togglePicker(e.target)
}}
>
<vaadin-icon icon="vaadin:smiley-o" slot="icon"></vaadin-icon>
</div>
${this.myAddress === this.originalMessage.sender ? ( ${this.myAddress === this.originalMessage.sender ? (
html` html`
<div <div

View File

@ -0,0 +1,166 @@
const getApiKey = () => {
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()
}