Merge branch 'master' into remote-desktop

This commit is contained in:
Evgeny Poberezkin 2023-10-05 14:15:25 +01:00
commit 27e8a81c9f
21 changed files with 419 additions and 338 deletions

View File

@ -9,6 +9,18 @@
import SwiftUI
import SimpleXChat
enum GroupProfileAlert: Identifiable {
case saveError(err: String)
case invalidName(validName: String)
var id: String {
switch self {
case let .saveError(err): return "saveError \(err)"
case let .invalidName(validName): return "invalidName \(validName)"
}
}
}
struct GroupProfileView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@ -18,8 +30,7 @@ struct GroupProfileView: View {
@State private var showImagePicker = false
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var showSaveErrorAlert = false
@State private var saveGroupError: String? = nil
@State private var alert: GroupProfileAlert?
@FocusState private var focusDisplayName
var body: some View {
@ -47,20 +58,29 @@ struct GroupProfileView: View {
.frame(maxWidth: .infinity, alignment: .center)
VStack(alignment: .leading) {
ZStack(alignment: .leading) {
if !validDisplayName(groupProfile.displayName) {
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.padding(.bottom, 10)
ZStack(alignment: .topLeading) {
if !validNewProfileName() {
Button {
alert = .invalidName(validName: mkValidName(groupProfile.displayName))
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
}
profileNameTextEdit("Group display name", $groupProfile.displayName)
.focused($focusDisplayName)
}
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
.padding(.bottom)
let fullName = groupInfo.groupProfile.fullName
if fullName != "" && fullName != groupProfile.displayName {
profileNameTextEdit("Group full name (optional)", $groupProfile.fullName)
.padding(.bottom)
}
HStack(spacing: 20) {
Button("Cancel") { dismiss() }
Button("Save group profile") { saveProfile() }
.disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName))
.disabled(groupProfile.displayName == "" || !validNewProfileName())
}
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@ -99,27 +119,35 @@ struct GroupProfileView: View {
focusDisplayName = true
}
}
.alert(isPresented: $showSaveErrorAlert) {
Alert(
title: Text("Error saving group profile"),
message: Text("\(saveGroupError ?? "Unexpected error")")
)
.alert(item: $alert) { a in
switch a {
case let .saveError(err):
return Alert(
title: Text("Error saving group profile"),
message: Text(err)
)
case let .invalidName(name):
return createInvalidNameAlert(name, $groupProfile.displayName)
}
}
.contentShape(Rectangle())
.onTapGesture { hideKeyboard() }
}
private func validNewProfileName() -> Bool {
groupProfile.displayName == groupInfo.groupProfile.displayName
|| validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces))
}
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.bottom)
.padding(.leading, 28)
.padding(.leading, 32)
}
func saveProfile() {
Task {
do {
groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces)
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
await MainActor.run {
groupInfo = gInfo
@ -128,8 +156,7 @@ struct GroupProfileView: View {
}
} catch let error {
let err = responseError(error)
saveGroupError = err
showSaveErrorAlert = true
alert = .saveError(err: err)
logger.error("GroupProfile apiUpdateGroup error: \(err)")
}
}

View File

@ -16,11 +16,11 @@ struct AddGroupView: View {
@State private var groupInfo: GroupInfo?
@State private var profile = GroupProfile(displayName: "", fullName: "")
@FocusState private var focusDisplayName
@FocusState private var focusFullName
@State private var showChooseSource = false
@State private var showImagePicker = false
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var showInvalidNameAlert = false
var body: some View {
if let chat = chat, let groupInfo = groupInfo {
@ -76,26 +76,24 @@ struct AddGroupView: View {
.padding(.bottom, 4)
ZStack(alignment: .topLeading) {
if !validDisplayName(profile.displayName) {
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.padding(.top, 4)
let name = profile.displayName.trimmingCharacters(in: .whitespaces)
if name != mkValidName(name) {
Button {
showInvalidNameAlert = true
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
}
textField("Group display name", text: $profile.displayName)
textField("Enter group name…", text: $profile.displayName)
.focused($focusDisplayName)
.submitLabel(.next)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { focusFullName = true }
else { focusDisplayName = true }
if canCreateProfile() { createGroup() }
}
}
textField("Group full name (optional)", text: $profile.fullName)
.focused($focusFullName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createGroup() }
else { focusFullName = true }
}
.padding(.bottom)
Spacer()
@ -133,6 +131,9 @@ struct AddGroupView: View {
didSelectItem in showImagePicker = false
}
}
.alert(isPresented: $showInvalidNameAlert) {
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
}
.onChange(of: chosenImage) { image in
if let image = image {
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
@ -146,15 +147,13 @@ struct AddGroupView: View {
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
TextField(placeholder, text: text)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.leading, 28)
.padding(.bottom)
.padding(.leading, 32)
}
func createGroup() {
hideKeyboard()
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
let gInfo = try apiNewGroup(profile)
Task {
let groupMembers = await apiListMembers(gInfo.groupId)
@ -180,7 +179,7 @@ struct AddGroupView: View {
}
func canCreateProfile() -> Bool {
profile.displayName != "" && validDisplayName(profile.displayName)
profile.displayName != "" && validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
}
}

View File

@ -9,175 +9,244 @@
import SwiftUI
import SimpleXChat
enum UserProfileAlert: Identifiable {
case duplicateUserError
case createUserError(error: LocalizedStringKey)
case invalidNameError(validName: String)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
case .createUserError: return "createUserError"
case let .invalidNameError(validName): return "invalidNameError \(validName)"
}
}
}
struct CreateProfile: View {
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@FocusState private var focusDisplayName
@State private var alert: UserProfileAlert?
var body: some View {
List {
Section {
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
Button {
createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss)
} label: {
Label("Create profile", systemImage: "checkmark")
}
.disabled(!canCreateProfile(displayName))
} header: {
HStack {
Text("Your profile")
let name = displayName.trimmingCharacters(in: .whitespaces)
let validName = mkValidName(name)
if name != validName {
Spacer()
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.onTapGesture {
alert = .invalidNameError(validName: validName)
}
}
}
.frame(height: 20)
} footer: {
VStack(alignment: .leading, spacing: 8) {
Text("Your profile, contacts and delivered messages are stored on your device.")
Text("The profile is only shared with your contacts.")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.navigationTitle("Create your profile")
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
focusDisplayName = true
}
}
.keyboardPadding()
}
}
struct CreateFirstProfile: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@State private var fullName: String = ""
@FocusState private var focusDisplayName
@FocusState private var focusFullName
@State private var alert: CreateProfileAlert?
private enum CreateProfileAlert: Identifiable {
case duplicateUserError
case createUserError(error: LocalizedStringKey)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
case .createUserError: return "createUserError"
}
}
}
var body: some View {
VStack(alignment: .leading) {
Text("Create your profile")
.font(.largeTitle)
.bold()
.padding(.bottom, 4)
.frame(maxWidth: .infinity)
Text("Your profile, contacts and delivered messages are stored on your device.")
.padding(.bottom, 4)
Text("The profile is only shared with your contacts.")
.padding(.bottom)
Group {
Text("Create your profile")
.font(.largeTitle)
.bold()
Text("Your profile, contacts and delivered messages are stored on your device.")
.foregroundColor(.secondary)
Text("The profile is only shared with your contacts.")
.foregroundColor(.secondary)
.padding(.bottom)
}
.padding(.bottom)
ZStack(alignment: .topLeading) {
if !validDisplayName(displayName) {
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.padding(.top, 4)
let name = displayName.trimmingCharacters(in: .whitespaces)
let validName = mkValidName(name)
if name != validName {
Button {
showAlert(.invalidNameError(validName: validName))
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
}
textField("Display name", text: $displayName)
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
.submitLabel(.next)
.onSubmit {
if canCreateProfile() { focusFullName = true }
else { focusDisplayName = true }
}
.padding(.leading, 32)
}
textField("Full name (optional)", text: $fullName)
.focused($focusFullName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createProfile() }
else { focusFullName = true }
}
.padding(.bottom)
Spacer()
HStack {
if m.users.isEmpty {
Button {
hideKeyboard()
withAnimation {
m.onboardingStage = .step1_SimpleXInfo
}
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
}
Spacer()
HStack {
Button {
createProfile()
} label: {
Text("Create")
Image(systemName: "greaterthan")
}
.disabled(!canCreateProfile())
}
}
onboardingButtons()
}
.onAppear() {
focusDisplayName = true
setLastVersionDefault()
}
.alert(item: $alert) { a in
switch a {
case .duplicateUserError: return duplicateUserAlert
case let .createUserError(err): return creatUserErrorAlert(err)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.keyboardPadding()
}
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View {
TextField(placeholder, text: text)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.leading, 28)
.padding(.bottom)
}
func createProfile() {
hideKeyboard()
let profile = Profile(
displayName: displayName,
fullName: fullName
)
do {
m.currentUser = try apiCreateActiveUser(profile)
if m.users.isEmpty {
try startChat()
func onboardingButtons() -> some View {
HStack {
Button {
hideKeyboard()
withAnimation {
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
m.onboardingStage = .step3_CreateSimpleXAddress
m.onboardingStage = .step1_SimpleXInfo
}
} else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()
}
} catch let error {
switch error as? ChatResponse {
case .chatCmdError(_, .errorStore(.duplicateName)),
.chatCmdError(_, .error(.userExists)):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} else {
alert = .duplicateUserError
}
default:
let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil {
AlertManager.shared.showAlert(creatUserErrorAlert(err))
} else {
alert = .createUserError(error: err)
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
logger.error("Failed to create user or start chat: \(responseError(error))")
Spacer()
Button {
createProfile(displayName, showAlert: showAlert, dismiss: dismiss)
} label: {
HStack {
Text("Create")
Image(systemName: "greaterthan")
}
}
.disabled(!canCreateProfile(displayName))
}
}
func canCreateProfile() -> Bool {
displayName != "" && validDisplayName(displayName)
}
private var duplicateUserAlert: Alert {
Alert(
title: Text("Duplicate display name!"),
message: Text("You already have a chat profile with the same display name. Please choose another name.")
)
}
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert(
title: Text("Error creating profile!"),
message: Text(err)
)
private func showAlert(_ alert: UserProfileAlert) {
AlertManager.shared.showAlert(userProfileAlert(alert, $displayName))
}
}
private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) {
hideKeyboard()
let profile = Profile(
displayName: displayName.trimmingCharacters(in: .whitespaces),
fullName: ""
)
let m = ChatModel.shared
do {
m.currentUser = try apiCreateActiveUser(profile)
if m.users.isEmpty {
try startChat()
withAnimation {
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
m.onboardingStage = .step3_CreateSimpleXAddress
}
} else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
m.users = try listUsers()
try getUserChatData()
}
} catch let error {
switch error as? ChatResponse {
case .chatCmdError(_, .errorStore(.duplicateName)),
.chatCmdError(_, .error(.userExists)):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} else {
showAlert(.duplicateUserError)
}
default:
let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil {
AlertManager.shared.showAlert(creatUserErrorAlert(err))
} else {
showAlert(.createUserError(error: err))
}
}
logger.error("Failed to create user or start chat: \(responseError(error))")
}
}
private func canCreateProfile(_ displayName: String) -> Bool {
let name = displayName.trimmingCharacters(in: .whitespaces)
return name != "" && mkValidName(name) == name
}
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert {
switch alert {
case .duplicateUserError: return duplicateUserAlert
case let .createUserError(err): return creatUserErrorAlert(err)
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
}
}
private var duplicateUserAlert: Alert {
Alert(
title: Text("Duplicate display name!"),
message: Text("You already have a chat profile with the same display name. Please choose another name.")
)
}
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert(
title: Text("Error creating profile!"),
message: Text(err)
)
}
func createInvalidNameAlert(_ name: String, _ displayName: Binding<String>) -> Alert {
name == ""
? Alert(title: Text("Invalid name!"))
: Alert(
title: Text("Invalid name!"),
message: Text("Correct name to \(name)?"),
primaryButton: .default(
Text("Ok"),
action: { displayName.wrappedValue = name }
),
secondaryButton: .cancel()
)
}
func validDisplayName(_ name: String) -> Bool {
name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#"
mkValidName(name.trimmingCharacters(in: .whitespaces)) == name
}
func mkValidName(_ s: String) -> String {
var c = s.cString(using: .utf8)!
return fromCString(chat_valid_name(&c)!)
}
struct CreateProfile_Previews: PreviewProvider {

View File

@ -14,7 +14,7 @@ struct OnboardingView: View {
var body: some View {
switch onboarding {
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
case .step2_CreateProfile: CreateProfile()
case .step2_CreateProfile: CreateFirstProfile()
case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
case .step4_SetNotificationsMode: SetNotificationsMode()
case .onboardingComplete: EmptyView()

View File

@ -381,7 +381,9 @@ struct ProfilePreview: View {
Text(profileOf.displayName)
.fontWeight(.bold)
.font(.title2)
Text(profileOf.fullName)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
Text(profileOf.fullName)
}
}
}
}

View File

@ -17,6 +17,8 @@ struct UserProfile: View {
@State private var showImagePicker = false
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var alert: UserProfileAlert?
@FocusState private var focusDisplayName
var body: some View {
let user: User = chatModel.currentUser!
@ -47,18 +49,27 @@ struct UserProfile: View {
VStack(alignment: .leading) {
ZStack(alignment: .leading) {
if !validDisplayName(profile.displayName) {
Image(systemName: "exclamationmark.circle")
.foregroundColor(.red)
.padding(.bottom, 10)
if !validNewProfileName(user) {
Button {
alert = .invalidNameError(validName: mkValidName(profile.displayName))
} label: {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
}
} else {
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
}
profileNameTextEdit("Display name", $profile.displayName)
profileNameTextEdit("Profile name", $profile.displayName)
.focused($focusDisplayName)
}
.padding(.bottom)
if showFullName(user) {
profileNameTextEdit("Full name (optional)", $profile.fullName)
.padding(.bottom)
}
profileNameTextEdit("Full name (optional)", $profile.fullName)
HStack(spacing: 20) {
Button("Cancel") { editProfile = false }
Button("Save (and notify contacts)") { saveProfile() }
.disabled(profile.displayName == "" || !validDisplayName(profile.displayName))
.disabled(!canSaveProfile(user))
}
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@ -74,11 +85,14 @@ struct UserProfile: View {
.frame(maxWidth: .infinity, alignment: .center)
VStack(alignment: .leading) {
profileNameView("Display name:", user.profile.displayName)
profileNameView("Full name:", user.profile.fullName)
profileNameView("Profile name:", user.profile.displayName)
if showFullName(user) {
profileNameView("Full name:", user.profile.fullName)
}
Button("Edit") {
profile = fromLocalProfile(user.profile)
editProfile = true
focusDisplayName = true
}
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@ -117,14 +131,12 @@ struct UserProfile: View {
profile.image = nil
}
}
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
}
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(.bottom)
.padding(.leading, 28)
.padding(.leading, 32)
}
func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
@ -141,9 +153,22 @@ struct UserProfile: View {
showChooseSource = true
}
private func validNewProfileName(_ user: User) -> Bool {
profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces))
}
private func showFullName(_ user: User) -> Bool {
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
}
private func canSaveProfile(_ user: User) -> Bool {
profile.displayName != "" && validNewProfileName(user)
}
func saveProfile() {
Task {
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
DispatchQueue.main.async {
chatModel.updateCurrentUser(newProfile)

View File

@ -23,6 +23,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name);
extern char *chat_encrypt_media(char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len);

View File

@ -11,7 +11,6 @@
local.properties
common/src/commonMain/cpp/android/libs/
common/src/commonMain/cpp/desktop/libs/
desktop/src/jvmMain/resources/libs/
android/build
android/release
common/build

View File

@ -54,12 +54,11 @@ add_library( # Sets the name of the library.
simplex-api.c)
add_library( simplex SHARED IMPORTED )
# Lib has different name because of version, find it
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${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()
@ -72,7 +71,7 @@ if(NOT APPLE)
else()
# Without direct linking it can't find hs_init in linking step
add_library( rts SHARED IMPORTED )
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT})
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSrts*_thr-*.${OS_LIB_EXT})
set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB})
target_link_libraries(app-lib rts simplex)

View File

@ -21,8 +21,6 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
actual val databaseExportDir: File = tmpDir
val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() }
actual fun desktopOpenDatabaseDir() {
if (Desktop.isDesktopSupported()) {
try {

View File

@ -8,12 +8,12 @@ private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config
private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex"
val desktopPlatform = detectDesktopPlatform()
enum class DesktopPlatform(val libPath: String, val libExtension: String, val configPath: String, val dataPath: String) {
LINUX_X86_64("/libs/linux-x86_64", "so", unixConfigPath, unixDataPath),
LINUX_AARCH64("/libs/aarch64", "so", unixConfigPath, unixDataPath),
WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"),
MAC_X86_64("/libs/mac-x86_64", "dylib", unixConfigPath, unixDataPath),
MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath);
enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) {
LINUX_X86_64("so", unixConfigPath, unixDataPath),
LINUX_AARCH64("so", unixConfigPath, unixDataPath),
WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"),
MAC_X86_64("dylib", unixConfigPath, unixDataPath),
MAC_AARCH64("dylib", unixConfigPath, unixDataPath);
fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64
fun isWindows() = this == WINDOWS_X86_64

View File

@ -52,6 +52,7 @@ compose {
}
//includeAllModules = true
outputBaseDir.set(project.file("../release"))
appResourcesRootDir.set(project.file("../build/links"))
targetFormats(
TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe
//, TargetFormat.AppImage // Gradle doesn't sync on Mac with it
@ -156,11 +157,10 @@ tasks.named("compileJava") {
afterEvaluate {
tasks.create("cmakeBuildAndCopy") {
dependsOn("cmakeBuild")
val copyDetails = mutableMapOf<String, ArrayList<FileCopyDetails>>()
doLast {
copy {
from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps")
into("src/jvmMain/resources/libs/linux-x86_64")
from("${project(":desktop").buildDir}/cmake/main/linux-amd64")
into("$cppPath/desktop/libs/linux-x86_64")
include("*.so*")
eachFile {
path = name
@ -169,16 +169,8 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
copy {
val destinationDir = "src/jvmMain/resources/libs/linux-x86_64/vlc"
from("$cppPath/desktop/libs/linux-x86_64/deps/vlc")
into(destinationDir)
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
copyIfNeeded(destinationDir, copyDetails)
}
copy {
from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps")
into("src/jvmMain/resources/libs/linux-aarch64")
from("${project(":desktop").buildDir}/cmake/main/linux-aarch64")
into("$cppPath/desktop/libs/linux-aarch64")
include("*.so*")
eachFile {
path = name
@ -187,16 +179,18 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
copy {
val destinationDir = "src/jvmMain/resources/libs/linux-aarch64/vlc"
from("$cppPath/desktop/libs/linux-aarch64/deps/vlc")
into(destinationDir)
from("${project(":desktop").buildDir}/cmake/main/windows-amd64")
into("$cppPath/desktop/libs/windows-x86_64")
include("*.dll")
eachFile {
path = name
}
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
copyIfNeeded(destinationDir, copyDetails)
}
copy {
from("${project(":desktop").buildDir}/cmake/main/windows-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps")
into("src/jvmMain/resources/libs/windows-x86_64")
copy {
from("${project(":desktop").buildDir}/cmake/main/windows-amd64")
into("../build/links/windows-x64")
include("*.dll")
eachFile {
path = name
@ -205,16 +199,8 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
copy {
val destinationDir = "src/jvmMain/resources/libs/windows-x86_64/vlc"
from("$cppPath/desktop/libs/windows-x86_64/deps/vlc")
into(destinationDir)
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
copyIfNeeded(destinationDir, copyDetails)
}
copy {
from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps")
into("src/jvmMain/resources/libs/mac-x86_64")
from("${project(":desktop").buildDir}/cmake/main/mac-x86_64")
into("$cppPath/desktop/libs/mac-x86_64")
include("*.dylib")
eachFile {
path = name
@ -223,16 +209,8 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
copy {
val destinationDir = "src/jvmMain/resources/libs/mac-x86_64/vlc"
from("$cppPath/desktop/libs/mac-x86_64/deps/vlc")
into(destinationDir)
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
copyIfNeeded(destinationDir, copyDetails)
}
copy {
from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps")
into("src/jvmMain/resources/libs/mac-aarch64")
from("${project(":desktop").buildDir}/cmake/main/mac-aarch64")
into("$cppPath/desktop/libs/mac-aarch64")
include("*.dylib")
eachFile {
path = name
@ -240,39 +218,6 @@ afterEvaluate {
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
copy {
val destinationDir = "src/jvmMain/resources/libs/mac-aarch64/vlc"
from("$cppPath/desktop/libs/mac-aarch64/deps/vlc")
into(destinationDir)
includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE
copyIfNeeded(destinationDir, copyDetails)
}
}
afterEvaluate {
doLast {
copyDetails.forEach { (destinationDir, details) ->
details.forEach { detail ->
val target = File(projectDir.absolutePath + File.separator + destinationDir + File.separator + detail.path)
if (target.exists()) {
target.setLastModified(detail.lastModified)
}
}
}
}
}
}
}
fun CopySpec.copyIfNeeded(destinationDir: String, into: MutableMap<String, ArrayList<FileCopyDetails>>) {
val details = arrayListOf<FileCopyDetails>()
eachFile {
val targetFile = File(destinationDir, path)
if (file.lastModified() == targetFile.lastModified() && file.length() == targetFile.length()) {
exclude()
} else {
details.add(this)
}
}
into[destinationDir] = details
}

View File

@ -18,51 +18,29 @@ fun main() {
@Suppress("UnsafeDynamicallyLoadedCode")
private fun initHaskell() {
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs")
copyResources(desktopPlatform.libPath, libsTmpDir.toPath())
vlcDir.deleteRecursively()
Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING)
val resourcesDir = File(System.getProperty("compose.application.resources.dir"))
val vlcDir = File(resourcesDir.absolutePath + File.separator + "vlc")
if (desktopPlatform == DesktopPlatform.WINDOWS_X86_64) {
windowsLoadRequiredLibs(libsTmpDir)
windowsLoadRequiredLibs(resourcesDir, vlcDir)
} else {
System.load(File(libsTmpDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath)
System.load(File(resourcesDir, "libapp-lib.${desktopPlatform.libExtension}").absolutePath)
}
// No picture without preloading it, only sound. However, with libs from AppImage it works without preloading
//val libXcb = "libvlc_xcb_events.so.0.0.0"
//System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath)
System.setProperty("jna.library.path", vlcDir.absolutePath)
//discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath)
libsTmpDir.deleteRecursively()
initHS()
}
private fun copyResources(from: String, to: Path) {
val resource = Class.forName("chat.simplex.desktop.MainKt").getResource("")!!.toURI()
val fileSystem = FileSystems.newFileSystem(resource, emptyMap<String, String>())
val resPath = fileSystem.getPath(from)
Files.walkFileTree(resPath, object: SimpleFileVisitor<Path>() {
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
Files.createDirectories(to.resolve(resPath.relativize(dir).toString()))
return FileVisitResult.CONTINUE
}
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
val dest = to.resolve(resPath.relativize(file).toString())
Files.copy(file, dest, StandardCopyOption.REPLACE_EXISTING)
// Setting the same time on file as the time set in script that generates VLC libs
if (dest.toString().contains("." + desktopPlatform.libExtension)) {
dest.setLastModifiedTime(FileTime.fromMillis(0))
}
return FileVisitResult.CONTINUE
}
})
}
private fun windowsLoadRequiredLibs(libsTmpDir: File) {
private fun windowsLoadRequiredLibs(libsTmpDir: File, vlcDir: File) {
val mainLibs = arrayOf(
"libcrypto-3-x64.dll",
"mcfgthread-12.dll",
"libgcc_s_seh-1.dll",
"libstdc++-6.dll",
"libffi-8.dll",
"libgmp-10.dll",
"libgmp-10.dll",
"libsimplex.dll",
"libapp-lib.dll"
)
@ -72,7 +50,7 @@ private fun windowsLoadRequiredLibs(libsTmpDir: File) {
val vlcLibs = arrayOf(
"libvlccore.dll",
"libvlc.dll",
"axvlc.dll",
"axvlc.dll",
"npvlc.dll"
)
vlcLibs.forEach {

View File

@ -1,9 +1,23 @@
#!/bin/bash
set -e
function readlink() {
echo "$(cd "$(dirname "$1")"; pwd -P)"
}
OS=linux
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
GHC_VERSION=9.6.2
if [ "$ARCH" == "aarch64" ]; then
COMPOSE_ARCH=arm64
else
COMPOSE_ARCH=x64
fi
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
cd $root_dir
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
rm -rf $BUILD_DIR
@ -11,16 +25,20 @@ cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -
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
mkdir deps
mkdir deps 2> /dev/null || true
ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
cd -
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/
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 -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/
scripts/desktop/prepare-vlc-linux.sh
links_dir=apps/multiplatform/build/links
mkdir -p $links_dir
cd $links_dir
ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH

View File

@ -4,10 +4,13 @@ set -e
OS=mac
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
COMPOSE_ARCH=$ARCH
GHC_VERSION=9.6.2
if [ "$ARCH" == "arm64" ]; then
ARCH=aarch64
else
COMPOSE_ARCH=x64
fi
LIB_EXT=dylib
@ -84,30 +87,29 @@ rm deps/`basename $LIB`
cd -
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/
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 -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/
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT
cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT
chmod 755 deps/libcrypto.1.1.$LIB_EXT
install_name_tool -id "libcrypto.1.1.$LIB_EXT" deps/libcrypto.1.1.$LIB_EXT
install_name_tool -id "libffi.8.$LIB_EXT" deps/libffi.$LIB_EXT
LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSdrct-*.$LIB_EXT
cp $LIBCRYPTO_PATH libcrypto.1.1.$LIB_EXT
chmod 755 libcrypto.1.1.$LIB_EXT
install_name_tool -id "libcrypto.1.1.$LIB_EXT" libcrypto.1.1.$LIB_EXT
install_name_tool -id "libffi.8.$LIB_EXT" libffi.$LIB_EXT
LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11)
if [ -n "$LIBCRYPTO_PATH" ]; then
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB
fi
LIBCRYPTO_PATH=$(otool -l deps/libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
LIBCRYPTO_PATH=$(otool -l libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
if [ -n "$LIBCRYPTO_PATH" ]; then
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSsmplxmq*.$LIB_EXT
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsmplxmq*.$LIB_EXT
fi
for lib in $(find . -type f -name "*.$LIB_EXT"); do
@ -126,3 +128,9 @@ fi
cd -
scripts/desktop/prepare-vlc-mac.sh
links_dir=apps/multiplatform/build/links
mkdir -p $links_dir
cd $links_dir
rm macos-$COMPOSE_ARCH 2>/dev/null | true
ln -sf ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ macos-$COMPOSE_ARCH

View File

@ -8,15 +8,26 @@ function readlink() {
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
OS=windows
ARCH=`uname -a | rev | cut -d' ' -f2 | rev`
ARCH="x86_64"
JOB_REPO=${1:-$SIMPLEX_CI_REPO_URL}
if [ "$ARCH" == "aarch64" ]; then
COMPOSE_ARCH=arm64
else
COMPOSE_ARCH=x64
fi
cd $root_dir
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/
rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
scripts/desktop/download-lib-windows.sh $JOB_REPO
scripts/desktop/prepare-vlc-windows.sh
links_dir=apps/multiplatform/build/links
mkdir -p $links_dir
cd $links_dir
rm -rf $OS-$COMPOSE_ARCH
ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH

View File

@ -7,7 +7,7 @@ function readlink() {
}
if [ -z "${1}" ]; then
echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{windows,windows-8107}"
echo "Job repo is unset. Provide it via first argument like: $(readlink "$0")/download-lib-windows.sh https://something.com/job/something/{master,stable}"
exit 1
fi
@ -16,12 +16,15 @@ arch=x86_64
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
output_dir="$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-$arch/"
mkdir -p "$output_dir"/deps 2> /dev/null
mkdir -p "$output_dir" 2> /dev/null
curl --location -o libsimplex.zip $job_repo/$arch-linux.$arch-windows:lib:simplex-chat/latest/download/1 && \
$WINDIR\\System32\\tar.exe -xf libsimplex.zip && \
mv libsimplex.dll "$output_dir" && \
mv libcrypto*.dll "$output_dir/deps" && \
mv libffi*.dll "$output_dir/deps" && \
mv libgmp*.dll "$output_dir/deps" && \
mv libcrypto*.dll "$output_dir" && \
mv libffi*.dll "$output_dir" && \
mv libgmp*.dll "$output_dir" && \
mv mcfgthread*.dll "$output_dir" && \
mv libgcc_s_seh*.dll "$output_dir" && \
mv libstdc++*.dll "$output_dir" && \
rm libsimplex.zip

View File

@ -11,13 +11,12 @@ multiplatform_dir=$root_dir/apps/multiplatform
release_app_dir=$root_dir/apps/multiplatform/release/main/app
cd $multiplatform_dir
libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/deps/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2)
trap "rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` 2> /dev/null || true" EXIT
cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*/deps
libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2)
trap "rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` 2> /dev/null || true" EXIT
cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*
./gradlew createDistributable
rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path`
rm desktop/src/jvmMain/resources/libs/*/`basename $libcrypto_path`
rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path`
rm -rf $release_app_dir/AppDir 2>/dev/null
mkdir -p $release_app_dir/AppDir/usr

View File

@ -6,7 +6,7 @@ function readlink() {
echo "$(cd "$(dirname "$1")"; pwd -P)"
}
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/deps/vlc
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/vlc
mkdir $vlc_dir || exit 0

View File

@ -16,7 +16,7 @@ function readlink() {
}
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/deps/vlc
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/vlc
#rm -rf $vlc_dir
mkdir -p $vlc_dir/vlc || exit 0

View File

@ -6,7 +6,7 @@ function readlink() {
echo "$(cd "$(dirname "$1")"; pwd -P)"
}
root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/deps/vlc
vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-x86_64/vlc
rm -rf $vlc_dir
mkdir -p $vlc_dir/vlc || exit 0