diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 019a42fd82..768fdb6bdc 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -190,7 +190,7 @@ struct ComposeView: View { Image(systemName: "paperclip") .resizable() } - .disabled(composeState.editing || composeState.voiceMessageRecordingState != .noRecording) + .disabled(composeState.editing || !(composeState.noPreview || composeState.linkPreview != nil)) .frame(width: 25, height: 25) .padding(.bottom, 12) .padding(.leading, 12) @@ -209,6 +209,7 @@ struct ComposeView: View { }, finishVoiceMessageRecording: { finishVoiceMessageRecording() }, allowVoiceMessagesToContact: { allowVoiceMessagesToContact() }, + onImageAdded: { image in chosenImages = [image] }, keyboardVisible: $keyboardVisible ) .padding(.trailing, 12) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift new file mode 100644 index 0000000000..23e3e3f06a --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -0,0 +1,96 @@ +// +// NativeTextEditor.swift +// SimpleX (iOS) +// +// Created by Avently on 15.12.2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct NativeTextEditor: UIViewRepresentable { + @Binding var text: String + let height: CGFloat + let font: UIFont + @FocusState.Binding var focused: Bool + let alignment: TextAlignment + let onImageAdded: (UIImage) -> Void + + func makeUIView(context: Context) -> UITextView { + let field = CustomUITextField() + field.allowsEditingTextAttributes = true + field.text = text + field.font = font + field.textAlignment = alignment == .leading ? .left : .right + field.autocapitalizationType = .sentences + field.setOnTextChangedListener { newText, image in + text = newText + if let image = image { + onImageAdded(image) + } + } + field.setOnFocusChangedListener { focused = $0 } + field.delegate = field + field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) + return field + } + + func updateUIView(_ field: UITextView, context: Context) { + field.text = text + field.font = font + field.textAlignment = alignment == .leading ? .left : .right + } +} + +private class CustomUITextField: UITextView, UITextViewDelegate { + var onTextChanged: (String, UIImage?) -> Void = { newText, image in } + var onFocusChanged: (Bool) -> Void = { focused in } + + func setOnTextChangedListener(onTextChanged: @escaping (String, UIImage?) -> Void) { + self.onTextChanged = onTextChanged + } + + func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) { + self.onFocusChanged = onFocusChanged + } + + func textViewDidChange(_ textView: UITextView) { + var image: UIImage? = nil + textView.attributedText.enumerateAttribute( + NSAttributedString.Key.attachment, + in: NSRange(location: 0, length: textView.attributedText.length), + options: [], + using: { value, range, _ in + if let attachment = (value as? NSTextAttachment)?.image { + image = attachment + let newText = NSMutableAttributedString(attributedString: textView.attributedText) + newText.replaceCharacters(in: range, with: "") + textView.attributedText = newText + } + } + ) + onTextChanged(textView.text, image) + } + + func textViewDidBeginEditing(_ textView: UITextView) { + onFocusChanged(true) + } + + func textViewDidEndEditing(_ textView: UITextView) { + onFocusChanged(false) + } +} + +struct NativeTextEditor_Previews: PreviewProvider{ + static var previews: some View { + @FocusState var keyboardVisible: Bool + return NativeTextEditor( + text: Binding.constant("Hello, world!"), + height: 100, + font: UIFont.preferredFont(forTextStyle: .body), + focused: $keyboardVisible, + alignment: TextAlignment.leading, + onImageAdded: { _ in } + ) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index dca67ed3d2..eedae62e9e 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -18,11 +18,13 @@ struct SendMessageView: View { var startVoiceMessageRecording: (() -> Void)? = nil var finishVoiceMessageRecording: (() -> Void)? = nil var allowVoiceMessagesToContact: (() -> Void)? = nil + var onImageAdded: (UIImage) -> Void @State private var holdingVMR = false @Namespace var namespace @FocusState.Binding var keyboardVisible: Bool @State private var teHeight: CGFloat = 42 @State private var teFont: Font = .body + @State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body) var maxHeight: CGFloat = 360 var minHeight: CGFloat = 37 @@ -44,19 +46,25 @@ struct SendMessageView: View { .lineLimit(10) .font(teFont) .multilineTextAlignment(alignment) +// put text on top (after NativeTextEditor) and set color to precisely align it on changes +// .foregroundColor(.red) .foregroundColor(.clear) .padding(.horizontal, 10) - .padding(.vertical, 8) + .padding(.top, 8) + .padding(.bottom, 6) .matchedGeometryEffect(id: "te", in: namespace) .background(GeometryReader(content: updateHeight)) - TextEditor(text: $composeState.message) - .focused($keyboardVisible) - .font(teFont) - .textInputAutocapitalization(.sentences) - .multilineTextAlignment(alignment) - .padding(.horizontal, 5) - .allowsTightening(false) - .frame(height: teHeight) + + NativeTextEditor( + text: $composeState.message, + height: teHeight, + font: teUiFont, + focused: $keyboardVisible, + alignment: alignment, + onImageAdded: onImageAdded + ) + .allowsTightening(false) + .frame(height: teHeight) } } @@ -195,11 +203,11 @@ struct SendMessageView: View { private func updateHeight(_ g: GeometryProxy) -> Color { DispatchQueue.main.async { teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight) - teFont = isShortEmoji(composeState.message) - ? composeState.message.count < 4 - ? largeEmojiFont - : mediumEmojiFont - : .body + (teFont, teUiFont) = isShortEmoji(composeState.message) + ? composeState.message.count < 4 + ? (largeEmojiFont, largeEmojiUIFont) + : (mediumEmojiFont, mediumEmojiUIFont) + : (.body, UIFont.preferredFont(forTextStyle: .body)) } return Color.clear } @@ -220,6 +228,7 @@ struct SendMessageView_Previews: PreviewProvider { SendMessageView( composeState: $composeStateNew, sendMessage: {}, + onImageAdded: { _ in }, keyboardVisible: $keyboardVisible ) } @@ -229,6 +238,7 @@ struct SendMessageView_Previews: PreviewProvider { SendMessageView( composeState: $composeStateEditing, sendMessage: {}, + onImageAdded: { _ in }, keyboardVisible: $keyboardVisible ) } diff --git a/apps/ios/Shared/Views/Chat/Emoji.swift b/apps/ios/Shared/Views/Chat/Emoji.swift index bac1478b24..43b4efdaa2 100644 --- a/apps/ios/Shared/Views/Chat/Emoji.swift +++ b/apps/ios/Shared/Views/Chat/Emoji.swift @@ -28,4 +28,6 @@ func isShortEmoji(_ str: String) -> Bool { } let largeEmojiFont = Font.custom("Emoji", size: 48, relativeTo: .largeTitle) +let largeEmojiUIFont: UIFont = UIFont(name: "Emoji", size: 48) ?? UIFont.systemFont(ofSize: 48) let mediumEmojiFont = Font.custom("Emoji", size: 36, relativeTo: .largeTitle) +let mediumEmojiUIFont: UIFont = UIFont(name: "Emoji", size: 36) ?? UIFont.systemFont(ofSize: 36) diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 1c63ae2b86..5add9b77ba 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -87,6 +87,7 @@ struct TerminalView: View { composeState: $composeState, sendMessage: sendMessage, showVoiceMessageButton: false, + onImageAdded: { _ in }, keyboardVisible: $keyboardVisible ) .padding(.horizontal, 12) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index b8f7bbd4d4..c38efbf1b5 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; + D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -370,6 +371,7 @@ 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = ""; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = ""; }; + D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -702,6 +704,7 @@ 6454036E2822A9750090DDFF /* ComposeFileView.swift */, 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */, 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */, + D72A9087294BD7A70047C86D /* NativeTextEditor.swift */, ); path = ComposeMessage; sourceTree = ""; @@ -955,6 +958,7 @@ 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, 5C10D88A28F187F300E58BF0 /* FullScreenImageView.swift in Sources */, + D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */, 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */,