diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index bbca50a1bf..2413f765f4 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -26,7 +26,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain BROADCAST_UPLOAD_EXTENSION_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).broadcastUploadExtension // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 14.0 +IPHONEOS_DEPLOYMENT_TARGET = 15.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 SWIFT_VERSION = 5.6 diff --git a/Podfile b/Podfile index d408e4f72f..bd57ace72f 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project -platform :ios, '14.0' +platform :ios, '15.0' # By default, ignore all warnings from any pod inhibit_all_warnings! diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4a7151c773..28fdda2b19 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Cocoanetics/DTCoreText", "state" : { - "revision" : "9d2d4d2296e5d2d852a7d3c592b817d913a5d020", - "version" : "1.6.27" + "revision" : "b664664825da565b4c2b7a17dbe2369f68ae43d9", + "version" : "1.6.26" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "1100b217c04d096dfe072afb4484660ff794d805", - "version" : "2.2.2" + "revision" : "dfb74c89bf54b41ea000d564d6435ac6444ba6b4", + "version" : "2.18.0" } }, { diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift index 819eb632fd..6c7e43a90a 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift @@ -47,7 +47,9 @@ class HTMLFormatter: NSObject { var options: [AnyHashable: Any] = [ DTUseiOS6Attributes: true, - DTDefaultFontDescriptor: font.fontDescriptor, + DTDefaultFontFamily: font.familyName, + DTDefaultFontName: font.fontName, + DTDefaultFontSize: font.pointSize, DTDefaultLinkDecoration: false, DTDefaultLinkColor: ThemeService.shared().theme.colors.links, DTWillFlushBlockCallBack: sanitizeCallback diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 727ca8f806..3468ffe304 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -200,15 +200,7 @@ extension RoomViewController { optionalTextView?.becomeFirstResponder() originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) } - // This tirggers a SwiftUI update that is handled correctly on iOS 16, but needs to be dispatchted async on older versions - // Dispatching on iOS 16 instead causes some weird SwiftUI update behaviours - if #available(iOS 16, *) { - wysiwygInputToolbar.showKeyboard() - } else { - DispatchQueue.main.async { - wysiwygInputToolbar.showKeyboard() - } - } + roomInputToolbarContainer.removeFromSuperview() let dimmingView = UIView() dimmingView.translatesAutoresizingMaskIntoConstraints = false @@ -235,7 +227,18 @@ extension RoomViewController { } let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPanRoomToolbarContainer(_ :))) roomInputToolbarContainer.addGestureRecognizer(panGesture) - optionalTextView?.removeFromSuperview() + if let optionalTextView { + // This tirggers a SwiftUI update that is handled correctly on iOS 16, but needs to be dispatchted async on older versions + // Dispatching on iOS 16 instead causes some weird SwiftUI update behaviours + if #available(iOS 16, *) { + wysiwygInputToolbar.showKeyboard() + } else { + DispatchQueue.main.async { + wysiwygInputToolbar.showKeyboard() + } + } + optionalTextView.removeFromSuperview() + } } else { let originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) var optionalTextView: UITextView? @@ -244,7 +247,6 @@ extension RoomViewController { optionalTextView = textView self.view.window?.addSubview(textView) optionalTextView?.becomeFirstResponder() - wysiwygInputToolbar.showKeyboard() } self.roomInputToolbarContainer.removeFromSuperview() maximisedToolbarDimmingView?.removeFromSuperview() @@ -257,7 +259,10 @@ extension RoomViewController { self.view.layoutIfNeeded() } roomInputToolbarContainer.gestureRecognizers?.removeAll() - optionalTextView?.removeFromSuperview() + if let optionalTextView { + wysiwygInputToolbar.showKeyboard() + optionalTextView.removeFromSuperview() + } } } @@ -384,8 +389,8 @@ extension RoomViewController: ComposerLinkActionBridgePresenterDelegate { } // MARK: - PermalinkReplacer -extension RoomViewController: PermalinkReplacer { - public func replacementForLink(_ url: String, text: String) -> NSAttributedString? { +extension RoomViewController: MentionReplacer { + public func replacementForMention(_ url: String, text: String) -> NSAttributedString? { guard #available(iOS 15.0, *), let url = URL(string: url), let session = roomDataSource.mxSession, diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 60b6edf943..864482dccf 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -140,6 +140,20 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp wysiwygViewModel.maxCompressedHeight } + override func paste(_ sender: Any?) { + let pasteboard = MXKPasteboardManager.shared.pasteboard + let types = pasteboard.types.map { UTI(rawValue: $0) } + + // Minimise the composer and dismiss the keyboard if it's an image, a video or a file + if types.contains(where: { $0.conforms(to: .image) || $0.conforms(to: .movie) || $0.conforms(to: .video) || $0.conforms(to: .application) }) { + wysiwygViewModel.maximised = false + DispatchQueue.main.async { + self.viewModel.dismissKeyboard() + } + } + super.paste(sender) + } + // MARK: - Setup override class func instantiate() -> MXKRoomInputToolbarView! { @@ -150,8 +164,8 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp return (delegate as? RoomInputToolbarViewDelegate) ?? nil } - private var permalinkReplacer: PermalinkReplacer? { - return (delegate as? PermalinkReplacer) + private var permalinkReplacer: MentionReplacer? { + return (delegate as? MentionReplacer) } override func awakeFromNib() { @@ -192,6 +206,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } func showKeyboard() { + self.wysiwygViewModel.textView.becomeFirstResponder() self.viewModel.showKeyboard() } @@ -238,7 +253,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self?.handleViewModelResult(result) } wysiwygViewModel.plainTextMode = !RiotSettings.shared.enableWysiwygTextFormatting - wysiwygViewModel.permalinkReplacer = permalinkReplacer + wysiwygViewModel.mentionReplacer = permalinkReplacer inputAccessoryViewForKeyboard = UIView(frame: .zero) diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index 33d73ef4a2..f0b3a37804 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -156,7 +156,7 @@ extension FormatItem { extension FormatType { /// Convenience method to map it to the external ViewModel action - var action: WysiwygAction { + var action: ComposerAction { switch self { case .bold: return .bold diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index a74b0bb4d6..f2a60ce066 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -32,6 +32,7 @@ struct Composer: View { @Environment(\.theme) private var theme: ThemeSwiftUI @State private var isActionButtonShowing = false + @FocusState private var focused: Bool private let horizontalPadding: CGFloat = 12 private let borderHeight: CGFloat = 40 @@ -58,17 +59,8 @@ struct Composer: View { viewModel.viewState.shouldDisplayContext ? contextBannerHeight + standardVerticalPadding + verticalComponentSpacing : 0 } - /// Computes the total height of the composer (excluding the RTE formatting bar). - /// This height includes the text view, as well as the context banner - /// and user suggestion list when displayed. - private var composerHeight: CGFloat { - wysiwygViewModel.idealHeight - + composerTopPadding - + composerVerticalPadding - // Extra padding added on top of the VStack containing the composer - + standardVerticalPadding - + additionalHeightForContextBanner - } + /// the total height of the composer (excluding the RTE formatting bar). + @State private var composerHeight: CGFloat = .zero private var cornerRadius: CGFloat { if shouldFixRoundCorner { @@ -139,20 +131,39 @@ struct Composer: View { .padding(.horizontal, horizontalPadding) } HStack(alignment: shouldFixRoundCorner ? .top : .center, spacing: 0) { - WysiwygComposerView( - focused: $viewModel.focused, - viewModel: wysiwygViewModel - ) - .tintColor(theme.colors.accent) - .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) - .onAppear { - if wysiwygViewModel.isContentEmpty { - wysiwygViewModel.setup() + // Use a GeometryReader to force the composer to fill the HStack + GeometryReader { _ in + WysiwygComposerView( + placeholder: viewModel.viewState.placeholder ?? "", + viewModel: wysiwygViewModel, + itemProviderHelper: nil, + keyCommandHandler: handleKeyCommand, + pasteHandler: nil + ) + .clipped() + .tint(theme.colors.accent) + .focused($focused) + .onChange(of: focused) { newValue in + viewModel.focused = newValue + } + .onChange(of: viewModel.focused) { newValue in + guard focused != newValue else { return } + focused = newValue + } + .onAppear { + if wysiwygViewModel.isContentEmpty { + wysiwygViewModel.setup() + } } } + if !viewModel.viewState.isMinimiseForced { Button { - wysiwygViewModel.maximised.toggle() + viewModel.focused = true + // Use a dispatched block so the focus state will be up to date when the composer size changes. + DispatchQueue.main.async { + wysiwygViewModel.maximised.toggle() + } } label: { Image(toggleButtonImageName) .resizable() @@ -167,15 +178,14 @@ struct Composer: View { .padding(.horizontal, horizontalPadding) .padding(.top, composerTopPadding) .padding(.bottom, composerVerticalPadding) + .layoutPriority(1) } .clipShape(rect) .overlay(rect.stroke(borderColor, lineWidth: 1)) .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) .padding(.top, standardVerticalPadding) .onTapGesture { - if viewModel.focused { - viewModel.focused = true - } + viewModel.focused = true } } @@ -218,6 +228,29 @@ struct Composer: View { } } + func handleKeyCommand(_ keyCommand: WysiwygKeyCommand) -> Bool { + switch keyCommand { + case .enter: + sendMessageAction(wysiwygViewModel.content) + wysiwygViewModel.clearContent() + return true + case .shiftEnter: + return false + } + } + + /// Computes the total height of the composer (excluding the RTE formatting bar). + /// This height includes the text view, as well as the context banner + /// and user suggestion list when displayed. + private func updateComposerHeight(idealHeight: CGFloat) { + composerHeight = idealHeight + + composerTopPadding + + composerVerticalPadding + // Extra padding added on top of the VStack containing the composer + + standardVerticalPadding + + additionalHeightForContextBanner + } + // MARK: Public init( @@ -287,6 +320,15 @@ struct Composer: View { .onChange(of: wysiwygViewModel.suggestionPattern) { newValue in sendMentionPattern(pattern: newValue) } + .onChange(of: wysiwygViewModel.idealHeight) { newValue in + updateComposerHeight(idealHeight: newValue) + } + .onChange(of: viewModel.viewState.shouldDisplayContext) { _ in + updateComposerHeight(idealHeight: wysiwygViewModel.idealHeight) + } + .task { + updateComposerHeight(idealHeight: wysiwygViewModel.idealHeight) + } } private func storeCurrentSelection() { diff --git a/changelog.d/7681.bugfix b/changelog.d/7681.bugfix new file mode 100644 index 0000000000..09ee4f2697 --- /dev/null +++ b/changelog.d/7681.bugfix @@ -0,0 +1 @@ +Editing a message that ends with an emoji now works as expected. diff --git a/project.yml b/project.yml index 430b39cbc0..a2f34eaa1e 100644 --- a/project.yml +++ b/project.yml @@ -59,10 +59,10 @@ packages: branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 2.2.2 + version: 2.18.0 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0 DTCoreText: url: https://github.com/Cocoanetics/DTCoreText - version: 1.6.27 + version: 1.6.26