Browse Source

Merge pull request #156 from Philreact/feature/chat-switch-base64-linking

switch receiving chat message to base64 and added qortal:// protocol …
qortal-ui-dev
AlphaX-Projects 1 year ago committed by GitHub
parent
commit
88a78af313
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      qortal-ui-crypto/api.js
  2. 24
      qortal-ui-crypto/api/deps/Base64.js
  3. 31
      qortal-ui-crypto/api/transactions/chat/decryptChatMessage.js
  4. 56
      qortal-ui-plugins/plugins/core/components/ChatPage.js
  5. 174
      qortal-ui-plugins/plugins/core/components/ChatScroller.js
  6. 9
      qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js
  7. 8
      qortal-ui-plugins/plugins/utils/replace-messages-edited.js

5
qortal-ui-crypto/api.js

@ -1,16 +1,19 @@
import { Sha256 } from 'asmcrypto.js'
import Base58 from './api/deps/Base58'
import Base64 from './api/deps/Base64'
import { base58PublicKeyToAddress } from './api/wallet/base58PublicKeyToAddress'
import { validateAddress } from './api/wallet/validateAddress'
import { decryptChatMessage } from './api/transactions/chat/decryptChatMessage'
import { decryptChatMessage, decryptChatMessageBase64 } from './api/transactions/chat/decryptChatMessage'
import _ from 'lodash'
window.Sha256 = Sha256
window.Base58 = Base58
window.Base64 = Base64
window._ = _
window.base58PublicKeyToAddress = base58PublicKeyToAddress
window.validateAddress = validateAddress
window.decryptChatMessage = decryptChatMessage
window.decryptChatMessageBase64 = decryptChatMessageBase64
export { initApi, store } from './api_deps.js'
export * from './api/deps/deps.js'

24
qortal-ui-crypto/api/deps/Base64.js vendored

@ -0,0 +1,24 @@
const Base64 = {};
Base64.decode = function (string) {
const binaryString = atob(string);
const binaryLength = binaryString.length;
const bytes = new Uint8Array(binaryLength);
for (let i = 0; i < binaryLength; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const decoder = new TextDecoder();
const decodedString = decoder.decode(bytes);
return decodedString;
};
export default Base64;

31
qortal-ui-crypto/api/transactions/chat/decryptChatMessage.js

@ -24,3 +24,34 @@ export const decryptChatMessage = (encryptedMessage, privateKey, recipientPublic
_decryptedMessage === false ? decryptedMessage : decryptedMessage = new TextDecoder('utf-8').decode(_decryptedMessage)
return decryptedMessage
}
export const decryptChatMessageBase64 = (encryptedMessage, privateKey, recipientPublicKey, lastReference) => {
let _encryptedMessage = atob(encryptedMessage);
const binaryLength = _encryptedMessage.length;
const bytes = new Uint8Array(binaryLength);
for (let i = 0; i < binaryLength; i++) {
bytes[i] = _encryptedMessage.charCodeAt(i);
}
const _base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? Base58.encode(recipientPublicKey) : recipientPublicKey
const _recipientPublicKey = Base58.decode(_base58RecipientPublicKey)
const _lastReference = lastReference instanceof Uint8Array ? lastReference : Base58.decode(lastReference)
const convertedPrivateKey = ed2curve.convertSecretKey(privateKey)
const convertedPublicKey = ed2curve.convertPublicKey(_recipientPublicKey)
const sharedSecret = new Uint8Array(32);
nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey)
const _chatEncryptionSeed = new Sha256().process(sharedSecret).finish().result
const _decryptedMessage = nacl.secretbox.open(bytes, _lastReference.slice(0, 24), _chatEncryptionSeed)
if (_decryptedMessage === false) {
return _decryptedMessage
}
return new TextDecoder('utf-8').decode(_decryptedMessage)
}

56
qortal-ui-plugins/plugins/core/components/ChatPage.js

