diff --git a/src/lib/adamant-api/asset.d.ts b/src/lib/adamant-api/asset.d.ts index 546a65593..4dbfbe5ab 100644 --- a/src/lib/adamant-api/asset.d.ts +++ b/src/lib/adamant-api/asset.d.ts @@ -1,3 +1,5 @@ +import { FileData } from '@/components/UploadFile.vue' + /** * AIP 12: Non-ADM crypto transfer messages * @see https://aips.adamant.im/AIPS/aip-12 @@ -158,4 +160,9 @@ export interface AttachmentAsset { comment?: string } -export function attachmentAsset(files: File[], comment?: string): AttachmentAsset +export function attachmentAsset( + files: FileData[], + nonces?: [string, string], + ids?: [string, string], + comment?: string +): AttachmentAsset diff --git a/src/lib/adamant-api/asset.js b/src/lib/adamant-api/asset.js index 43ae58924..4cef13937 100644 --- a/src/lib/adamant-api/asset.js +++ b/src/lib/adamant-api/asset.js @@ -38,26 +38,35 @@ export function reactionAsset(reactToId, reactMessage) { /** * AIP-18: https://github.com/Adamant-im/AIPs/pull/54/files - * @param {Array} files - * @param {string} comment + * @param {Array} files + * @param {Array<[string, string]>} [nonces] First element is the nonce of original file, second is nonce of preview + * @param {Array<[string, string]>} [ids] List of files IDs after uploading to IPFS. First element is the ID of original file, second is ID of preview. + * @param {string} [comment] */ -export function attachmentAsset(files, comment) { +export function attachmentAsset(files, nonces, ids, comment) { return { - files: files.map((file) => ({ - mimeType: file.type, - name: file.name, - extension: file.name.split('.').pop(), - // resolution?: [number, number] // @todo - // duration?: number // @todo - id: '', - size: file.size, - nonce: '', - preview: { - id: '', - nonce: '', - extension: '' + files: files.map(({ file, width, height }, index) => { + const [name, extension] = file.name.split('.') + const resolution = width && height ? [width, height] : undefined + const [nonce, previewNonce] = nonces?.[index] || [] + const [id, previewId] = ids?.[index] || [] + + return { + mimeType: file.type, + name, + extension, + resolution, + duration: undefined, // @todo check if is a video or audio file + size: file.size, + id, + nonce, + preview: { + id: previewId, + nonce: previewNonce, + extension + } } - })), + }), comment, storage: { id: 'ipfs' } } diff --git a/src/lib/adamant-api/index.d.ts b/src/lib/adamant-api/index.d.ts index 345882fb0..2731347b9 100644 --- a/src/lib/adamant-api/index.d.ts +++ b/src/lib/adamant-api/index.d.ts @@ -89,10 +89,13 @@ export type SendMessageParams = { amount?: number } export function sendMessage(params: SendMessageParams): Promise -export function encodeFile( - file: Uint8Array, - params: SendMessageParams -): Promise<{ binary: Uint8Array; nonce: string }> + +export type EncodedFile = { + binary: Uint8Array + nonce: string +} + +export function encodeFile(file: Uint8Array, params: SendMessageParams): Promise export function sendSpecialMessage( to: string, diff --git a/src/lib/chat/helpers/createAttachment.ts b/src/lib/chat/helpers/createAttachment.ts index 8fac974b6..38f4eb37d 100644 --- a/src/lib/chat/helpers/createAttachment.ts +++ b/src/lib/chat/helpers/createAttachment.ts @@ -1,5 +1,6 @@ import { FileData } from '@/components/UploadFile.vue' import utils from '@/lib/adamant' +import { EncodedFile } from '@/lib/adamant-api' import { NormalizedChatMessageTransaction } from '@/lib/chat/helpers/normalizeMessage' import { TransactionStatus as TS, TransactionStatusType } from '@/lib/constants' import { attachmentAsset } from '@/lib/adamant-api/asset' @@ -8,6 +9,9 @@ type Params = { recipientId: string senderId: string files: FileData[] + nonces?: [string, string] + ids?: [string, string] + encodedFiles?: EncodedFile[] message?: string replyToId?: string status?: TransactionStatusType @@ -19,13 +23,14 @@ export function createAttachment({ recipientId, senderId, files, + nonces, + ids, message, replyToId, status = TS.PENDING }: Params) { const timestamp = Date.now() const id = utils.epochTime().toString() - const filesList = files.map((file) => file.file) const transaction: NormalizedChatMessageTransaction = { id, @@ -37,8 +42,8 @@ export function createAttachment({ timestamp: Date.now(), type: 'attachment', asset: replyToId - ? { replyto_id: replyToId, reply_message: attachmentAsset(filesList) } - : attachmentAsset(filesList), + ? { replyto_id: replyToId, reply_message: attachmentAsset(files, nonces, ids, message) } + : attachmentAsset(files, nonces, ids, message), /** * When sending a message, we need to store the files locally. */ diff --git a/src/lib/file.ts b/src/lib/file.ts index 98ba6f918..6eda63083 100644 --- a/src/lib/file.ts +++ b/src/lib/file.ts @@ -1,3 +1,7 @@ +import { FileData } from '@/components/UploadFile.vue' +import { EncodedFile } from '@/lib/adamant-api' +import ipfs from '@/lib/nodes/ipfs' + export function readFileAsDataURL(file: File): Promise<{ raw: Uint8Array; dataURL: string }> { return new Promise((resolve) => { const reader = new FileReader() @@ -29,3 +33,26 @@ export function readFileAsBuffer(file: File): Promise { reader.readAsArrayBuffer(file) }) } + +export async function uploadFiles(files: FileData[], encodedFiles: EncodedFile[]) { + const formData = new FormData() + + for (const [index, file] of Object.entries(files)) { + const encodedFile = encodedFiles[+index]?.binary + if (!encodedFile) { + throw new Error(`Missing binary data for ${file.file.name}`) + } + + const blob = new Blob([encodedFile], { type: 'application/octet-stream' }) + formData.append('files', blob, file.file.name) + } + + const response = await ipfs.upload(formData, (progress) => { + const percentCompleted = Math.round((progress.loaded * 100) / (progress.total || 0)) + + console.log(`Progress ${percentCompleted}%`) + }) + console.log(`Uploaded CIDs`, response) + + return response +} diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index 09757cb32..c302bde86 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -7,14 +7,16 @@ import { createMessage, createTransaction, createReaction, - normalizeMessage + normalizeMessage, + createAttachment } from '@/lib/chat/helpers' import { i18n } from '@/i18n' import { isNumeric } from '@/lib/numericHelpers' import { Cryptos, TransactionStatus as TS, MessageType } from '@/lib/constants' import { isStringEqualCI } from '@/lib/textHelpers' -import { replyMessageAsset } from '@/lib/adamant-api/asset' -import { createAttachment } from '../../../lib/chat/helpers' +import { replyMessageAsset, attachmentAsset } from '@/lib/adamant-api/asset' +import { encodeFile } from '../../../lib/adamant-api' +import { readFileAsBuffer, uploadFiles } from '../../../lib/file' import { generateAdamantChats } from './utils/generateAdamantChats' import { isAllNodesDisabledError, isAllNodesOfflineError } from '@/lib/nodes/utils/errors' @@ -460,7 +462,7 @@ const mutations = { * @param {string} realId Real id (from server) * @param {string} status Message status */ - updateMessage(state, { partnerId, id, realId, status }) { + updateMessage(state, { partnerId, id, realId, status, asset }) { const chat = state.chats[partnerId] if (chat) { @@ -473,6 +475,9 @@ const mutations = { if (status) { message.status = status } + if (asset) { + message.asset = asset + } } } }, @@ -766,27 +771,54 @@ const actions = { * @returns {Promise} */ async sendAttachment({ commit, rootState }, { files, message, recipientId, replyToId }) { - const messageObject = createAttachment({ + let messageObject = createAttachment({ message, recipientId, senderId: rootState.address, files, replyToId }) + console.log('Pushed message', messageObject) commit('pushMessage', { message: messageObject, userId: rootState.address }) - const type = replyToId ? MessageType.RICH_CONTENT_MESSAGE : MessageType.BASIC_ENCRYPTED_MESSAGE + const encodedFiles = await Promise.all( + files.map((file) => + readFileAsBuffer(file.file).then((buffer) => encodeFile(buffer, { to: recipientId })) + ) + ) - // const promises = files.map(file => readFile(file)) - // const result = await Promise.all(promises); - // console.log('result', result) + // Updating nonces + const nonces = encodedFiles.map((file) => [file.nonce, file.nonce]) // @todo preview nonce + let newAsset = replyToId + ? { replyto_id: replyToId, reply_message: attachmentAsset(files, nonces, undefined, message) } + : attachmentAsset(files, nonces, undefined, message) + commit('updateMessage', { + id: messageObject.id, + partnerId: recipientId, + asset: newAsset + }) + console.log('Updated Nonces', newAsset) - throw new Error('TODO') - return queueMessage(messageObject.asset, recipientId, type) + const uploadData = await uploadFiles(files, encodedFiles) + console.log('Files uploaded', uploadData) + + // Update CIDs + const cids = uploadData.cids.map((cid) => [cid, cid]) // @todo preview cid + newAsset = replyToId + ? { replyto_id: replyToId, reply_message: attachmentAsset(files, nonces, cids, message) } + : attachmentAsset(files, nonces, cids, message) + commit('updateMessage', { + id: messageObject.id, + partnerId: recipientId, + asset: newAsset + }) + console.log('Updated CIDs', newAsset) + + return queueMessage(newAsset, recipientId, MessageType.RICH_CONTENT_MESSAGE) .then((res) => { if (!res.success) { throw new Error(i18n.global.t('chats.message_rejected'))