From 12fbf61f326e15ee643c72b3202bbcd6a758a07f Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 26 May 2026 09:03:41 +0000 Subject: [PATCH] core, ui: require update for public groups (#7009) --- apps/ios/Shared/Model/AppAPITypes.swift | 1 + .../Shared/Views/NewChat/NewChatView.swift | 27 ++++++++++++++++ .../chat/simplex/common/model/SimpleXAPI.kt | 1 + .../common/views/newchat/ConnectPlan.kt | 27 ++++++++++++++++ .../commonMain/resources/MR/base/strings.xml | 2 ++ .../src/Directory/Service.hs | 1 + bots/api/TYPES.md | 4 +++ .../types/typescript/src/types.ts | 7 ++++ .../src/simplex_chat/types/_types.py | 7 +++- src/Simplex/Chat/Controller.hs | 2 ++ src/Simplex/Chat/Library/Commands.hs | 32 +++++++++++-------- src/Simplex/Chat/View.hs | 1 + 12 files changed, 97 insertions(+), 15 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index b459f36c9d..a5a56174b1 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1404,6 +1404,7 @@ enum GroupLinkPlan: Decodable, Hashable { case connectingProhibit(groupInfo_: GroupInfo?) case known(groupInfo: GroupInfo) case noRelays(groupSLinkData_: GroupShortLinkData?) + case updateRequired(groupSLinkData_: GroupShortLinkData?) } struct ChatTagData: Encodable { diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 9bcc326a66..f73a2f1503 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1559,6 +1559,33 @@ func planAndConnect( cleanup?() } } + case let .updateRequired(groupSLinkData_): + logger.debug("planAndConnect, .groupLink, .updateRequired") + await MainActor.run { + if let groupSLinkData = groupSLinkData_ { + showOpenChatAlert( + profileName: groupSLinkData.groupProfile.displayName, + profileFullName: groupSLinkData.groupProfile.fullName, + profileImage: + ProfileImage( + imageStr: groupSLinkData.groupProfile.image, + iconName: "person.2.circle.fill", + size: alertProfileImageSize + ), + theme: theme, + subtitle: NSLocalizedString("This group requires a newer version of the app. Please update the app to join.", comment: "alert subtitle"), + cancelTitle: NSLocalizedString("OK", comment: "alert button"), + confirmTitle: nil, + onCancel: { cleanup?() } + ) + } else { + showAlert( + NSLocalizedString("App update required", comment: "alert title"), + message: NSLocalizedString("This group requires a newer version of the app. Please update the app to join.", comment: "alert message") + ) + cleanup?() + } + } } case let .error(chatError): logger.debug("planAndConnect, .error \(chatErrorString(chatError))") 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 a31dc145a3..8f7cce21c4 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 @@ -6993,6 +6993,7 @@ sealed class GroupLinkPlan { @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("noRelays") class NoRelays(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() + @Serializable @SerialName("updateRequired") class UpdateRequired(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() } abstract class TerminalItem { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index cafad97574..87cf01403c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -316,6 +316,33 @@ private suspend fun planAndConnectTask( cleanup() } } + is GroupLinkPlan.UpdateRequired -> { + Log.d(TAG, "planAndConnect, .GroupLink, .UpdateRequired") + val groupSLinkData = connectionPlan.groupLinkPlan.groupSLinkData_ + if (groupSLinkData != null) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = groupSLinkData.groupProfile.displayName, + profileFullName = groupSLinkData.groupProfile.fullName, + profileImage = { + ProfileImage( + size = alertProfileImageSize, + image = groupSLinkData.groupProfile.image, + icon = MR.images.ic_supervised_user_circle_filled + ) + }, + subtitle = generalGetString(MR.strings.group_link_requires_newer_version), + confirmText = null, + dismissText = generalGetString(MR.strings.ok), + onDismiss = { cleanup() } + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.app_update_required), + generalGetString(MR.strings.group_link_requires_newer_version) + ) + cleanup() + } + } } is ConnectionPlan.Error -> { Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") 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 375edecd44..5a0bc77ccf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -196,6 +196,8 @@ This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Channel temporarily unavailable Channel has no active relays. Please try to join later. + App update required + This group requires a newer version of the app. Please update the app to join. Connection error (AUTH) Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection. Connection blocked diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 6e414ef011..577cc99752 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -970,6 +970,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName GLPConnectingProhibit _ -> sendMessage cc ct $ "Already connecting to this " <> gt <> "." GLPConnectingConfirmReconnect -> sendMessage cc ct $ "Already connecting to this " <> gt <> "." GLPNoRelays _ -> sendMessage cc ct $ T.toTitle gt <> " has no active relays. Please try again later." + GLPUpdateRequired _ -> sendMessage cc ct $ T.toTitle gt <> " requires a newer version." GLPOwnLink _ -> sendMessage cc ct "Unexpected error. Please report it to directory admins." _ -> sendMessage cc ct "Unexpected error. Please report it to directory admins." diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index b4edb9bd22..3db6dcbcfc 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2331,6 +2331,10 @@ NoRelays: - type: "noRelays" - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? +UpdateRequired: +- type: "updateRequired" +- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? + --- diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 7e618e05c8..44949611b2 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2602,6 +2602,7 @@ export type GroupLinkPlan = | GroupLinkPlan.ConnectingProhibit | GroupLinkPlan.Known | GroupLinkPlan.NoRelays + | GroupLinkPlan.UpdateRequired export namespace GroupLinkPlan { export type Tag = @@ -2611,6 +2612,7 @@ export namespace GroupLinkPlan { | "connectingProhibit" | "known" | "noRelays" + | "updateRequired" interface Interface { type: Tag @@ -2649,6 +2651,11 @@ export namespace GroupLinkPlan { type: "noRelays" groupSLinkData_?: GroupShortLinkData } + + export interface UpdateRequired extends Interface { + type: "updateRequired" + groupSLinkData_?: GroupShortLinkData + } } export interface GroupMember { diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index b2fc00a44c..409a187245 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -1854,6 +1854,10 @@ class GroupLinkPlan_noRelays(TypedDict): type: Literal["noRelays"] groupSLinkData_: NotRequired["GroupShortLinkData"] +class GroupLinkPlan_updateRequired(TypedDict): + type: Literal["updateRequired"] + groupSLinkData_: NotRequired["GroupShortLinkData"] + GroupLinkPlan = ( GroupLinkPlan_ok | GroupLinkPlan_ownLink @@ -1861,9 +1865,10 @@ GroupLinkPlan = ( | GroupLinkPlan_connectingProhibit | GroupLinkPlan_known | GroupLinkPlan_noRelays + | GroupLinkPlan_updateRequired ) -GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays"] +GroupLinkPlan_Tag = Literal["ok", "ownLink", "connectingConfirmReconnect", "connectingProhibit", "known", "noRelays", "updateRequired"] class GroupMember(TypedDict): groupMemberId: int # int64 diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index fa2d0af009..fe5b67f041 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1051,6 +1051,7 @@ data GroupLinkPlan | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} | GLPKnown {groupInfo :: GroupInfo, groupUpdated :: BoolDef, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner} | GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData} + | GLPUpdateRequired {groupSLinkData_ :: Maybe GroupShortLinkData} deriving (Show) data GroupLinkOwner = GroupLinkOwner @@ -1096,6 +1097,7 @@ connectionPlanProceed = \case GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True GLPNoRelays _ -> False + GLPUpdateRequired _ -> False _ -> False CPError _ -> True diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index bb31ee26a5..8d9d882366 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4120,21 +4120,25 @@ processChatCommand vr nm = \case Nothing -> do (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData - if not direct && null relays - then pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) - else do - let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd - linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} - let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> - fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup - case (B64UrlByteString <$> linkEntityId, profilePGId) of - (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () - (Nothing, Nothing) -> pure () - _ -> throwChatError CEInvalidConnReq - let ov = verifyLinkOwner rootKey owners l' sig_ - plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov - pure (con cReq, plan) + if + | not direct && unsupportedGroupType groupSLinkData_ -> pure (con (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_)) + | not direct && null relays -> pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) + | otherwise -> do + let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd + linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} + let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> + fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup + case (B64UrlByteString <$> linkEntityId, profilePGId) of + (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () + (Nothing, Nothing) -> pure () + _ -> throwChatError CEInvalidConnReq + let ov = verifyLinkOwner rootKey owners l' sig_ + plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov + pure (con cReq, plan) where + unsupportedGroupType = \case + Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel + _ -> False knownLinkPlans = withFastStore $ \db -> liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 477850d4b0..838d15245a 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -2138,6 +2138,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case ] knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"] + GLPUpdateRequired _ -> [grpLink "this group requires a newer version of the app, please upgrade"] where connecting g = [grpOrBizLink g <> ": connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] grpLink = ("group link: " <>)