From d0546b66fde74e2e3f2c64280403a13fb18b6c6d Mon Sep 17 00:00:00 2001 From: skranee <125743202+skranee@users.noreply.github.com> Date: Sun, 6 Apr 2025 23:01:20 +0300 Subject: [PATCH 01/18] feat: starting a new chat placholder init --- src/assets/styles/components/_chat.scss | 1 + src/components/AChat/AChat.vue | 7 +++ src/components/Chat/Chat.vue | 21 +++++++- src/components/Chat/ChatPlaceholder.vue | 71 +++++++++++++++++++++++++ src/locales/en.json | 6 +++ src/locales/ru.json | 6 +++ 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/components/Chat/ChatPlaceholder.vue diff --git a/src/assets/styles/components/_chat.scss b/src/assets/styles/components/_chat.scss index da7f78afd..232da69d8 100644 --- a/src/assets/styles/components/_chat.scss +++ b/src/assets/styles/components/_chat.scss @@ -52,6 +52,7 @@ $scroll-bar-width: 4px; left: 0; overflow-y: scroll; overflow-x: hidden; + scrollbar-gutter: stable both-edges; width: 100%; height: 100%; padding: 16px 16px 0 16px; diff --git a/src/components/AChat/AChat.vue b/src/components/AChat/AChat.vue index 278c34350..2f6495865 100644 --- a/src/components/AChat/AChat.vue +++ b/src/components/AChat/AChat.vue @@ -17,6 +17,9 @@
+
+ +
+ + - diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 8d9dacdc0..4654fe519 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -431,14 +431,12 @@ onBeforeMount(() => { onMounted(async () => { if (isFulfilled.value && chatPage.value <= 0) await fetchChatMessages() - if (chatPage.value) { - const userMessages = messages.value.filter( - (message: NormalizedChatMessageTransaction) => - message.senderId && message.senderId === userId.value - ) + const userMessages = messages.value.filter( + (message: NormalizedChatMessageTransaction) => + message.senderId && message.senderId === userId.value + ) - showNewChatPlaceholder.value = !userMessages.length - } + showNewChatPlaceholder.value = !userMessages.length scrollBehavior() nextTick(() => { diff --git a/src/components/Chat/ChatPlaceholder.vue b/src/components/Chat/ChatPlaceholder.vue index 9e89ba2cf..8d0b69683 100644 --- a/src/components/Chat/ChatPlaceholder.vue +++ b/src/components/Chat/ChatPlaceholder.vue @@ -3,7 +3,7 @@

- {{ $t(`chats.placeholder.${key}`) }} + {{ t(`chats.placeholder.${key}`) }}

@@ -11,6 +11,9 @@ @@ -24,6 +39,9 @@ const keys = ['encrypted', 'ipfs', 'anonymous', 'censorship'] @use '@/assets/styles/settings/_colors.scss'; .chat-placeholder { + display: flex; + flex-direction: column; + row-gap: 10px; width: 100%; padding: 8px 0; margin: 16px 0; @@ -39,6 +57,11 @@ const keys = ['encrypted', 'ipfs', 'anonymous', 'censorship'] padding: 16px; background: map.get(colors.$adm-colors, 'black'); border-radius: 8px; + + &_public-key { + flex-direction: row; + column-gap: 8px; + } } &__row { @@ -57,6 +80,10 @@ const keys = ['encrypted', 'ipfs', 'anonymous', 'censorship'] 0 1px 1px hsla(0, 0%, 39.2%, 0.04), 0 2px 10px -1px hsla(0, 0%, 39.2%, 0.02); } + + &__spinner { + color: map.get(colors.$adm-colors, 'grey'); + } } } @@ -69,6 +96,10 @@ const keys = ['encrypted', 'ipfs', 'anonymous', 'censorship'] 0 1px 1px hsla(0, 0%, 39.2%, 0.04), 0 2px 10px -1px hsla(0, 0%, 39.2%, 0.02); } + + &__spinner { + color: map.get(colors.$adm-colors, 'regular'); + } } } diff --git a/src/components/ChatStartDialog.vue b/src/components/ChatStartDialog.vue index e5c37ca0a..3a6d70ec1 100644 --- a/src/components/ChatStartDialog.vue +++ b/src/components/ChatStartDialog.vue @@ -143,21 +143,6 @@ export default { true, this.recipientName ) - - // return this.$store - // .dispatch('chat/createChat', { - // partnerId: this.recipientAddress, - // partnerName: this.recipientName - // }) - // .then((_publicKey) => { - // this.$emit('start-chat', this.recipientAddress, this.uriMessage) - // this.show = false - // }) - // .catch((err) => { - // this.$store.dispatch('snackbar/show', { - // message: err.message // @todo translations - // }) - // }) }, /** diff --git a/src/locales/en.json b/src/locales/en.json index c2c199dae..58df71db4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -34,10 +34,11 @@ "partner_info": "Partner info", "partner_name": "Partner name", "placeholder": { + "anonymous": "\uD83D\uDD76\uFE0F Anonymous: partners never see your IP address", + "censorship": "⛓\uFE0F Censorship-resistant blockchain architecture", "encrypted": "\uD83D\uDD10 End-to-end encrypted across all devices", "ipfs": "☁\uFE0F Decentralized IPFS storage for files/media", - "anonymous": "\uD83D\uDD76\uFE0F Anonymous: partners never see your IP address", - "censorship": "⛓\uFE0F Censorship-resistant blockchain architecture" + "public-key": "Retrieving your partner’s key to encrypt messages…" }, "received_label": "Received", "recipient": "Partner address", diff --git a/src/locales/ru.json b/src/locales/ru.json index 7a08b5548..ef62e43b5 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -34,10 +34,11 @@ "partner_info": "Собеседник", "partner_name": "Имя собеседника", "placeholder": { + "anonymous": "\uD83D\uDD76\uFE0F Анонимно: ваш IP-адрес скрыт от собеседников", + "censorship": "⛓\uFE0F Устойчивая к цензуре блокчейн-архитектура", "encrypted": "\uD83D\uDD10 Сквозное шифрование на всех устройствах", "ipfs": "☁\uFE0F Распределенное IPFS-хранилище файлов и медиа", - "anonymous": "\uD83D\uDD76\uFE0F Анонимно: ваш IP-адрес скрыт от собеседников", - "censorship": "⛓\uFE0F Устойчивая к цензуре блокчейн-архитектура" + "public-key": "Получаю ключ вашего собеседника для шифрования сообщений…" }, "received_label": "Получили", "recipient": "Адрес собеседника", diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index dd9f7ff70..75915e962 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -51,7 +51,8 @@ const state = () => ({ lastMessageHeight: 0, // `height` value of the last message isFulfilled: false, // false - getChats did not start or in progress, true - getChats finished offset: 0, // for loading chat list with pagination. -1 if all of chats loaded - noActiveNodesDialog: undefined // true - visible dialog, false - hidden dialog, but shown before, undefined - not shown + noActiveNodesDialog: undefined, // true - visible dialog, false - hidden dialog, but shown before, undefined - not shown + newChats: {} // { [partnerId]: partnerName }, for pointing if a chat needs further handling after being opened }) const getters = { @@ -319,6 +320,14 @@ const getters = { */ chatListOffset: (state) => { return state.offset + }, + + isNewChat: (state) => (partnerId) => { + return partnerId in state.newChats + }, + + getPartnerName: (state) => (partnerId) => { + return state.newChats[partnerId] } } @@ -373,6 +382,14 @@ const mutations = { state.chats[partnerId] = createChat() }, + addNewChat(state, { partnerId, partnerName }) { + state.newChats[partnerId] = partnerName + }, + + removeNewChat(state, partnerId) { + delete state.newChats[partnerId] + }, + /** * Push an message to a specific chat by senderId. * @param {string} userId Your address diff --git a/src/views/Chats.vue b/src/views/Chats.vue index c194a7f95..63620d0dc 100644 --- a/src/views/Chats.vue +++ b/src/views/Chats.vue @@ -146,31 +146,15 @@ export default { }, methods: { openChat(partnerId, messageText, retrieveKey = false, partnerName) { + if (retrieveKey) { + this.$store.commit('chat/addNewChat', { partnerId, partnerName }) + } + this.$router.push({ name: 'Chat', params: { partnerId }, query: { messageText } }) - - if (retrieveKey) { - this.retrievePublicKey(partnerId, partnerName) - } - }, - retrievePublicKey(partnerId, partnerName) { - this.$store - .dispatch('chat/createChat', { - partnerId, - partnerName - }) - .then((_publicKey) => { - console.log(`Success! Retrieved public key: ${_publicKey}`) - }) - .catch((err) => { - console.log(`Error while retrieving public key: ${err}`) - this.$store.dispatch('snackbar/show', { - message: err.message // @todo translations - }) - }) }, isAdamantChat, getAdamantChatMeta, From b4760785c961d034f86dda510d87bb93c8788559 Mon Sep 17 00:00:00 2001 From: skranee <125743202+skranee@users.noreply.github.com> Date: Thu, 17 Apr 2025 11:39:15 +0300 Subject: [PATCH 07/18] feat: notify if public key is missing --- src/components/AChat/AChatForm.vue | 4 ++-- src/components/Chat/Chat.vue | 15 ++++++++++----- src/components/Chat/ChatPlaceholder.vue | 24 +++++++++++++++++++----- src/locales/en.json | 1 + src/locales/ru.json | 1 + 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/components/AChat/AChatForm.vue b/src/components/AChat/AChatForm.vue index c67e91a32..409d6388d 100644 --- a/src/components/AChat/AChatForm.vue +++ b/src/components/AChat/AChatForm.vue @@ -10,7 +10,7 @@ v-model="message" @input="onInput" :placeholder="label" - :disabled="isInputDisabled" + :disabled="isKeyMissing" hide-details single-line auto-grow @@ -72,7 +72,7 @@ export default { type: String, default: 'Type a message' }, - isInputDisabled: { + isKeyMissing: { type: Boolean, default: false }, diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 05d33242e..855ffd610 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -87,6 +87,7 @@ @@ -201,7 +202,7 @@ :send-on-enter="sendMessageOnEnter" :show-divider="true" :label="t('chats.message')" - :is-input-disabled="isInputDisabled" + :is-key-missing="isKeyMissing" :message-text=" $route.query.messageText || $store.getters['draftMessage/draftMessage'](partnerId) " @@ -361,7 +362,7 @@ const replyMessageId = ref(-1) const showEmojiPicker = ref(false) const showNewChatPlaceholder = ref(false) const isGettingPublicKey = ref(false) -const isInputDisabled = ref(false) +const isKeyMissing = ref(false) const messages = computed(() => store.getters['chat/messages'](props.partnerId)) const userId = computed(() => store.state.address) @@ -436,14 +437,18 @@ onBeforeMount(() => { }) onMounted(async () => { + if (chatPage.value <= 0) await fetchChatMessages() + + if (!messages.value.length) { + store.commit('chat/addNewChat', { partnerId: props.partnerId }) + } + if (isNewChat.value) { const partnerName = store.getters['chat/getPartnerName'](props.partnerId) await retrievePublicKey(props.partnerId, partnerName) } - if (chatPage.value <= 0) await fetchChatMessages() - const userMessages = messages.value.filter( (message: NormalizedChatMessageTransaction) => message.senderId && message.senderId === userId.value @@ -479,7 +484,7 @@ async function retrievePublicKey(partnerId: string, partnerName: string) { } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Unknown error' vibrate.long() - isInputDisabled.value = true + isKeyMissing.value = true store.dispatch('snackbar/show', { message: message diff --git a/src/components/Chat/ChatPlaceholder.vue b/src/components/Chat/ChatPlaceholder.vue index f6a872acd..b487214de 100644 --- a/src/components/Chat/ChatPlaceholder.vue +++ b/src/components/Chat/ChatPlaceholder.vue @@ -1,14 +1,22 @@ @@ -22,6 +30,7 @@ const { t } = useI18n() defineProps<{ showPlaceholder: boolean isGettingPublicKey: boolean + isKeyMissing: boolean }>() const className = 'chat-placeholder' @@ -64,6 +73,11 @@ const keys = ['encrypted', 'ipfs', 'anonymous', 'censorship'] } } + &__spinner { + margin-bottom: 4px; + margin-right: 4px; + } + &__row { width: 100%; text-align: center; diff --git a/src/locales/en.json b/src/locales/en.json index 58df71db4..bd0d13250 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -38,6 +38,7 @@ "censorship": "⛓\uFE0F Censorship-resistant blockchain architecture", "encrypted": "\uD83D\uDD10 End-to-end encrypted across all devices", "ipfs": "☁\uFE0F Decentralized IPFS storage for files/media", + "key-missing": "Unable to start a chat, public key is missing", "public-key": "Retrieving your partner’s key to encrypt messages…" }, "received_label": "Received", diff --git a/src/locales/ru.json b/src/locales/ru.json index ef62e43b5..3f76b3350 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -38,6 +38,7 @@ "censorship": "⛓\uFE0F Устойчивая к цензуре блокчейн-архитектура", "encrypted": "\uD83D\uDD10 Сквозное шифрование на всех устройствах", "ipfs": "☁\uFE0F Распределенное IPFS-хранилище файлов и медиа", + "key-missing": "Невозможно начать чат, публичного ключа не существует", "public-key": "Получаю ключ вашего собеседника для шифрования сообщений…" }, "received_label": "Получили", From 9788df991f8711ceaf8bcc34c170238a292b8276 Mon Sep 17 00:00:00 2001 From: skranee <125743202+skranee@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:52:53 +0300 Subject: [PATCH 08/18] refactor: AChatForm to composition API --- src/components/AChat/AChatForm.vue | 463 ++++++++++++++--------------- 1 file changed, 227 insertions(+), 236 deletions(-) diff --git a/src/components/AChat/AChatForm.vue b/src/components/AChat/AChatForm.vue index 409d6388d..9fd427297 100644 --- a/src/components/AChat/AChatForm.vue +++ b/src/components/AChat/AChatForm.vue @@ -43,262 +43,253 @@ - diff --git a/src/components/AChat/AChatForm.vue b/src/components/AChat/AChatForm.vue index b3d052815..7bf572fdc 100644 --- a/src/components/AChat/AChatForm.vue +++ b/src/components/AChat/AChatForm.vue @@ -10,7 +10,7 @@ v-model="message" @input="onInput" :placeholder="placeholder" - :disabled="isKeyMissing" + :disabled="shouldDisableInput" hide-details single-line auto-grow @@ -60,7 +60,7 @@ type Props = { showSendButton?: boolean sendOnEnter?: boolean label?: string - isKeyMissing?: boolean + shouldDisableInput?: boolean showDivider?: boolean validator: (message: string) => string | false } @@ -71,7 +71,7 @@ const props = withDefaults(defineProps(), { showSendButton: true, sendOnEnter: true, label: undefined, - isKeyMissing: false, + shouldDisableInput: false, showDivider: false }) diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 58d1c776e..468d1bfb0 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -202,7 +202,7 @@ :send-on-enter="sendMessageOnEnter" :show-divider="true" :label="t('chats.message')" - :is-key-missing="isKeyMissing" + :should-disable-input="shouldDisableInput" :message-text=" $route.query.messageText || $store.getters['draftMessage/draftMessage'](partnerId) " @@ -308,6 +308,7 @@ import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import { useStore } from 'vuex' import ChatPlaceholder from '@/components/Chat/ChatPlaceholder.vue' +import { watchImmediate } from '@vueuse/core' const validationErrors = { emptyMessage: 'EMPTY_MESSAGE', @@ -365,8 +366,16 @@ const isGettingPublicKey = ref(false) const isKeyMissing = ref(false) const messages = computed(() => store.getters['chat/messages'](props.partnerId)) +const userMessages = computed(() => + messages.value.filter( + (message: NormalizedChatMessageTransaction) => message.senderId === userId.value + ) +) const userId = computed(() => store.state.address) const isNewChat = computed(() => store.getters['chat/isNewChat'](props.partnerId)) +const shouldDisableInput = computed( + () => isGettingPublicKey.value || isKeyMissing.value || !store.state.publicKeys[props.partnerId] +) const getPartnerName = (address: string) => { const name: string = store.getters['partners/displayName'](address) || '' @@ -432,12 +441,24 @@ watch(replyMessageId, (messageId) => { }) }) +watch(userMessages, () => { + if (noMoreMessages.value) { + showNewChatPlaceholder.value = !userMessages.value.length + } +}) + +watchImmediate(messages, (updatedMessages) => { + if (isFulfilled.value && !updatedMessages.length) { + store.commit('chat/addNewChat', { partnerId: props.partnerId }) + } +}) + onBeforeMount(() => { window.addEventListener('keyup', onKeyPress) }) -onMounted(async () => { - if (chatPage.value <= 0) await fetchChatMessages() +async function handleEmptyChat() { + showNewChatPlaceholder.value = !userMessages.value.length if (!messages.value.length) { store.commit('chat/addNewChat', { partnerId: props.partnerId }) @@ -448,13 +469,16 @@ onMounted(async () => { await createChat(props.partnerId, partnerName) } +} - const userMessages = messages.value.filter( - (message: NormalizedChatMessageTransaction) => - message.senderId && message.senderId === userId.value - ) +onMounted(async () => { + if (isNewChat.value) { + showNewChatPlaceholder.value = true + } - showNewChatPlaceholder.value = !userMessages.length + if (chatPage.value <= 0) await fetchChatMessages() + + await handleEmptyChat() scrollBehavior() nextTick(() => { @@ -485,14 +509,9 @@ async function createChat(partnerId: string, partnerName: string) { partnerId, partnerName }) - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Unknown error' + } catch { vibrate.long() isKeyMissing.value = true - - store.dispatch('snackbar/show', { - message: message - }) } finally { isGettingPublicKey.value = false } diff --git a/src/components/Chat/ChatPlaceholder.vue b/src/components/Chat/ChatPlaceholder.vue index ce1f7705c..ba4cb2b5e 100644 --- a/src/components/Chat/ChatPlaceholder.vue +++ b/src/components/Chat/ChatPlaceholder.vue @@ -1,7 +1,7 @@ diff --git a/src/components/ChatStartDialog.vue b/src/components/ChatStartDialog.vue index 9ae200579..2208aa1a9 100644 --- a/src/components/ChatStartDialog.vue +++ b/src/components/ChatStartDialog.vue @@ -136,13 +136,7 @@ export default { return Promise.reject(new Error(this.$t('chats.incorrect_address'))) } - return this.$emit( - 'start-chat', - this.recipientAddress, - this.uriMessage, - this.recipientName, - true - ) + this.$emit('start-chat', this.recipientAddress, this.uriMessage, this.recipientName, true) }, /** diff --git a/src/locales/en.json b/src/locales/en.json index 034847c9a..068d6f682 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -38,7 +38,11 @@ "censorship": "⛓\uFE0F Censorship-resistant blockchain architecture", "encrypted": "\uD83D\uDD10 End-to-end encrypted across all devices", "ipfs": "☁\uFE0F Decentralized IPFS storage for files/media", - "key-missing": "Unable to start a chat, public key is missing", + "can_not_message": "Cannot message this user yet—their account isn't activated", + "what_does_it_mean": { + "text": "What does it mean?", + "link": "https://news.adamant.im/chats-and-uninitialized-accounts-in-adamant-5035438e2fcd" + }, "public-key": "Retrieving your partner’s key to encrypt messages…" }, "received_label": "Received", diff --git a/src/locales/ru.json b/src/locales/ru.json index 5ece842c4..08c315e9e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -38,7 +38,11 @@ "censorship": "⛓\uFE0F Устойчивая к цензуре блокчейн-архитектура", "encrypted": "\uD83D\uDD10 Сквозное шифрование на всех устройствах", "ipfs": "☁\uFE0F Распределенное IPFS-хранилище файлов и медиа", - "key-missing": "Невозможно начать чат, публичного ключа не существует", + "can_not_message": "Написать этому пользователю пока не получится — его аккаунт не активирован", + "what_does_it_mean": { + "text": "Что это значит?", + "link": "https://news.adamant.im/chats-and-uninitialized-accounts-in-adamant-5035438e2fcd" + }, "public-key": "Получаю ключ вашего собеседника для шифрования сообщений…" }, "received_label": "Получили", From 81c3bc047f73a6070a28b461a68a08998f27cf53 Mon Sep 17 00:00:00 2001 From: skranee <125743202+skranee@users.noreply.github.com> Date: Sun, 20 Apr 2025 00:45:22 +0300 Subject: [PATCH 12/18] refactor: declare function after component lifecycle stages --- src/components/Chat/Chat.vue | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 468d1bfb0..d2cefdba7 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -457,20 +457,6 @@ onBeforeMount(() => { window.addEventListener('keyup', onKeyPress) }) -async function handleEmptyChat() { - showNewChatPlaceholder.value = !userMessages.value.length - - if (!messages.value.length) { - store.commit('chat/addNewChat', { partnerId: props.partnerId }) - } - - if (isNewChat.value) { - const partnerName = store.getters['chat/getPartnerName'](props.partnerId) - - await createChat(props.partnerId, partnerName) - } -} - onMounted(async () => { if (isNewChat.value) { showNewChatPlaceholder.value = true @@ -502,6 +488,20 @@ onBeforeUnmount(() => { } }) +async function handleEmptyChat() { + showNewChatPlaceholder.value = !userMessages.value.length + + if (!messages.value.length) { + store.commit('chat/addNewChat', { partnerId: props.partnerId }) + } + + if (isNewChat.value) { + const partnerName = store.getters['chat/getPartnerName'](props.partnerId) + + await createChat(props.partnerId, partnerName) + } +} + async function createChat(partnerId: string, partnerName: string) { try { isGettingPublicKey.value = true From a65e875414877cefd3ec86c88a43f68b9fdf1e52 Mon Sep 17 00:00:00 2001 From: skranee <125743202+skranee@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:55:30 +0300 Subject: [PATCH 13/18] refactor: fix styles --- src/assets/styles/components/_chat.scss | 2 ++ src/components/AChat/AChat.vue | 13 ++++++++++++- src/components/Chat/Chat.vue | 1 + src/components/Chat/ChatPlaceholder.vue | 9 ++++++--- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/assets/styles/components/_chat.scss b/src/assets/styles/components/_chat.scss index 921925782..87f31f3f3 100644 --- a/src/assets/styles/components/_chat.scss +++ b/src/assets/styles/components/_chat.scss @@ -19,6 +19,8 @@ $chat-avatar-size: 40px; $scroll-bar-width: 4px; +$placeholder-height: 248px; + .a-chat { display: block; text-decoration: none; diff --git a/src/components/AChat/AChat.vue b/src/components/AChat/AChat.vue index e6295bcdf..5710b19d3 100644 --- a/src/components/AChat/AChat.vue +++ b/src/components/AChat/AChat.vue @@ -10,7 +10,7 @@ @@ -253,6 +253,17 @@ defineExpose({