diff --git a/.eslintrc.js b/.eslintrc.js index b1f9235af1..d3bf179cbe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,8 @@ module.exports = { 'import/named': 0, 'standard/no-callback-literal': 0, '@typescript-eslint/no-unused-vars': 0, - '@typescript-eslint/require-await': 0, + 'require-await': 0, + 'no-empty-pattern': 0, 'no-else-return': [ 'error', { diff --git a/libraries/Enums/types/prop-common-events.ts b/libraries/Enums/types/prop-common-events.ts index 38fa31ba4d..1b08f1378d 100644 --- a/libraries/Enums/types/prop-common-events.ts +++ b/libraries/Enums/types/prop-common-events.ts @@ -1,12 +1,13 @@ export enum PropCommonEnum { - UPDATE = "Update", - NORMAL = "Normal", - DEFAULT = "default", - WAIT = "wait", - FULFILLED = "fulfilled", - CREATED_AT = "created_at", - TYPING = "TYPING", - NOT_TYPING = "NOT_TYPING", + UPDATE = 'Update', + NORMAL = 'Normal', + DEFAULT = 'default', + WAIT = 'wait', + FULFILLED = 'fulfilled', + CREATED_AT = 'created_at', + MOD = '_mod', + TYPING = 'TYPING', + NOT_TYPING = 'NOT_TYPING', } -export type PropCommon = keyof typeof PropCommonEnum +export type PropCommon = keyof typeof PropCommonEnum diff --git a/libraries/Textile/MailboxManager.ts b/libraries/Textile/MailboxManager.ts index 43ca82ef24..fb5bd5ee18 100644 --- a/libraries/Textile/MailboxManager.ts +++ b/libraries/Textile/MailboxManager.ts @@ -20,9 +20,9 @@ import { Message, } from '~/types/textile/mailbox' import { TextileInitializationData } from '~/types/textile/manager' -import {PropCommonEnum} from "~/libraries/Enums/types/prop-common-events"; -import {MessagingTypesEnum} from "~/libraries/Enums/types/messaging-types"; -import {EncodingTypesEnum} from "~/libraries/Enums/types/encoding-types"; +import { PropCommonEnum } from '~/libraries/Enums/types/prop-common-events' +import { MessagingTypesEnum } from '~/libraries/Enums/types/messaging-types' +import { EncodingTypesEnum } from '~/libraries/Enums/types/encoding-types' export class MailboxManager { senderAddress: string @@ -75,16 +75,32 @@ export class MailboxManager { * Retrieve a conversation with a specific user, filtered by the given query parameters * @param friendIdentifier friend mailboxId * @param query parameters for filtering + * @param lastInbound timestamp of last received message * @returns an array of messages */ - async getConversation( - friendIdentifier: string, - query: ConversationQuery, - ): Promise { + async getConversation({ + friendIdentifier, + query, + lastInbound, + }: { + friendIdentifier: string + query: ConversationQuery + lastInbound?: number + }): Promise { const thread = await this.textile.users.getThread('hubmail') const threadID = ThreadID.fromString(thread.id) - const inboxQuery = Query.where('from').eq(friendIdentifier).orderByIDDesc() + let inboxQuery = Query.where('from').eq(friendIdentifier).orderByIDDesc() + + // if messages are stored in indexeddb, only fetch new messages from textile + if (lastInbound) { + lastInbound = lastInbound * 1000000 // textile has a more specific unix timestamp, matching theirs + inboxQuery = Query.where('from') + .eq(friendIdentifier) + .and(PropCommonEnum.MOD) + .ge(lastInbound) + .orderByIDDesc() + } if (query?.limit) { inboxQuery.limitTo(query.limit) @@ -113,11 +129,16 @@ export class MailboxManager { sentboxQuery.and(PropCommonEnum.CREATED_AT).lt(lastMessageTime) } - const encryptedSentbox = await this.textile.client.find( - threadID, - MailboxSubscriptionType.sentbox, - sentboxQuery, - ) + let encryptedSentbox: MessageFromThread[] = [] + + // only fetch sent messages from textile if indexeddb is empty. after that, fetch sent messages from indexeddb + if (lastInbound === undefined) { + encryptedSentbox = await this.textile.client.find( + threadID, + MailboxSubscriptionType.sentbox, + sentboxQuery, + ) + } const messages = [...encryptedInbox, ...encryptedSentbox].sort( (a, b) => a.created_at - b.created_at, @@ -208,9 +229,18 @@ export class MailboxManager { at: Date.now(), type: message.type, payload: message.payload, - reactedTo: message.type === MessagingTypesEnum.REACTION ? message.reactedTo : undefined, - repliedTo: message.type === MessagingTypesEnum.REPLY ? message.repliedTo : undefined, - replyType: message.type === MessagingTypesEnum.REPLY ? message.replyType : undefined, + reactedTo: + message.type === MessagingTypesEnum.REACTION + ? message.reactedTo + : undefined, + repliedTo: + message.type === MessagingTypesEnum.REPLY + ? message.repliedTo + : undefined, + replyType: + message.type === MessagingTypesEnum.REPLY + ? message.replyType + : undefined, pack: message.pack, }), ) @@ -245,8 +275,14 @@ export class MailboxManager { editedAt: Date.now(), type: message.type, payload: message.payload, - reactedTo: message.type === MessagingTypesEnum.REACTION ? message.reactedTo : undefined, - repliedTo: message.type === MessagingTypesEnum.REPLY ? message.repliedTo : undefined, + reactedTo: + message.type === MessagingTypesEnum.REACTION + ? message.reactedTo + : undefined, + repliedTo: + message.type === MessagingTypesEnum.REPLY + ? message.repliedTo + : undefined, }), ) diff --git a/locales b/locales index b356584e1f..97803fb3d9 160000 --- a/locales +++ b/locales @@ -1 +1 @@ -Subproject commit b356584e1f85690481b342976752a01e37625760 +Subproject commit 97803fb3d9f66bbf4db124dfac913d8bc8484352 diff --git a/nuxt.config.js b/nuxt.config.js index 9bf846433e..e4412d3a9b 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -79,6 +79,7 @@ export default defineNuxtConfig({ { src: '~/plugins/thirdparty/videoplayer.ts' }, { src: '~/plugins/thirdparty/vuetify.ts' }, { src: '~/plugins/thirdparty/swiper.ts' }, + { src: '~/plugins/thirdparty/dexie.ts' }, // Local { src: '~/plugins/local/classLoader.ts' }, { src: '~/plugins/local/notifications.ts', mode: 'client' }, diff --git a/package.json b/package.json index f1bf9fb0f1..9bfad1ffda 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "curve25519-js": "^0.0.4", "dayjs": "^1.10.4", "detectrtc": "^1.4.1", + "dexie": "^3.2.0", "ed2curve": "^0.3.0", "emoji-mart-vue-fast": "^10.0.1", "events": "^3.3.0", diff --git a/plugins/thirdparty/dexie.ts b/plugins/thirdparty/dexie.ts new file mode 100644 index 0000000000..9b72ad5a0a --- /dev/null +++ b/plugins/thirdparty/dexie.ts @@ -0,0 +1,20 @@ +import Dexie from 'dexie' +import { Message } from '~/types/textile/mailbox' + +export type DexieMessage = { + key: string + conversation: Message[] +} +class SatelliteDB extends Dexie { + public conversations: Dexie.Table + + public constructor() { + super('SatelliteDB') + this.version(1).stores({ + conversations: 'key', + }) + this.conversations = this.table('conversations') + } +} + +export const db = new SatelliteDB() diff --git a/store/textile/actions.ts b/store/textile/actions.ts index c9dee7f964..5cb5f4efe1 100644 --- a/store/textile/actions.ts +++ b/store/textile/actions.ts @@ -8,6 +8,7 @@ import { MessageRouteEnum, PropCommonEnum } from '~/libraries/Enums/enums' import { Config } from '~/config' import { MailboxSubscriptionType, Message } from '~/types/textile/mailbox' import { UploadDropItemType } from '~/types/files/file' +import { db, DexieMessage } from '~/plugins/thirdparty/dexie' export default { /** @@ -61,10 +62,42 @@ export default { const query = { limit: Config.chat.defaultMessageLimit, skip: 0 } - const conversation = await $MailboxManager.getConversation( - friend.textilePubkey, - query, - ) + let conversation: Message[] = [] + + const dbMessages = await db.conversations.get(address).then((convo) => { + return convo?.conversation ?? [] + }) + + // if nothing stored in indexeddb, fetch entire conversation + if (!dbMessages.length) { + conversation = await $MailboxManager.getConversation({ + friendIdentifier: friend.textilePubkey, + query, + }) + } + // otherwise, combine new textile messages with stored messages + else { + const lastInbound = rootState.textile.conversations[address].lastInbound + const textileMessages = await $MailboxManager.getConversation({ + friendIdentifier: friend.textilePubkey, + query, + lastInbound, + }) + + // use textileMessages as primary source. this way, edited messages will use the newest version + const ids = new Set(textileMessages.map((d) => d.id)) + conversation = [ + ...textileMessages, + ...dbMessages.filter((d) => !ids.has(d.id)), + ] + } + + // store latest data in indexeddb + const dbData: DexieMessage = { + conversation, + key: address, + } + db.conversations.put(dbData) commit('setConversation', { address: friend.address, @@ -88,6 +121,7 @@ export default { async subscribeToMailbox({ commit, rootState, + dispatch, }: ActionsArguments) { const $TextileManager: TextileManager = Vue.prototype.$TextileManager const MailboxManager = $TextileManager.mailboxManager @@ -118,6 +152,7 @@ export default { sender: MessageRouteEnum.INBOUND, message, }) + dispatch('storeMessage', { address: sender.address, message }) }) }, /** @@ -147,7 +182,7 @@ export default { * and the text message to be sent */ async sendTextMessage( - { commit, rootState }: ActionsArguments, + { commit, rootState, dispatch }: ActionsArguments, { to, text }: { to: string; text: string }, ) { const $TextileManager: TextileManager = Vue.prototype.$TextileManager @@ -180,6 +215,7 @@ export default { sender: MessageRouteEnum.OUTBOUND, message: result, }) + dispatch('storeMessage', { address: friend.address, message: result }) commit('setMessageLoading', { loading: false }) }, clearUploadStatus({ commit }: ActionsArguments) { @@ -192,7 +228,7 @@ export default { * file: UploadDropItemType to be sent users bucket for textile */ async sendFileMessage( - { commit, rootState }: ActionsArguments, + { commit, rootState, dispatch }: ActionsArguments, { to, file }: { to: string; file: UploadDropItemType }, ) { document.body.style.cursor = PropCommonEnum.WAIT @@ -204,7 +240,7 @@ export default { path, (progress: number) => { commit('setUploadingFileProgress', { - progress: progress, + progress, name: file.file.name, }) }, @@ -235,6 +271,7 @@ export default { sender: MessageRouteEnum.OUTBOUND, message: sendFileResult, }) + dispatch('storeMessage', { address: friend.address, sendFileResult }) }, /** * @description Sends a reaction message to a given friend @@ -243,7 +280,7 @@ export default { * the emoji and the id of the message the user reacted to */ async sendReactionMessage( - { commit, rootState }: ActionsArguments, + { commit, rootState, dispatch }: ActionsArguments, { to, reactTo, emoji }: { to: string; reactTo: string; emoji: string }, ) { const $TextileManager: TextileManager = Vue.prototype.$TextileManager @@ -275,6 +312,7 @@ export default { sender: MessageRouteEnum.OUTBOUND, message: result, }) + dispatch('storeMessage', { address: friend.address, message: result }) }, /** * @description Sends a reply message to a given friend @@ -283,7 +321,7 @@ export default { * the text message and the id of the message the user replied to */ async sendReplyMessage( - { commit, rootState }: ActionsArguments, + { commit, rootState, dispatch }: ActionsArguments, { to, replyTo, @@ -327,7 +365,7 @@ export default { payload: text, repliedTo: replyTo, type: 'reply', - replyType: replyType, + replyType, }, ) @@ -336,6 +374,7 @@ export default { sender: MessageRouteEnum.OUTBOUND, message: result, }) + dispatch('storeMessage', { address: friend.address, message: result }) commit('setMessageLoading', { loading: false }) }, @@ -346,7 +385,7 @@ export default { * and the text message to be sent */ async editTextMessage( - { commit, rootState }: ActionsArguments, + { commit, rootState, dispatch }: ActionsArguments, { to, original, text }: { to: string; text: string; original: Message }, ) { const $TextileManager: TextileManager = Vue.prototype.$TextileManager @@ -386,12 +425,14 @@ export default { sender: MessageRouteEnum.OUTBOUND, message: result, }) + dispatch('storeMessage', { address: friend.address, message: result }) } else { commit('addMessageToConversation', { address: friend.address, sender: MessageRouteEnum.OUTBOUND, message: original, }) + dispatch('storeMessage', { address: friend.address, message: original }) } }, /** @@ -401,7 +442,7 @@ export default { * glyph to be sent, and pack name */ async sendGlyphMessage( - { commit, rootState }: ActionsArguments, + { commit, rootState, dispatch }: ActionsArguments, { to, src, pack }: { to: string; src: string; pack: string }, ) { const $TextileManager: TextileManager = Vue.prototype.$TextileManager @@ -425,7 +466,7 @@ export default { { to: friend.textilePubkey, payload: src, - pack: pack, + pack, type: 'glyph', }, ) @@ -435,7 +476,43 @@ export default { sender: MessageRouteEnum.OUTBOUND, message: result, }) + dispatch('storeMessage', { address: friend.address, message: result }) commit('setMessageLoading', { loading: false }) }, + /** + * @description Store a new sent message in indexeddb. If edited, replace old message + * @param param0 Action Arguments + * @param param1 an object containing the recipient address (textile public key) and the message to be stored, + * glyph to be sent, and pack name + */ + async storeMessage( + {}: ActionsArguments, + { + address, + message, + }: { + address: string + message: Message + }, + ) { + // replace old message with new edited version + if (message.editedAt !== undefined) { + db.conversations.get(address).then((convo) => { + if (!convo) { + return + } + const index = convo.conversation.map((e) => e.id).indexOf(message.id) + convo.conversation[index] = message + db.conversations.put(convo) + }) + return + } + + // add regular message to indexeddb + db.conversations + .where('key') + .equals(address) + .modify((convo) => convo.conversation.push(message)) + }, } diff --git a/store/textile/mutations.ts b/store/textile/mutations.ts index 26401f3225..e36592787c 100644 --- a/store/textile/mutations.ts +++ b/store/textile/mutations.ts @@ -116,38 +116,38 @@ const mutations = { setMessageLoading(state: TextileState, { loading }: { loading: boolean }) { state.messageLoading = loading }, - clearUploadProgress( - state: TextileState, - ) { + clearUploadProgress(state: TextileState) { state.uploadProgress = {} }, setUploadingFileProgress( state: TextileState, - {progress, name} : { - progress: number, - name: string, + { + progress, + name, + }: { + progress: number + name: string }, ) { if (progress !== 100) { state.uploadProgress = { ...state.uploadProgress, [name]: { - progress: progress, + progress, finished: false, - name: name - } + name, + }, } } if (progress === 100) { state.uploadProgress = { ...state.uploadProgress, [name]: { - progress: progress, + progress, finished: true, - name: name - } - } - + name, + }, + } } }, } diff --git a/store/textile/types.ts b/store/textile/types.ts index d690a26b4e..2acd4b9089 100644 --- a/store/textile/types.ts +++ b/store/textile/types.ts @@ -3,21 +3,21 @@ import { RepliesTracker, ReactionsTracker, } from '~/types/textile/mailbox' - +export interface Conversation { + [key: string]: { + messages: MessagesTracker + replies: RepliesTracker + reactions: ReactionsTracker + lastInbound: number // the last time a message was received by any member of conversation, other than account owner + lastUpdate: number // the last time a message was received by any member of conversation, including account owner + limit: number + skip: number + end: boolean + } +} export interface TextileState { initialized: boolean - conversations: { - [key: string]: { - messages: MessagesTracker - replies: RepliesTracker - reactions: ReactionsTracker - lastInbound: number // the last time a message was received by any member of conversation, other than account owner - lastUpdate: number // the last time a message was received by any member of conversation, including account owner - limit: number - skip: number - end: boolean - } - } + conversations: Conversation conversationLoading: boolean messageLoading: boolean uploadProgress: {