Merge branch 'master-ghc8107' into master-android

This commit is contained in:
Evgeny Poberezkin 2023-10-07 21:10:22 +01:00
commit 2bd049db87
48 changed files with 816 additions and 588 deletions

View File

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

View File

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

View File

@ -9,175 +9,244 @@
import SwiftUI import SwiftUI
import SimpleXChat 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 { 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 @EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@State private var displayName: String = "" @State private var displayName: String = ""
@State private var fullName: String = ""
@FocusState private var focusDisplayName @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 { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Create your profile") Group {
.font(.largeTitle) Text("Create your profile")
.bold() .font(.largeTitle)
.padding(.bottom, 4) .bold()
.frame(maxWidth: .infinity) Text("Your profile, contacts and delivered messages are stored on your device.")
Text("Your profile, contacts and delivered messages are stored on your device.") .foregroundColor(.secondary)
.padding(.bottom, 4) Text("The profile is only shared with your contacts.")
Text("The profile is only shared with your contacts.") .foregroundColor(.secondary)
.padding(.bottom) .padding(.bottom)
}
.padding(.bottom)
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
if !validDisplayName(displayName) { let name = displayName.trimmingCharacters(in: .whitespaces)
Image(systemName: "exclamationmark.circle") let validName = mkValidName(name)
.foregroundColor(.red) if name != validName {
.padding(.top, 4) 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) .focused($focusDisplayName)
.submitLabel(.next) .padding(.leading, 32)
.onSubmit {
if canCreateProfile() { focusFullName = true }
else { focusDisplayName = true }
}
} }
textField("Full name (optional)", text: $fullName) .padding(.bottom)
.focused($focusFullName)
.submitLabel(.go)
.onSubmit {
if canCreateProfile() { createProfile() }
else { focusFullName = true }
}
Spacer() Spacer()
onboardingButtons()
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())
}
}
} }
.onAppear() { .onAppear() {
focusDisplayName = true focusDisplayName = true
setLastVersionDefault() setLastVersionDefault()
} }
.alert(item: $alert) { a in
switch a {
case .duplicateUserError: return duplicateUserAlert
case let .createUserError(err): return creatUserErrorAlert(err)
}
}
.padding() .padding()
.frame(maxWidth: .infinity, alignment: .leading)
.keyboardPadding() .keyboardPadding()
} }
func textField(_ placeholder: LocalizedStringKey, text: Binding<String>) -> some View { func onboardingButtons() -> some View {
TextField(placeholder, text: text) HStack {
.textInputAutocapitalization(.never) Button {
.disableAutocorrection(true) hideKeyboard()
.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()
withAnimation { withAnimation {
onboardingStageDefault.set(.step3_CreateSimpleXAddress) m.onboardingStage = .step1_SimpleXInfo
m.onboardingStage = .step3_CreateSimpleXAddress
} }
} else { } label: {
onboardingStageDefault.set(.onboardingComplete) HStack {
m.onboardingStage = .onboardingComplete Image(systemName: "lessthan")
dismiss() Text("About SimpleX")
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)
} }
} }
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 { private func showAlert(_ alert: UserProfileAlert) {
displayName != "" && validDisplayName(displayName) AlertManager.shared.showAlert(userProfileAlert(alert, $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 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 { 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 { struct CreateProfile_Previews: PreviewProvider {

View File

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

View File

@ -381,7 +381,9 @@ struct ProfilePreview: View {
Text(profileOf.displayName) Text(profileOf.displayName)
.fontWeight(.bold) .fontWeight(.bold)
.font(.title2) .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 showImagePicker = false
@State private var showTakePhoto = false @State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil @State private var chosenImage: UIImage? = nil
@State private var alert: UserProfileAlert?
@FocusState private var focusDisplayName
var body: some View { var body: some View {
let user: User = chatModel.currentUser! let user: User = chatModel.currentUser!
@ -47,18 +49,27 @@ struct UserProfile: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
if !validDisplayName(profile.displayName) { if !validNewProfileName(user) {
Image(systemName: "exclamationmark.circle") Button {
.foregroundColor(.red) alert = .invalidNameError(validName: mkValidName(profile.displayName))
.padding(.bottom, 10) } 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) { HStack(spacing: 20) {
Button("Cancel") { editProfile = false } Button("Cancel") { editProfile = false }
Button("Save (and notify contacts)") { saveProfile() } Button("Save (and notify contacts)") { saveProfile() }
.disabled(profile.displayName == "" || !validDisplayName(profile.displayName)) .disabled(!canSaveProfile(user))
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@ -74,11 +85,14 @@ struct UserProfile: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
VStack(alignment: .leading) { VStack(alignment: .leading) {
profileNameView("Display name:", user.profile.displayName) profileNameView("Profile name:", user.profile.displayName)
profileNameView("Full name:", user.profile.fullName) if showFullName(user) {
profileNameView("Full name:", user.profile.fullName)
}
Button("Edit") { Button("Edit") {
profile = fromLocalProfile(user.profile) profile = fromLocalProfile(user.profile)
editProfile = true editProfile = true
focusDisplayName = true
} }
} }
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@ -117,14 +131,12 @@ struct UserProfile: View {
profile.image = nil profile.image = nil
} }
} }
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
} }
func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View { func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View {
TextField(label, text: name) TextField(label, text: name)
.textInputAutocapitalization(.never) .padding(.leading, 32)
.disableAutocorrection(true)
.padding(.bottom)
.padding(.leading, 28)
} }
func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View { func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View {
@ -141,9 +153,22 @@ struct UserProfile: View {
showChooseSource = true 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.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user)
}
func saveProfile() { func saveProfile() {
Task { Task {
do { do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
DispatchQueue.main.async { DispatchQueue.main.async {
chatModel.updateCurrentUser(newProfile) 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_markdown(char *str);
extern char *chat_parse_server(char *str); extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt); 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_encrypt_media(char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len);

View File

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

View File

@ -50,6 +50,7 @@ actual fun PlatformTextField(
userIsObserver: Boolean, userIsObserver: Boolean,
onMessageChange: (String) -> Unit, onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit, onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
onDone: () -> Unit, onDone: () -> Unit,
) { ) {
val cs = composeState.value val cs = composeState.value

View File

@ -5,6 +5,8 @@ import android.os.Build
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatItem import chat.simplex.common.model.ChatItem
@ -41,3 +43,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
showMenu.value = false showMenu.value = false
}) })
} }
actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) {
clipboard.setText(AnnotatedString(cItem.content.text))
}

View File

@ -46,6 +46,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(const char *str); extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern char *chat_write_file(const char *path, char *ptr, int length); extern char *chat_write_file(const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(const char *from_path, const char *to_path); extern char *chat_encrypt_file(const char *from_path, const char *to_path);
@ -121,6 +122,14 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused
return res; return res;
} }
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) {
const char *_name = (*env)->GetStringUTFChars(env, name, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_valid_name(_name));
(*env)->ReleaseStringUTFChars(env, name, _name);
return res;
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);

View File

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

View File

@ -21,6 +21,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(const char *str); extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern char *chat_write_file(const char *path, char *ptr, int length); extern char *chat_write_file(const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(const char *from_path, const char *to_path); extern char *chat_encrypt_file(const char *from_path, const char *to_path);
@ -75,7 +76,7 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla
jstring res = decode_to_utf8_string(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); jstring res = decode_to_utf8_string(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm); (*env)->ReleaseStringUTFChars(env, confirm, _confirm);
// Creating array of Object's (boxed values can be passed, eg. Long instead of long) // Creating array of Object's (boxed values can be passed, eg. Long instead of long)
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
@ -133,6 +134,14 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass cl
return res; return res;
} }
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) {
const char *_name = encode_to_utf8_chars(env, name);
jstring res = decode_to_utf8_string(env, chat_valid_name(_name));
(*env)->ReleaseStringUTFChars(env, name, _name);
return res;
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path); const char *_path = encode_to_utf8_chars(env, path);

View File

@ -17,6 +17,7 @@ import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateFirstProfile
import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.common.views.SplashView import chat.simplex.common.views.SplashView
import chat.simplex.common.views.call.ActiveCallView import chat.simplex.common.views.call.ActiveCallView
@ -135,7 +136,7 @@ fun MainScreen() {
ModalManager.fullscreen.showInView() ModalManager.fullscreen.showInView()
} }
} }
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {} onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel) onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)

View File

@ -20,6 +20,7 @@ external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
external fun chatParseMarkdown(str: String): String external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String
external fun chatWriteFile(path: String, buffer: ByteBuffer): String external fun chatWriteFile(path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(fromPath: String, toPath: String): String external fun chatEncryptFile(fromPath: String, toPath: String): String

View File

@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import chat.simplex.common.views.chat.ComposeState import chat.simplex.common.views.chat.ComposeState
import java.io.File
import java.net.URI
@Composable @Composable
expect fun PlatformTextField( expect fun PlatformTextField(
@ -14,5 +16,6 @@ expect fun PlatformTextField(
userIsObserver: Boolean, userIsObserver: Boolean,
onMessageChange: (String) -> Unit, onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit, onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
onDone: () -> Unit, onDone: () -> Unit,
) )

View File

@ -97,6 +97,7 @@ fun TerminalLayout(
updateLiveMessage = null, updateLiveMessage = null,
editPrevMessage = {}, editPrevMessage = {},
onMessageChange = ::onMessageChange, onMessageChange = ::onMessageChange,
onFilesPasted = {},
textStyle = textStyle textStyle = textStyle
) )
} }

View File

@ -1,10 +1,10 @@
package chat.simplex.common.views package chat.simplex.common.views
import SectionTextFooter
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -18,115 +18,160 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.* import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.Profile import chat.simplex.common.model.Profile
import chat.simplex.common.platform.appPlatform import chat.simplex.common.platform.*
import chat.simplex.common.platform.navigationBarsWithImePadding
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
fun isValidDisplayName(name: String) : Boolean {
return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#")
}
@Composable @Composable
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) { fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val displayName = rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope()
val fullName = rememberSaveable { mutableStateOf("") } val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() } val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
Column( ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) Box(
) { modifier = Modifier
/*CloseSheetBar(close = { .fillMaxSize()
if (chatModel.users.isEmpty()) { .padding(top = 20.dp)
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo ) {
} else { val displayName = rememberSaveable { mutableStateOf("") }
close() val focusRequester = remember { FocusRequester() }
}
})*/ Column(
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) ) {
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1) Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1) AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
Spacer(Modifier.height(DEFAULT_PADDING)) Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(
Text( stringResource(MR.strings.display_name),
stringResource(MR.strings.display_name), fontSize = 16.sp
fontSize = 16.sp )
) val name = displayName.value.trim()
if (!isValidDisplayName(displayName.value)) { val validName = mkValidName(name)
Text( Spacer(Modifier.height(20.dp))
stringResource(MR.strings.no_spaces), if (name != validName) {
fontSize = 16.sp, IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) {
color = Color.Red Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error)
) }
} }
} }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING)) }
Text( SettingsActionItem(
stringResource(MR.strings.full_name_optional__prompt), painterResource(MR.images.ic_check),
fontSize = 16.sp, stringResource(MR.strings.create_another_profile_button),
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) disabled = !canCreateProfile(displayName.value),
) textColor = MaterialTheme.colors.primary,
ProfileNameField(fullName, "") iconColor = MaterialTheme.colors.primary,
} click = { createProfileInProfiles(chatModel, displayName.value, close) },
Spacer(Modifier.fillMaxHeight().weight(1f)) )
Row { SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device))
if (chatModel.users.isEmpty()) { SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts))
SimpleButtonDecorated(
text = stringResource(MR.strings.about_simplex), LaunchedEffect(Unit) {
icon = painterResource(MR.images.ic_arrow_back_ios_new), delay(300)
textDecoration = TextDecoration.None, focusRequester.requestFocus()
fontWeight = FontWeight.Medium }
) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } }
} if (savedKeyboardState != keyboardState) {
Spacer(Modifier.fillMaxWidth().weight(1f)) LaunchedEffect(keyboardState) {
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) scope.launch {
val createModifier: Modifier savedKeyboardState = keyboardState
val createColor: Color scrollState.animateScrollTo(scrollState.maxValue)
if (enabled) {
createModifier = Modifier.clickable {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
createProfileInProfiles(chatModel, displayName.value, fullName.value, close)
} else {
createProfileOnboarding(chatModel, displayName.value, fullName.value, close)
} }
}.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
createColor = MaterialTheme.colors.secondary
}
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
} }
} }
}
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
} }
} }
} }
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { @Composable
fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 20.dp)
) {
val displayName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
/*CloseSheetBar(close = {
if (chatModel.users.isEmpty()) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
close()
}
})*/
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING)
ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
Spacer(Modifier.height(DEFAULT_PADDING))
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(MR.strings.display_name),
fontSize = 16.sp
)
val name = displayName.value.trim()
val validName = mkValidName(name)
Spacer(Modifier.height(20.dp))
if (name != validName) {
IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) {
Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error)
}
}
}
ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
OnboardingButtons(displayName, close)
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
}
}
LaunchedEffect(Unit) {
setLastVersionDefault(chatModel)
}
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
savedKeyboardState = keyboardState
scrollState.animateScrollTo(scrollState.maxValue)
}
}
}
}
}
}
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withApi {
val user = chatModel.controller.apiCreateActiveUser( val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null) Profile(displayName.trim(), "", null)
) ?: return@withApi ) ?: return@withApi
chatModel.currentUser.value = user chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) { if (chatModel.users.isEmpty()) {
@ -142,10 +187,10 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName:
} }
} }
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withApi {
chatModel.controller.apiCreateActiveUser( chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null) Profile(displayName.trim(), "", null)
) ?: return@withApi ) ?: return@withApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) { if (chatModel.users.isEmpty()) {
@ -163,6 +208,28 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName:
} }
} }
@Composable
fun OnboardingButtons(displayName: MutableState<String>, close: () -> Unit) {
Row {
SimpleButtonDecorated(
text = stringResource(MR.strings.about_simplex),
icon = painterResource(MR.images.ic_arrow_back_ios_new),
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }
Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = canCreateProfile(displayName.value)
val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp)
val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor)
}
}
}
}
@Composable @Composable
fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) {
var valid by rememberSaveable { mutableStateOf(true) } var valid by rememberSaveable { mutableStateOf(true) }
@ -195,10 +262,6 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
onValueChange = { name.value = it }, onValueChange = { name.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground), textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true, singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.secondary) cursorBrush = SolidColor(MaterialTheme.colors.secondary)
) )
@ -211,3 +274,28 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
} }
} }
} }
private fun canCreateProfile(displayName: String): Boolean {
val name = displayName.trim()
return name.isNotEmpty() && mkValidName(name) == name
}
fun showInvalidNameAlert(name: String, displayName: MutableState<String>) {
if (name.isEmpty()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_name),
)
} else {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.invalid_name),
text = generalGetString(MR.strings.correct_name_to).format(name),
onConfirm = {
displayName.value = name
}
)
}
}
fun isValidDisplayName(name: String) : Boolean = mkValidName(name.trim()) == name
fun mkValidName(s: String): String = chatValidName(s)

