diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..f764d4404cee --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "vendor/sqldelight"] + path = vendor/sqldelight + url = https://github.com/MohamadJaara/sqldelight.git + branch = 2.2.1-with-custom-notify diff --git a/build.gradle.kts b/build.gradle.kts index ecf03e11fa51..c5a7902046cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,7 @@ import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension buildscript { repositories { + maven(url = uri("$rootDir/vendor/sqldelight/build/localMaven")) google() mavenCentral() } @@ -31,7 +32,6 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:${libs.versions.agp.get()}") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}") - classpath("app.cash.sqldelight:gradle-plugin:${libs.versions.sqldelight.get()}") classpath("org.jetbrains.dokka:dokka-gradle-plugin:${libs.versions.dokka.get()}") classpath("com.google.protobuf:protobuf-gradle-plugin:${libs.versions.protobufCodegen.get()}") classpath("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${libs.versions.detekt.get()}") @@ -40,6 +40,7 @@ buildscript { repositories { wireDetektRulesRepo() + maven(url = uri("$rootDir/vendor/sqldelight/build/localMaven")) google() mavenCentral() maven(url = "https://oss.sonatype.org/content/repositories/snapshots") @@ -121,6 +122,7 @@ val kaliumGitHash: Provider = providers.environmentVariable("GITHUB_SHA" allprojects { version = kaliumGitHash.get() repositories { + maven(url = uri("$rootDir/vendor/sqldelight/build/localMaven")) google() mavenCentral() } diff --git a/data/persistence/build.gradle.kts b/data/persistence/build.gradle.kts index 1b665388faad..dff96dd6e0c1 100644 --- a/data/persistence/build.gradle.kts +++ b/data/persistence/build.gradle.kts @@ -19,7 +19,7 @@ @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.kotlin.serialization) - id(libs.plugins.sqldelight.get().pluginId) + alias(libs.plugins.sqldelight) id(libs.plugins.kalium.library.get().pluginId) alias(libs.plugins.ksp) alias(libs.plugins.mockative) @@ -35,6 +35,7 @@ sqldelight { databases { create("UserDatabase") { dialect(libs.sqldelight.dialect.get().toString()) + enableCustomQueryKeys.set(true) packageName.set("com.wire.kalium.persistence") val sourceFolderName = "db_user" srcDirs.setFrom(listOf("src/commonMain/$sourceFolderName")) diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq index c201914be627..a41595884687 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq @@ -97,6 +97,11 @@ ORDER BY name COLLATE NOCASE ASC; selectConversationDetailsWithEvents: +-- @CustomKey conversation_list_membership +-- @CustomKey conversation_list_order +-- @CustomKey conversation_list_last_message +-- @CustomKey conversation_list_unread +-- @CustomKey conversation_list_draft SELECT * FROM ConversationDetailsWithEvents WHERE archived = :fromArchive @@ -131,6 +136,11 @@ WHERE OFFSET :offset; selectConversationDetailsWithEventsFromSearch: +-- @CustomKey conversation_list_membership +-- @CustomKey conversation_list_order +-- @CustomKey conversation_list_last_message +-- @CustomKey conversation_list_unread +-- @CustomKey conversation_list_draft SELECT * FROM ConversationDetailsWithEvents WHERE archived = :fromArchive @@ -166,6 +176,7 @@ LIMIT :limit OFFSET :offset; countConversations: +-- @CustomKey conversation_list_membership SELECT COUNT(*) FROM Conversation WHERE @@ -227,6 +238,7 @@ WHERE AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END; countConversationDetailsWithEventsFromSearch: +-- @CustomKey conversation_list_membership SELECT COUNT(*) FROM ConversationDetails WHERE ConversationDetails.type IS NOT 'SELF' diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq index 3bd7f668b287..44f69977a0bd 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq @@ -76,17 +76,25 @@ conversationIDByGroupId: SELECT qualified_id, verification_status FROM Conversation WHERE mls_group_id = :groupId; deleteAllConversations: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership DELETE FROM Conversation; deleteConversation: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership DELETE FROM Conversation WHERE qualified_id = ?; markAsDeletedLocally: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET deleted_locally = 1 WHERE qualified_id = ?; insertConversation: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership INSERT INTO Conversation(qualified_id, name, type, team_id, mls_group_id, mls_group_state, mls_epoch, protocol, muted_status, muted_time, creator_id, last_modified_date, last_notified_date, access_list, access_role_list, last_read_date, mls_last_keying_material_update_date, mls_cipher_suite, receipt_mode, message_timer, user_message_timer, incomplete_metadata, archived, archived_date_time, is_channel, channel_access, channel_add_permission, wire_cell, history_sharing_retention_seconds) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(qualified_id) DO UPDATE SET @@ -124,11 +132,15 @@ wire_cell = excluded.wire_cell, history_sharing_retention_seconds = excluded.history_sharing_retention_seconds; updateConversation: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET name = ?, type = ?, team_id = ? WHERE qualified_id = ?; updateConversationGroupState: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET mls_group_state = CASE WHEN mls_group_state = 'ESTABLISHED' THEN :mls_group_state @@ -138,6 +150,8 @@ END WHERE mls_group_id = ?; updateConversationGroupStateByConversationId: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET mls_group_state = CASE WHEN mls_group_state = 'ESTABLISHED' THEN :mls_group_state @@ -147,6 +161,8 @@ END WHERE qualified_id = :conversation_id; updateMlsGroupStateAndCipherSuite: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET mls_group_state = CASE WHEN mls_group_state = 'ESTABLISHED' THEN :mls_group_state @@ -157,6 +173,8 @@ mls_cipher_suite = :mls_cipher_suite WHERE mls_group_id = :mls_group_id; updateMLSGroupIdAndState: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET mls_group_id = :new_group_id, mls_group_state = CASE @@ -193,11 +211,15 @@ SET last_notified_date = ( ); updateConversationModifiedDate: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_order UPDATE Conversation SET last_modified_date = ? WHERE qualified_id = ?; updateConversationModifiedDateToMaxOfSources: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_order UPDATE Conversation SET last_modified_date = ( SELECT MAX(last_modified_date) @@ -274,11 +296,15 @@ selectConversationIds: SELECT qualified_id FROM Conversation WHERE protocol = :protocol AND type = :type AND (:teamId IS NULL OR team_id = :teamId); updateConversationMutingStatus: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_unread UPDATE Conversation SET muted_status = ?, muted_time = ? WHERE qualified_id = ?; updateConversationArchivingStatus: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET archived = ?, archived_date_time = ? WHERE qualified_id = ?; @@ -311,16 +337,23 @@ SELECT sender_user_id FROM Message WHERE id IN ( ) ORDER BY creation_date DESC LIMIT 1; updateConversationName: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership +-- @NotifyCustomKey conversation_list_order UPDATE Conversation SET name = ?, last_modified_date = ? WHERE qualified_id = ?; updateConversationType: +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET type = ? WHERE qualified_id = ?; updateConversationGroupIdAndProtocolInfo { +-- @NotifyCustomKey Conversation +-- @NotifyCustomKey conversation_list_membership UPDATE Conversation SET mls_group_id = :groupId, protocol = :protocol, mls_cipher_suite = :mls_cipher_suite WHERE qualified_id = :qualified_id AND diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq index 1ec7f1e3097d..eb51e44be6dd 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq @@ -16,9 +16,13 @@ CREATE TABLE MessageDraft ( ); deleteDraft: +-- @NotifyCustomKey MessageDraft +-- @NotifyCustomKey conversation_list_draft DELETE FROM MessageDraft WHERE conversation_id = ?; upsertDraft: +-- @NotifyCustomKey MessageDraft +-- @NotifyCustomKey conversation_list_draft INSERT INTO MessageDraft(conversation_id, text, edit_message_id, quoted_message_id, mention_list) VALUES( ?, ?, ?, ?, ?) ON CONFLICT(conversation_id) DO UPDATE SET diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq index 7db2b791dc0e..12d8bed65faa 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq @@ -235,21 +235,36 @@ AND content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL') GROUP BY conversation_id; deleteAllMessages: +-- @NotifyCustomKey Message +-- @NotifyCustomKey conversation_list_last_message +-- @NotifyCustomKey message_search_all DELETE FROM Message; deleteMessage: -DELETE FROM Message WHERE id = ? AND conversation_id = ?; +-- @NotifyCustomKey Message +-- @NotifyCustomKey conversation_list_last_message +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id +DELETE FROM Message WHERE id = :message_id AND conversation_id = :conversation_id; deleteMessageLinkPreviews: DELETE FROM MessageLinkPreview WHERE message_id = ? AND conversation_id = ?; deleteMessageMentions: -DELETE FROM MessageMention WHERE message_id = ? AND conversation_id = ?; +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id +DELETE FROM MessageMention WHERE message_id = :message_id AND conversation_id = :conversation_id; deleteMessageById: +-- @NotifyCustomKey Message +-- @NotifyCustomKey conversation_list_last_message DELETE FROM Message WHERE id = ?; markMessageAsDeleted { + -- @NotifyCustomKey Message + -- @NotifyCustomKey conversation_list_last_message + -- @NotifyCustomKey message_list_:conversation_id + -- @NotifyCustomKey message_search_:conversation_id UPDATE Message SET visibility = 'DELETED' WHERE id = :message_id AND conversation_id = :conversation_id; @@ -264,6 +279,10 @@ markMessageAsDeleted { } markMessageAsEdited: +-- @NotifyCustomKey Message +-- @NotifyCustomKey conversation_list_last_message +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE Message SET last_edit_date = ? WHERE id = ? AND conversation_id = ?; @@ -275,12 +294,20 @@ selectChanges: SELECT changes(); insertOrIgnoreMessage: +-- @NotifyCustomKey Message +-- @NotifyCustomKey conversation_list_last_message +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO Message(id, content_type, conversation_id, creation_date, sender_user_id, sender_client_id, status, last_edit_date, visibility, expects_read_confirmation, expire_after_millis, self_deletion_end_date) -VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); +VALUES(:id, :content_type, :conversation_id, :creation_date, :sender_user_id, :sender_client_id, :status, :last_edit_date, :visibility, :expects_read_confirmation, :expire_after_millis, :self_deletion_end_date); insertMessage: +-- @NotifyCustomKey Message +-- @NotifyCustomKey conversation_list_last_message +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT INTO Message(id, content_type, conversation_id, creation_date, sender_user_id, sender_client_id, status, last_edit_date, visibility, expects_read_confirmation, expire_after_millis, self_deletion_end_date) -VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); +VALUES(:id, :content_type, :conversation_id, :creation_date, :sender_user_id, :sender_client_id, :status, :last_edit_date, :visibility, :expects_read_confirmation, :expire_after_millis, :self_deletion_end_date); insertOrIgnoreBulkSystemMessage: INSERT OR IGNORE INTO Message(id, content_type, conversation_id, creation_date, sender_user_id, sender_client_id, status, visibility, expects_read_confirmation) @@ -291,10 +318,14 @@ INSERT OR IGNORE INTO MessageLinkPreview(message_id, conversation_id, url, url_o VALUES (?, ?, ?, ?, ?, ?, ?); insertMessageMention: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageMention(message_id, conversation_id, start, length, user_id) -VALUES (?, ?, ?, ?, ?); +VALUES (:message_id, :conversation_id, :start, :length, :user_id); insertMessageTextContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageTextContent(message_id, conversation_id, text_body, quoted_message_id, is_quote_verified, is_quoting_self) VALUES(:message_id, :conversation_id, :text_body, :quoted_message_id, :is_quote_verified, CASE WHEN @@ -311,71 +342,105 @@ CASE WHEN 0 ))END); insertMessageRestrictedAssetContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageRestrictedAssetContent(message_id, conversation_id, asset_mime_type,asset_size,asset_name) -VALUES(?, ?, ?,?,?); +VALUES(:message_id, :conversation_id, :asset_mime_type, :asset_size, :asset_name); insertMessageAssetContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageAssetContent(message_id, conversation_id, asset_size, asset_name, asset_mime_type, asset_otr_key, asset_sha256, asset_id, asset_token, asset_domain, asset_encryption_algorithm, asset_width, asset_height, asset_duration_ms, asset_normalized_loudness) -VALUES(?, ?, ?, ?, ?, ? ,?, ?, ?, ?, ?, ?, ?, ?, ?); +VALUES(:message_id, :conversation_id, :asset_size, :asset_name, :asset_mime_type, :asset_otr_key, :asset_sha256, :asset_id, :asset_token, :asset_domain, :asset_encryption_algorithm, :asset_width, :asset_height, :asset_duration_ms, :asset_normalized_loudness); insertMessageUnknownContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageUnknownContent(message_id, conversation_id, unknown_type_name, unknown_encoded_data) -VALUES(?, ?, ?, ?); +VALUES(:message_id, :conversation_id, :unknown_type_name, :unknown_encoded_data); insertMissedCallMessage: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageMissedCallContent(message_id, conversation_id, caller_id) -VALUES(?, ?, ?); +VALUES(:message_id, :conversation_id, :caller_id); insertLocationMessageContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageConversationLocationContent(message_id, conversation_id, latitude, longitude, name, zoom) -VALUES(?, ?, ?, ?, ?, ?); +VALUES(:message_id, :conversation_id, :latitude, :longitude, :name, :zoom); insertSystemMessageContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, text_1, integer_1, boolean_1, list_1, enum_1, blob_1) -VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?); +VALUES(:message_id, :conversation_id, :content_type, :text_1, :integer_1, :boolean_1, :list_1, :enum_1, :blob_1); insertSystemMemberChange: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, list_1, enum_1) -VALUES(?, ?, 'MEMBER_CHANGE', :member_change_list, :member_change_type); +VALUES(:message_id, :conversation_id, 'MEMBER_CHANGE', :member_change_list, :member_change_type); insertSystemFailedDecryption: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, blob_1, boolean_1, integer_1) -VALUES(?, ?, 'FAILED_DECRYPT', :unknown_encoded_data, 0, :error_code); +VALUES(:message_id, :conversation_id, 'FAILED_DECRYPT', :unknown_encoded_data, 0, :error_code); insertSystemConversationRenamed: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, text_1) VALUES(:message_id, :conversation_id, 'CONVERSATION_RENAMED', :conversation_name); insertSystemNewConversationReceiptMode: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, boolean_1) -VALUES(?, ?, 'NEW_CONVERSATION_RECEIPT_MODE', :receipt_mode); +VALUES(:message_id, :conversation_id, 'NEW_CONVERSATION_RECEIPT_MODE', :receipt_mode); insertSystemConversationReceiptModeChanged: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, boolean_1) -VALUES(?, ?, 'CONVERSATION_RECEIPT_MODE_CHANGED', :receipt_mode); +VALUES(:message_id, :conversation_id, 'CONVERSATION_RECEIPT_MODE_CHANGED', :receipt_mode); insertSystemConversationAppsEnabledChanged: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, boolean_1) -VALUES(?, ?, 'CONVERSATION_APPS_ENABLED_CHANGED', :is_apps_enabled); +VALUES(:message_id, :conversation_id, 'CONVERSATION_APPS_ENABLED_CHANGED', :is_apps_enabled); insertSystemConversationTimerChanged: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, integer_1) VALUES(:message_id, :conversation_id, 'CONVERSATION_TIMER_CHANGED', :message_timer); insertSystemFederationTerminated: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, list_1, enum_1) VALUES(:message_id, :conversation_id, 'FEDERATION_TERMINATED', :domain_list, :federation_type); insertSystemConversationProtocolChanged: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, enum_1) VALUES(:message_id, :conversation_id, 'CONVERSATION_PROTOCOL_CHANGED', :protocol); insertSystemLegalHold: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageSystemContent(message_id, conversation_id, content_type, list_1, enum_1) -VALUES(?, ?, 'LEGAL_HOLD', :legal_hold_member_list, :legal_hold_type); +VALUES(:message_id, :conversation_id, 'LEGAL_HOLD', :legal_hold_member_list, :legal_hold_type); -- Update query for system content updateSystemMessageLegalHoldMembers: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE MessageSystemContent SET list_1 = :user_id_list WHERE message_id = :message_id AND conversation_id = :conversation_id AND content_type = 'LEGAL_HOLD'; @@ -392,11 +457,15 @@ WHERE message_id IN ( ); updateMessageStatus: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE Message SET status = ? WHERE id = ? AND conversation_id = ?; updateMessagesStatusIfNotRead: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE Message SET status = ? WHERE id IN ? @@ -404,6 +473,8 @@ AND conversation_id = ? AND status != 'READ'; updateQuotedMessageId: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE MessageTextContent SET quoted_message_id = ? WHERE quoted_message_id = ? AND conversation_id = ?; @@ -412,6 +483,10 @@ selectMessageVisibility: SELECT visibility FROM Message WHERE id = ? AND conversation_id = ?; updateAssetContent { + -- @NotifyCustomKey Message + -- @NotifyCustomKey conversation_list_last_message + -- @NotifyCustomKey message_list_:conversationId + -- @NotifyCustomKey message_search_:conversationId UPDATE OR ROLLBACK Message SET visibility = :visibility WHERE id = :messageId AND conversation_id = :conversationId AND visibility IS NOT 'DELETED'; @@ -422,11 +497,15 @@ updateAssetContent { } updateMessageTextContent: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE MessageTextContent SET text_body = ? WHERE message_id = ? AND conversation_id = ?; updateMessageId: +-- @NotifyCustomKey message_list_:conversationId +-- @NotifyCustomKey message_search_:conversationId UPDATE Message SET id = :newId WHERE id = :oldId AND conversation_id = :conversationId; @@ -438,12 +517,14 @@ selectById: SELECT * FROM MessageDetailsView WHERE id = ? AND conversationId = ?; countByConversationIdAndVisibility: -SELECT count(*) FROM Message WHERE conversation_id = ? AND visibility IN ?; +-- @CustomKey message_list_:conversationId +SELECT count(*) FROM Message WHERE conversation_id = :conversationId AND visibility IN :visibility; selectOldestVisibleMessageTimestampByConversationId: SELECT MIN(creation_date) FROM Message WHERE conversation_id = ? AND visibility = "VISIBLE"; selectByConversationIdAndVisibility: +-- @CustomKey message_list_:conversationId SELECT * FROM MessageDetailsView WHERE conversationId = :conversationId AND visibility IN :visibility ORDER BY date DESC LIMIT :limit OFFSET :offset; countBackupMessages: @@ -493,6 +574,8 @@ ORDER BY Message.creation_date DESC; promoteMessageToSentUpdatingServerTime { +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE Message SET creation_date = CASE WHEN :server_creation_date IS NULL THEN creation_date ELSE :server_creation_date END, @@ -519,20 +602,30 @@ AND visibility = "VISIBLE" AND selfDeletionEndDate <= STRFTIME('%s', 'now') * 1000; -- Checks if message end date is lower than current time in millis markSelfDeletionEndDate: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE Message SET self_deletion_end_date = ? WHERE conversation_id = ? AND id = ?; insertMessageRecipientsFailure: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT OR IGNORE INTO MessageRecipientFailure(message_id, conversation_id, recipient_failure_list, recipient_failure_type) -VALUES(?, ?, ?, ?); +VALUES(:message_id, :conversation_id, :recipient_failure_list, :recipient_failure_type); moveMessages: +-- @NotifyCustomKey message_list_:from +-- @NotifyCustomKey message_list_:to +-- @NotifyCustomKey message_search_:from +-- @NotifyCustomKey message_search_:to UPDATE OR REPLACE Message SET conversation_id = :to WHERE conversation_id = :from; selectConversationMessagesFromSearch: +-- @CustomKey message_search_:conversationId +-- @CustomKey message_search_all SELECT MessageDetailsView.* FROM MessageTextContent AS TextContent JOIN MessageDetailsView @@ -558,6 +651,8 @@ LIMIT :limit OFFSET :offset; countBySearchedMessageAndConversationId: +-- @CustomKey message_search_:conversationId +-- @CustomKey message_search_all SELECT COUNT(*) FROM MessageTextContent AS TextContent JOIN Message @@ -584,6 +679,7 @@ ORDER BY Message.creation_date ASC LIMIT 1; updateAudioMessageNormalizedLoudness: +-- @NotifyCustomKey message_list_:conversation_id UPDATE MessageAssetContent SET asset_normalized_loudness = ? WHERE message_id = ? AND conversation_id = ?; diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Metadata.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Metadata.sq index 2896d08b8e36..55367af7283a 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Metadata.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Metadata.sq @@ -4,15 +4,23 @@ CREATE TABLE Metadata ( ); insertValue: +-- @NotifyCustomKey metadata_:key +-- @NotifyCustomKey Metadata INSERT INTO Metadata(key, stringValue) -VALUES (?, ?) +VALUES (:key, :stringValue) ON CONFLICT(key) DO UPDATE SET stringValue = excluded.stringValue; selectValueByKey: -SELECT stringValue FROM Metadata WHERE key = ?; +-- @CustomKey metadata_:key +SELECT stringValue FROM Metadata WHERE key = :key; deleteValue: -DELETE FROM Metadata WHERE key = ?; +-- @NotifyCustomKey metadata_:key +-- @NotifyCustomKey Metadata +DELETE FROM Metadata WHERE key = :key; + +selectAllMetadata: +SELECT * FROM Metadata; deleteAllExcept: DELETE FROM Metadata WHERE key NOT IN ?; diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq index 60a47e422032..42ac25abe750 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq @@ -16,17 +16,25 @@ doesMessageExist: SELECT 1 FROM Message WHERE id = :message_id AND conversation_id = :conversation_id; deleteAllReactionsOnMessageFromUser: -DELETE FROM Reaction WHERE message_id = ? AND conversation_id = ? AND sender_id = ?; +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id +DELETE FROM Reaction WHERE message_id = :message_id AND conversation_id = :conversation_id AND sender_id = :sender_id; deleteReaction: -DELETE FROM Reaction WHERE message_id = ? AND conversation_id = ? AND sender_id = ? AND emoji = ?; +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id +DELETE FROM Reaction WHERE message_id = :message_id AND conversation_id = :conversation_id AND sender_id = :sender_id AND emoji = :emoji; insertReaction: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT INTO Reaction(message_id, conversation_id, sender_id, emoji, date) -VALUES(?, ?, ?, ?, ?); +VALUES(:message_id, :conversation_id, :sender_id, :emoji, :date); deleteAllReactionsForMessage: -DELETE FROM Reaction WHERE message_id = ? AND conversation_id = ?; +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id +DELETE FROM Reaction WHERE message_id = :message_id AND conversation_id = :conversation_id; selectByMessageIdAndConversationIdAndSenderId: SELECT * FROM Reaction WHERE message_id = ? AND conversation_id = ? AND sender_id = ?; diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq index 708d5ca7b029..c7662f95b61e 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq @@ -16,8 +16,16 @@ CREATE INDEX receipt_msg_type ON Receipt(message_id, type); -- TODO: Cast to proper types when/if SQLDelight supports it: insertReceipt: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id WITH insertion(message_id, conversation_id, user_id, type, date) AS( - VALUES (CAST(? AS TEXT), CAST(? AS TEXT), CAST(? AS TEXT), CAST(? AS TEXT), CAST(? AS TEXT)) + VALUES ( + CAST(:message_id AS TEXT), + CAST(:conversation_id AS TEXT), + CAST(:user_id AS TEXT), + CAST(:type AS TEXT), + CAST(:date AS TEXT) + ) ) INSERT OR IGNORE INTO Receipt(message_id, conversation_id, user_id, type, date) SELECT diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq index ddd3eb926cef..9e1a47262d05 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq @@ -15,16 +15,24 @@ CREATE INDEX unread_event_conv_tyoe ON UnreadEvent(conversation_id, type); CREATE INDEX unread_event_conv_date ON UnreadEvent(conversation_id, creation_date); deleteUnreadEvent: +-- @NotifyCustomKey UnreadEvent +-- @NotifyCustomKey conversation_list_unread DELETE FROM UnreadEvent WHERE id = ? AND conversation_id = ?; deleteUnreadEvents: +-- @NotifyCustomKey UnreadEvent +-- @NotifyCustomKey conversation_list_unread DELETE FROM UnreadEvent WHERE creation_date <= ? AND conversation_id = ?; insertEvent: +-- @NotifyCustomKey UnreadEvent +-- @NotifyCustomKey conversation_list_unread INSERT OR IGNORE INTO UnreadEvent(id, type, conversation_id, creation_date) VALUES(?, ?, ?, ?); updateEvent: +-- @NotifyCustomKey UnreadEvent +-- @NotifyCustomKey conversation_list_unread UPDATE OR IGNORE UnreadEvent SET type = :type WHERE id = :id AND conversation_id = :conversation_id; diff --git a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/content/ButtonContent.sq b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/content/ButtonContent.sq index 00336f029258..ebc2e7a3cf9d 100644 --- a/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/content/ButtonContent.sq +++ b/data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/content/ButtonContent.sq @@ -14,10 +14,14 @@ CREATE TABLE ButtonContent ( ); insertButton: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id INSERT INTO ButtonContent (message_id, conversation_id, id, text) VALUES (:message_id, :conversation_id, :id, :text); markSelected { + -- @NotifyCustomKey message_list_:conversation_id + -- @NotifyCustomKey message_search_:conversation_id UPDATE ButtonContent SET is_selected = 0 WHERE conversation_id = :conversation_id AND @@ -31,7 +35,11 @@ markSelected { } removeAllSelection: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id UPDATE ButtonContent SET is_selected = 0 WHERE conversation_id = :conversation_id AND message_id = :message_id; deleteAllButtons: +-- @NotifyCustomKey message_list_:conversation_id +-- @NotifyCustomKey message_search_:conversation_id DELETE FROM ButtonContent WHERE conversation_id = :conversation_id AND message_id = :message_id; diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt index 7de51076963f..3186f277d682 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt @@ -40,8 +40,12 @@ import com.wire.kalium.persistence.dao.message.KaliumPager import com.wire.kalium.persistence.dao.message.MessageDAO import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO import com.wire.kalium.persistence.db.ReadDispatcher +import com.wire.kalium.persistence.db.UserDatabaseBuilder import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newDraftMessageEntity +import com.wire.kalium.persistence.utils.stubs.newRegularMessageEntity import com.wire.kalium.persistence.utils.stubs.newUserEntity +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import kotlin.test.AfterTest @@ -54,6 +58,7 @@ import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds class ConversationExtensionsTest : BaseDatabaseTest() { + private lateinit var databaseBuilder: UserDatabaseBuilder private lateinit var conversationExtensions: ConversationExtensions private lateinit var messageDAO: MessageDAO private lateinit var messageDraftDAO: MessageDraftDAO @@ -66,7 +71,8 @@ class ConversationExtensionsTest : BaseDatabaseTest() { @BeforeTest fun setUp() { deleteDatabase(selfUserId) - val db = createDatabase(selfUserId, encryptedDBSecret, true) + databaseBuilder = createDatabase(selfUserId, encryptedDBSecret, true) + val db = databaseBuilder val queries = db.database.conversationDetailsWithEventsQueries messageDAO = db.messageDAO messageDraftDAO = db.messageDraftDAO @@ -185,6 +191,111 @@ class ConversationExtensionsTest : BaseDatabaseTest() { } } } + + @Test + fun givenConversationListPagingSource_whenConversationIsInserted_thenItInvalidates() = runTest(dispatcher) { + populateData(count = 1, isChannel = false) + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + conversationDAO.insertConversation( + newConversationEntity(ConversationIDEntity("new_conversation", "domain")).copy( + name = "new conversation", + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.parse("2024-01-01T00:00:00Z"), + lastReadDate = Instant.parse("2023-12-31T23:59:59Z"), + isChannel = false, + ) + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenConversationListPagingSource_whenMessageIsInserted_thenItInvalidates() = runTest(dispatcher) { + populateData(count = 1, isChannel = false) + val conversationId = conversationId() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "message_after_load", + conversationId = conversationId, + senderUserId = otherUserId, + date = Instant.parse("2024-01-01T00:00:00Z"), + ) + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenConversationListPagingSource_whenUnreadChanges_thenItInvalidates() = runTest(dispatcher) { + populateData(count = 1, isChannel = false) + val conversationId = conversationId() + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "message_before_load", + conversationId = conversationId, + senderUserId = otherUserId, + date = Instant.parse("2024-01-01T00:00:00Z"), + ) + ) + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + databaseBuilder.database.unreadEventsQueries.deleteUnreadEvents( + Instant.parse("2025-01-01T00:00:00Z"), + conversationId, + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenConversationListPagingSource_whenDraftChanges_thenItInvalidates() = runTest(dispatcher) { + populateData(count = 1, isChannel = false) + val conversationId = conversationId() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + messageDraftDAO.upsertMessageDraft(newDraftMessageEntity(conversationId = conversationId)) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenConversationListPagingSource_whenUnrelatedMetadataChanges_thenItDoesNotInvalidate() = runTest(dispatcher) { + populateData(count = 1, isChannel = false) + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + databaseBuilder.database.metadataQueries.insertValue("some_key", "some_value") + + advanceUntilIdle() + + assertFalse(invalidated()) + } + private fun getPager(searchQuery: String = "", fromArchive: Boolean = false, filter: ConversationFilterEntity = ConversationFilterEntity.ALL): KaliumPager = conversationExtensions.getPagerForConversationDetailsWithEventsSearch( pagingConfig = PagingConfig(PAGE_SIZE), @@ -197,6 +308,16 @@ class ConversationExtensionsTest : BaseDatabaseTest() { private suspend fun PagingSource.nextPageForOffset(key: Int) = load(PagingSourceLoadParamsAppend(key, PAGE_SIZE, true)) + private fun PagingSource.observeInvalidation(): () -> Boolean { + var invalidated = false + registerInvalidatedCallback { + invalidated = true + } + return { invalidated } + } + + private fun conversationId(index: Int = 0, prefix: String = CONVERSATION_ID_PREFIX) = ConversationIDEntity("$prefix$index", "domain") + private suspend fun populateData( archived: Boolean = false, count: Int = CONVERSATION_COUNT, @@ -225,5 +346,6 @@ class ConversationExtensionsTest : BaseDatabaseTest() { const val CONVERSATION_ID_PREFIX = "conversation_" const val ARCHIVED_CONVERSATION_ID_PREFIX = "archived_conversation_" const val PAGE_SIZE = 20 + val otherUserId = UserIDEntity("user", "domain") } } diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/MetadataDAOTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/MetadataDAOTest.kt index b9b4781183fa..b5a946406deb 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/MetadataDAOTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/MetadataDAOTest.kt @@ -18,8 +18,11 @@ package com.wire.kalium.persistence.dao +import app.cash.sqldelight.coroutines.asFlow import app.cash.turbine.test import com.wire.kalium.persistence.BaseDatabaseTest +import com.wire.kalium.persistence.db.UserDatabaseBuilder +import com.wire.kalium.persistence.util.mapToList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -39,12 +42,13 @@ class MetadataDAOTest : BaseDatabaseTest() { private val selfUserId = UserIDEntity("selfValue", "selfDomain") private lateinit var metadataDAO: MetadataDAO + private lateinit var databaseBuilder: UserDatabaseBuilder @BeforeTest fun setUp() { deleteDatabase(selfUserId) - val db = createDatabase(selfUserId, encryptedDBSecret, true) - metadataDAO = db.metadataDAO + databaseBuilder = createDatabase(selfUserId, encryptedDBSecret, true) + metadataDAO = databaseBuilder.metadataDAO } @Test @@ -92,4 +96,65 @@ class MetadataDAOTest : BaseDatabaseTest() { cancelAndIgnoreRemainingEvents() } } + + @Test + fun givenExistingKey_whenValueHasBeenDeleted_thenEmitNull() = runTest(dispatcher) { + metadataDAO.insertValue(value1, key1) + + metadataDAO.valueByKeyFlow(key1).test { + assertEquals(value1, awaitItem()) + metadataDAO.deleteValue(key1) + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun givenExistingKey_whenClearDeletesThatKey_thenEmitNull() = runTest(dispatcher) { + metadataDAO.insertValue(value1, key1) + metadataDAO.insertValue(value2, key2) + + metadataDAO.valueByKeyFlow(key1).test { + assertEquals(value1, awaitItem()) + metadataDAO.clear(keysToKeep = listOf(key2)) + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun givenExistingKey_whenClearDeletesAll_thenEmitNull() = runTest(dispatcher) { + metadataDAO.insertValue(value1, key1) + + metadataDAO.valueByKeyFlow(key1).test { + assertEquals(value1, awaitItem()) + metadataDAO.clear(keysToKeep = null) + assertNull(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun givenMetadataTableFlow_whenAnyKeyChanges_thenTableFlowEmitsUpdatedRows() = runTest(dispatcher) { + databaseBuilder.database.metadataQueries.selectAllMetadata() + .asFlow() + .mapToList() + .test { + assertEquals(emptyList(), awaitItem()) + + metadataDAO.insertValue(value1, key1) + assertEquals(listOf(key1 to value1), awaitItem().map { it.key to it.stringValue }.sortedBy { it.first }) + + metadataDAO.insertValue(value2, key2) + assertEquals( + listOf(key1 to value1, key2 to value2), + awaitItem().map { it.key to it.stringValue }.sortedBy { it.first } + ) + + metadataDAO.clear(keysToKeep = listOf(key2)) + assertEquals(listOf(key2 to value2), awaitItem().map { it.key to it.stringValue }) + + cancelAndIgnoreRemainingEvents() + } + } } diff --git a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/message/MessageExtensionsTest.kt b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/message/MessageExtensionsTest.kt index 6bfeb135683e..993f679f4014 100644 --- a/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/message/MessageExtensionsTest.kt +++ b/data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/message/MessageExtensionsTest.kt @@ -35,13 +35,18 @@ import com.wire.kalium.persistence.dao.message.MessageEntityContent import com.wire.kalium.persistence.dao.message.MessageExtensions import com.wire.kalium.persistence.dao.message.MessageExtensionsImpl import com.wire.kalium.persistence.dao.message.MessageMapper +import com.wire.kalium.persistence.dao.reaction.ReactionDAO +import com.wire.kalium.persistence.dao.receipt.ReceiptDAO +import com.wire.kalium.persistence.dao.receipt.ReceiptTypeEntity import com.wire.kalium.persistence.db.ReadDispatcher +import com.wire.kalium.persistence.db.UserDatabaseBuilder import com.wire.kalium.persistence.db.WriteDispatcher import com.wire.kalium.persistence.utils.stubs.newConversationEntity import com.wire.kalium.persistence.utils.stubs.newRegularMessageEntity import com.wire.kalium.persistence.utils.stubs.newUserEntity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import kotlinx.datetime.Instant @@ -56,23 +61,30 @@ import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class MessageExtensionsTest : BaseDatabaseTest() { + private lateinit var databaseBuilder: UserDatabaseBuilder private lateinit var messageExtensions: MessageExtensions private lateinit var messageDAO: MessageDAO private lateinit var conversationDAO: ConversationDAO private lateinit var userDAO: UserDAO + private lateinit var reactionDAO: ReactionDAO + private lateinit var receiptDAO: ReceiptDAO private val selfUserId = UserIDEntity("selfValue", "selfDomain") + private val otherUserId = UserIDEntity("user", "domain") @BeforeTest fun setUp() { Dispatchers.setMain(dispatcher) deleteDatabase(selfUserId) - val db = createDatabase(selfUserId, encryptedDBSecret, true) + databaseBuilder = createDatabase(selfUserId, encryptedDBSecret, true) + val db = databaseBuilder val messagesQueries = db.database.messagesQueries val messageAssetViewQueries = db.database.messageAssetViewQueries messageDAO = db.messageDAO conversationDAO = db.conversationDAO userDAO = db.userDAO + reactionDAO = db.reactionDAO + receiptDAO = db.receiptDAO messageExtensions = MessageExtensionsImpl( messagesQueries = messagesQueries, messageAssetViewQueries = messageAssetViewQueries, @@ -191,6 +203,235 @@ class MessageExtensionsTest : BaseDatabaseTest() { } } + @Test + fun givenMessageListPagingSource_whenMessageIsInsertedInSameConversation_thenItInvalidates() = runTest { + populateMessageData() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "message_after_load", + conversationId = CONVERSATION_ID, + senderUserId = otherUserId, + date = Instant.parse("2024-01-01T00:00:00Z"), + ) + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenMessageListPagingSource_whenMessageIsInsertedInOtherConversation_thenItDoesNotInvalidate() = runTest { + populateMessageData() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + val otherConversationId = ConversationIDEntity("other-conversation", "domain") + conversationDAO.insertConversation(newConversationEntity(id = otherConversationId)) + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "other_conversation_message", + conversationId = otherConversationId, + senderUserId = otherUserId, + date = Instant.parse("2024-01-01T00:00:00Z"), + ) + ) + + advanceUntilIdle() + + assertFalse(invalidated()) + } + + @Test + fun givenMessageListPagingSource_whenReactionChangesInSameConversation_thenItInvalidates() = runTest { + populateMessageData() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + reactionDAO.insertReaction( + originalMessageId = "0", + conversationId = CONVERSATION_ID, + senderUserId = otherUserId, + instant = Instant.parse("2024-01-01T00:00:00Z"), + emoji = "🔥" + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenMessageListPagingSource_whenReceiptChangesInSameConversation_thenItInvalidates() = runTest { + populateMessageData() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + receiptDAO.insertReceipts( + userId = otherUserId, + conversationId = CONVERSATION_ID, + date = Instant.parse("2024-01-01T00:00:00Z"), + type = ReceiptTypeEntity.READ, + messageIds = listOf("0"), + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenMessageListPagingSource_whenTextMessageIsEditedInSameConversation_thenItInvalidates() = runTest { + populateMessageData() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + messageDAO.updateTextMessageContent( + editInstant = Instant.parse("2024-01-01T00:00:00Z"), + conversationId = CONVERSATION_ID, + currentMessageId = "0", + newTextContent = MessageEntityContent.Text("edited message"), + newMessageId = "0-edited" + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + } + + @Test + fun givenMessageListPagingSource_whenUnrelatedMetadataChanges_thenItDoesNotInvalidate() = runTest { + populateMessageData() + val pagingSource = getPager().pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + databaseBuilder.database.metadataQueries.insertValue("some_key", "some_value") + + advanceUntilIdle() + + assertFalse(invalidated()) + } + + @Test + fun givenMessageSearchPagingSource_whenMatchingMessageIsInserted_thenItInvalidates() = runTest { + populateMessageData() + val pagingSource = getSearchMessagesPager(searchQuery = "needle").pagingSource + + pagingSource.refresh().also { result -> + assertIs>(result) + assertTrue(result.data.isEmpty()) + } + val invalidated = pagingSource.observeInvalidation() + + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "search_message_after_load", + conversationId = CONVERSATION_ID, + senderUserId = otherUserId, + date = Instant.parse("2024-01-01T00:00:00Z"), + content = MessageEntityContent.Text("contains needle") + ) + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + getSearchMessagesPager(searchQuery = "needle").pagingSource.refresh().also { result -> + assertIs>(result) + assertEquals(listOf("search_message_after_load"), result.data.map { it.id }) + } + } + + @Test + fun givenMessageSearchPagingSource_whenTextMessageIsEditedToMatch_thenItInvalidates() = runTest { + populateMessageData() + val pagingSource = getSearchMessagesPager(searchQuery = "edited").pagingSource + + pagingSource.refresh().also { result -> + assertIs>(result) + assertTrue(result.data.isEmpty()) + } + val invalidated = pagingSource.observeInvalidation() + + messageDAO.updateTextMessageContent( + editInstant = Instant.parse("2024-01-01T00:00:00Z"), + conversationId = CONVERSATION_ID, + currentMessageId = "0", + newTextContent = MessageEntityContent.Text("edited message"), + newMessageId = "0-edited-search" + ) + + advanceUntilIdle() + + assertTrue(invalidated()) + getSearchMessagesPager(searchQuery = "edited").pagingSource.refresh().also { result -> + assertIs>(result) + assertEquals(listOf("0-edited-search"), result.data.map { it.id }) + } + } + + @Test + fun givenMessageSearchPagingSource_whenMatchingMessageIsInsertedInOtherConversation_thenItDoesNotInvalidateAndResultsStayEmpty() = runTest { + populateMessageData() + val pagingSource = getSearchMessagesPager(searchQuery = "needle").pagingSource + + pagingSource.refresh().also { result -> + assertIs>(result) + assertTrue(result.data.isEmpty()) + } + val invalidated = pagingSource.observeInvalidation() + + val otherConversationId = ConversationIDEntity("other-search-conversation", "domain") + conversationDAO.insertConversation(newConversationEntity(id = otherConversationId)) + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "other_search_message", + conversationId = otherConversationId, + senderUserId = otherUserId, + date = Instant.parse("2024-01-01T00:00:00Z"), + content = MessageEntityContent.Text("contains needle") + ) + ) + + advanceUntilIdle() + + assertFalse(invalidated()) + getSearchMessagesPager(searchQuery = "needle").pagingSource.refresh().also { result -> + assertIs>(result) + assertTrue(result.data.isEmpty()) + } + } + + @Test + fun givenMessageSearchPagingSource_whenUnrelatedMetadataChanges_thenItDoesNotInvalidate() = runTest { + populateMessageData() + val pagingSource = getSearchMessagesPager(searchQuery = "message").pagingSource + + pagingSource.refresh() + val invalidated = pagingSource.observeInvalidation() + + databaseBuilder.database.metadataQueries.insertValue("search_key", "search_value") + + advanceUntilIdle() + + assertFalse(invalidated()) + } + private fun getPager(): KaliumPager = messageExtensions.getPagerForConversation( conversationId = CONVERSATION_ID, visibilities = MessageEntity.Visibility.entries.toList(), @@ -213,9 +454,16 @@ class MessageExtensionsTest : BaseDatabaseTest() { PagingSourceLoadParamsAppend(key, PAGE_SIZE, true) ) + private fun PagingSource.observeInvalidation(): () -> Boolean { + var invalidated = false + registerInvalidatedCallback { + invalidated = true + } + return { invalidated } + } + private suspend fun populateMessageData(prefix: String = "") { - val userId = UserIDEntity("user", "domain") - userDAO.upsertUser(newUserEntity(qualifiedID = userId)) + userDAO.upsertUser(newUserEntity(qualifiedID = otherUserId)) conversationDAO.insertConversation(newConversationEntity(id = CONVERSATION_ID)) val messages = buildList { repeat(MESSAGE_COUNT) { @@ -223,7 +471,7 @@ class MessageExtensionsTest : BaseDatabaseTest() { newRegularMessageEntity( id = it.toString(), conversationId = CONVERSATION_ID, - senderUserId = userId, + senderUserId = otherUserId, content = MessageEntityContent.Text("message $it"), // Ordered by date - Inserting with decreasing date is important to assert pagination date = Instant.fromEpochSeconds(MESSAGE_COUNT - it.toLong()) diff --git a/settings.gradle.kts b/settings.gradle.kts index f05be30fa8bc..903ac0390c6e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,7 +34,36 @@ rootDir include(":$projectPath") } +val sqlDelightBuildPath = "vendor/sqldelight" +val sqlDelightModules = mapOf( + "app.cash.sqldelight:gradle-plugin" to ":sqldelight-gradle-plugin", + "app.cash.sqldelight:runtime" to ":runtime", + "app.cash.sqldelight:coroutines-extensions" to ":extensions:coroutines-extensions", + "app.cash.sqldelight:android-driver" to ":drivers:android-driver", + "app.cash.sqldelight:androidx-paging3-extensions" to ":extensions:androidx-paging3", + "app.cash.sqldelight:native-driver" to ":drivers:native-driver", + "app.cash.sqldelight:sqlite-driver" to ":drivers:sqlite-driver", + "app.cash.sqldelight:web-worker-driver" to ":drivers:web-worker-driver", + "app.cash.sqldelight:primitive-adapters" to ":adapters:primitive-adapters", + "app.cash.sqldelight:compiler-env" to ":sqldelight-compiler:environment", + "app.cash.sqldelight:migration-env" to ":sqlite-migrations:environment", + "app.cash.sqldelight:sqlite-3-38-dialect" to ":dialects:sqlite-3-38", + "app.cash.sqldelight:postgresql-dialect" to ":dialects:postgresql", + "app.cash.sqldelight:r2dbc-driver" to ":drivers:r2dbc-driver", + "app.cash.sqldelight:async-extensions" to ":extensions:async-extensions", +) + +fun org.gradle.api.initialization.ConfigurableIncludedBuild.useLocalSqlDelightFork() { + dependencySubstitution { + sqlDelightModules.forEach { (moduleCoordinates, projectPath) -> + substitute(module(moduleCoordinates)).using(project(projectPath)) + } + } +} + pluginManagement { + includeBuild("vendor/sqldelight") + repositories { gradlePluginPortal() google() @@ -46,6 +75,10 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } +includeBuild(sqlDelightBuildPath) { + useLocalSqlDelightFork() +} + dependencyResolutionManagement { repositories { mavenCentral() diff --git a/vendor/sqldelight b/vendor/sqldelight new file mode 160000 index 000000000000..950a68392c82 --- /dev/null +++ b/vendor/sqldelight @@ -0,0 +1 @@ +Subproject commit 950a68392c82fec754aff30ff4a58d36ed9becb0