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
53 changed files with 710 additions and 544 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' if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils 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' 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 - name: Unix prepare cabal.project.local for Ubuntu
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
@@ -322,6 +326,7 @@ jobs:
git git
perl perl
make make
mingw-w64-x86_64-libjpeg-turbo
pacboy: >- pacboy: >-
toolchain:p toolchain:p
cmake:p cmake:p
-3
View File
@@ -79,6 +79,3 @@ website/package-lock.json
website/.cache website/.cache
website/test/stubs-layout-cache/_includes/*.js website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release apps/android/app/release
packages/simplex-chat-nodejs/build
packages/simplex-chat-nodejs/.vscode
packages/simplex-chat-nodejs/libs
+39 -21
View File
@@ -440,6 +440,7 @@ struct ChatView: View {
maxWidth: maxWidth, maxWidth: maxWidth,
composeState: $composeState, composeState: $composeState,
selectedMember: $selectedMember, selectedMember: $selectedMember,
showChatInfoSheet: $showChatInfoSheet,
revealedChatItem: $revealedChatItem, revealedChatItem: $revealedChatItem,
selectedChatItems: $selectedChatItems, selectedChatItems: $selectedChatItems,
forwardedChatItems: $forwardedChatItems forwardedChatItems: $forwardedChatItems
@@ -893,12 +894,14 @@ struct ChatView: View {
private struct ChatItemWithMenu: View { private struct ChatItemWithMenu: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
@Binding @ObservedObject var chat: Chat @Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared @ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem let chatItem: ChatItem
let maxWidth: CGFloat let maxWidth: CGFloat
@Binding var composeState: ComposeState @Binding var composeState: ComposeState
@Binding var selectedMember: GMember? @Binding var selectedMember: GMember?
@Binding var showChatInfoSheet: Bool
@Binding var revealedChatItem: ChatItem? @Binding var revealedChatItem: ChatItem?
@State private var deletingItem: ChatItem? = nil @State private var deletingItem: ChatItem? = nil
@@ -1255,16 +1258,22 @@ struct ChatView: View {
setReaction(ci, add: !r.userReacted, reaction: r.reaction) setReaction(ci, add: !r.userReacted, reaction: r.reaction)
} }
} }
if case let .group(groupInfo) = chat.chatInfo { switch chat.chatInfo {
case let .group(groupInfo):
v.contextMenu { v.contextMenu {
ReactionContextMenu( ReactionContextMenu(
groupInfo: groupInfo, groupInfo: groupInfo,
itemId: ci.id, itemId: ci.id,
reactionCount: r, reactionCount: r,
selectedMember: $selectedMember selectedMember: $selectedMember,
profileRadius: profileRadius
) )
} }
} else { case let .direct(contact):
v.contextMenu {
contactReactionMenu(contact, r)
}
default:
v v
} }
} }
@@ -1768,6 +1777,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 { private struct SelectedChatItem: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
var ciId: Int64 var ciId: Int64
@@ -1859,13 +1882,12 @@ struct ReactionContextMenu: View {
var itemId: Int64 var itemId: Int64
var reactionCount: CIReactionCount var reactionCount: CIReactionCount
@Binding var selectedMember: GMember? @Binding var selectedMember: GMember?
var profileRadius: CGFloat
@State private var memberReactions: [MemberReaction] = [] @State private var memberReactions: [MemberReaction] = []
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View { var body: some View {
groupMemberReactionList() groupMemberReactionList()
.task { .task {
logger.debug("ReactionContextMenu task \(radius)")
await loadChatItemReaction() await loadChatItemReaction()
} }
} }
@@ -1889,28 +1911,13 @@ struct ReactionContextMenu: View {
selectedMember = member selectedMember = member
} }
} label: { } label: {
HStack { profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius)
Text(mem.displayName)
if let img = cropImage(mem.image) {
Image(uiImage: img)
} else {
Image(systemName: "person.crop.circle")
}
}
} }
.disabled(userMember) .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 { private func loadChatItemReaction() async {
do { do {
let memberReactions = try await apiGetReactionMembers( let memberReactions = try await apiGetReactionMembers(
@@ -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 { func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
let path = Path { path in let path = Path { path in
if radius >= 50 { if radius >= 50 {
+20 -20
View File
@@ -1931,7 +1931,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1956,7 +1956,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES_THIN; LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1980,7 +1980,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -2005,7 +2005,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2021,11 +2021,11 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2041,11 +2041,11 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2066,7 +2066,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = s;
@@ -2081,7 +2081,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2103,7 +2103,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO; ENABLE_CODE_COVERAGE = NO;
@@ -2118,7 +2118,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2140,7 +2140,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2166,7 +2166,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2191,7 +2191,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2217,7 +2217,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2242,7 +2242,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2257,7 +2257,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2276,7 +2276,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254; CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2291,7 +2291,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.2; MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; 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 { public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data {
var cPath = path.cString(using: .utf8)! var cPath = path.cString(using: .utf8)!
var cKey = cryptoArgs.fileKey.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) { public func removeFile(_ url: URL) {
do { do {
try FileManager.default.removeItem(atPath: url.path) 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? { public func resizeImageToStrSizeSync(_ image: UIImage, maxDataSize: Int64) -> String? {
var img = image // XXX: only needed when the original encoding isn't available
let hasAlpha = imageHasAlpha(image) let tmpFile = generateNewFileName(getTempFilesDirectory().path + "/" + "resize", "png", fullPath: true)
var str = compressImageStr(img, hasAlpha: hasAlpha) // encode as png and let the backend deal with alpha and formats
var dataSize = str?.count ?? 0 guard let d = image.pngData() else { return nil }
while dataSize != 0 && dataSize > maxDataSize { if let _ = saveFile(d, tmpFile, encrypted: false) {
let ratio = sqrt(Double(dataSize) / Double(maxDataSize)) defer { removeFile(tmpFile) }
let clippedRatio = min(ratio, 2.0) let ptr = chat_resize_image_to_str_size(tmpFile, maxDataSize)
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha) if let ptr = ptr {
str = compressImageStr(img, hasAlpha: hasAlpha) let str = fromCString(ptr)
dataSize = str?.count ?? 0 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? { public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) async -> String? {
@@ -311,7 +319,7 @@ private func getTimestamp() -> String {
} }
public func dropImagePrefix(_ s: String) -> 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 { 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 int chat_json_length(char *str);
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len); 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_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 // 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); 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: // chat_read_file returns a buffer with:
// result status (1 byte), then if // result status (1 byte), then if
// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length. // 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 boofcv.struct.image.GrayU8
import chat.simplex.common.R import chat.simplex.common.R
import chat.simplex.common.views.helpers.errorBitmap 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.getFileName
import chat.simplex.common.views.helpers.removeFile
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.net.URI import java.net.URI
import kotlin.math.min import kotlin.math.min
@@ -25,26 +29,25 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString val imageString = base64ImageString
.removePrefix("data:image/png;base64,") .removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,") .removePrefix("data:image/jpg;base64,")
.removePrefix("data:image/jpeg;base64,")
return try { return try {
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP) val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap() BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "base64ToBitmap error: $e") Log.e(TAG, "base64ToBitmap error: $e for input '$base64ImageString' -> '$imageString'")
errorBitmap.asImageBitmap() errorBitmap.asImageBitmap()
} }
} }
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String { actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
var img = image val tmpFileName = generateNewFileName("IMG", "png", tmpDir)
var str = compressImageStr(img) val tmpFile = File(tmpDir, tmpFileName)
while (str.length > maxDataSize) { val output = FileOutputStream(tmpFile)
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble()) compressImageData(image, true).writeTo(output)
val clippedRatio = min(ratio, 2.0) output.flush()
val width = (img.width.toDouble() / clippedRatio).toInt() output.close()
val height = img.height * width / img.width var str = chatResizeImageToStrSize(tmpFile.absolutePath, maxDataSize)
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap() removeFile(tmpFileName)
str = compressImageStr(img)
}
return str return str
} }
@@ -71,11 +74,11 @@ fun Bitmap.clipToCircle(): Bitmap {
return circle return circle
} }
actual fun compressImageStr(bitmap: ImageBitmap): String { // actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha() // val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg" // val ext = if (usePng) "png" else "jpg"
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP) // return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
} // }
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream { actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
@@ -83,19 +86,19 @@ actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut
return stream return stream
} }
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream { // actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image // var img = image
var stream = compressImageData(img, usePng) // var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) { // while (stream.size() > maxDataSize) {
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble()) // val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val clippedRatio = min(ratio, 2.0) // val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt() // val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width // val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap() // img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
stream = compressImageData(img, usePng) // stream = compressImageData(img, usePng)
} // }
return stream // return stream
} // }
actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap() actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap()
@@ -1,10 +1,11 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import android.util.Log import android.util.Log
import chat.simplex.common.model.ChatController.appPrefs
actual object Log { actual object Log {
actual fun d(tag: String, text: String) = Log.d(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) = Log.e(tag, text).run{} 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) = Log.i(tag, text).run{} 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) = Log.w(tag, text).run{} 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 <jni.h>
#include <string.h> #include <string.h>
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
//#include <stdlib.h> #include <stdlib.h>
//#include <android/log.h> //#include <android/log.h>
// from the RTS // 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 char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str); 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_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_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_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_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 JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { 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; 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 JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) { 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); 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); (*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res; 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;
}
@@ -54,10 +54,11 @@ add_library( # Sets the name of the library.
simplex-api.c) simplex-api.c)
add_library( simplex SHARED IMPORTED ) add_library( simplex SHARED IMPORTED )
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libsimplex.${OS_LIB_EXT})
if(WIN32) if(WIN32)
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT})
set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB}) set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB})
else() else()
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex-chat*.${OS_LIB_EXT})
set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB})
endif() endif()
@@ -1,5 +1,6 @@
#include <jni.h> #include <jni.h>
#include <string.h> #include <string.h>
#include <stdbool.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdint.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 char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str); 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_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_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_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_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 // As a reference: https://stackoverflow.com/a/60002045
jstring decode_to_utf8_string(JNIEnv *env, char *string) { 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; 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 JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) { 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); 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); (*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res; 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 package chat.simplex.common.model
import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import kotlinx.serialization.* import kotlinx.serialization.*
import java.nio.ByteBuffer 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 { fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce) val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)
val status = (res[0] as Integer).toInt() val status = (res[0] as Integer).toInt()
@@ -132,6 +132,7 @@ class AppPreferences {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false) val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, 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 showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false) val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, 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_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" 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_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls" private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" 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 chatValidName(name: String): String
external fun chatJsonLength(str: String): Int external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String 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 chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
external fun chatDecryptFile(fromPath: String, key: String, nonce: 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 val chatModel: ChatModel
get() = chatController.chatModel get() = chatController.chatModel
@@ -7,10 +7,11 @@ import java.io.InputStream
import java.net.URI import java.net.URI
expect fun base64ToBitmap(base64ImageString: String): ImageBitmap expect fun base64ToBitmap(base64ImageString: String): ImageBitmap
// XXX: Not a part of platform services anymore?
expect fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String 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 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 compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream
expect fun GrayU8.toImageBitmap(): ImageBitmap expect fun GrayU8.toImageBitmap(): ImageBitmap
@@ -2,6 +2,10 @@ package chat.simplex.common.platform
const val TAG = "SIMPLEX" const val TAG = "SIMPLEX"
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
expect object Log { expect object Log {
fun d(tag: String, text: String) fun d(tag: String, text: String)
fun e(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()
SectionBottomSpacer()
} }
} }
@@ -309,6 +310,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
QuotedMsgView(qi) QuotedMsgView(qi)
} }
SectionBottomSpacer() SectionBottomSpacer()
SectionBottomSpacer()
} }
} }
@@ -324,6 +326,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ForwardedFromView(forwardedFromItem) ForwardedFromView(forwardedFromItem)
} }
SectionBottomSpacer() SectionBottomSpacer()
SectionBottomSpacer()
} }
} }
@@ -395,6 +398,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
SectionBottomSpacer()
} }
} }
@@ -433,12 +437,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column { Column {
if (numTabs() > 1) { if (numTabs() > 1) {
Column( Box(
Modifier Modifier
.fillMaxHeight(), .fillMaxHeight()
verticalArrangement = Arrangement.SpaceBetween
) { ) {
Column(Modifier.weight(1f)) { Column {
when (val sel = selection.value) { when (val sel = selection.value) {
is CIInfoTab.Delivery -> { is CIInfoTab.Delivery -> {
DeliveryTab(sel.memberDeliveryStatuses) DeliveryTab(sel.memberDeliveryStatuses)
@@ -479,7 +482,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state } 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( TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
Modifier.height(AppBarHeight * fontSizeSqrtMultiplier), Modifier.height(AppBarHeight * fontSizeSqrtMultiplier),
@@ -665,13 +665,18 @@ fun ChatLayout(
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) { if (chatInfo != null) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
ChatItemsList( // disables scrolling to top of chat item on click inside the bubble
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec {
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages, override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, }) {
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, ChatItemsList(
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, 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( Box(
@@ -939,6 +944,7 @@ fun BoxScope.ChatItemsList(
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>, selectedChatItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupInfo, GroupMember) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit, deleteMessages: (List<Long>) -> Unit,
@@ -1066,7 +1072,7 @@ fun BoxScope.ChatItemsList(
highlightedItems.value = setOf() 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 androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.currentUser
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.math.* import kotlin.math.*
@@ -51,6 +51,12 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString =
withStyle(chatEventStyle) { append("$eventText $ts") } withStyle(chatEventStyle) { append("$eventText $ts") }
} }
data class ChatItemReactionMenuItem (
val name: String,
val image: String?,
val onClick: (() -> Unit)?
)
@Composable @Composable
fun ChatItemView( fun ChatItemView(
rhId: Long?, rhId: Long?,
@@ -87,6 +93,7 @@ fun ChatItemView(
showItemDetails: (ChatInfo, ChatItem) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit,
reveal: (Boolean) -> Unit, reveal: (Boolean) -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean, showViaProxy: Boolean,
showTimestamp: Boolean, showTimestamp: Boolean,
@@ -120,7 +127,7 @@ fun ChatItemView(
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
cItem.reactions.forEach { r -> cItem.reactions.forEach { r ->
val showReactionMenu = remember { mutableStateOf(false) } val showReactionMenu = remember { mutableStateOf(false) }
val reactionMembers = remember { mutableStateOf(emptyList<MemberReaction>()) } val reactionMenuItems = remember { mutableStateOf(emptyList<ChatItemReactionMenuItem>()) }
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val enterInteraction = remember { HoverInteraction.Enter() } val enterInteraction = remember { HoverInteraction.Enter() }
KeyChangeEffect(highlighted.value) { KeyChangeEffect(highlighted.value) {
@@ -134,18 +141,39 @@ fun ChatItemView(
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
if (cInfo.featureEnabled(ChatFeature.Reactions)) { if (cInfo.featureEnabled(ChatFeature.Reactions)) {
fun showReactionsMenu() { fun showReactionsMenu() {
if (cInfo is ChatInfo.Group) { when (cInfo) {
withBGApi { is ChatInfo.Group -> {
try { withBGApi {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) try {
if (members != null) { val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
showReactionMenu.value = true if (members != null) {
reactionMembers.value = members 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 modifier = modifier
@@ -166,19 +194,19 @@ fun ChatItemView(
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
ReactionIcon(r.reaction.text, fontSize = 12.sp) ReactionIcon(r.reaction.text, fontSize = 12.sp)
DefaultDropdownMenu(showMenu = showReactionMenu) { DefaultDropdownMenu(showMenu = showReactionMenu) {
reactionMembers.value.forEach { m -> reactionMenuItems.value.forEach { m ->
ItemAction( ItemAction(
text = m.groupMember.displayName, text = m.name,
composable = { ProfileImage(44.dp, m.groupMember.image) }, composable = { ProfileImage(44.dp, m.image) },
onClick = { onClick = {
if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) { val click = m.onClick
showMemberInfo(cInfo.groupInfo, m.groupMember) if (click != null) {
showReactionMenu.value = false click()
} else {
showReactionMenu.value = false showReactionMenu.value = false
} }
}, },
lineLimit = 1 lineLimit = 1,
color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor
) )
} }
} }
@@ -1188,6 +1216,7 @@ fun PreviewChatItemView(
showItemDetails = { _, _ -> }, showItemDetails = { _, _ -> },
reveal = {}, reveal = {},
showMemberInfo = { _, _ ->}, showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false, developerTools = false,
showViaProxy = false, showViaProxy = false,
showTimestamp = true, showTimestamp = true,
@@ -1233,6 +1262,7 @@ fun PreviewChatItemViewDeletedContent() {
showItemDetails = { _, _ -> }, showItemDetails = { _, _ -> },
reveal = {}, reveal = {},
showMemberInfo = { _, _ ->}, showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false, developerTools = false,
showViaProxy = false, showViaProxy = false,
preview = true, preview = true,
@@ -30,6 +30,7 @@ import kotlinx.datetime.*
import java.io.* import java.io.*
import java.net.URI import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -44,11 +45,14 @@ fun DatabaseView() {
val chatArchiveFile = remember { mutableStateOf<String?>(null) } val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val stopped = remember { m.chatRunning }.value == false val stopped = remember { m.chatRunning }.value == false
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
val file = chatArchiveFile.value val archive = chatArchiveFile.value
if (file != null && to != null) { if (archive != null && to != null) {
copyFileToFile(File(file), to) { copyFileToFile(File(archive), to) {}
chatArchiveFile.value = null }
} // 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)) } val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
@@ -680,6 +684,8 @@ suspend fun importArchive(
} finally { } finally {
File(archivePath).delete() File(archivePath).delete()
} }
} else {
progressIndicator.value = false
} }
return false return false
} }
@@ -691,14 +697,15 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
if (inputStream != null && archiveName != null) { if (inputStream != null && archiveName != null) {
val archivePath = "$databaseExportDir${File.separator}$archiveName" val archivePath = "$databaseExportDir${File.separator}$archiveName"
val destFile = File(archivePath) val destFile = File(archivePath)
Files.copy(inputStream, destFile.toPath()) Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
archivePath archivePath
} else { } else {
Log.e(TAG, "saveArchiveFromURI null inputStream") Log.e(TAG, "saveArchiveFromURI null inputStream")
null null
} }
} catch (e: Exception) { } 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 null
} }
} }
@@ -169,24 +169,19 @@ fun saveImage(image: ImageBitmap): CryptoFile? {
return try { return try {
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
val ext = if (image.hasAlpha()) "png" else "jpg" 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 destFileName = generateNewFileName("IMG", ext, File(getAppFilePath("")))
val destFile = File(getAppFilePath(destFileName)) val destFile = File(getAppFilePath(destFileName))
if (encrypted) { try {
try { val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, encrypted)
val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray()) if (encrypted) {
CryptoFile(destFileName, args) CryptoFile(destFileName, args)
} catch (e: Exception) { } else {
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString()) CryptoFile.plain(destFileName)
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
null
} }
} else { } catch (e: Exception) {
val output = FileOutputStream(destFile) Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
dataResized.writeTo(output) AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
output.flush() null
output.close()
CryptoFile.plain(destFileName)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}") Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}")
@@ -198,14 +193,10 @@ fun desktopSaveImageInTmp(uri: URI): CryptoFile? {
val image = getBitmapFromUri(uri) ?: return null val image = getBitmapFromUri(uri) ?: return null
return try { return try {
val ext = if (image.hasAlpha()) "png" else "jpg" 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 destFileName = generateNewFileName("IMG", ext, tmpDir)
val destFile = File(tmpDir, destFileName) val destFile = File(tmpDir, destFileName)
val output = FileOutputStream(destFile) val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, false)
dataResized.writeTo(output) CryptoFile(destFileName, args)
output.flush()
output.close()
CryptoFile.plain(destFile.absolutePath)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Util.kt desktopSaveImageInTmp error: ${e.stackTraceToString()}") Log.e(TAG, "Util.kt desktopSaveImageInTmp error: ${e.stackTraceToString()}")
null null
@@ -301,11 +292,7 @@ fun saveWallpaperFile(uri: URI): String? {
fun saveWallpaperFile(image: ImageBitmap): String { fun saveWallpaperFile(image: ImageBitmap): String {
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath(""))) val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
val destFile = File(getWallpaperFilePath(destFileName)) val destFile = File(getWallpaperFilePath(destFileName))
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000) writeCryptoImage(5_000_000, image, destFile.absolutePath, false)
val output = FileOutputStream(destFile)
dataResized.use {
it.writeTo(output)
}
return destFile.name return destFile.name
} }
@@ -365,7 +352,10 @@ fun formatBytes(bytes: Long): String {
} }
fun removeFile(fileName: String): Boolean { fun removeFile(fileName: String): Boolean {
val file = File(getAppFilePath(fileName)) return removeFile(File(getAppFilePath(fileName)))
}
fun removeFile(file: File): Boolean {
val fileDeleted = file.delete() val fileDeleted = file.delete()
if (!fileDeleted) { if (!fileDeleted) {
Log.e(TAG, "Util.kt removeFile error") 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.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@@ -44,6 +45,12 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
if (devTools.value) { if (devTools.value) {
SectionDividerSpaced(maxTopPadding = true) SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) { 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) SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
if (appPlatform.isDesktop) { if (appPlatform.isDesktop) {
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked -> TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
@@ -907,6 +907,7 @@
<string name="show_dev_options">Show:</string> <string name="show_dev_options">Show:</string>
<string name="hide_dev_options">Hide:</string> <string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</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">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string> <string name="developer_options_section">Developer options</string>
<string name="show_internal_errors">Show internal errors</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_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_not_all_files">Some file(s) were not exported</string>
<string name="chat_database_exported_continue">Continue</string> <string name="chat_database_exported_continue">Continue</string>
<string name="error_saving_database">Error saving database</string>
<!-- DatabaseEncryptionView.kt --> <!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string> <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() ntf.second.close()
} catch (e: Exception) { } catch (e: Exception) {
// Can be java.lang.UnsupportedOperationException, for example. May do nothing // 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() { 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 { withBGApi {
prevNtfsMutex.withLock { prevNtfsMutex.withLock {
prevNtfs.clear() prevNtfs.clear()
@@ -153,7 +154,7 @@ object NtfManager {
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream()) ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
newFile.absolutePath newFile.absolutePath
} catch (e: Exception) { } 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 null
} }
} else null } else null
@@ -5,6 +5,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import boofcv.io.image.ConvertBufferedImage import boofcv.io.image.ConvertBufferedImage
import boofcv.struct.image.GrayU8 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 chat.simplex.res.MR
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.awt.RenderingHints import java.awt.RenderingHints
@@ -25,40 +27,40 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString val imageString = base64ImageString
.removePrefix("data:image/png;base64,") .removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,") .removePrefix("data:image/jpg;base64,")
.removePrefix("data:image/jpeg;base64,")
return try { return try {
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap() ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap()
} catch (e: IOException) { } catch (e: Exception) { // ByteArrayInputStream returns null
Log.e(TAG, "base64ToBitmap error: $e") Log.e(TAG, "base64ToBitmap error: $e for input '$base64ImageString' -> '$imageString'")
errorBitmap() errorBitmap()
} }
} }
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String { actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
var img = image val tmpFileName = generateNewFileName("IMG", "png", tmpDir)
var str = compressImageStr(img) val tmpFile = File(tmpDir, tmpFileName)
while (str.length > maxDataSize) { val output = FileOutputStream(tmpFile)
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble()) compressImageData(image, true).writeTo(output)
val clippedRatio = kotlin.math.min(ratio, 2.0) output.flush()
val width = (img.width.toDouble() / clippedRatio).toInt() output.close()
val height = img.height * width / img.width var str = chatResizeImageToStrSize(tmpFile.absolutePath, maxDataSize)
img = img.scale(width, height) removeFile(tmpFile)
str = compressImageStr(img)
}
return str return str
} }
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image // actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var stream = compressImageData(img, usePng) // var img = image
while (stream.size() > maxDataSize) { // var stream = compressImageData(img, usePng)
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble()) // while (stream.size() > maxDataSize) {
val clippedRatio = kotlin.math.min(ratio, 2.0) // val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val width = (img.width.toDouble() / clippedRatio).toInt() // val clippedRatio = kotlin.math.min(ratio, 2.0)
val height = img.height * width / img.width // val width = (img.width.toDouble() / clippedRatio).toInt()
img = img.scale(width, height) // val height = img.height * width / img.width
stream = compressImageData(img, usePng) // img = img.scale(width, height)
} // stream = compressImageData(img, usePng)
return stream // }
} // return stream
// }
actual fun cropToSquare(image: ImageBitmap): ImageBitmap { actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
var xOffset = 0 var xOffset = 0
@@ -81,17 +83,17 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
return croppedImage return croppedImage
} }
actual fun compressImageStr(bitmap: ImageBitmap): String { // actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha() // val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg" // val ext = if (usePng) "png" else "jpg"
return try { // return try {
val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray()) // val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
"data:image/$ext;base64,$encoded" // "data:image/$ext;base64,$encoded"
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(TAG, "resizeImageToStrSize error: $e") // Log.e(TAG, "resizeImageToStrSize error: $e")
throw e // throw e
} // }
} // }
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream { actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next() val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next()
@@ -1,8 +1,10 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import chat.simplex.common.model.ChatController.appPrefs
actual object Log { actual object Log {
actual fun d(tag: String, text: String) = println("D: $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) = println("E: $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) = println("I: $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) = println("W: $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.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11 kotlin.jvm.target=11
android.version_name=6.2 android.version_name=6.2.1
android.version_code=259 android.version_code=261
desktop.version_name=6.2 desktop.version_name=6.2.1
desktop.version_code=82 desktop.version_code=83
kotlin.version=1.9.23 kotlin.version=1.9.23
gradle.plugin.version=8.2.0 gradle.plugin.version=8.2.0
+7 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 79e9447b73cc315ce35042b0a5f210c07ea39b07 tag: 0a827307d666de819d5cef17f70a78be5cd0e410
source-repository-package source-repository-package
type: git type: git
@@ -63,3 +63,9 @@ source-repository-package
location: https://github.com/simplex-chat/wai.git location: https://github.com/simplex-chat/wai.git
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0 tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
subdir: warp 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 [ packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
pkgs.pkgsCross.mingwW64.openssl pkgs.pkgsCross.mingwW64.openssl
]; ];
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true; packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
pkgs.pkgsCross.mingwW64.openssl pkgs.pkgsCross.mingwW64.openssl
@@ -336,6 +337,7 @@
packages.direct-sqlcipher.patches = [ packages.direct-sqlcipher.patches = [
./scripts/nix/direct-sqlcipher-android-log.patch ./scripts/nix/direct-sqlcipher-android-log.patch
]; ];
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true; packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(android32Pkgs.openssl.override { static = true; enableKTLS = false; }) (android32Pkgs.openssl.override { static = true; enableKTLS = false; })
@@ -389,6 +391,8 @@
"chat_valid_name" "chat_valid_name"
"chat_json_length" "chat_json_length"
"chat_write_file" "chat_write_file"
"chat_write_image"
"chat_resize_image_to_str_size"
]; ];
postInstall = '' postInstall = ''
set -x set -x
@@ -445,6 +449,11 @@
packages.direct-sqlcipher.patches = [ packages.direct-sqlcipher.patches = [
./scripts/nix/direct-sqlcipher-android-log.patch ./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.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(androidPkgs.openssl.override { static = true; }) (androidPkgs.openssl.override { static = true; })
@@ -493,6 +502,8 @@
"chat_valid_name" "chat_valid_name"
"chat_json_length" "chat_json_length"
"chat_write_file" "chat_write_file"
"chat_write_image"
"chat_resize_image_to_str_size"
]; ];
postInstall = '' postInstall = ''
set -x set -x
@@ -550,6 +561,7 @@
packages.simplexmq.flags.swift = true; packages.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true; packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true; packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true; packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
# TODO: have a cross override for iOS, that sets this. # TODO: have a cross override for iOS, that sets this.
@@ -565,6 +577,7 @@
extra-modules = [{ extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true; packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true; packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true; packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) ((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.simplexmq.flags.swift = true;
packages.direct-sqlcipher.flags.commoncrypto = true; packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true; packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true; packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(pkgs.openssl.override { static = true; }) (pkgs.openssl.override { static = true; })
@@ -597,6 +611,7 @@
extra-modules = [{ extra-modules = [{
packages.direct-sqlcipher.flags.commoncrypto = true; packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true; packages.entropy.flags.DoNotGetEntropy = true;
packages."jpeg-turbo".flags.static = true;
packages.simplexmq.flags.client_library = true; packages.simplexmq.flags.client_library = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(pkgs.openssl.override { static = true; }) (pkgs.openssl.override { static = true; })
+2
View File
@@ -16,6 +16,8 @@ EXPORTS
chat_encrypt_media chat_encrypt_media
chat_decrypt_media chat_decrypt_media
chat_write_file chat_write_file
chat_write_image
chat_read_file chat_read_file
chat_encrypt_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.* - unliftio-core == 0.2.*
- uuid == 1.3.* - uuid == 1.3.*
- zip == 2.0.* - zip == 2.0.*
- JuicyPixels
- JuicyPixels-jpeg-turbo
- JuicyPixels-stbir
flags: flags:
swift: swift:
-14
View File
@@ -1,14 +0,0 @@
{
"targets": [
{
"target_name": "addon",
"sources": [ "cpp/simplex.cc" ],
"libraries": [
"-Wl,-rpath,libs",
"-L<(module_root_dir)/build/Release/",
"-L<(module_root_dir)/libs",
"-lsimplex"
]
}
]
}
-188
View File
@@ -1,188 +0,0 @@
#include <sstream>
#include <string>
#include <node.h>
#include "simplex.h"
namespace simplex
{
using v8::Array;
using v8::ArrayBuffer;
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Maybe;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
void haskell_init()
{
int argc = 5;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A64m", // chunk size for new allocations
"-H64m", // initial heap size
"-xn", // non-moving GC
0};
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
void ChatMigrateInit(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
std::string path(*(v8::String::Utf8Value(isolate, args[0])));
std::string key(*(v8::String::Utf8Value(isolate, args[1])));
std::string confirm(*(v8::String::Utf8Value(isolate, args[2])));
long *ctrl(0);
std::string res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl);
std::stringstream ss;
ss << ctrl
<< "\n"
<< res;
// printf("ChatCtrl after init %d", cChatCtrl);
// v8::Local<v8::String> ctrl = v8::String::NewFromUtf8(isolate, cppChatCtrl.c_str(), v8::String::kNormalString);
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, ss.str().c_str())
.ToLocalChecked());
}
void ChatCloseStore(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_close_store(ctrl))
.ToLocalChecked());
}
void ChatSendCmd(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
std::string cmd(*(v8::String::Utf8Value(isolate, args[1])));
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_send_cmd(ctrl, cmd.c_str()))
.ToLocalChecked());
}
void ChatRecvMsgWait(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
long wait = ((long)args[1].As<Number>()->Value());
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_recv_msg_wait(ctrl, wait))
.ToLocalChecked());
}
void ChatWriteFile(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
std::string path(*(v8::String::Utf8Value(isolate, args[1])));
Local<v8::ArrayBuffer> arr = args[2].As<ArrayBuffer>();
char *buffer = (char *)arr->Data();
int len(arr->ByteLength());
// std::array address(*arr);
// v8::Array a = *buffer;
// std::string data(*((isolate, args[1])));
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_write_file(ctrl, path.c_str(), buffer, len))
.ToLocalChecked());
}
void ChatReadFile(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
std::string path(*(v8::String::Utf8Value(isolate, args[1])));
Local<v8::ArrayBuffer> arr = args[2].As<ArrayBuffer>();
char *buffer = (char *)arr->Data();
int len(arr->ByteLength());
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_write_file(ctrl, path.c_str(), buffer, len))
.ToLocalChecked());
}
void ChatEncryptFile(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
long *ctrl = (long *)((long)args[0].As<Number>()->Value());
std::string fromPath(*(v8::String::Utf8Value(isolate, args[1])));
std::string toPath(*(v8::String::Utf8Value(isolate, args[2])));
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()))
.ToLocalChecked());
}
void ChatDecryptFile(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
std::string fromPath(*(v8::String::Utf8Value(isolate, args[0])));
std::string key(*(v8::String::Utf8Value(isolate, args[1])));
std::string nonce(*(v8::String::Utf8Value(isolate, args[2])));
std::string toPath(*(v8::String::Utf8Value(isolate, args[3])));
args.GetReturnValue().Set(
String::NewFromUtf8(
isolate, chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()))
.ToLocalChecked());
}
void Initialize(Local<Object> exports)
{
haskell_init();
NODE_SET_METHOD(exports, "chat_migrate_init", ChatMigrateInit);
NODE_SET_METHOD(exports, "chat_close_store", ChatCloseStore);
NODE_SET_METHOD(exports, "chat_send_cmd", ChatSendCmd);
NODE_SET_METHOD(exports, "chat_recv_msg_wait", ChatRecvMsgWait);
NODE_SET_METHOD(exports, "chat_write_file", ChatWriteFile);
NODE_SET_METHOD(exports, "chat_read_file", ChatReadFile);
NODE_SET_METHOD(exports, "chat_encrypt_file", ChatEncryptFile);
NODE_SET_METHOD(exports, "chat_decrypt_file", ChatDecryptFile);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace simplex
@@ -1,46 +0,0 @@
//
// simplex.h
// SimpleX
//
// Created by Evgeny on 30/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
#ifndef SimpleX_h
#define SimpleX_h
extern "C" void hs_init(int argc, char **argv[]);
extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]);
typedef long* chat_ctrl;
// the last parameter is used to return the pointer to chat controller
extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
extern "C" char *chat_close_store(chat_ctrl ctrl);
extern "C" char *chat_reopen_store(chat_ctrl ctrl);
extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
extern "C" char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern "C" char *chat_parse_markdown(const char *str);
extern "C" char *chat_parse_server(const char *str);
extern "C" char *chat_password_hash(const char *pwd, const char *salt);
extern "C" char *chat_valid_name(const char *name);
extern "C" int chat_json_length(const char *str);
extern "C" char *chat_encrypt_media(chat_ctrl ctrl, const char *key, const char *frame, const int len);
extern "C" char *chat_decrypt_media(const char *key, const char *frame, const int len);
// chat_write_file returns null-terminated string with JSON of WriteFileResult
extern "C" char *chat_write_file(chat_ctrl ctrl, const char *path, const char *data, const int len);
// 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.
// status == 1 (error): null-terminated error message string.
extern "C" char *chat_read_file(const char *path, const char *key, const char *nonce);
// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult
extern "C" char *chat_encrypt_file(chat_ctrl ctrl, const char *fromPath, const char *toPath);
// chat_decrypt_file returns null-terminated string with the error message
extern "C" char *chat_decrypt_file(const char *fromPath, const char *key, const char *nonce, const char *toPath);
#endif /* simplex_h */
-31
View File
@@ -1,31 +0,0 @@
{
"name": "simplexjs",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"install-tools": "npm install -g node-gyp",
"configure": "node-gyp configure; mkdir libs 2> /dev/null | true",
"rebuild": "node-gyp rebuild",
"build": "node-gyp build",
"run": "node src/index.js",
"build-run": "node-gyp build && node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/simplex-chat/simplex-chat.git"
},
"keywords": [
"messenger",
"chat",
"privacy",
"security"
],
"author": "SimpleX Chat",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/simplex-chat/simplex-chat/issues"
},
"homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-nodejs#readme",
"description": ""
}
-22
View File
@@ -1,22 +0,0 @@
const addon = require('../build/Release/addon');
const fs = require('fs');
const ctrlAndRes = addon.chat_migrate_init("db", "a", "yesUp")
const ctrl = Number(ctrlAndRes.split("\n")[0])
const res = ctrlAndRes.split("\n")[1]
console.log("Migrate ctrl:", ctrl, "res:", res)
console.log(addon.chat_send_cmd(ctrl, "/v"))
// const wait = 15_000_000
// console.log(ctrl, addon.chat_recv_msg_wait(ctrl, wait))
const path = "/tmp/write_file.txt"
const data = [0, 1, 2]
console.log(addon.chat_write_file(ctrl, path, new ArrayBuffer(data)))
fs.writeFileSync("/tmp/file_unencrypted.txt", "unencrypted")
const encRes = JSON.parse(addon.chat_encrypt_file(ctrl, "/tmp/file_unencrypted.txt", "/tmp/file_encrypted.txt"))
const key = encRes.cryptoArgs.fileKey
const nonce = encRes.cryptoArgs.fileNonce
console.log(encRes)
console.log(addon.chat_decrypt_file("/tmp/file_encrypted.txt", key, nonce, "/tmp/file_decrypted.txt"))
+7 -8
View File
@@ -24,19 +24,18 @@ exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/C
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
#rm -rf $BUILD_DIR rm -rf $BUILD_DIR
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library' cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library'
cd $BUILD_DIR/build cd $BUILD_DIR/build
mv libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so libsimplex.so 2> /dev/null || true #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libsimplex.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-rpath '$ORIGIN' libsimplex.so
# GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it # GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it
# but libffi.so.8 is shipped as an external library with other libs # but libffi.so.8 is shipped as an external library with other libs
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libsimplex.so patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
mkdir deps 2> /dev/null || true mkdir deps 2> /dev/null || true
ldd libsimplex.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
cd - cd -
@@ -45,7 +44,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libsimplex.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
scripts/desktop/prepare-vlc-linux.sh scripts/desktop/prepare-vlc-linux.sh
links_dir=apps/multiplatform/build/links links_dir=apps/multiplatform/build/links
+3 -4
View File
@@ -14,7 +14,7 @@ else
fi fi
LIB_EXT=dylib LIB_EXT=dylib
LIB=libsimplex.$LIB_EXT LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
GHC_LIBS_DIR=$(ghc --print-libdir) GHC_LIBS_DIR=$(ghc --print-libdir)
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-* BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
@@ -24,10 +24,9 @@ for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
rm -rf $BUILD_DIR rm -rf $BUILD_DIR
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library'
cd $BUILD_DIR/build cd $BUILD_DIR/build
mv libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT libsimplex.dylib 2> /dev/null || true
mkdir deps 2> /dev/null || true mkdir deps 2> /dev/null || true
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available # It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
@@ -96,7 +95,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/$LIB apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
+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/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "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/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0";
"https://github.com/yesodweb/wai.git"."ec5e017d896a78e787a5acea62b37a4e677dec2e" = "1ckcpmpjfy9jiqrb52q20lj7ln4hmq9v2jk6kpkf3m68c1m9c2bx"; "https://github.com/yesodweb/wai.git"."ec5e017d896a78e787a5acea62b37a4e677dec2e" = "1ckcpmpjfy9jiqrb52q20lj7ln4hmq9v2jk6kpkf3m68c1m9c2bx";
"https://github.com/simplex-chat/wai.git"."2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0" = "199g4rjdf1zp1fcw8nqdsyr1h36hmg424qqx03071jk7j00z7ay4"; "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.Core
Simplex.Chat.Files Simplex.Chat.Files
Simplex.Chat.Help Simplex.Chat.Help
Simplex.Chat.Image
Simplex.Chat.Markdown Simplex.Chat.Markdown
Simplex.Chat.Messages Simplex.Chat.Messages
Simplex.Chat.Messages.Batch Simplex.Chat.Messages.Batch
@@ -205,7 +206,10 @@ library
StrictData 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 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: build-depends:
aeson ==2.2.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@@ -270,7 +274,10 @@ executable simplex-bot
StrictData 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 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: build-depends:
aeson ==2.2.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@@ -336,7 +343,10 @@ executable simplex-bot-advanced
StrictData 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 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: build-depends:
aeson ==2.2.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@@ -405,7 +415,10 @@ executable simplex-broadcast-bot
Paths_simplex_chat 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 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: build-depends:
aeson ==2.2.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@@ -472,7 +485,10 @@ executable simplex-chat
StrictData 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 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: build-depends:
aeson ==2.2.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@@ -545,7 +561,10 @@ executable simplex-directory-service
Paths_simplex_chat 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 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: build-depends:
aeson ==2.2.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@@ -618,6 +637,7 @@ test-suite simplex-chat-test
ChatTests.Profiles ChatTests.Profiles
ChatTests.Utils ChatTests.Utils
JSONTests JSONTests
LinkPreviewTests
MarkdownTests MarkdownTests
MessageBatching MessageBatching
MobileTests MobileTests
@@ -645,7 +665,10 @@ test-suite simplex-chat-test
StrictData 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 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: build-depends:
QuickCheck ==2.14.* JuicyPixels
, JuicyPixels-jpeg-turbo
, JuicyPixels-stbir
, QuickCheck ==2.14.*
, aeson ==2.2.* , aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
+1 -2
View File
@@ -92,9 +92,8 @@ import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle) import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
import qualified Simplex.Chat.Util as U import qualified Simplex.Chat.Util as U
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) 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 qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import qualified Simplex.FileTransfer.Transport as XFTP 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 Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Either (fromRight)
import Data.Functor (($>)) import Data.Functor (($>))
import Data.List (find) import Data.List (find)
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
@@ -29,13 +30,14 @@ import Data.Word (Word8)
import Database.SQLite.Simple (SQLError (..)) import Database.SQLite.Simple (SQLError (..))
import qualified Database.SQLite.Simple as DB import qualified Database.SQLite.Simple as DB
import Foreign.C.String import Foreign.C.String
import Foreign.C.Types (CInt (..)) import Foreign.C.Types (CBool (..), CInt (..), CLong (..))
import Foreign.Ptr import Foreign.Ptr
import Foreign.StablePtr import Foreign.StablePtr
import Foreign.Storable (poke) import Foreign.Storable (poke)
import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding) import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding)
import Simplex.Chat import Simplex.Chat
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Image (readResizeable, resizeImageToSize)
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.Shared
@@ -45,6 +47,7 @@ import Simplex.Chat.Remote.Types
import Simplex.Chat.Store import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (liftIOEither)
import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Client (agentClientStore)
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore) 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_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_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_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_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 -- | check / migrate database and initialize chat controller on success
cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
cChatMigrateInit fp key conf = cChatMigrateInitKey fp key 0 conf 0 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 :: CString -> IO CInt
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s 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 :: String -> ChatOpts
mobileChatOpts dbFilePrefix = mobileChatOpts dbFilePrefix =
ChatOpts ChatOpts
+29 -4
View File
@@ -7,6 +7,7 @@
module Simplex.Chat.Mobile.File module Simplex.Chat.Mobile.File
( cChatWriteFile, ( cChatWriteFile,
cChatWriteImage,
cChatReadFile, cChatReadFile,
cChatEncryptFile, cChatEncryptFile,
cChatDecryptFile, cChatDecryptFile,
@@ -35,6 +36,8 @@ import Foreign.Ptr
import Foreign.StablePtr import Foreign.StablePtr
import Foreign.Storable (poke, pokeByteOff) import Foreign.Storable (poke, pokeByteOff)
import Simplex.Chat.Controller (ChatController (..)) 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.Mobile.Shared
import Simplex.Chat.Util (chunkSize, encryptFile) import Simplex.Chat.Util (chunkSize, encryptFile)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..))
@@ -45,7 +48,7 @@ import Simplex.Messaging.Util (catchAll)
import UnliftIO (Handle, IOMode (..), atomically, withFile) import UnliftIO (Handle, IOMode (..), atomically, withFile)
data WriteFileResult data WriteFileResult
= WFResult {cryptoArgs :: CryptoFileArgs} = WFResult {cryptoArgs :: Maybe CryptoFileArgs}
| WFError {writeError :: String} | WFError {writeError :: String}
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult) $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
@@ -61,10 +64,32 @@ cChatWriteFile cc cPath ptr len = do
chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult
chatWriteFile ChatController {random} path s = do chatWriteFile ChatController {random} path s = do
cfArgs <- atomically $ CF.randomArgs random cfArgs <- atomically $ CF.randomArgs random
let file = CryptoFile path $ Just cfArgs chatWriteFile_ (Just cfArgs) path s
either WFError (\_ -> WFResult cfArgs)
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) <$> 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 data ReadFileResult
= RFResult {fileSize :: Int} = RFResult {fileSize :: Int}
| RFError {readError :: String} | RFError {readError :: String}
@@ -106,7 +131,7 @@ chatEncryptFile ChatController {random} fromPath toPath =
encrypt = do encrypt = do
cfArgs <- atomically $ CF.randomArgs random cfArgs <- atomically $ CF.randomArgs random
encryptFile fromPath toPath cfArgs encryptFile fromPath toPath cfArgs
pure cfArgs pure $ Just cfArgs
cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
cChatDecryptFile cFromPath cKey cNonce cToPath = do cChatDecryptFile cFromPath cKey cNonce cToPath = do
+1 -1
View File
@@ -620,7 +620,7 @@ testXFTPFileTransferEncrypted =
createDirectoryIfMissing True "./tests/tmp/alice/" createDirectoryIfMissing True "./tests/tmp/alice/"
createDirectoryIfMissing True "./tests/tmp/bob/" createDirectoryIfMissing True "./tests/tmp/bob/"
WFResult cfArgs <- chatWriteFile (chatController alice) srcPath src 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 withXFTPServer $ do
connectUsers alice bob connectUsers alice bob
alice ##> ("/_send @2 json [{\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}]") 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 ptr <- mallocBytes $ B.length src
putByteString ptr src putByteString ptr src
r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r Just (WFResult cfArgs@(Just (CFArgs key nonce))) <- jDecode r
let encryptedFile = CryptoFile path $ Just cfArgs let encryptedFile = CryptoFile path cfArgs
CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src) CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key cKey <- encodedCString key
cNonce <- encodedCString nonce cNonce <- encodedCString nonce
@@ -318,8 +318,8 @@ testFileEncryptionCApi fileName tmp = do
let toPath = tmp </> (fileName <> ".encrypted.pdf") let toPath = tmp </> (fileName <> ".encrypted.pdf")
cToPath <- newCString toPath cToPath <- newCString toPath
r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r Just (WFResult cfArgs@(Just (CFArgs key nonce))) <- jDecode r
CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src) CF.getFileContentsSize (CryptoFile toPath cfArgs) `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key cKey <- encodedCString key
cNonce <- encodedCString nonce cNonce <- encodedCString nonce
let toPath' = tmp </> (fileName <> ".decrypted.pdf") let toPath' = tmp </> (fileName <> ".decrypted.pdf")
+3 -1
View File
@@ -6,6 +6,7 @@ import ChatTests.Utils (xdescribe'')
import Control.Logger.Simple import Control.Logger.Simple
import Data.Time.Clock.System import Data.Time.Clock.System
import JSONTests import JSONTests
import LinkPreviewTests
import MarkdownTests import MarkdownTests
import MessageBatching import MessageBatching
import MobileTests import MobileTests
@@ -22,7 +23,7 @@ import WebRTCTests
main :: IO () main :: IO ()
main = do main = do
setLogLevel LogError setLogLevel LogDebug
withGlobalLogging logCfg . hspec $ do withGlobalLogging logCfg . hspec $ do
describe "Schema dump" schemaDumpTest describe "Schema dump" schemaDumpTest
describe "SimpleX chat markdown" markdownTests describe "SimpleX chat markdown" markdownTests
@@ -40,6 +41,7 @@ main = do
xdescribe'' "SimpleX Broadcast bot" broadcastBotTests xdescribe'' "SimpleX Broadcast bot" broadcastBotTests
xdescribe'' "SimpleX Directory service bot" directoryServiceTests xdescribe'' "SimpleX Directory service bot" directoryServiceTests
describe "Remote session" remoteTests describe "Remote session" remoteTests
around testBracket $ describe "Link preview" linkPreviewTests
where where
testBracket test = withSmpServer $ tmpBracket test testBracket test = withSmpServer $ tmpBracket test
tmpBracket test = do tmpBracket test = do
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB