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: " <>)