diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq index a2764f0a990..8a084426572 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq @@ -15,6 +15,7 @@ CREATE TABLE Call ( CREATE INDEX call_date_index ON Call(created_at); CREATE INDEX call_conversation_index ON Call(conversation_id); CREATE INDEX call_caller_index ON Call(caller_id); +CREATE INDEX call_status ON Call(status); insertCall: INSERT INTO Call(conversation_id, id, status, caller_id, conversation_type, created_at, type) diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq index 4f7328188af..55b4081b6b2 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq @@ -2,22 +2,22 @@ CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS SELECT ConversationDetails.*, -- unread events - SUM(CASE WHEN UnreadEvent.type = 'KNOCK' THEN 1 ELSE 0 END) AS unreadKnocksCount, - SUM(CASE WHEN UnreadEvent.type = 'MISSED_CALL' THEN 1 ELSE 0 END) AS unreadMissedCallsCount, - SUM(CASE WHEN UnreadEvent.type = 'MENTION' THEN 1 ELSE 0 END) AS unreadMentionsCount, - SUM(CASE WHEN UnreadEvent.type = 'REPLY' THEN 1 ELSE 0 END) AS unreadRepliesCount, - SUM(CASE WHEN UnreadEvent.type = 'MESSAGE' THEN 1 ELSE 0 END) AS unreadMessagesCount, + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, CASE WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN CASE - WHEN COUNT(UnreadEvent.id) > 0 THEN 1 -- if any unread events, move it to the top + WHEN (UnreadEventCountsGrouped.knocksCount + UnreadEventCountsGrouped.missedCallsCount + UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount + UnreadEventCountsGrouped.messagesCount) > 0 THEN 1 -- if any unread events, move it to the top WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top ELSE 0 END WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN CASE - WHEN SUM(CASE WHEN UnreadEvent.type IN ('MENTION', 'REPLY') THEN 1 ELSE 0 END) > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN (UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount) > 0 THEN 1 -- only if unread mentions or replies, move it to the top WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top ELSE 0 END @@ -29,16 +29,16 @@ SELECT MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, MessageDraft.mention_list AS messageDraftMentionList, -- last message - LastMessage.id AS lastMessageId, - LastMessage.content_type AS lastMessageContentType, - MAX(LastMessage.creation_date) AS lastMessageDate, - LastMessage.visibility AS lastMessageVisibility, - LastMessage.sender_user_id AS lastMessageSenderUserId, - (LastMessage.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + Message.id AS lastMessageId, + Message.content_type AS lastMessageContentType, + Message.creation_date AS lastMessageDate, + Message.visibility AS lastMessageVisibility, + Message.sender_user_id AS lastMessageSenderUserId, + (Message.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, User.name AS lastMessageSenderName, User.connection_status AS lastMessageSenderConnectionStatus, User.deleted AS lastMessageSenderIsDeleted, - (LastMessage.sender_user_id IS NOT NULL AND LastMessage.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + (Message.sender_user_id IS NOT NULL AND Message.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, MemberChangeContent.member_change_list AS lastMessageMemberChangeList, MemberChangeContent.member_change_type AS lastMessageMemberChangeType, ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, @@ -47,24 +47,26 @@ SELECT TextContent.text_body AS lastMessageText, AssetContent.asset_mime_type AS lastMessageAssetMimeType FROM ConversationDetails -LEFT JOIN UnreadEvent - ON UnreadEvent.conversation_id = ConversationDetails.qualifiedId +LEFT JOIN UnreadEventCountsGrouped + ON UnreadEventCountsGrouped.conversationId = ConversationDetails.qualifiedId LEFT JOIN MessageDraft ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations -LEFT JOIN Message AS LastMessage +LEFT JOIN LastMessage ON LastMessage.conversation_id = ConversationDetails.qualifiedId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN Message + ON LastMessage.message_id = Message.id AND LastMessage.conversation_id = Message.conversation_id LEFT JOIN User - ON LastMessage.sender_user_id = User.qualified_id + ON Message.sender_user_id = User.qualified_id LEFT JOIN MessageMemberChangeContent AS MemberChangeContent - ON LastMessage.id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id + ON LastMessage.message_id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id LEFT JOIN MessageMention AS Mention - ON LastMessage.id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id + ON LastMessage.message_id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent - ON LastMessage.id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id + ON LastMessage.message_id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id LEFT JOIN MessageAssetContent AS AssetContent - ON LastMessage.id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id + ON LastMessage.message_id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id LEFT JOIN MessageTextContent AS TextContent - ON LastMessage.id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id + ON LastMessage.message_id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id WHERE ConversationDetails.type IS NOT 'SELF' AND ( @@ -74,8 +76,7 @@ WHERE OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata ) AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) - AND ConversationDetails.isActive -GROUP BY ConversationDetails.qualifiedId; + AND ConversationDetails.isActive; selectAllConversationDetailsWithEvents: SELECT * FROM ConversationDetailsWithEvents diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/LastMessage.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/LastMessage.sq new file mode 100644 index 00000000000..d9c63f6e6b6 --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/LastMessage.sq @@ -0,0 +1,111 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.ConversationEntity; +import com.wire.kalium.persistence.dao.message.MessageEntity.ContentType; +import com.wire.kalium.persistence.dao.message.MessageEntity.FederationType; +import com.wire.kalium.persistence.dao.message.MessageEntity.LegalHoldType; +import com.wire.kalium.persistence.dao.message.MessageEntity.MemberChangeType; +import com.wire.kalium.persistence.dao.message.MessageEntity; +import com.wire.kalium.persistence.dao.message.RecipientFailureTypeEntity; +import kotlin.Boolean; +import kotlin.Float; +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; +import kotlinx.datetime.Instant; + +CREATE TABLE LastMessage ( + conversation_id TEXT AS QualifiedIDEntity, + message_id TEXT, + creation_date INTEGER AS Instant, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + PRIMARY KEY (conversation_id) +); + +-- update last message when newly inserted message is newer than the current last message +CREATE TRIGGER updateLastMessageAfterInsertingNewMessage +AFTER INSERT ON Message +WHEN + new.visibility IN ('VISIBLE', 'DELETED') + AND new.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + VALUES (new.conversation_id, new.id, new.creation_date) + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after deleting the current last message by finding new last message for the conversation +CREATE TRIGGER updateLastMessageAfterDeletingLastMessage +AFTER DELETE ON LastMessage +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after a message got updated and now this one should be the new last message +-- or if the current last message shouldn't be the last message anymore because of the visibility update for instance +CREATE TRIGGER updateLastMessageAfterUpdatingMessage +AFTER UPDATE OF id, conversation_id, visibility, content_type, creation_date ON Message +WHEN + new.creation_date >= (SELECT creation_date FROM LastMessage WHERE conversation_id = new.conversation_id LIMIT 1) +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + new.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after there was a foreign key updated to null by finding new last message for that conversation +-- this happens when last message is moved to another conversation or id of last message is changed +CREATE TRIGGER updateLastMessageAfterForeignKeyUpdatedToNull +AFTER UPDATE OF conversation_id ON LastMessage +WHEN + new.conversation_id IS NULL +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; + DELETE FROM LastMessage WHERE conversation_id IS NULL; +END; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq index 935465988f2..90308374887 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq @@ -501,14 +501,11 @@ selectByConversationIdAndVisibility: SELECT * FROM MessageDetailsView WHERE conversationId = :conversationId AND visibility IN :visibility ORDER BY date DESC LIMIT :limit OFFSET :offset; selectLastMessagesByConversationIds: -SELECT MessageDetailsView.* FROM MessageDetailsView -JOIN ( - SELECT conversationId, MAX(date) AS latest_date - FROM MessageDetailsView - WHERE conversationId IN :conversationIds - GROUP BY conversationId -) AS lastMessage -ON MessageDetailsView.conversationId = lastMessage.conversationId AND MessageDetailsView.date = lastMessage.latest_date; +SELECT MessageDetailsView.* +FROM LastMessage +INNER JOIN MessageDetailsView +ON MessageDetailsView.conversationId = LastMessage.conversation_id AND MessageDetailsView.id = LastMessage.message_id +WHERE LastMessage.conversation_id IN :conversationIds; selectMessagesByConversationIdAndVisibilityAfterDate: SELECT * FROM MessageDetailsView WHERE MessageDetailsView.conversationId = ? AND visibility IN ? AND date > ? ORDER BY date DESC; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq index 6f685e20e3b..ac20a8a5d00 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq @@ -11,6 +11,9 @@ CREATE TABLE UnreadEvent ( FOREIGN KEY (id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (id, conversation_id) ); +CREATE INDEX unread_event_conversation ON UnreadEvent(conversation_id); +CREATE INDEX unread_event_date ON UnreadEvent(creation_date); +CREATE INDEX unread_event_type ON UnreadEvent(type); deleteUnreadEvent: DELETE FROM UnreadEvent WHERE id = ? AND conversation_id = ?; diff --git a/persistence/src/commonMain/db_user/migrations/90.sqm b/persistence/src/commonMain/db_user/migrations/90.sqm new file mode 100644 index 00000000000..129f67d433f --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/90.sqm @@ -0,0 +1,208 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.ConversationEntity; +import com.wire.kalium.persistence.dao.message.MessageEntity.ContentType; +import com.wire.kalium.persistence.dao.message.MessageEntity.FederationType; +import com.wire.kalium.persistence.dao.message.MessageEntity.LegalHoldType; +import com.wire.kalium.persistence.dao.message.MessageEntity.MemberChangeType; +import com.wire.kalium.persistence.dao.message.MessageEntity; +import com.wire.kalium.persistence.dao.message.RecipientFailureTypeEntity; +import kotlin.Boolean; +import kotlin.Float; +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; +import kotlinx.datetime.Instant; + +CREATE INDEX call_status ON Call(status); +CREATE INDEX unread_event_conversation ON UnreadEvent(conversation_id); +CREATE INDEX unread_event_date ON UnreadEvent(creation_date); +CREATE INDEX unread_event_type ON UnreadEvent(type); + +CREATE TABLE LastMessage ( + conversation_id TEXT AS QualifiedIDEntity, + message_id TEXT, + creation_date INTEGER AS Instant, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + PRIMARY KEY (conversation_id) +); + +-- update last message when newly inserted message is newer than the current last message +CREATE TRIGGER updateLastMessageAfterInsertingNewMessage +AFTER INSERT ON Message +WHEN + new.visibility IN ('VISIBLE', 'DELETED') + AND new.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + VALUES (new.conversation_id, new.id, new.creation_date) + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after deleting the current last message by finding new last message for the conversation +CREATE TRIGGER updateLastMessageAfterDeletingLastMessage +AFTER DELETE ON LastMessage +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after a message got updated and now this one should be the new last message +-- or if the current last message shouldn't be the last message anymore because of the visibility update for instance +CREATE TRIGGER updateLastMessageAfterUpdatingMessage +AFTER UPDATE OF id, conversation_id, visibility, content_type, creation_date ON Message +WHEN + new.creation_date >= (SELECT creation_date FROM LastMessage WHERE conversation_id = new.conversation_id LIMIT 1) +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + new.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after there was a foreign key updated to null by finding new last message for that conversation +-- this happens when last message is moved to another conversation or id of last message is changed +CREATE TRIGGER updateLastMessageAfterForeignKeyUpdatedToNull +AFTER UPDATE OF conversation_id ON LastMessage +WHEN + new.conversation_id IS NULL +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; + DELETE FROM LastMessage WHERE conversation_id IS NULL; +END; + +-- populate LastMessage table with the last message of each conversation +INSERT INTO LastMessage(conversation_id, message_id, creation_date) +SELECT conversation_id, id, creation_date +FROM Message +WHERE + visibility IN ('VISIBLE', 'DELETED') + AND content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') +GROUP BY conversation_id +HAVING creation_date = MAX(creation_date); + +DROP VIEW IF EXISTS ConversationDetailsWithEvents; + +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + -- unread events + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN (UnreadEventCountsGrouped.knocksCount + UnreadEventCountsGrouped.missedCallsCount + UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount + UnreadEventCountsGrouped.messagesCount) > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN (UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount) > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + -- draft message + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList, + -- last message + Message.id AS lastMessageId, + Message.content_type AS lastMessageContentType, + Message.creation_date AS lastMessageDate, + Message.visibility AS lastMessageVisibility, + Message.sender_user_id AS lastMessageSenderUserId, + (Message.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + User.name AS lastMessageSenderName, + User.connection_status AS lastMessageSenderConnectionStatus, + User.deleted AS lastMessageSenderIsDeleted, + (Message.sender_user_id IS NOT NULL AND Message.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + MemberChangeContent.member_change_list AS lastMessageMemberChangeList, + MemberChangeContent.member_change_type AS lastMessageMemberChangeType, + ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, + (Mention.user_id IS NOT NULL) AS lastMessageIsMentioningSelfUser, + TextContent.is_quoting_self AS lastMessageIsQuotingSelfUser, + TextContent.text_body AS lastMessageText, + AssetContent.asset_mime_type AS lastMessageAssetMimeType +FROM ConversationDetails +LEFT JOIN UnreadEventCountsGrouped + ON UnreadEventCountsGrouped.conversationId = ConversationDetails.qualifiedId +LEFT JOIN MessageDraft + ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +LEFT JOIN LastMessage + ON LastMessage.conversation_id = ConversationDetails.qualifiedId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN Message + ON LastMessage.message_id = Message.id AND LastMessage.conversation_id = Message.conversation_id +LEFT JOIN User + ON Message.sender_user_id = User.qualified_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent + ON LastMessage.message_id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageMention AS Mention + ON LastMessage.message_id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent + ON LastMessage.message_id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent + ON LastMessage.message_id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageTextContent AS TextContent + ON LastMessage.message_id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index 67bc60f24c5..fbceb8364ea 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -26,6 +26,7 @@ import com.wire.kalium.persistence.Client import com.wire.kalium.persistence.Connection import com.wire.kalium.persistence.Conversation import com.wire.kalium.persistence.ConversationLegalHoldStatusChangeNotified +import com.wire.kalium.persistence.LastMessage import com.wire.kalium.persistence.Member import com.wire.kalium.persistence.Message import com.wire.kalium.persistence.MessageAssetContent @@ -265,4 +266,9 @@ internal object TableMapper { conversation_idAdapter = QualifiedIDAdapter, mention_listAdapter = MentionListAdapter() ) + + val lastMessageAdapter = LastMessage.Adapter( + conversation_idAdapter = QualifiedIDAdapter, + creation_dateAdapter = InstantTypeAdapter, + ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 6dc85f9d0f5..bc9fe1464c4 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -157,7 +157,8 @@ class UserDatabaseBuilder internal constructor( TableMapper.messageConversationProtocolChangedDuringACAllContentAdapter, ConversationLegalHoldStatusChangeNotifiedAdapter = TableMapper.conversationLegalHoldStatusChangeNotifiedAdapter, MessageAssetTransferStatusAdapter = TableMapper.messageAssetTransferStatusAdapter, - MessageDraftAdapter = TableMapper.messageDraftsAdapter + MessageDraftAdapter = TableMapper.messageDraftsAdapter, + LastMessageAdapter = TableMapper.lastMessageAdapter, ) init { diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt index 03cf3341da5..da4724ba739 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt @@ -1946,15 +1946,6 @@ class MessageDAOTest : BaseDatabaseTest() { fun givenMessagesAreInserted_whenGettingLastMessagesByConversations_thenOnlyLastMessagesForEachConversationAreReturned() = runTest { // given insertInitialData() - fun createMessage(id: String, conversationId: QualifiedIDEntity, date: Instant) = newRegularMessageEntity( - id = id, - conversationId = conversationId, - date = date, - senderUserId = userEntity1.id, - senderName = userEntity1.name!!, - sender = userDetailsEntity1 - ) - val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") val messages = listOf( createMessage(id = "1A", conversationId = conversationEntity1.id, date = baseInstant), @@ -1972,6 +1963,124 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals(null, result[conversationEntity3.id]) } + + @Test + fun givenNewMessageIsInserted_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val lastMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val newMessage = createMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds) + messageDAO.insertOrIgnoreMessages(listOf(lastMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.insertOrIgnoreMessages(listOf(newMessage)) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(lastMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(newMessage.id, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenLastMessageIsDeleted_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val olderMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val lastMessage = createMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds) + messageDAO.insertOrIgnoreMessages(listOf(olderMessage, lastMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.deleteMessage(lastMessage.id, conversationEntity1.id) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(lastMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(olderMessage.id, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenLastMessageIsMovedToAnotherConversation_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val lastMessageConversation1 = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val lastMessageConversation2 = createMessage(id = "2", conversationId = conversationEntity2.id, date = baseInstant + 1.seconds) + messageDAO.insertOrIgnoreMessages(listOf(lastMessageConversation1, lastMessageConversation2)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id, conversationEntity2.id)) + messageDAO.moveMessages(conversationEntity2.id, conversationEntity1.id) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id, conversationEntity2.id)) + // then + assertEquals(lastMessageConversation1.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(lastMessageConversation2.id, resultBefore[conversationEntity2.id]?.id) + + assertEquals(lastMessageConversation2.id, resultAfter[conversationEntity1.id]?.id) + assertEquals(null, resultAfter[conversationEntity2.id]?.id) // conversation 2 should be empty - all messages are moved to 1 + } + + @Test + fun givenLastMessageIsEdited_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val lastMessageTextContent = MessageEntityContent.Text("message") + val lastMessage = createMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds) + .copy(content = lastMessageTextContent) + val editedLastMessageId = lastMessage.id + "_edit" + messageDAO.insertOrIgnoreMessages(listOf(lastMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.updateTextMessageContent( + editInstant = baseInstant + 2.seconds, + conversationId = conversationEntity1.id, + currentMessageId = lastMessage.id, + newTextContent = lastMessageTextContent.copy(messageBody = "edited"), + newMessageId = editedLastMessageId, + ) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(lastMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(editedLastMessageId, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenNewAssetMessageWithIncompleteDataIsInserted_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val currentLastVisibleMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val newerAssetMessageIncompleteData = + createImageAssetMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds, isComplete = false) + messageDAO.insertOrIgnoreMessages(listOf(currentLastVisibleMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.insertOrIgnoreMessage(newerAssetMessageIncompleteData) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(currentLastVisibleMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(currentLastVisibleMessage.id, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenLastAssetMessageRemoteDataUpdated_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val currentLastVisibleMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val newerAssetMessageIncompleteData = + createImageAssetMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds, isComplete = false) + val newerAssetMessageCompleteData = + createImageAssetMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds, isComplete = true) + messageDAO.insertOrIgnoreMessages(listOf(currentLastVisibleMessage, newerAssetMessageIncompleteData)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.insertOrIgnoreMessage(newerAssetMessageCompleteData) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(currentLastVisibleMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(newerAssetMessageCompleteData.id, resultAfter[conversationEntity1.id]?.id) + } + @Test fun givenUnverifiedWarningMessageIsInserted_whenInsertingSuchMessageAgain_thenOnlyIdIsUpdatedNoNewMessages() = runTest { // given @@ -2245,4 +2354,39 @@ class MessageDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversationEntity2) conversationDAO.insertConversation(conversationEntity3) } + + private fun createMessage(id: String, conversationId: QualifiedIDEntity, date: Instant) = newRegularMessageEntity( + id = id, + conversationId = conversationId, + date = date, + senderUserId = userEntity1.id, + senderName = userEntity1.name!!, + sender = userDetailsEntity1, + ) + + private fun createImageAssetMessage(id: String, conversationId: QualifiedIDEntity, date: Instant, isComplete: Boolean) = + newRegularMessageEntity( + id = id, + conversationId = conversationId, + date = date, + senderUserId = userEntity1.id, + senderName = userEntity1.name!!, + sender = userDetailsEntity1, + content = MessageEntityContent.Asset( + assetSizeInBytes = if (isComplete) 100000L else 0L, + assetName = "test name", + assetMimeType = "JPG", + assetId = if (isComplete) "assetId" else "", + assetOtrKey = if (isComplete) byteArrayOf(1) else byteArrayOf(), + assetSha256Key = if (isComplete) byteArrayOf(1) else byteArrayOf(), + assetToken = "", + assetDomain = "domain", + assetEncryptionAlgorithm = "", + assetWidth = if (isComplete) 100 else null, + assetHeight = if (isComplete) 100 else null, + assetDurationMs = null, + assetNormalizedLoudness = null, + ), + visibility = if (isComplete) MessageEntity.Visibility.VISIBLE else MessageEntity.Visibility.HIDDEN + ) }