@ -2514,7 +2514,7 @@ class ChatPage extends LitElement {
if (this.isReceipient) {
const getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=${limit}&reverse=true&before=${before}&after=${after}&haschatreference=false`,
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=${limit}&reverse=true&before=${before}&after=${after}&haschatreference=false&encoding=BASE64`,
});
const decodeMsgs = getInitialMessages.map((eachMessage) => {
@ -2548,7 +2548,7 @@ class ChatPage extends LitElement {
} else {
const getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=${limit}&reverse=true&before=${before}&after=${after}&haschatreference=false`,
url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=${limit}&reverse=true&before=${before}&after=${after}&haschatreference=false&encoding=BASE64`,
});
@ -2589,7 +2589,7 @@ class ChatPage extends LitElement {
if (this.isReceipient) {
const getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false`,
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false&encoding=BASE64`,
});
const decodeMsgs = getInitialMessages.map((eachMessage) => {
@ -2622,7 +2622,7 @@ class ChatPage extends LitElement {
} else {
const getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=20&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false`,
url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=20&reverse=true&before=${scrollElement.messageObj.timestamp}&haschatreference=false&encoding=BASE64`,
});
@ -2660,7 +2660,7 @@ class ChatPage extends LitElement {
if (this.isReceipient) {
const getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=true&afer=${scrollElement.messageObj.timestamp}&haschatreference=false`,
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${this._chatId}&limit=20&reverse=true&afer=${scrollElement.messageObj.timestamp}&haschatreference=false&encoding=BASE64`,
});
const decodeMsgs = getInitialMessages.map((eachMessage) => {
@ -2693,7 +2693,7 @@ class ChatPage extends LitElement {
} else {
const getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=20&reverse=true&after=${scrollElement.messageObj.timestamp}&haschatreference=false`,
url: `/chat/messages?txGroupId=${Number(this._chatId)}&limit=20&reverse=true&after=${scrollElement.messageObj.timestamp}&haschatreference=false&encoding=BASE64`,
});
@ -2739,8 +2739,7 @@ class ChatPage extends LitElement {
let _eachMessage = this.decodeMessage(eachMessage)
return _eachMessage
}
})
})
if (isInitial) {
this.chatEditorPlaceholder = await this.renderPlaceholder();
const replacedMessages = await replaceMessagesEdited({
@ -2759,6 +2758,7 @@ class ChatPage extends LitElement {
// TODO: Determine number of initial messages by screen height...
this.messagesRendered = this._messages;
this.isLoadingMessages = false;
setTimeout(() => this.downElementObserver(), 500);
} else {
const replacedMessages = await replaceMessagesEdited({
@ -2885,11 +2885,10 @@ class ChatPage extends LitElement {
if (isReceipientVar === true) {
// direct chat
if (encodedMessageObj.isEncrypted === true && _publicKeyVar.hasPubKey === true && encodedMessageObj.data) {
let decodedMessage = window.parent.decryptChatMessage(encodedMessageObj.data, window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey, _publicKeyVar.key, encodedMessageObj.reference);
let decodedMessage = window.parent.decryptChatMessageBase64(encodedMessageObj.data, window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey, _publicKeyVar.key, encodedMessageObj.reference);
decodedMessageObj = { ...encodedMessageObj, decodedMessage };
} else if (encodedMessageObj.isEncrypted === false && encodedMessageObj.data) {
let bytesArray = window.parent.Base58.decode(encodedMessageObj.data);
let decodedMessage = new TextDecoder('utf-8').decode(bytesArray);
let decodedMessage = window.parent.Base64.decode(encodedMessageObj.data);
decodedMessageObj = { ...encodedMessageObj, decodedMessage };
} else {
decodedMessageObj = { ...encodedMessageObj, decodedMessage: "Cannot Decrypt Message!" };
@ -2897,8 +2896,7 @@ class ChatPage extends LitElement {
} else {
// group chat
let bytesArray = window.parent.Base58.decode(encodedMessageObj.data);
let decodedMessage = new TextDecoder('utf-8').decode(bytesArray);
let decodedMessage = window.parent.Base64.decode(encodedMessageObj.data);
decodedMessageObj = { ...encodedMessageObj, decodedMessage };
}
@ -2919,11 +2917,11 @@ class ChatPage extends LitElement {
if (window.parent.location.protocol === "https:") {
directSocketLink = `wss://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}`;
directSocketLink = `wss://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&encoding=BASE64`;
} else {
// Fallback to http
directSocketLink = `ws://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}`;
directSocketLink = `ws://${nodeUrl}/websockets/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&encoding=BASE64`;
}
this.webSocket = new WebSocket(directSocketLink);
@ -2942,13 +2940,13 @@ class ChatPage extends LitElement {
const lastMessage = cachedData[cachedData.length - 1]
const newMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&limit=20&reverse=true&after=${lastMessage.timestamp}&haschatreference=false`,
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&limit=20&reverse=true&after=${lastMessage.timestamp}&haschatreference=false&encoding=BASE64`,
});
getInitialMessages = [...cachedData, ...newMessages].slice(-20)
} else {
getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&limit=20&reverse=true&haschatreference=false`,
url: `/chat/messages?involving=${window.parent.reduxStore.getState().app.selectedAddress.address}&involving=${cid}&limit=20&reverse=true&haschatreference=false&encoding=BASE64`,
});
@ -2959,9 +2957,14 @@ class ChatPage extends LitElement {
initial = initial + 1
} else {
try {
if(e.data){
this.processMessages(JSON.parse(e.data), false)
}
} catch (error) {
}
}
}
@ -3009,11 +3012,11 @@ class ChatPage extends LitElement {
if (window.parent.location.protocol === "https:") {
groupSocketLink = `wss://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}`;
groupSocketLink = `wss://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}&encoding=BASE64`;
} else {
// Fallback to http
groupSocketLink = `ws://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}`;
groupSocketLink = `ws://${nodeUrl}/websockets/chat/messages?txGroupId=${groupId}&encoding=BASE64`;
}
this.webSocket = new WebSocket(groupSocketLink);
@ -3037,17 +3040,15 @@ class ChatPage extends LitElement {
const newMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?txGroupId=${groupId}&limit=20&reverse=true&after=${lastMessage.timestamp}&haschatreference=false`,
url: `/chat/messages?txGroupId=${groupId}&limit=20&reverse=true&after=${lastMessage.timestamp}&haschatreference=false&encoding=BASE64`,
});
getInitialMessages = [...cachedData, ...newMessages].slice(-20)
} else {
getInitialMessages = await parentEpml.request('apiCall', {
type: 'api',
url: `/chat/messages?txGroupId=${groupId}&limit=20&reverse=true&haschatreference=false`,
url: `/chat/messages?txGroupId=${groupId}&limit=20&reverse=true&haschatreference=false&encoding=BASE64`,
});
}
@ -3055,9 +3056,14 @@ class ChatPage extends LitElement {
initial = initial + 1
} else {
if(e.data){
this.processMessages(JSON.parse(e.data), false)
try {
if (e.data) {
this.processMessages(JSON.parse(e.data), false)
}
} catch (error) {
}
}
}

174
qortal-ui-plugins/plugins/core/components/ChatScroller.js

@ -28,6 +28,112 @@ import Highlight from '@tiptap/extension-highlight'
const parentEpml = new Epml({ type: 'WINDOW', source: window.parent })
let toggledMessage = {}
const getApiKey = () => {
const myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node];
let apiKey = myNode.apiKey;
return apiKey;
}
const extractComponents = async (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
let responseObj = await parentEpml.request('apiCall', {
url: `/arbitrary/resource/status/${service}/${name}/${identifier}?apiKey=${getApiKey()}`
})
if (responseObj.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;
}
function processText(input) {
const linkRegex = /(qortal:\/\/\S+)/g;
function processNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const parts = node.textContent.split(linkRegex);
if (parts.length > 1) {
const fragment = document.createDocumentFragment();
parts.forEach((part) => {
if (part.startsWith('qortal://')) {
const link = document.createElement('span');
// Store the URL in a data attribute
link.setAttribute('data-url', part);
link.textContent = part;
link.style.color = 'var(--nav-text-color)';
link.style.textDecoration = 'underline';
link.style.cursor = 'pointer'
link.addEventListener('click', async (e) => {
e.preventDefault();
try {
const res = await extractComponents(part)
if (!res) return
const { service, name, identifier, path } = res
window.location = `../../qdn/browser/index.html?service=${service}&name=${name}&identifier=${identifier}&path=${path}`
} catch (error) {
console.log({ error })
}
});
fragment.appendChild(link);
} else {
const textNode = document.createTextNode(part);
fragment.appendChild(textNode);
}
});
node.replaceWith(fragment);
}
} else {
for (const childNode of Array.from(node.childNodes)) {
processNode(childNode);
}
}
}
const wrapper = document.createElement('div');
wrapper.innerHTML = input;
processNode(wrapper);
return wrapper
}
class ChatScroller extends LitElement {
static get properties() {
return {
@ -61,7 +167,9 @@ class ChatScroller extends LitElement {
}
}
static styles = [chatStyles]
static get styles() {
return [chatStyles];
}
constructor() {
super()
@ -103,7 +211,10 @@ class ChatScroller extends LitElement {
const isSameGroup = Math.abs(timestamp - message.timestamp) < 600000 && sender === message.sender && !repliedToData;
if (isSameGroup) {
messageArray[messageArray.length - 1].messages = [...(messageArray[messageArray.length - 1]?.messages || []), message];
messageArray[messageArray.length - 1].messages = [
...(messageArray[messageArray.length - 1].messages || []),
message
];
} else {
messageArray.push({
messages: [message],
@ -324,7 +435,10 @@ class MessageTemplate extends LitElement {
this.viewImage = false
}
static styles = [chatStyles]
static get styles() {
return [chatStyles];
}
// Open & Close Private Message Chat Modal
showPrivateMessageModal() {
@ -369,7 +483,7 @@ class MessageTemplate extends LitElement {
}
}
firstUpdated(){
const autoSeeChatList = window.parent.reduxStore.getState().app?.autoLoadImageChats
const autoSeeChatList = window.parent.reduxStore.getState().app.autoLoadImageChats
if(autoSeeChatList.includes(this.chatId) || this.listSeenMessages.includes(this.messageObj.signature)){
this.viewImage = true
}
@ -386,6 +500,7 @@ class MessageTemplate extends LitElement {
const hidemsg = this.hideMessages;
let message = "";
let messageVersion2 = ""
let messageVersion2WithLink = null
let reactions = [];
let repliedToData = null;
let image = null;
@ -405,6 +520,8 @@ class MessageTemplate extends LitElement {
Highlight
// other extensions …
])
messageVersion2WithLink = processText(messageVersion2)
}
message = parsedMessageObj.messageText;
repliedToData = this.messageObj.repliedToData;
@ -424,7 +541,6 @@ class MessageTemplate extends LitElement {
gif = parsedMessageObj.gifs[0];
}
} catch (error) {
console.error(error);
message = this.messageObj.decodedMessage;
}
let avatarImg = '';
@ -550,8 +666,28 @@ class MessageTemplate extends LitElement {
}
}
const escapedMessage = this.escapeHTML(message)
const replacedMessage = escapedMessage.replace(new RegExp('\r?\n','g'), '<br />');
let repliedToMessageText = ''
if (repliedToData && repliedToData.decodedMessage && repliedToData.decodedMessage.messageText) {
try {
repliedToMessageText = generateHTML(repliedToData.decodedMessage.messageText, [
StarterKit,
Underline,
Highlight
// other extensions …
])
} catch (error) {
}
}
let replacedMessage = ''
if (message && +version < 2) {
const escapedMessage = this.escapeHTML(message)
if (escapedMessage) {
replacedMessage = escapedMessage.replace(new RegExp('\r?\n', 'g'), '<br />');
}
}
return hideit ? html`<li class="clearfix"></li>` : html`
<li
@ -642,19 +778,14 @@ class MessageTemplate extends LitElement {
class=${this.myAddress !== repliedToData.sender
? "original-message-sender"
: "message-data-my-name"}>
${repliedToData.senderName ?? cropAddress(repliedToData.sender)}
${repliedToData.senderName ? cropAddress(repliedToData.sender) : ''}
</p>
<p class="replied-message">
${version.toString() === '1' ? html`
${version && version.toString() === '1' ? html`
${repliedToData.decodedMessage.messageText}
` : ''}
${+version > 1 ? html`
${unsafeHTML(generateHTML(repliedToData.decodedMessage.messageText, [
StarterKit,
Underline,
Highlight
// other extensions …
]))}
${+version > 1 && repliedToMessageText ? html`
${unsafeHTML(repliedToMessageText)}
`
: ''}
</p>
@ -764,13 +895,14 @@ class MessageTemplate extends LitElement {
id="messageContent"
class="message"
style=${(image && replacedMessage !== "") &&"margin-top: 15px;"}>
${+version > 1 ? html`
${+version > 1 ? messageVersion2WithLink ? html`${messageVersion2WithLink}` : html`
${unsafeHTML(messageVersion2)}
` : ''}
${version.toString() === '1' ? html`
${version && version.toString() === '1' ? html`
${unsafeHTML(this.emojiPicker.parse(replacedMessage))}
` : ''}
${version.toString() === '0' ? html`
${version && version.toString() === '0' ? html`
${unsafeHTML(this.emojiPicker.parse(replacedMessage))}
` : ''}
<div
@ -1039,7 +1171,9 @@ class ChatMenu extends LitElement {
this.showBlockUserModal = () => {};
}
static styles = [chatStyles]
static get styles() {
return [chatStyles];
}
// Copy address to clipboard
async copyToClipboard(text) {

9
qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js

@ -146,15 +146,15 @@ class WebBrowser extends LitElement {
// Build initial display URL
let displayUrl = 'qortal://' + this.service + '/' + this.name;
if (
this.identifier != null &&
data.identifier != '' &&
this.identifier && this.identifier != 'null' &&
this.identifier != 'default'
)
{
displayUrl = displayUrl.concat('/' + this.identifier);
}
if (this.path != null && this.path != '/')
displayUrl = displayUrl.concat(this.path);
this.displayUrl = displayUrl;
const getFollowedNames = async () => {
let followedNames = await parentEpml.request('apiCall', {
url: `/lists/followedNames?apiKey=${this.getApiKey()}`,
@ -193,8 +193,9 @@ class WebBrowser extends LitElement {
}
else {
// Normal mode
this.url = `${nodeUrl}/render/${this.service}/${this.name}${this.path != null ? this.path : ''
}?theme=${this.theme}&identifier=${this.identifier != null ? this.identifier : ''
}?theme=${this.theme}&identifier=${(this.identifier != null && this.identifier != 'null') ? this.identifier : ''
}`
}
}

8
qortal-ui-plugins/plugins/utils/replace-messages-edited.js

@ -14,7 +14,7 @@ export const replaceMessagesEdited = async ({
}
const response = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?chatreference=${msg.signature}&reverse=true${msgQuery}&limit=1&sender=${msg.sender}`,
url: `/chat/messages?chatreference=${msg.signature}&reverse=true${msgQuery}&limit=1&sender=${msg.sender}&encoding=BASE64`,
})
if (response && Array.isArray(response) && response.length !== 0) {
@ -54,13 +54,13 @@ export const replaceMessagesEdited = async ({
if(+parsedMessageObj.version > 2){
originalReply = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/message/${parsedMessageObj.repliedTo}`,
url: `/chat/message/${parsedMessageObj.repliedTo}?encoding=BASE64`,
})
}
if(+parsedMessageObj.version < 3){
originalReply = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?reference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}`,
url: `/chat/messages?reference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&encoding=BASE64`,
})
}
@ -71,7 +71,7 @@ export const replaceMessagesEdited = async ({
const response = await parentEpml.request("apiCall", {
type: "api",
url: `/chat/messages?chatreference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&limit=1&sender=${originalReplyMessage.sender}`,
url: `/chat/messages?chatreference=${parsedMessageObj.repliedTo}&reverse=true${msgQuery}&limit=1&sender=${originalReplyMessage.sender}&encoding=BASE64`,
})
if (

Loading…
Cancel
Save