View File

@ -458,17 +458,7 @@ fun ChatLayout(
.fillMaxWidth() .fillMaxWidth()
.desktopOnExternalDrag( .desktopOnExternalDrag(
enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value,
onFiles = { paths -> onFiles = { paths -> composeState.onFilesAttached(paths.map { URI.create(it) }) },
val uris = paths.map { URI.create(it) }
val groups = uris.groupBy { isImage(it) }
val images = groups[true] ?: emptyList()
val files = groups[false] ?: emptyList()
if (images.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(images, null) }
} else if (files.isNotEmpty()) {
composeState.processPickedFile(uris.first(), null)
}
},
onImage = { onImage = {
val tmpFile = File.createTempFile("image", ".bmp", tmpDir) val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
tmpFile.deleteOnExit() tmpFile.deleteOnExit()

View File

@ -159,6 +159,17 @@ expect fun AttachmentSelection(
processPickedMedia: (List<URI>, String?) -> Unit processPickedMedia: (List<URI>, String?) -> Unit
) )
fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
val groups = uris.groupBy { isImage(it) }
val images = groups[true] ?: emptyList()
val files = groups[false] ?: emptyList()
if (images.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) }
} else if (files.isNotEmpty()) {
processPickedFile(uris.first(), null)
}
}
fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) { fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
if (uri != null) { if (uri != null) {
val fileSize = getFileSize(uri) val fileSize = getFileSize(uri)
@ -816,6 +827,7 @@ fun ComposeView(
chatModel.removeLiveDummy() chatModel.removeLiveDummy()
}, },
editPrevMessage = ::editPrevMessage, editPrevMessage = ::editPrevMessage,
onFilesPasted = { composeState.onFilesAttached(it) },
onMessageChange = ::onMessageChange, onMessageChange = ::onMessageChange,
textStyle = textStyle textStyle = textStyle
) )

View File

@ -29,6 +29,8 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File
import java.net.URI
@Composable @Composable
fun SendMsgView( fun SendMsgView(
@ -52,6 +54,7 @@ fun SendMsgView(
updateLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null,
cancelLiveMessage: (() -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null,
editPrevMessage: () -> Unit, editPrevMessage: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
onMessageChange: (String) -> Unit, onMessageChange: (String) -> Unit,
textStyle: MutableState<TextStyle> textStyle: MutableState<TextStyle>
) { ) {
@ -79,7 +82,7 @@ fun SendMsgView(
val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) { PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
if (!cs.inProgress) { if (!cs.inProgress) {
sendMessage(null) sendMessage(null)
} }
@ -612,6 +615,7 @@ fun PreviewSendMsgView() {
sendMessage = {}, sendMessage = {},
editPrevMessage = {}, editPrevMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
onFilesPasted = {},
textStyle = textStyle textStyle = textStyle
) )
} }
@ -645,6 +649,7 @@ fun PreviewSendMsgViewEditing() {
sendMessage = {}, sendMessage = {},
editPrevMessage = {}, editPrevMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
onFilesPasted = {},
textStyle = textStyle textStyle = textStyle
) )
} }
@ -678,6 +683,7 @@ fun PreviewSendMsgViewInProgress() {
sendMessage = {}, sendMessage = {},
editPrevMessage = {}, editPrevMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
onFilesPasted = {},
textStyle = textStyle textStyle = textStyle
) )
} }

View File

