diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 851ff9122..f6709a6f9 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -985,7 +985,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -1147,7 +1147,7 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1353,7 +1353,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1471,7 +1471,7 @@ B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1567,7 +1567,7 @@ CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CE1CD5EC6265A09315772DB7 /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1659,7 +1659,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -1673,7 +1673,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; @@ -5996,7 +5996,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-wysiwyg-composer-swift"; requirement = { kind = exactVersion; - version = 2.14.1; + version = 2.14.2; }; }; 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 5d41917be..6dd1c4832 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" : "6e1a77e6061c4148caa087221bc1750ceeb37724", - "version" : "2.14.1" + "revision" : "6e567f647f34b58f0ff6d48f2bb6e535e58a6593", + "version" : "2.14.2" } }, { @@ -165,8 +165,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/PostHog/posthog-ios", "state" : { - "revision" : "3a6afc24d6bde730a19470d4e6b713f44d076ad9", - "version" : "2.0.3" + "revision" : "9298825fe26899a58b976d400254e0b050daa578", + "version" : "2.0.5" } }, { @@ -210,8 +210,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version" : "1.0.5" } }, { diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 51956d3df..19b425868 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -220,15 +220,15 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } } - private func addAllUsersMention(_ attributedString: NSMutableAttributedString) { + func addAllUsersMention(_ attributedString: NSMutableAttributedString) { MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).forEach { match in - if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) == nil { + if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) as? URL == nil { attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range) } } } - private func detectPermalinks(_ attributedString: NSMutableAttributedString) { + func detectPermalinks(_ attributedString: NSMutableAttributedString) { attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in if value != nil { if let url = value as? URL { diff --git a/ElementX/Sources/Other/Pills/MentionBuilder.swift b/ElementX/Sources/Other/Pills/MentionBuilder.swift index 9074d9032..fe32bbd5c 100644 --- a/ElementX/Sources/Other/Pills/MentionBuilder.swift +++ b/ElementX/Sources/Other/Pills/MentionBuilder.swift @@ -37,7 +37,7 @@ struct MentionBuilder: MentionBuilderProtocol { return } - var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID] + var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID, .font: font] if let blockquote { // mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute attachmentAttributes[.MatrixBlockquote] = blockquote @@ -61,7 +61,7 @@ struct MentionBuilder: MentionBuilderProtocol { return } - var attachmentAttributes: [NSAttributedString.Key: Any] = [:] + var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font] if let blockquote { // mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute attachmentAttributes[.MatrixBlockquote] = blockquote diff --git a/ElementX/Sources/Other/Pills/MessageText.swift b/ElementX/Sources/Other/Pills/MessageText.swift index 95b3511b1..3f8afcb9d 100644 --- a/ElementX/Sources/Other/Pills/MessageText.swift +++ b/ElementX/Sources/Other/Pills/MessageText.swift @@ -16,7 +16,7 @@ import SwiftUI -final class MessageTextView: UITextView { +final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate { var roomContext: RoomScreenViewModel.Context? var updateClosure: (() -> Void)? @@ -44,6 +44,10 @@ final class MessageTextView: UITextView { invalidateTextAttachmentsDisplay(update: false) super.layoutSubviews() } + + func registerPillView(_ pillView: UIView) { + // No need to be implemented in this view + } } struct MessageText: UIViewRepresentable { diff --git a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift index e6c05fe32..d01366e51 100644 --- a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift +++ b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift @@ -18,8 +18,17 @@ import SwiftUI import SwiftUIIntrospect import UIKit +import WysiwygComposer + +protocol PillAttachmentViewProviderDelegate: AnyObject { + var roomContext: RoomScreenViewModel.Context? { get } + + func registerPillView(_ pillView: UIView) + func invalidateTextAttachmentsDisplay(update: Bool) +} + final class PillAttachmentViewProvider: NSTextAttachmentViewProvider { - private weak var messageTextView: MessageTextView? + private weak var delegate: PillAttachmentViewProviderDelegate? // MARK: - Override @@ -27,7 +36,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider { super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) // Keep a reference to the parent text view for size adjustments and pills flushing. - messageTextView = parentView?.superview as? MessageTextView + delegate = parentView?.superview as? PillAttachmentViewProviderDelegate tracksTextAttachmentViewBounds = true } @@ -46,7 +55,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider { // The mock viewModel simulates the loading logic for testing purposes context = PillContext.mock(type: .loadUser(isOwn: false)) imageProvider = MockMediaProvider() - } else if let roomContext = messageTextView?.roomContext { + } else if let roomContext = delegate?.roomContext { context = PillContext(roomContext: roomContext, data: textAttachmentData) imageProvider = roomContext.imageProvider } else { @@ -55,12 +64,34 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider { } let view = PillView(imageProvider: imageProvider, context: context) { [weak self] in - self?.messageTextView?.invalidateTextAttachmentsDisplay(update: true) + self?.delegate?.invalidateTextAttachmentsDisplay(update: true) } let controller = UIHostingController(rootView: view) controller.view.backgroundColor = .clear // This allows the text view to handle it as a link controller.view.isUserInteractionEnabled = false self.view = controller.view + delegate?.registerPillView(controller.view) } } + +final class ComposerMentionDisplayHelper: MentionDisplayHelper { + weak var roomContext: RoomScreenViewModel.Context? + + init(roomContext: RoomScreenViewModel.Context) { + self.roomContext = roomContext + } + + @MainActor + static var mock: Self { + Self(roomContext: RoomScreenViewModel.mock.context) + } +} + +extension WysiwygTextView: PillAttachmentViewProviderDelegate { + var roomContext: RoomScreenViewModel.Context? { + (mentionDisplayHelper as? ComposerMentionDisplayHelper)?.roomContext + } + + func invalidateTextAttachmentsDisplay(update: Bool) { } +} diff --git a/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift b/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift index 0ed10150e..19d30b22f 100644 --- a/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift +++ b/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift @@ -17,7 +17,7 @@ import Foundation import UIKit -enum PillType: Codable { +enum PillType: Codable, Equatable { /// A pill that mentions a user case user(userID: String) /// A pill that mentions all users in a room @@ -29,7 +29,7 @@ struct PillTextAttachmentData { /// Pill type let type: PillType - + /// Font for the display name let font: UIFont } diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift index 85846d661..23eee165b 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift @@ -40,11 +40,11 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool private var currentLinkData: WysiwygLinkData? - init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, appSettings: AppSettings) { + init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, appSettings: AppSettings, mentionDisplayHelper: MentionDisplayHelper) { self.wysiwygViewModel = wysiwygViewModel self.completionSuggestionService = completionSuggestionService self.appSettings = appSettings - + super.init(initialViewState: ComposerToolbarViewState(areSuggestionsEnabled: completionSuggestionService.areSuggestionsEnabled, bindings: .init()), imageProvider: mediaProvider) context.$viewState @@ -84,6 +84,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool completionSuggestionService.suggestionsPublisher .weakAssign(to: \.state.suggestions, on: self) .store(in: &cancellables) + + if appSettings.mentionsEnabled { + setupMentionsHandling(mentionDisplayHelper: mentionDisplayHelper) + } } // MARK: - Public @@ -155,6 +159,27 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool // MARK: - Private + private func setupMentionsHandling(mentionDisplayHelper: MentionDisplayHelper) { + wysiwygViewModel.textView.mentionDisplayHelper = mentionDisplayHelper + + let attributedStringBuilder = AttributedStringBuilder(cacheKey: "Composer", permalinkBaseURL: appSettings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled)) + + wysiwygViewModel.mentionReplacer = ComposerMentionReplacer { urlString, string in + let attributedString: NSMutableAttributedString + // This is the all room mention case + if urlString == "#" { + attributedString = NSMutableAttributedString(string: string, attributes: [.MatrixAllUsersMention: true]) + } else { + attributedString = NSMutableAttributedString(string: string, attributes: [.link: URL(string: urlString) as Any]) + } + attributedStringBuilder.detectPermalinks(attributedString) + + // In RTE mentions don't need to be handled as links + attributedString.removeAttribute(.link, range: NSRange(location: 0, length: attributedString.length)) + return attributedString + } + } + private func handleSuggestion(_ suggestion: SuggestionItem) { switch suggestion { case let .user(item): @@ -177,6 +202,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } private func set(text: String) { + wysiwygViewModel.textView.flushPills() wysiwygViewModel.setHtmlContent(text) } @@ -293,3 +319,25 @@ private extension LinkAction { return url } } + +private final class ComposerMentionReplacer: MentionReplacer { + let replacementForMentionClosure: (_ urlString: String, _ text: String) -> (NSAttributedString?) + + init(replacementForMentionClosure: @escaping (String, String) -> (NSAttributedString?)) { + self.replacementForMentionClosure = replacementForMentionClosure + } + + // There is no internal Markdown to RTE switch implemented yet in the room so this one is never called + func postProcessMarkdown(in attributedString: NSAttributedString) -> NSAttributedString { + attributedString + } + + // There is no internal RTE to Markdown switch implemented yet in the room so this one is never called + func restoreMarkdown(in attributedString: NSAttributedString) -> String { + attributedString.string + } + + func replacementForMention(_ url: String, text: String) -> NSAttributedString? { + replacementForMentionClosure(url, text) + } +} diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift index 4def925f8..55af41c39 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift @@ -197,7 +197,8 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)), mediaProvider: MockMediaProvider(), - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + mentionDisplayHelper: ComposerMentionDisplayHelper.mock) static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))] @@ -223,7 +224,8 @@ extension ComposerToolbar { let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), mediaProvider: MockMediaProvider(), - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + mentionDisplayHelper: ComposerMentionDisplayHelper.mock) return ComposerToolbar(context: composerViewModel.context, wysiwygViewModel: wysiwygViewModel, keyCommandHandler: { _ in false }) diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift index ee1ed6a61..43e18ff7a 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -118,7 +118,8 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview { static let viewModel = ComposerToolbarViewModel(wysiwygViewModel: WysiwygComposerViewModel(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), mediaProvider: MockMediaProvider(), - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + mentionDisplayHelper: ComposerMentionDisplayHelper.mock) static var previews: some View { RoomAttachmentPicker(context: viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 86f5a54e6..ecb81ab2b 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -71,7 +71,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: parameters.completionSuggestionService, mediaProvider: parameters.mediaProvider, - appSettings: parameters.appSettings) + appSettings: parameters.appSettings, + mentionDisplayHelper: ComposerMentionDisplayHelper(roomContext: viewModel.context)) } // MARK: - Public diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 593113c90..a12b52914 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -31,13 +31,15 @@ class ComposerToolbarViewModelTests: XCTestCase { AppSettings.reset() appSettings = AppSettings() appSettings.richTextEditorEnabled = true + appSettings.mentionsEnabled = true ServiceLocator.shared.register(appSettings: appSettings) wysiwygViewModel = WysiwygComposerViewModel() completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init()) viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: completionSuggestionServiceMock, mediaProvider: MockMediaProvider(), - appSettings: ServiceLocator.shared.settings) + appSettings: appSettings, + mentionDisplayHelper: ComposerMentionDisplayHelper.mock) } func testComposerFocus() { @@ -107,7 +109,8 @@ class ComposerToolbarViewModelTests: XCTestCase { viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: mockCompletionSuggestionService, mediaProvider: MockMediaProvider(), - appSettings: ServiceLocator.shared.settings) + appSettings: ServiceLocator.shared.settings, + mentionDisplayHelper: ComposerMentionDisplayHelper.mock) XCTAssertEqual(viewModel.state.suggestions, suggestions) } @@ -126,4 +129,13 @@ class ComposerToolbarViewModelTests: XCTestCase { XCTAssertEqual(wysiwygViewModel.content.html, "Test ") } + + func testUserMentionInRTE() { + let userID = "@test:matrix.org" + let suggestion = SuggestionItem.user(item: .init(id: userID, displayName: "Test", avatarURL: nil)) + viewModel.context.send(viewAction: .selectedSuggestion(suggestion)) + + let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment + XCTAssertEqual(attachment?.pillData?.type, .user(userID: userID)) + } } diff --git a/project.yml b/project.yml index 54a1b332e..1e47a372b 100644 --- a/project.yml +++ b/project.yml @@ -115,7 +115,8 @@ packages: minorVersion: 2.0.0 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - exactVersion: 2.14.1 + exactVersion: 2.14.2 +# path: ../matrix-wysiwyg/platforms/ios/lib/WysiwygComposer SwiftOGG: url: https://github.com/vector-im/swift-ogg branch: 0.0.1