From 4280d83ba8546d82b6b79f325dd0414651450424 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 26 Jun 2023 14:07:44 +0200 Subject: [PATCH 1/4] fix: bump libsession to allow createdAt to be given during migration --- package.json | 2 +- ts/node/migration/sessionMigrations.ts | 5 ++++- ts/session/utils/libsession/libsession_utils_contacts.ts | 1 + ts/types/sqlSharedTypes.ts | 3 +++ yarn.lock | 6 +++--- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 514b4e3a8..8f496c186 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "glob": "7.1.2", "image-type": "^4.1.0", "ip2country": "1.0.1", - "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.19/libsession_util_nodejs-v0.1.19.tar.gz", + "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.20/libsession_util_nodejs-v0.1.20.tar.gz", "libsodium-wrappers-sumo": "^0.7.9", "linkify-it": "3.0.2", "lodash": "^4.17.21", diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index 1471ad499..6f3efd3ea 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -1235,6 +1235,8 @@ function insertContactIntoContactWrapper( dbProfileKey: contact.profileKey || undefined, dbProfileUrl: contact.avatarPointer || undefined, priority, + dbCreatedAtSeconds: Math.floor((contact.active_at || Date.now()) / 1000), + // expirationTimerSeconds, }); @@ -1259,6 +1261,7 @@ function insertContactIntoContactWrapper( dbProfileKey: undefined, dbProfileUrl: undefined, priority: CONVERSATION_PRIORITIES.default, + dbCreatedAtSeconds: Math.floor(Date.now() / 1000), // expirationTimerSeconds: 0, }) ); @@ -1308,7 +1311,7 @@ function insertCommunityIntoWrapper( const convoId = community.id; // the id of a conversation has the prefix, the serverUrl and the roomToken already present, but not the pubkey const roomDetails = sqlNode.getV2OpenGroupRoom(convoId, db); - hasDebugEnvVariable && console.info('insertCommunityIntoWrapper: ', community); + // hasDebugEnvVariable && console.info('insertCommunityIntoWrapper: ', community); if ( !roomDetails || diff --git a/ts/session/utils/libsession/libsession_utils_contacts.ts b/ts/session/utils/libsession/libsession_utils_contacts.ts index a960352ae..a5d16d5df 100644 --- a/ts/session/utils/libsession/libsession_utils_contacts.ts +++ b/ts/session/utils/libsession/libsession_utils_contacts.ts @@ -70,6 +70,7 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise 0 // ? expirationTimerSeconds diff --git a/yarn.lock b/yarn.lock index 84fe9eff7..97425bef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5148,9 +5148,9 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.19/libsession_util_nodejs-v0.1.19.tar.gz": - version "0.1.19" - resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.19/libsession_util_nodejs-v0.1.19.tar.gz#294c6e8ea6b767d375a9c0249bef98b65f3ae252" +"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.20/libsession_util_nodejs-v0.1.20.tar.gz": + version "0.1.20" + resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.20/libsession_util_nodejs-v0.1.20.tar.gz#4ff8331e4efb1725cf8bba0ef1af7496ebd8533d" dependencies: cmake-js "^7.2.1" node-addon-api "^6.1.0" From 524debb307184fbe803f3ee3fb620cc518ce53ef Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 27 Jun 2023 07:52:25 +0200 Subject: [PATCH 2/4] fix: improve delete messages perfs and search logic --- ts/data/data.ts | 45 +++++++++++-------- ts/node/database_utility.ts | 18 ++++---- ts/node/migration/sessionMigrations.ts | 28 +++++++++++- ts/node/migration/signalMigrations.ts | 4 +- ts/node/sql.ts | 25 +++++------ .../job_runners/jobs/ConfigurationSyncJob.ts | 4 +- 6 files changed, 78 insertions(+), 46 deletions(-) diff --git a/ts/data/data.ts b/ts/data/data.ts index d4e9ab035..07acd7a16 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -497,27 +497,36 @@ async function getSeenMessagesByHashList(hashes: Array): Promise { } async function removeAllMessagesInConversation(conversationId: string): Promise { - let messages; - do { - // Yes, we really want the await in the loop. We're deleting 500 at a - // time so we don't use too much memory. - // eslint-disable-next-line no-await-in-loop - messages = await getLastMessagesByConversation(conversationId, 500, false); - if (!messages.length) { - return; - } - - const ids = messages.map(message => message.id); + let start = Date.now(); + const messages = await getLastMessagesByConversation(conversationId, 50, false); + window.log.info( + `removeAllMessagesInConversation ${conversationId} ${messages.length} took ${Date.now() - + start}` + ); + if (!messages.length) { + return; + } - // Note: It's very important that these models are fully hydrated because - // we need to delete all associated on-disk files along with the database delete. - // eslint-disable-next-line no-await-in-loop + // Note: It's very important that these models are fully hydrated because + // we need to delete all associated on-disk files along with the database delete. + // eslint-disable-next-line no-await-in-loop - await Promise.all(messages.map(message => message.cleanup())); + start = Date.now(); + for (let index = 0; index < messages.length; index++) { + const message = messages.at(index); + await message.cleanup(); + } + window.log.info( + `removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${Date.now() - + start}ms` + ); + start = Date.now(); - // eslint-disable-next-line no-await-in-loop - await channels.removeMessagesByIds(ids); - } while (messages.length > 0); + // eslint-disable-next-line no-await-in-loop + await channels.removeAllMessagesInConversation(conversationId); + window.log.info( + `removeAllMessagesInConversation: ${conversationId} took ${Date.now() - start}ms` + ); } async function getMessagesBySentAt(sentAt: number): Promise { diff --git a/ts/node/database_utility.ts b/ts/node/database_utility.ts index 428d7754b..f36f15135 100644 --- a/ts/node/database_utility.ts +++ b/ts/node/database_utility.ts @@ -242,29 +242,29 @@ export function rebuildFtsTable(db: BetterSqlite3.Database) { db.exec(` -- Then we create our full-text search table and populate it CREATE VIRTUAL TABLE ${MESSAGES_FTS_TABLE} - USING fts5(id UNINDEXED, body); - INSERT INTO ${MESSAGES_FTS_TABLE}(id, body) - SELECT id, body FROM ${MESSAGES_TABLE}; + USING fts5(body); + INSERT INTO ${MESSAGES_FTS_TABLE}(rowid, body) + SELECT rowid, body FROM ${MESSAGES_TABLE}; -- Then we set up triggers to keep the full-text search table up to date CREATE TRIGGER messages_on_insert AFTER INSERT ON ${MESSAGES_TABLE} BEGIN INSERT INTO ${MESSAGES_FTS_TABLE} ( - id, + rowid, body ) VALUES ( - new.id, + new.rowid, new.body ); END; CREATE TRIGGER messages_on_delete AFTER DELETE ON ${MESSAGES_TABLE} BEGIN - DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id; + DELETE FROM ${MESSAGES_FTS_TABLE} WHERE rowid = old.rowid; END; CREATE TRIGGER messages_on_update AFTER UPDATE ON ${MESSAGES_TABLE} WHEN new.body <> old.body BEGIN - DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id; + DELETE FROM ${MESSAGES_FTS_TABLE} WHERE rowid = old.rowid; INSERT INTO ${MESSAGES_FTS_TABLE}( - id, + rowid, body ) VALUES ( - new.id, + new.rowid, new.body ); END; diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index 6f3efd3ea..9c25bf3f4 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -33,6 +33,7 @@ import { } from '../database_utility'; import { getIdentityKeys, sqlNode } from '../sql'; +import { sleepFor } from '../../session/utils/Promise'; const hasDebugEnvVariable = Boolean(process.env.SESSION_DEBUG); @@ -100,6 +101,7 @@ const LOKI_SCHEMA_VERSIONS = [ updateToSessionSchemaVersion29, updateToSessionSchemaVersion30, updateToSessionSchemaVersion31, + updateToSessionSchemaVersion32, ]; function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) { @@ -1204,7 +1206,6 @@ function updateToSessionSchemaVersion29(currentVersion: number, db: BetterSqlite conversationId );`); rebuildFtsTable(db); - // Keeping this empty migration because some people updated to this already, even if it is not needed anymore writeSessionSchemaVersion(targetVersion, db); })(); @@ -1835,6 +1836,26 @@ function updateToSessionSchemaVersion31(currentVersion: number, db: BetterSqlite })(); } +function updateToSessionSchemaVersion32(currentVersion: number, db: BetterSqlite3.Database) { + const targetVersion = 32; + if (currentVersion >= targetVersion) { + return; + } + + console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`); + + db.transaction(() => { + db.exec(`CREATE INDEX messages_conversationId ON ${MESSAGES_TABLE} ( + conversationId + );`); + dropFtsAndTriggers(db); + rebuildFtsTable(db); + writeSessionSchemaVersion(targetVersion, db); + })(); + + console.log(`updateToSessionSchemaVersion${targetVersion}: success!`); +} + export function printTableColumns(table: string, db: BetterSqlite3.Database) { console.info(db.pragma(`table_info('${table}');`)); } @@ -1849,7 +1870,7 @@ function writeSessionSchemaVersion(newVersion: number, db: BetterSqlite3.Databas ).run({ newVersion }); } -export function updateSessionSchema(db: BetterSqlite3.Database) { +export async function updateSessionSchema(db: BetterSqlite3.Database) { const result = db .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema';`) .get(); @@ -1866,5 +1887,8 @@ export function updateSessionSchema(db: BetterSqlite3.Database) { for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) { const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index]; runSchemaUpdate(lokiSchemaVersion, db); + if (index > lokiSchemaVersion) { + await sleepFor(200); // give some time for the UI to not freeze between 2 migrations + } } } diff --git a/ts/node/migration/signalMigrations.ts b/ts/node/migration/signalMigrations.ts index 0c23a7258..ff4caed75 100644 --- a/ts/node/migration/signalMigrations.ts +++ b/ts/node/migration/signalMigrations.ts @@ -527,7 +527,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion11, ]; -export function updateSchema(db: BetterSqlite3.Database) { +export async function updateSchema(db: BetterSqlite3.Database) { const sqliteVersion = getSQLiteVersion(db); const sqlcipherVersion = getSQLCipherVersion(db); const userVersion = getUserVersion(db); @@ -545,7 +545,7 @@ export function updateSchema(db: BetterSqlite3.Database) { const runSchemaUpdate = SCHEMA_VERSIONS[index]; runSchemaUpdate(schemaVersion, db); } - updateSessionSchema(db); + await updateSessionSchema(db); } function migrateSchemaVersion(db: BetterSqlite3.Database) { diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 863f29e6e..6b227126e 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -28,7 +28,6 @@ import { ATTACHMENT_DOWNLOADS_TABLE, CLOSED_GROUP_V2_KEY_PAIRS_TABLE, CONVERSATIONS_TABLE, - dropFtsAndTriggers, formatRowOfConversation, GUARD_NODE_TABLE, HEX_KEY, @@ -41,7 +40,6 @@ import { NODES_FOR_PUBKEY_TABLE, objectToJSON, OPEN_GROUP_ROOMS_V2_TABLE, - rebuildFtsTable, toSqliteBoolean, } from './database_utility'; import { LocaleMessagesType } from './locale'; // checked - only node @@ -166,7 +164,7 @@ async function initializeSql({ if (!db) { throw new Error('db is not set'); } - updateSchema(db); + await updateSchema(db); // test database @@ -698,9 +696,9 @@ function searchMessages(query: string, limit: number) { ${MESSAGES_TABLE}.json, snippet(${MESSAGES_FTS_TABLE}, -1, '<>', '<>', '...', 5) as snippet FROM ${MESSAGES_FTS_TABLE} - INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.id = ${MESSAGES_TABLE}.id + INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.rowid = ${MESSAGES_TABLE}.rowid WHERE - ${MESSAGES_FTS_TABLE} match $query + ${MESSAGES_FTS_TABLE}.body match $query ${orderByMessageCoalesceClause} LIMIT $limit;` ) @@ -958,11 +956,13 @@ function removeMessagesByIds(ids: Array, instance?: BetterSqlite3.Databa if (!ids.length) { throw new Error('removeMessagesByIds: No ids to delete!'); } + const start = Date.now(); - // Our node interface doesn't seem to allow you to replace one single ? with an array + // TODO we might need to do the same thing as assertGlobalInstanceOrInstance(instance) .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id IN ( ${ids.map(() => '?').join(', ')} );`) .run(ids); + console.log(`removeMessagesByIds of length ${ids.length} took ${Date.now() - start}ms`); } function removeAllMessagesInConversation( @@ -972,11 +972,13 @@ function removeAllMessagesInConversation( if (!conversationId) { return; } + const inst = assertGlobalInstanceOrInstance(instance); - // Our node interface doesn't seem to allow you to replace one single ? with an array - assertGlobalInstanceOrInstance(instance) - .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId`) - .run({ conversationId }); + inst.transaction(() => { + inst + .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId`) + .run({ conversationId }); + })(); } function getMessageIdsFromServerIds(serverIds: Array, conversationId: string) { @@ -2233,7 +2235,6 @@ function cleanUpOldOpengroupsOnStart() { // first remove very old messages for each opengroups const db = assertGlobalInstance(); db.transaction(() => { - dropFtsAndTriggers(db); v2ConvosIds.forEach(convoId => { const messagesInConvoBefore = getMessagesCountByConversation(convoId); @@ -2316,8 +2317,6 @@ function cleanUpOldOpengroupsOnStart() { } cleanUpMessagesJson(); - - rebuildFtsTable(db); })(); } diff --git a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts index f50135a14..0cef20c86 100644 --- a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts @@ -294,7 +294,7 @@ async function queueNewJobIfNeeded() { !lastRunConfigSyncJobTimestamp || lastRunConfigSyncJobTimestamp < Date.now() - defaultMsBetweenRetries ) { - window.log.debug('Scheduling ConfSyncJob: ASAP'); + // window.log.debug('Scheduling ConfSyncJob: ASAP'); // we postpone by 1000ms to make sure whoever is adding this job is done with what is needs to do first // this call will make sure that there is only one configuration sync job at all times await runners.configurationSyncRunner.addJob( @@ -305,7 +305,7 @@ async function queueNewJobIfNeeded() { const diff = Math.max(Date.now() - lastRunConfigSyncJobTimestamp, 0); // but we want to run every 30, so what we need is actually `30-10` from now = 20 const leftBeforeNextTick = Math.max(defaultMsBetweenRetries - diff, 1000); - window.log.debug('Scheduling ConfSyncJob: LATER'); + // window.log.debug('Scheduling ConfSyncJob: LATER'); await runners.configurationSyncRunner.addJob( new ConfigurationSyncJob({ nextAttemptTimestamp: Date.now() + leftBeforeNextTick }) From dc3e8450e9a45e4f1b80ff326fa03f978366f0fa Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 27 Jun 2023 11:08:00 +0200 Subject: [PATCH 3/4] fix: memoize selected conversation props to avoid unneeded rerenders --- package.json | 1 + password_preload.js | 8 --- .../SessionMessagesListContainer.tsx | 6 +- .../composition/CompositionBox.tsx | 7 +- ts/data/data.ts | 64 ++++++++++++------- ts/node/sql.ts | 9 +-- ts/state/selectors/conversations.ts | 13 +++- ts/state/selectors/selectedConversation.ts | 7 +- ts/state/smart/SessionConversation.ts | 6 +- 9 files changed, 66 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 8f496c186..9dcd755e4 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "scripts": { "start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod$MULTI electron .", + "start-dev": "cross-env NODE_ENV=development NODE_APP_INSTANCE=devprod$MULTI electron .", "build-everything": "yarn clean && yarn protobuf && yarn update-git-info && yarn sass && tsc && yarn build:workers", "build-everything:watch": "yarn clean && yarn protobuf && yarn update-git-info && yarn sass && yarn build:workers && tsc -w", "build:workers": "yarn worker:utils && yarn worker:libsession", diff --git a/password_preload.js b/password_preload.js index 18e3aad60..7463ad9b0 100644 --- a/password_preload.js +++ b/password_preload.js @@ -20,14 +20,6 @@ window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; -const { SessionPasswordPrompt } = require('./ts/components/SessionPasswordPrompt'); - -window.Signal = { - Components: { - SessionPasswordPrompt, - }, -}; - window.clearLocalData = async () => { window.log.info('reset database'); ipcRenderer.send('resetDatabase'); diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx index fccacbddf..efadb4958 100644 --- a/ts/components/conversation/SessionMessagesListContainer.tsx +++ b/ts/components/conversation/SessionMessagesListContainer.tsx @@ -17,12 +17,10 @@ import { import { StateType } from '../../state/reducer'; import { getQuotedMessageToAnimate, + getSelectedConversation, getSortedMessagesOfSelectedConversation, } from '../../state/selectors/conversations'; -import { - getSelectedConversation, - getSelectedConversationKey, -} from '../../state/selectors/selectedConversation'; +import { getSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { SessionMessagesList } from './SessionMessagesList'; import { TypingBubble } from './TypingBubble'; import { ConversationMessageRequestButtons } from './MessageRequestButtons'; diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx index 7a0547e5e..a01548740 100644 --- a/ts/components/conversation/composition/CompositionBox.tsx +++ b/ts/components/conversation/composition/CompositionBox.tsx @@ -31,7 +31,11 @@ import { ToastUtils } from '../../../session/utils'; import { ReduxConversationType } from '../../../state/ducks/conversations'; import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments'; import { StateType } from '../../../state/reducer'; -import { getMentionsInput, getQuotedMessage } from '../../../state/selectors/conversations'; +import { + getMentionsInput, + getQuotedMessage, + getSelectedConversation, +} from '../../../state/selectors/conversations'; import { AttachmentUtil } from '../../../util'; import { Flex } from '../../basic/Flex'; import { CaptionEditor } from '../../CaptionEditor'; @@ -53,7 +57,6 @@ import styled from 'styled-components'; import { FixedBaseEmoji } from '../../../types/Reaction'; import { getSelectedCanWrite, - getSelectedConversation, getSelectedConversationKey, } from '../../../state/selectors/selectedConversation'; import { SettingsKey } from '../../../data/settings-key'; diff --git a/ts/data/data.ts b/ts/data/data.ts index 07acd7a16..6ecbfe3bd 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -497,35 +497,51 @@ async function getSeenMessagesByHashList(hashes: Array): Promise { } async function removeAllMessagesInConversation(conversationId: string): Promise { + const startFunction = Date.now(); let start = Date.now(); - const messages = await getLastMessagesByConversation(conversationId, 50, false); - window.log.info( - `removeAllMessagesInConversation ${conversationId} ${messages.length} took ${Date.now() - - start}` - ); - if (!messages.length) { - return; - } - // Note: It's very important that these models are fully hydrated because - // we need to delete all associated on-disk files along with the database delete. - // eslint-disable-next-line no-await-in-loop - - start = Date.now(); - for (let index = 0; index < messages.length; index++) { - const message = messages.at(index); - await message.cleanup(); - } - window.log.info( - `removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${Date.now() - - start}ms` - ); - start = Date.now(); + let messages; + do { + // Yes, we really want the await in the loop. We're deleting 500 at a + // time so we don't use too much memory. + // eslint-disable-next-line no-await-in-loop + messages = await getLastMessagesByConversation(conversationId, 1000, false); + if (!messages.length) { + return; + } + window.log.info( + `removeAllMessagesInConversation getLastMessagesByConversation ${conversationId} ${ + messages.length + } took ${Date.now() - start}ms` + ); + + // Note: It's very important that these models are fully hydrated because + // we need to delete all associated on-disk files along with the database delete. + const ids = messages.map(message => message.id); + start = Date.now(); + for (let index = 0; index < messages.length; index++) { + const message = messages.at(index); + // eslint-disable-next-line no-await-in-loop + await message.cleanup(); + } + window.log.info( + `removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${Date.now() - + start}ms` + ); + start = Date.now(); + + // eslint-disable-next-line no-await-in-loop + await channels.removeMessagesByIds(ids); + window.log.info( + `removeAllMessagesInConversation: removeMessagesByIds ${conversationId} took ${Date.now() - + start}ms` + ); + } while (messages.length); - // eslint-disable-next-line no-await-in-loop await channels.removeAllMessagesInConversation(conversationId); window.log.info( - `removeAllMessagesInConversation: ${conversationId} took ${Date.now() - start}ms` + `removeAllMessagesInConversation: complete time ${conversationId} took ${Date.now() - + startFunction}ms` ); } diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 6b227126e..47d40da65 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -958,7 +958,6 @@ function removeMessagesByIds(ids: Array, instance?: BetterSqlite3.Databa } const start = Date.now(); - // TODO we might need to do the same thing as assertGlobalInstanceOrInstance(instance) .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE id IN ( ${ids.map(() => '?').join(', ')} );`) .run(ids); @@ -974,11 +973,9 @@ function removeAllMessagesInConversation( } const inst = assertGlobalInstanceOrInstance(instance); - inst.transaction(() => { - inst - .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId`) - .run({ conversationId }); - })(); + inst + .prepare(`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId`) + .run({ conversationId }); } function getMessageIdsFromServerIds(serverIds: Array, conversationId: string) { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index f9d4245e2..c9c44fa9e 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -31,7 +31,7 @@ import { getIntl } from './user'; import { filter, isEmpty, isNumber, pick, sortBy } from 'lodash'; import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions'; -import { getSelectedConversation, getSelectedConversationKey } from './selectedConversation'; +import { getSelectedConversationKey } from './selectedConversation'; import { getModeratorsOutsideRedux } from './sogsRoomInfo'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -626,6 +626,17 @@ export const isFirstUnreadMessageIdAbove = createSelector( const getMessageId = (_whatever: any, id: string | undefined) => id; +/** + * A lot of our UI changes on the main panel need to happen quickly (composition box). + */ +export const getSelectedConversation = createSelector( + getConversationLookup, + getSelectedConversationKey, + (lookup, selectedConvo) => { + return selectedConvo ? lookup[selectedConvo] : undefined; + } +); + // tslint:disable: cyclomatic-complexity export const getMessagePropsByMessageId = createSelector( diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index a1e186d9c..31523151f 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -3,9 +3,9 @@ import { useSelector } from 'react-redux'; import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes'; import { PubKey } from '../../session/types'; import { UserUtils } from '../../session/utils'; -import { ReduxConversationType } from '../ducks/conversations'; import { StateType } from '../reducer'; import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo'; +import { getSelectedConversation } from './conversations'; /** * Returns the formatted text for notification setting. @@ -58,11 +58,6 @@ export const getSelectedConversationKey = (state: StateType): string | undefined return state.conversations.selectedConversation; }; -export const getSelectedConversation = (state: StateType): ReduxConversationType | undefined => { - const selected = getSelectedConversationKey(state); - return selected ? state.conversations.conversationLookup[selected] : undefined; -}; - /** * Returns true if the current conversation selected is a public group and false otherwise. */ diff --git a/ts/state/smart/SessionConversation.ts b/ts/state/smart/SessionConversation.ts index c5850f2f1..6b40c2549 100644 --- a/ts/state/smart/SessionConversation.ts +++ b/ts/state/smart/SessionConversation.ts @@ -6,15 +6,13 @@ import { getHasOngoingCallWithFocusedConvo } from '../selectors/call'; import { getIsSelectedConvoInitialLoadingInProgress, getLightBoxOptions, + getSelectedConversation, getSelectedMessageIds, getSortedMessagesOfSelectedConversation, isMessageDetailView, isRightPanelShowing, } from '../selectors/conversations'; -import { - getSelectedConversation, - getSelectedConversationKey, -} from '../selectors/selectedConversation'; +import { getSelectedConversationKey } from '../selectors/selectedConversation'; import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments'; import { getTheme } from '../selectors/theme'; import { getOurNumber } from '../selectors/user'; From 43badfa1343625fe96cd924a3a2793aafe4f243e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 28 Jun 2023 13:41:40 +0200 Subject: [PATCH 4/4] fix: virtualize list of search result so input typing is fast --- _locales/en/messages.json | 3 +- package.json | 4 +- ts/components/leftpane/LeftPane.tsx | 12 +-- .../leftpane/LeftPaneMessageSection.tsx | 10 +- ts/components/search/MessageSearchResults.tsx | 8 +- ts/components/search/SearchResults.tsx | 94 ++++++++++++------- ts/state/selectors/search.ts | 33 ++++++- ts/types/LocalizerKeys.ts | 1 + yarn.lock | 32 +++++-- 9 files changed, 133 insertions(+), 64 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 35e26b3f4..9c76d2416 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -70,9 +70,10 @@ "show": "Show", "sessionMessenger": "Session", "noSearchResults": "No results found for \"$searchTerm$\"", - "conversationsHeader": "Contacts and Groups", + "conversationsHeader": "Contacts and Groups: $count$", "contactsHeader": "Contacts", "messagesHeader": "Conversations", + "searchMessagesHeader": "Messages: $count$", "settingsHeader": "Settings", "typingAlt": "Typing animation for this conversation", "contactAvatarAlt": "Avatar for contact $name$", diff --git a/package.json b/package.json index 9dcd755e4..68aa9af6f 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@emoji-mart/data": "^1.0.6", "@emoji-mart/react": "^1.0.1", "@reduxjs/toolkit": "1.8.5", + "@types/react-mentions": "^4.1.8", "abort-controller": "3.0.0", "auto-bind": "^4.0.0", "backbone": "1.3.3", @@ -122,7 +123,7 @@ "react-draggable": "^4.4.4", "react-h5-audio-player": "^3.2.0", "react-intersection-observer": "^8.30.3", - "react-mentions": "^4.2.0", + "react-mentions": "^4.4.9", "react-portal": "^4.2.0", "react-qr-svg": "^2.2.1", "react-redux": "8.0.4", @@ -169,7 +170,6 @@ "@types/rc-slider": "^8.6.5", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", - "@types/react-mentions": "^4.1.1", "@types/react-mic": "^12.4.1", "@types/react-portal": "^4.0.2", "@types/react-redux": "^7.1.24", diff --git a/ts/components/leftpane/LeftPane.tsx b/ts/components/leftpane/LeftPane.tsx index f113fa594..e02c069a6 100644 --- a/ts/components/leftpane/LeftPane.tsx +++ b/ts/components/leftpane/LeftPane.tsx @@ -4,7 +4,7 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { SectionType } from '../../state/ducks/section'; import { getLeftPaneConversationIds } from '../../state/selectors/conversations'; -import { getSearchResultsIdsOnly, isSearching } from '../../state/selectors/search'; +import { getHasSearchResults } from '../../state/selectors/search'; import { getFocusedSection, getOverlayMode } from '../../state/selectors/section'; import { SessionTheme } from '../../themes/SessionTheme'; import { SessionToastContainer } from '../SessionToastContainer'; @@ -22,18 +22,14 @@ const StyledLeftPane = styled.div` `; const InnerLeftPaneMessageSection = () => { - const showSearch = useSelector(isSearching); - - const searchResults = useSelector(getSearchResultsIdsOnly); - + const hasSearchResults = useSelector(getHasSearchResults); const conversationIds = useSelector(getLeftPaneConversationIds); const overlayMode = useSelector(getOverlayMode); return ( - // tslint:disable-next-line: use-simple-attributes ); diff --git a/ts/components/leftpane/LeftPaneMessageSection.tsx b/ts/components/leftpane/LeftPaneMessageSection.tsx index 98504cd85..95ef19647 100644 --- a/ts/components/leftpane/LeftPaneMessageSection.tsx +++ b/ts/components/leftpane/LeftPaneMessageSection.tsx @@ -1,7 +1,7 @@ import autoBind from 'auto-bind'; import React from 'react'; import { AutoSizer, List, ListRowProps } from 'react-virtualized'; -import { SearchResults, SearchResultsProps } from '../search/SearchResults'; +import { SearchResults } from '../search/SearchResults'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; import { MessageRequestsBanner } from './MessageRequestsBanner'; @@ -21,7 +21,7 @@ import { assertUnreachable } from '../../types/sqlSharedTypes'; export interface Props { conversationIds?: Array; - searchResults?: SearchResultsProps; + hasSearchResults: boolean; overlayMode: OverlayMode | undefined; } @@ -88,10 +88,10 @@ export class LeftPaneMessageSection extends React.Component { }; public renderList(): JSX.Element { - const { conversationIds, searchResults } = this.props; + const { conversationIds, hasSearchResults } = this.props; - if (searchResults) { - return ; + if (hasSearchResults) { + return ; } if (!conversationIds) { diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx index b53a34e69..79f0df58f 100644 --- a/ts/components/search/MessageSearchResults.tsx +++ b/ts/components/search/MessageSearchResults.tsx @@ -6,7 +6,7 @@ import { ContactName } from '../conversation/ContactName'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { Timestamp } from '../conversation/Timestamp'; import { MessageBodyHighlight } from '../basic/MessageBodyHighlight'; -import styled from 'styled-components'; +import styled, { CSSProperties } from 'styled-components'; import { MessageAttributes } from '../../models/messageType'; import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector'; import { UserUtils } from '../../session/utils'; @@ -172,7 +172,9 @@ const StyledTimestampContaimer = styled.div` color: var(--conversation-tab-text-color); `; -export const MessageSearchResult = (props: MessageResultProps) => { +type MessageSearchResultProps = MessageResultProps & { style: CSSProperties }; + +export const MessageSearchResult = (props: MessageSearchResultProps) => { const { id, conversationId, @@ -183,6 +185,7 @@ export const MessageSearchResult = (props: MessageResultProps) => { serverTimestamp, timestamp, direction, + style, } = props; /** destination is only used for search results (showing the `from:` and `to`) @@ -210,6 +213,7 @@ export const MessageSearchResult = (props: MessageResultProps) => { return ( { void openConversationToSpecificMessage({ diff --git a/ts/components/search/SearchResults.tsx b/ts/components/search/SearchResults.tsx index 894a03759..2b39a07b1 100644 --- a/ts/components/search/SearchResults.tsx +++ b/ts/components/search/SearchResults.tsx @@ -1,13 +1,16 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { CSSProperties } from 'styled-components'; import { ConversationListItem } from '../leftpane/conversation-list-item/ConversationListItem'; -import { MessageResultProps, MessageSearchResult } from './MessageSearchResults'; - -export type SearchResultsProps = { - contactsAndGroupsIds: Array; - messages: Array; - searchTerm: string; -}; +import { MessageSearchResult } from './MessageSearchResults'; +import { AutoSizer, List } from 'react-virtualized'; +import { useSelector } from 'react-redux'; +import { + SearchResultsMergedListItem, + getHasSearchResults, + getSearchResultsList, + getSearchTerm, +} from '../../state/selectors/search'; +import { isString } from 'lodash'; const StyledSeparatorSection = styled.div` height: 36px; @@ -35,37 +38,58 @@ const NoResults = styled.div` text-align: center; `; -export const SearchResults = (props: SearchResultsProps) => { - const { contactsAndGroupsIds, messages, searchTerm } = props; +const SectionHeader = ({ title, style }: { title: string; style: CSSProperties }) => { + return {title}; +}; - const haveContactsAndGroup = Boolean(contactsAndGroupsIds?.length); - const haveMessages = Boolean(messages?.length); - const noResults = !haveContactsAndGroup && !haveMessages; +function isContact(item: SearchResultsMergedListItem): item is { contactConvoId: string } { + return (item as any).contactConvoId !== undefined; +} +const VirtualizedList = () => { + const searchResultList = useSelector(getSearchResultsList); return ( - - {noResults ? {window.i18n('noSearchResults', [searchTerm])} : null} - {haveContactsAndGroup ? ( - <> - {window.i18n('conversationsHeader')} - {contactsAndGroupsIds.map(conversationId => ( - - ))} - - ) : null} + + {({ height, width }) => ( + { + return isString(searchResultList[rowPos.index]) ? 36 : 64; + }} + rowRenderer={({ index, key, style }) => { + const row = searchResultList[index]; + if (!row) { + return null; + } + if (isString(row)) { + return ; + } + if (isContact(row)) { + return ( + + ); + } + return ; + }} + width={width} + autoHeight={false} + /> + )} + + ); +}; + +export const SearchResults = () => { + const searchTerm = useSelector(getSearchTerm); + const hasSearchResults = useSelector(getHasSearchResults); - {haveMessages && ( - <> - - {`${window.i18n('messagesHeader')}: ${messages.length}`} - - {messages.map(message => ( - - ))} - + return ( + + {!hasSearchResults ? ( + {window.i18n('noSearchResults', [searchTerm])} + ) : ( + )} ); diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index b05e06cbf..6238742b8 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -1,11 +1,12 @@ import { createSelector } from '@reduxjs/toolkit'; -import { compact } from 'lodash'; +import { compact, isEmpty } from 'lodash'; import { StateType } from '../reducer'; import { ConversationLookupType } from '../ducks/conversations'; import { SearchStateType } from '../ducks/search'; import { getConversationLookup } from './conversations'; +import { MessageResultProps } from '../../components/search/MessageSearchResults'; export const getSearch = (state: StateType): SearchStateType => state.search; @@ -41,6 +42,10 @@ const getSearchResults = createSelector( } ); +export const getSearchTerm = createSelector([getSearchResults], searchResult => { + return searchResult.searchTerm; +}); + export const getSearchResultsIdsOnly = createSelector([getSearchResults], searchState => { return { ...searchState, @@ -48,6 +53,32 @@ export const getSearchResultsIdsOnly = createSelector([getSearchResults], search }; }); +export const getHasSearchResults = createSelector([getSearchResults], searchState => { + return !isEmpty(searchState.contactsAndGroups) || !isEmpty(searchState.messages); +}); + export const getSearchResultsContactOnly = createSelector([getSearchResults], searchState => { return searchState.contactsAndGroups.filter(m => m.isPrivate).map(m => m.id); }); + +/** + * + * When type is string, we render a sectionHeader. + * When type just has a conversationId field, we render a ConversationListItem. + * When type is MessageResultProps we render a MessageSearchResult + */ +export type SearchResultsMergedListItem = string | { contactConvoId: string } | MessageResultProps; + +export const getSearchResultsList = createSelector([getSearchResults], searchState => { + const { contactsAndGroups, messages } = searchState; + const builtList: Array = []; + if (contactsAndGroups.length) { + builtList.push(window.i18n('conversationsHeader', [`${contactsAndGroups.length}`])); + builtList.push(...contactsAndGroups.map(m => ({ contactConvoId: m.id }))); + } + if (messages.length) { + builtList.push(window.i18n('searchMessagesHeader', [`${messages.length}`])); + builtList.push(...messages); + } + return builtList; +}); diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 1c7f7a0e0..7401354a6 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -73,6 +73,7 @@ export type LocalizerKeys = | 'conversationsHeader' | 'contactsHeader' | 'messagesHeader' + | 'searchMessagesHeader' | 'settingsHeader' | 'typingAlt' | 'contactAvatarAlt' diff --git a/yarn.lock b/yarn.lock index 97425bef0..2b8c5284f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -411,13 +411,20 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.4": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" + integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" @@ -1192,10 +1199,10 @@ dependencies: "@types/react" "^17" -"@types/react-mentions@^4.1.1": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.1.6.tgz#0ecdb61785c22edbf9c7d6718505d4814ad3a65c" - integrity sha512-f4/BdnjlMxT47q+WqlcYYwFABbBMVQrDoFFeMeljtFC5nnR9/x8TOFmN18BJKgNuWMgivy9uE5EKtsjlay751w== +"@types/react-mentions@^4.1.8": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.1.8.tgz#4bebe54c5c74181d8eedf1e613a208d03b4a8d7e" + integrity sha512-Go86ozdnh0FTNbiGiDPAcNqYqtab9iGzLOgZPYUKrnhI4539jGzfJtP6rFHcXgi9Koe58yhkeyKYib6Ucul/sQ== dependencies: "@types/react" "*" @@ -6551,10 +6558,10 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-mentions@^4.2.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.3.2.tgz#93a2a674648f31d2b40e8c90d30b94c377cbb582" - integrity sha512-NV8ixuE5W9zuvBNWLpPlO+f4QYEkR+p6mR3Jfpfcbytrqqn2nbVb27YXE/M4qSP8N8C+ktgeMUV4jVhm86gt1A== +react-mentions@^4.4.9: + version "4.4.9" + resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.4.9.tgz#5f68c7978c107518646c5c34c47515c30259e23a" + integrity sha512-CUDt8GOVbAmo3o+a8l1UxcJ/gJMdFdqeiJM3U5+krcNoUwyKv7Zcy67WfFZQJfChpJ8LTiD0FtCSRoyEzC6Ysw== dependencies: "@babel/runtime" "7.4.5" invariant "^2.2.4" @@ -6759,7 +6766,12 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.2: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==