mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c4794fe33 | |||
| f179db9545 | |||
| 52160f2602 | |||
| 2a99a5235b | |||
| 4fe7b674e6 | |||
| a5e7fa2f9f |
@@ -150,13 +150,9 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
|
||||
run: sudo apt install -y desktop-file-utils
|
||||
|
||||
- name: Install 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
|
||||
- name: Install openssl for Mac
|
||||
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
|
||||
run: brew install openssl@3.0 pkg-config jpeg-turbo
|
||||
run: brew install openssl@3.0
|
||||
|
||||
- name: Unix prepare cabal.project.local for Ubuntu
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
|
||||
@@ -326,7 +322,6 @@ jobs:
|
||||
git
|
||||
perl
|
||||
make
|
||||
mingw-w64-x86_64-libjpeg-turbo
|
||||
pacboy: >-
|
||||
toolchain:p
|
||||
cmake:p
|
||||
|
||||
@@ -79,3 +79,6 @@ website/package-lock.json
|
||||
website/.cache
|
||||
website/test/stubs-layout-cache/_includes/*.js
|
||||
apps/android/app/release
|
||||
packages/simplex-chat-nodejs/build
|
||||
packages/simplex-chat-nodejs/.vscode
|
||||
packages/simplex-chat-nodejs/libs
|
||||
|
||||
@@ -440,7 +440,6 @@ struct ChatView: View {
|
||||
maxWidth: maxWidth,
|
||||
composeState: $composeState,
|
||||
selectedMember: $selectedMember,
|
||||
showChatInfoSheet: $showChatInfoSheet,
|
||||
revealedChatItem: $revealedChatItem,
|
||||
selectedChatItems: $selectedChatItems,
|
||||
forwardedChatItems: $forwardedChatItems
|
||||
@@ -894,14 +893,12 @@ struct ChatView: View {
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
|
||||
@Binding @ObservedObject var chat: Chat
|
||||
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
|
||||
let chatItem: ChatItem
|
||||
let maxWidth: CGFloat
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var selectedMember: GMember?
|
||||
@Binding var showChatInfoSheet: Bool
|
||||
@Binding var revealedChatItem: ChatItem?
|
||||
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@@ -1258,22 +1255,16 @@ struct ChatView: View {
|
||||
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
|
||||
}
|
||||
}
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo):
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
v.contextMenu {
|
||||
ReactionContextMenu(
|
||||
groupInfo: groupInfo,
|
||||
itemId: ci.id,
|
||||
reactionCount: r,
|
||||
selectedMember: $selectedMember,
|
||||
profileRadius: profileRadius
|
||||
selectedMember: $selectedMember
|
||||
)
|
||||
}
|
||||
case let .direct(contact):
|
||||
v.contextMenu {
|
||||
contactReactionMenu(contact, r)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
@@ -1776,20 +1767,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View {
|
||||
if !r.userReacted || r.totalReacted > 1 {
|
||||
Button { showChatInfoSheet = true } label: {
|
||||
profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius)
|
||||
}
|
||||
}
|
||||
if r.userReacted {
|
||||
Button {} label: {
|
||||
profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius)
|
||||
}
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SelectedChatItem: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@@ -1882,12 +1859,13 @@ struct ReactionContextMenu: View {
|
||||
var itemId: Int64
|
||||
var reactionCount: CIReactionCount
|
||||
@Binding var selectedMember: GMember?
|
||||
var profileRadius: CGFloat
|
||||
@State private var memberReactions: [MemberReaction] = []
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||
|
||||
var body: some View {
|
||||
groupMemberReactionList()
|
||||
.task {
|
||||
logger.debug("ReactionContextMenu task \(radius)")
|
||||
await loadChatItemReaction()
|
||||
}
|
||||
}
|
||||
@@ -1911,12 +1889,27 @@ struct ReactionContextMenu: View {
|
||||
selectedMember = member
|
||||
}
|
||||
} label: {
|
||||
profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius)
|
||||
HStack {
|
||||
Text(mem.displayName)
|
||||
if let img = cropImage(mem.image) {
|
||||
Image(uiImage: img)
|
||||
} else {
|
||||
Image(systemName: "person.crop.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(userMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cropImage(_ img: String?) -> UIImage? {
|
||||
return if let originalImage = imageFromBase64(img) {
|
||||
maskToCustomShape(originalImage, size: 30, radius: radius)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private func loadChatItemReaction() async {
|
||||
do {
|
||||
@@ -1934,17 +1927,6 @@ struct ReactionContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View {
|
||||
HStack {
|
||||
nameText
|
||||
if let image, let img = imageFromBase64(image) {
|
||||
Image(uiImage: maskToCustomShape(img, size: 30, radius: radius))
|
||||
} else {
|
||||
Image(systemName: "person.crop.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
|
||||
let path = Path { path in
|
||||
if radius >= 50 {
|
||||
|
||||
@@ -1931,7 +1931,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1956,7 +1956,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1980,7 +1980,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2005,7 +2005,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2021,11 +2021,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2041,11 +2041,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2066,7 +2066,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2081,7 +2081,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2103,7 +2103,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2118,7 +2118,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2140,7 +2140,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2166,7 +2166,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2191,7 +2191,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2217,7 +2217,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2242,7 +2242,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2257,7 +2257,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2276,7 +2276,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 254;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2291,7 +2291,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -25,18 +25,6 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
|
||||
}
|
||||
}
|
||||
|
||||
public func writeCryptoImage(maxSize: Int, path: String, data: Data, encrypted: Bool) throws -> CryptoFileArgs {
|
||||
let ptr: UnsafeMutableRawPointer = malloc(data.count)
|
||||
memcpy(ptr, (data as NSData).bytes, data.count)
|
||||
var cPath = path.cString(using: .utf8)!
|
||||
let cjson = chat_write_image(getChatCtrl(), maxSize, &cPath, ptr, Int32(data.count), encrypted)!
|
||||
let d = fromCString(cjson).data(using: .utf8)!
|
||||
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
|
||||
case let .result(cfArgs): return cfArgs
|
||||
case let .error(err): throw RuntimeError(err)
|
||||
}
|
||||
}
|
||||
|
||||
public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data {
|
||||
var cPath = path.cString(using: .utf8)!
|
||||
var cKey = cryptoArgs.fileKey.cString(using: .utf8)!
|
||||
|
||||
@@ -223,17 +223,6 @@ public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> Crypt
|
||||
}
|
||||
}
|
||||
|
||||
public func saveImage(_ data: Data, _ fileName: String, maxSize: Long, encrypted: Bool) -> CryptoFile? {
|
||||
let filePath = getAppFilePath(fileName)
|
||||
do {
|
||||
let cfArgs = try writeCryptoImage(maxSize: maxSize, path: filePath.path, data: data, encrypted: encrypted)
|
||||
return CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
|
||||
} catch {
|
||||
logger.error("FileUtils.saveImage error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func removeFile(_ url: URL) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: url.path)
|
||||
|
||||
@@ -101,27 +101,19 @@ public func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha
|
||||
}
|
||||
|
||||
public func resizeImageToStrSizeSync(_ image: UIImage, maxDataSize: Int64) -> String? {
|
||||
// XXX: only needed when the original encoding isn't available
|
||||
let tmpFile = generateNewFileName(getTempFilesDirectory().path + "/" + "resize", "png", fullPath: true)
|
||||
// encode as png and let the backend deal with alpha and formats
|
||||
guard let d = image.pngData() else { return nil }
|
||||
if let _ = saveFile(d, tmpFile, encrypted: false) {
|
||||
defer { removeFile(tmpFile) }
|
||||
let ptr = chat_resize_image_to_str_size(tmpFile, maxDataSize)
|
||||
if let ptr = ptr {
|
||||
let str = fromCString(ptr)
|
||||
let dataSize = str.count
|
||||
if dataSize <= 0 { return nil }
|
||||
logger.debug("resizeImageToStrSize final \(dataSize)")
|
||||
return str
|
||||
} else {
|
||||
logger.error("resizeImageToStrSize failed")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.error("saveFile failed")
|
||||
return nil
|
||||
var img = image
|
||||
let hasAlpha = imageHasAlpha(image)
|
||||
var str = compressImageStr(img, hasAlpha: hasAlpha)
|
||||
var dataSize = str?.count ?? 0
|
||||
while dataSize != 0 && dataSize > maxDataSize {
|
||||
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
||||
let clippedRatio = min(ratio, 2.0)
|
||||
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha)
|
||||
str = compressImageStr(img, hasAlpha: hasAlpha)
|
||||
dataSize = str?.count ?? 0
|
||||
}
|
||||
logger.debug("resizeImageToStrSize final \(dataSize)")
|
||||
return str
|
||||
}
|
||||
|
||||
public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) async -> String? {
|
||||
@@ -319,7 +311,7 @@ private func getTimestamp() -> String {
|
||||
}
|
||||
|
||||
public func dropImagePrefix(_ s: String) -> String {
|
||||
dropPrefix(dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,"), "data:image/jpeg;base64,")
|
||||
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
|
||||
}
|
||||
|
||||
private func dropPrefix(_ s: String, _ prefix: String) -> String {
|
||||
|
||||
@@ -29,14 +29,10 @@ extern char *chat_valid_name(char *name);
|
||||
extern int chat_json_length(char *str);
|
||||
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
|
||||
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
||||
extern char *chat_resize_image_to_str_size(const char *path, long maxSize);
|
||||
|
||||
// chat_write_file returns null-terminated string with JSON of WriteFileResult
|
||||
extern char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len);
|
||||
|
||||
// chat_write_image returns null-terminated string with JSON of WriteFileResult
|
||||
extern char *chat_write_image(chat_ctrl ctl, long maxSize, char *path, char *data, int len, bool encrypted);
|
||||
|
||||
// chat_read_file returns a buffer with:
|
||||
// result status (1 byte), then if
|
||||
// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length.
|
||||
|
||||
+29
-32
@@ -14,12 +14,8 @@ import boofcv.android.ConvertBitmap
|
||||
import boofcv.struct.image.GrayU8
|
||||
import chat.simplex.common.R
|
||||
import chat.simplex.common.views.helpers.errorBitmap
|
||||
import chat.simplex.common.views.helpers.generateNewFileName
|
||||
import chat.simplex.common.views.helpers.getFileName
|
||||
import chat.simplex.common.views.helpers.removeFile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URI
|
||||
import kotlin.math.min
|
||||
@@ -29,25 +25,26 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
|
||||
val imageString = base64ImageString
|
||||
.removePrefix("data:image/png;base64,")
|
||||
.removePrefix("data:image/jpg;base64,")
|
||||
.removePrefix("data:image/jpeg;base64,")
|
||||
return try {
|
||||
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "base64ToBitmap error: $e for input '$base64ImageString' -> '$imageString'")
|
||||
Log.e(TAG, "base64ToBitmap error: $e")
|
||||
errorBitmap.asImageBitmap()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
|
||||
val tmpFileName = generateNewFileName("IMG", "png", tmpDir)
|
||||
val tmpFile = File(tmpDir, tmpFileName)
|
||||
val output = FileOutputStream(tmpFile)
|
||||
compressImageData(image, true).writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
var str = chatResizeImageToStrSize(tmpFile.absolutePath, maxDataSize)
|
||||
removeFile(tmpFileName)
|
||||
var img = image
|
||||
var str = compressImageStr(img)
|
||||
while (str.length > maxDataSize) {
|
||||
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble())
|
||||
val clippedRatio = min(ratio, 2.0)
|
||||
val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
val height = img.height * width / img.width
|
||||
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
|
||||
str = compressImageStr(img)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
@@ -74,11 +71,11 @@ fun Bitmap.clipToCircle(): Bitmap {
|
||||
return circle
|
||||
}
|
||||
|
||||
// actual fun compressImageStr(bitmap: ImageBitmap): String {
|
||||
// val usePng = bitmap.hasAlpha()
|
||||
// val ext = if (usePng) "png" else "jpg"
|
||||
// return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
|
||||
// }
|
||||
actual fun compressImageStr(bitmap: ImageBitmap): String {
|
||||
val usePng = bitmap.hasAlpha()
|
||||
val ext = if (usePng) "png" else "jpg"
|
||||
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
|
||||
val stream = ByteArrayOutputStream()
|
||||
@@ -86,19 +83,19 @@ actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut
|
||||
return stream
|
||||
}
|
||||
|
||||
// actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
|
||||
// var img = image
|
||||
// var stream = compressImageData(img, usePng)
|
||||
// while (stream.size() > maxDataSize) {
|
||||
// val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
|
||||
// val clippedRatio = min(ratio, 2.0)
|
||||
// val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
// val height = img.height * width / img.width
|
||||
// img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
|
||||
// stream = compressImageData(img, usePng)
|
||||
// }
|
||||
// return stream
|
||||
// }
|
||||
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
|
||||
var img = image
|
||||
var stream = compressImageData(img, usePng)
|
||||
while (stream.size() > maxDataSize) {
|
||||
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
|
||||
val clippedRatio = min(ratio, 2.0)
|
||||
val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
val height = img.height * width / img.width
|
||||
img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap()
|
||||
stream = compressImageData(img, usePng)
|
||||
}
|
||||
return stream
|
||||
}
|
||||
|
||||
actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap()
|
||||
|
||||
|
||||
+4
-5
@@ -1,11 +1,10 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import android.util.Log
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
|
||||
actual object Log {
|
||||
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) }
|
||||
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) }
|
||||
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) }
|
||||
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) }
|
||||
actual fun d(tag: String, text: String) = Log.d(tag, text).run{}
|
||||
actual fun e(tag: String, text: String) = Log.e(tag, text).run{}
|
||||
actual fun i(tag: String, text: String) = Log.i(tag, text).run{}
|
||||
actual fun w(tag: String, text: String) = Log.w(tag, text).run{}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
//#include <stdlib.h>
|
||||
//#include <android/log.h>
|
||||
|
||||
// from the RTS
|
||||
@@ -69,31 +68,9 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
extern char *chat_valid_name(const char *name);
|
||||
extern int chat_json_length(const char *str);
|
||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length, bool encrypt);
|
||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
||||
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
|
||||
extern char *chat_resize_image_to_str_size(const char *from_path, long max_size);
|
||||
|
||||
char * encode_to_utf8_chars(JNIEnv *env, jstring string) {
|
||||
if (!string) return "";
|
||||
|
||||
const jclass cls_string = (*env)->FindClass(env, "java/lang/String");
|
||||
const jmethodID mid_getBytes = (*env)->GetMethodID(env, cls_string, "getBytes", "(Ljava/lang/String;)[B");
|
||||
const jbyteArray jbyte_array = (jbyteArray) (*env)->CallObjectMethod(env, string, mid_getBytes, (*env)->NewStringUTF(env, "UTF-8"));
|
||||
jint length = (jint) (*env)->GetArrayLength(env, jbyte_array);
|
||||
jbyte *jbytes = malloc(length + 1);
|
||||
(*env)->GetByteArrayRegion(env, jbyte_array, 0, length, jbytes);
|
||||
// char * should be null terminated but jbyte * isn't. Terminate it with \0. Otherwise, Haskell will not see the end of string
|
||||
jbytes[length] = '\0';
|
||||
|
||||
//for (int i = 0; i < length; ++i)
|
||||
// fprintf(stderr, "%d: %02x\n", i, jbytes[i]);
|
||||
|
||||
(*env)->DeleteLocalRef(env, jbyte_array);
|
||||
(*env)->DeleteLocalRef(env, cls_string);
|
||||
return (char *) jbytes;
|
||||
}
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
|
||||
@@ -205,16 +182,6 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer, jboolean encrypt) {
|
||||
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
|
||||
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
|
||||
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity, encrypt));
|
||||
(*env)->ReleaseStringUTFChars(env, path, _path);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) {
|
||||
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
|
||||
@@ -277,11 +244,3 @@ Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass cla
|
||||
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatResizeImageToStrSize(JNIEnv *env, jclass clazz, jstring from_path, jlong max_size) {
|
||||
const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_resize_image_to_str_size(_from_path, max_size));
|
||||
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -54,11 +54,10 @@ add_library( # Sets the name of the library.
|
||||
simplex-api.c)
|
||||
|
||||
add_library( simplex SHARED IMPORTED )
|
||||
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libsimplex.${OS_LIB_EXT})
|
||||
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})
|
||||
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})
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -42,11 +41,9 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
extern char *chat_valid_name(const char *name);
|
||||
extern int chat_json_length(const char *str);
|
||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||
extern char *chat_write_image(chat_ctrl ctrl, long max_size, const char *path, char *ptr, int length, bool encrypt);
|
||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
||||
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
|
||||
extern char *chat_resize_image_to_str_size(const char *from_path, long max_size);
|
||||
|
||||
// As a reference: https://stackoverflow.com/a/60002045
|
||||
jstring decode_to_utf8_string(JNIEnv *env, char *string) {
|
||||
@@ -195,16 +192,6 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatWriteImage(JNIEnv *env, jclass clazz, jlong controller, jlong maxSize, jstring path, jobject buffer, jboolean encrypt) {
|
||||
const char *_path = encode_to_utf8_chars(env, path);
|
||||
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
|
||||
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
jstring res = decode_to_utf8_string(env, chat_write_image((void*)controller, maxSize, _path, buff, capacity, encrypt));
|
||||
(*env)->ReleaseStringUTFChars(env, path, _path);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) {
|
||||
const char *_path = encode_to_utf8_chars(env, path);
|
||||
@@ -267,11 +254,3 @@ Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass cla
|
||||
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatResizeImageToStrSize(JNIEnv *env, jclass clazz, jstring from_path, jlong max_size) {
|
||||
const char *_from_path = encode_to_utf8_chars(env, from_path);
|
||||
jstring res = decode_to_utf8_string(env, chat_resize_image_to_str_size(_from_path, max_size));
|
||||
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
|
||||
return res;
|
||||
}
|
||||
|
||||
-14
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.common.model
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlinx.serialization.*
|
||||
import java.nio.ByteBuffer
|
||||
@@ -33,19 +32,6 @@ fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
|
||||
}
|
||||
}
|
||||
|
||||
fun writeCryptoImage(maxSize: Long, image: ImageBitmap, path: String, encrypt: Boolean): CryptoFileArgs {
|
||||
val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
|
||||
val data = compressImageData(image, image.hasAlpha).toByteArray()
|
||||
val buffer = ByteBuffer.allocateDirect(data.size)
|
||||
buffer.put(data)
|
||||
buffer.rewind()
|
||||
val str = chatWriteImage(ctrl, maxSize, path, buffer, encrypt)
|
||||
return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) {
|
||||
is WriteFileResult.Result -> d.cryptoArgs
|
||||
is WriteFileResult.Error -> throw Exception(d.writeError)
|
||||
}
|
||||
}
|
||||
|
||||
fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
|
||||
val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)
|
||||
val status = (res[0] as Integer).toInt()
|
||||
|
||||
-2
@@ -132,7 +132,6 @@ class AppPreferences {
|
||||
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
|
||||
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
|
||||
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
|
||||
val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } }
|
||||
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
|
||||
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
|
||||
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
|
||||
@@ -394,7 +393,6 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
|
||||
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
|
||||
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
|
||||
private const val SHARED_PREFS_LOG_LEVEL = "LogLevel"
|
||||
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
|
||||
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
|
||||
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
|
||||
|
||||
@@ -32,11 +32,9 @@ external fun chatPasswordHash(pwd: String, salt: String): String
|
||||
external fun chatValidName(name: String): String
|
||||
external fun chatJsonLength(str: String): Int
|
||||
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
|
||||
external fun chatWriteImage(ctrl: ChatCtrl, maxSize: Long, path: String, buffer: ByteBuffer, encrypt: Boolean): String
|
||||
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
|
||||
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
|
||||
external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String
|
||||
external fun chatResizeImageToStrSize(fromPath: String, maxSize: Long): String
|
||||
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
|
||||
+2
-3
@@ -7,11 +7,10 @@ import java.io.InputStream
|
||||
import java.net.URI
|
||||
|
||||
expect fun base64ToBitmap(base64ImageString: String): ImageBitmap
|
||||
// XXX: Not a part of platform services anymore?
|
||||
expect fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String
|
||||
// expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
|
||||
expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream
|
||||
expect fun cropToSquare(image: ImageBitmap): ImageBitmap
|
||||
// expect fun compressImageStr(bitmap: ImageBitmap): String
|
||||
expect fun compressImageStr(bitmap: ImageBitmap): String
|
||||
expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream
|
||||
|
||||
expect fun GrayU8.toImageBitmap(): ImageBitmap
|
||||
|
||||
@@ -2,10 +2,6 @@ package chat.simplex.common.platform
|
||||
|
||||
const val TAG = "SIMPLEX"
|
||||
|
||||
enum class LogLevel {
|
||||
DEBUG, INFO, WARNING, ERROR
|
||||
}
|
||||
|
||||
expect object Log {
|
||||
fun d(tag: String, text: String)
|
||||
fun e(tag: String, text: String)
|
||||
|
||||
+5
-8
@@ -296,7 +296,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +309,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
QuotedMsgView(qi)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +324,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
ForwardedFromView(forwardedFromItem)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +395,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,11 +433,12 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
|
||||
Column {
|
||||
if (numTabs() > 1) {
|
||||
Box(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (val sel = selection.value) {
|
||||
is CIInfoTab.Delivery -> {
|
||||
DeliveryTab(sel.memberDeliveryStatuses)
|
||||
@@ -482,7 +479,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
|
||||
Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
|
||||
TabRow(
|
||||
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
|
||||
Modifier.height(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
|
||||
+8
-14
@@ -665,18 +665,13 @@ fun ChatLayout(
|
||||
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
|
||||
if (chatInfo != null) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
// disables scrolling to top of chat item on click inside the bubble
|
||||
CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec {
|
||||
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
|
||||
}) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||
)
|
||||
}
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
@@ -944,7 +939,6 @@ fun BoxScope.ChatItemsList(
|
||||
linkMode: SimplexLinkMode,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
showChatInfo: () -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
@@ -1072,7 +1066,7 @@ fun BoxScope.ChatItemsList(
|
||||
highlightedItems.value = setOf()
|
||||
}
|
||||
}
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, 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, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-49
@@ -24,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.currentUser
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.*
|
||||
|
||||
@@ -51,12 +51,6 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString =
|
||||
withStyle(chatEventStyle) { append("$eventText $ts") }
|
||||
}
|
||||
|
||||
data class ChatItemReactionMenuItem (
|
||||
val name: String,
|
||||
val image: String?,
|
||||
val onClick: (() -> Unit)?
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(
|
||||
rhId: Long?,
|
||||
@@ -93,7 +87,6 @@ fun ChatItemView(
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
reveal: (Boolean) -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
showChatInfo: () -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
@@ -127,7 +120,7 @@ fun ChatItemView(
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
|
||||
cItem.reactions.forEach { r ->
|
||||
val showReactionMenu = remember { mutableStateOf(false) }
|
||||
val reactionMenuItems = remember { mutableStateOf(emptyList<ChatItemReactionMenuItem>()) }
|
||||
val reactionMembers = remember { mutableStateOf(emptyList<MemberReaction>()) }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val enterInteraction = remember { HoverInteraction.Enter() }
|
||||
KeyChangeEffect(highlighted.value) {
|
||||
@@ -141,39 +134,18 @@ fun ChatItemView(
|
||||
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
|
||||
if (cInfo.featureEnabled(ChatFeature.Reactions)) {
|
||||
fun showReactionsMenu() {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
withBGApi {
|
||||
try {
|
||||
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
|
||||
if (members != null) {
|
||||
showReactionMenu.value = true
|
||||
reactionMenuItems.value = members.map {
|
||||
val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId
|
||||
val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null
|
||||
ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
|
||||
if (cInfo is ChatInfo.Group) {
|
||||
withBGApi {
|
||||
try {
|
||||
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
|
||||
if (members != null) {
|
||||
showReactionMenu.value = true
|
||||
reactionMembers.value = members
|
||||
}
|
||||
} 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
|
||||
@@ -194,19 +166,19 @@ fun ChatItemView(
|
||||
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
ReactionIcon(r.reaction.text, fontSize = 12.sp)
|
||||
DefaultDropdownMenu(showMenu = showReactionMenu) {
|
||||
reactionMenuItems.value.forEach { m ->
|
||||
reactionMembers.value.forEach { m ->
|
||||
ItemAction(
|
||||
text = m.name,
|
||||
composable = { ProfileImage(44.dp, m.image) },
|
||||
text = m.groupMember.displayName,
|
||||
composable = { ProfileImage(44.dp, m.groupMember.image) },
|
||||
onClick = {
|
||||
val click = m.onClick
|
||||
if (click != null) {
|
||||
click()
|
||||
if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) {
|
||||
showMemberInfo(cInfo.groupInfo, m.groupMember)
|
||||
showReactionMenu.value = false
|
||||
} else {
|
||||
showReactionMenu.value = false
|
||||
}
|
||||
},
|
||||
lineLimit = 1,
|
||||
color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor
|
||||
lineLimit = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1216,7 +1188,6 @@ fun PreviewChatItemView(
|
||||
showItemDetails = { _, _ -> },
|
||||
reveal = {},
|
||||
showMemberInfo = { _, _ ->},
|
||||
showChatInfo = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showTimestamp = true,
|
||||
@@ -1262,7 +1233,6 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
showItemDetails = { _, _ -> },
|
||||
reveal = {},
|
||||
showMemberInfo = { _, _ ->},
|
||||
showChatInfo = {},
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
preview = true,
|
||||
|
||||
+7
-14
@@ -30,7 +30,6 @@ import kotlinx.datetime.*
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
@@ -45,14 +44,11 @@ fun DatabaseView() {
|
||||
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
|
||||
val stopped = remember { m.chatRunning }.value == false
|
||||
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
|
||||
val archive = chatArchiveFile.value
|
||||
if (archive != null && to != null) {
|
||||
copyFileToFile(File(archive), to) {}
|
||||
}
|
||||
// delete no matter the database was exported or canceled the export process
|
||||
if (archive != null) {
|
||||
File(archive).delete()
|
||||
chatArchiveFile.value = null
|
||||
val file = chatArchiveFile.value
|
||||
if (file != null && to != null) {
|
||||
copyFileToFile(File(file), to) {
|
||||
chatArchiveFile.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
|
||||
@@ -684,8 +680,6 @@ suspend fun importArchive(
|
||||
} finally {
|
||||
File(archivePath).delete()
|
||||
}
|
||||
} else {
|
||||
progressIndicator.value = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -697,15 +691,14 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
|
||||
if (inputStream != null && archiveName != null) {
|
||||
val archivePath = "$databaseExportDir${File.separator}$archiveName"
|
||||
val destFile = File(archivePath)
|
||||
Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
Files.copy(inputStream, destFile.toPath())
|
||||
archivePath
|
||||
} else {
|
||||
Log.e(TAG, "saveArchiveFromURI null inputStream")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString())
|
||||
Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}")
|
||||
Log.e(TAG, "saveArchiveFromURI error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
+26
-16
@@ -169,19 +169,24 @@ fun saveImage(image: ImageBitmap): CryptoFile? {
|
||||
return try {
|
||||
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath("")))
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
try {
|
||||
val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, encrypted)
|
||||
if (encrypted) {
|
||||
if (encrypted) {
|
||||
try {
|
||||
val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray())
|
||||
CryptoFile(destFileName, args)
|
||||
} else {
|
||||
CryptoFile.plain(destFileName)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
|
||||
null
|
||||
} else {
|
||||
val output = FileOutputStream(destFile)
|
||||
dataResized.writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
CryptoFile.plain(destFileName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}")
|
||||
@@ -193,10 +198,14 @@ fun desktopSaveImageInTmp(uri: URI): CryptoFile? {
|
||||
val image = getBitmapFromUri(uri) ?: return null
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val destFileName = generateNewFileName("IMG", ext, tmpDir)
|
||||
val destFile = File(tmpDir, destFileName)
|
||||
val args = writeCryptoImage(MAX_IMAGE_SIZE, image, destFile.absolutePath, false)
|
||||
CryptoFile(destFileName, args)
|
||||
val output = FileOutputStream(destFile)
|
||||
dataResized.writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
CryptoFile.plain(destFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt desktopSaveImageInTmp error: ${e.stackTraceToString()}")
|
||||
null
|
||||
@@ -292,7 +301,11 @@ fun saveWallpaperFile(uri: URI): String? {
|
||||
fun saveWallpaperFile(image: ImageBitmap): String {
|
||||
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||
val destFile = File(getWallpaperFilePath(destFileName))
|
||||
writeCryptoImage(5_000_000, image, destFile.absolutePath, false)
|
||||
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
|
||||
val output = FileOutputStream(destFile)
|
||||
dataResized.use {
|
||||
it.writeTo(output)
|
||||
}
|
||||
return destFile.name
|
||||
}
|
||||
|
||||
@@ -352,10 +365,7 @@ fun formatBytes(bytes: Long): String {
|
||||
}
|
||||
|
||||
fun removeFile(fileName: String): Boolean {
|
||||
return removeFile(File(getAppFilePath(fileName)))
|
||||
}
|
||||
|
||||
fun removeFile(file: File): Boolean {
|
||||
val file = File(getAppFilePath(fileName))
|
||||
val fileDeleted = file.delete()
|
||||
if (!fileDeleted) {
|
||||
Log.e(TAG, "Util.kt removeFile error")
|
||||
|
||||
-7
@@ -14,7 +14,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -45,12 +44,6 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
|
||||
if (devTools.value) {
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) {
|
||||
DefaultSwitch(
|
||||
checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG,
|
||||
onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) }
|
||||
)
|
||||
}
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
|
||||
if (appPlatform.isDesktop) {
|
||||
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
|
||||
|
||||
@@ -907,7 +907,6 @@
|
||||
<string name="show_dev_options">Show:</string>
|
||||
<string name="hide_dev_options">Hide:</string>
|
||||
<string name="show_developer_options">Show developer options</string>
|
||||
<string name="debug_logs">Enable logs</string>
|
||||
<string name="developer_options">Database IDs and Transport isolation option.</string>
|
||||
<string name="developer_options_section">Developer options</string>
|
||||
<string name="show_internal_errors">Show internal errors</string>
|
||||
@@ -1339,7 +1338,6 @@
|
||||
<string name="chat_database_exported_migrate">You may migrate the exported database.</string>
|
||||
<string name="chat_database_exported_not_all_files">Some file(s) were not exported</string>
|
||||
<string name="chat_database_exported_continue">Continue</string>
|
||||
<string name="error_saving_database">Error saving database</string>
|
||||
|
||||
<!-- DatabaseEncryptionView.kt -->
|
||||
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 592 B |
+3
-4
@@ -67,7 +67,7 @@ object NtfManager {
|
||||
ntf.second.close()
|
||||
} catch (e: Exception) {
|
||||
// Can be java.lang.UnsupportedOperationException, for example. May do nothing
|
||||
Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}")
|
||||
println("Failed to close notification: ${e.stackTraceToString()}")
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,7 @@ object NtfManager {
|
||||
}
|
||||
|
||||
fun cancelAllNotifications() {
|
||||
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e
|
||||
// .stackTraceToString()}") } }
|
||||
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } }
|
||||
withBGApi {
|
||||
prevNtfsMutex.withLock {
|
||||
prevNtfs.clear()
|
||||
@@ -154,7 +153,7 @@ object NtfManager {
|
||||
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
|
||||
newFile.absolutePath
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
|
||||
println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
} else null
|
||||
|
||||
+36
-38
@@ -5,8 +5,6 @@ import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import boofcv.io.image.ConvertBufferedImage
|
||||
import boofcv.struct.image.GrayU8
|
||||
import chat.simplex.common.views.helpers.generateNewFileName
|
||||
import chat.simplex.common.views.helpers.removeFile
|
||||
import chat.simplex.res.MR
|
||||
import org.jetbrains.skia.Image
|
||||
import java.awt.RenderingHints
|
||||
@@ -27,40 +25,40 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
|
||||
val imageString = base64ImageString
|
||||
.removePrefix("data:image/png;base64,")
|
||||
.removePrefix("data:image/jpg;base64,")
|
||||
.removePrefix("data:image/jpeg;base64,")
|
||||
return try {
|
||||
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap()
|
||||
} catch (e: Exception) { // ByteArrayInputStream returns null
|
||||
Log.e(TAG, "base64ToBitmap error: $e for input '$base64ImageString' -> '$imageString'")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "base64ToBitmap error: $e")
|
||||
errorBitmap()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String {
|
||||
val tmpFileName = generateNewFileName("IMG", "png", tmpDir)
|
||||
val tmpFile = File(tmpDir, tmpFileName)
|
||||
val output = FileOutputStream(tmpFile)
|
||||
compressImageData(image, true).writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
var str = chatResizeImageToStrSize(tmpFile.absolutePath, maxDataSize)
|
||||
removeFile(tmpFile)
|
||||
var img = image
|
||||
var str = compressImageStr(img)
|
||||
while (str.length > maxDataSize) {
|
||||
val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble())
|
||||
val clippedRatio = kotlin.math.min(ratio, 2.0)
|
||||
val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
val height = img.height * width / img.width
|
||||
img = img.scale(width, height)
|
||||
str = compressImageStr(img)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
|
||||
// var img = image
|
||||
// var stream = compressImageData(img, usePng)
|
||||
// while (stream.size() > maxDataSize) {
|
||||
// val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
|
||||
// val clippedRatio = kotlin.math.min(ratio, 2.0)
|
||||
// val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
// val height = img.height * width / img.width
|
||||
// img = img.scale(width, height)
|
||||
// stream = compressImageData(img, usePng)
|
||||
// }
|
||||
// return stream
|
||||
// }
|
||||
actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
|
||||
var img = image
|
||||
var stream = compressImageData(img, usePng)
|
||||
while (stream.size() > maxDataSize) {
|
||||
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
|
||||
val clippedRatio = kotlin.math.min(ratio, 2.0)
|
||||
val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
val height = img.height * width / img.width
|
||||
img = img.scale(width, height)
|
||||
stream = compressImageData(img, usePng)
|
||||
}
|
||||
return stream
|
||||
}
|
||||
|
||||
actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
|
||||
var xOffset = 0
|
||||
@@ -83,17 +81,17 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
|
||||
return croppedImage
|
||||
}
|
||||
|
||||
// actual fun compressImageStr(bitmap: ImageBitmap): String {
|
||||
// val usePng = bitmap.hasAlpha()
|
||||
// val ext = if (usePng) "png" else "jpg"
|
||||
// return try {
|
||||
// val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
|
||||
// "data:image/$ext;base64,$encoded"
|
||||
// } catch (e: Exception) {
|
||||
// Log.e(TAG, "resizeImageToStrSize error: $e")
|
||||
// throw e
|
||||
// }
|
||||
// }
|
||||
actual fun compressImageStr(bitmap: ImageBitmap): String {
|
||||
val usePng = bitmap.hasAlpha()
|
||||
val ext = if (usePng) "png" else "jpg"
|
||||
return try {
|
||||
val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray())
|
||||
"data:image/$ext;base64,$encoded"
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "resizeImageToStrSize error: $e")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream {
|
||||
val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next()
|
||||
|
||||
+4
-6
@@ -1,10 +1,8 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
|
||||
actual object Log {
|
||||
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") }
|
||||
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") }
|
||||
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") }
|
||||
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") }
|
||||
actual fun d(tag: String, text: String) = println("D: $text")
|
||||
actual fun e(tag: String, text: String) = println("E: $text")
|
||||
actual fun i(tag: String, text: String) = println("I: $text")
|
||||
actual fun w(tag: String, text: String) = println("W: $text")
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.2.1
|
||||
android.version_code=261
|
||||
android.version_name=6.2
|
||||
android.version_code=259
|
||||
|
||||
desktop.version_name=6.2.1
|
||||
desktop.version_code=83
|
||||
desktop.version_name=6.2
|
||||
desktop.version_code=82
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
+1
-7
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 0a827307d666de819d5cef17f70a78be5cd0e410
|
||||
tag: 79e9447b73cc315ce35042b0a5f210c07ea39b07
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
@@ -63,9 +63,3 @@ source-repository-package
|
||||
location: https://github.com/simplex-chat/wai.git
|
||||
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
|
||||
subdir: warp
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://gitlab.com/dpwiz/hs-jpeg-turbo
|
||||
tag: d2844e00e7834124e27171cde21cf280d6772aa8
|
||||
subdir: JuicyPixels-jpeg-turbo jpeg-turbo
|
||||
|
||||
@@ -198,7 +198,6 @@
|
||||
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
||||
pkgs.pkgsCross.mingwW64.openssl
|
||||
];
|
||||
packages."jpeg-turbo".flags.static = true;
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
pkgs.pkgsCross.mingwW64.openssl
|
||||
@@ -337,7 +336,6 @@
|
||||
packages.direct-sqlcipher.patches = [
|
||||
./scripts/nix/direct-sqlcipher-android-log.patch
|
||||
];
|
||||
packages."jpeg-turbo".flags.static = true;
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
|
||||
@@ -391,8 +389,6 @@
|
||||
"chat_valid_name"
|
||||
"chat_json_length"
|
||||
"chat_write_file"
|
||||
"chat_write_image"
|
||||
"chat_resize_image_to_str_size"
|
||||
];
|
||||
postInstall = ''
|
||||
set -x
|
||||
@@ -449,11 +445,6 @@
|
||||
packages.direct-sqlcipher.patches = [
|
||||
./scripts/nix/direct-sqlcipher-android-log.patch
|
||||
];
|
||||
packages.jpeg-turbo.flags.static = true;
|
||||
packages.jpeg-turbo.components.library.libs = pkgs.lib.mkForce [
|
||||
(androidPkgs.libjpeg_turbo.override { enableStatic = true; })
|
||||
];
|
||||
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
(androidPkgs.openssl.override { static = true; })
|
||||
@@ -502,8 +493,6 @@
|
||||
"chat_valid_name"
|
||||
"chat_json_length"
|
||||
"chat_write_file"
|
||||
"chat_write_image"
|
||||
"chat_resize_image_to_str_size"
|
||||
];
|
||||
postInstall = ''
|
||||
set -x
|
||||
@@ -561,7 +550,6 @@
|
||||
packages.simplexmq.flags.swift = true;
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
packages.entropy.flags.DoNotGetEntropy = true;
|
||||
packages."jpeg-turbo".flags.static = true;
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
# TODO: have a cross override for iOS, that sets this.
|
||||
@@ -577,7 +565,6 @@
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
packages.entropy.flags.DoNotGetEntropy = true;
|
||||
packages."jpeg-turbo".flags.static = true;
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;}))
|
||||
@@ -596,7 +583,6 @@
|
||||
packages.simplexmq.flags.swift = true;
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
packages.entropy.flags.DoNotGetEntropy = true;
|
||||
packages."jpeg-turbo".flags.static = true;
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
(pkgs.openssl.override { static = true; })
|
||||
@@ -611,7 +597,6 @@
|
||||
extra-modules = [{
|
||||
packages.direct-sqlcipher.flags.commoncrypto = true;
|
||||
packages.entropy.flags.DoNotGetEntropy = true;
|
||||
packages."jpeg-turbo".flags.static = true;
|
||||
packages.simplexmq.flags.client_library = true;
|
||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||
(pkgs.openssl.override { static = true; })
|
||||
|
||||
+1
-3
@@ -16,8 +16,6 @@ EXPORTS
|
||||
chat_encrypt_media
|
||||
chat_decrypt_media
|
||||
chat_write_file
|
||||
chat_write_image
|
||||
chat_read_file
|
||||
chat_encrypt_file
|
||||
chat_decrypt_file
|
||||
chat_resize_image_to_str_size
|
||||
chat_decrypt_file
|
||||
@@ -52,9 +52,6 @@ dependencies:
|
||||
- unliftio-core == 0.2.*
|
||||
- uuid == 1.3.*
|
||||
- zip == 2.0.*
|
||||
- JuicyPixels
|
||||
- JuicyPixels-jpeg-turbo
|
||||
- JuicyPixels-stbir
|
||||
|
||||
flags:
|
||||
swift:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "addon",
|
||||
"sources": [ "cpp/simplex.cc" ],
|
||||
"libraries": [
|
||||
"-Wl,-rpath,libs",
|
||||
"-L<(module_root_dir)/build/Release/",
|
||||
"-L<(module_root_dir)/libs",
|
||||
"-lsimplex"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
#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
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// 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 */
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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"))
|
||||
@@ -24,18 +24,19 @@ 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\"" 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
|
||||
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library'
|
||||
#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'
|
||||
cd $BUILD_DIR/build
|
||||
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
||||
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
||||
mv libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so libsimplex.so 2> /dev/null || true
|
||||
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libsimplex.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
|
||||
# but libffi.so.8 is shipped as an external library with other libs
|
||||
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
||||
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libsimplex.so
|
||||
|
||||
mkdir deps 2> /dev/null || true
|
||||
ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
|
||||
ldd libsimplex.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
|
||||
|
||||
cd -
|
||||
|
||||
@@ -44,7 +45,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
|
||||
|
||||
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 $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so 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/
|
||||
scripts/desktop/prepare-vlc-linux.sh
|
||||
|
||||
links_dir=apps/multiplatform/build/links
|
||||
|
||||
@@ -14,7 +14,7 @@ else
|
||||
fi
|
||||
|
||||
LIB_EXT=dylib
|
||||
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
|
||||
LIB=libsimplex.$LIB_EXT
|
||||
GHC_LIBS_DIR=$(ghc --print-libdir)
|
||||
|
||||
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
|
||||
@@ -24,9 +24,10 @@ 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
|
||||
|
||||
rm -rf $BUILD_DIR
|
||||
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'
|
||||
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'
|
||||
|
||||
cd $BUILD_DIR/build
|
||||
mv libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT libsimplex.dylib 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
|
||||
@@ -95,7 +96,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
|
||||
|
||||
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 $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT 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/
|
||||
|
||||
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."0a827307d666de819d5cef17f70a78be5cd0e410" = "09z0y6w1c51szd9r90fl259inn76yahfbb164mawd34sr41ccp7g";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."79e9447b73cc315ce35042b0a5f210c07ea39b07" = "16z7z5a3f7gw0h188manykp008d1bqpydlrj7h497mgyjmp4cy9m";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
@@ -9,5 +9,4 @@
|
||||
"https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0";
|
||||
"https://github.com/yesodweb/wai.git"."ec5e017d896a78e787a5acea62b37a4e677dec2e" = "1ckcpmpjfy9jiqrb52q20lj7ln4hmq9v2jk6kpkf3m68c1m9c2bx";
|
||||
"https://github.com/simplex-chat/wai.git"."2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0" = "199g4rjdf1zp1fcw8nqdsyr1h36hmg424qqx03071jk7j00z7ay4";
|
||||
"https://gitlab.com/dpwiz/hs-jpeg-turbo"."d2844e00e7834124e27171cde21cf280d6772aa8" = "1gbvpkx7g2r62bfgpmf8ljdvpccnskdnhqzblm515cb70pyrfq07";
|
||||
}
|
||||
|
||||
+7
-30
@@ -35,7 +35,6 @@ library
|
||||
Simplex.Chat.Core
|
||||
Simplex.Chat.Files
|
||||
Simplex.Chat.Help
|
||||
Simplex.Chat.Image
|
||||
Simplex.Chat.Markdown
|
||||
Simplex.Chat.Messages
|
||||
Simplex.Chat.Messages.Batch
|
||||
@@ -206,10 +205,7 @@ library
|
||||
StrictData
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
@@ -274,10 +270,7 @@ executable simplex-bot
|
||||
StrictData
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
@@ -343,10 +336,7 @@ executable simplex-bot-advanced
|
||||
StrictData
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
@@ -415,10 +405,7 @@ executable simplex-broadcast-bot
|
||||
Paths_simplex_chat
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
@@ -485,10 +472,7 @@ executable simplex-chat
|
||||
StrictData
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
@@ -561,10 +545,7 @@ executable simplex-directory-service
|
||||
Paths_simplex_chat
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
@@ -637,7 +618,6 @@ test-suite simplex-chat-test
|
||||
ChatTests.Profiles
|
||||
ChatTests.Utils
|
||||
JSONTests
|
||||
LinkPreviewTests
|
||||
MarkdownTests
|
||||
MessageBatching
|
||||
MobileTests
|
||||
@@ -665,10 +645,7 @@ test-suite simplex-chat-test
|
||||
StrictData
|
||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
JuicyPixels
|
||||
, JuicyPixels-jpeg-turbo
|
||||
, JuicyPixels-stbir
|
||||
, QuickCheck ==2.14.*
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
|
||||
+2
-1
@@ -92,8 +92,9 @@ import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
|
||||
import qualified Simplex.Chat.Util as U
|
||||
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
|
||||
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription, maxFileSize, maxFileSizeHard)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
|
||||
import qualified Simplex.FileTransfer.Description as FD
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
{-# 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
|
||||
@@ -21,7 +21,6 @@ import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.List (find)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
@@ -30,14 +29,13 @@ import Data.Word (Word8)
|
||||
import Database.SQLite.Simple (SQLError (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Foreign.C.String
|
||||
import Foreign.C.Types (CBool (..), CInt (..), CLong (..))
|
||||
import Foreign.C.Types (CInt (..))
|
||||
import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Storable (poke)
|
||||
import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding)
|
||||
import Simplex.Chat
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Image (readResizeable, resizeImageToSize)
|
||||
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
|
||||
import Simplex.Chat.Mobile.File
|
||||
import Simplex.Chat.Mobile.Shared
|
||||
@@ -47,7 +45,6 @@ import Simplex.Chat.Remote.Types
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Store.Profiles
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (liftIOEither)
|
||||
import Simplex.Messaging.Agent.Client (agentClientStore)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore)
|
||||
@@ -105,16 +102,12 @@ foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Wo
|
||||
|
||||
foreign export ccall "chat_write_file" cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_write_image" cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> CBool -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
|
||||
|
||||
foreign export ccall "chat_encrypt_file" cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
|
||||
|
||||
foreign export ccall "chat_resize_image_to_str_size" cChatResizeImageToStrSize :: CString -> CLong -> IO CString
|
||||
|
||||
-- | check / migrate database and initialize chat controller on success
|
||||
cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
|
||||
cChatMigrateInit fp key conf = cChatMigrateInitKey fp key 0 conf 0
|
||||
@@ -189,37 +182,6 @@ cChatValidName cName = newCString . mkValidName =<< peekCString cName
|
||||
cChatJsonLength :: CString -> IO CInt
|
||||
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s
|
||||
|
||||
-- -- | Resize image at path to match specified dimensions preserving aspect ratio
|
||||
-- cChatResizeImageToFit :: CString -> CInt -> CInt -> IO CString
|
||||
-- cChatResizeImageToFit path maxWidth maxHeight = error "todo"
|
||||
|
||||
-- -- | Resize image at path to match specified dimensions, cropping the extra pixels
|
||||
-- cChatResizeImageCrop :: CString -> CInt -> CInt -> IO CString
|
||||
-- cChatResizeImageCrop path width height = error "todo" -- аватарки
|
||||
|
||||
-- -- | Downscale image at path until it fits into specified size
|
||||
-- cChatResizeImageToDataSize :: CString -> CInt -> IO CString
|
||||
-- cChatResizeImageToDataSize path maxSize = error "todo"
|
||||
|
||||
-- | Downscale image at path until its data-uri encoding fits into specified size.
|
||||
-- Returns data-uri/base64 encoded image as 0-terminated string.
|
||||
-- Empty result string means operation failure.
|
||||
-- The caller must free the result ptr.
|
||||
cChatResizeImageToStrSize :: CString -> CLong -> IO CString
|
||||
cChatResizeImageToStrSize fp' maxSize = do
|
||||
fp <- peekCString fp'
|
||||
res <- runExceptT $ do
|
||||
(ri, _) <- liftIOEither $ readResizeable fp
|
||||
let resized = resizeImageToSize True previewMinQuality (fromIntegral maxSize) ri
|
||||
if LB.length resized > fromIntegral maxSize then throwError "unable to fit" else pure resized
|
||||
newCStringFromLazyBS $ fromRight "" res
|
||||
where
|
||||
previewMinQuality = 20
|
||||
|
||||
-- -- | Strip EXIF etc metadata from image, inlplace
|
||||
-- cChatStripImageMetadata :: CString -> IO CBool
|
||||
-- cChatStripImageMetadata path = error "todo"
|
||||
|
||||
mobileChatOpts :: String -> ChatOpts
|
||||
mobileChatOpts dbFilePrefix =
|
||||
ChatOpts
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
module Simplex.Chat.Mobile.File
|
||||
( cChatWriteFile,
|
||||
cChatWriteImage,
|
||||
cChatReadFile,
|
||||
cChatEncryptFile,
|
||||
cChatDecryptFile,
|
||||
@@ -36,8 +35,6 @@ import Foreign.Ptr
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Storable (poke, pokeByteOff)
|
||||
import Simplex.Chat.Controller (ChatController (..))
|
||||
import Simplex.Chat.Image (resizeImageToSize)
|
||||
import qualified Simplex.Chat.Image as Picture
|
||||
import Simplex.Chat.Mobile.Shared
|
||||
import Simplex.Chat.Util (chunkSize, encryptFile)
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..))
|
||||
@@ -48,7 +45,7 @@ import Simplex.Messaging.Util (catchAll)
|
||||
import UnliftIO (Handle, IOMode (..), atomically, withFile)
|
||||
|
||||
data WriteFileResult
|
||||
= WFResult {cryptoArgs :: Maybe CryptoFileArgs}
|
||||
= WFResult {cryptoArgs :: CryptoFileArgs}
|
||||
| WFError {writeError :: String}
|
||||
|
||||
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
|
||||
@@ -64,32 +61,10 @@ cChatWriteFile cc cPath ptr len = do
|
||||
chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult
|
||||
chatWriteFile ChatController {random} path s = do
|
||||
cfArgs <- atomically $ CF.randomArgs random
|
||||
chatWriteFile_ (Just cfArgs) path s
|
||||
|
||||
chatWriteFile_ :: Maybe CryptoFileArgs -> FilePath -> ByteString -> IO WriteFileResult
|
||||
chatWriteFile_ cfArgs_ path s = do
|
||||
let file = CryptoFile path cfArgs_
|
||||
either WFError (\_ -> WFResult cfArgs_)
|
||||
let file = CryptoFile path $ Just cfArgs
|
||||
either WFError (\_ -> WFResult cfArgs)
|
||||
<$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
|
||||
|
||||
cChatWriteImage :: StablePtr ChatController -> CLong -> CString -> Ptr Word8 -> CInt -> CBool -> IO CJSONString
|
||||
cChatWriteImage cc maxSize cPath ptr len encrypt = do
|
||||
c <- deRefStablePtr cc
|
||||
path <- peekCString cPath
|
||||
src <- getByteString ptr len
|
||||
cfArgs_ <- if encrypt /= 0 then Just <$> atomically (CF.randomArgs $ random c) else pure Nothing
|
||||
r <-
|
||||
case Picture.decodeResizeable src of
|
||||
Left e -> pure $ WFError e
|
||||
Right (ri, _metadata) -> do
|
||||
let resized = resizeImageToSize False storeMinQuality (fromIntegral maxSize) ri
|
||||
if LB.length resized > fromIntegral maxSize
|
||||
then pure $ WFError "unable to fit"
|
||||
else chatWriteFile_ cfArgs_ path (LB.toStrict resized)
|
||||
newCStringFromLazyBS $ J.encode r
|
||||
where
|
||||
storeMinQuality = 20
|
||||
|
||||
data ReadFileResult
|
||||
= RFResult {fileSize :: Int}
|
||||
| RFError {readError :: String}
|
||||
@@ -131,7 +106,7 @@ chatEncryptFile ChatController {random} fromPath toPath =
|
||||
encrypt = do
|
||||
cfArgs <- atomically $ CF.randomArgs random
|
||||
encryptFile fromPath toPath cfArgs
|
||||
pure $ Just cfArgs
|
||||
pure cfArgs
|
||||
|
||||
cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
|
||||
cChatDecryptFile cFromPath cKey cNonce cToPath = do
|
||||
|
||||
@@ -620,7 +620,7 @@ testXFTPFileTransferEncrypted =
|
||||
createDirectoryIfMissing True "./tests/tmp/alice/"
|
||||
createDirectoryIfMissing True "./tests/tmp/bob/"
|
||||
WFResult cfArgs <- chatWriteFile (chatController alice) srcPath src
|
||||
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath cfArgs
|
||||
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs
|
||||
withXFTPServer $ do
|
||||
connectUsers alice bob
|
||||
alice ##> ("/_send @2 json [{\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}]")
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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
|
||||
@@ -282,8 +282,8 @@ testFileCApi fileName tmp = do
|
||||
ptr <- mallocBytes $ B.length src
|
||||
putByteString ptr src
|
||||
r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen
|
||||
Just (WFResult cfArgs@(Just (CFArgs key nonce))) <- jDecode r
|
||||
let encryptedFile = CryptoFile path cfArgs
|
||||
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
|
||||
let encryptedFile = CryptoFile path $ Just cfArgs
|
||||
CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src)
|
||||
cKey <- encodedCString key
|
||||
cNonce <- encodedCString nonce
|
||||
@@ -318,8 +318,8 @@ testFileEncryptionCApi fileName tmp = do
|
||||
let toPath = tmp </> (fileName <> ".encrypted.pdf")
|
||||
cToPath <- newCString toPath
|
||||
r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath
|
||||
Just (WFResult cfArgs@(Just (CFArgs key nonce))) <- jDecode r
|
||||
CF.getFileContentsSize (CryptoFile toPath cfArgs) `shouldReturn` fromIntegral (B.length src)
|
||||
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
|
||||
CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src)
|
||||
cKey <- encodedCString key
|
||||
cNonce <- encodedCString nonce
|
||||
let toPath' = tmp </> (fileName <> ".decrypted.pdf")
|
||||
|
||||
+1
-3
@@ -6,7 +6,6 @@ import ChatTests.Utils (xdescribe'')
|
||||
import Control.Logger.Simple
|
||||
import Data.Time.Clock.System
|
||||
import JSONTests
|
||||
import LinkPreviewTests
|
||||
import MarkdownTests
|
||||
import MessageBatching
|
||||
import MobileTests
|
||||
@@ -23,7 +22,7 @@ import WebRTCTests
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug
|
||||
setLogLevel LogError
|
||||
withGlobalLogging logCfg . hspec $ do
|
||||
describe "Schema dump" schemaDumpTest
|
||||
describe "SimpleX chat markdown" markdownTests
|
||||
@@ -41,7 +40,6 @@ main = do
|
||||
xdescribe'' "SimpleX Broadcast bot" broadcastBotTests
|
||||
xdescribe'' "SimpleX Directory service bot" directoryServiceTests
|
||||
describe "Remote session" remoteTests
|
||||
around testBracket $ describe "Link preview" linkPreviewTests
|
||||
where
|
||||
testBracket test = withSmpServer $ tmpBracket test
|
||||
tmpBracket test = do
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
Reference in New Issue
Block a user