@ -19,12 +19,12 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.isValidDisplayName
import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URI import java.net.URI
@ -65,13 +65,13 @@ fun GroupProfileLayout(
fullName.value == groupProfile.fullName && fullName.value == groupProfile.fullName &&
groupProfile.image == profileImage.value groupProfile.image == profileImage.value
val closeWithAlert = { val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) { if (dataUnchanged || !canUpdateProfile(displayName.value, groupProfile)) {
close() close()
} else { } else {
showUnsavedChangesAlert({ showUnsavedChangesAlert({
saveProfile( saveProfile(
groupProfile.copy( groupProfile.copy(
displayName = displayName.value, displayName = displayName.value.trim(),
fullName = fullName.value, fullName = fullName.value,
image = profileImage.value image = profileImage.value
) )
@ -125,32 +125,32 @@ fun GroupProfileLayout(
stringResource(MR.strings.group_display_name_field), stringResource(MR.strings.group_display_name_field),
fontSize = 16.sp fontSize = 16.sp
) )
if (!isValidDisplayName(displayName.value)) { if (!isValidNewProfileName(displayName.value, groupProfile)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF)) Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text( IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) {
stringResource(MR.strings.no_spaces), Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error)
fontSize = 16.sp, }
color = Color.Red
)
} }
} }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) ProfileNameField(displayName, "", { isValidNewProfileName(it, groupProfile) }, focusRequester)
if (groupProfile.fullName.isNotEmpty() && groupProfile.fullName != groupProfile.displayName) {
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(MR.strings.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
}
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
Text( val enabled = !dataUnchanged && canUpdateProfile(displayName.value, groupProfile)
stringResource(MR.strings.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) { if (enabled) {
Text( Text(
stringResource(MR.strings.save_group_profile), stringResource(MR.strings.save_group_profile),
modifier = Modifier.clickable { modifier = Modifier.clickable {
saveProfile( saveProfile(
groupProfile.copy( groupProfile.copy(
displayName = displayName.value, displayName = displayName.value.trim(),
fullName = fullName.value, fullName = fullName.value,
image = profileImage.value image = profileImage.value
) )
@ -178,6 +178,12 @@ fun GroupProfileLayout(
} }
} }
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =
displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, groupProfile)
private fun isValidNewProfileName(displayName: String, groupProfile: GroupProfile): Boolean =
displayName == groupProfile.displayName || isValidDisplayName(displayName.trim())
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked( AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.save_preferences_question), title = generalGetString(MR.strings.save_preferences_question),

View File

@ -201,7 +201,7 @@ fun ChatItemView(
showMenu.value = false showMenu.value = false
}) })
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
clipboard.setText(AnnotatedString(cItem.content.text)) copyItemToClipboard(cItem, clipboard)
showMenu.value = false showMenu.value = false
}) })
if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && getLoadedFilePath(cItem.file) != null) { if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && getLoadedFilePath(cItem.file) != null) {
@ -561,6 +561,8 @@ private fun showMsgDeliveryErrorAlert(description: String) {
) )
} }
expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
@Preview @Preview
@Composable @Composable
fun PreviewChatItemView() { fun PreviewChatItemView() {

View File

@ -19,15 +19,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.ProfileNameField
import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chat.group.AddGroupMembersView
import chat.simplex.common.views.chatlist.setGroupMembers import chat.simplex.common.views.chatlist.setGroupMembers
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.isValidDisplayName
import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.views.usersettings.DeleteImageButton import chat.simplex.common.views.usersettings.DeleteImageButton
import chat.simplex.common.views.usersettings.EditImageButton import chat.simplex.common.views.usersettings.EditImageButton
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -60,7 +59,6 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val displayName = rememberSaveable { mutableStateOf("") } val displayName = rememberSaveable { mutableStateOf("") }
val fullName = rememberSaveable { mutableStateOf("") }
val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) } val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) }
val profileImage = rememberSaveable { mutableStateOf<String?>(null) } val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -110,31 +108,22 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
stringResource(MR.strings.group_display_name_field), stringResource(MR.strings.group_display_name_field),
fontSize = 16.sp fontSize = 16.sp
) )
if (!isValidDisplayName(displayName.value)) { if (!isValidDisplayName(displayName.value.trim())) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF)) Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text( IconButton({ showInvalidNameAlert(mkValidName(displayName.value.trim()), displayName) }, Modifier.size(20.dp)) {
stringResource(MR.strings.no_spaces), Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error)
fontSize = 16.sp, }
color = Color.Red
)
} }
} }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(MR.strings.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName, "")
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) val enabled = canCreateProfile(displayName.value)
if (enabled) { if (enabled) {
CreateGroupButton(MaterialTheme.colors.primary, Modifier CreateGroupButton(MaterialTheme.colors.primary, Modifier
.clickable { .clickable {
createGroup(GroupProfile( createGroup(GroupProfile(
displayName = displayName.value, displayName = displayName.value.trim(),
fullName = fullName.value, fullName = "",
image = profileImage.value image = profileImage.value
)) ))
} }
@ -167,6 +156,8 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
} }
} }
fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim())
@Preview @Preview
@Composable @Composable
fun PreviewAddGroupLayout() { fun PreviewAddGroupLayout() {

View File

@ -1,16 +1,5 @@
package chat.simplex.common.views.onboarding package chat.simplex.common.views.onboarding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.ProvideWindowInsets
import chat.simplex.common.views.CreateProfilePanel
import chat.simplex.common.platform.getKeyboardState
import kotlinx.coroutines.launch
enum class OnboardingStage { enum class OnboardingStage {
Step1_SimpleXInfo, Step1_SimpleXInfo,
Step2_CreateProfile, Step2_CreateProfile,
@ -19,32 +8,3 @@ enum class OnboardingStage {
Step4_SetNotificationsMode, Step4_SetNotificationsMode,
OnboardingComplete OnboardingComplete
} }
@Composable
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 20.dp)
) {
CreateProfilePanel(chatModel, close)
LaunchedEffect(Unit) {
setLastVersionDefault(chatModel)
}
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
savedKeyboardState = keyboardState
scrollState.animateScrollTo(scrollState.maxValue)
}
}
}
}
}
}

View File

