From 0b4628f99e7a8d8b8033244f46203310666cb90d Mon Sep 17 00:00:00 2001 From: Yulia Hermak Date: Thu, 28 Mar 2024 05:50:01 +0000 Subject: [PATCH 001/111] feat: preview image --- src/components/AChat/AChatForm.vue | 1 + src/components/AChat/AChatPreviewFile.vue | 94 +++++++++++++++++++++++ src/components/AChat/index.js | 4 +- src/components/Chat/Chat.vue | 18 ++++- src/components/Chat/ChatMenu.vue | 50 ++++++++---- 5 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 src/components/AChat/AChatPreviewFile.vue diff --git a/src/components/AChat/AChatForm.vue b/src/components/AChat/AChatForm.vue index d58f31b67..0d35b9523 100644 --- a/src/components/AChat/AChatForm.vue +++ b/src/components/AChat/AChatForm.vue @@ -3,6 +3,7 @@ + +
+
+ Selected Image + + +
+
+ + + + + diff --git a/src/components/AChat/index.js b/src/components/AChat/index.js index 668af46bf..94472d86c 100644 --- a/src/components/AChat/index.js +++ b/src/components/AChat/index.js @@ -8,6 +8,7 @@ import AChatMessageActionsList from './AChatMessageActionsList.vue' import AChatMessageActionsMenu from './AChatMessageActionsMenu.vue' import AChatReactionSelect from './AChatReactionSelect/AChatReactionSelect.vue' import AChatActionsOverlay from './AChatActionsOverlay.vue' +import AChatPreviewFile from './AChatPreviewFile.vue' export { AChat, @@ -19,5 +20,6 @@ export { AChatMessageActionsList, AChatMessageActionsMenu, AChatReactionSelect, - AChatActionsOverlay + AChatActionsOverlay, + AChatPreviewFile } diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index da6d0324c..d9009ec86 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -223,9 +223,14 @@ class="chat-menu" :partner-id="partnerId" :reply-to-id="replyMessageId > -1 ? replyMessageId : undefined" + @image-selected="handleImageSelected" /> + + - - - - - {{ $t('chats.send_crypto', { crypto: c }) }} - - + - + + From cb40c0d00f1930a705e6a5e011de1d7fb3217765 Mon Sep 17 00:00:00 2001 From: Yulia Hermak Date: Wed, 10 Apr 2024 23:58:35 +0100 Subject: [PATCH 006/111] fix: classes names --- src/components/AChat/AChatFile.vue | 4 +- src/components/AChat/AChatImageModal.vue | 91 ++++++++++++----------- src/components/AChat/AChatPreviewFile.vue | 23 +++--- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/components/AChat/AChatFile.vue b/src/components/AChat/AChatFile.vue index 650568f41..b8a037bb3 100644 --- a/src/components/AChat/AChatFile.vue +++ b/src/components/AChat/AChatFile.vue @@ -36,7 +36,7 @@ const className = 'a-chat-file' const classes = { root: className, container: `${className}__container`, - containerWithElement: `${className}__containerWithElement`, + containerWithElement: `${className}__container-with-element`, img: `${className}__img` } @@ -90,7 +90,7 @@ export default defineComponent({ border-radius: 8px; } - &__containerWithElement { + &__container-with-element { display: grid; gap: 2px; grid-template-columns: repeat(2, minmax(50px, 1fr)); diff --git a/src/components/AChat/AChatImageModal.vue b/src/components/AChat/AChatImageModal.vue index 0540ee156..c0f05ad4a 100644 --- a/src/components/AChat/AChatImageModal.vue +++ b/src/components/AChat/AChatImageModal.vue @@ -1,13 +1,17 @@ @@ -393,7 +393,7 @@ export default { }, emits: ['click:chat-avatar'], data: () => ({ - selectedImage: [], + files: [], loading: false, replyLoadingChatHistory: false, noMoreMessages: false, @@ -509,18 +509,14 @@ export default { nextTick(() => this.$refs.chat.scrollToBottom()) this.replyMessageId = -1 }, - handleImageSelected(imageData) { - if (this.selectedImage.length < 10) { - this.selectedImage.push(imageData) - } else { - alert('можно загрузить только 10 файлов') - } + handleFiles(filesList) { + this.files = filesList }, removeItem(index) { - this.selectedImage.splice(index, 1) + this.files.splice(index, 1) }, cancelPreviewFile() { - this.selectedImage = [] + this.files.length = 0 }, onMessageError(error) { switch (error) { diff --git a/src/components/Chat/ChatMenu.vue b/src/components/Chat/ChatMenu.vue index e5c972d4f..bdddc7f57 100644 --- a/src/components/Chat/ChatMenu.vue +++ b/src/components/Chat/ChatMenu.vue @@ -4,11 +4,14 @@ - + - - + diff --git a/src/lib/constants/index.d.ts b/src/lib/constants/index.d.ts index 6d0204ace..fd9554d8c 100644 --- a/src/lib/constants/index.d.ts +++ b/src/lib/constants/index.d.ts @@ -29,6 +29,8 @@ export declare const Rates: { export declare const RatesNames: Record +export declare const UPLOAD_MAX_FILE_COUNT: number + export declare const Fees: { KVS: number ADM_TRANSFER: number diff --git a/src/lib/constants/index.js b/src/lib/constants/index.js index dda2f63e6..6438a19ee 100644 --- a/src/lib/constants/index.js +++ b/src/lib/constants/index.js @@ -40,6 +40,8 @@ export const RatesNames = { [Rates.JPY]: 'JPY (¥)' // Japanese Yen } +export const UPLOAD_MAX_FILE_COUNT = 6 + /** Fees for the misc ADM operations */ export const Fees = { /** Storing a value into the KVS */ diff --git a/src/locales/en.json b/src/locales/en.json index 8924e9271..4dd51d36e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -16,6 +16,7 @@ "incorrect_address": "Incorrect ADM address", "no_public_key": "This address is not active yet. Unable to start a chat", "unable_to_retrieve_no_public_key": "`Unable to decrypt message: no partner's public key`", + "max_files": "You can upload max 6 files", "me": "Me", "message": "Type a message", "my_qr_code": "My QR code", diff --git a/src/locales/ru.json b/src/locales/ru.json index 45720cbd9..ea5e6658c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -16,6 +16,7 @@ "incorrect_address": "Неправильный ADM-адрес", "no_public_key": "Этот адрес еще не использовался. С ним нельзя начать чат", "unable_to_retrieve_no_public_key": "`Не могу прочитать сообщение: нет публичного ключа собеседника`", + "max_files": "Вы можете загрузить максимум 6 файлов", "me": "Я", "message": "Введите сообщение", "my_qr_code": "Мой QR-код", From 3e0b5a0c3c1ab196c6d0ed2a02124d7707746ecb Mon Sep 17 00:00:00 2001 From: Yulia Hermak Date: Thu, 18 Apr 2024 00:26:41 +0100 Subject: [PATCH 008/111] fix: upload a file and photo with a limited selection --- src/components/Chat/ChatMenu.vue | 48 +++++++++++++++++--------------- src/components/UploadFile.vue | 12 +++++++- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/components/Chat/ChatMenu.vue b/src/components/Chat/ChatMenu.vue index bdddc7f57..fb050249f 100644 --- a/src/components/Chat/ChatMenu.vue +++ b/src/components/Chat/ChatMenu.vue @@ -4,21 +4,35 @@ - + + - - + + + {{ $t('chats.attach_image') }} + - {{ $t(item.title) }} + + + + {{ $t('chats.attach_file') }} @@ -61,23 +75,13 @@ export default { } }, data: () => ({ - menuItems: [ - { - type: 'action', - title: 'chats.attach_image', - icon: 'mdi-image' - }, - { - type: 'action', - title: 'chats.attach_file', - icon: 'mdi-file' - } - ], dialog: false, dialogTitle: '', dialogText: '', crypto: '', - filesList: [] + filesList: [], + acceptImage: 'image/* , video/*', + acceptFile: 'application/* ,text/*, audio/*' }), computed: { orderedVisibleWalletSymbols() { diff --git a/src/components/UploadFile.vue b/src/components/UploadFile.vue index 066f73cec..67b46a98d 100644 --- a/src/components/UploadFile.vue +++ b/src/components/UploadFile.vue @@ -1,8 +1,18 @@ diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index f235938a0..41e43a9f6 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -143,6 +143,13 @@ + + + + + ({ + attachments: {} +}) + +const mutations: MutationTree = { + setAttachments(state, attachments) { + // TODO: Poor realization + state.attachments = attachments + }, + + reset(state) { + state.attachments = {} + } +} + +const getters: GetterTree = { + getFileMessage: (state) => (id: string) => { + const objMessage = state.attachments[id] + if (objMessage?.files) { + return objMessage.files + } + } +} + +const actions: ActionTree = { + getAttachment({ commit }, cid: string) { + const url = getRandomServiceUrl('adm', 'attachmentService') + return new Promise((resolve, reject) => { + axios + .get(`${url}/file/${cid}`) + .then((res) => { + const responce = res.data + commit('setAttachments', responce) + resolve(res) + }) + .catch((err) => { + reject(err) + }) + }) + }, + resetState(context) { + context.commit('reset') + } +} + +export default { + state, + mutations, + getters, + actions, + namespaced: true +} diff --git a/src/store/modules/attachment/types.ts b/src/store/modules/attachment/types.ts new file mode 100644 index 000000000..d0e704213 --- /dev/null +++ b/src/store/modules/attachment/types.ts @@ -0,0 +1,24 @@ +export type AttachmentImage = { + preview_nonce: string + file_resolution: Array + file_id: string + file_type: string + file_size: number + file_name: string + preview_id: string + nonce: string +} + +export type AttachmentMessage = { + files: Array + storage: Storage | string + comment: string +} + +export type Storage = { + name: string +} + +export interface AttachmentsState { + attachments: { [key: string]: AttachmentMessage } +} From fb81a5c37a15dde7e798777ad8ebeb55040c16ee Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 19 Apr 2024 21:17:35 +0300 Subject: [PATCH 010/111] feat(normalizeMessage): reply with image. Work in progress --- src/lib/chat/helpers/normalizeMessage.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/chat/helpers/normalizeMessage.js b/src/lib/chat/helpers/normalizeMessage.js index a92433419..9df74465b 100644 --- a/src/lib/chat/helpers/normalizeMessage.js +++ b/src/lib/chat/helpers/normalizeMessage.js @@ -51,6 +51,11 @@ export function normalizeMessage(abstract) { } else { transaction.type = 'message' } + } else if (abstract.message.reply_message.files) { + transaction.asset = abstract.message + transaction.message = abstract.message.reply_message || '' + transaction.hash = abstract.id + transaction.type = 'attachment' } else { // reply with a crypto transfer transaction.asset = abstract.message From 042b2015e0b07bf0badab2c6636734e3874dab27 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 20 Apr 2024 16:18:40 +0300 Subject: [PATCH 011/111] feat(QuotedMessage): rewritten to TS. Ready to interact with attachments. Work in progress --- src/components/AChat/QuotedMessage.vue | 80 ++++++++++++++++++-------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/src/components/AChat/QuotedMessage.vue b/src/components/AChat/QuotedMessage.vue index 2b5ac374a..dd6e1a153 100644 --- a/src/components/AChat/QuotedMessage.vue +++ b/src/components/AChat/QuotedMessage.vue @@ -15,27 +15,31 @@ {{ '{ ' + $t('chats.message_not_found') + ' }' }} -
- - {{ cryptoTransferLabel }} - - +
+ + +
- diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 41e43a9f6..90932710c 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -144,7 +144,18 @@ - + From 64924143b04060747260179a93ff107d241e7009 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 23 Apr 2024 20:41:44 +0300 Subject: [PATCH 017/111] chore(nodes): preparation for IPFS infoService health check --- src/components/nodes/ipfs/IpfsNodesTable.vue | 48 +++++++ .../nodes/ipfs/IpfsNodesTableItem.vue | 122 +++++++++++++++++ src/components/nodes/ipfs/index.ts | 1 + src/lib/nodes/constants.ts | 2 + src/lib/nodes/ipfs/IpfsClient.ts | 62 +++++++++ src/lib/nodes/ipfs/IpfsNode.ts | 124 ++++++++++++++++++ src/lib/nodes/ipfs/index.ts | 8 ++ src/lib/nodes/nodes.ts | 4 +- src/lib/nodes/types.ts | 2 +- src/store/modules/nodes/nodes-actions.js | 3 + src/store/modules/nodes/nodes-getters.js | 3 + src/store/modules/nodes/nodes-mutations.js | 3 + src/store/modules/nodes/nodes-plugin.js | 5 + src/store/modules/nodes/nodes-state.js | 4 +- 14 files changed, 388 insertions(+), 3 deletions(-) create mode 100644 src/components/nodes/ipfs/IpfsNodesTable.vue create mode 100644 src/components/nodes/ipfs/IpfsNodesTableItem.vue create mode 100644 src/components/nodes/ipfs/index.ts create mode 100644 src/lib/nodes/ipfs/IpfsClient.ts create mode 100644 src/lib/nodes/ipfs/IpfsNode.ts create mode 100644 src/lib/nodes/ipfs/index.ts diff --git a/src/components/nodes/ipfs/IpfsNodesTable.vue b/src/components/nodes/ipfs/IpfsNodesTable.vue new file mode 100644 index 000000000..5202282e5 --- /dev/null +++ b/src/components/nodes/ipfs/IpfsNodesTable.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/nodes/ipfs/IpfsNodesTableItem.vue b/src/components/nodes/ipfs/IpfsNodesTableItem.vue new file mode 100644 index 000000000..769f83bef --- /dev/null +++ b/src/components/nodes/ipfs/IpfsNodesTableItem.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/src/components/nodes/ipfs/index.ts b/src/components/nodes/ipfs/index.ts new file mode 100644 index 000000000..bde8905e4 --- /dev/null +++ b/src/components/nodes/ipfs/index.ts @@ -0,0 +1 @@ +export { default as IpfsNodesTable } from './IpfsNodesTable.vue' diff --git a/src/lib/nodes/constants.ts b/src/lib/nodes/constants.ts index 6cd0ddee3..ff43dafc5 100644 --- a/src/lib/nodes/constants.ts +++ b/src/lib/nodes/constants.ts @@ -7,6 +7,7 @@ export type TNodeLabel = | 'doge-node' | 'doge-indexer' | 'dash-node' + | 'ipfs-node' | 'lsk-node' | 'lsk-indexer' | 'rates-info' @@ -36,6 +37,7 @@ export const NODE_LABELS: NodeLabels = { DogeNode: 'doge-node', DogeIndexer: 'doge-indexer', DashNode: 'dash-node', + IpfsNode: 'ipfs-node', LskNode: 'lsk-node', LskIndexer: 'lsk-indexer', RatesInfo: 'rates-info' diff --git a/src/lib/nodes/ipfs/IpfsClient.ts b/src/lib/nodes/ipfs/IpfsClient.ts new file mode 100644 index 000000000..4f22c59b2 --- /dev/null +++ b/src/lib/nodes/ipfs/IpfsClient.ts @@ -0,0 +1,62 @@ +import { isNodeOfflineError } from '@/lib/nodes/utils/errors' +import { IpfsNode, Payload, RequestConfig } from './IpfsNode.ts' +import { Client } from '../abstract.client' + +/** + * Provides methods for calling the ADAMANT API. + * + * The `ApiClient` instance automatically selects an ADAMANT node to + * send the API-requests to and switches to another node if the current one + * is not available at the moment. + */ +export class IpfsClient extends Client { + constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { + super('ipfs') + this.nodes = endpoints.map((endpoint) => new IpfsNode(endpoint, minNodeVersion)) + this.minNodeVersion = minNodeVersion + + void this.watchNodeStatusChange() + } + + /** + * Performs a GET API request. + * @param {String} url relative API url + * @param {any} params request params (an object) or a function that accepts `ApiNode` and returns the request params + */ + get

(url: string, params: P) { + return this.request({ method: 'get', url, payload: params }) + } + + /** + * Performs a POST API request. + * @param {String} url relative API url + * @param {any} payload request payload (an object) or a function that accepts `ApiNode` and returns the request payload + */ + post

(url: string, payload: P) { + return this.request({ method: 'post', url, payload }) + } + + /** + * Performs an API request. + * @param {RequestConfig} config request config + */ + async request

(config: RequestConfig

): Promise { + const node = this.useFastest ? this.getFastestNode() : this.getRandomNode() + if (!node) { + // All nodes seem to be offline: let's refresh the statuses + this.checkHealth() + // But there's nothing we can do right now + return Promise.reject(new Error('No online nodes at the moment')) + } + + return node.request(config).catch((error) => { + if (isNodeOfflineError(error)) { + // Initiate nodes status check + this.checkHealth() + // If the selected node is not available, repeat the request with another one. + return this.request(config) + } + throw error + }) + } +} diff --git a/src/lib/nodes/ipfs/IpfsNode.ts b/src/lib/nodes/ipfs/IpfsNode.ts new file mode 100644 index 000000000..65bc8dc18 --- /dev/null +++ b/src/lib/nodes/ipfs/IpfsNode.ts @@ -0,0 +1,124 @@ +import utils from '@/lib/adamant' +import { NodeOfflineError } from '@/lib/nodes/utils/errors' +import { GetNodeStatusResponseDto } from '@/lib/schema/client' +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' +import { Node } from '@/lib/nodes/abstract.node' +import { NODE_LABELS } from '@/lib/nodes/constants' + +type FetchNodeInfoResult = { + socketSupport: boolean + version: string + height: number + wsPort?: string +} + +export type Payload = + | Record + | { + (ctx: IpfsNode): Record + } +export type RequestConfig

= { + url: string + method?: string + payload?: P +} + +/** + * Encapsulates a node. Provides methods to send API-requests + * to the node and verify is status (online/offline, version, ping, etc.) + */ +export class IpfsNode extends Node { + constructor(url: string, minNodeVersion = '0.0.0') { + super(url, 'ipfs', 'node', NODE_LABELS.IpfsNode, '', minNodeVersion) + + this.wsPort = '36668' // default wsPort + this.wsProtocol = this.protocol === 'https:' ? 'wss:' : 'ws:' + this.wsPortNeeded = this.wsProtocol === 'ws:' && !this.hostname.includes('.onion') + } + + protected buildClient(): AxiosInstance { + return axios.create({ + baseURL: this.url + }) + } + + /** + * Performs an API request. + * + * The `payload` of the `cfg` can be either an object or a function that + * accepts `ApiNode` as a first argument and returns an object. + */ + request

(cfg: RequestConfig

): Promise { + const { url, method = 'get', payload } = cfg + + const config: AxiosRequestConfig = { + url, + method: method.toLowerCase(), + [method === 'get' ? 'params' : 'data']: + typeof payload === 'function' ? payload(this) : payload + } + + return this.client.request(config).then( + (response) => { + const body = response.data + // Refresh time delta on each request + if (body && isFinite(body.nodeTimestamp)) { + this.timeDelta = utils.epochTime() - body.nodeTimestamp + } + + return body + }, + (error) => { + // According to https://github.com/axios/axios#handling-errors this means, that request was sent, + // but server could not respond. + if (!error.response && error.request) { + this.online = false + throw new NodeOfflineError() + } + throw error + } + ) + } + + /** + * Fetch node version, block height and ping. + * @returns {Promise<{version: string, height: number, ping: number}>} + */ + private async fetchNodeInfo(): Promise { + const { success, version, network, wsClient } = await this.request< + Payload, + GetNodeStatusResponseDto + >({ url: '/api/node/status' }) + + if (success) { + const readableVersion = version.version + const height = Number(network.height) + const socketSupport = wsClient ? wsClient.enabled : false + const wsPort = wsClient ? String(wsClient.port) : '' + + this.version = readableVersion + this.height = height + this.socketSupport = socketSupport + this.wsPort = wsPort + + return { + version: readableVersion, + height, + socketSupport, + wsPort + } + } + + throw new Error('Request to /api/node/status was unsuccessful') + } + + protected async checkHealth() { + const time = Date.now() + const { height } = await this.fetchNodeInfo() + + return { + height, + ping: Date.now() - time + } + } +} diff --git a/src/lib/nodes/ipfs/index.ts b/src/lib/nodes/ipfs/index.ts new file mode 100644 index 000000000..3d06a1b96 --- /dev/null +++ b/src/lib/nodes/ipfs/index.ts @@ -0,0 +1,8 @@ +import config from '@/config' +import { NodeInfo } from '@/types/wallets' +import { IpfsClient } from './IpfsClient.ts' + +const endpoints = (config.ipfs.nodes.list as NodeInfo[]).map((endpoint) => endpoint.url) +export const ipfs = new IpfsClient(endpoints, config.ipfs.nodes.minVersion) + +export default ipfs diff --git a/src/lib/nodes/nodes.ts b/src/lib/nodes/nodes.ts index 9be8d5dcf..682bb305e 100644 --- a/src/lib/nodes/nodes.ts +++ b/src/lib/nodes/nodes.ts @@ -4,6 +4,7 @@ import { dash } from './dash' import { doge } from './doge' import { eth } from './eth' import { lsk } from './lsk' +import { ipfs } from './ipfs' export const nodes = { adm, @@ -11,5 +12,6 @@ export const nodes = { dash, doge, eth, - lsk + lsk, + ipfs } diff --git a/src/lib/nodes/types.ts b/src/lib/nodes/types.ts index d938906fb..89eacc12e 100644 --- a/src/lib/nodes/types.ts +++ b/src/lib/nodes/types.ts @@ -5,7 +5,7 @@ export type NodeStatus = | 'sync' // node is out of sync (too low block height) | 'unsupported_version' // node version is too low -export type NodeType = 'adm' | 'eth' | 'btc' | 'doge' | 'dash' | 'lsk' +export type NodeType = 'adm' | 'eth' | 'btc' | 'doge' | 'dash' | 'ipfs' | 'lsk' export type NodeKind = 'node' | 'service' export type HealthcheckInterval = 'normal' | 'crucial' | 'onScreen' diff --git a/src/store/modules/nodes/nodes-actions.js b/src/store/modules/nodes/nodes-actions.js index 3661d4e06..8e8602ef7 100644 --- a/src/store/modules/nodes/nodes-actions.js +++ b/src/store/modules/nodes/nodes-actions.js @@ -16,5 +16,8 @@ export default { }, setUseFastestCoinNode(context, payload) { context.commit('useFastestCoinNode', payload) + }, + setUseFastestIpfsNode(context, payload) { + context.commit('useFastestIpfsNode', payload) } } diff --git a/src/store/modules/nodes/nodes-getters.js b/src/store/modules/nodes/nodes-getters.js index e43ac11ba..23593b9f7 100644 --- a/src/store/modules/nodes/nodes-getters.js +++ b/src/store/modules/nodes/nodes-getters.js @@ -14,6 +14,9 @@ export default { dash(state) { return Object.values(state.dash) }, + ipfs(state) { + return Object.values(state.ipfs) + }, lsk(state) { return Object.values(state.lsk) }, diff --git a/src/store/modules/nodes/nodes-mutations.js b/src/store/modules/nodes/nodes-mutations.js index 8c747c815..bfec4e219 100644 --- a/src/store/modules/nodes/nodes-mutations.js +++ b/src/store/modules/nodes/nodes-mutations.js @@ -5,6 +5,9 @@ export default { useFastestCoinNode(state, value) { state.useFastestCoinNode = value }, + useFastestIpfsNode(state, value) { + state.useFastestIpfsNode = value + }, toggle(state, payload) { const node = state[payload.type][payload.url] diff --git a/src/store/modules/nodes/nodes-plugin.js b/src/store/modules/nodes/nodes-plugin.js index 1ce0cb4b6..f63766585 100644 --- a/src/store/modules/nodes/nodes-plugin.js +++ b/src/store/modules/nodes/nodes-plugin.js @@ -10,6 +10,7 @@ export default (store) => { } store.commit('nodes/useFastestAdmNode', nodes.adm.useFastest) store.commit('nodes/useFastestCoinNode', nodes.btc.useFastest) + store.commit('nodes/useFastestIpfsNode', nodes.ipfs.useFastest) store.subscribe((mutation) => { const { type, payload } = mutation @@ -26,6 +27,10 @@ export default (store) => { nodes.lsk.setUseFastest(!!payload) } + if (type === 'nodes/useFastestIpfsNode') { + nodes.ipfs.setUseFastest(!!payload) + } + if (type === 'nodes/toggle') { const selectedNodeType = payload.type const newStatus = nodes[selectedNodeType].toggleNode(payload.url, payload.active) diff --git a/src/store/modules/nodes/nodes-state.js b/src/store/modules/nodes/nodes-state.js index ac1dc1535..b082aabec 100644 --- a/src/store/modules/nodes/nodes-state.js +++ b/src/store/modules/nodes/nodes-state.js @@ -4,7 +4,9 @@ export default { btc: {}, doge: {}, dash: {}, + ipfs: {}, lsk: {}, useFastestAdmNode: false, - useFastestCoinNode: true + useFastestCoinNode: true, + useFastestIpfsNode: true } From d8019980d6e4cc963f256b466525482900039861 Mon Sep 17 00:00:00 2001 From: Yulia Hermak Date: Fri, 26 Apr 2024 00:09:50 +0100 Subject: [PATCH 018/111] fix: number of files in chat preview and quoted message --- src/components/AChat/AChat.vue | 9 --------- src/components/AChat/AChatAttachment.vue | 3 +-- src/components/AChat/AChatFile.vue | 3 --- src/components/AChat/QuotedMessage.vue | 5 +++++ src/components/ChatPreview.vue | 16 ++++++++++++++-- src/locales/en.json | 1 + src/locales/ru.json | 1 + 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/AChat/AChat.vue b/src/components/AChat/AChat.vue index 117282d76..8320387a8 100644 --- a/src/components/AChat/AChat.vue +++ b/src/components/AChat/AChat.vue @@ -32,7 +32,6 @@ - @@ -44,7 +43,6 @@ + diff --git a/src/components/AChat/AChatFile.vue b/src/components/AChat/AChatFile.vue index b075fba23..e0752d753 100644 --- a/src/components/AChat/AChatFile.vue +++ b/src/components/AChat/AChatFile.vue @@ -1,13 +1,11 @@ export default { props: { - accept: String + accept: String, + partnerId: { + type: String, + default: '' + } }, emits: ['image-selected'], methods: { uploadFile(event) { const selectedFiles = event.target.files + console.log('files', selectedFiles) if (selectedFiles.length > 0) { for (const file of selectedFiles) { @@ -29,6 +34,13 @@ export default { isImage: file.type.includes('image/') } this.$emit('image-selected', imageData) + const publicKey = this.$store.getters.publicKey(this.partnerId) + console.log(this.partnerId) + console.log(publicKey) + this.$store.dispatch('attachment/uploadAttachment', { + file, + publicKey + }) } reader.readAsDataURL(file) } diff --git a/src/lib/attachment-api/index.ts b/src/lib/attachment-api/index.ts index 920ad27ff..4bcdd5f9e 100644 --- a/src/lib/attachment-api/index.ts +++ b/src/lib/attachment-api/index.ts @@ -22,6 +22,9 @@ export class AttachmentApi { const { cids } = await ipfs.post(`api/file/upload`, formData, { 'Content-Type': 'multipart/form-data' }) + + console.log('File:', file) + console.log('Public key:', publicKey) return { cids, nonce } } } diff --git a/src/store/modules/attachment/index.ts b/src/store/modules/attachment/index.ts index 041f1f527..ad1a26869 100644 --- a/src/store/modules/attachment/index.ts +++ b/src/store/modules/attachment/index.ts @@ -2,6 +2,7 @@ import { MutationTree, GetterTree, ActionTree } from 'vuex' import { RootState } from '@/store/types' import { AttachmentsState } from '@/store/modules/attachment/types.ts' import { AttachmentApi } from '@/lib/attachment-api' +import { store } from '@/store' const state = (): AttachmentsState => ({ attachments: {} @@ -13,6 +14,11 @@ const mutations: MutationTree = { state.attachments = attachments }, + setAttachment(state, { сid, url }) { + state.attachments = { ...state.attachments, [сid]: url } + console.log(state.attachments) + }, + reset(state) { state.attachments = {} } @@ -36,16 +42,39 @@ const actions: ActionTree = { } }, getAttachment( - state, + _context, { cid, publicKey, nonce }: { cid: string; publicKey: Uint8Array; nonce: string } ) { return attachmentApi?.getFile(cid, nonce, publicKey) }, - uploadAttachment( + async getAttachmentUrl( state, { cid, publicKey, nonce }: { cid: string; publicKey: Uint8Array; nonce: string } ) { - return attachmentApi?.getFile(cid, nonce, publicKey) + console.log(state.state) + if (state.state.attachments[cid]) { + return state.state.attachments[cid] + } else { + try { + const fileData = await attachmentApi?.getFile(cid, nonce, publicKey) + if (!fileData) { + throw new Error('Failed to fetch image') + } + + const blob = new Blob([fileData], { type: 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + if (fileData !== undefined) { + store.commit('setAttachment', { cid, url }) + } + return url + } catch (error) { + console.error('Error fetching image:', error) + throw error + } + } + }, + async uploadAttachment(state, { file, publicKey }: { file: Uint8Array; publicKey: Uint8Array }) { + return attachmentApi?.uploadFile(file, publicKey) }, resetState(context) { context.commit('reset') From 95a6cc95791fbe2390fc88581229da5dc5725ba6 Mon Sep 17 00:00:00 2001 From: bludnic Date: Mon, 3 Jun 2024 22:29:14 +0100 Subject: [PATCH 027/111] refactor: remove legacy README.md --- src/components/AChat/README.md | 283 --------------------------------- 1 file changed, 283 deletions(-) delete mode 100644 src/components/AChat/README.md diff --git a/src/components/AChat/README.md b/src/components/AChat/README.md deleted file mode 100644 index 65631b931..000000000 --- a/src/components/AChat/README.md +++ /dev/null @@ -1,283 +0,0 @@ -# Not used file - -## Adamant chat - -A chat built with Vuetify. - -## Installation - -```shell -# yarn -yarn add file:packages/chat -``` - -```shell -# npm -npm i --save file:packages/chat -``` - -## Usage - -```vue - - - -``` - -## Types - -```ts -export type Message = { - id: number - senderId: string - message: string - timestamp: number - admTimestamp: number - amount: number - i18n: boolean - status: MessageStatus - type: MessageType -} - -export enum MessageType { - Message = 'message', - ADM = 'ADM', - ETH = 'ETH' -} - -export enum MessageStatus { - sent, - confirmed, - rejected -} - -export type User = { - id: string - name?: string -} -``` - -## AChat.vue - -### Props - -| Name | Type | Default | Description | -| :-----------------: | :---------: | :---------: | :------------------------------------------------------- | -| **[`messages`](#)** | `Message[]` | `[]` | Array of messages | -| **[`partners`](#)** | `User[]` | `[]` | Array of users who participate in chat (including owner) | -| **[`user-id`](#)** | `string` | `undefined` | Owner ID | -| **[`loading`](#)** | `boolean` | `false` | Show spinner | -| **[`locale`](#)** | `string` | `'en'` | Moment.js locale | - -### Events - -| Name | Arguments | Description | -| :-----------------------: | :-------: | :-------------------------------------------------------------------------------- | -| **[`@scroll:top`](#)** | | When user scrolled top of messages. Use this event to fetch messages from history | -| **[`@scroll:bottom`](#)** | | When scrolled bottom of messages | - -### Slots - -| Name | Props | Description | -| :-----------------: | :-------------------------------------------------------------------: | :----------------- | --- | -| **[`messages`](#)** | `{ messages: Message[] }` | | -| **[`message`](#)** | `{ message: Message, sender: User, userId: string], locale: string }` | | -| **[`header`](#)** | | Chat toolbar | | -| **[`form`](#)** | | Input message form | | - -### Methods - -| Name | Params | Description | -| :--------------------------------: | :----: | :------------------------------------------------------------------- | -| **[`@scrollToBottom`](#)** | | Move scrollbar-thumb to the bottom. Use this after push new message. | -| **[`@maintainScrollPosition`](#)** | | Fix scroll position after unshift new messages | - -## AChatMessage.vue - -### Props - -| Name | Type | Default | Description | -| :--------------------: | :----------------: | :-----------: | :--------------------------------------------------------- | -| **[`id`](#)** | `{string\|number}` | `undefined` | Message ID | -| **[`message`](#)** | `string` | `''` | Message text | -| **[`timestamp`](#)** | `number` | `0` | Message timestamp | -| **[`status`](#)** | `MessageStatus` | `'confirmed'` | Message status. Can be 'sent', 'CONFIRMED', 'REJECTED'. | -| **[`user-id`](#)** | `string` | `''` | Chat owner | -| **[`sender`](#)** | `User` | `undefined` | Can be accessed from `props.message.sender` of `AChat.vue` | -| **[`show-avatar`](#)** | `boolean` | `true` | Display user avatars in chat | -| **[`locale`](#)** | `string` | `'en'` | Moment.js locale | -| **[`html`](#)** | `boolean` | `false` | Uses `v-html` or `v-text` to display message | - -### Events - -| Name | Arguments | Description | -| :----------------: | :-------: | :----------------------------------- | -| **[`@resend`](#)** | | When user clicked on the resend icon | - -### Slots - -| Name | Props | Description | -| :---------------: | :---: | :------------ | -| **[`avatar`](#)** | | Custom avatar | - -## AChatTransaction.vue - -### Props - -| Name | Type | Default | Description | -| :------------------: | :-------------------------: | :------------------------------------: | :--------------------------------------------------------- | -| **[`id`](#)** | `{string\|number}` | `undefined` | Transaction ID | -| **[`message`](#)** | `string` | `''` | Transaction text | -| **[`timestamp`](#)** | `number` | `0` | Transaction timestamp | -| **[`user-id`](#)** | `string` | `''` | Chat owner | -| **[`sender`](#)** | `User` | `undefined` | Can be accessed from `props.message.sender` of `AChat.vue` | -| **[`amount`](#)** | `number` | 0 | Crypto amount | -| **[`currency`](#)** | `string` | 'ADM' | Crypto currency | -| **[`i18n`](#)** | `{ [key: string]: string }` | { sent: 'Sent', received: 'Received' } | Transaction localization | -| **[`locale`](#)** | `string` | `'en'` | Moment.js locale | - -### Events - -| Name | Arguments | Description | -| :---------------------------: | :-----------------------: | :------------------------ | -| **[`@click:transaction`](#)** | `(transactionId: string)` | On click transaction icon | -| **[`@mount`](#)** | | Emit mount when mounted | - -## AChatForm.vue - -### Props - -| Name | Type | Default | Description | -| :-----------------------: | :-------: | :--------------: | :------------------------------ | -| **[`showSendButton`](#)** | `boolean` | `true` | Show send button | -| **[`showDivider`](#)** | `boolean` | `false` | Show divider on top of the form | -| **[`sendOnEnter`](#)** | `boolean` | `true` | Send message on enter | -| **[`label`](#)** | `string` | `Type a message` | Input message placeholder | - -### Events - -| Name | Arguments | Description | -| :-----------------: | :-----------------: | :---------------------------- | -| **[`@message`](#)** | `(message: string)` | Called when sending a message | From 3af78a5a689eef8978eeadd2c274618744268dda Mon Sep 17 00:00:00 2001 From: RealGoodProgrammer Date: Sat, 13 Jul 2024 21:18:57 +0300 Subject: [PATCH 028/111] Update src/components/nodes/ipfs/IpfsNodesTable.vue Co-authored-by: bludnic --- src/components/nodes/ipfs/IpfsNodesTable.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/nodes/ipfs/IpfsNodesTable.vue b/src/components/nodes/ipfs/IpfsNodesTable.vue index 9c3643748..277279b84 100644 --- a/src/components/nodes/ipfs/IpfsNodesTable.vue +++ b/src/components/nodes/ipfs/IpfsNodesTable.vue @@ -16,7 +16,7 @@ import NodesTableHead from '@/components/nodes/components/NodesTableHead.vue' import IpfsNodesTableItem from './IpfsNodesTableItem.vue' import { sortNodesFn } from '@/components/nodes/utils/sortNodesFn' -const className = 'adm-nodes-table' +const className = 'ipfs-nodes-table' const classes = { root: className } From 0420727b356f0dd459fab36ff5b0e4c58b278011 Mon Sep 17 00:00:00 2001 From: bludnic Date: Wed, 11 Sep 2024 18:48:28 +0100 Subject: [PATCH 029/111] fix(AChatAttachment): fix status icon --- src/components/AChat/AChatAttachment.vue | 12 +++++------- src/components/Chat/Chat.vue | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/AChat/AChatAttachment.vue b/src/components/AChat/AChatAttachment.vue index 9d0d06dfd..f2af8b675 100644 --- a/src/components/AChat/AChatAttachment.vue +++ b/src/components/AChat/AChatAttachment.vue @@ -32,13 +32,15 @@

-
+
+ ⚭ +
{{ time }}
!isWelcomeChat(partnerId.value)) - const statusIcon = computed(() => tsIcon(props.status.virtualStatus)) + const statusIcon = computed(() => tsIcon(props.transaction.status)) const isOutgoingMessage = computed(() => isStringEqualCI(props.transaction.senderId, userId.value) ) diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index a1ec3148d..28217d001 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -143,7 +143,6 @@ Date: Wed, 11 Sep 2024 18:53:23 +0100 Subject: [PATCH 030/111] fix(vuex): commit is not defined in attachment module --- src/store/modules/attachment/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/store/modules/attachment/index.ts b/src/store/modules/attachment/index.ts index ad1a26869..f6c92ee72 100644 --- a/src/store/modules/attachment/index.ts +++ b/src/store/modules/attachment/index.ts @@ -48,12 +48,11 @@ const actions: ActionTree = { return attachmentApi?.getFile(cid, nonce, publicKey) }, async getAttachmentUrl( - state, + { state, commit }, { cid, publicKey, nonce }: { cid: string; publicKey: Uint8Array; nonce: string } ) { - console.log(state.state) - if (state.state.attachments[cid]) { - return state.state.attachments[cid] + if (state.attachments[cid]) { + return state.attachments[cid] } else { try { const fileData = await attachmentApi?.getFile(cid, nonce, publicKey) @@ -64,7 +63,7 @@ const actions: ActionTree = { const blob = new Blob([fileData], { type: 'application/octet-stream' }) const url = URL.createObjectURL(blob) if (fileData !== undefined) { - store.commit('setAttachment', { cid, url }) + commit('setAttachment', { cid, url }) } return url } catch (error) { From 12d06d8694351d1ceaaf50218059dbdc166fb1ca Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 13 Sep 2024 21:40:43 +0100 Subject: [PATCH 031/111] fix(UploadFile.vue): convert File to Uint8Array and refactor to TS --- src/components/UploadFile.vue | 61 +++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/src/components/UploadFile.vue b/src/components/UploadFile.vue index cc854fe1d..8b85c65ad 100644 --- a/src/components/UploadFile.vue +++ b/src/components/UploadFile.vue @@ -8,44 +8,51 @@ @change="uploadFile" /> - From 65cdac4bae1fc70f51774876f753dc9ec24e4980 Mon Sep 17 00:00:00 2001 From: bludnic Date: Sat, 14 Sep 2024 03:09:18 +0100 Subject: [PATCH 032/111] refactor(Chat.vue): refactor to Composition API --- src/components/Chat/Chat.vue | 869 +++++++++++++++++------------------ 1 file changed, 412 insertions(+), 457 deletions(-) diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 28217d001..ab09f76d5 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -2,7 +2,7 @@ @@ -271,7 +271,7 @@ depressed fab size="small" - @click="$refs.chat.scrollToBottom()" + @click="chatRef.scrollToBottom()" > @@ -282,12 +282,13 @@ - From 603e47952c136cef837f873e94de045162171acd Mon Sep 17 00:00:00 2001 From: bludnic Date: Sat, 14 Sep 2024 03:34:26 +0100 Subject: [PATCH 033/111] fix(UploadFile): remove extra symbols --- src/components/UploadFile.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UploadFile.vue b/src/components/UploadFile.vue index 8b85c65ad..96bd61c7b 100644 --- a/src/components/UploadFile.vue +++ b/src/components/UploadFile.vue @@ -8,7 +8,7 @@ @change="uploadFile" /> -x( + + + diff --git a/src/components/AChat/AChatImageModal.vue b/src/components/AChat/AChatAttachment/AChatImageModal.vue similarity index 100% rename from src/components/AChat/AChatImageModal.vue rename to src/components/AChat/AChatAttachment/AChatImageModal.vue diff --git a/src/components/AChat/AChatImageModalItem.vue b/src/components/AChat/AChatAttachment/AChatImageModalItem.vue similarity index 100% rename from src/components/AChat/AChatImageModalItem.vue rename to src/components/AChat/AChatAttachment/AChatImageModalItem.vue diff --git a/src/components/AChat/AChatAttachment/ImageLayout.vue b/src/components/AChat/AChatAttachment/ImageLayout.vue new file mode 100644 index 000000000..48cd73165 --- /dev/null +++ b/src/components/AChat/AChatAttachment/ImageLayout.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/components/AChat/AChatFile.vue b/src/components/AChat/AChatFile.vue deleted file mode 100644 index f2c2cf21d..000000000 --- a/src/components/AChat/AChatFile.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - - - diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 0613a8353..2c90064e3 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -314,7 +314,7 @@ import FreeTokensDialog from '@/components/FreeTokensDialog.vue' import { isMobile } from '@/lib/display-mobile' import { isAdamantChat, isWelcomeChat, isWelcomeMessage } from '@/lib/chat/meta/utils' import ProgressIndicator from '@/components/ProgressIndicator.vue' -import AChatAttachment from '@/components/AChat/AChatAttachment.vue' +import AChatAttachment from '@/components/AChat/AChatAttachment/AChatAttachment.vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import { useStore } from 'vuex' From fb69d483a808d9b6d7f2d632214d70ed349f1a69 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 27 Sep 2024 16:47:21 +0100 Subject: [PATCH 066/111] feat(AChatAttachment): add inline layout --- .../AChat/AChatAttachment/AChatAttachment.vue | 33 +++- .../AChat/AChatAttachment/AChatFile.vue | 161 ++++++++++++++++++ .../AChat/AChatAttachment/AChatImage.vue | 7 +- .../AChat/AChatAttachment/InlineLayout.vue | 59 +++++++ 4 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 src/components/AChat/AChatAttachment/AChatFile.vue create mode 100644 src/components/AChat/AChatAttachment/InlineLayout.vue diff --git a/src/components/AChat/AChatAttachment/AChatAttachment.vue b/src/components/AChat/AChatAttachment/AChatAttachment.vue index 2bc141438..24da662f1 100644 --- a/src/components/AChat/AChatAttachment/AChatAttachment.vue +++ b/src/components/AChat/AChatAttachment/AChatAttachment.vue @@ -78,11 +78,20 @@
+ + + + diff --git a/src/components/AChat/AChatAttachment/AChatImage.vue b/src/components/AChat/AChatAttachment/AChatImage.vue index 005f240bb..e99782c8d 100644 --- a/src/components/AChat/AChatAttachment/AChatImage.vue +++ b/src/components/AChat/AChatAttachment/AChatImage.vue @@ -18,10 +18,9 @@ import { AChatFileLoader } from './AChatFileLoader.tsx' import { LocalFile, NormalizedChatMessageTransaction } from '@/lib/chat/helpers' import { FileAsset } from '@/lib/adamant-api/asset' -const className = 'a-chat-file' +const className = 'a-chat-image' const classes = { - root: className, - img: `${className}__img` + root: className } export default defineComponent({ @@ -52,7 +51,7 @@ export default defineComponent({ @import '@/assets/styles/settings/_colors.scss'; @import '@/assets/styles/themes/adamant/_mixins.scss'; -.a-chat-file { +.a-chat-image { } .v-theme--light { diff --git a/src/components/AChat/AChatAttachment/InlineLayout.vue b/src/components/AChat/AChatAttachment/InlineLayout.vue new file mode 100644 index 000000000..ad7f65bd1 --- /dev/null +++ b/src/components/AChat/AChatAttachment/InlineLayout.vue @@ -0,0 +1,59 @@ + + + + + From 6f3b964dfde38645b245bfc654a290d2d01676c8 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 27 Sep 2024 17:18:55 +0100 Subject: [PATCH 067/111] fix(Chat): attachment does not require a message --- src/components/Chat/Chat.vue | 57 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 2c90064e3..4b8ff5d0d 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -325,31 +325,6 @@ const validationErrors = { notEnoughFundsNewAccount: 'NON_ENOUGH_FUNDS_NEW_ACCOUNT', messageTooLong: 'MESSAGE_LENGTH_EXCEED' } -/** - * Validate message before sending. - * @param message - * @returns If `false` then validation passed without errors. - */ -function validateMessage(message: string): string | false { - // Ensure that message contains at least one non-whitespace character - if (!message.trim().length) { - return validationErrors.emptyMessage - } - - if (store.state.balance < Fees.NOT_ADM_TRANSFER) { - if (store.getters.isAccountNew()) { - return validationErrors.notEnoughFundsNewAccount - } else { - return validationErrors.notEnoughFunds - } - } - - if (message.length * 1.5 > 20000) { - return validationErrors.messageTooLong - } - - return false -} const props = defineProps({ partnerId: { @@ -378,6 +353,7 @@ const showEmojiPicker = ref(false) const messages = computed(() => store.getters['chat/messages'](props.partnerId)) const userId = computed(() => store.state.address) +const hasAttachment = computed(() => files.value.length > 0) const getPartnerName = (address: string) => { const name: string = store.getters['partners/displayName'](address) || '' @@ -466,6 +442,37 @@ onBeforeUnmount(() => { Visibility.unbind(Number(visibilityId.value)) }) +/** + * Validate message before sending. + * @param message + * @returns If `false` then validation passed without errors. + */ +function validateMessage(message: string): string | false { + if (hasAttachment.value) { + // When attaching files, the message is not mandatory + return false + } + + // Ensure that message contains at least one non-whitespace character + if (!message.trim().length) { + return validationErrors.emptyMessage + } + + if (store.state.balance < Fees.NOT_ADM_TRANSFER) { + if (store.getters.isAccountNew()) { + return validationErrors.notEnoughFundsNewAccount + } else { + return validationErrors.notEnoughFunds + } + } + + if (message.length * 1.5 > 20000) { + return validationErrors.messageTooLong + } + + return false +} + const onMessage = (message: string) => { sendMessage(message) nextTick(() => chatRef.value.scrollToBottom()) From a72366abb2d1d7d6bfda89b3874f091980e70c09 Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 27 Sep 2024 17:32:51 +0100 Subject: [PATCH 068/111] fix(Chat): reset preview attachments after message was send --- src/components/Chat/Chat.vue | 12 ++++++++++-- src/components/Chat/ChatMenu.vue | 11 ++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 4b8ff5d0d..e3491053b 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -291,7 +291,7 @@ import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, wat import Visibility from 'visibilityjs' import copyToClipboard from 'copy-to-clipboard' -import { Cryptos, Fees } from '@/lib/constants' +import { Cryptos, Fees, UPLOAD_MAX_FILE_COUNT } from '@/lib/constants' import EmojiPicker from '@/components/EmojiPicker.vue' import { @@ -477,9 +477,17 @@ const onMessage = (message: string) => { sendMessage(message) nextTick(() => chatRef.value.scrollToBottom()) replyMessageId.value = -1 + files.value = [] } const handleFiles = (filesList: FileData[]) => { - files.value = filesList + files.value = [...files.value, ...filesList] + + if (files.value.length > UPLOAD_MAX_FILE_COUNT) { + files.value = files.value.slice(0, UPLOAD_MAX_FILE_COUNT) + store.dispatch('snackbar/show', { + message: t('chats.max_files') + }) + } } const removeItem = (index: number) => { files.value = files.value.filter((_, i) => i !== index) diff --git a/src/components/Chat/ChatMenu.vue b/src/components/Chat/ChatMenu.vue index da133321c..0f47d9489 100644 --- a/src/components/Chat/ChatMenu.vue +++ b/src/components/Chat/ChatMenu.vue @@ -52,7 +52,7 @@ + From cd770dd3350966807588e6dbdbe6b4dd100efcf3 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 10 Oct 2024 21:42:03 +0100 Subject: [PATCH 078/111] feat(AChatImageModal): close slider when click outside --- .../AChat/AChatAttachment/AChatImageModal.vue | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/AChat/AChatAttachment/AChatImageModal.vue b/src/components/AChat/AChatAttachment/AChatImageModal.vue index ee89101b7..2863be2c4 100644 --- a/src/components/AChat/AChatAttachment/AChatImageModal.vue +++ b/src/components/AChat/AChatAttachment/AChatImageModal.vue @@ -9,7 +9,13 @@ - + { + const clickedOutside = (e.target as HTMLElement)?.classList?.contains('v-window-item') + + if (clickedOutside) { + emit('close') + } + } + return { slide, show, @@ -124,6 +138,7 @@ export default { prevSlide, nextSlide, handleKeydown, + handleClick, classes } } From cc5537c79769db2f5c375ff303914de1b528f66c Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 10 Oct 2024 22:16:38 +0100 Subject: [PATCH 079/111] refactor(AChatImageModalItem): fix types --- .../AChatAttachment/AChatImageModalItem.vue | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/src/components/AChat/AChatAttachment/AChatImageModalItem.vue b/src/components/AChat/AChatAttachment/AChatImageModalItem.vue index 4223dfbdd..c9ef5bed5 100644 --- a/src/components/AChat/AChatAttachment/AChatImageModalItem.vue +++ b/src/components/AChat/AChatAttachment/AChatImageModalItem.vue @@ -6,13 +6,9 @@ import { defineComponent, PropType, ref, onMounted, computed } from 'vue' import { useStore } from 'vuex' -import { LocalFile, NormalizedChatMessageTransaction } from '@/lib/chat/helpers' +import { NormalizedChatMessageTransaction } from '@/lib/chat/helpers' import { FileAsset } from '@/lib/adamant-api/asset' -function isLocalFile(file: FileAsset | LocalFile): file is LocalFile { - return 'file' in file && file.file?.file instanceof File -} - const className = 'a-chat-image-modal-item' const classes = { root: className @@ -21,7 +17,7 @@ const classes = { export default defineComponent({ props: { file: { - type: Object as PropType, + type: Object as PropType, required: true }, transaction: { @@ -33,42 +29,22 @@ export default defineComponent({ const store = useStore() const imageUrl = ref('') - const width = computed(() => { - if (isLocalFile(props.file)) { - return props.file.file.width - } else { - return props.file.resolution?.[0] - } - }) - const height = computed(() => { - if (isLocalFile(props.file)) { - return props.file.file.height - } else { - return props.file.resolution?.[1] - } - }) + const width = computed(() => props.file.resolution?.[0]) + const height = computed(() => props.file.resolution?.[1]) + const publicKey = computed(() => + props.transaction.senderId === store.state.address + ? props.transaction.recipientPublicKey + : props.transaction.senderPublicKey + ) const getFileFromStorage = async () => { - const myAddress = store.state.address - - const cid = props.file?.id - const fileName = props.file?.name - const fileType = props.file?.type - const nonce = props.file?.nonce + const { id, nonce } = props.file - const publicKey = - props.transaction.senderId === myAddress - ? props.transaction.recipientPublicKey - : props.transaction.senderPublicKey imageUrl.value = await store.dispatch('attachment/getAttachmentUrl', { - cid, - publicKey, + cid: id, + publicKey: publicKey.value, nonce }) - if (!!fileName && !!fileType) { - // TODO: resolve MIME-type - // downloadFile(data, fileName, '') - } } onMounted(() => { From 373510f99042085c32ffb69f075e53f0f9814f7c Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 10 Oct 2024 22:26:19 +0100 Subject: [PATCH 080/111] fix(AChatImageModal): fix `files` prop type --- src/components/AChat/AChatAttachment/AChatImageModal.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AChat/AChatAttachment/AChatImageModal.vue b/src/components/AChat/AChatAttachment/AChatImageModal.vue index 2863be2c4..30e8383c1 100644 --- a/src/components/AChat/AChatAttachment/AChatImageModal.vue +++ b/src/components/AChat/AChatAttachment/AChatImageModal.vue @@ -43,7 +43,7 @@ import { FileAsset } from '@/lib/adamant-api/asset' import { ref, computed, onMounted, PropType } from 'vue' import AChatImageModalItem from './AChatImageModalItem.vue' -import { LocalFile, NormalizedChatMessageTransaction } from '@/lib/chat/helpers' +import { NormalizedChatMessageTransaction } from '@/lib/chat/helpers' const className = 'a-chat-image-modal' const classes = { @@ -62,7 +62,7 @@ export default { required: true }, files: { - type: Array as PropType>, + type: Array as PropType>, required: true }, /** From 1fd6b26e1dda45cd8e46e8f4f636dc2aa4146518 Mon Sep 17 00:00:00 2001 From: bludnic Date: Thu, 10 Oct 2024 22:40:38 +0100 Subject: [PATCH 081/111] fix(AChatImageModal): download file by URL --- .../AChat/AChatAttachment/AChatImageModal.vue | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/components/AChat/AChatAttachment/AChatImageModal.vue b/src/components/AChat/AChatAttachment/AChatImageModal.vue index 30e8383c1..f78663358 100644 --- a/src/components/AChat/AChatAttachment/AChatImageModal.vue +++ b/src/components/AChat/AChatAttachment/AChatImageModal.vue @@ -6,7 +6,7 @@
{{ slide + 1 }} of {{ files.length }}
- + import { FileAsset } from '@/lib/adamant-api/asset' import { ref, computed, onMounted, PropType } from 'vue' +import { useStore } from 'vuex' import AChatImageModalItem from './AChatImageModalItem.vue' import { NormalizedChatMessageTransaction } from '@/lib/chat/helpers' +function downloadFileByUrl(url: string, filename = 'unnamed') { + const anchor = document.createElement('a') + anchor.href = url + anchor.download = filename + + document.body.appendChild(anchor) + anchor.click() + + document.body.removeChild(anchor) +} + const className = 'a-chat-image-modal' const classes = { root: className, @@ -85,6 +97,7 @@ export default { }, emits: ['close', 'update:modal'], setup(props, { emit }) { + const store = useStore() const slide = ref(0) onMounted(() => { @@ -131,6 +144,31 @@ export default { } } + const publicKey = computed(() => + props.transaction.senderId === store.state.address + ? props.transaction.recipientPublicKey + : props.transaction.senderPublicKey + ) + const downloadImage = async () => { + const file = props.files[slide.value] + if (!file) { + console.warn( + `Failed to download the file. Reason: The file with index ${slide.value} does not exist` + ) + return + } + + const { id, nonce } = file + const imageUrl = await store.dispatch('attachment/getAttachmentUrl', { + cid: id, + publicKey: publicKey.value, + nonce + }) + const fileName = file.name ? `${file.name}.${file.extension}` : undefined + + downloadFileByUrl(imageUrl, fileName) + } + return { slide, show, @@ -139,6 +177,7 @@ export default { nextSlide, handleKeydown, handleClick, + downloadImage, classes } } From 2add32b072601d20a1397d8b24035ea0cf3f41aa Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 11 Oct 2024 00:02:25 +0100 Subject: [PATCH 082/111] feat(ImageLayout): improve image grid layout --- .../AChat/AChatAttachment/ImageLayout.vue | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/AChat/AChatAttachment/ImageLayout.vue b/src/components/AChat/AChatAttachment/ImageLayout.vue index 48cd73165..60ac47dcd 100644 --- a/src/components/AChat/AChatAttachment/ImageLayout.vue +++ b/src/components/AChat/AChatAttachment/ImageLayout.vue @@ -80,15 +80,42 @@ export default defineComponent({ From 46b0d8d307bead400735797ddf6eeecb4263eced Mon Sep 17 00:00:00 2001 From: bludnic Date: Fri, 11 Oct 2024 00:24:59 +0100 Subject: [PATCH 083/111] feat(AChatImage): show error icon when failed to load the image --- .../AChat/AChatAttachment/AChatImage.vue | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/AChat/AChatAttachment/AChatImage.vue b/src/components/AChat/AChatAttachment/AChatImage.vue index 5b88d1e36..e90ce3439 100644 --- a/src/components/AChat/AChatAttachment/AChatImage.vue +++ b/src/components/AChat/AChatAttachment/AChatImage.vue @@ -2,7 +2,15 @@