Merge branch 'master' into chat-relays

This commit is contained in:
Evgeny Poberezkin
2026-03-31 08:52:24 +01:00
13 changed files with 86 additions and 29 deletions
@@ -98,12 +98,13 @@ struct CIImageView: View {
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(width: w)
.scaledToFill()
.frame(width: w, height: w * heightRatio(img.size))
.clipped()
} else {
SwiftyGif(image: img)
.frame(width: w, height: w * img.size.height / img.size.width)
.scaledToFit()
SwiftyGif(image: img, contentMode: .scaleAspectFill)
.frame(width: w, height: w * heightRatio(img.size))
.clipped()
}
if !blurred || !showDownloadButton(chatItem.file?.fileStatus) {
loadingIndicator()
@@ -21,7 +21,8 @@ struct CILinkView: View {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.aspectRatio(1 / heightRatio(uiImage.size), contentMode: .fill)
.clipped()
.modifier(PrivacyBlur(blurred: $blurred))
.if(!blurred) { v in
v.simultaneousGesture(TapGesture().onEnded {
@@ -187,7 +187,8 @@ struct CIVideoView: View {
ZStack(alignment: .center) {
let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
VideoPlayerView(player: player, url: url, showControls: false)
.frame(width: w, height: w * preview.size.height / preview.size.width)
.frame(width: w, height: w * heightRatio(preview.size))
.clipped()
.onChange(of: m.stopPreviousRecPlay) { playingUrl in
if playingUrl != url {
player.pause()
@@ -315,8 +316,9 @@ struct CIVideoView: View {
return ZStack(alignment: .topTrailing) {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(width: w)
.scaledToFill()
.frame(width: w, height: w * heightRatio(img.size))
.clipped()
.modifier(PrivacyBlur(blurred: $blurred))
if !blurred || !showDownloadButton(chatItem.file?.fileStatus) {
fileStatusIcon()
@@ -29,6 +29,7 @@ struct VideoPlayerView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
let controller = AVPlayerViewController()
controller.showsPlaybackControls = showControls
controller.videoGravity = .resizeAspectFill
if #available(iOS 16.0, *) {
controller.speeds = []
}
+10 -10
View File
@@ -2019,7 +2019,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
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 = 321;
CURRENT_PROJECT_VERSION = 323;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
+5
View File
@@ -402,6 +402,11 @@ extension UIImage {
}
}
// Max image height/width ratio for chat item display, taller images are cropped
public func heightRatio(_ size: CGSize) -> CGFloat {
size.width > 0 ? min(size.height / size.width, 2.33) : 1
}
public func imageFromBase64(_ base64Encoded: String?) -> UIImage? {
if let base64Encoded {
if let img = imageCache.object(forKey: base64Encoded as NSString) {
@@ -21,12 +21,19 @@ import java.net.URI
import kotlin.math.min
import kotlin.math.sqrt
private const val MAX_IMAGE_DIMENSION = 4320
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
return try {
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
if (options.outWidth <= 0 || options.outHeight <= 0 || options.outWidth > MAX_IMAGE_DIMENSION || options.outHeight > MAX_IMAGE_DIMENSION || options.outHeight > options.outWidth * 256) {
return errorBitmap.asImageBitmap()
}
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
} catch (e: Exception) {
Log.e(TAG, "base64ToBitmap error: $e")
@@ -3903,7 +3903,7 @@ object MsgReactionSerializer : KSerializer<MsgReaction> {
when(val t = json["type"]?.jsonPrimitive?.content ?: "") {
"emoji" -> {
val msgReaction = try {
val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString())
val emoji = decoder.json.decodeFromString<MREmojiChar>(json["emoji"].toString())
MsgReaction.Emoji(emoji)
} catch (e: Throwable) {
MsgReaction.Unknown(t, json)
@@ -4345,7 +4345,7 @@ object MsgContentSerializer : KSerializer<MsgContent> {
when (t) {
"text" -> MsgContent.MCText(text)
"link" -> {
val preview = Json.decodeFromString<LinkPreview>(json["preview"].toString())
val preview = decoder.json.decodeFromString<LinkPreview>(json["preview"].toString())
MsgContent.MCLink(text, preview)
}
"image" -> {
@@ -4363,11 +4363,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
}
"file" -> MsgContent.MCFile(text)
"report" -> {
val reason = Json.decodeFromString<ReportReason>(json["reason"].toString())
val reason = decoder.json.decodeFromString<ReportReason>(json["reason"].toString())
MsgContent.MCReport(text, reason)
}
"chat" -> {
val chatLink = Json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
val chatLink = decoder.json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
MsgContent.MCChat(text, chatLink)
}
else -> MsgContent.MCUnknown(t, text, json)
@@ -437,7 +437,10 @@ fun PriorityLayout(
) { measureable, constraints ->
// Find important element which should tell what max width other elements can use
// Expecting only one such element. Can be less than one but not more
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints)
// Max image height for chat item display, taller images are cropped
val maxImageHeight = (constraints.maxWidth * 2.33f).toInt().coerceAtMost(constraints.maxHeight)
val imageConstraints = constraints.copy(maxHeight = maxImageHeight)
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(imageConstraints)
val placeables: List<Placeable> = measureable.map {
if (it.layoutId == priorityLayoutId)
imagePlaceable!!
@@ -25,13 +25,27 @@ private val base64BitmapCache = Collections.synchronizedMap(object : LinkedHashM
override fun removeEldestEntry(eldest: Map.Entry<String, ImageBitmap>): Boolean = size > 200
})
private const val MAX_IMAGE_DIMENSION = 4320
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
base64BitmapCache[base64ImageString]?.let { return it }
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
return try {
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap().also {
val bytes = Base64.getMimeDecoder().decode(imageString)
val stream = ImageIO.createImageInputStream(ByteArrayInputStream(bytes))
val reader = ImageIO.getImageReaders(stream).next()
reader.setInput(stream)
val width = reader.getWidth(0)
val height = reader.getHeight(0)
if (width <= 0 || height <= 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION || height > width * 256) {
reader.dispose()
return errorBitmap()
}
val image = reader.read(0)
reader.dispose()
image.toComposeImageBitmap().also {
base64BitmapCache[base64ImageString] = it
}
} catch (e: Throwable) {
+4 -4
View File
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.5-beta.5
android.version_code=335
android.version_name=6.5-beta.6
android.version_code=338
android.bundle=false
desktop.version_name=6.5-beta.5
desktop.version_code=131
desktop.version_name=6.5-beta.6
desktop.version_code=133
kotlin.version=2.1.20
gradle.plugin.version=8.7.0
+2
View File
@@ -8,6 +8,8 @@ revision: 09.09.2024
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
If you cannot access GitHub, you can download SimpleX Chat apps from our mirror at [git.simplex.chat](https://git.simplex.chat/simplex-chat/simplex-chat/releases)
- [desktop](#desktop-app)
- [mobile](#mobile-apps)
- [terminal](#terminal-console-app) (console)
@@ -38,6 +38,27 @@
</description>
<releases>
<release version="6.4.11" date="2026-03-30">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.11:</p>
<ul>
<li>improve image, video and link messages.</li>
</ul>
<p>New in v6.4-6.4.10:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
<li>better information about network errors.</li>
</ul>
</description>
</release>
<release version="6.4.10" date="2026-01-29">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>