@ -364,7 +364,7 @@ fun AppVersionItem(showVersion: () -> Unit) {
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (profileOf.fullName.isNotEmpty()) { if (profileOf.fullName.isNotEmpty() && profileOf.fullName != profileOf.displayName) {
Text( Text(
profileOf.fullName, profileOf.fullName,
Modifier.padding(vertical = 5.dp), Modifier.padding(vertical = 5.dp),

View File

@ -17,14 +17,12 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.ProfileNameField
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.isValidDisplayName
import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.Profile
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URI import java.net.URI
@ -39,7 +37,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
close, close,
saveProfile = { displayName, fullName, image -> saveProfile = { displayName, fullName, image ->
withApi { withApi {
val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image)) val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName.trim(), fullName = fullName, image = image))
if (updated != null) { if (updated != null) {
val (newProfile, _) = updated val (newProfile, _) = updated
chatModel.updateCurrentUser(newProfile) chatModel.updateCurrentUser(newProfile)
@ -89,7 +87,7 @@ fun UserProfileLayout(
profile.image == profileImage.value profile.image == profileImage.value
val closeWithAlert = { val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) { if (dataUnchanged || !canSaveProfile(displayName.value, profile)) {
close() close()
} else { } else {
showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close) showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close)
@ -128,36 +126,27 @@ fun UserProfileLayout(
stringResource(MR.strings.display_name__field), stringResource(MR.strings.display_name__field),
fontSize = 16.sp fontSize = 16.sp
) )
if (!isValidDisplayName(displayName.value)) { if (!isValidNewProfileName(displayName.value, profile)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF)) Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Text( IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) {
stringResource(MR.strings.no_spaces), Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error)
fontSize = 16.sp, }
color = Color.Red
)
} }
} }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester) ProfileNameField(displayName, "", { isValidNewProfileName(it, profile) }, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING)) if (showFullName(profile)) {
Text( Spacer(Modifier.height(DEFAULT_PADDING))
stringResource(MR.strings.full_name__field), Text(
fontSize = 16.sp, stringResource(MR.strings.full_name__field),
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) fontSize = 16.sp,
) modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
ProfileNameField(fullName) )
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val saveModifier: Modifier
val saveColor: Color
if (enabled) {
saveModifier = Modifier
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
saveColor = MaterialTheme.colors.primary
} else {
saveModifier = Modifier
saveColor = MaterialTheme.colors.secondary
} }
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && canSaveProfile(displayName.value, profile)
val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, profileImage.value) }
val saveColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
Text( Text(
stringResource(MR.strings.save_and_notify_contacts), stringResource(MR.strings.save_and_notify_contacts),
modifier = saveModifier, modifier = saveModifier,
@ -216,6 +205,15 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
) )
} }
private fun isValidNewProfileName(displayName: String, profile: Profile): Boolean =
displayName == profile.displayName || isValidDisplayName(displayName.trim())
private fun showFullName(profile: Profile): Boolean =
profile.fullName.isNotEmpty() && profile.fullName != profile.displayName
private fun canSaveProfile(displayName: String, profile: Profile): Boolean =
displayName.trim().isNotEmpty() && isValidNewProfileName(displayName, profile)
@Preview/*( @Preview/*(
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true, showBackground = true,

View File

@ -28,9 +28,8 @@ import chat.simplex.common.views.chatlist.UserProfilePickerItem
import chat.simplex.common.views.chatlist.UserProfileRow import chat.simplex.common.views.chatlist.UserProfileRow
import chat.simplex.common.views.database.PassphraseField import chat.simplex.common.views.database.PassphraseField
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.CreateProfile
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform import chat.simplex.common.platform.appPlatform
import chat.simplex.common.views.CreateProfile
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@ -675,7 +675,7 @@
<string name="your_contacts_will_see_it">Your contacts in SimpleX will see it.\nYou can change it in Settings.</string> <string name="your_contacts_will_see_it">Your contacts in SimpleX will see it.\nYou can change it in Settings.</string>
<!-- User profile details - UserProfileView.kt --> <!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Display name:</string> <string name="display_name__field">Profile name:</string>
<string name="full_name__field">Full name:</string> <string name="full_name__field">Full name:</string>
<string name="your_current_profile">Your current profile</string> <string name="your_current_profile">Your current profile</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</string>
@ -703,11 +703,12 @@
<string name="create_profile">Create profile</string> <string name="create_profile">Create profile</string>
<string name="your_profile_is_stored_on_your_device">Your profile, contacts and delivered messages are stored on your device.</string> <string name="your_profile_is_stored_on_your_device">Your profile, contacts and delivered messages are stored on your device.</string>
<string name="profile_is_only_shared_with_your_contacts">The profile is only shared with your contacts.</string> <string name="profile_is_only_shared_with_your_contacts">The profile is only shared with your contacts.</string>
<string name="no_spaces">No spaces!</string>
<string name="display_name_cannot_contain_whitespace">Display name cannot contain whitespace.</string> <string name="display_name_cannot_contain_whitespace">Display name cannot contain whitespace.</string>
<string name="display_name">Display Name</string> <string name="display_name">Enter your name:</string>
<string name="full_name_optional__prompt">Full Name (optional)</string>
<string name="create_profile_button">Create</string> <string name="create_profile_button">Create</string>
<string name="create_another_profile_button">Create profile</string>
<string name="invalid_name">Invalid name!</string>
<string name="correct_name_to">Correct name to %s?</string>
<string name="about_simplex">About SimpleX</string> <string name="about_simplex">About SimpleX</string>
<!-- markdown demo - MarkdownHelpView.kt --> <!-- markdown demo - MarkdownHelpView.kt -->
@ -1290,7 +1291,7 @@
<!-- AddGroupView.kt --> <!-- AddGroupView.kt -->
<string name="create_secret_group_title">Create secret group</string> <string name="create_secret_group_title">Create secret group</string>
<string name="group_is_decentralized">The group is fully decentralized it is visible only to the members.</string> <string name="group_is_decentralized">The group is fully decentralized it is visible only to the members.</string>
<string name="group_display_name_field">Group display name:</string> <string name="group_display_name_field">Enter group name:</string>
<string name="group_full_name_field">Group full name:</string> <string name="group_full_name_field">Group full name:</string>
<string name="group_main_profile_sent">Your chat profile will be sent to group members</string> <string name="group_main_profile_sent">Your chat profile will be sent to group members</string>

View File

@ -21,8 +21,6 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
actual val databaseExportDir: File = tmpDir actual val databaseExportDir: File = tmpDir
val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() }
actual fun desktopOpenDatabaseDir() { actual fun desktopOpenDatabaseDir() {
if (Desktop.isDesktopSupported()) { if (Desktop.isDesktopSupported()) {
try { 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" private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex"
val desktopPlatform = detectDesktopPlatform() val desktopPlatform = detectDesktopPlatform()
enum class DesktopPlatform(val libPath: String, val libExtension: String, val configPath: String, val dataPath: String) { enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) {
LINUX_X86_64("/libs/linux-x86_64", "so", unixConfigPath, unixDataPath), LINUX_X86_64("so", unixConfigPath, unixDataPath),
LINUX_AARCH64("/libs/aarch64", "so", unixConfigPath, unixDataPath), LINUX_AARCH64("so", unixConfigPath, unixDataPath),
WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), 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_X86_64("dylib", unixConfigPath, unixDataPath),
MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath); MAC_AARCH64("dylib", unixConfigPath, unixDataPath);
fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64
fun isWindows() = this == WINDOWS_X86_64 fun isWindows() = this == WINDOWS_X86_64

View File

@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
@ -27,6 +26,9 @@ import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.io.File
import java.net.URI
import kotlin.io.path.*
import kotlin.math.min import kotlin.math.min
import kotlin.text.substring import kotlin.text.substring
@ -39,6 +41,7 @@ actual fun PlatformTextField(
userIsObserver: Boolean, userIsObserver: Boolean,
onMessageChange: (String) -> Unit, onMessageChange: (String) -> Unit,
onUpArrow: () -> Unit, onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
onDone: () -> Unit, onDone: () -> Unit,
) { ) {
val cs = composeState.value val cs = composeState.value
@ -63,10 +66,20 @@ actual fun PlatformTextField(
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) } val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
val textFieldValue = textFieldValueState.copy(text = cs.message) val textFieldValue = textFieldValueState.copy(text = cs.message)
val clipboard = LocalClipboardManager.current
BasicTextField( BasicTextField(
value = textFieldValue, value = textFieldValue,
onValueChange = { onValueChange = onValueChange@ {
if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it.text != "")) { if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it.text != "")) {
val diff = textFieldValueState.selection.length + (it.text.length - textFieldValueState.text.length)
if (diff > 1 && it.text != textFieldValueState.text && it.selection.max - diff >= 0) {
val pasted = it.text.substring(it.selection.max - diff, it.selection.max)
val files = parseToFiles(AnnotatedString(pasted))
if (files.isNotEmpty()) {
onFilesPasted(files)
return@onValueChange
}
}
textFieldValueState = it textFieldValueState = it
onMessageChange(it.text) onMessageChange(it.text)
} }
@ -98,6 +111,12 @@ actual fun PlatformTextField(
} else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) { } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) {
onUpArrow() onUpArrow()
true true
} else if (it.key == Key.V &&
it.type == KeyEventType.KeyDown &&
((it.isCtrlPressed && !desktopPlatform.isMac()) || (it.isMetaPressed && desktopPlatform.isMac())) &&
parseToFiles(clipboard.getText()).isNotEmpty()) {
onFilesPasted(parseToFiles(clipboard.getText()))
true
} }
else false else false
}, },
@ -142,3 +161,19 @@ private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextS
style = textStyle.value.copy(fontStyle = FontStyle.Italic) style = textStyle.value.copy(fontStyle = FontStyle.Italic)
) )
} }
private fun parseToFiles(text: AnnotatedString?): List<URI> {
text ?: return emptyList()
val files = ArrayList<URI>()
text.lines().forEach {
try {
val uri = File(it.removePrefix("\"").removeSuffix("\"")).toURI()
val path = uri.toPath()
if (!path.exists() || !path.isAbsolute || path.isDirectory()) return emptyList()
files.add(uri)
} catch (e: Exception) {
return emptyList()
}
}
return files
}

View File

@ -7,17 +7,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatItem import chat.simplex.common.model.*
import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.*
import chat.simplex.common.platform.FileChooserLauncher
import chat.simplex.common.platform.desktopPlatform
import chat.simplex.common.ui.theme.EmojiFont import chat.simplex.common.ui.theme.EmojiFont
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import java.io.File
import java.util.*
@Composable @Composable
actual fun ReactionIcon(text: String, fontSize: TextUnit) { actual fun ReactionIcon(text: String, fontSize: TextUnit) {
@ -39,3 +41,23 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
showMenu.value = false showMenu.value = false
}) })
} }
actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) {
val fileSource = getLoadedFileSource(cItem.file)
if (fileSource != null) {
val filePath: String = if (fileSource.cryptoArgs != null) {
val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit()
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath)
tmpFile.absolutePath
} else {
getAppFilePath(fileSource.filePath)
}
when {
desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\""))
else -> clipboard.setText(AnnotatedString(filePath))
}
} else {
clipboard.setText(AnnotatedString(cItem.content.text))
}
}

View File

@ -52,6 +52,7 @@ compose {
} }
//includeAllModules = true //includeAllModules = true
outputBaseDir.set(project.file("../release")) outputBaseDir.set(project.file("../release"))
appResourcesRootDir.set(project.file("../build/links"))
targetFormats( targetFormats(
TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe TargetFormat.Deb, TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe
//, TargetFormat.AppImage // Gradle doesn't sync on Mac with it //, TargetFormat.AppImage // Gradle doesn't sync on Mac with it
@ -156,11 +157,10 @@ tasks.named("compileJava") {
afterEvaluate { afterEvaluate {
tasks.create("cmakeBuildAndCopy") { tasks.create("cmakeBuildAndCopy") {
dependsOn("cmakeBuild") dependsOn("cmakeBuild")
val copyDetails = mutableMapOf<String, ArrayList<FileCopyDetails>>()
doLast { doLast {
copy { copy {
from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps") from("${project(":desktop").buildDir}/cmake/main/linux-amd64")
into("src/jvmMain/resources/libs/linux-x86_64") into("$cppPath/desktop/libs/linux-x86_64")
include("*.so*") include("*.so*")
eachFile { eachFile {
path = name path = name
@ -169,16 +169,8 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
} }
copy { copy {
val destinationDir = "src/jvmMain/resources/libs/linux-x86_64/vlc" from("${project(":desktop").buildDir}/cmake/main/linux-aarch64")
from("$cppPath/desktop/libs/linux-x86_64/deps/vlc") into("$cppPath/desktop/libs/linux-aarch64")
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")
include("*.so*") include("*.so*")
eachFile { eachFile {
path = name path = name
@ -187,16 +179,18 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
} }
copy { copy {
val destinationDir = "src/jvmMain/resources/libs/linux-aarch64/vlc" from("${project(":desktop").buildDir}/cmake/main/windows-amd64")
from("$cppPath/desktop/libs/linux-aarch64/deps/vlc") into("$cppPath/desktop/libs/windows-x86_64")
into(destinationDir) include("*.dll")
eachFile {
path = name
}
includeEmptyDirs = false includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
copyIfNeeded(destinationDir, copyDetails)
} }
copy { copy {
from("${project(":desktop").buildDir}/cmake/main/windows-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps") from("${project(":desktop").buildDir}/cmake/main/windows-amd64")
into("src/jvmMain/resources/libs/windows-x86_64") into("../build/links/windows-x64")
include("*.dll") include("*.dll")
eachFile { eachFile {
path = name path = name
@ -205,16 +199,8 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
} }
copy { copy {
val destinationDir = "src/jvmMain/resources/libs/windows-x86_64/vlc" from("${project(":desktop").buildDir}/cmake/main/mac-x86_64")
from("$cppPath/desktop/libs/windows-x86_64/deps/vlc") into("$cppPath/desktop/libs/mac-x86_64")
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")
include("*.dylib") include("*.dylib")
eachFile { eachFile {
path = name path = name
@ -223,16 +209,8 @@ afterEvaluate {
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
} }
copy { copy {
val destinationDir = "src/jvmMain/resources/libs/mac-x86_64/vlc" from("${project(":desktop").buildDir}/cmake/main/mac-aarch64")
from("$cppPath/desktop/libs/mac-x86_64/deps/vlc") into("$cppPath/desktop/libs/mac-aarch64")
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")
include("*.dylib") include("*.dylib")
eachFile { eachFile {
path = name path = name
@ -240,39 +218,6 @@ afterEvaluate {
includeEmptyDirs = false includeEmptyDirs = false
duplicatesStrategy = DuplicatesStrategy.INCLUDE 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") @Suppress("UnsafeDynamicallyLoadedCode")
private fun initHaskell() { private fun initHaskell() {
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs") val resourcesDir = File(System.getProperty("compose.application.resources.dir"))
copyResources(desktopPlatform.libPath, libsTmpDir.toPath()) val vlcDir = File(resourcesDir.absolutePath + File.separator + "vlc")
vlcDir.deleteRecursively()
Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING)
if (desktopPlatform == DesktopPlatform.WINDOWS_X86_64) { if (desktopPlatform == DesktopPlatform.WINDOWS_X86_64) {
windowsLoadRequiredLibs(libsTmpDir) windowsLoadRequiredLibs(resourcesDir, vlcDir)
} else { } 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 // 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" //val libXcb = "libvlc_xcb_events.so.0.0.0"
//System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath) //System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath)
System.setProperty("jna.library.path", vlcDir.absolutePath) System.setProperty("jna.library.path", vlcDir.absolutePath)
//discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath) //discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath)
libsTmpDir.deleteRecursively()
initHS() initHS()
} }
private fun copyResources(from: String, to: Path) { private fun windowsLoadRequiredLibs(libsTmpDir: File, vlcDir: File) {
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) {
val mainLibs = arrayOf( val mainLibs = arrayOf(
"libcrypto-3-x64.dll", "libcrypto-3-x64.dll",
"mcfgthread-12.dll",
"libgcc_s_seh-1.dll",
"libstdc++-6.dll",
"libffi-8.dll", "libffi-8.dll",
"libgmp-10.dll", "libgmp-10.dll",
"libsimplex.dll", "libsimplex.dll",
"libapp-lib.dll" "libapp-lib.dll"
) )
@ -72,7 +50,7 @@ private fun windowsLoadRequiredLibs(libsTmpDir: File) {
val vlcLibs = arrayOf( val vlcLibs = arrayOf(
"libvlccore.dll", "libvlccore.dll",
"libvlc.dll", "libvlc.dll",
"axvlc.dll", "axvlc.dll",
"npvlc.dll" "npvlc.dll"
) )
vlcLibs.forEach { vlcLibs.forEach {

View File

@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 899d26e8c8a66d903b98ad64bb068803cfa3d81d tag: 9f0f53d2ec34de09ab684036844578ed5994c24b
source-repository-package source-repository-package
type: git type: git
@ -34,7 +34,7 @@ source-repository-package
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/aeson.git location: https://github.com/simplex-chat/aeson.git
tag: 3eb66f9a68f103b5f1489382aad89f5712a64db7 tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b
source-repository-package source-repository-package
type: git type: git

View File

@ -13,7 +13,7 @@ extra-source-files:
- cabal.project - cabal.project
dependencies: dependencies:
- aeson == 2.0.* - aeson == 2.2.*
- ansi-terminal >= 0.10 && < 0.12 - ansi-terminal >= 0.10 && < 0.12
- async == 2.2.* - async == 2.2.*
- attoparsec == 0.14.* - attoparsec == 0.14.*

View File

@ -1,9 +1,23 @@
#!/bin/bash #!/bin/bash
set -e
function readlink() {
echo "$(cd "$(dirname "$1")"; pwd -P)"
}
OS=linux OS=linux
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`} ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
GHC_VERSION=8.10.7 GHC_VERSION=8.10.7
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-* BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
rm -rf $BUILD_DIR rm -rf $BUILD_DIR
@ -11,16 +25,20 @@ cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN' --ghc-optio
cd $BUILD_DIR/build cd $BUILD_DIR/build
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-rpath '$ORIGIN' 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/ ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
cd - cd -
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ 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 rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
scripts/desktop/prepare-vlc-linux.sh scripts/desktop/prepare-vlc-linux.sh
links_dir=apps/multiplatform/build/links
mkdir -p $links_dir
cd $links_dir
ln -sfT ../../common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ $OS-$COMPOSE_ARCH

View File

@ -4,8 +4,12 @@ set -e
OS=mac OS=mac
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
COMPOSE_ARCH=$ARCH
if [ "$ARCH" == "arm64" ]; then if [ "$ARCH" == "arm64" ]; then
ARCH=aarch64 ARCH=aarch64
else
COMPOSE_ARCH=x64
fi fi
LIB_EXT=dylib LIB_EXT=dylib
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
@ -66,30 +70,29 @@ rm deps/`basename $LIB`
cd - cd -
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ 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 rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSdrct-*.$LIB_EXT
cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT cp $LIBCRYPTO_PATH libcrypto.1.1.$LIB_EXT
chmod 755 deps/libcrypto.1.1.$LIB_EXT chmod 755 libcrypto.1.1.$LIB_EXT
install_name_tool -id "libcrypto.1.1.$LIB_EXT" deps/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" deps/libffi.$LIB_EXT install_name_tool -id "libffi.8.$LIB_EXT" libffi.$LIB_EXT
LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11) LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11)
if [ -n "$LIBCRYPTO_PATH" ]; then if [ -n "$LIBCRYPTO_PATH" ]; then
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB
fi 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 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 fi
for lib in $(find . -type f -name "*.$LIB_EXT"); do for lib in $(find . -type f -name "*.$LIB_EXT"); do
@ -108,3 +111,9 @@ fi
cd - cd -
scripts/desktop/prepare-vlc-mac.sh 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")")")" root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
OS=windows OS=windows
ARCH=`uname -a | rev | cut -d' ' -f2 | rev` ARCH="x86_64"
JOB_REPO=${1:-$SIMPLEX_CI_REPO_URL} JOB_REPO=${1:-$SIMPLEX_CI_REPO_URL}
if [ "$ARCH" == "aarch64" ]; then
COMPOSE_ARCH=arm64
else
COMPOSE_ARCH=x64
fi
cd $root_dir cd $root_dir
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ 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 rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
scripts/desktop/download-lib-windows.sh $JOB_REPO scripts/desktop/download-lib-windows.sh $JOB_REPO
scripts/desktop/prepare-vlc-windows.sh 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 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 exit 1
fi fi
@ -16,12 +16,15 @@ arch=x86_64
root_dir="$(dirname "$(dirname "$(readlink "$0")")")" root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
output_dir="$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/windows-$arch/" 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 && \ curl --location -o libsimplex.zip $job_repo/$arch-linux.$arch-windows:lib:simplex-chat/latest/download/1 && \
$WINDIR\\System32\\tar.exe -xf libsimplex.zip && \ $WINDIR\\System32\\tar.exe -xf libsimplex.zip && \
mv libsimplex.dll "$output_dir" && \ mv libsimplex.dll "$output_dir" && \
mv libcrypto*.dll "$output_dir/deps" && \ mv libcrypto*.dll "$output_dir" && \
mv libffi*.dll "$output_dir/deps" && \ mv libffi*.dll "$output_dir" && \
mv libgmp*.dll "$output_dir/deps" && \ 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 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 release_app_dir=$root_dir/apps/multiplatform/release/main/app
cd $multiplatform_dir 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) 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/*/deps/`basename $libcrypto_path` 2> /dev/null || true" EXIT 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/*/deps cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*
./gradlew createDistributable ./gradlew createDistributable
rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path`
rm desktop/src/jvmMain/resources/libs/*/`basename $libcrypto_path`
rm -rf $release_app_dir/AppDir 2>/dev/null rm -rf $release_app_dir/AppDir 2>/dev/null
mkdir -p $release_app_dir/AppDir/usr mkdir -p $release_app_dir/AppDir/usr

View File

@ -6,7 +6,7 @@ function readlink() {
echo "$(cd "$(dirname "$1")"; pwd -P)" echo "$(cd "$(dirname "$1")"; pwd -P)"
} }
root_dir="$(dirname "$(dirname "$(readlink "$0")")")" 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 mkdir $vlc_dir || exit 0

View File

@ -16,7 +16,7 @@ function readlink() {
} }
root_dir="$(dirname "$(dirname "$(readlink "$0")")")" 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 #rm -rf $vlc_dir
mkdir -p $vlc_dir/vlc || exit 0 mkdir -p $vlc_dir/vlc || exit 0

View File

@ -6,7 +6,7 @@ function readlink() {
echo "$(cd "$(dirname "$1")"; pwd -P)" echo "$(cd "$(dirname "$1")"; pwd -P)"
} }
root_dir="$(dirname "$(dirname "$(readlink "$0")")")" 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 rm -rf $vlc_dir
mkdir -p $vlc_dir/vlc || exit 0 mkdir -p $vlc_dir/vlc || exit 0

View File

@ -1,10 +1,10 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."899d26e8c8a66d903b98ad64bb068803cfa3d81d" = "0jj5wl3l0r6gf01bwimmalr12s8c0jcdbbfhhyi0mivph886319r"; "https://github.com/simplex-chat/simplexmq.git"."9f0f53d2ec34de09ab684036844578ed5994c24b" = "0n8as86gaaws3yqy8kvflpqw8jgjzdyrskb59aqs9k4yimg6wk50";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr";
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
} }

View File

@ -146,7 +146,7 @@ library
src src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@ -194,7 +194,7 @@ executable simplex-bot
apps/simplex-bot apps/simplex-bot
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@ -243,7 +243,7 @@ executable simplex-bot-advanced
apps/simplex-bot-advanced apps/simplex-bot-advanced
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@ -294,7 +294,7 @@ executable simplex-broadcast-bot
apps/simplex-broadcast-bot/src apps/simplex-broadcast-bot/src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@ -344,7 +344,7 @@ executable simplex-chat
apps/simplex-chat apps/simplex-chat
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@ -398,7 +398,7 @@ executable simplex-directory-service
apps/simplex-directory-service/src apps/simplex-directory-service/src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*
@ -472,7 +472,7 @@ test-suite simplex-chat-test
apps/simplex-directory-service/src apps/simplex-directory-service/src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends: build-depends:
aeson ==2.0.* aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12 , ansi-terminal >=0.10 && <0.12
, async ==2.2.* , async ==2.2.*
, attoparsec ==0.14.* , attoparsec ==0.14.*

View File

@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq # - ../simplexmq
- github: simplex-chat/simplexmq - github: simplex-chat/simplexmq
commit: 899d26e8c8a66d903b98ad64bb068803cfa3d81d commit: 9f0f53d2ec34de09ab684036844578ed5994c24b
- github: kazu-yamamoto/http2 - github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher # - ../direct-sqlcipher
@ -60,7 +60,7 @@ extra-deps:
commit: 5e154a2aeccc33ead6c243ec07195ab673137221 commit: 5e154a2aeccc33ead6c243ec07195ab673137221
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
- github: simplex-chat/aeson - github: simplex-chat/aeson
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 commit: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b
- github: simplex-chat/haskell-terminal - github: simplex-chat/haskell-terminal
commit: f708b00009b54890172068f168bf98508ffcd495 commit: f708b00009b54890172068f168bf98508ffcd495
# #