From 01906ae1b2528e2049623baebdc7ca593b94e876 Mon Sep 17 00:00:00 2001
From: sh <37271604+shumvgolove@users.noreply.github.com>
Date: Wed, 29 Apr 2026 09:52:38 +0000
Subject: [PATCH 1/9] android, desktop: support link preview generation with
socks (#6907)
* android, desktop: fix link previews bypassing SOCKS proxy
getLinkPreview used Jsoup.connect() and URL.openStream() directly,
bypassing the configured SOCKS proxy. Both the HTML fetch and image
download now route through the proxy when one is configured.
If the proxy address is misconfigured (unparseable port), the preview
is cancelled and the user is alerted rather than falling back to a
direct connection.
When enabling SOCKS proxy with link previews active, or enabling link
previews while SOCKS is active, the user is warned that DNS lookups
may still occur locally and given the option to disable previews.
Updates the SOCKS proxy limitations notice to clarify that calls
cannot be proxied, and highlights it in warning colour.
Note: DNS lookups may still occur locally before the SOCKS connection
is established. Full SOCKS5h hostname forwarding is a separate follow-up.
* android, desktop: fix SOCKS proxy parser, auth credentials, and repeated alert in link previews
- Build proxy from typed NetworkProxy fields instead of parsing socksProxy string, fixing breakage on IPv6 hosts and USERNAME auth configurations
- Register java.net.Authenticator for SOCKS5 credential negotiation (Java 21 SocksSocketImpl uses RequestorType.SERVER for this callback)
- Remove per-keystroke invalid-proxy alert, which fired on every URL change for valid but unparseable proxy strings
* ui: drop link preview SOCKS warnings and strings
* ui: soften link preview alert when SOCKS is on
Show the link previews opt-in alert in both SOCKS-on and SOCKS-off
cases (previously skipped entirely when SOCKS was on). When SOCKS
is on, use a softer description that mentions the proxy and the
remaining local DNS lookup risk, and render the Disable button in
primary colour instead of red.
Also drop the link-previews caveat from the SOCKS limitations
footer since previews now go through the proxy.
* fix: harden socks proxy auth in link previews
- Gate the SOCKS5 Authenticator on host:port match so destination 401
challenges no longer leak proxy credentials via the JDK auto-retry.
- Snapshot Authenticator.getDefault() and restore in finally to stop
leaking process-global state.
- Mutex around getLinkPreview to serialize concurrent calls.
- Generate a random UUID per call in ISOLATE mode for stream isolation.
- Skip auth when USERNAME mode has empty username or password.
* ui: shift red emphasis from Disable to Enable in link preview alert
Disable is now always primary; Enable is red by default and primary
when SOCKS is on. The dangerous action is enabling without proxy
protection, not disabling.
* ui: append SOCKS notice to link preview alert
---------
Co-authored-by: iversonianGremling <24989959+iversonianGremling@users.noreply.github.com>
---
.../simplex/common/views/chat/ComposeView.kt | 19 ++-
.../common/views/helpers/LinkPreviews.kt | 145 ++++++++++++------
.../commonMain/resources/MR/base/strings.xml | 3 +-
3 files changed, 115 insertions(+), 52 deletions(-)
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 480320e33e..51128646e7 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
@@ -419,9 +419,9 @@ fun ComposeView(
withLongRunningApi(slow = 60_000) {
if (wait != null) delay(wait)
if (pendingLinkUrl.value != url) return@withLongRunningApi
- if (chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.get()
- && !chatModel.controller.appPrefs.networkUseSocksProxy.get()) {
- showLinkPreviewsConfirmAlert { enable ->
+ if (chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.get()) {
+ val socksEnabled = chatModel.controller.appPrefs.networkUseSocksProxy.get()
+ showLinkPreviewsConfirmAlert(socksEnabled) { enable ->
if (enable != null) {
chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false)
chatModel.controller.appPrefs.privacyLinkPreviews.set(enable)
@@ -1703,10 +1703,15 @@ fun ComposeView(
}
}
-private fun showLinkPreviewsConfirmAlert(onChoice: (Boolean?) -> Unit) {
+private fun showLinkPreviewsConfirmAlert(socksEnabled: Boolean, onChoice: (Boolean?) -> Unit) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.link_previews_alert_title),
- text = AnnotatedString(generalGetString(MR.strings.link_previews_alert_desc)),
+ text = AnnotatedString(
+ if (socksEnabled)
+ generalGetString(MR.strings.link_previews_alert_desc) + "\n\n" + generalGetString(MR.strings.link_previews_alert_desc_socks)
+ else
+ generalGetString(MR.strings.link_previews_alert_desc)
+ ),
onDismissRequest = { onChoice(null) },
buttons = {
Column {
@@ -1714,13 +1719,13 @@ private fun showLinkPreviewsConfirmAlert(onChoice: (Boolean?) -> Unit) {
AlertManager.shared.hideAlert()
onChoice(false)
}) {
- Text(stringResource(MR.strings.link_previews_alert_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
+ Text(stringResource(MR.strings.link_previews_alert_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
onChoice(true)
}) {
- Text(stringResource(MR.strings.link_previews_alert_enable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
+ Text(stringResource(MR.strings.link_previews_alert_enable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = if (socksEnabled) MaterialTheme.colors.primary else Color.Red)
}
// SectionItemView({
// AlertManager.shared.hideAlert()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt
index 9c529e547a..d4b2915297 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt
@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.LinkPreview
+import chat.simplex.common.model.NetworkProxyAuth
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.chatViewScrollState
@@ -24,67 +25,123 @@ import chat.simplex.common.views.chat.item.CHAT_IMAGE_LAYOUT_ID
import chat.simplex.common.views.chat.item.imageViewFullWidth
import chat.simplex.res.MR
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
+import java.net.Authenticator
+import java.net.InetSocketAddress
+import java.net.PasswordAuthentication
+import java.net.Proxy
import java.net.URL
+import java.util.UUID
private const val OG_SELECT_QUERY = "meta[property^=og:]"
private const val ICON_SELECT_QUERY = "link[rel^=icon],link[rel^=apple-touch-icon],link[rel^=shortcut icon]"
private val IMAGE_SUFFIXES = listOf(".jpg", ".png", ".ico", ".webp", ".gif")
+// Authenticator.setDefault is process-global. The mutex serializes preview fetches
+// so concurrent calls cannot clobber each other's authenticator, and so the
+// snapshot/restore in getLinkPreview is race-free.
+private val previewMutex = Mutex()
+
suspend fun getLinkPreview(url: String): LinkPreview? {
return withContext(Dispatchers.IO) {
- try {
- val title: String?
- val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withContext null
- var imageUri = when {
- IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> {
- title = u.path.substringAfterLast("/")
- url
- }
- else -> {
- val connection = Jsoup.connect(url)
- .ignoreContentType(true)
- .timeout(10000)
- .followRedirects(true)
-
- val response = if (url.lowercase().startsWith("https://x.com/")) {
- // Apple sends request with special user-agent which handled differently by X.com.
- // Different response that includes video poster from post
- connection
- .userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0")
- .execute()
- } else {
- connection
- .execute()
- }
- val doc = response.parse()
- val ogTags = doc.select(OG_SELECT_QUERY)
- title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title()
- ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content")
- ?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href")
- }
- }
- if (imageUri != null) {
- imageUri = normalizeImageUri(u, imageUri)
+ previewMutex.withLock {
+ val previousAuthenticator = Authenticator.getDefault()
+ try {
try {
- val stream = URL(imageUri).openStream()
- val image = resizeImageToStrSize(stream.use(::loadImageBitmap), maxDataSize = 14000)
- // TODO add once supported in iOS
- // val description = ogTags.firstOrNull {
- // it.attr("property") == "og:description"
- // }?.attr("content") ?: ""
- if (title != null) {
- return@withContext LinkPreview(url, title, description = "", image)
+ val title: String?
+ val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withLock null
+ val useSocksProxy = appPrefs.networkUseSocksProxy.get()
+ val proxy: Proxy?
+ if (useSocksProxy) {
+ val networkProxy = appPrefs.networkProxy.get()
+ proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(networkProxy.host, networkProxy.port))
+ val (authUser, authPass) = when (networkProxy.auth) {
+ NetworkProxyAuth.USERNAME ->
+ if (networkProxy.username.isNotEmpty() && networkProxy.password.isNotEmpty())
+ networkProxy.username to networkProxy.password
+ else
+ null to null
+ // Per-call random credentials drive Tor-style stream isolation: each
+ // preview gets its own circuit, and previews don't share a circuit
+ // with other unauthenticated traffic on the proxy.
+ NetworkProxyAuth.ISOLATE ->
+ UUID.randomUUID().toString() to UUID.randomUUID().toString()
+ }
+ if (authUser != null && authPass != null) {
+ Authenticator.setDefault(object : Authenticator() {
+ override fun getPasswordAuthentication(): PasswordAuthentication? =
+ // Only respond when the SOCKS proxy itself challenges. A destination
+ // server returning 401 also triggers RequestorType.SERVER; without
+ // this gate, the JDK's auto-retry would post our SOCKS credentials
+ // in an Authorization header to the destination.
+ if (requestingHost == networkProxy.host && requestingPort == networkProxy.port)
+ PasswordAuthentication(authUser, authPass.toCharArray())
+ else null
+ })
+ } else {
+ Authenticator.setDefault(null)
+ }
+ } else {
+ proxy = null
+ Authenticator.setDefault(null)
+ }
+ var imageUri = when {
+ IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> {
+ title = u.path.substringAfterLast("/")
+ url
+ }
+ else -> {
+ val connection = Jsoup.connect(url)
+ .ignoreContentType(true)
+ .timeout(10000)
+ .followRedirects(true)
+ .proxy(proxy)
+
+ val response = if (url.lowercase().startsWith("https://x.com/")) {
+ // Apple sends request with special user-agent which handled differently by X.com.
+ // Different response that includes video poster from post
+ connection
+ .userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0")
+ .execute()
+ } else {
+ connection
+ .execute()
+ }
+ val doc = response.parse()
+ val ogTags = doc.select(OG_SELECT_QUERY)
+ title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title()
+ ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content")
+ ?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href")
+ }
+ }
+ if (imageUri != null) {
+ imageUri = normalizeImageUri(u, imageUri)
+ try {
+ val conn = URL(imageUri).openConnection(proxy ?: Proxy.NO_PROXY)
+ val stream = conn.getInputStream()
+ val image = resizeImageToStrSize(stream.use(::loadImageBitmap), maxDataSize = 14000)
+ // TODO add once supported in iOS
+ // val description = ogTags.firstOrNull {
+ // it.attr("property") == "og:description"
+ // }?.attr("content") ?: ""
+ if (title != null) {
+ return@withLock LinkPreview(url, title, description = "", image)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
}
} catch (e: Exception) {
e.printStackTrace()
}
+ return@withLock null
+ } finally {
+ Authenticator.setDefault(previousAuthenticator)
}
- } catch (e: Exception) {
- e.printStackTrace()
}
- return@withContext null
}
}
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 b6494accde..8d654b5af8 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -1067,7 +1067,7 @@
for each contact and group member.\nPlease note: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]>
Update transport isolation mode?
Use .onion hosts to No if SOCKS proxy does not support them.]]>
- Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]>
+ Please note: message and file relays are connected via SOCKS proxy. Calls use direct connection.]]>
Private routing
Always
Unknown servers
@@ -3061,6 +3061,7 @@
Enable link previews?
Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later.
+ Link preview will be requested via SOCKS proxy. DNS lookup may still happen locally via your DNS resolver.
Enable
Disable
\ No newline at end of file
From 1ba45acdfd37d29960f9ff9415c84b04c5f8977f Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin
Date: Wed, 29 Apr 2026 11:28:24 +0100
Subject: [PATCH 2/9] 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 f2ec8d0b2d..a85454f4cb 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.17-3blIIOCVZmm6YZ1Ew74Hcu-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu-ghc9.6.3.a */; };
- 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu.a */; };
+ 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi-ghc9.6.3.a */; };
+ 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi.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 */; };
@@ -559,8 +559,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.17-3blIIOCVZmm6YZ1Ew74Hcu-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu-ghc9.6.3.a"; sourceTree = ""; };
- 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu.a"; sourceTree = ""; };
+ 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi-ghc9.6.3.a"; sourceTree = ""; };
+ 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi.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 = ""; };
@@ -729,8 +729,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
- 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu-ghc9.6.3.a in Frameworks */,
- 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu.a in Frameworks */,
+ 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi-ghc9.6.3.a in Frameworks */,
+ 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -816,8 +816,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
- 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu-ghc9.6.3.a */,
- 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.17-3blIIOCVZmm6YZ1Ew74Hcu.a */,
+ 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi-ghc9.6.3.a */,
+ 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.18-BcXIpdD8LSsInPsEEY8eHi.a */,
);
path = Libraries;
sourceTree = "";
From c2ad26fa65004b7e495a1d780df2708a134d00c8 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Wed, 29 Apr 2026 11:37:56 +0100
Subject: [PATCH 3/9] 6.5-beta.12: ios 329
---
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 a85454f4cb..cf53134450 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -2069,7 +2069,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2119,7 +2119,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2161,7 +2161,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2181,7 +2181,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2206,7 +2206,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2243,7 +2243,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2280,7 +2280,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2331,7 +2331,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2385,7 +2385,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2419,7 +2419,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 328;
+ CURRENT_PROJECT_VERSION = 329;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
From aa7f1049b5216a6bea7c6756daf7787dcaac5dc8 Mon Sep 17 00:00:00 2001
From: Evgeny
Date: Wed, 29 Apr 2026 13:19:23 +0100
Subject: [PATCH 4/9] android: fix link preview fetch via SOCKS (#6919)
* android: fix link preview fetch via SOCKS
* fix: add timeouts to link preview image fetch
Match the 10s timeout already on the Jsoup HTML fetch. Without these,
a slow or dead SOCKS proxy hangs the image fetch indefinitely, holding
the previewMutex and blocking every subsequent preview.
* timeout
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: shum
---
.../kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt | 5 +++--
.../common/src/commonMain/resources/MR/base/strings.xml | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt
index d4b2915297..d2a98ae101 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt
@@ -48,7 +48,6 @@ private val previewMutex = Mutex()
suspend fun getLinkPreview(url: String): LinkPreview? {
return withContext(Dispatchers.IO) {
previewMutex.withLock {
- val previousAuthenticator = Authenticator.getDefault()
try {
try {
val title: String?
@@ -121,6 +120,8 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
imageUri = normalizeImageUri(u, imageUri)
try {
val conn = URL(imageUri).openConnection(proxy ?: Proxy.NO_PROXY)
+ conn.connectTimeout = 20_000
+ conn.readTimeout = 20_000
val stream = conn.getInputStream()
val image = resizeImageToStrSize(stream.use(::loadImageBitmap), maxDataSize = 14000)
// TODO add once supported in iOS
@@ -139,7 +140,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
}
return@withLock null
} finally {
- Authenticator.setDefault(previousAuthenticator)
+ Authenticator.setDefault(null)
}
}
}
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 8d654b5af8..c9ed86af11 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -2646,7 +2646,7 @@
Easier to invite your friends 👋
We made connecting simpler for new users.
Safe web links
- - opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking.
+ - opt-in to send link previews.\n- use SOCKS proxy if enabled.\n- prevent hyperlink phishing.\n- remove link tracking.
Non-profit governance
To make SimpleX Network last.
View updated conditions
From c0125e66223966a3d9c3e906dc96a11ce4b04b88 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 29 Apr 2026 14:45:42 +0000
Subject: [PATCH 5/9] android: fix connect banner card layout (wip) (#6922)
---
.../kotlin/chat/simplex/common/views/chatlist/ChatListView.kt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 93513f9edb..01dcd021f7 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
@@ -351,7 +351,7 @@ private fun ConnectBannerCard() {
painterResource(if (isDark) MR.images.banner_create_link_light else MR.images.banner_create_link),
contentDescription = null,
contentScale = ContentScale.FillWidth,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO)
)
} else {
BannerGradientBox(isDark) {
@@ -382,7 +382,7 @@ private fun ConnectBannerCard() {
painterResource(if (isDark) MR.images.banner_paste_link_light else MR.images.banner_paste_link),
contentDescription = null,
contentScale = ContentScale.FillWidth,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO)
)
} else {
BannerGradientBox(isDark) {
From 12e208178d5c1b49ea124e38b62d337f9788b850 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin
Date: Wed, 29 Apr 2026 16:10:06 +0100
Subject: [PATCH 6/9] 6.5-beta.12: android 344, desktop 139
---
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 64a43a7a80..16d50eef7e 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.11
-android.version_code=343
+android.version_name=6.5-beta.12
+android.version_code=344
android.bundle=false
-desktop.version_name=6.5-beta.11
-desktop.version_code=138
+desktop.version_name=6.5-beta.12
+desktop.version_code=139
kotlin.version=2.1.20
gradle.plugin.version=8.7.0
From 7ea3db7b3cb147bee540e9559725ec5312a48e9d Mon Sep 17 00:00:00 2001
From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
Date: Wed, 29 Apr 2026 19:58:01 +0000
Subject: [PATCH 7/9] ui: fix crash opening About SimpleX Chat (#6902)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ui: fix crash and logo cutoff on About SimpleX Chat
OnboardingShrinkingLayout calls .first() on each slot's measurables,
crashing when the button slot is empty. About passes onboardingStage =
null, in which case the button slot emits no children. Have the About
branch emit a 0x0 Spacer so the slot always has one measurable.
Also add a top inset for the SimpleX logo when reached from About in
non-oneHandUI mode — without it the top app bar overlaps the logo.
* style
Co-authored-by: Evgeny
* Apply suggestion from @epoberezkin
Co-authored-by: Evgeny
---------
Co-authored-by: Evgeny
---
.../chat/simplex/common/views/onboarding/SimpleXInfo.kt | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt
index 5b186875fa..74dadcd671 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt
@@ -105,7 +105,9 @@ fun SimpleXInfoLayout(
user: User?,
onboardingStage: SharedPreference?
) {
- Column(Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), horizontalAlignment = Alignment.CenterHorizontally) {
+ val topBar = onboardingStage == null && !appPrefs.oneHandUI.state.value
+ val modifier = Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING)
+ Column(if (topBar) modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier) else modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Box(Modifier.padding(top = DEFAULT_PADDING * 2).widthIn(max = if (appPlatform.isAndroid) 185.dp else 160.dp), contentAlignment = Alignment.Center) {
SimpleXLogo()
}
@@ -156,6 +158,8 @@ fun SimpleXInfoLayout(
ModalManager.fullscreen.showModal { HowItWorks(user, onboardingStage) }
})
}
+ } else {
+ Spacer(Modifier)
}
}
)
From ef0eb4c97ec30bcec5d144b381a314ace680cf3e Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin
Date: Wed, 29 Apr 2026 21:46:44 +0100
Subject: [PATCH 8/9] directory: fix test
---
tests/Bots/DirectoryTests.hs | 8 --------
1 file changed, 8 deletions(-)
diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs
index 9961fa4c1a..68acec0493 100644
--- a/tests/Bots/DirectoryTests.hs
+++ b/tests/Bots/DirectoryTests.hs
@@ -2216,14 +2216,6 @@ testLinkCheckUpdatesCount ps = do
superUser <## " Channel approved!"
bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!")
bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved."
- -- search shows initial count
- bob #> "@'SimpleX Directory' news"
- bob <# "'SimpleX Directory'> > news"
- bob <## " Found 1 group(s)."
- bob <# "'SimpleX Directory'> news"
- bob <##. "Link to join channel: "
- bob <## "You need SimpleX Chat app v6.5 to join."
- bob <## "1 subscribers"
-- link check updates count (bot joined)
threadDelay 1000000
bob #> "@'SimpleX Directory' news"
From edb9958f53eb981c3c9e1696b5102d81effb42eb Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin
Date: Wed, 29 Apr 2026 22:49:07 +0100
Subject: [PATCH 9/9] core: 6.5.0.19 (simplexmq 6.5.0.17)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
simplex-chat.cabal | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/cabal.project b/cabal.project
index 557dee3951..6a432babca 100644
--- a/cabal.project
+++ b/cabal.project
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 858fac7f4f821a2df6fbea03a1bfbb82ea9717c5
+ tag: ba6af65c547cf941af0a1d1645188f7b7f234de1
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 9fe726a02b..efad54fa9a 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."858fac7f4f821a2df6fbea03a1bfbb82ea9717c5" = "1fhzynf80db7h6y2wv61fsdfd80f0blja9ljsfh405r11yg2yxvi";
+ "https://github.com/simplex-chat/simplexmq.git"."ba6af65c547cf941af0a1d1645188f7b7f234de1" = "0sqdj0yawjvgqf92vm0jzzckbi9b2xh8wl8j4ygxikg3d919ksdh";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 70809d41ae..24510ebc70 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.18
+version: 6.5.0.19
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat