From c04e812a02791f3aa7e54d282cad3268d37826b8 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:39:18 +0200 Subject: [PATCH] Replies now display mentions with @displayname (#1957) * reply with mentions * fixed a bug and wrote a test for it * error message --- ElementX.xcodeproj/project.pbxproj | 22 ++++----- .../HTMLParsing/AttributedStringBuilder.swift | 43 +++++++++-------- .../Sources/Other/Pills/MentionBuilder.swift | 2 +- .../Sources/Other/Pills/MessageText.swift | 15 ++++-- .../View/Replies/TimelineReplyView.swift | 44 ++++++++++++++++-- .../AttributedStringBuilderTests.swift | 46 +++++++++++++++++++ .../PreviewTests/test_timelineReplyView.1.png | 4 +- 7 files changed, 132 insertions(+), 44 deletions(-) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 6e5baf479..5ec5ec110 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -91,7 +91,6 @@ 1795EA6A6C4942CAE0459DF0 /* SecureBackupKeyBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */; }; 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; 1830E5431DB426E2F3660D58 /* NotificationSettingsEditScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46F52419AEEDA2C006CB7181 /* NotificationSettingsEditScreenUITests.swift */; }; - 184D68B82AE7A01400141160 /* SettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184D68B72AE7A01400141160 /* SettingsFlowCoordinator.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; }; @@ -425,6 +424,7 @@ 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; }; 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; }; 754602A7B2AAD443C4228ED4 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; + 755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */; }; 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 762DAF94846C7AC8550F1CC1 /* MediaPlayerProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */; }; 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; }; @@ -1090,7 +1090,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; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; 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 = ""; }; @@ -1106,7 +1106,6 @@ 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; - 184D68B72AE7A01400141160 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.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 = ""; }; 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = ""; }; @@ -1507,7 +1506,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; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; 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 = ""; }; @@ -1642,7 +1641,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; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; 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 = ""; }; @@ -1744,7 +1743,7 @@ CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; 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 = ""; }; @@ -1757,6 +1756,7 @@ D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformViewVersionPredicate.swift; sourceTree = ""; }; + D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.swift; sourceTree = ""; }; D2E61DDB42C0DE429C0955D8 /* VoiceMessageRecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingButton.swift; sourceTree = ""; }; D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelProtocol.swift; sourceTree = ""; }; D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; @@ -1846,7 +1846,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; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; 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 = ""; }; @@ -1861,7 +1861,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; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; 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 = ""; }; @@ -2934,9 +2934,9 @@ FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */, 0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */, 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */, + D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */, C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */, E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */, - 184D68B72AE7A01400141160 /* SettingsFlowCoordinator.swift */, ); path = FlowCoordinators; sourceTree = ""; @@ -5407,7 +5407,6 @@ 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */, 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */, FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */, - 184D68B82AE7A01400141160 /* SettingsFlowCoordinator.swift in Sources */, 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */, 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */, 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, @@ -5751,6 +5750,7 @@ B27D3190784F85916DA1C394 /* SessionVerificationScreenStateMachine.swift in Sources */, F4433EF57B4BB3C077F8B00E /* SessionVerificationScreenViewModel.swift in Sources */, E570117376826665640F0CFD /* SessionVerificationScreenViewModelProtocol.swift in Sources */, + 755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */, 34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */, AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */, B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 19b425868..40daeb689 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -56,8 +56,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } let mutableAttributedString = NSMutableAttributedString(string: string) - addLinks(mutableAttributedString) - addAllUsersMention(mutableAttributedString) + addLinksAndMentions(mutableAttributedString) detectPermalinks(mutableAttributedString) removeLinkColors(mutableAttributedString) @@ -110,8 +109,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) removeDefaultForegroundColor(mutableAttributedString) - addLinks(mutableAttributedString) - addAllUsersMention(mutableAttributedString) + addLinksAndMentions(mutableAttributedString) replaceMarkedBlockquotes(mutableAttributedString) replaceMarkedCodeBlocks(mutableAttributedString) detectPermalinks(mutableAttributedString) @@ -150,7 +148,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } } - func replaceMarkedCodeBlocks(_ attributedString: NSMutableAttributedString) { + private func replaceMarkedCodeBlocks(_ attributedString: NSMutableAttributedString) { attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in if let value = value as? UIColor, value == temporaryCodeBlockMarkingColor { @@ -175,7 +173,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } } - private func addLinks(_ attributedString: NSMutableAttributedString) { + private func addLinksAndMentions(_ attributedString: NSMutableAttributedString) { let string = attributedString.string var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []) @@ -187,11 +185,16 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: []) matches.append(contentsOf: linkMatches) + let allUserMentionsMatches = MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []) + matches.append(contentsOf: allUserMentionsMatches) + + let allUsersMentionsCount = allUserMentionsMatches.count + guard matches.count > 0 else { return } // Sort the links by length so the longest one always takes priority - matches.sorted { $0.range.length > $1.range.length }.forEach { match in + matches.sorted { $0.range.length > $1.range.length }.enumerated().forEach { offset, match in guard let matchRange = Range(match.range, in: string) else { return } @@ -208,22 +211,18 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return } - var link = String(string[matchRange]) - - if linkMatches.contains(match), !link.contains("://") { - link.insert(contentsOf: "https://", at: link.startIndex) - } - - if let url = URL(string: link) { - attributedString.addAttribute(.link, value: url, range: match.range) - } - } - } - - 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) as? URL == nil { + if offset > matches.count - allUsersMentionsCount - 1 { attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range) + } else { + var link = String(string[matchRange]) + + if linkMatches.contains(match), !link.contains("://") { + link.insert(contentsOf: "https://", at: link.startIndex) + } + + if let url = URL(string: link) { + attributedString.addAttribute(.link, value: url, range: match.range) + } } } } diff --git a/ElementX/Sources/Other/Pills/MentionBuilder.swift b/ElementX/Sources/Other/Pills/MentionBuilder.swift index fe32bbd5c..110253921 100644 --- a/ElementX/Sources/Other/Pills/MentionBuilder.swift +++ b/ElementX/Sources/Other/Pills/MentionBuilder.swift @@ -61,7 +61,7 @@ struct MentionBuilder: MentionBuilderProtocol { return } - var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font] + var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font, .MatrixAllUsersMention: true] 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 0fbb7e4ae..f53d65664 100644 --- a/ElementX/Sources/Other/Pills/MessageText.swift +++ b/ElementX/Sources/Other/Pills/MessageText.swift @@ -67,7 +67,12 @@ struct MessageText: UIViewRepresentable { let textView = MessageTextView(usingTextLayoutManager: false) textView.roomContext = viewModel textView.updateClosure = { - attributedString = AttributedString(textView.attributedText) + do { + attributedString = try AttributedString(textView.attributedText, including: \.elementX) + } catch { + MXLog.error("[MessageText] Failed to update attributedString: \(error)]") + return + } } textView.isEditable = false textView.isScrollEnabled = false @@ -87,14 +92,16 @@ struct MessageText: UIViewRepresentable { textView.textContainer.lineFragmentPadding = 0 textView.layoutManager.usesFontLeading = false textView.backgroundColor = .clear - textView.attributedText = NSAttributedString(attributedString) + if let attributedText = try? NSAttributedString(attributedString, including: \.elementX) { + textView.attributedText = attributedText + } textView.delegate = context.coordinator return textView } func updateUIView(_ uiView: MessageTextView, context: Context) { - let newAttributedText = NSAttributedString(attributedString) - if uiView.attributedText != newAttributedText { + if let newAttributedText = try? NSAttributedString(attributedString, including: \.elementX), + uiView.attributedText != newAttributedText { uiView.flushPills() uiView.attributedText = newAttributedText } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift index 4be9b085e..9a1645bde 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift @@ -126,8 +126,26 @@ struct TimelineReplyView: View { /// and render with a consistent font size. This conversion is done to avoid /// showing markdown characters in the preview for messages with formatting. var messagePreview: String { - guard let formattedBody else { return plainBody } - return String(formattedBody.characters) + guard let formattedBody, + let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else { + return plainBody + } + + let range = NSRange(location: 0, length: attributedString.length) + attributedString.enumerateAttributes(in: range) { attributes, range, _ in + if let userID = attributes[.MatrixUserID] as? String { + if let displayName = context.viewState.members[userID]?.displayName { + attributedString.replaceCharacters(in: range, with: "@\(displayName)") + } else { + attributedString.replaceCharacters(in: range, with: userID) + } + } + + if attributes[.MatrixAllUsersMention] as? Bool == true { + attributedString.replaceCharacters(in: range, with: PillConstants.atRoom) + } + } + return attributedString.string } var body: some View { @@ -187,6 +205,18 @@ struct TimelineReplyView: View { struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel.mock + static let attributedStringWithMention = { + var attributedString = AttributedString("To be replaced") + attributedString.userID = "@alice:matrix.org" + return attributedString + }() + + static let attributedStringWithAtRoomMention = { + var attributedString = AttributedString("to be replaced") + attributedString.allUsersMention = true + return attributedString + }() + static var previewItems: [TimelineReplyView] { let imageSource = MediaSourceProxy(url: "https://mock.com", mimeType: "image/png") @@ -204,7 +234,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { contentType: .emote(.init(body: "says hello")))), TimelineReplyView(placement: .timeline, - timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bot"), + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), contentType: .notice(.init(body: "Hello world")))), TimelineReplyView(placement: .timeline, @@ -244,7 +274,13 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { duration: 0, waveform: nil, source: nil, - contentType: nil)))) + contentType: nil)))), + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), + contentType: .notice(.init(body: "", formattedBody: attributedStringWithMention)))), + TimelineReplyView(placement: .timeline, + timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"), + contentType: .notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention)))) ] } diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index f1b29a7d3..303cf4baa 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -555,6 +555,52 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(foundLink, url) XCTAssertEqual(foundAttachments, 2) } + + func testMultipleMentions2() { + guard let url = URL(string: "https://matrix.to/#/@test:matrix.org") else { + XCTFail("Invalid url") + return + } + + let string = "\(url) @room" + guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else { + XCTFail("Attributed string is nil") + return + } + + var foundAttachments = 0 + var foundLink: URL? + for run in attributedStringFromHTML.runs { + if run.attachment != nil { + foundAttachments += 1 + } + + if let link = run.link { + foundLink = link + } + } + XCTAssertEqual(foundLink, url) + XCTAssertEqual(foundAttachments, 2) + + guard let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) else { + XCTFail("Attributed string is nil") + return + } + + foundAttachments = 0 + foundLink = nil + for run in attributedStringFromPlain.runs { + if run.attachment != nil { + foundAttachments += 1 + } + + if let link = run.link { + foundLink = link + } + } + XCTAssertEqual(foundLink, url) + XCTAssertEqual(foundAttachments, 2) + } // MARK: - Private diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineReplyView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineReplyView.1.png index 30f6b41b3..b91ab637d 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineReplyView.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineReplyView.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d20d3c8a4bcebf86630fb5173f1e19b80c25fa2aad136024cbc36c876cca47cb -size 132631 +oid sha256:23ba537d93a78dd7c69dc1d9482ae972ba50f0eb05dadbcb0f0ecc2b9fea2695 +size 145983