diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 641ca6d08..24ea26943 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -720,8 +720,9 @@ func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? { let userId = try currentUserId("apiUpdateProfile") let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) switch r { - case .userProfileNoChange: return nil + case .userProfileNoChange: return (profile, []) case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts) + case .chatCmdError(_, .errorStore(.duplicateName)): return nil; default: throw r } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index ec4cc0fc4..b90c9e747 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -242,7 +242,7 @@ struct ChatInfoView: View { } .actionSheet(isPresented: $showDeleteContactActionSheet) { if contact.ready && contact.active { - ActionSheet( + return ActionSheet( title: Text("Delete contact?\nThis cannot be undone!"), buttons: [ .destructive(Text("Delete and notify contact")) { deleteContact(notify: true) }, @@ -251,7 +251,7 @@ struct ChatInfoView: View { ] ) } else { - ActionSheet( + return ActionSheet( title: Text("Delete contact?\nThis cannot be undone!"), buttons: [ .destructive(Text("Delete")) { deleteContact() }, diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index be912d666..088335d19 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -78,7 +78,7 @@ struct ChatListNavLink: View { .frame(height: rowHeights[dynamicTypeSize]) .actionSheet(isPresented: $showDeleteContactActionSheet) { if contact.ready && contact.active { - ActionSheet( + return ActionSheet( title: Text("Delete contact?\nThis cannot be undone!"), buttons: [ .destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } }, @@ -87,7 +87,7 @@ struct ChatListNavLink: View { ] ) } else { - ActionSheet( + return ActionSheet( title: Text("Delete contact?\nThis cannot be undone!"), buttons: [ .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index b1a362a5a..b64ec21de 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -174,11 +174,13 @@ struct UserProfile: View { chatModel.updateCurrentUser(newProfile) profile = newProfile } + editProfile = false + } else { + alert = .duplicateUserError } } catch { logger.error("UserProfile apiUpdateProfile error: \(responseError(error))") } - editProfile = false } } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 4943dbd4e..cc61fae53 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -83,9 +83,9 @@ public enum AppState: String { public var canSuspend: Bool { switch self { - case .active: true - case .bgRefresh: true - default: false + case .active: return true + case .bgRefresh: return true + default: return false } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 2dff3d604..19305c2e5 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -143,6 +143,7 @@ fun processExternalIntent(intent: Intent?) { val text = intent.getStringExtra(Intent.EXTRA_TEXT) val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri if (uri != null) { + if (uri.scheme != "content") return showNonContentUriAlert() // Shared file that contains plain text, like `*.log` file chatModel.sharedContent.value = SharedContent.File(text ?: "", uri.toURI()) } else if (text != null) { @@ -153,12 +154,14 @@ fun processExternalIntent(intent: Intent?) { isMediaIntent(intent) -> { val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri if (uri != null) { + if (uri.scheme != "content") return showNonContentUriAlert() chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri.toURI())) } // All other mime types } else -> { val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri if (uri != null) { + if (uri.scheme != "content") return showNonContentUriAlert() chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri.toURI()) } } @@ -173,6 +176,7 @@ fun processExternalIntent(intent: Intent?) { isMediaIntent(intent) -> { val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) as? List if (uris != null) { + if (uris.any { it.scheme != "content" }) return showNonContentUriAlert() chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris.map { it.toURI() }) } // All other mime types } @@ -185,6 +189,13 @@ fun processExternalIntent(intent: Intent?) { fun isMediaIntent(intent: Intent): Boolean = intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true +private fun showNonContentUriAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.non_content_uri_alert_title), + text = generalGetString(MR.strings.non_content_uri_alert_text) + ) +} + //fun testJson() { // val str: String = """ // """.trimIndent() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e2d05857c..edf5f7fd7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -951,6 +951,9 @@ object ChatController { val r = sendCmd(CC.ApiUpdateProfile(userId, profile)) if (r is CR.UserProfileNoChange) return profile to emptyList() if (r is CR.UserProfileUpdated) return r.toProfile to r.updateSummary.changedContacts + if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc)) + } Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}") return null } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 972bc6621..84a5879b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -324,8 +324,26 @@ fun ComposeView( } fun deleteUnusedFiles() { - chatModel.filesToDelete.forEach { it.delete() } - chatModel.filesToDelete.clear() + val shared = chatModel.sharedContent.value + if (shared == null) { + chatModel.filesToDelete.forEach { it.delete() } + chatModel.filesToDelete.clear() + } else { + val sharedPaths = when (shared) { + is SharedContent.Media -> shared.uris.map { it.toString() } + is SharedContent.File -> listOf(shared.uri.toString()) + is SharedContent.Text -> emptyList() + } + // When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing + chatModel.filesToDelete.removeAll { file -> + if (sharedPaths.any { it.endsWith(file.name) }) { + false + } else { + file.delete() + true + } + } + } } suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index 38c9e58ad..ea4ef79d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -42,8 +42,8 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { val (newProfile, _) = updated chatModel.updateCurrentUser(newProfile) profile = newProfile + close() } - close() } } ) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 1ae85cd05..b5ffd6630 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -16,6 +16,8 @@ Opening database… + Invalid file path + You shared an invalid file path. Report the issue to the app developers. connected diff --git a/cabal.project b/cabal.project index 8a05c0bb9..eb557e52f 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc + tag: 55a6157880396be899c010f880a42322cf65258a source-repository-package type: git diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index 64b76e7fe..94b8d9197 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -7,7 +7,7 @@ revision: 01.10.2023 | Updated 01.10.2023 | Languages: EN | # Download SimpleX apps -The latest stable version is v5.3.1. +The latest stable version is v5.3.2. You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). @@ -21,9 +21,9 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps. -**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-ubuntu-22_04-x86_64.deb). +**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-desktop-ubuntu-22_04-x86_64.deb). -**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). +**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0-beta.0/simplex-desktop-windows-x86-64.msi) (BETA). @@ -31,14 +31,14 @@ Using the same profile as on mobile device is not yet supported – you need to **iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu). -**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-armv7a.apk). +**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-armv7a.apk). ## Terminal (console) app See [Using terminal app](/docs/CLI.md). -**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-ubuntu-22_04-x86-64). +**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-chat-ubuntu-22_04-x86-64). -**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). +**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). -**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.1/simplex-chat-windows-x86-64). +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.2/simplex-chat-windows-x86-64). diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 60b9505fe..6c41ff2a1 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc" = "0xcbvxz2nszm1sdh6gvmfzjf9n2ldsarmmzbl6j6b5hg9i1mppc6"; + "https://github.com/simplex-chat/simplexmq.git"."55a6157880396be899c010f880a42322cf65258a" = "1fhhyi2060pp72izrqki6gazb759hcv9wypxf39jkwpqpvrn81hv"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."804fa283f067bd3fd89b8c5f8d25b3047813a517" = "1j67wp7rfybfx3ryx08z6gqmzj85j51hmzhgx47ihgmgr47sl895"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index de2fd5f06..949a2a1af 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1534,13 +1534,15 @@ processChatCommand = \case chatRef <- getChatRef user chatName chatItemId <- getChatItemIdByText user chatRef msg processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction - APINewGroup userId gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do + APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do checkValidName displayName gVar <- asks idsDrg - groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile + -- [incognito] generate incognito profile for group membership + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile incognitoProfile pure $ CRGroupCreated user groupInfo - NewGroup gProfile -> withUser $ \User {userId} -> - processChatCommand $ APINewGroup userId gProfile + NewGroup incognito gProfile -> withUser $ \User {userId} -> + processChatCommand $ APINewGroup userId incognito gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db user contactId @@ -1571,12 +1573,12 @@ processChatCommand = \case Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do - (invitation, ct) <- withStore $ \db -> do - inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db user groupId - (inv,) <$> getContactViaMember db user fromMember - let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation - Contact {activeConn = Connection {peerChatVRange}} = ct withChatLock "joinGroup" . procCmd $ do + (invitation, ct) <- withStore $ \db -> do + inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db user groupId + (inv,) <$> getContactViaMember db user fromMember + let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation + Contact {activeConn = Connection {peerChatVRange}} = ct subMode <- chatReadVar subscriptionMode dm <- directMessage $ XGrpAcpt membership.memberId agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm subMode @@ -5748,8 +5750,8 @@ chatCommandP = ("/help settings" <|> "/hs") $> ChatHelp HSSettings, ("/help db" <|> "/hd") $> ChatHelp HSDatabase, ("/help" <|> "/h") $> ChatHelp HSMain, - ("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile), - "/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP), + ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), + "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)), ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName), ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index d2179b1a9..1bb28f89d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -375,8 +375,8 @@ data ChatCommand | EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text} | UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text} | ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text} - | APINewGroup UserId GroupProfile - | NewGroup GroupProfile + | APINewGroup UserId IncognitoEnabled GroupProfile + | NewGroup IncognitoEnabled GroupProfile | AddMember GroupName ContactName GroupMemberRole | JoinGroup GroupName | MemberRole GroupName ContactName GroupMemberRole diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 563cc337e..477361acd 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -193,17 +193,6 @@ createIncognitoProfile db User {userId} p = do createdAt <- getCurrentTime createIncognitoProfile_ db userId createdAt p -createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 -createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do - DB.execute - db - [sql| - INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?) - |] - (displayName, fullName, image, userId, Just True, createdAt, createdAt) - insertedRowId db - createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact createDirectContact db user@User {userId} activeConn@Connection {connId, localAlias} p@Profile {preferences} = do createdAt <- liftIO getCurrentTime diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 76d68cc6b..0b296b17e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -283,11 +283,12 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo -createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do +createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo +createNewGroup db gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime + customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do groupId <- liftIO $ do DB.execute @@ -301,7 +302,7 @@ createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do (ldn, userId, profileId, True, currentTs, currentTs, currentTs) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 - membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs + membership <- createContactMemberInv_ db user groupId user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser customUserProfileId currentTs let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs} diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 5cc1e87d5..d00dce718 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -190,6 +190,17 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange where ent ct = if connType == ct then entityId else Nothing +createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 +createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do + DB.execute + db + [sql| + INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?) + |] + (displayName, fullName, image, userId, Just True, createdAt, createdAt) + insertedRowId db + setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO () setPeerChatVRange db connId (VersionRange minVer maxVer) = DB.execute diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 6c0e703d0..501e232a6 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -133,7 +133,7 @@ responseToView (currentRH, user_) ChatConfig {logLevel, showReactions, showRecei CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"] - CRGroupCreated u g -> ttyUser u $ viewGroupCreated g + CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRGroupsList u gs -> ttyUser u $ viewGroupsList gs CRSentGroupInvitation u g c _ -> @@ -806,11 +806,22 @@ viewReceivedContactRequest c Profile {fullName} = "to reject: " <> highlight ("/rc " <> viewName c) <> " (the sender will NOT be notified)" ] -viewGroupCreated :: GroupInfo -> [StyledString] -viewGroupCreated g = - [ "group " <> ttyFullGroup g <> " is created", - "to add members use " <> highlight ("/a " <> viewGroupName g <> " ") <> " or " <> highlight ("/create link #" <> viewGroupName g) - ] +viewGroupCreated :: GroupInfo -> Bool -> [StyledString] +viewGroupCreated g testView = + case incognitoMembershipProfile g of + Just localProfile + | testView -> incognitoProfile' profile : message + | otherwise -> message + where + profile = fromLocalProfile localProfile + message = + [ "group " <> ttyFullGroup g <> " is created, your incognito profile for this group is " <> incognitoProfile' profile, + "to add members use " <> highlight ("/create link #" <> viewGroupName g) + ] + Nothing -> + [ "group " <> ttyFullGroup g <> " is created", + "to add members use " <> highlight ("/a " <> viewGroupName g <> " ") <> " or " <> highlight ("/create link #" <> viewGroupName g) + ] viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation g c = @@ -1711,7 +1722,7 @@ viewChatError logLevel = \case _ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role) CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> plain (strEncode role) <> ", use member or observer"] CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"] - CEGroupIncognitoCantInvite -> ["you've connected to this group using an incognito profile - prohibited to invite contacts"] + CEGroupIncognitoCantInvite -> ["you are using an incognito profile for this group - prohibited to invite contacts"] CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> viewGroupName g)] CEGroupMemberNotActive -> ["your group connection is not active yet, try later"] diff --git a/stack.yaml b/stack.yaml index 99b9d179c..e6f6f49b7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc + commit: 55a6157880396be899c010f880a42322cf65258a - github: kazu-yamamoto/http2 commit: 804fa283f067bd3fd89b8c5f8d25b3047813a517 # - ../direct-sqlcipher diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 7a8b1368b..97b749106 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -23,6 +23,7 @@ chatGroupTests = do describe "chat groups" $ do it "add contacts, create group and send/receive messages" testGroup it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages + it "create group with incognito membership" testNewGroupIncognito it "create and join group with 4 members" testGroup2 it "create and delete group" testGroupDelete it "create group with the same displayName" testGroupSameName @@ -277,6 +278,56 @@ testGroupShared alice bob cath checkMessages = do alice #$> ("/_unread chat #1 on", id, "ok") alice #$> ("/_unread chat #1 off", id, "ok") +testNewGroupIncognito :: HasCallStack => FilePath -> IO () +testNewGroupIncognito = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + -- alice creates group with incognito membership + alice ##> "/g i team" + aliceIncognito <- getTermLine alice + alice <## ("group #team is created, your incognito profile for this group is " <> aliceIncognito) + alice <## "to add members use /create link #team" + + -- alice invites bob + alice ##> "/a team bob" + alice <## "you are using an incognito profile for this group - prohibited to invite contacts" + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + bob ##> ("/c " <> gLink) + bob <## "connection request sent!" + alice <## "bob_1 (Bob): accepting request to join group #team..." + _ <- getTermLine alice + concurrentlyN_ + [ do + alice <## ("bob_1 (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## "use /i bob_1 to print out this incognito profile again" + alice <## "bob_1 invited to group #team via your group link" + alice <## "#team: bob_1 joined the group", + do + bob <## (aliceIncognito <> ": contact is connected") + bob <## "#team: you joined the group" + ] + + alice <##> bob + + alice ?#> "@bob_1 hi, I'm incognito" + bob <# (aliceIncognito <> "> hi, I'm incognito") + bob #> ("@" <> aliceIncognito <> " hey, I'm bob") + alice ?<# "bob_1> hey, I'm bob" + + alice ?#> "#team hello" + bob <# ("#team " <> aliceIncognito <> "> hello") + bob #> "#team hi there" + alice ?<# "#team bob_1> hi there" + + alice ##> "/gs" + alice <## "i #team (2 members)" + bob ##> "/gs" + bob <## "#team (2 members)" + testGroup2 :: HasCallStack => FilePath -> IO () testGroup2 = testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 68b925342..d806290d6 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1085,7 +1085,7 @@ testJoinGroupIncognito = ] -- cath cannot invite to the group because her membership is incognito cath ##> "/a secret_club dan" - cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts" + cath <## "you are using an incognito profile for this group - prohibited to invite contacts" -- alice invites dan alice ##> "/a secret_club dan admin" concurrentlyN_ diff --git a/website/langs/en.json b/website/langs/en.json index 434aed834..1bb64c7ef 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -37,12 +37,12 @@ "hero-overlay-2-title": "Why user IDs are bad for privacy?", "hero-overlay-3-title": "Security assessment", "feature-1-title": "E2E-encrypted messages with markdown and editing", - "feature-2-title": "E2E-encrypted
images and files", - "feature-3-title": "Decentralized secret groups —
only users know they exist", + "feature-2-title": "E2E-encrypted
images, videos and files", + "feature-3-title": "E2E-encrypted decentralized groups — only users know they exist", "feature-4-title": "E2E-encrypted voice messages", "feature-5-title": "Disappearing messages", "feature-6-title": "E2E-encrypted
audio and video calls", - "feature-7-title": "Portable encrypted database — move your profile to another device", + "feature-7-title": "Portable encrypted app storage — move profile to another device", "feature-8-title": "Incognito mode —
unique to SimpleX Chat", "simplex-network-overlay-1-title": "Comparison with P2P messaging protocols", "simplex-private-1-title": "2-layers of
end-to-end encryption",