diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 9b544d5299..f01e31dab7 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -87,7 +87,7 @@ struct ChatListNavLink: View { ChatPreviewView(chat: chat) .frame(height: rowHeights[dynamicTypeSize]) .swipeActions(edge: .trailing, allowsFullSwipe: true) { - joinGroupButton(groupInfo.hostConnCustomUserProfileId) + joinGroupButton() if groupInfo.canDelete { deleteGroupChatButton(groupInfo) } @@ -137,7 +137,7 @@ struct ChatListNavLink: View { } } - private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View { + private func joinGroupButton() -> some View { Button { joinGroup(chat.chatInfo.apiId) } label: { diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index d98bea82fc..e62f4d0c00 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -493,7 +493,7 @@ processChatCommand = \case -- functions below are called in separate transactions to prevent crashes on android -- (possibly, race condition on integrity check?) withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct - withStore' $ \db -> deleteContact db userId ct + withStore' $ \db -> deleteContact db user ct unsetActive $ ActiveC localDisplayName pure $ CRContactDeleted ct CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do diff --git a/src/Simplex/Chat/Migrations/M20220101_initial.hs b/src/Simplex/Chat/Migrations/M20220101_initial.hs index b1ff292211..2568b0b672 100644 --- a/src/Simplex/Chat/Migrations/M20220101_initial.hs +++ b/src/Simplex/Chat/Migrations/M20220101_initial.hs @@ -41,7 +41,7 @@ CREATE TABLE display_names ( CREATE TABLE contacts ( contact_id INTEGER PRIMARY KEY, - contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile + contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user @@ -223,7 +223,7 @@ CREATE TABLE contact_requests ( ON UPDATE CASCADE ON DELETE CASCADE, agent_invitation_id BLOB NOT NULL, contact_profile_id INTEGER REFERENCES contact_profiles - ON DELETE SET NULL -- NULL if it's an incognito profile + ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, local_display_name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 40ea1741bd..a9958146b4 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -47,10 +47,10 @@ CREATE TABLE display_names( ) WITHOUT ROWID; CREATE TABLE contacts( contact_id INTEGER PRIMARY KEY, - contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, -- NULL if it's an incognito profile -user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, -local_display_name TEXT NOT NULL, -is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user + contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + local_display_name TEXT NOT NULL, + is_user INTEGER NOT NULL DEFAULT 0, -- 1 if this contact is a user via_group INTEGER REFERENCES groups(group_id) ON DELETE SET NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT CHECK(updated_at NOT NULL), @@ -278,18 +278,20 @@ CREATE TABLE contact_requests( ON UPDATE CASCADE ON DELETE CASCADE, agent_invitation_id BLOB NOT NULL, contact_profile_id INTEGER REFERENCES contact_profiles - ON DELETE SET NULL -- NULL if it's an incognito profile -DEFERRABLE INITIALLY DEFERRED, -local_display_name TEXT NOT NULL, -created_at TEXT NOT NULL DEFAULT(datetime('now')), -user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB, -FOREIGN KEY(user_id, local_display_name) -REFERENCES display_names(user_id, local_display_name) -ON UPDATE CASCADE -ON DELETE CASCADE -DEFERRABLE INITIALLY DEFERRED, -UNIQUE(user_id, local_display_name), -UNIQUE(user_id, contact_profile_id) + ON DELETE SET NULL + DEFERRABLE INITIALLY DEFERRED, + local_display_name TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TEXT CHECK(updated_at NOT NULL), + xcontact_id BLOB, + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON UPDATE CASCADE + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, contact_profile_id) ); CREATE TABLE messages( message_id INTEGER PRIMARY KEY, diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 1e25b37cd3..264aca2af9 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -560,8 +560,8 @@ deleteContactConnectionsAndFiles db userId Contact {contactId} = do (userId, contactId) DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) -deleteContact :: DB.Connection -> UserId -> Contact -> IO () -deleteContact db userId Contact {contactId, localDisplayName} = do +deleteContact :: DB.Connection -> User -> Contact -> IO () +deleteContact db user@User {userId} Contact {contactId, localDisplayName, activeConn = Connection {customUserProfileId}} = do DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) ctMember :: (Maybe ContactId) <- maybeFirstRow fromOnly $ DB.query db "SELECT contact_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId) if isNothing ctMember @@ -572,6 +572,25 @@ deleteContact db userId Contact {contactId, localDisplayName} = do currentTs <- getCurrentTime DB.execute db "UPDATE group_members SET contact_id = NULL, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId) DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) + forM_ customUserProfileId $ \profileId -> deleteUnusedIncognitoProfileById_ db user profileId + +deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () +deleteUnusedIncognitoProfileById_ db User {userId} profile_id = + DB.executeNamed + db + [sql| + DELETE FROM contact_profiles + WHERE user_id = :user_id AND contact_profile_id = :profile_id AND incognito = 1 + AND 1 NOT IN ( + SELECT 1 FROM connections + WHERE user_id = :user_id AND custom_user_profile_id = :profile_id LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM group_members + WHERE user_id = :user_id AND member_profile_id = :profile_id LIMIT 1 + ) + |] + [":user_id" := userId, ":profile_id" := profile_id] deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO () deleteContactProfile_ db userId contactId = @@ -2023,19 +2042,21 @@ deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId} = DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId) cleanupMemberContactAndProfile_ db user m +-- it's important this function is used in transaction after the actual group_members record is deleted, see checkIncognitoProfileInUse_ cleanupMemberContactAndProfile_ :: DB.Connection -> User -> GroupMember -> IO () -cleanupMemberContactAndProfile_ db User {userId} GroupMember {groupMemberId, localDisplayName, memberContactId, memberContactProfileId} = +cleanupMemberContactAndProfile_ db user@User {userId} m@GroupMember {groupMemberId, localDisplayName, memberContactId, memberContactProfileId, memberProfile = LocalProfile {profileId}} = case memberContactId of Just contactId -> runExceptT (getContact db userId contactId) >>= \case Right ct@Contact {activeConn = Connection {connLevel, viaGroupLink}, contactUsed} -> - unless ((connLevel == 0 && not viaGroupLink) || contactUsed) $ deleteContact db userId ct + unless ((connLevel == 0 && not viaGroupLink) || contactUsed) $ deleteContact db user ct _ -> pure () Nothing -> do sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId) unless (isJust sameProfileMember) $ do DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId) DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user profileId deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO () deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} =