Compare commits

...

36 Commits

Author SHA1 Message Date
IC Rainbow 54c7c905eb Merge remote-tracking branch 'origin/master' into ab/resize-image 2024-12-15 21:05:25 +02:00
IC Rainbow 07c4cb6a0f Fix resize in android 2024-12-15 21:01:10 +02:00
IC Rainbow 921df7bb95 Fix android C api 2024-12-15 20:48:16 +02:00
IC Rainbow 658561392d Fix nix for aarch64/android 2024-12-15 20:22:59 +02:00
Evgeny 6bae86d93b core: reduce simplexmq modules used in the client (#5368)
* core: reduce simplexmq modules used in the client

* remove websockets from simplexmq

* simplexmq
2024-12-14 16:10:14 +00:00
Evgeny Poberezkin 4d82afe602 6.2.1: ios 255, android 261, desktop 83 2024-12-12 21:00:16 +00:00
Diogo 591f74a8e3 android, desktop: show contact reactions (#5376)
* android, desktop: show contact reactions

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-12 20:33:50 +00:00
Evgeny aede65db14 ios: show who reacted for contacts (#5375) 2024-12-12 18:57:40 +00:00
Stanislav Dmitrenko 0bf82c08a1 android, desktop: fix importing the same database after exporting it (#5372)
* android, desktop: fix importing the same database after exporting it

* delete archive after export or cancel

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-12 17:07:08 +00:00
Stanislav Dmitrenko ddef5a122c android, desktop: disable scroll to top of the message when chat bubble was clicked (#5365) 2024-12-12 16:25:51 +00:00
Stanislav Dmitrenko 0fdd2e04cc android, desktop: hide debug logs by default (#5362)
* android, desktop: hide debug logs by default

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-12 16:14:55 +00:00
Stanislav Dmitrenko d1e66386f5 android: fix bottom bar positon in chat item info page (#5373)
* android: fix bottom bar positon in chat item info page

* change
2024-12-12 15:56:59 +00:00
IC Rainbow f579a5270e set static flag for jpeg-turbo 2024-11-14 23:25:49 +02:00
IC Rainbow 8b7af71630 Bump jpeg-turbo 2024-11-14 23:20:34 +02:00
IC Rainbow ca64c76a55 android fixes 2024-11-13 16:58:13 +02:00
IC Rainbow 3e6f510c5a jpeg-turbo fix for 8.10 2024-11-13 16:58:13 +02:00
IC Rainbow 75b3f78556 bump jpeg-turbo for 20.04 fix 2024-11-13 16:08:12 +02:00
IC Rainbow 41e61f553c add encoding funs to android lib 2024-11-13 15:46:42 +02:00
IC Rainbow 6f5321277e Merge remote-tracking branch 'origin/master' into ab/resize-image 2024-11-13 15:24:53 +02:00
IC Rainbow b03f3ae814 add CI deps 2024-11-13 12:13:45 +02:00
IC Rainbow 13e5dd1a8f bump jpeg-turbo 2024-11-13 11:47:23 +02:00
IC Rainbow 25eb60c9ff add nix pins 2024-11-12 20:29:40 +02:00
IC Rainbow b7b34d4ab5 Use jpeg-turbo encoder 2024-11-12 18:25:21 +02:00
IC Rainbow d0e5f18329 Merge remote-tracking branch 'origin/master' into ab/resize-image 2024-11-07 15:55:07 +02:00
IC Rainbow 1aa4c59f1a Fix cfArgs in tests 2024-11-02 13:37:47 +02:00
IC Rainbow 7fbf37e523 add chatWriteImage 2024-11-01 23:06:28 +02:00
IC Rainbow 7fee8b0dd5 fix syntax, raise jpeg quality back 2024-10-31 18:03:20 +02:00
IC Rainbow d91bae8c9c fixes 2024-10-31 13:32:45 +02:00
IC Rainbow b7c77651bc fix resizeToStr 2024-10-31 13:15:31 +02:00
IC Rainbow a787447a2d WIP: add chat_write_image 2024-10-30 17:51:53 +02:00
IC Rainbow 52a48ec045 Add non-base64 resize and cWriteImage 2024-10-30 08:58:21 +02:00
IC Rainbow 0e3d55fc65 WIP: add JNI wrappers 2024-10-22 17:14:33 +03:00
IC Rainbow e76e82634d WIP: fix resizeImageToStrSizeSync 2024-10-22 13:04:00 +03:00
IC Rainbow 87c13d5600 WIP: switch ios resizeImageToStrSizeSync to backend 2024-10-22 11:21:31 +03:00
IC Rainbow 8bfff48f94 WIP: add FFI export wrapper 2024-10-20 13:28:46 +03:00
IC Rainbow e957f866ce WIP: Haskell impl of resizeImageToStrSize 2024-10-18 16:58:01 +03:00
44 changed files with 698 additions and 227 deletions
+7 -2
View File
@@ -150,9 +150,13 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils
- name: Install openssl for Mac
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
run: sudo apt install -y libturbojpeg0-dev
- name: Install pkg-config for Mac
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
run: brew install openssl@3.0
run: brew install openssl@3.0 pkg-config jpeg-turbo
- name: Unix prepare cabal.project.local for Ubuntu
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
@@ -322,6 +326,7 @@ jobs:
git
perl
make
mingw-w64-x86_64-libjpeg-turbo
pacboy: >-
toolchain:p
cmake:p
+39 -21
View File
@@ -440,6 +440,7 @@ struct ChatView: View {
maxWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
showChatInfoSheet: $showChatInfoSheet,
revealedChatItem: $revealedChatItem,
selectedChatItems: $selectedChatItems,
forwardedChatItems: $forwardedChatItems
@@ -893,12 +894,14 @@ struct ChatView: View {
private struct ChatItemWithMenu: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
@Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
@Binding var showChatInfoSheet: Bool
@Binding var revealedChatItem: ChatItem?
@State private var deletingItem: ChatItem? = nil
@@ -1255,16 +1258,22 @@ struct ChatView: View {
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
}
}
if case let .group(groupInfo) = chat.chatInfo {
switch chat.chatInfo {
case let .group(groupInfo):
v.contextMenu {
ReactionContextMenu(
groupInfo: groupInfo,
itemId: ci.id,
reactionCount: r,
selectedMember: $selectedMember
selectedMember: $selectedMember,
profileRadius: profileRadius
)
}
} else {
case let .direct(contact):
v.contextMenu {
contactReactionMenu(contact, r)
}
default:
v
}
}
@@ -1767,6 +1776,20 @@ struct ChatView: View {
}
}
}
@ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View {
if !r.userReacted || r.totalReacted > 1 {
Button { showChatInfoSheet = true } label: {
profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius)
}
}
if r.userReacted {
Button {} label: {
profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius)
}
.disabled(true)
}
}
private struct SelectedChatItem: View {
@EnvironmentObject var theme: AppTheme
@@ -1859,13 +1882,12 @@ struct ReactionContextMenu: View {
var itemId: Int64
var reactionCount: CIReactionCount
@Binding var selectedMember: GMember?
var profileRadius: CGFloat
@State private var memberReactions: [MemberReaction] = []
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
groupMemberReactionList()
.task {
logger.debug("ReactionContextMenu task \(radius)")
await loadChatItemReaction()
}
}
@@ -1889,27 +1911,12 @@ struct ReactionContextMenu: View {
selectedMember = member
}
} label: {
HStack {
Text(mem.displayName)
if let img = cropImage(mem.image) {
Image(uiImage: img)
} else {
Image(systemName: "person.crop.circle")
}
}
profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius)
}
.disabled(userMember)
}
}
}
private func cropImage(_ img: String?) -> UIImage? {
return if let originalImage = imageFromBase64(img) {
maskToCustomShape(originalImage, size: 30, radius: radius)
} else {
nil
}
}
private func loadChatItemReaction() async {
do {
@@ -1927,6 +1934,17 @@ struct ReactionContextMenu: View {
}
}
func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View {
HStack {
nameText
if let image, let img = imageFromBase64(image) {
Image(uiImage: maskToCustomShape(img, size: 30, radius: radius))
} else {
Image(systemName: "person.crop.circle")
}
}
}
func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
let path = Path { path in
if radius >= 50 {
+20 -20
View File
@@ -1931,7 +1931,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1956,7 +1956,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1980,7 +1980,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2005,7 +2005,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -2021,11 +2021,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2041,11 +2041,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2066,7 +2066,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2081,7 +2081,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2103,7 +2103,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2118,7 +2118,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2140,7 +2140,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2166,7 +2166,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2191,7 +2191,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2217,7 +2217,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2242,7 +2242,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2257,7 +2257,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2276,7 +2276,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2291,7 +2291,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+12
View File
@@ -25,6 +25,18 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
}
}
public func writeCryptoImage(maxSize: Int, path: String, data: Data, encrypted: Bool) throws -> CryptoFileArgs {
let ptr: UnsafeMutableRawPointer = malloc(data.count)
memcpy(ptr, (data as NSData).bytes, data.count)
var cPath = path.cString(using: .utf8)!
let cjson = chat_write_image(getChatCtrl(), maxSize, &cPath, ptr, Int32(data.count), encrypted)!
let d = fromCString(cjson).data(using: .utf8)!
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
case let .result(cfArgs): return cfArgs
case let .error(err): throw RuntimeError(err)
}
}
public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data {
var cPath = path.cString(using: .utf8)!
var cKey = cryptoArgs.fileKey.cString(using: .utf8)!
+11
View File
@@ -223,6 +223,17 @@ public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> Crypt
}
}
public func saveImage(_ data: Data, _ fileName: String, maxSize: Long, encrypted: Bool) -> CryptoFile? {
let filePath = getAppFilePath(fileName)
do {
let cfArgs = try writeCryptoImage(maxSize: maxSize, path: filePath.path, data: data, encrypted: encrypted)
return CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
} catch {
logger.error("FileUtils.saveImage error: \(error.localizedDescription)")
return nil
}
}
public func removeFile(_ url: URL) {
do {
try FileManager.default.removeItem(atPath: url.path)
+21 -13
View File
@@ -101,19 +101,27 @@ public func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha
}
public func resizeImageToStrSizeSync(_ image: UIImage, maxDataSize: Int64) -> String? {
var img = image
let hasAlpha = imageHasAlpha(image)
var str = compressImageStr(img, hasAlpha: hasAlpha)
var dataSize = str?.count ?? 0
while dataSize != 0 && dataSize > maxDataSize {
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
let clippedRatio = min(ratio, 2.0)
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha)
str = compressImageStr(img, hasAlpha: hasAlpha)
dataSize = str?.count ?? 0
// XXX: only needed when the original encoding isn't available
let tmpFile = generateNewFileName(getTempFilesDirectory().path + "/" + "resize", "png", fullPath: true)
// encode as png and let the backend deal with alpha and formats
guard let d = image.pngData() else { return nil }
if let _ = saveFile(d, tmpFile, encrypted: false) {
defer { removeFile(tmpFile) }
let ptr = chat_resize_image_to_str_size(tmpFile, maxDataSize)
if let ptr = ptr {
let str = fromCString(ptr)
let dataSize = str.count
if dataSize <= 0 { return nil }
logger.debug("resizeImageToStrSize final \(dataSize)")
return str
} else {
logger.error("resizeImageToStrSize failed")
return nil
}
} else {
logger.error("saveFile failed")
return nil
}
logger.debug("resizeImageToStrSize final \(dataSize)")
return str
}
public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) async -> String? {
@@ -311,7 +319,7 @@ private func getTimestamp() -> String {
}
public func dropImagePrefix(_ s: String) -> String {
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
dropPrefix(dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,"), "data:image/jpeg;base64,")
}
private func dropPrefix(_ s: String, _ prefix: String) -> String {
+4
View File
@@ -29,10 +29,14 @@ extern char *chat_valid_name(char *name);
extern int chat_json_length(char *str);
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len);
extern char *chat_resize_image_to_str_size(const char *path, long maxSize);
// chat_write_file returns null-terminated string with JSON of WriteFileResult
extern char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len);
// chat_write_image returns null-terminated string with JSON of WriteFileResult
extern char *chat_write_image(chat_ctrl ctl, long maxSize, char *path, char *data, int len, bool encrypted);
// chat_read_file returns a buffer with:
// result status (1 byte), then if
// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length.
@@ -14,8 +14,12 @@ import boofcv.android.ConvertBitmap
import boofcv.struct.image.GrayU8
import chat.simplex.common.R
import chat.simplex.common.views.helpers.errorBitmap
import chat.simplex.common.views.helpers.generateNewFileName
import chat.simplex.common.views.helpers.getFileName
import chat.simplex.common.views.helpers.removeFile
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.net.URI
import kotlin.math.min
@@ -25,26 +29,25 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
.removePrefix("data:image/jpeg;base64,")
return try {
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
} catch (e: Exception) {
Log.e(TAG, "base64ToBitmap error: $e")
Log.e(TAG, "base64ToBitmap error: $e for input '$base64ImageString' -> '$imageString'")
errorBitmap.asImageBitmap()
}
}
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
var img = image
var str = compressImageStr(img)
while (str.length > maxDataSize) {
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble())
val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
str = compressImageStr(img)
}
val tmpFileName = generateNewFileName("IMG", "png", tmpDir)
val tmpFile = File(tmpDir, tmpFileName)
val output = FileOutputStream(tmpFile)
compressImageData(image, true).writeTo(output)
output.flush()
output.close()
var str = chatResizeImageToStrSize(tmpFile.absolutePath, maxDataSize)
removeFile(tmpFileName)
return str
}
@@ -71,11 +74,11 @@ fun Bitmap.clipToCircle(): Bitmap {
return circle
}
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg"
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
}
// actual fun compressImageStr(bitmap: ImageBitmap): String {
// val usePng = bitmap.hasAlpha()
// val ext = if (usePng) "png" else "jpg"
// return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
// }
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val stream = ByteArrayOutputStream()
@@ -83,19 +86,19 @@ actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut
return stream
}
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image
var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) {
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
stream = compressImageData(img, usePng)
}
return stream
}
// actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
// var img = image
// var stream = compressImageData(img, usePng)
// while (stream.size() > maxDataSize) {
// val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
// val clippedRatio = min(ratio, 2.0)
// val width = (img.width.toDouble() / clippedRatio).toInt()
// val height = img.height * width / img.width
// img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
// stream = compressImageData(img, usePng)
// }
// return stream
// }
actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap()
@@ -1,10 +1,11 @@
package chat.simplex.common.platform
import android.util.Log
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = Log.d(tag, text).run{}
actual fun e(tag: String, text: String) = Log.e(tag, text).run{}
actual fun i(tag: String, text: String) = Log.i(tag, text).run{}
actual fun w(tag: String, text: String) = Log.w(tag, text).run{}
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) }
}
@@ -1,7 +1,8 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
//#include <stdlib.h>
#include <stdlib.h>
//#include <android/log.h>
// from the RTS
@@ -68,9 +69,31 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length, bool encrypt);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
extern char *chat_resize_image_to_str_size(const char *from_path, long max_size);
char * encode_to_utf8_chars(JNIEnv *env, jstring string) {
if (!string) return "";
const jclass cls_string = (*env)->FindClass(env, "java/lang/String");
const jmethodID mid_getBytes = (*env)->GetMethodID(env, cls_string, "getBytes", "(Ljava/lang/String;)[B");
const jbyteArray jbyte_array = (jbyteArray) (*env)->CallObjectMethod(env, string, mid_getBytes, (*env)->NewStringUTF(env, "UTF-8"));
jint length = (jint) (*env)->GetArrayLength(env, jbyte_array);
jbyte *jbytes = malloc(length + 1);
(*env)->GetByteArrayRegion(env, jbyte_array, 0, length, jbytes);
// char * should be null terminated but jbyte * isn't. Terminate it with \0. Otherwise, Haskell will not see the end of string
jbytes[length] = '\0';
//for (int i = 0; i < length; ++i)
// fprintf(stderr, "%d: %02x\n", i, jbytes[i]);
(*env)->DeleteLocalRef(env, jbyte_array);
(*env)->DeleteLocalRef(env, cls_string);
return (char *) jbytes;
}
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
@@ -182,6 +205,16 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer, jboolean encrypt) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = (*env)->NewStringUTF(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity, encrypt));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
@@ -244,3 +277,11 @@ Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass cla
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatResizeImageToStrSize(JNIEnv *env, jclass clazz, jstring from_path, jlong max_size) {
const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_resize_image_to_str_size(_from_path, max_size));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
return res;
}
@@ -1,5 +1,6 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>
@@ -41,9 +42,11 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length, bool encrypt);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
extern char *chat_resize_image_to_str_size(const char *from_path, long max_size);
// As a reference: https://stackoverflow.com/a/60002045
jstring decode_to_utf8_string(JNIEnv *env, char *string) {
@@ -192,6 +195,16 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer, jboolean encrypt) {
const char *_path = encode_to_utf8_chars(env, path);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = decode_to_utf8_string(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity, encrypt));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) {
const char *_path = encode_to_utf8_chars(env, path);
@@ -254,3 +267,11 @@ Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass cla
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatResizeImageToStrSize(JNIEnv *env, jclass clazz, jstring from_path, jlong max_size) {
const char *_from_path = encode_to_utf8_chars(env, from_path);
jstring res = decode_to_utf8_string(env, chat_resize_image_to_str_size(_from_path, max_size));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
return res;
}
@@ -1,5 +1,6 @@
package chat.simplex.common.model
import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.common.platform.*
import kotlinx.serialization.*
import java.nio.ByteBuffer
@@ -32,6 +33,19 @@ fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
}
}
fun writeCryptoImage(maxSize: Long, image: ImageBitmap, path: String, encrypt: Boolean): CryptoFileArgs {
val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
val data = compressImageData(image, image.hasAlpha).toByteArray()
val buffer = ByteBuffer.allocateDirect(data.size)
buffer.put(data)
buffer.rewind()
val str = chatWriteImage(ctrl, maxSize, path, buffer, encrypt)
return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) {
is WriteFileResult.Result -> d.cryptoArgs
is WriteFileResult.Error -> throw Exception(d.writeError)
}
}
fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)
val status = (res[0] as Integer).toInt()
@@ -132,6 +132,7 @@ class AppPreferences {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } }
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
@@ -393,6 +394,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_LOG_LEVEL = "LogLevel"
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
@@ -32,9 +32,11 @@ external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String
external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatWriteImage(ctrl: ChatCtrl, maxSize: Long, path: String, buffer: ByteBuffer, encrypt: Boolean): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String
external fun chatResizeImageToStrSize(fromPath: String, maxSize: Long): String
val chatModel: ChatModel
get() = chatController.chatModel
@@ -7,10 +7,11 @@ import java.io.InputStream
import java.net.URI
expect fun base64ToBitmap(base64ImageString: String): ImageBitmap
// XXX: Not a part of platform services anymore?
expect fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String
expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
// expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
expect fun cropToSquare(image: ImageBitmap): ImageBitmap
expect fun compressImageStr(bitmap: ImageBitmap): String
// expect fun compressImageStr(bitmap: ImageBitmap): String
expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream
expect fun GrayU8.toImageBitmap(): ImageBitmap
@@ -2,6 +2,10 @@ package chat.simplex.common.platform
const val TAG = "SIMPLEX"
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
expect object Log {
fun d(tag: String, text: String)
fun e(tag: String, text: String)
@@ -296,6 +296,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -309,6 +310,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
QuotedMsgView(qi)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -324,6 +326,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ForwardedFromView(forwardedFromItem)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -395,6 +398,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -433,12 +437,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column {
if (numTabs() > 1) {
Column(
Box(
Modifier
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
.fillMaxHeight()
) {
Column(Modifier.weight(1f)) {
Column {
when (val sel = selection.value) {
is CIInfoTab.Delivery -> {
DeliveryTab(sel.memberDeliveryStatuses)
@@ -479,7 +482,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
Modifier.height(AppBarHeight * fontSizeSqrtMultiplier),
@@ -665,13 +665,18 @@ fun ChatLayout(
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) {
Box(Modifier.fillMaxSize()) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
// disables scrolling to top of chat item on click inside the bubble
CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec {
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
}) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
}
}
}
Box(
@@ -939,6 +944,7 @@ fun BoxScope.ChatItemsList(
linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
@@ -1066,7 +1072,7 @@ fun BoxScope.ChatItemsList(
highlightedItems.value = setOf()
}
}
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
}
}
@@ -24,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.currentUser
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlin.math.*
@@ -51,6 +51,12 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString =
withStyle(chatEventStyle) { append("$eventText $ts") }
}
data class ChatItemReactionMenuItem (
val name: String,
val image: String?,
val onClick: (() -> Unit)?
)
@Composable
fun ChatItemView(
rhId: Long?,
@@ -87,6 +93,7 @@ fun ChatItemView(
showItemDetails: (ChatInfo, ChatItem) -> Unit,
reveal: (Boolean) -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
developerTools: Boolean,
showViaProxy: Boolean,
showTimestamp: Boolean,
@@ -120,7 +127,7 @@ fun ChatItemView(
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
cItem.reactions.forEach { r ->
val showReactionMenu = remember { mutableStateOf(false) }
val reactionMembers = remember { mutableStateOf(emptyList<MemberReaction>()) }
val reactionMenuItems = remember { mutableStateOf(emptyList<ChatItemReactionMenuItem>()) }
val interactionSource = remember { MutableInteractionSource() }
val enterInteraction = remember { HoverInteraction.Enter() }
KeyChangeEffect(highlighted.value) {
@@ -134,18 +141,39 @@ fun ChatItemView(
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
if (cInfo.featureEnabled(ChatFeature.Reactions)) {
fun showReactionsMenu() {
if (cInfo is ChatInfo.Group) {
withBGApi {
try {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
if (members != null) {
showReactionMenu.value = true
reactionMembers.value = members
when (cInfo) {
is ChatInfo.Group -> {
withBGApi {
try {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
if (members != null) {
showReactionMenu.value = true
reactionMenuItems.value = members.map {
val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId
val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null
ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click)
}
}
} catch (e: Exception) {
Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
}
} catch (e: Exception) {
Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
}
}
is ChatInfo.Direct -> {
showReactionMenu.value = true
val reactions = mutableListOf<ChatItemReactionMenuItem>()
if (!r.userReacted || r.totalReacted > 1) {
val contact = cInfo.contact
reactions.add(ChatItemReactionMenuItem(contact.displayName, contact.image, showChatInfo))
}
if (r.userReacted) {
reactions.add(ChatItemReactionMenuItem(generalGetString(MR.strings.sender_you_pronoun), currentUser.value?.image, null))
}
reactionMenuItems.value = reactions
}
else -> {}
}
}
modifier = modifier
@@ -166,19 +194,19 @@ fun ChatItemView(
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
ReactionIcon(r.reaction.text, fontSize = 12.sp)
DefaultDropdownMenu(showMenu = showReactionMenu) {
reactionMembers.value.forEach { m ->
reactionMenuItems.value.forEach { m ->
ItemAction(
text = m.groupMember.displayName,
composable = { ProfileImage(44.dp, m.groupMember.image) },
text = m.name,
composable = { ProfileImage(44.dp, m.image) },
onClick = {
if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) {
showMemberInfo(cInfo.groupInfo, m.groupMember)
showReactionMenu.value = false
} else {
val click = m.onClick
if (click != null) {
click()
showReactionMenu.value = false
}
},
lineLimit = 1
lineLimit = 1,
color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor
)
}
}
@@ -1188,6 +1216,7 @@ fun PreviewChatItemView(
showItemDetails = { _, _ -> },
reveal = {},
showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false,
showViaProxy = false,
showTimestamp = true,
@@ -1233,6 +1262,7 @@ fun PreviewChatItemViewDeletedContent() {
showItemDetails = { _, _ -> },
reveal = {},
showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false,
showViaProxy = false,
preview = true,
@@ -30,6 +30,7 @@ import kotlinx.datetime.*
import java.io.*
import java.net.URI
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
@@ -44,11 +45,14 @@ fun DatabaseView() {
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val stopped = remember { m.chatRunning }.value == false
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
val file = chatArchiveFile.value
if (file != null && to != null) {
copyFileToFile(File(file), to) {
chatArchiveFile.value = null
}
val archive = chatArchiveFile.value
if (archive != null && to != null) {
copyFileToFile(File(archive), to) {}
}
// delete no matter the database was exported or canceled the export process
if (archive != null) {
File(archive).delete()
chatArchiveFile.value = null
}
}
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
@@ -680,6 +684,8 @@ suspend fun importArchive(
} finally {
File(archivePath).delete()
}
} else {
progressIndicator.value = false
}
return false
}
@@ -691,14 +697,15 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
if (inputStream != null && archiveName != null) {
val archivePath = "$databaseExportDir${File.separator}$archiveName"
val destFile = File(archivePath)
Files.copy(inputStream, destFile.toPath())
Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
archivePath
} else {
Log.e(TAG, "saveArchiveFromURI null inputStream")
null
}
} catch (e: Exception) {
Log.e(TAG, "saveArchiveFromURI error: ${e.message}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString())
Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}")
null
}
}
@@ -169,24 +169,19 @@ fun saveImage(image: ImageBitmap): CryptoFile? {
return try {
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath("")))
val destFile = File(getAppFilePath(destFileName))
if (encrypted) {
try {
val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray())
try {
val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, encrypted)
if (encrypted) {
CryptoFile(destFileName, args)
} catch (e: Exception) {
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
null
} else {
CryptoFile.plain(destFileName)
}
} else {
val output = FileOutputStream(destFile)
dataResized.writeTo(output)
output.flush()
output.close()
CryptoFile.plain(destFileName)
} catch (e: Exception) {
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
null
}
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}")
@@ -198,14 +193,10 @@ fun desktopSaveImageInTmp(uri: URI): CryptoFile? {
val image = getBitmapFromUri(uri) ?: return null
return try {
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val destFileName = generateNewFileName("IMG", ext, tmpDir)
val destFile = File(tmpDir, destFileName)
val output = FileOutputStream(destFile)
dataResized.writeTo(output)
output.flush()
output.close()
CryptoFile.plain(destFile.absolutePath)
val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, false)
CryptoFile(destFileName, args)
} catch (e: Exception) {
Log.e(TAG, "Util.kt desktopSaveImageInTmp error: ${e.stackTraceToString()}")
null
@@ -301,11 +292,7 @@ fun saveWallpaperFile(uri: URI): String? {
fun saveWallpaperFile(image: ImageBitmap): String {
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
val destFile = File(getWallpaperFilePath(destFileName))
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
val output = FileOutputStream(destFile)
dataResized.use {
it.writeTo(output)
}
writeCryptoImage(5_000_000, image, destFile.absolutePath, false)
return destFile.name
}
@@ -365,7 +352,10 @@ fun formatBytes(bytes: Long): String {
}
fun removeFile(fileName: String): Boolean {
val file = File(getAppFilePath(fileName))
return removeFile(File(getAppFilePath(fileName)))
}
fun removeFile(file: File): Boolean {
val fileDeleted = file.delete()
if (!fileDeleted) {
Log.e(TAG, "Util.kt removeFile error")
@@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -44,6 +45,12 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
if (devTools.value) {
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) {
DefaultSwitch(
checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG,
onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) }
)
}
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
if (appPlatform.isDesktop) {
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
@@ -907,6 +907,7 @@
<string name="show_dev_options">Show:</string>
<string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</string>
<string name="debug_logs">Enable logs</string>
<string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string>
<string name="show_internal_errors">Show internal errors</string>
@@ -1338,6 +1339,7 @@
<string name="chat_database_exported_migrate">You may migrate the exported database.</string>
<string name="chat_database_exported_not_all_files">Some file(s) were not exported</string>
<string name="chat_database_exported_continue">Continue</string>
<string name="error_saving_database">Error saving database</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="M262.5-285q11.5 0 20.25-8.5t8.75-20q0-11.5-8.75-20.25t-20.25-8.75q-11.5 0-20 8.75T234-313.5q0 11.5 8.5 20t20 8.5Zm-29-168.5H291v-227h-57.5v227Zm217.5 174h275.5V-337H451v57.5Zm0-174h275.5V-511H451v57.5Zm0-169.5h275.5v-57.5H451v57.5ZM134.5-124.5q-22.97 0-40.23-17.27Q77-159.03 77-182v-596q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h691q22.97 0 40.23 17.27Q883-800.97 883-778v596q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-691Zm0-57.5h691v-596h-691v596Zm0 0v-596 596Z"/></svg>

After

Width:  |  Height:  |  Size: 592 B

@@ -67,7 +67,7 @@ object NtfManager {
ntf.second.close()
} catch (e: Exception) {
// Can be java.lang.UnsupportedOperationException, for example. May do nothing
println("Failed to close notification: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}")
}*/
}
}
@@ -85,7 +85,8 @@ object NtfManager {
}
fun cancelAllNotifications() {
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } }
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e
// .stackTraceToString()}") } }
withBGApi {
prevNtfsMutex.withLock {
prevNtfs.clear()
@@ -153,7 +154,7 @@ object NtfManager {
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
newFile.absolutePath
} catch (e: Exception) {
println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
null
}
} else null
@@ -5,6 +5,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import boofcv.io.image.ConvertBufferedImage
import boofcv.struct.image.GrayU8
import chat.simplex.common.views.helpers.generateNewFileName
import chat.simplex.common.views.helpers.removeFile
import chat.simplex.res.MR
import org.jetbrains.skia.Image
import java.awt.RenderingHints
@@ -25,40 +27,40 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
.removePrefix("data:image/jpeg;base64,")
return try {
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap()
} catch (e: IOException) {
Log.e(TAG, "base64ToBitmap error: $e")
} catch (e: Exception) { // ByteArrayInputStream returns null
Log.e(TAG, "base64ToBitmap error: $e for input '$base64ImageString' -> '$imageString'")
errorBitmap()
}
}
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
var img = image
var str = compressImageStr(img)
while (str.length > maxDataSize) {
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble())
val clippedRatio = kotlin.math.min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = img.scale(width, height)
str = compressImageStr(img)
}
val tmpFileName = generateNewFileName("IMG", "png", tmpDir)
val tmpFile = File(tmpDir, tmpFileName)
val output = FileOutputStream(tmpFile)
compressImageData(image, true).writeTo(output)
output.flush()
output.close()
var str = chatResizeImageToStrSize(tmpFile.absolutePath, maxDataSize)
removeFile(tmpFile)
return str
}
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image
var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) {
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val clippedRatio = kotlin.math.min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = img.scale(width, height)
stream = compressImageData(img, usePng)
}
return stream
}
// actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
// var img = image
// var stream = compressImageData(img, usePng)
// while (stream.size() > maxDataSize) {
// val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
// val clippedRatio = kotlin.math.min(ratio, 2.0)
// val width = (img.width.toDouble() / clippedRatio).toInt()
// val height = img.height * width / img.width
// img = img.scale(width, height)
// stream = compressImageData(img, usePng)
// }
// return stream
// }
actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
var xOffset = 0
@@ -81,17 +83,17 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
return croppedImage
}
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg"
return try {
val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
"data:image/$ext;base64,$encoded"
} catch (e: Exception) {
Log.e(TAG, "resizeImageToStrSize error: $e")
throw e
}
}
// actual fun compressImageStr(bitmap: ImageBitmap): String {
// val usePng = bitmap.hasAlpha()
// val ext = if (usePng) "png" else "jpg"
// return try {
// val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
// "data:image/$ext;base64,$encoded"
// } catch (e: Exception) {
// Log.e(TAG, "resizeImageToStrSize error: $e")
// throw e
// }
// }
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next()
@@ -1,8 +1,10 @@
package chat.simplex.common.platform
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = println("D: $text")
actual fun e(tag: String, text: String) = println("E: $text")
actual fun i(tag: String, text: String) = println("I: $text")
actual fun w(tag: String, text: String) = println("W: $text")
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") }
}
+4 -4
View File
@@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.2
android.version_code=259
android.version_name=6.2.1
android.version_code=261
desktop.version_name=6.2
desktop.version_code=82
desktop.version_name=6.2.1
desktop.version_code=83
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
+7 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 79e9447b73cc315ce35042b0a5f210c07ea39b07
tag: 0a827307d666de819d5cef17f70a78be5cd0e410
source-repository-package
type: git
@@ -63,3 +63,9 @@ source-repository-package
location: https://github.com/simplex-chat/wai.git
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
subdir: warp
source-repository-package
type: git
location: https://gitlab.com/dpwiz/hs-jpeg-turbo
tag: d2844e00e7834124e27171cde21cf280d6772aa8
subdir: JuicyPixels-jpeg-turbo jpeg-turbo
+15
View File
@@ -198,6 +198,7 @@
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
pkgs.pkgsCross.mingwW64.openssl
];
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
pkgs.pkgsCross.mingwW64.openssl
@@ -336,6 +337,7 @@
packages.direct-sqlcipher.patches = [
./scripts/nix/direct-sqlcipher-android-log.patch
];
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
@@ -389,6 +391,8 @@
"chat_valid_name"
"chat_json_length"
"chat_write_file"
"chat_write_image"
"chat_resize_image_to_str_size"
];
postInstall = ''
set -x
@@ -445,6 +449,11 @@
packages.direct-sqlcipher.patches = [
./scripts/nix/direct-sqlcipher-android-log.patch
];
packages.jpeg-turbo.flags.static = true;
packages.jpeg-turbo.components.library.libs = pkgs.lib.mkForce [
(androidPkgs.libjpeg_turbo.override { enableStatic = true; })
];
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(androidPkgs.openssl.override { static = true; })
@@ -493,6 +502,8 @@
"chat_valid_name"
"chat_json_length"
"chat_write_file"
"chat_write_image"
"chat_resize_image_to_str_size"
];
postInstall = ''
set -x
@@ -550,6 +561,7 @@
packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
# TODO: have a cross override for iOS, that sets this.
@@ -565,6 +577,7 @@
extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;}))
@@ -583,6 +596,7 @@
packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(pkgs.openssl.override { static = true; })
@@ -597,6 +611,7 @@
extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(pkgs.openssl.override { static = true; })
+3 -1
View File
@@ -16,6 +16,8 @@ EXPORTS
chat_encrypt_media
chat_decrypt_media
chat_write_file
chat_write_image
chat_read_file
chat_encrypt_file
chat_decrypt_file
chat_decrypt_file
chat_resize_image_to_str_size
+3
View File
@@ -52,6 +52,9 @@ dependencies:
- unliftio-core == 0.2.*
- uuid == 1.3.*
- zip == 2.0.*
- JuicyPixels
- JuicyPixels-jpeg-turbo
- JuicyPixels-stbir
flags:
swift:
+2 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."79e9447b73cc315ce35042b0a5f210c07ea39b07" = "16z7z5a3f7gw0h188manykp008d1bqpydlrj7h497mgyjmp4cy9m";
"https://github.com/simplex-chat/simplexmq.git"."0a827307d666de819d5cef17f70a78be5cd0e410" = "09z0y6w1c51szd9r90fl259inn76yahfbb164mawd34sr41ccp7g";
"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";
@@ -9,4 +9,5 @@
"https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0";
"https://github.com/yesodweb/wai.git"."ec5e017d896a78e787a5acea62b37a4e677dec2e" = "1ckcpmpjfy9jiqrb52q20lj7ln4hmq9v2jk6kpkf3m68c1m9c2bx";
"https://github.com/simplex-chat/wai.git"."2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0" = "199g4rjdf1zp1fcw8nqdsyr1h36hmg424qqx03071jk7j00z7ay4";
"https://gitlab.com/dpwiz/hs-jpeg-turbo"."d2844e00e7834124e27171cde21cf280d6772aa8" = "1gbvpkx7g2r62bfgpmf8ljdvpccnskdnhqzblm515cb70pyrfq07";
}
+30 -7
View File
@@ -35,6 +35,7 @@ library
Simplex.Chat.Core
Simplex.Chat.Files
Simplex.Chat.Help
Simplex.Chat.Image
Simplex.Chat.Markdown
Simplex.Chat.Messages
Simplex.Chat.Messages.Batch
@@ -205,7 +206,10 @@ library
StrictData
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns
build-depends:
aeson ==2.2.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
@@ -270,7 +274,10 @@ executable simplex-bot
StrictData
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
@@ -336,7 +343,10 @@ executable simplex-bot-advanced
StrictData
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
@@ -405,7 +415,10 @@ executable simplex-broadcast-bot
Paths_simplex_chat
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
@@ -472,7 +485,10 @@ executable simplex-chat
StrictData
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
@@ -545,7 +561,10 @@ executable simplex-directory-service
Paths_simplex_chat
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
build-depends:
aeson ==2.2.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
, attoparsec ==0.14.*
@@ -618,6 +637,7 @@ test-suite simplex-chat-test
ChatTests.Profiles
ChatTests.Utils
JSONTests
LinkPreviewTests
MarkdownTests
MessageBatching
MobileTests
@@ -645,7 +665,10 @@ test-suite simplex-chat-test
StrictData
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
build-depends:
QuickCheck ==2.14.*
JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, QuickCheck ==2.14.*
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, async ==2.2.*
+1 -2
View File
@@ -92,9 +92,8 @@ import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
import qualified Simplex.Chat.Util as U
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription, maxFileSize, maxFileSizeHard)
import qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import qualified Simplex.FileTransfer.Transport as XFTP
+127
View File
@@ -0,0 +1,127 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Simplex.Chat.Image where
import qualified Codec.Picture as Picture
import qualified Codec.Picture.JPEGTurbo as JT
import Codec.Picture.Metadata (Metadatas)
import Codec.Picture.Png (encodePng)
import qualified Codec.Picture.STBIR as STBIR
import Codec.Picture.Types (dropAlphaLayer, pixelFoldMap)
import Control.Monad.Except (ExceptT (..), runExceptT, throwError)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Base64.Lazy as LB64
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.Monoid (All (..))
import System.IO.Unsafe (unsafePerformIO)
resizeImageToSize :: Bool -> Int -> Int64 -> ResizeableImage -> LB.ByteString
resizeImageToSize toURI minJpegQuality maxSize (ResizeableImage fmt img encoder) = either resizePNG resizeJPG encoder
where
halveAndRetry = resizeImageToSize toURI minJpegQuality maxSize (ResizeableImage fmt imgHalved encoder)
imgHalved = downscale img 2.0
resizePNG enc
| LB.length encoded <= maxSize = encoded
| LB.length (encode imgHalved) > maxSize = halveAndRetry
| otherwise = fitScale 1.0 2.0
where
encode
| toURI = toDataUri "png" . enc
| otherwise = enc
encoded = encode img
fitScale l u
| u - l < 1 / 64 = encode $ downscale img u -- prefer lower resolution
| otherwise =
case compare maxSize (LB.length result) of
LT -> fitScale m u -- over budget, scale harder
EQ -> result
GT -> fitScale l m -- keep more pixels
where
m = (l + u) / 2
result = encode $ downscale img m
resizeJPG enc
| minSize > maxSize = halveAndRetry
| otherwise = fitQuality minJpegQuality 95
where
encode q
| toURI = toDataUri "jpg" $ enc q img -- the correct mime type is "jpeg", but only "jpg" is supported by older clients
| otherwise = enc q img
minSize = LB.length $ encode minJpegQuality
fitQuality l u
| u - l <= 1 = encode l -- prefer higher compression
| otherwise =
case compare (LB.length result) maxSize of
LT -> fitQuality m u -- keep more data
EQ -> result
GT -> fitQuality l m -- over budget, reduce quality
where
m = (l + u) `div` 2
result = encode m
downscale :: STBIR.STBIRPixel pixel => Picture.Image pixel -> Float -> Picture.Image pixel
downscale img scale = STBIR.resize STBIR.defaultOptions (scaled Picture.imageWidth) (scaled Picture.imageHeight) img
where
scaled f = round $ fromIntegral (f img) / min 2.0 (max 0.5 scale)
toDataUri :: LB.ByteString -> LB.ByteString -> LB.ByteString
toDataUri fmt body = "data:image/" <> fmt <> ";base64," <> LB64.encode body
decodeResizeable :: ByteString -> Either String (ResizeableImage, Metadatas)
decodeResizeable source = do
(input, metadata) <- Picture.decodeImageWithMetadata source
maybe (Left "unsupported image format") (pure . (,metadata)) $ resizeableImage (tryDropOpacity input)
readResizeable :: FilePath -> IO (Either String (ResizeableImage, Metadatas))
readResizeable inputPath = runExceptT $ do
(input, metadata) <- ExceptT $ Picture.readImageWithMetadata inputPath -- will use mmap instead of reading the whole file
maybe (throwError "unsupported image format") (pure . (,metadata)) $ resizeableImage (tryDropOpacity input)
tryDropOpacity :: Picture.DynamicImage -> Picture.DynamicImage
tryDropOpacity dyn = case dyn of
Picture.ImageRGBA16 img | opaque img -> Picture.ImageRGB16 $ dropAlphaLayer img
Picture.ImageRGBA8 img | opaque img -> Picture.ImageRGB8 $ dropAlphaLayer img
Picture.ImageYA16 img | opaque img -> Picture.ImageY16 $ dropAlphaLayer img
Picture.ImageYA8 img | opaque img -> Picture.ImageY8 $ dropAlphaLayer img
_ -> dyn
where
opaque :: (Picture.Pixel a, Eq (Picture.PixelBaseComponent a), Bounded (Picture.PixelBaseComponent a)) => Picture.Image a -> Bool
opaque = getAll . pixelFoldMap (All . \pix -> Picture.pixelOpacity pix == maxBound)
data ResizeableImage where
ResizeableImage ::
STBIR.STBIRPixel a =>
-- | format label
String ->
-- | current image data
Picture.Image a ->
ImageEncoder a ->
ResizeableImage
type ImageEncoder a = Either (PNGEncoder a) (JPGEncoder a)
type PNGEncoder a = Picture.Image a -> LB.ByteString
type JPGEncoder a = Int -> Picture.Image a -> LB.ByteString
resizeableImage :: Picture.DynamicImage -> Maybe ResizeableImage
resizeableImage dyn = case dyn of
Picture.ImageY8 img -> Just $ ResizeableImage "Y8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q . Picture.convertRGB8 . Picture.ImageY8 -- TODO: directly
Picture.ImageY16 img -> Just $ ResizeableImage "Y16" img $ Left encodePng
Picture.ImageY32 _ -> Nothing
Picture.ImageYF _ -> Nothing
Picture.ImageYA8 img -> Just $ ResizeableImage "YA8" img $ Left encodePng
Picture.ImageYA16 img -> Just $ ResizeableImage "YA16" img $ Left encodePng
Picture.ImageRGB8 img -> Just $ ResizeableImage "RGB8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q
Picture.ImageRGB16 img -> Just $ ResizeableImage "RGB16" img $ Left encodePng
Picture.ImageRGBF _ -> Nothing
Picture.ImageRGBA8 img -> Just $ ResizeableImage "RGBA8" img $ Left encodePng
Picture.ImageRGBA16 img -> Just $ ResizeableImage "RGBA16" img $ Left encodePng
Picture.ImageYCbCr8 img -> Just $ ResizeableImage "YCbCr8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q . Picture.convertRGB8 . Picture.ImageYCbCr8 -- TODO: JT.encodeYUV
Picture.ImageCMYK8 img -> Just $ ResizeableImage "CMYK8" img $ Right $ \q -> LB.fromStrict . unsafePerformIO . JT.encodeRGB q . Picture.convertRGB8 . Picture.ImageCMYK8
Picture.ImageCMYK16 _ -> Nothing
+39 -1
View File
@@ -21,6 +21,7 @@ import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Either (fromRight)
import Data.Functor (($>))
import Data.List (find)
import qualified Data.List.NonEmpty as L
@@ -29,13 +30,14 @@ import Data.Word (Word8)
import Database.SQLite.Simple (SQLError (..))
import qualified Database.SQLite.Simple as DB
import Foreign.C.String
import Foreign.C.Types (CInt (..))
import Foreign.C.Types (CBool (..), CInt (..), CLong (..))
import Foreign.Ptr
import Foreign.StablePtr
import Foreign.Storable (poke)
import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding)
import Simplex.Chat
import Simplex.Chat.Controller
import Simplex.Chat.Image (readResizeable, resizeImageToSize)
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared
@@ -45,6 +47,7 @@ import Simplex.Chat.Remote.Types
import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types
import Simplex.Chat.Util (liftIOEither)
import Simplex.Messaging.Agent.Client (agentClientStore)
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore)
@@ -102,12 +105,16 @@ foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Wo
foreign export ccall "chat_write_file" cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString
foreign export ccall "chat_write_image" cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> CBool -> IO CJSONString
foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
foreign export ccall "chat_encrypt_file" cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString
foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
foreign export ccall "chat_resize_image_to_str_size" cChatResizeImageToStrSize :: CString -> CLong -> IO CString
-- | check / migrate database and initialize chat controller on success
cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
cChatMigrateInit fp key conf = cChatMigrateInitKey fp key 0 conf 0
@@ -182,6 +189,37 @@ cChatValidName cName = newCString . mkValidName =<< peekCString cName
cChatJsonLength :: CString -> IO CInt
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s
-- -- | Resize image at path to match specified dimensions preserving aspect ratio
-- cChatResizeImageToFit :: CString -> CInt -> CInt -> IO CString
-- cChatResizeImageToFit path maxWidth maxHeight = error "todo"
-- -- | Resize image at path to match specified dimensions, cropping the extra pixels
-- cChatResizeImageCrop :: CString -> CInt -> CInt -> IO CString
-- cChatResizeImageCrop path width height = error "todo" -- аватарки
-- -- | Downscale image at path until it fits into specified size
-- cChatResizeImageToDataSize :: CString -> CInt -> IO CString
-- cChatResizeImageToDataSize path maxSize = error "todo"
-- | Downscale image at path until its data-uri encoding fits into specified size.
-- Returns data-uri/base64 encoded image as 0-terminated string.
-- Empty result string means operation failure.
-- The caller must free the result ptr.
cChatResizeImageToStrSize :: CString -> CLong -> IO CString
cChatResizeImageToStrSize fp' maxSize = do
fp <- peekCString fp'
res <- runExceptT $ do
(ri, _) <- liftIOEither $ readResizeable fp
let resized = resizeImageToSize True previewMinQuality (fromIntegral maxSize) ri
if LB.length resized > fromIntegral maxSize then throwError "unable to fit" else pure resized
newCStringFromLazyBS $ fromRight "" res
where
previewMinQuality = 20
-- -- | Strip EXIF etc metadata from image, inlplace
-- cChatStripImageMetadata :: CString -> IO CBool
-- cChatStripImageMetadata path = error "todo"
mobileChatOpts :: String -> ChatOpts
mobileChatOpts dbFilePrefix =
ChatOpts
+29 -4
View File
@@ -7,6 +7,7 @@
module Simplex.Chat.Mobile.File
( cChatWriteFile,
cChatWriteImage,
cChatReadFile,
cChatEncryptFile,
cChatDecryptFile,
@@ -35,6 +36,8 @@ import Foreign.Ptr
import Foreign.StablePtr
import Foreign.Storable (poke, pokeByteOff)
import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Image (resizeImageToSize)
import qualified Simplex.Chat.Image as Picture
import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Util (chunkSize, encryptFile)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..))
@@ -45,7 +48,7 @@ import Simplex.Messaging.Util (catchAll)
import UnliftIO (Handle, IOMode (..), atomically, withFile)
data WriteFileResult
= WFResult {cryptoArgs :: CryptoFileArgs}
= WFResult {cryptoArgs :: Maybe CryptoFileArgs}
| WFError {writeError :: String}
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
@@ -61,10 +64,32 @@ cChatWriteFile cc cPath ptr len = do
chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult
chatWriteFile ChatController {random} path s = do
cfArgs <- atomically $ CF.randomArgs random
let file = CryptoFile path $ Just cfArgs
either WFError (\_ -> WFResult cfArgs)
chatWriteFile_ (Just cfArgs) path s
chatWriteFile_ :: Maybe CryptoFileArgs -> FilePath -> ByteString -> IO WriteFileResult
chatWriteFile_ cfArgs_ path s = do
let file = CryptoFile path cfArgs_
either WFError (\_ -> WFResult cfArgs_)
<$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> CBool -> IO CJSONString
cChatWriteImage cc maxSize cPath ptr len encrypt = do
c <- deRefStablePtr cc
path <- peekCString cPath
src <- getByteString ptr len
cfArgs_ <- if encrypt /= 0 then Just <$> atomically (CF.randomArgs $ random c) else pure Nothing
r <-
case Picture.decodeResizeable src of
Left e -> pure $ WFError e
Right (ri, _metadata) -> do
let resized = resizeImageToSize False storeMinQuality (fromIntegral maxSize) ri
if LB.length resized > fromIntegral maxSize
then pure $ WFError "unable to fit"
else chatWriteFile_ cfArgs_ path (LB.toStrict resized)
newCStringFromLazyBS $ J.encode r
where
storeMinQuality = 20
data ReadFileResult
= RFResult {fileSize :: Int}
| RFError {readError :: String}
@@ -106,7 +131,7 @@ chatEncryptFile ChatController {random} fromPath toPath =
encrypt = do
cfArgs <- atomically $ CF.randomArgs random
encryptFile fromPath toPath cfArgs
pure cfArgs
pure $ Just cfArgs
cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
cChatDecryptFile cFromPath cKey cNonce cToPath = do
+1 -1
View File
@@ -620,7 +620,7 @@ testXFTPFileTransferEncrypted =
createDirectoryIfMissing True "./tests/tmp/alice/"
createDirectoryIfMissing True "./tests/tmp/bob/"
WFResult cfArgs <- chatWriteFile (chatController alice) srcPath src
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath cfArgs
withXFTPServer $ do
connectUsers alice bob
alice ##> ("/_send @2 json [{\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}]")
+32
View File
@@ -0,0 +1,32 @@
module LinkPreviewTests where
import qualified Data.ByteString.Base64.Lazy as LB64
import qualified Data.ByteString.Lazy.Char8 as LB
import qualified Codec.Picture as Picture
import Control.Logger.Simple
import Control.Monad
import Simplex.Chat.Image (ResizeableImage (..))
import qualified Simplex.Chat.Image as Image
import Simplex.Messaging.Util (tshow)
import Test.Hspec
linkPreviewTests :: SpecWith FilePath
linkPreviewTests = do
fdescribe "Image resize" $ do
it "JPG" $ resizeToStrTest "tests/fixtures/test.jpg"
it "PNG with alpha" $ resizeToStrTest "tests/fixtures/logo-large-rgba.png"
it "PNG without alpha" $ resizeToStrTest "tests/fixtures/preview-issue1.png"
resizeToStrTest :: FilePath -> FilePath -> IO ()
resizeToStrTest inputPath tmp = do
(ri@(ResizeableImage imgFormat _img encoder), metadata) <- either error pure =<< Image.readResizeable inputPath
logDebug $ tshow (metadata, imgFormat, either (const "png") (const "jpg") encoder)
let res = Image.resizeImageToSize True 20 maxSize ri
finalSize = LB.length res
unless (finalSize <= maxSize) $ error $ "Final size larger than maximum size: " <> show (finalSize, maxSize)
let (fmt, b64) = fmap (LB.drop 8) . LB.break (== ';') $ LB.drop 11 res
outFile = tmp ++ "/out." ++ LB.unpack fmt
either error (LB.writeFile outFile) $ LB64.decode b64
Picture.readImageWithMetadata outFile >>= either error (logDebug . tshow . snd)
where
maxSize = 14000
+4 -4
View File
@@ -282,8 +282,8 @@ testFileCApi fileName tmp = do
ptr <- mallocBytes $ B.length src
putByteString ptr src
r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
let encryptedFile = CryptoFile path $ Just cfArgs
Just (WFResult cfArgs@(Just (CFArgs key nonce))) <- jDecode r
let encryptedFile = CryptoFile path cfArgs
CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key
cNonce <- encodedCString nonce
@@ -318,8 +318,8 @@ testFileEncryptionCApi fileName tmp = do
let toPath = tmp </> (fileName <> ".encrypted.pdf")
cToPath <- newCString toPath
r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src)
Just (WFResult cfArgs@(Just (CFArgs key nonce))) <- jDecode r
CF.getFileContentsSize (CryptoFile toPath cfArgs) `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key
cNonce <- encodedCString nonce
let toPath' = tmp </> (fileName <> ".decrypted.pdf")
+3 -1
View File
@@ -6,6 +6,7 @@ import ChatTests.Utils (xdescribe'')
import Control.Logger.Simple
import Data.Time.Clock.System
import JSONTests
import LinkPreviewTests
import MarkdownTests
import MessageBatching
import MobileTests
@@ -22,7 +23,7 @@ import WebRTCTests
main :: IO ()
main = do
setLogLevel LogError
setLogLevel LogDebug
withGlobalLogging logCfg . hspec $ do
describe "Schema dump" schemaDumpTest
describe "SimpleX chat markdown" markdownTests
@@ -40,6 +41,7 @@ main = do
xdescribe'' "SimpleX Broadcast bot" broadcastBotTests
xdescribe'' "SimpleX Directory service bot" directoryServiceTests
describe "Remote session" remoteTests
around testBracket $ describe "Link preview" linkPreviewTests
where
testBracket test = withSmpServer $ tmpBracket test
tmpBracket test = do
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB