From 058fe79f25d7521c2891f9297db359bef880d9df Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Thu, 14 Sep 2023 13:10:42 +0200 Subject: [PATCH] Move RTE feature flag to settings (#1703) * Update matrix-wysiwyg-composer-swift to 2.10.1 * Delete MessageComposerTextField * Always use rte composer * Fix corner radius * Cleanup --- ElementX.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Sources/Application/AppSettings.swift | 8 +- .../ComposerToolbarModels.swift | 11 +- .../ComposerToolbarViewModel.swift | 19 +- .../View/ComposerToolbar.swift | 3 +- .../View/MessageComposer.swift | 44 +--- .../View/MessageComposerTextField.swift | 246 ------------------ .../RoomScreen/RoomScreenViewModel.swift | 4 +- .../View/AdvancedSettingsScreen.swift | 1 - project.yml | 2 +- 11 files changed, 27 insertions(+), 321 deletions(-) delete mode 100644 ElementX/Sources/Screens/ComposerToolbar/View/MessageComposerTextField.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 4d8b7dadf6..b83e21a525 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -98,7 +98,6 @@ 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; - 20BB987875F99190A3E28632 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */; }; 2185C1F6724C78FFF355D6FA /* WelcomeScreenScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */; }; 21BF2B7CEDFE3CA67C5355AD /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = C733D11B421CFE3A657EF230 /* test_image.png */; }; 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; @@ -935,7 +934,6 @@ 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = ""; }; - 1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReadReceiptsView.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; @@ -2305,7 +2303,6 @@ D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */, 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */, A0A01AECFF54281CF35909A6 /* MessageComposer.swift */, - 1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */, 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */, ); path = View; @@ -4686,7 +4683,6 @@ 9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */, 8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */, 858B0A45257174AAFD448EA0 /* MessageComposer.swift in Sources */, - 20BB987875F99190A3E28632 /* MessageComposerTextField.swift in Sources */, C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */, 2BBA132149DEBED6624084A8 /* MessageForwardingScreenCoordinator.swift in Sources */, 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */, @@ -5665,7 +5661,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-wysiwyg-composer-swift"; requirement = { kind = exactVersion; - version = 2.8.0; + version = 2.10.1; }; }; 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ad897da5aa..e47ee4ad06 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,8 +138,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "30d41b07b636f67e06bc50d8982c98ee3b97d4ae", - "version" : "2.8.0" + "revision" : "73a2e76929c02c7ca85ca1132e8974ec330cdc04", + "version" : "2.10.1" } }, { diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index f35194a344..0402fd0f98 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -32,6 +32,7 @@ final class AppSettings { case logLevel case otlpTracingEnabled case viewSourceEnabled + case richTextEditorEnabled // Feature flags case shouldCollapseRoomStateEvents @@ -39,7 +40,6 @@ final class AppSettings { case readReceiptsEnabled case hasShownWelcomeScreen case swiftUITimelineEnabled - case richTextEditorEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -191,6 +191,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.viewSourceEnabled, defaultValue: false, storageType: .userDefaults(store)) var viewSourceEnabled + + @UserPreference(key: UserDefaultsKeys.richTextEditorEnabled, defaultValue: true, storageType: .userDefaults(store)) + var richTextEditorEnabled // MARK: - Notifications @@ -241,7 +244,4 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.swiftUITimelineEnabled, defaultValue: false, storageType: .volatile) var swiftUITimelineEnabled - - @UserPreference(key: UserDefaultsKeys.richTextEditorEnabled, defaultValue: false, storageType: .userDefaults(store)) - var richTextEditorEnabled } diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift index 76f73b6bf4..ddd93f1664 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift @@ -19,9 +19,7 @@ import UIKit import WysiwygComposer enum ComposerToolbarViewModelAction { - case sendMessage(plain: String, html: String, mode: RoomScreenComposerMode) - case sendPlainTextMessage(message: String, mode: RoomScreenComposerMode) - + case sendMessage(plain: String, html: String?, mode: RoomScreenComposerMode) case displayCameraPicker case displayMediaPicker case displayDocumentPicker @@ -56,16 +54,11 @@ struct ComposerToolbarViewState: BindableState { var bindings: ComposerToolbarViewStateBindings var sendButtonDisabled: Bool { - if ServiceLocator.shared.settings.richTextEditorEnabled { - return composerEmpty - } else { - return bindings.composerPlainText.isEmpty - } + composerEmpty } } struct ComposerToolbarViewStateBindings { - var composerPlainText = "" var composerFocused = false var composerActionsEnabled = false var composerExpanded = false diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift index be865623bf..6752b982e5 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift @@ -81,15 +81,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool wysiwygViewModel.setup() case .sendMessage: guard !state.sendButtonDisabled else { return } - - if ServiceLocator.shared.settings.richTextEditorEnabled { - actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown, - html: wysiwygViewModel.content.html, - mode: state.composerMode)) - } else { - actionsSubject.send(.sendPlainTextMessage(message: context.composerPlainText, - mode: state.composerMode)) - } + let sendHTML = ServiceLocator.shared.settings.richTextEditorEnabled + actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown, + html: sendHTML ? wysiwygViewModel.content.html : nil, + mode: state.composerMode)) case .cancelReply: set(mode: .default) case .cancelEdit: @@ -156,11 +151,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } private func set(text: String) { - if ServiceLocator.shared.settings.richTextEditorEnabled { - wysiwygViewModel.setMarkdownContent(text) - } else { - state.bindings.composerPlainText = text - } + wysiwygViewModel.setMarkdownContent(text) } private func createLinkAlert() { diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift index 3a4e99ce18..793dda5026 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift @@ -97,8 +97,7 @@ struct ComposerToolbar: View { } private var messageComposer: some View { - MessageComposer(plainText: $context.composerPlainText, - composerView: composerView, + MessageComposer(composerView: composerView, mode: context.viewState.composerMode, showResizeGrabber: context.viewState.bindings.composerActionsEnabled, isExpanded: $context.composerExpanded) { diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposer.swift index 70fa9ab17f..f8c198e118 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposer.swift @@ -21,7 +21,6 @@ typealias EnterKeyHandler = () -> Void typealias PasteHandler = (NSItemProvider) -> Void struct MessageComposer: View { - @Binding var plainText: String let composerView: WysiwygComposerView let mode: RoomScreenComposerMode let showResizeGrabber: Bool @@ -33,7 +32,6 @@ struct MessageComposer: View { let onAppearAction: () -> Void @FocusState private var focused: Bool - @State private var isMultiline = false @State private var composerTranslation: CGFloat = 0 var body: some View { @@ -42,6 +40,7 @@ struct MessageComposer: View { resizeGrabber } + let borderRadius: CGFloat = 21 mainContent .padding(.horizontal, 12.0) .clipShape(RoundedRectangle(cornerRadius: borderRadius)) @@ -67,28 +66,15 @@ struct MessageComposer: View { private var mainContent: some View { VStack(alignment: .leading, spacing: -6) { header - HStack(alignment: .bottom) { - if ServiceLocator.shared.settings.richTextEditorEnabled { - composerView - .frame(minHeight: composerHeight, alignment: .top) - .tint(.compound.iconAccentTertiary) - .padding(.vertical, 10) - .focused($focused) - .onAppear { - onAppearAction() - } - } else { - MessageComposerTextField(placeholder: L10n.richTextEditorComposerPlaceholder, - text: $plainText, - isMultiline: $isMultiline, - maxHeight: 300, - enterKeyHandler: sendAction, - pasteHandler: pasteAction) - .tint(.compound.iconAccentTertiary) - .padding(.vertical, 10) - .focused($focused) + + composerView + .frame(minHeight: composerHeight, alignment: .top) + .tint(.compound.iconAccentTertiary) + .padding(.vertical, 10) + .focused($focused) + .onAppear { + onAppearAction() } - } } } @@ -108,15 +94,6 @@ struct MessageComposer: View { EmptyView() } } - - private var borderRadius: CGFloat { - switch mode { - case .default: - return isMultiline ? 20 : 28 - case .reply, .edit: - return 20 - } - } private var resizeGrabber: some View { Capsule() @@ -213,8 +190,7 @@ struct MessageComposer_Previews: PreviewProvider { keyCommandHandler: nil, pasteHandler: nil) - return MessageComposer(plainText: .constant(content), - composerView: composerView, + return MessageComposer(composerView: composerView, mode: mode, showResizeGrabber: false, isExpanded: .constant(false), diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposerTextField.swift deleted file mode 100644 index e38d9ab37b..0000000000 --- a/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposerTextField.swift +++ /dev/null @@ -1,246 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -import SwiftUI - -struct MessageComposerTextField: View { - let placeholder: String - @Binding var text: String - @Binding var isMultiline: Bool - - let maxHeight: CGFloat - let enterKeyHandler: EnterKeyHandler - let pasteHandler: PasteHandler - - var body: some View { - UITextViewWrapper(text: $text, - isMultiline: $isMultiline, - maxHeight: maxHeight, - enterKeyHandler: enterKeyHandler, - pasteHandler: pasteHandler) - .accessibilityLabel(placeholder) - .background(placeholderView, alignment: .topLeading) - } - - @ViewBuilder - private var placeholderView: some View { - if text.isEmpty { - Text(placeholder) - .foregroundColor(.compound.textPlaceholder) - .accessibilityHidden(true) - } - } -} - -private struct UITextViewWrapper: UIViewRepresentable { - typealias UIViewType = UITextView - - @Binding var text: String - @Binding var isMultiline: Bool - - let maxHeight: CGFloat - - let enterKeyHandler: EnterKeyHandler - let pasteHandler: PasteHandler - - private let font = UIFont.preferredFont(forTextStyle: .body) - - func makeUIView(context: UIViewRepresentableContext) -> UITextView { - let textView = ElementTextView() - textView.isMultiline = $isMultiline - textView.delegate = context.coordinator - textView.elementDelegate = context.coordinator - textView.textColor = .compound.textPrimary - textView.isEditable = true - textView.font = font - textView.isSelectable = true - textView.isUserInteractionEnabled = true - textView.backgroundColor = UIColor.clear - textView.returnKeyType = .default - textView.textContainer.lineFragmentPadding = 0.0 - textView.textContainerInset = .zero - textView.keyboardType = .default - - textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - return textView - } - - func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? { - // Note: Coalescing a width of zero here returns a size for the view with 1 line of text visible. - let newSize = uiView.sizeThatFits(CGSize(width: proposal.width ?? .zero, - height: CGFloat.greatestFiniteMagnitude)) - let width = proposal.width ?? newSize.width - let height = min(maxHeight, newSize.height) - - return CGSize(width: width, height: height) - } - - func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext) { - if textView.text != text { - textView.text = text - - if text.isEmpty { - // text cleared, probably because the written text is sent - // reload keyboard type - if textView.isFirstResponder { - textView.keyboardType = .twitter - textView.reloadInputViews() - textView.keyboardType = .default - textView.reloadInputViews() - } - } - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(text: $text, - maxHeight: maxHeight, - enterKeyHandler: enterKeyHandler, - pasteHandler: pasteHandler) - } - - final class Coordinator: NSObject, UITextViewDelegate, ElementTextViewDelegate { - private var text: Binding - - private let maxHeight: CGFloat - - private let enterKeyHandler: EnterKeyHandler - private let pasteHandler: PasteHandler - - init(text: Binding, - maxHeight: CGFloat, - enterKeyHandler: @escaping EnterKeyHandler, - pasteHandler: @escaping PasteHandler) { - self.text = text - self.maxHeight = maxHeight - self.enterKeyHandler = enterKeyHandler - self.pasteHandler = pasteHandler - } - - func textViewDidChange(_ textView: UITextView) { - text.wrappedValue = textView.text - } - - func textViewDidReceiveEnterKeyPress(_ textView: UITextView) { - enterKeyHandler() - } - - func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) { - textView.insertText("\n") - } - - func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider) { - pasteHandler(provider) - } - } -} - -private protocol ElementTextViewDelegate: AnyObject { - func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) - func textViewDidReceiveEnterKeyPress(_ textView: UITextView) - func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider) -} - -private class ElementTextView: UITextView { - weak var elementDelegate: ElementTextViewDelegate? - - var isMultiline: Binding? - - override var keyCommands: [UIKeyCommand]? { - [UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)), - UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))] - } - - @objc func shiftEnterKeyPressed(sender: UIKeyCommand) { - elementDelegate?.textViewDidReceiveShiftEnterKeyPress(self) - } - - @objc func enterKeyPressed(sender: UIKeyCommand) { - elementDelegate?.textViewDidReceiveEnterKeyPress(self) - } - - override func layoutSubviews() { - super.layoutSubviews() - - guard let isMultiline, let font else { return } - - let numberOfLines = frame.height / font.lineHeight - if numberOfLines > 1.5 { - if !isMultiline.wrappedValue { - isMultiline.wrappedValue = true - } - } else { - if isMultiline.wrappedValue { - isMultiline.wrappedValue = false - } - } - } - - // Pasting support - - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if super.canPerformAction(action, withSender: sender) { - return true - } - - guard action == #selector(paste(_:)) else { - return false - } - - return UIPasteboard.general.itemProviders.first?.isSupportedForPasteOrDrop ?? false - } - - override func paste(_ sender: Any?) { - guard let provider = UIPasteboard.general.itemProviders.first, - provider.isSupportedForPasteOrDrop else { - // If the item is not supported for media upload then - // just try pasting its contents into the textfield - super.paste(sender) - return - } - - elementDelegate?.textView(self, didReceivePasteWith: provider) - } -} - -struct MessageComposerTextField_Previews: PreviewProvider { - static var previews: some View { - VStack { - PreviewWrapper(text: "123") - PreviewWrapper(text: "") - PreviewWrapper(text: "A really long message that will wrap to multiple lines on a phone in portrait.") - } - } - - struct PreviewWrapper: View { - @State var text: String - @State var isMultiline: Bool - - init(text: String) { - _text = .init(initialValue: text) - _isMultiline = .init(initialValue: false) - } - - var body: some View { - MessageComposerTextField(placeholder: "Placeholder", - text: $text, - isMultiline: $isMultiline, - maxHeight: 300, - enterKeyHandler: { }, - pasteHandler: { _ in }) - } - } -} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index aa98f826a9..b51926b189 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -146,8 +146,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol switch composerAction { case .sendMessage(let message, let html, let mode): Task { await sendCurrentMessage(message, html: html, mode: mode) } - case .sendPlainTextMessage(let message, let mode): - Task { await sendCurrentMessage(message, html: nil, mode: mode) } case .displayCameraPicker: actionsSubject.send(.displayCameraPicker) case .displayMediaPicker: @@ -441,7 +439,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol await timelineController.sendMessage(message, html: html, inReplyTo: itemId) case .edit(let originalItemId): await timelineController.editMessage(message, html: html, original: originalItemId) - default: + case .default: await timelineController.sendMessage(message, html: html) } } diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift index 5415349660..91f1f4dbb4 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift @@ -19,7 +19,6 @@ import SwiftUI struct AdvancedSettingsScreen: View { @ObservedObject var context: AdvancedSettingsScreenViewModel.Context - @State private var showConfetti = false var body: some View { Form { diff --git a/project.yml b/project.yml index eaacd8629b..e6b7925cec 100644 --- a/project.yml +++ b/project.yml @@ -112,4 +112,4 @@ packages: minorVersion: 2.0.0 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - exactVersion: 2.8.0 + exactVersion: 2.10.1