From 825a63d3d27d8a805c0b5da544299cc5f0bd2a9a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 4 Apr 2026 19:38:44 +0100 Subject: [PATCH 01/10] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 0a0e922e34..6dc183179c 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -182,8 +182,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -553,8 +553,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -716,8 +716,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -803,8 +803,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.11-ATEGehbVMVHFTQkduzmQix.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a */, ); path = Libraries; sourceTree = ""; From de09eca6ab14b771c349fe95fc3aa0100bdbc39b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:32:23 +0100 Subject: [PATCH 02/10] ios: 6.5-beta.7 (build 324) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 6dc183179c..32d74395ba 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -2019,7 +2019,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2069,7 +2069,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2111,7 +2111,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2131,7 +2131,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2156,7 +2156,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2193,7 +2193,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2230,7 +2230,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2281,7 +2281,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2332,7 +2332,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2366,7 +2366,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 323; + CURRENT_PROJECT_VERSION = 324; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; From 4d267b6ca16f8dd57171ba2c39513499c046341d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 4 Apr 2026 21:28:09 +0100 Subject: [PATCH 03/10] 6.5-beta.7: android 339, desktop 134 --- apps/multiplatform/gradle.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 1e0dcc8f98..1926f35f0f 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5-beta.6 -android.version_code=338 +android.version_name=6.5-beta.7 +android.version_code=339 android.bundle=false -desktop.version_name=6.5-beta.6 -desktop.version_code=133 +desktop.version_name=6.5-beta.7 +desktop.version_code=134 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 From 281eb22f27e5e7a719689f89c398ade00bb4710f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 4 Apr 2026 23:05:40 +0100 Subject: [PATCH 04/10] cli: fix build --- src/Simplex/Chat/Terminal/Output.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 18026c30f3..03f644e641 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -209,7 +209,7 @@ chatEventNotification t@ChatTerminal {sendNotification} cc = \case when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") CEvtReceivedContactRequest u UserContactRequest {localDisplayName = n} _ -> when (userNtf u) $ sendNtf (viewName n <> ">", "wants to connect to you") - CEvtDeletedMemberUser _u g m _withMessages -> + CEvtDeletedMemberUser _u g m _withMessages _signed -> sendNtf ("#" <> viewGroupName g, viewMemberName m <> " removed you from the group") _ -> pure () where From 312e99b61dde3a74379f24213889906c1d105368 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 5 Apr 2026 08:47:04 +0100 Subject: [PATCH 05/10] fix tests --- tests/ChatTests/Direct.hs | 2 +- tests/OperatorTests.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 2df8f28cc0..740e757ed8 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -2376,7 +2376,7 @@ testDisableCIExpirationOnlyForOneUser ps = do alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 2500000 -- second user messages are deleted alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 38f2ac4c94..5e659dd82c 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -56,7 +56,7 @@ validateServersTest = describe "validate user servers" $ do validateUserServers [duplicateChatRelayName] [] `shouldBe` ([], []) it "should fail with duplicate chat relay address" $ do validateUserServers [invalidDuplicateChatRelayAddress] [] - `shouldBe` ( [ USEDuplicateChatRelayAddress "chat_relay_1" duplicateAddr, + `shouldBe` ( [ USEDuplicateChatRelayAddress "SimpleX Chat Relay 2" duplicateAddr, USEDuplicateChatRelayAddress "chat_relay_4" duplicateAddr ], [] @@ -180,4 +180,4 @@ invalidDuplicateChatRelayAddress = } duplicateAddr :: ShortLinkContact -duplicateAddr = either error id $ strDecode "https://smp111.simplex.im/r#Pz9qz7ZVljMofoRxiDDpL_w2DZSazK8IgafxqnWKv6Y" +duplicateAddr = either error id $ strDecode "https://smp6.simplex.im/r#_qlQfogHGDJ8MAF2wKmkglRBM-xHR142gDJstKiGRQQ" From 7194755f8d66f467c5bb68da4efaab0ea1449894 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:23:46 +0000 Subject: [PATCH 06/10] core: fix channel profile updates breaking link (#6762) --- src/Simplex/Chat/Library/Internal.hs | 6 +- src/Simplex/Chat/Library/Subscriber.hs | 5 +- tests/ChatTests/Groups.hs | 78 +++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index f91ee19bce..4ff677f379 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1195,6 +1195,9 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c where descrEvent_ :: Maybe (ChatMsgEvent 'Json) descrEvent_ + -- in channels sendHistory runs on the relay, which cannot author XMsgNew (GRRelay < GRObserver); + -- the welcome message reaches new members via the channel link data instead + | useRelays' gInfo = Nothing | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do let GroupInfo {groupProfile = GroupProfile {description}} = gInfo fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description @@ -1299,7 +1302,8 @@ setGroupLinkData nm user gInfo gLink = do (conn, groupRelays) <- withFastStore $ \db -> (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays - sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) + tagShortLink = if useRelays' gInfo then toShortChannelLink else toShortGroupLink + sLnk <- shortenShortLink' . tagShortLink =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index a8e9bcfdf5..2676224631 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -3138,7 +3138,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') groupMsgToView cInfo ci createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'' - void $ forkIO $ void $ setGroupLinkData' NRMBackground user g'' + -- in channels, link data is updated by the owner making the change in runUpdateGroupProfile; + -- other owners receiving the update do not refresh the same link + unless (useRelays' g'') $ + void $ forkIO $ void $ setGroupLinkData' NRMBackground user g'' Just _ -> updateGroupPrefs_ msgSigned g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 3a75a00a20..aedbb5e1e9 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -251,6 +251,8 @@ chatGroupTests = do it "should share same incognito profile with all relays" testChannels2RelaysIncognito describe "channel operations" $ do it "should update channel profile (signed)" testChannelUpdateProfileSigned + it "should preserve working link after profile update" testChannelLinkAfterProfileUpdate + it "should preserve working link after welcome message update" testChannelLinkAfterWelcomeUpdate it "should update channel preferences (signed)" testChannelUpdatePrefsSigned it "should change member role (signed)" testChannelChangeRoleSigned it "should block member for all (signed)" testChannelBlockMemberSigned @@ -8537,7 +8539,7 @@ memberJoinChannel gName relays owners shortLink fullLink member = do ] ] <> [ do - relay <## (mFullName <> ": accepting request to join group #team...") + relay <## (mFullName <> ": accepting request to join group #" <> gName <> "...") relay <## ("#" <> gName <> ": " <> mName <> " joined the group") | relay <- relays ] @@ -8569,7 +8571,7 @@ memberJoinChannelIncognito gName relays owners shortLink fullLink member = do ] ] <> [ do - relay <## (memIncognito <> ": accepting request to join group #team...") + relay <## (memIncognito <> ": accepting request to join group #" <> gName <> "...") relay <## ("#" <> gName <> ": " <> memIncognito <> " joined the group") | relay <- relays ] @@ -8770,6 +8772,78 @@ testChannelUpdateProfileSigned ps = ] alice #$> ("/_get chat #1 count=1", chat, [(1, "group profile updated (signed)")]) +testChannelLinkAfterProfileUpdate :: HasCallStack => TestParams -> IO () +testChannelLinkAfterProfileUpdate ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + -- owner updates channel profile + alice ##> "/gp team my_team My team description" + alice <## "changed to #my_team (My team description)" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "changed to #my_team (My team description)", + do + cath <## "alice updated group #team: (signed)" + cath <## "changed to #my_team (My team description)" + ] + alice #$> ("/_get chat #1 count=1", chat, [(1, "group profile updated (signed)")]) + + -- late subscriber joins via the same channel link after profile update + threadDelay 100000 + alice ##> "/show link #my_team" + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + shortLink' `shouldBe` shortLink + fullLink' `shouldBe` fullLink + memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan + + alice #> "#my_team hi" + bob <# "#my_team> hi" + [cath, dan] *<# "#my_team> hi [>>]" + +testChannelLinkAfterWelcomeUpdate :: HasCallStack => TestParams -> IO () +testChannelLinkAfterWelcomeUpdate ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + -- owner updates channel welcome message + alice ##> "/set welcome #team welcome to team" + alice <## "welcome message changed to:" + alice <## "welcome to team" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "welcome message changed to:" + bob <## "welcome to team", + do + cath <## "alice updated group #team: (signed)" + cath <## "welcome message changed to:" + cath <## "welcome to team" + ] + alice #$> ("/_get chat #1 count=1", chat, [(1, "group profile updated (signed)")]) + + -- re-fetch updated link, late subscriber joins + threadDelay 100000 + alice ##> "/show link #team" + (shortLink', fullLink') <- getGroupLinks alice "team" GRMember False + shortLink' `shouldBe` shortLink + fullLink' `shouldBe` fullLink + memberJoinChannel "team" [bob] [alice] shortLink' fullLink' dan + dan #$> ("/_get chat #1 count=100", chat, groupFeaturesNoE2E <> [(0, "welcome to team"), (0, e2eeInfoNoPQStr), (0, "connected")]) + + alice #> "#team hi" + bob <# "#team> hi" + [cath, dan] *<# "#team> hi [>>]" + testChannelUpdatePrefsSigned :: HasCallStack => TestParams -> IO () testChannelUpdatePrefsSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> From e3003fd1f517ac1a08ca22b30bc7422fdca6256f Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:37:21 +0000 Subject: [PATCH 07/10] simplex-chat-nodejs: fix userChatRelay type error in apiCreateActiveUser (#6764) The @simplex-chat/types package (auto-generated from Haskell types) added a required `userChatRelay: boolean` field to the NewUser interface, but apiCreateActiveUser was never updated to pass it, causing a TypeScript compilation error. Set userChatRelay to false, which preserves the pre-existing behavior (no chat relay provisioned for the new user profile). --- packages/simplex-chat-nodejs/src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index c3e85b3915..f5d2a5168e 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -813,7 +813,7 @@ export class ChatApi { * Network usage: no. */ async apiCreateActiveUser(profile?: T.Profile): Promise { - const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}})) + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false}})) if (r.type === "activeUser") return r.user throw new ChatCommandError("unexpected response", r) } From 6583aafbdd4423b8085ba7302184d388c6ba8a83 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:25:57 +0000 Subject: [PATCH 08/10] core: bump @simplex-chat/types (#6765) --- packages/simplex-chat-client/types/typescript/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index a135b286c2..8d05eb460c 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.3.0", + "version": "0.4.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", From 24435f5b74318b98cd13f048a86b9ab26e81222a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:44:23 +0000 Subject: [PATCH 09/10] ui: fix edit channel profile texts (#6766) --- .../Views/Chat/Group/GroupProfileView.swift | 26 +++++++++++------- .../chat/simplex/common/model/SimpleXAPI.kt | 7 ++--- .../views/chat/group/GroupPreferences.kt | 2 +- .../views/chat/group/GroupProfileView.kt | 27 ++++++++++++------- .../views/chat/group/MemberAdmission.kt | 2 +- .../views/chat/group/WelcomeMessageView.kt | 2 +- .../commonMain/resources/MR/base/strings.xml | 5 ++++ 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 69587c0152..24a52b4b60 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -42,7 +42,7 @@ struct GroupProfileView: View { Section { HStack { - TextField("Group display name", text: $groupProfile.displayName) + TextField(groupInfo.useRelays ? "Channel display name" : "Group display name", text: $groupProfile.displayName) .focused($focusDisplayName) if !validNewProfileName { Button { @@ -54,7 +54,7 @@ struct GroupProfileView: View { } let fullName = groupInfo.groupProfile.fullName if fullName != "" && fullName != groupProfile.displayName { - TextField("Group full name (optional)", text: $groupProfile.fullName) + TextField(groupInfo.useRelays ? "Channel full name (optional)" : "Group full name (optional)", text: $groupProfile.fullName) } HStack { TextField("Short description", text: $shortDescr) @@ -67,7 +67,7 @@ struct GroupProfileView: View { } } } footer: { - Text("Group profile is stored on members' devices, not on the servers.") + Text(groupInfo.useRelays ? "Channel profile is stored on subscribers' devices and on the chat relays." : "Group profile is stored on members' devices, not on the servers.") } Section { @@ -80,11 +80,11 @@ struct GroupProfileView: View { currentProfileHash == groupProfile.hashValue && (groupInfo.groupProfile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) ) - Button("Save group profile", action: saveProfile) + Button(groupInfo.useRelays ? "Save channel profile" : "Save group profile", action: saveProfile) .disabled(!canUpdateProfile) } } - .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { + .confirmationDialog(groupInfo.useRelays ? "Channel image" : "Group image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true } @@ -130,9 +130,15 @@ struct GroupProfileView: View { .onDisappear { if canUpdateProfile { showAlert( - title: NSLocalizedString("Save group profile?", comment: "alert title"), - message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), - buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"), + title: groupInfo.useRelays + ? NSLocalizedString("Save channel profile?", comment: "alert title") + : NSLocalizedString("Save group profile?", comment: "alert title"), + message: groupInfo.useRelays + ? NSLocalizedString("Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers.", comment: "alert message") + : NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), + buttonTitle: groupInfo.useRelays + ? NSLocalizedString("Save (and notify subscribers)", comment: "alert button") + : NSLocalizedString("Save (and notify members)", comment: "alert button"), buttonAction: saveProfile, cancelButton: true ) @@ -142,14 +148,14 @@ struct GroupProfileView: View { switch a { case let .saveError(err): return Alert( - title: Text("Error saving group profile"), + title: Text(groupInfo.useRelays ? "Error saving channel profile" : "Error saving group profile"), message: Text(err) ) case let .invalidName(name): return createInvalidNameAlert(name, $groupProfile.displayName) } } - .navigationBarTitle("Group profile") + .navigationBarTitle(groupInfo.useRelays ? "Channel profile" : "Group profile") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large) } 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 cb42ee2aba..661b7e767f 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 @@ -2217,18 +2217,19 @@ object ChatController { return emptyList() } - suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? { + suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile, isChannel: Boolean): GroupInfo? { val r = sendCmd(rh, CC.ApiUpdateGroupProfile(groupId, groupProfile)) + val errorTitle = if (isChannel) MR.strings.error_saving_channel_profile else MR.strings.error_saving_group_profile return when { r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup r is API.Error -> { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_group_profile), "$r.err") + AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "$r.err") null } else -> { Log.e(TAG, "apiUpdateGroup bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.error_saving_group_profile), + generalGetString(errorTitle), "${r.responseType}: ${r.details}" ) null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index b8db5969a1..ddf0456822 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -43,7 +43,7 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> fun savePrefs(afterSave: () -> Unit = {}) { withBGApi { val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) - val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays) if (g != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, g) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index f15f70673a..d144065399 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -32,10 +32,11 @@ import java.net.URI fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { GroupProfileLayout( close = close, + groupInfo = groupInfo, groupProfile = groupInfo.groupProfile, saveProfile = { p -> withBGApi { - val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) + val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p, groupInfo.useRelays) if (gInfo != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, gInfo) @@ -50,9 +51,11 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl @Composable fun GroupProfileLayout( close: () -> Unit, + groupInfo: GroupInfo, groupProfile: GroupProfile, saveProfile: (GroupProfile) -> Unit, ) { + val isChannel = groupInfo.useRelays val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) } val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) } @@ -71,7 +74,7 @@ fun GroupProfileLayout( if (dataUnchanged || !canUpdateProfile(displayName.value, shortDescr.value, groupProfile)) { close() } else { - showUnsavedChangesAlert({ + showUnsavedChangesAlert(isChannel, { saveProfile( groupProfile.copy( displayName = displayName.value.trim(), @@ -103,7 +106,11 @@ fun GroupProfileLayout( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) ) { - ReadableText(MR.strings.group_profile_is_stored_on_members_devices, TextAlign.Center) + ReadableText( + if (isChannel) MR.strings.channel_profile_is_stored_on_subscribers_devices + else MR.strings.group_profile_is_stored_on_members_devices, + TextAlign.Center + ) Box( Modifier .fillMaxWidth() @@ -122,7 +129,7 @@ fun GroupProfileLayout( } Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( - stringResource(MR.strings.group_display_name_field), + stringResource(if (isChannel) MR.strings.channel_display_name_field else MR.strings.group_display_name_field), fontSize = 16.sp ) if (!isValidNewProfileName(displayName.value, groupProfile)) { @@ -136,7 +143,7 @@ fun GroupProfileLayout( if (groupProfile.fullName.trim().isNotEmpty() && groupProfile.fullName.trim() != groupProfile.displayName.trim()) { Spacer(Modifier.height(DEFAULT_PADDING)) Text( - stringResource(MR.strings.group_full_name_field), + stringResource(if (isChannel) MR.strings.channel_full_name_field else MR.strings.group_full_name_field), fontSize = 16.sp, modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) ) @@ -164,9 +171,10 @@ fun GroupProfileLayout( Spacer(Modifier.height(DEFAULT_PADDING)) val enabled = !dataUnchanged && canUpdateProfile(displayName.value, shortDescr.value, groupProfile) + val saveProfileLabel = if (isChannel) MR.strings.save_channel_profile else MR.strings.save_group_profile if (enabled) { Text( - stringResource(MR.strings.save_group_profile), + stringResource(saveProfileLabel), modifier = Modifier.clickable { saveProfile( groupProfile.copy( @@ -181,7 +189,7 @@ fun GroupProfileLayout( ) } else { Text( - stringResource(MR.strings.save_group_profile), + stringResource(saveProfileLabel), color = MaterialTheme.colors.secondary ) } @@ -204,10 +212,10 @@ private fun canUpdateProfile(displayName: String, shortDescr: String, groupProfi private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean = displayName == groupProfile.displayName || isValidDisplayName(displayName.trim()) -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { +private fun showUnsavedChangesAlert(isChannel: Boolean, save: () -> Unit, revert: () -> Unit) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), - confirmText = generalGetString(MR.strings.save_and_notify_group_members), + confirmText = generalGetString(if (isChannel) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members), dismissText = generalGetString(MR.strings.exit_without_saving), onConfirm = save, onDismiss = revert, @@ -224,6 +232,7 @@ fun PreviewGroupProfileLayout() { SimpleXTheme { GroupProfileLayout( close = {}, + groupInfo = GroupInfo.sampleData, groupProfile = GroupProfile.sampleData, saveProfile = { _ -> } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt index 48171bfeb7..7c9db58316 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt @@ -34,7 +34,7 @@ fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> fun saveAdmission(afterSave: () -> Unit = {}) { withBGApi { val gp = gInfo.groupProfile.copy(memberAdmission = admission) - val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp, gInfo.useRelays) if (g != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, g) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 1e99c7f527..927e9940b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -45,7 +45,7 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () welcome = null } val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome) - val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) + val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated, gInfo.useRelays) if (res != null) { gInfo = res withContext(Dispatchers.Main) { 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 ac9f9b2fc8..f2872f3f0a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1171,6 +1171,7 @@ Save and notify contact Save and notify contacts Save and notify group members + Save and notify channel subscribers Exit without saving @@ -1999,6 +2000,7 @@ Fully decentralized – visible only to members. Enter group name: Group full name: + Channel full name: Short description: Description too large Your chat profile will be sent to group members @@ -2007,8 +2009,11 @@ Group profile is stored on members\' devices, not on the servers. + Channel profile is stored on subscribers\' devices and on the chat relays. Save group profile + Save channel profile Error saving group profile + Error saving channel profile Preset servers From e6dde90c40da36c0509220bfa7302969dbf2916b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 10 Apr 2026 09:31:26 +0100 Subject: [PATCH 10/10] core: 6.5.0.14 --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index c4317c85c7..621a784ed8 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.0.12 +version: 6.5.0.14 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat