From 73130bf3215aa37a8b2ec013a566d1cb650864e2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:48:25 +0000 Subject: [PATCH 1/5] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 60a9e2ff00..f5701af155 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -116,11 +116,11 @@ 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; - 5CCD1A602B27927E001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */; }; - 5CCD1A612B27927E001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5C2B27927E001A4199 /* libgmp.a */; }; - 5CCD1A622B27927E001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5D2B27927E001A4199 /* libffi.a */; }; - 5CCD1A632B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */; }; - 5CCD1A642B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */; }; + 5CCD1A882B2A5D56001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A832B2A5D55001A4199 /* libgmp.a */; }; + 5CCD1A892B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */; }; + 5CCD1A8A2B2A5D56001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */; }; + 5CCD1A8B2B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */; }; + 5CCD1A8C2B2A5D56001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A872B2A5D56001A4199 /* libffi.a */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -402,11 +402,11 @@ 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; - 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CCD1A5C2B27927E001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CCD1A5D2B27927E001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a"; sourceTree = ""; }; - 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a"; sourceTree = ""; }; + 5CCD1A832B2A5D55001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a"; sourceTree = ""; }; + 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a"; sourceTree = ""; }; + 5CCD1A872B2A5D56001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; @@ -517,13 +517,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CCD1A602B27927E001A4199 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CCD1A612B27927E001A4199 /* libgmp.a in Frameworks */, - 5CCD1A622B27927E001A4199 /* libffi.a in Frameworks */, + 5CCD1A8B2B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a in Frameworks */, + 5CCD1A8A2B2A5D56001A4199 /* libgmpxx.a in Frameworks */, + 5CCD1A882B2A5D56001A4199 /* libgmp.a in Frameworks */, + 5CCD1A8C2B2A5D56001A4199 /* libffi.a in Frameworks */, + 5CCD1A892B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CCD1A642B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */, - 5CCD1A632B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -585,11 +585,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CCD1A5D2B27927E001A4199 /* libffi.a */, - 5CCD1A5C2B27927E001A4199 /* libgmp.a */, - 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */, - 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */, - 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */, + 5CCD1A872B2A5D56001A4199 /* libffi.a */, + 5CCD1A832B2A5D55001A4199 /* libgmp.a */, + 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */, + 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */, + 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */, ); path = Libraries; sourceTree = ""; From 8cec5428ee59cd84c54929524d28496801ff7851 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:08:40 +0400 Subject: [PATCH 2/5] core: save CIContent tag in chat_items table (#3555) --- simplex-chat.cabal | 1 + src/Simplex/Chat/Messages/CIContent.hs | 29 +++++++++++++++++++ .../Migrations/M20231214_item_content_tag.hs | 18 ++++++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store/Messages.hs | 9 +++--- src/Simplex/Chat/Store/Migrations.hs | 4 ++- 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d8c6f24fb4..ce066bc3c1 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -126,6 +126,7 @@ library Simplex.Chat.Migrations.M20231114_remote_control Simplex.Chat.Migrations.M20231126_remote_ctrl_address Simplex.Chat.Migrations.M20231207_chat_list_pagination + Simplex.Chat.Migrations.M20231214_item_content_tag Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 6b7e66bdb3..a987603bb6 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -574,3 +574,32 @@ dbParseACIContent = fmap aciContentDBJSON . J.eitherDecodeStrict' . encodeUtf8 -- platform specific instance FromJSON ACIContent where parseJSON = fmap aciContentJSON . J.parseJSON + +toCIContentTag :: CIContent e -> Text +toCIContentTag ciContent = case ciContent of + CISndMsgContent _ -> "sndMsgContent" + CIRcvMsgContent _ -> "rcvMsgContent" + CISndDeleted _ -> "sndDeleted" + CIRcvDeleted _ -> "rcvDeleted" + CISndCall {} -> "sndCall" + CIRcvCall {} -> "rcvCall" + CIRcvIntegrityError _ -> "rcvIntegrityError" + CIRcvDecryptionError {} -> "rcvDecryptionError" + CIRcvGroupInvitation {} -> "rcvGroupInvitation" + CISndGroupInvitation {} -> "sndGroupInvitation" + CIRcvDirectEvent _ -> "rcvDirectEvent" + CIRcvGroupEvent _ -> "rcvGroupEvent" + CISndGroupEvent _ -> "sndGroupEvent" + CIRcvConnEvent _ -> "rcvConnEvent" + CISndConnEvent _ -> "sndConnEvent" + CIRcvChatFeature {} -> "rcvChatFeature" + CISndChatFeature {} -> "sndChatFeature" + CIRcvChatPreference {} -> "rcvChatPreference" + CISndChatPreference {} -> "sndChatPreference" + CIRcvGroupFeature {} -> "rcvGroupFeature" + CISndGroupFeature {} -> "sndGroupFeature" + CIRcvChatFeatureRejected _ -> "rcvChatFeatureRejected" + CIRcvGroupFeatureRejected _ -> "rcvGroupFeatureRejected" + CISndModerated -> "sndModerated" + CIRcvModerated -> "rcvModerated" + CIInvalidJSON _ -> "invalidJSON" diff --git a/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs b/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs new file mode 100644 index 0000000000..cd4cd136e5 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20231214_item_content_tag where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20231214_item_content_tag :: Query +m20231214_item_content_tag = + [sql| +ALTER TABLE chat_items ADD COLUMN item_content_tag TEXT; +|] + +down_m20231214_item_content_tag :: Query +down_m20231214_item_content_tag = + [sql| +ALTER TABLE chat_items DROP COLUMN item_content_tag; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 3b83b132df..7f5945d394 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -379,7 +379,8 @@ CREATE TABLE chat_items( item_live INTEGER, item_deleted_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, item_deleted_ts TEXT, - forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL + forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, + item_content_tag TEXT ); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 87e6667124..b817c844d5 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -399,18 +399,19 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q -- user and IDs user_id, created_by_msg_id, contact_id, group_id, group_member_id, -- meta - item_sent, item_ts, item_content, item_text, item_status, shared_msg_id, forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, + item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id, + forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((userId, msgId_) :. idsRow :. itemRow :. quoteRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId where - itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, CIStatus d, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime) - itemRow = (msgDirection @d, itemTs, ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed + itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime) + itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64) idsRow = case chatDirection of CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index c8a04c42a1..af9985b837 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -92,6 +92,7 @@ import Simplex.Chat.Migrations.M20231113_group_forward import Simplex.Chat.Migrations.M20231114_remote_control import Simplex.Chat.Migrations.M20231126_remote_ctrl_address import Simplex.Chat.Migrations.M20231207_chat_list_pagination +import Simplex.Chat.Migrations.M20231214_item_content_tag import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -183,7 +184,8 @@ schemaMigrations = ("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward), ("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control), ("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address), - ("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination) + ("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination), + ("20231214_item_content_tag", m20231214_item_content_tag, Just down_m20231214_item_content_tag) ] -- | The list of migrations in ascending order by date From 974fa448b4ec52387ca9e062734d3378d1967266 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:11:19 +0800 Subject: [PATCH 3/5] android, desktop: some alerts became privacy sensitive (#3554) * android, desktop: some alerts became privacy sensitive * changes --- .../java/chat/simplex/app/MainActivity.kt | 4 +- .../kotlin/chat/simplex/common/App.kt | 21 ++++++-- .../chat/simplex/common/model/ChatModel.kt | 4 +- .../chat/simplex/common/model/SimpleXAPI.kt | 3 +- .../simplex/common/views/chat/ComposeView.kt | 2 +- .../views/chatlist/ChatListNavLinkView.kt | 10 ++-- .../common/views/chatlist/ChatListView.kt | 9 +--- .../common/views/helpers/AlertManager.kt | 9 +++- .../common/views/localauth/LocalAuthView.kt | 3 +- .../common/views/newchat/ScanToConnectView.kt | 50 +++++++++---------- 10 files changed, 67 insertions(+), 48 deletions(-) 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 cbe0ef7b16..082c10582c 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 @@ -124,7 +124,9 @@ fun processIntent(intent: Intent?) { when (intent?.action) { "android.intent.action.VIEW" -> { val uri = intent.data - if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId(), uri.toURI(), ChatModel) + if (uri != null) { + chatModel.appOpenUrl.value = null to uri.toURI() + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 4387adf95e..0082972c7a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -162,11 +162,26 @@ fun MainScreen() { AuthView() } else { SplashView() + ModalManager.fullscreen.showPasscodeInView() + } + } else { + if (chatModel.showCallView.value) { + ActiveCallView() + } else { + // It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked + ModalManager.fullscreen.showPasscodeInView() + } + AlertManager.privacySensitive.showInView() + if (onboarding == OnboardingStage.OnboardingComplete) { + LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) { + val (rhId, url) = chatModel.appOpenUrl.value ?: (null to null) + if (url != null) { + chatModel.appOpenUrl.value = null + connectIfOpenedViaUri(rhId, url, chatModel) + } + } } - } else if (chatModel.showCallView.value) { - ActiveCallView() } - ModalManager.fullscreen.showPasscodeInView() val invitation = chatModel.activeCallInvitation.value if (invitation != null) IncomingCallAlertView(invitation, chatModel) AlertManager.shared.showInView() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 25c87d64ad..2305862b68 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -70,8 +70,8 @@ object ChatModel { // Only needed during onboarding when user skipped password setup (left as random password) val desktopOnboardingRandomPassword = mutableStateOf(false) - // set when app is opened via contact or invitation URI - val appOpenUrl = mutableStateOf(null) + // set when app is opened via contact or invitation URI (rhId, uri) + val appOpenUrl = mutableStateOf?>(null) // preferences val notificationPreviewMode by lazy { 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 ad897c60fd..4af3e3f2ed 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 @@ -2023,7 +2023,8 @@ object ChatController { chatModel.chatId.value = null ModalManager.center.closeModals() ModalManager.end.closeModals() - AlertManager.shared.alertViews.clear() + AlertManager.shared.hideAllAlerts() + AlertManager.privacySensitive.hideAllAlerts() chatModel.currentRemoteHost.value = switchRemoteHost(rhId) reloadRemoteHosts() val user = apiGetActiveUser(rhId) 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 ddcfcf5947..b230d261f1 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 @@ -201,7 +201,7 @@ suspend fun MutableState.processPickedMedia(uris: List, text: // Image val drawable = getDrawableFromUri(uri) // Do not show alert in case it's already shown from the function above - bitmap = getBitmapFromUri(uri, withAlertOnException = AlertManager.shared.alertViews.isEmpty()) + bitmap = getBitmapFromUri(uri, withAlertOnException = !AlertManager.shared.hasAlertsShown()) if (isAnimImage(uri, drawable)) { // It's a gif or webp val fileSize = getFileSize(uri) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 9d662758f8..9ae0da2a31 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -611,12 +611,12 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close: (() -> Unit)?, openChat: Boolean ) { - AlertManager.shared.showAlertDialogButtonsColumn( + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName), buttons = { Column { SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) @@ -628,7 +628,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) @@ -640,7 +640,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -654,7 +654,7 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactI val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) if (contact != null) { chatModel.updateContact(rhId, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), hostDevice = hostDevice(rhId), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index a91e5e7b3c..18252d0e22 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -49,13 +49,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false) } - LaunchedEffect(chatModel.appOpenUrl.value) { - val url = chatModel.appOpenUrl.value - if (url != null) { - chatModel.appOpenUrl.value = null - connectIfOpenedViaUri(chatModel.remoteHostId(), url, chatModel) - } - } if (appPlatform.isDesktop) { KeyChangeEffect(chatModel.chatId.value) { if (chatModel.chatId.value != null) { @@ -302,7 +295,7 @@ expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow Unit)>() + private var alertViews = mutableStateListOf<(@Composable () -> Unit)>() fun showAlert(alert: @Composable () -> Unit) { Log.d(TAG, "AlertManager.showAlert") @@ -35,6 +35,12 @@ class AlertManager { alertViews.removeLastOrNull() } + fun hideAllAlerts() { + alertViews.clear() + } + + fun hasAlertsShown() = alertViews.isNotEmpty() + fun showAlertDialogButtons( title: String, text: String? = null, @@ -220,6 +226,7 @@ class AlertManager { companion object { val shared = AlertManager() + val privacySensitive = AlertManager() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index c64c3dd29a..468dd8580e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -70,7 +70,8 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( m.controller.startChat(createdUser) } ModalManager.fullscreen.closeModals() - AlertManager.shared.hideAlert() + AlertManager.shared.hideAllAlerts() + AlertManager.privacySensitive.hideAllAlerts() completed(LAResult.Success) } catch (e: Exception) { completed(LAResult.Error(generalGetString(MR.strings.incorrect_passcode))) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt index 2439b16c36..9f28074aef 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt @@ -20,7 +20,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.IncognitoView import chat.simplex.res.MR import java.net.URI @@ -58,7 +58,7 @@ suspend fun planAndConnect( InvitationLinkPlan.OwnLink -> { Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), @@ -80,13 +80,13 @@ suspend fun planAndConnect( val contact = connectionPlan.invitationLinkPlan.contact_ if (contact != null) { openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), hostDevice = hostDevice(rhId), ) } else { - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_connecting), generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link), hostDevice = hostDevice(rhId), @@ -97,7 +97,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") val contact = connectionPlan.invitationLinkPlan.contact openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), hostDevice = hostDevice(rhId), @@ -121,7 +121,7 @@ suspend fun planAndConnect( ContactAddressPlan.OwnLink -> { Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), @@ -141,7 +141,7 @@ suspend fun planAndConnect( ContactAddressPlan.ConnectingConfirmReconnect -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_repeat_connection_request), text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), @@ -162,7 +162,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), hostDevice = hostDevice(rhId), @@ -172,7 +172,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), hostDevice = hostDevice(rhId), @@ -193,7 +193,7 @@ suspend fun planAndConnect( GroupLinkPlan.Ok -> { Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_via_group_link), text = generalGetString(MR.strings.you_will_join_group), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), @@ -217,7 +217,7 @@ suspend fun planAndConnect( GroupLinkPlan.ConnectingConfirmReconnect -> { Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_repeat_join_request), text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), @@ -238,12 +238,12 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ if (groupInfo != null) { - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_group_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) ) } else { - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_joining_the_group), generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), hostDevice = hostDevice(rhId), @@ -254,7 +254,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo openKnownGroup(chatModel, rhId, close, groupInfo) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_group_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName), hostDevice = hostDevice(rhId), @@ -289,7 +289,7 @@ suspend fun connectViaUri( if (pcc != null) { chatModel.updateContactConnection(rhId, pcc) close?.invoke() - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = when (connLinkType) { @@ -320,14 +320,14 @@ fun askCurrentOrIncognitoProfileAlert( text: AnnotatedString? = null, connectDestructive: Boolean, ) { - AlertManager.shared.showAlertDialogButtonsColumn( + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = title, text = text, buttons = { Column { val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close) } @@ -335,7 +335,7 @@ fun askCurrentOrIncognitoProfileAlert( Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close) } @@ -343,7 +343,7 @@ fun askCurrentOrIncognitoProfileAlert( Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -372,14 +372,14 @@ fun ownGroupLinkConfirmConnect( groupInfo: GroupInfo, close: (() -> Unit)?, ) { - AlertManager.shared.showAlertDialogButtonsColumn( + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.connect_plan_join_your_group), text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)), buttons = { Column { // Open group SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() openKnownGroup(chatModel, rhId, close, groupInfo) }) { Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) @@ -387,7 +387,7 @@ fun ownGroupLinkConfirmConnect( if (incognito != null) { // Join incognito / Join with current profile SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } @@ -400,7 +400,7 @@ fun ownGroupLinkConfirmConnect( } else { // Use current profile SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close) } @@ -409,7 +409,7 @@ fun ownGroupLinkConfirmConnect( } // Use new incognito profile SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close) } @@ -419,7 +419,7 @@ fun ownGroupLinkConfirmConnect( } // Cancel SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } From 6fa0001ea72e80fd1124f59efa1f53ad665398cf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:36:25 +0000 Subject: [PATCH 4/5] ios: delay suspendChat in NSE, background schedule depends on notifications mode (#3561) * ios: delay suspendChat in NSE * different background refresh interval depending on the settings * simplify * comment * reduce NSE suspend interval * space --- apps/ios/Shared/AppDelegate.swift | 2 +- apps/ios/Shared/Model/BGManager.swift | 30 +++++++++++++++---- .../ios/SimpleX NSE/NotificationService.swift | 25 ++++++++++++---- apps/ios/SimpleXChat/AppGroup.swift | 3 ++ 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index bb1de94359..145e362797 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -81,7 +81,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages { logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") - if m.ntfEnablePeriodic && allowBackgroundRefresh() { + if m.ntfEnablePeriodic && allowBackgroundRefresh() && BGManager.shared.lastRanLongAgo { receiveMessages(completionHandler) } else { completionHandler(.noData) diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index a39155efe8..25eab6c69e 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -16,7 +16,12 @@ private let receiveTaskId = "chat.simplex.app.receive" private let waitForMessages: TimeInterval = 6 // This is the smallest interval between refreshes, and also target interval in "off" mode -private let bgRefreshInterval: TimeInterval = 600 +private let bgRefreshInterval: TimeInterval = 600 // 10 minutes + +// This intervals are used for background refresh in instant and periodic modes +private let periodicBgRefreshInterval: TimeInterval = 1200 // 20 minutes + +private let maxBgRefreshInterval: TimeInterval = 2400 // 40 minutes private let maxTimerCount = 9 @@ -34,14 +39,14 @@ class BGManager { } } - func schedule() { + func schedule(interval: TimeInterval? = nil) { if !ChatModel.shared.ntfEnableLocal { logger.debug("BGManager.schedule: disabled") return } logger.debug("BGManager.schedule") let request = BGAppRefreshTaskRequest(identifier: receiveTaskId) - request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshInterval) + request.earliestBeginDate = Date(timeIntervalSinceNow: interval ?? runInterval) do { try BGTaskScheduler.shared.submit(request) } catch { @@ -49,20 +54,34 @@ class BGManager { } } + var runInterval: TimeInterval { + switch ChatModel.shared.notificationMode { + case .instant: maxBgRefreshInterval + case .periodic: periodicBgRefreshInterval + case .off: bgRefreshInterval + } + } + + var lastRanLongAgo: Bool { + Date.now.timeIntervalSince(chatLastBackgroundRunGroupDefault.get()) > runInterval + } + private func handleRefresh(_ task: BGAppRefreshTask) { if !ChatModel.shared.ntfEnableLocal { logger.debug("BGManager.handleRefresh: disabled") return } logger.debug("BGManager.handleRefresh") - schedule() - if allowBackgroundRefresh() { + let shouldRun_ = lastRanLongAgo + if allowBackgroundRefresh() && shouldRun_ { + schedule() let completeRefresh = completionHandler { task.setTaskCompleted(success: true) } task.expirationHandler = { completeRefresh("expirationHandler") } receiveMessages(completeRefresh) } else { + schedule(interval: shouldRun_ ? bgRefreshInterval : runInterval) logger.debug("BGManager.completionHandler: already active, not started") task.setTaskCompleted(success: true) } @@ -91,6 +110,7 @@ class BGManager { } self.completed = false DispatchQueue.main.async { + chatLastBackgroundRunGroupDefault.set(Date.now) let m = ChatModel.shared if (!m.chatInitialized) { setAppState(.bgRefresh) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index eaa1131eb8..c286ee1c3c 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -14,9 +14,11 @@ import SimpleXChat let logger = Logger() -let suspendingDelay: UInt64 = 2_500_000_000 +let appSuspendingDelay: UInt64 = 2_500_000_000 -let nseSuspendTimeout: Int = 10 +let nseSuspendDelay: TimeInterval = 2 + +let nseSuspendTimeout: Int = 5 typealias NtfStream = ConcurrentQueue @@ -177,6 +179,10 @@ class NSEThreads { return false } } + + var noThreads: Bool { + allThreads.isEmpty + } } // Notification service extension creates a new instance of the class and calls didReceive for each notification. @@ -261,7 +267,7 @@ class NotificationService: UNNotificationServiceExtension { let dbStatus = startChat() if case .ok = dbStatus, let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo), privacy: .public)") + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)") if let connEntity = ntfInfo.connEntity_ { setBestAttemptNtf( ntfInfo.ntfsEnabled @@ -326,7 +332,15 @@ class NotificationService: UNNotificationServiceExtension { if let t = threadId { threadId = nil if NSEThreads.shared.endThread(t) { - suspendChat(nseSuspendTimeout) + logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") + // suspension is delayed to allow chat core finalise any processing + // (e.g., send delivery receipts) + DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { + if NSEThreads.shared.noThreads { + logger.debug("NotificationService.deliverBestAttemptNtf: suspending...") + suspendChat(nseSuspendTimeout) + } + } } } if let handler = contentHandler, let ntf = bestAttemptNtf { @@ -497,7 +511,7 @@ func suspendChat(_ timeout: Int) { NSEChatState.shared.set(.suspending) if apiSuspendChat(timeoutMicroseconds: timeout * 1000000) { - logger.debug("NotificationService: activateChat: after apiActivateChat") + logger.debug("NotificationService: suspendChat: after apiSuspendChat") DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: chatSuspended) } else { NSEChatState.shared.set(state) @@ -510,6 +524,7 @@ func chatSuspended() { if case .suspending = NSEChatState.shared.value { NSEChatState.shared.set(.suspended) chatCloseStore() + logger.debug("NotificationService chatSuspended: suspended") } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 10625e2edf..f79c294e0c 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -15,6 +15,7 @@ let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" +public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used @@ -156,6 +157,8 @@ public let dbContainerGroupDefault = EnumDefault( public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START) +public let chatLastBackgroundRunGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN) + public let ntfPreviewModeGroupDefault = EnumDefault( defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_PREVIEW_MODE, From f0338a03d1c986c33fb91711a58ea12e5ee21836 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:41:08 +0000 Subject: [PATCH 5/5] directory: better search, allow both simplex:/ and simplex.chat links in description (#3546) * directory: new commands * better search * search test * return group links in simplex.chat domain, allow both simplex:/ and simplex.chat links in group description --- .../src/Directory/Events.hs | 24 +++- .../src/Directory/Options.hs | 2 + .../src/Directory/Search.hs | 32 +++++ .../src/Directory/Service.hs | 128 ++++++++++++++---- simplex-chat.cabal | 2 + tests/Bots/DirectoryTests.hs | 95 ++++++++++++- 6 files changed, 250 insertions(+), 33 deletions(-) create mode 100644 apps/simplex-directory-service/src/Directory/Search.hs diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 89231e4db1..a187ac3e82 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -21,14 +21,18 @@ where import Control.Applicative ((<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A +import Data.Functor (($>)) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Directory.Store import Simplex.Chat.Controller import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util ((<$?>)) import Data.Char (isSpace) import Data.Either (fromRight) @@ -83,6 +87,10 @@ deriving instance Show (SDirectoryRole r) data DirectoryCmdTag (r :: DirectoryRole) where DCHelp_ :: DirectoryCmdTag 'DRUser + DCSearchNext_ :: DirectoryCmdTag 'DRUser + DCAllGroups_ :: DirectoryCmdTag 'DRUser + DCRecentGroups_ :: DirectoryCmdTag 'DRUser + DCSubmitGroup_ :: DirectoryCmdTag 'DRUser DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser @@ -100,6 +108,10 @@ data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r) data DirectoryCmd (r :: DirectoryRole) where DCHelp :: DirectoryCmd 'DRUser DCSearchGroup :: Text -> DirectoryCmd 'DRUser + DCSearchNext :: DirectoryCmd 'DRUser + DCAllGroups :: DirectoryCmd 'DRUser + DCRecentGroups :: DirectoryCmd 'DRUser + DCSubmitGroup :: ConnReqContact -> DirectoryCmd 'DRUser DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser @@ -120,7 +132,9 @@ deriving instance Show ADirectoryCmd directoryCmdP :: Parser ADirectoryCmd directoryCmdP = - (A.char '/' *> cmdStrP) <|> (ADC SDRUser . DCSearchGroup <$> A.takeText) + (A.char '/' *> cmdStrP) + <|> (A.char '.' $> ADC SDRUser DCSearchNext) + <|> (ADC SDRUser . DCSearchGroup <$> A.takeText) where cmdStrP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) @@ -128,6 +142,10 @@ directoryCmdP = tagP = A.takeTill (== ' ') >>= \case "help" -> u DCHelp_ "h" -> u DCHelp_ + "next" -> u DCSearchNext_ + "all" -> u DCAllGroups_ + "new" -> u DCRecentGroups_ + "submit" -> u DCSubmitGroup_ "confirm" -> u DCConfirmDuplicateGroup_ "list" -> u DCListUserGroups_ "ls" -> u DCListUserGroups_ @@ -146,6 +164,10 @@ directoryCmdP = cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) cmdP = \case DCHelp_ -> pure DCHelp + DCSearchNext_ -> pure DCSearchNext + DCAllGroups_ -> pure DCAllGroups + DCRecentGroups_ -> pure DCRecentGroups + DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (A.takeWhile1 isSpace *> A.takeText) DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup DCListUserGroups_ -> pure DCListUserGroups DCDeleteGroup_ -> gc DCDeleteGroup diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 0ca8cee789..6d4e1296f2 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -21,6 +21,7 @@ data DirectoryOpts = DirectoryOpts superUsers :: [KnownContact], directoryLog :: Maybe FilePath, serviceName :: String, + searchResults :: Int, testing :: Bool } @@ -54,6 +55,7 @@ directoryOpts appDir defaultDbFileName = do superUsers, directoryLog, serviceName, + searchResults = 10, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Search.hs b/apps/simplex-directory-service/src/Directory/Search.hs new file mode 100644 index 0000000000..822182b053 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Search.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Directory.Search where + +import Data.List (sortOn) +import Data.Ord (Down (..)) +import Data.Set (Set) +import qualified Data.Set as S +import Data.Text (Text) +import Data.Time.Clock (UTCTime) +import Simplex.Chat.Types + +data SearchRequest = SearchRequest + { searchType :: SearchType, + searchTime :: UTCTime, + sentGroups :: Set GroupId + } + +data SearchType = STAll | STRecent | STSearch Text + +takeTop :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] +takeTop n = take n . sortOn (Down . currentMembers . snd) + +takeRecent :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] +takeRecent n = take n . sortOn (Down . (\GroupInfo {createdAt} -> createdAt) . fst) + +groupIds :: [(GroupInfo, GroupSummary)] -> Set GroupId +groupIds = S.fromList . map (\(GroupInfo {groupId}, _) -> groupId) + +filterNotSent :: Set GroupId -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] +filterNotSent sentGroups = filter (\(GroupInfo {groupId}, _) -> groupId `S.notMember` sentGroups) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index fb187bbebe..ea79dabb10 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -17,16 +17,16 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B -import Data.List (sortOn) import Data.Maybe (fromMaybe, maybeToList) -import Data.Ord (Down(..)) +import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.Time.Clock (getCurrentTime) +import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Directory.Events import Directory.Options +import Directory.Search import Directory.Store import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts @@ -36,8 +36,10 @@ import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types -import Simplex.Chat.View (serializeChatResponse) +import Simplex.Chat.View (serializeChatResponse, simplexChatContact) import Simplex.Messaging.Encoding.String +import Simplex.Messaging.TMap (TMap) +import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) @@ -55,6 +57,15 @@ data GroupRolesStatus | GRSBadRoles deriving (Eq) +data ServiceState = ServiceState + { searchRequests :: TMap ContactId SearchRequest + } + +newServiceState :: IO ServiceState +newServiceState = do + searchRequests <- atomically TM.empty + pure ServiceState {searchRequests} + welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do appDir <- getAppUserDataDirectory "simplex" @@ -65,8 +76,9 @@ welcomeGetOpts = do pure opts directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {userId} cc = do +directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testing} user@User {userId} cc = do initializeBotAddress' (not testing) cc + env <- newServiceState race_ (forever $ void getLine) . forever $ do (_, _, resp) <- atomically . readTBQueue $ outputQ cc forM_ (crDirectoryEvent resp) $ \case @@ -84,7 +96,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { DEItemEditIgnored _ct -> pure () DEItemDeleteIgnored _ct -> pure () DEContactCommand ct ciId aCmd -> case aCmd of - ADC SDRUser cmd -> deUserCommand ct ciId cmd + ADC SDRUser cmd -> deUserCommand env ct ciId cmd ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd where withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId @@ -105,8 +117,11 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)]) - getGroups search = - sendChatCmd cc (APIListGroups userId Nothing $ Just $ T.unpack search) >>= \case + getGroups = getGroups_ . Just + + getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)]) + getGroups_ search_ = + sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case CRGroupsList {groups} -> pure $ Just groups _ -> pure Nothing @@ -140,7 +155,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { sendMessage cc ct $ "Welcome to " <> serviceName <> " service!\n\ \Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ - \For example, send _privacy_ to find groups about privacy.\n\n\ + \For example, send _privacy_ to find groups about privacy.\n\ + \Or send */all* or */new* to list groups.\n\n\ \Content and privacy policy: https://simplex.chat/docs/directory.html" deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () @@ -201,7 +217,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { "Created the public link to join the group via this directory service that is always online.\n\n\ \Please add it to the group welcome message.\n\ \For example, add:" - notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode connReqContact) + notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode $ simplexChatContact connReqContact) CRChatCmdError _ (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." @@ -276,9 +292,10 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { where profileUpdate = \case CRGroupLink {connReqContact} -> - let groupLink = safeDecodeUtf8 $ strEncode connReqContact - hadLinkBefore = groupLink `isInfix` description p - hasLinkNow = groupLink `isInfix` description p' + let groupLink1 = safeDecodeUtf8 $ strEncode connReqContact + groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact + hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p + hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' in if | hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore -> GPServiceLinkRemoved @@ -379,8 +396,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." - deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () - deUserCommand ct ciId = \case + deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () + deUserCommand env@ServiceState {searchRequests} ct ciId = \case DCHelp -> sendMessage cc ct $ "You must be the owner to add the group to the directory:\n\ @@ -389,20 +406,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { \3. You will then need to add this link to the group welcome message.\n\ \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ \Start from inviting the bot to your group as admin - it will guide you through the process" - DCSearchGroup s -> - getGroups s >>= \case - Just groups -> - atomically (filterListedGroups st groups) >>= \case - [] -> sendReply "No groups found" - gs -> do - sendReply $ "Found " <> show (length gs) <> " group(s)" <> if length gs > 10 then ", sending 10." else "" - void . forkIO $ forM_ (take 10 $ sortOn (Down . currentMembers . snd) gs) $ - \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do - let membersStr = "_" <> tshow currentMembers <> " members_" - text = groupInfoText p <> "\n" <> membersStr - msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ - sendComposedMessage cc ct Nothing msg - Nothing -> sendReply "Error: getGroups. Please notify the developers." + DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s + DCSearchNext -> + atomically (TM.lookup (contactId' ct) searchRequests) >>= \case + Just search@SearchRequest {searchType, searchTime} -> do + currentTime <- getCurrentTime + if diffUTCTime currentTime searchTime > 300 -- 5 minutes + then do + atomically $ TM.delete (contactId' ct) searchRequests + showAllGroups + else case searchType of + STSearch s -> withFoundListedGroups (Just s) $ sendNextSearchResults takeTop search + STAll -> withFoundListedGroups Nothing $ sendNextSearchResults takeTop search + STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search + Nothing -> showAllGroups + where + showAllGroups = deUserCommand env ct ciId DCAllGroups + DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll + DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent + DCSubmitGroup _link -> pure () DCConfirmDuplicateGroup ugrId gName -> atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" @@ -429,6 +451,54 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { DCCommandError tag -> sendReply $ "Command error: " <> show tag where sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + withFoundListedGroups s_ action = + getGroups_ s_ >>= \case + Just groups -> atomically (filterListedGroups st groups) >>= action + Nothing -> sendReply "Error: getGroups. Please notify the developers." + sendSearchResults s = \case + [] -> sendReply "No groups found" + gs -> do + let gs' = takeTop searchResults gs + moreGroups = length gs - length gs' + more = if moreGroups > 0 then ", sending top " <> show (length gs') else "" + sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." + updateSearchRequest (STSearch s) $ groupIds gs' + sendFoundGroups gs' moreGroups + sendAllGroups takeFirst sortName searchType = \case + [] -> sendReply "No groups listed" + gs -> do + let gs' = takeFirst searchResults gs + moreGroups = length gs - length gs' + more = if moreGroups > 0 then ", sending " <> sortName <> " " <> show (length gs') else "" + sendReply $ show (length gs) <> " group(s) listed" <> more <> "." + updateSearchRequest searchType $ groupIds gs' + sendFoundGroups gs' moreGroups + sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case + [] -> do + sendReply "Sorry, no more groups" + atomically $ TM.delete (contactId' ct) searchRequests + gs -> do + let gs' = takeFirst searchResults $ filterNotSent sentGroups gs + sentGroups' = sentGroups <> groupIds gs' + moreGroups = length gs - S.size sentGroups' + sendReply $ "Sending " <> show (length gs') <> " more group(s)." + updateSearchRequest searchType sentGroups' + sendFoundGroups gs' moreGroups + updateSearchRequest :: SearchType -> Set GroupId -> IO () + updateSearchRequest searchType sentGroups = do + searchTime <- getCurrentTime + let search = SearchRequest {searchType, searchTime, sentGroups} + atomically $ TM.insert (contactId' ct) search searchRequests + sendFoundGroups gs moreGroups = + void . forkIO $ do + forM_ gs $ + \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do + let membersStr = "_" <> tshow currentMembers <> " members_" + text = groupInfoText p <> "\n" <> membersStr + msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ + sendComposedMessage cc ct Nothing msg + when (moreGroups > 0) $ + sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () deSuperUserCommand ct ciId cmd diff --git a/simplex-chat.cabal b/simplex-chat.cabal index ce066bc3c1..f3918dfecd 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -467,6 +467,7 @@ executable simplex-directory-service other-modules: Directory.Events Directory.Options + Directory.Search Directory.Service Directory.Store Paths_simplex_chat @@ -553,6 +554,7 @@ test-suite simplex-chat-test Broadcast.Options Directory.Events Directory.Options + Directory.Search Directory.Service Directory.Store Paths_simplex_chat diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index b31d6f36fe..3c6991bb52 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -30,6 +30,7 @@ directoryServiceTests = do it "should suspend and resume group" testSuspendResume it "should join found group via link" testJoinGroup it "should support group names with spaces" testGroupNameWithSpaces + it "should return more groups in search, all and recent groups" testSearchGroups describe "de-listing the group" $ do it "should de-list if owner leaves the group" testDelistedOwnerLeaves it "should de-list if owner is removed from the group" testDelistedOwnerRemoved @@ -67,6 +68,7 @@ mkDirectoryOpts tmp superUsers = superUsers, directoryLog = Just $ tmp "directory_service.log", serviceName = "SimpleX-Directory", + searchResults = 3, testing = True } @@ -158,7 +160,7 @@ testDirectoryService tmp = search u s welcome = do u #> ("@SimpleX-Directory " <> s) u <# ("SimpleX-Directory> > " <> s) - u <## " Found 1 group(s)" + u <## " Found 1 group(s)." u <# "SimpleX-Directory> PSA (Privacy, Security & Anonymity)" u <## "Welcome message:" u <## welcome @@ -206,7 +208,7 @@ testJoinGroup tmp = cath `connectVia` dsLink cath #> "@SimpleX-Directory privacy" cath <# "SimpleX-Directory> > privacy" - cath <## " Found 1 group(s)" + cath <## " Found 1 group(s)." cath <# "SimpleX-Directory> privacy (Privacy)" cath <## "Welcome message:" welcomeMsg <- getTermLine cath @@ -263,6 +265,92 @@ testGroupNameWithSpaces tmp = bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is listed in the directory again!" groupFound bob "Privacy & Security" +testSearchGroups :: HasCallStack => FilePath -> IO () +testSearchGroups tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i + connectUsers bob cath + fullAddMember "MyGroup" "" bob cath GRMember + joinGroup "MyGroup" cath bob + cath <## "#MyGroup: member SimpleX-Directory_1 is connected" + cath <## "contact and member are merged: SimpleX-Directory, #MyGroup SimpleX-Directory_1" + cath <## "use @SimpleX-Directory to send messages" + cath #> "@SimpleX-Directory MyGroup" + cath <# "SimpleX-Directory> > MyGroup" + cath <## " Found 7 group(s), sending top 3." + receivedGroup cath 0 3 + receivedGroup cath 1 2 + receivedGroup cath 2 2 + cath <# "SimpleX-Directory> Send /next or just . for 4 more result(s)." + cath #> "@SimpleX-Directory /next" + cath <# "SimpleX-Directory> > /next" + cath <## " Sending 3 more group(s)." + receivedGroup cath 3 2 + receivedGroup cath 4 2 + receivedGroup cath 5 2 + cath <# "SimpleX-Directory> Send /next or just . for 1 more result(s)." + -- search of another user does not affect the search of the first user + groupFound bob "Another" + cath #> "@SimpleX-Directory ." + cath <# "SimpleX-Directory> > ." + cath <## " Sending 1 more group(s)." + receivedGroup cath 6 2 + cath #> "@SimpleX-Directory /all" + cath <# "SimpleX-Directory> > /all" + cath <## " 8 group(s) listed, sending top 3." + receivedGroup cath 0 3 + receivedGroup cath 1 2 + receivedGroup cath 2 2 + cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)." + cath #> "@SimpleX-Directory /new" + cath <# "SimpleX-Directory> > /new" + cath <## " 8 group(s) listed, sending the most recent 3." + receivedGroup cath 7 2 + receivedGroup cath 6 2 + receivedGroup cath 5 2 + cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)." + cath #> "@SimpleX-Directory term3" + cath <# "SimpleX-Directory> > term3" + cath <## " Found 3 group(s)." + receivedGroup cath 4 2 + receivedGroup cath 5 2 + receivedGroup cath 6 2 + cath #> "@SimpleX-Directory term1" + cath <# "SimpleX-Directory> > term1" + cath <## " Found 6 group(s), sending top 3." + receivedGroup cath 1 2 + receivedGroup cath 2 2 + receivedGroup cath 3 2 + cath <# "SimpleX-Directory> Send /next or just . for 3 more result(s)." + cath #> "@SimpleX-Directory ." + cath <# "SimpleX-Directory> > ." + cath <## " Sending 3 more group(s)." + receivedGroup cath 4 2 + receivedGroup cath 5 2 + receivedGroup cath 6 2 + where + groups :: [String] + groups = + [ "MyGroup", + "MyGroup term1 1", + "MyGroup term1 2", + "MyGroup term1 term2", + "MyGroup term1 term2 term3", + "MyGroup term1 term2 term3 term4", + "MyGroup term1 term2 term3 term4 term5", + "Another" + ] + receivedGroup :: TestCC -> Int -> Int -> IO () + receivedGroup u ix count = do + u <#. ("SimpleX-Directory> " <> groups !! ix) + u <## "Welcome message:" + u <##. "Link to join the group " + u <## (show count <> " members") + testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO () testDelistedOwnerLeaves tmp = withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> @@ -930,6 +1018,7 @@ u `connectVia` dsLink = do u <## "Send a search string to find groups or /help to learn how to add groups to directory." u <## "" u <## "For example, send privacy to find groups about privacy." + u <## "Or send /all or /new to list groups." u <## "" u <## "Content and privacy policy: https://simplex.chat/docs/directory.html" @@ -967,7 +1056,7 @@ groupFoundN :: Int -> TestCC -> String -> IO () groupFoundN count u name = do u #> ("@SimpleX-Directory " <> name) u <# ("SimpleX-Directory> > " <> name) - u <## " Found 1 group(s)" + u <## " Found 1 group(s)." u <#. ("SimpleX-Directory> " <> name) u <## "Welcome message:" u <##. "Link to join the group "