diff --git a/src/api/api.js b/src/api/api.js index 20192d69..a48d5857 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -81,6 +81,15 @@ class Api { return; } + if (message.message_decryption_failed) { + if (this.onMessageDecryptionFailedListener) { + this.onMessageDecryptionFailedListener( + message.message_decryption_failed + ); + } + return; + } + if (message.message) { if (message.message.error) { this.responsesPromises[ @@ -401,6 +410,21 @@ class Api { return this.sendPromise(requestData, resObjKey); } + async markDecrypionFailedMessages(data) { + const requestData = { + request: { + message_decryption_failed: { + cid: data.cid, + ids: data.mids, + }, + id: getUniqueId("markDecrypionFailedMessages"), + }, + }; + + const resObjKey = "success"; + return this.sendPromise(requestData, resObjKey); + } + async messageDelete(data) { //===============to do const requestData = { diff --git a/src/assets/icons/_helpers/CornerAccent.svg b/src/assets/icons/_helpers/CornerAccent.svg index a964996c..4d54cf96 100644 --- a/src/assets/icons/_helpers/CornerAccent.svg +++ b/src/assets/icons/_helpers/CornerAccent.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/assets/icons/_helpers/CornerDanger.svg b/src/assets/icons/_helpers/CornerDanger.svg new file mode 100644 index 00000000..9a95fadb --- /dev/null +++ b/src/assets/icons/_helpers/CornerDanger.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/status/DecryptionFailed.svg b/src/assets/icons/status/DecryptionFailed.svg new file mode 100644 index 00000000..33993437 --- /dev/null +++ b/src/assets/icons/status/DecryptionFailed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/auth/components/LoginLinks.js b/src/components/auth/components/LoginLinks.js index 51e0fbb1..55e6862a 100644 --- a/src/components/auth/components/LoginLinks.js +++ b/src/components/auth/components/LoginLinks.js @@ -21,10 +21,10 @@ export default function LoginLinks({ changePage, content }) { setIsLoader(true); try { const userData = await usersService.login(content); + await garbageCleaningService.resetDataOnAuth(); await encryptionService.registerDevice(userData._id); navigateTo("/"); subscribeForNotifications(); - await garbageCleaningService.resetDataOnAuth(); dispatch(setCurrentUserId(userData._id)); dispatch(upsertUser(userData)); } catch (err) { diff --git a/src/components/auth/components/SignUpLinks.js b/src/components/auth/components/SignUpLinks.js index aab805b8..ad98faa1 100644 --- a/src/components/auth/components/SignUpLinks.js +++ b/src/components/auth/components/SignUpLinks.js @@ -25,9 +25,9 @@ export default function SignUpLinks({ changePage, content }) { if (isLogin) { const userData = await usersService.login(content); + await garbageCleaningService.resetDataOnAuth(); await encryptionService.registerDevice(userData._id); subscribeForNotifications(); - await garbageCleaningService.resetDataOnAuth(); dispatch(upsertUser(userData)); dispatch(setCurrentUserId(userData._id)); } diff --git a/src/components/hub/elements/ChatMessage.js b/src/components/hub/elements/ChatMessage.js index fdff7857..dbcf679a 100644 --- a/src/components/hub/elements/ChatMessage.js +++ b/src/components/hub/elements/ChatMessage.js @@ -2,6 +2,7 @@ import MessageAttachments from "@components/message/elements/MessageAttachments" import MessageStatus from "@components/message/elements/MessageStatus"; import MessageUserIcon from "@components/hub/elements/MessageUserIcon"; import addSuffix from "@utils/navigation/add_suffix"; +import encryptionService from "@services/encryptionService"; import getUserFullName from "@utils/user/get_user_full_name"; import { urlify } from "@utils/text/urlify"; import { useLocation } from "react-router-dom"; @@ -11,6 +12,7 @@ import "@styles/hub/elements/ChatMessage.css"; import { ReactComponent as CornerLight } from "@icons/_helpers/CornerLight.svg"; import { ReactComponent as CornerAccent } from "@icons/_helpers/CornerAccent.svg"; +import { ReactComponent as CornerDanger } from "@icons/_helpers/CornerDanger.svg"; export default function ChatMessage({ sender, @@ -23,6 +25,7 @@ export default function ChatMessage({ const { body, from, attachments, status, t } = message; const isCurrentUser = from === currentUserId; + const isError = status === "decryption_failed"; const timeSend = useMemo(() => { const time = new Date(t * 1000); @@ -36,9 +39,14 @@ export default function ChatMessage({ return (
encryptionService.createNewSessionAndSendMessage(message) + : undefined + } >
{next ? null : ( @@ -50,12 +58,17 @@ export default function ChatMessage({
)}
-
- {next ? null : isCurrentUser ? ( - - ) : ( - - )} +
+ {!next && + (isCurrentUser ? ( + isError ? ( + + ) : ( + + ) + ) : ( + + ))} {prev ? null : (
, read: , default: }, + accent: { + sent: , + read: , + decryption_failed: , + default: , + }, white: { sent: , read: , + decryption_failed: , default: , }, }; diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js index 11d79fc5..6ba6f67d 100644 --- a/src/services/conversationsService.js +++ b/src/services/conversationsService.js @@ -1,6 +1,7 @@ import DownloadManager from "@src/adapters/downloadManager"; import api from "@api/api"; import eventEmitter from "@event/eventEmitter"; +import isEqualsNativeIds from "@utils/user/isEqualsNativeIds"; import isHeic from "@utils/media/is_heic"; import navigateTo from "@utils/navigation/navigate_to"; import processFile from "@utils/media/process_file"; diff --git a/src/services/encryptionService.js b/src/services/encryptionService.js index d0ead2c8..2f7566bd 100644 --- a/src/services/encryptionService.js +++ b/src/services/encryptionService.js @@ -1,5 +1,8 @@ import CryptoJS from "crypto-js"; import api from "@api/api"; +import getOpponentId from "@utils/user/get_opponent_id"; +import indexedDB from "@store/indexedDB"; +import messagesService from "./messagesService"; import initVodozemac, { Account, OlmMessage, @@ -15,6 +18,7 @@ class EncryptionService { #encryptionSessions = {}; #account = null; #vodozemacInitialized = false; + visibleBody = "Can`t decrypt this message, not for this device"; constructor() { this.#initializeVodozemac(); @@ -30,6 +34,10 @@ class EncryptionService { } } + markDecrypionFailedMessages(cid, mids) { + api.markDecrypionFailedMessages({ cid, mids }); + } + hasAccount() { return !!this.#account; } @@ -44,6 +52,11 @@ class EncryptionService { await localforage.clear(); } + async clearStoredSessionWithUser(userId) { + delete this.#encryptionSessions[userId]; + await localforage.removeItem(`encryptedSession${userId}`); + } + encryptMessage(text, userId) { const session = this.#encryptionSessions[userId]; return session.encrypt(text); @@ -61,14 +74,18 @@ class EncryptionService { console.log( "[encryption] Encrypted session with the opponent is missing" ); - return "Can`t decrypt this message, not for this device"; + return this.visibleBody; } try { return session.decrypt(olmMessage); } catch (error) { console.log("[encryption] Failed to decrypt an encrypted message", error); - return "Can`t decrypt this message, not for this device"; + this.markDecrypionFailedMessages(olmMessageParams.cid, [ + olmMessageParams._id, + ]); + + return this.visibleBody; } } @@ -274,6 +291,7 @@ class EncryptionService { console.log("Encrypted session from local store:", session); const decryptMessage = this.decryptMessage(olmMessageParams, userId); + indexedDB.upsertEncryptionMessage(olmMessageParams._id, decryptMessage); store.dispatch( upsertMessage({ _id: olmMessageParams._id, body: decryptMessage }) ); @@ -319,20 +337,26 @@ class EncryptionService { if (olmMessage) { console.log("Create session with olmMessage: ", olmMessage); + let decryptMessage = this.visibleBody; try { const inboundSession = this.#account.create_inbound_session( userKeys.identity_key, olmMessage ); - const decryptMessage = `${inboundSession.plaintext}`; + decryptMessage = `${inboundSession.plaintext}`; session = inboundSession.session; store.dispatch( upsertMessage({ _id: olmMessageParams._id, body: decryptMessage }) ); } catch (error) { + this.markDecrypionFailedMessages(olmMessageParams.cid, [ + olmMessageParams._id, + ]); + console.error("Failed to create inbound session:", error); } + indexedDB.upsertEncryptionMessage(olmMessageParams._id, decryptMessage); //check if the top block worked successfully -> mb need to clear the session param if (session) { @@ -361,6 +385,22 @@ class EncryptionService { } } + async createNewSessionAndSendMessage(message) { + console.log("[encryption] Recreate a new encrypted session"); + const messageParams = Object.assign({}, message); + const { _id: mid, cid, from } = messageParams; + + const conversation = store.getState().conversations.entities[cid]; + const opponentId = getOpponentId(conversation, from); + + await this.clearStoredSessionWithUser(opponentId); + await this.createEncryptionSession(opponentId); + + await messagesService.removeMessageFromLocalStore(mid, cid); + + await messagesService.sendEncryptedMessage(messageParams, opponentId); + } + async encrypteDataForLocalStore(data) { const secretKey = await this.#getPickleKey( "pickleKey", diff --git a/src/services/garbageCleaningService.js b/src/services/garbageCleaningService.js index f1633d1d..6c6bbd6f 100644 --- a/src/services/garbageCleaningService.js +++ b/src/services/garbageCleaningService.js @@ -12,8 +12,8 @@ import { updateNetworkState } from "@store/values/NetworkState"; class GarbageCleaningService { async clearConversationMessages(cid) { if (!cid) return; - store.dispatch(clearMessagesToLocalLimit(cid)); store.dispatch(clearMessageIdsToLocalLimit(cid)); + store.dispatch(clearMessagesToLocalLimit(cid)); } async resetDataOnAuth() { diff --git a/src/services/messagesService.js b/src/services/messagesService.js index e604b1af..da828d34 100644 --- a/src/services/messagesService.js +++ b/src/services/messagesService.js @@ -10,9 +10,9 @@ import { addUser } from "@store/values/Participants"; import { addMessage, addMessages, - markMessagesAsRead, removeMessage, selectActiveConversationMessages, + updateMessagesStatus, upsertMessage, upsertMessages, } from "@store/values/Messages"; @@ -20,6 +20,7 @@ import { setSelectedConversation } from "@store/values/SelectedConversation"; import { markConversationAsRead, removeChat, + removeMessageFromConversation, updateLastMessageField, upsertChat, upsertParticipants, @@ -38,6 +39,7 @@ class MessagesService { message, message.from ); + indexedDB.upsertEncryptionMessage(message._id, decryptedMessage); store.dispatch( upsertMessage({ _id: message._id, @@ -49,8 +51,10 @@ class MessagesService { constructor() { api.onMessageStatusListener = (message) => { - indexedDB.markMessagesAsRead(message.ids); - store.dispatch(markMessagesAsRead(message.ids)); + indexedDB.updateMessagesStatus(message.ids, "read"); + store.dispatch( + updateMessagesStatus({ mids: message.ids, status: "read" }) + ); store.dispatch( markConversationAsRead({ cid: message.cid, @@ -59,6 +63,13 @@ class MessagesService { ); }; + api.onMessageDecryptionFailedListener = (message) => { + indexedDB.updateMessagesStatus(message.ids, "decryption_failed"); + store.dispatch( + updateMessagesStatus({ mids: message.ids, status: "decryption_failed" }) + ); + }; + api.onMessageListener = async (message) => { const attachments = message.attachments; if (attachments) { @@ -201,11 +212,15 @@ class MessagesService { handleRetrievedMessages(messages) { const messagesIds = messages.map((el) => el._id).reverse(); - const messagesRedux = - selectActiveConversationMessages(store.getState()) || []; + const messagesReduxIds = ( + selectActiveConversationMessages(store.getState()) || [] + ).map((el) => el._id); const uniqueMessageIds = [ - ...new Set([...messagesIds, ...messagesRedux.map((el) => el._id)]), + ...new Set([ + ...messagesIds.filter((el) => !messagesReduxIds.includes(el)), + ...messagesReduxIds, + ]), ]; store.dispatch(addMessages(messages)); @@ -230,6 +245,15 @@ class MessagesService { return this.handleRetrievedMessages(messagesDB); } + const lastExistMessage = messagesDB[0]; + if (lastExistMessage) { + params.updated_at = { + gt: + lastExistMessage.created_at || + new Date(lastExistMessage.t * 1000).toISOString(), + }; + } + const messagesAPI = await api.messageList(params); await indexedDB.insertManyMessages(messagesAPI); @@ -245,7 +269,7 @@ class MessagesService { }; const allConversationMessages = Object.values( - store.getState().messages.entities + selectActiveConversationMessages(store.getState()) || {} ); const lastMessage = allConversationMessages.splice(-1)[0]; @@ -258,9 +282,9 @@ class MessagesService { } try { - if (allConversationMessages.length === params.limit) return; + if (allConversationMessages.length >= params.limit) return; - let messages = await this.retrieveMessages(params); + await this.retrieveMessages(params); const conv = store.getState().conversations?.entities?.[this.currentChatId]; @@ -276,15 +300,6 @@ class MessagesService { }) ); } - - if (conv.is_encrypted) { - setTimeout(() => { - //replace in the future, should be called after the session is created - messages.forEach( - async (message) => await this.#tryToCreateESession(message) - ); - }, 500); - } } catch (error) { console.log(error); store.dispatch(removeChat(cid)); @@ -329,6 +344,12 @@ class MessagesService { await this.sendMessage(message); } + + async removeMessageFromLocalStore(mid, cid) { + store.dispatch(removeMessageFromConversation({ mid, cid })); + store.dispatch(removeMessage(mid)); + await indexedDB.removeMessage(mid); + } } const messagesService = new MessagesService(); diff --git a/src/store/indexedDB.js b/src/store/indexedDB.js index 77c7270e..3d147b48 100644 --- a/src/store/indexedDB.js +++ b/src/store/indexedDB.js @@ -10,9 +10,9 @@ class IndexedDB { this.db = db; } - markMessagesAsRead(mids) { + updateMessagesStatus(mids, status) { this.db.messages.bulkUpdate( - mids.map((id) => ({ key: id, changes: { status: "read" } })) + mids.map((id) => ({ key: id, changes: { status } })) ); } @@ -62,9 +62,19 @@ class IndexedDB { ); } + async upsertEncryptionMessage(mid, body) { + await this.db.messages.update(mid, { + body: await encryptionService.encrypteDataForLocalStore(body), + }); + } + async removeAllMessages() { await this.db.messages.clear(); } + + async removeMessage(mid) { + await this.db.messages.delete(mid); + } } const indexedDB = new IndexedDB(); diff --git a/src/store/values/Conversations.js b/src/store/values/Conversations.js index e194dcbb..690b730a 100644 --- a/src/store/values/Conversations.js +++ b/src/store/values/Conversations.js @@ -181,7 +181,15 @@ export const conversations = createSlice({ clearMessageIdsToLocalLimit: (state, { payload }) => { conversationsAdapter.upsertOne(state, { _id: payload, - messagesIds: state.entities[payload].messagesIds.slice(-30), + messagesIds: state.entities[payload].messagesIds?.slice(-30), + }); + }, + removeMessageFromConversation: (state, { payload }) => { + conversationsAdapter.upsertOne(state, { + _id: payload.cid, + messagesIds: state.entities[payload.cid].messagesIds.filter( + (mid) => mid !== payload.mid + ), }); }, }, @@ -200,6 +208,7 @@ export const { upsertChat, upsertParticipants, clearMessageIdsToLocalLimit, + removeMessageFromConversation, } = conversations.actions; export default conversations.reducer; diff --git a/src/store/values/Messages.js b/src/store/values/Messages.js index 428c65ea..b7368f33 100644 --- a/src/store/values/Messages.js +++ b/src/store/values/Messages.js @@ -26,20 +26,18 @@ export const messages = createSlice({ addMessages: messagesAdapter.addMany, upsertMessage: messagesAdapter.upsertOne, upsertMessages: messagesAdapter.upsertMany, - markMessagesAsRead: (state, action) => { - const mids = action.payload + updateMessagesStatus: (state, { payload: { mids, status } }) => { + const upsertParams = mids .filter((id) => !!state.entities[id]) - .map((id) => { - return { _id: id, status: "read" }; - }); - messagesAdapter.upsertMany(state, mids); + .map((id) => ({ _id: id, status })); + messagesAdapter.upsertMany(state, upsertParams); }, clearMessagesToLocalLimit: (state, { payload }) => { const messageIds = Object.values(state.entities) .filter((message) => message.cid === payload) - .sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) - .slice(0, 30) + .splice(30) .map((message) => message._id); + messagesAdapter.removeMany(state, messageIds); }, removeMessage: messagesAdapter.removeOne, @@ -65,7 +63,9 @@ export const { addMessages, upsertMessage, upsertMessages, + updateMessagesStatus, markMessagesAsRead, + markDecryptionFailedMessages, removeMessage, clearMessagesToLocalLimit, } = messages.actions; diff --git a/src/styles/GlobalParam.css b/src/styles/GlobalParam.css index 6e79a63c..b12479fe 100644 --- a/src/styles/GlobalParam.css +++ b/src/styles/GlobalParam.css @@ -32,6 +32,7 @@ --color-black-50: rgba(0, 0, 0, 0.5); --color-black-75: rgba(0, 0, 0, 0.75); --color-grey-50: rgba(26, 26, 26, 0.5); + --color-red-light: #f19ba0; --color-red: #df2e38; --color-bg-light: #f6f6f6; --color-bg-light-25: rgba(246, 246, 246, 0.25); diff --git a/src/styles/hub/elements/ChatMessage.css b/src/styles/hub/elements/ChatMessage.css index 4d6f18dc..c35e065b 100644 --- a/src/styles/hub/elements/ChatMessage.css +++ b/src/styles/hub/elements/ChatMessage.css @@ -156,6 +156,12 @@ background-color: var(--color-accent-dark); } +.message__container--my.danger .message-content__container { + background-color: var(--color-red-light); + + cursor: pointer; +} + .message__container--my .photo__container, .message__container--my .content__text, .message__container--my .content__uname, diff --git a/src/utils/user/isEqualsNativeIds.js b/src/utils/user/isEqualsNativeIds.js new file mode 100644 index 00000000..e01eb79c --- /dev/null +++ b/src/utils/user/isEqualsNativeIds.js @@ -0,0 +1,3 @@ +export default function isEqualsNativeIds(id1, id2) { + return id1.toString() === id2.toString(); +}