diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index a812d64a7..40e998187 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 */ @@ -445,8 +445,10 @@ 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; }; 8B76191B9DDD1AC90A6E3A35 /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; 8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */; }; + 8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */; }; 8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F21ED7205048668BEB44A38 /* AppActivityView.swift */; }; 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; + 8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15748C254911E3654C93B0ED /* MentionBuilder.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D605456793F243649EC96AA /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = CD6B0C4639E066915B5E6463 /* target.yml */; }; @@ -711,10 +713,10 @@ D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */; }; D8385A51A3D0FA9283556281 /* RoundedLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 745323FCF9AF21A117252C53 /* RoundedLabelItem.swift */; }; D84D5BDFB1B915389AC807B4 /* CreatePollScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A328F9E556F5CFA89332017 /* CreatePollScreenViewModel.swift */; }; - D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; }; D871C8CF46950F959C9A62C3 /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54464351F170D570110AFCA /* WelcomeScreen.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; + D9092786ACCFF72565AD7389 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B203689DFD1431181A795F4 /* PillContext.swift */; }; D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; @@ -724,6 +726,7 @@ DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */; }; DDB47D29C6865669288BF87C /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; + DEF1477A76F5AAE0A2EB0F32 /* NSEMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AF5A7EE5CA321724ED32CC /* NSEMentionBuilder.swift */; }; DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */; }; DF05F9C9D3D977EB77E13692 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A593735D882778FD2C9A185B /* DesignKit */; }; DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */; }; @@ -830,6 +833,7 @@ FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FCDA202B246F75BA28E10C5F /* MapTilerAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; + FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; }; @@ -943,13 +947,14 @@ 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 = ""; }; 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = ""; }; 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = ""; }; 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsSummaryView.swift; sourceTree = ""; }; + 15748C254911E3654C93B0ED /* MentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionBuilder.swift; sourceTree = ""; }; 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = ""; }; 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenUITests.swift; sourceTree = ""; }; 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = ""; }; @@ -1095,7 +1100,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 = ""; }; @@ -1109,6 +1114,7 @@ 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = ""; }; 4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = ""; }; + 4B203689DFD1431181A795F4 /* PillContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContext.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = ""; }; @@ -1293,7 +1299,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 = ""; }; @@ -1302,6 +1308,7 @@ 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = ""; }; + 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = ""; }; 91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileCell.swift; sourceTree = ""; }; 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAuthenticationPresenter.swift; sourceTree = ""; }; @@ -1403,12 +1410,13 @@ B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenModels.swift; sourceTree = ""; }; B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenCoordinator.swift; sourceTree = ""; }; B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; + B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = ""; }; B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = ""; }; 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 = ""; }; @@ -1430,7 +1438,6 @@ BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenCoordinator.swift; sourceTree = ""; }; - BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; @@ -1455,6 +1462,7 @@ C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = ""; }; C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKGeneratedMocks.swift; sourceTree = ""; }; C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = ""; }; + C3AF5A7EE5CA321724ED32CC /* NSEMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEMentionBuilder.swift; sourceTree = ""; }; C49C1CEBA9BCF5D2AD1884FA /* OnboardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenViewModel.swift; sourceTree = ""; }; C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = ""; }; C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenCoordinator.swift; sourceTree = ""; }; @@ -1499,7 +1507,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; 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 = ""; }; @@ -1587,7 +1595,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 = ""; }; @@ -1601,7 +1609,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 = ""; }; @@ -2470,6 +2478,7 @@ children = ( 4959CECEC984B3995616F427 /* DataProtectionManager.swift */, D3D455BC2423D911A62ACFB2 /* NSELogger.swift */, + C3AF5A7EE5CA321724ED32CC /* NSEMentionBuilder.swift */, E9DFC0FBA0FC6FC4DC0FC9FC /* NSESettings.swift */, EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */, 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */, @@ -3200,10 +3209,13 @@ 9C4193C4524B35FD6B94B5A9 /* Pills */ = { isa = PBXGroup; children = ( + 15748C254911E3654C93B0ED /* MentionBuilder.swift */, E1E0B4A34E69BD2132BEC521 /* MessageText.swift */, 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */, 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */, + 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */, 7773CBFDBD458E0B7E270507 /* PillView.swift */, + 4B203689DFD1431181A795F4 /* PillContext.swift */, ); path = Pills; sourceTree = ""; @@ -3533,11 +3545,11 @@ C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */, 10B7F8EE25775DE2A305CBB5 /* NotificationCenterProtocol.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, + B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */, 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */, B1E227F34BE43B08E098796E /* TestablePreview.swift */, - BB3073CCD77D906B330BC1D6 /* Tests.swift */, 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */, 35FA991289149D31F4286747 /* UserPreference.swift */, 7431C962E314ADAE38B6D708 /* Analytics */, @@ -4409,6 +4421,7 @@ E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */, 4FC085B1E5D1EB804495E2F4 /* MockMediaProvider.swift in Sources */, 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */, + DEF1477A76F5AAE0A2EB0F32 /* NSEMentionBuilder.swift in Sources */, E571163060CBE87D82CE24FD /* NSESettings.swift in Sources */, 30CC4F796B27BE8B1DFDBF5A /* NSEUserSession.swift in Sources */, 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */, @@ -4776,6 +4789,7 @@ A969147E0EEE0E27EE226570 /* MediaUploadPreviewScreenViewModel.swift in Sources */, 9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */, 8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */, + 8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */, 64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */, 858B0A45257174AAFD448EA0 /* MessageComposer.swift in Sources */, C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */, @@ -4846,7 +4860,9 @@ 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */, EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */, 7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */, + 8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */, 7E2BB42805C59DB57E95610F /* PillView.swift in Sources */, + D9092786ACCFF72565AD7389 /* PillContext.swift in Sources */, 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */, 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */, 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */, @@ -4854,6 +4870,7 @@ 864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */, 153E22E8227F46545E5D681C /* PollRoomTimelineView.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, + FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */, 2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */, 9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */, 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */, @@ -5005,7 +5022,6 @@ 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */, 2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */, 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */, - D85D4FA590305180B4A41795 /* Tests.swift in Sources */, 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */, A2A5AB2E8B3F5CA769E531FA /* TextBasedRoomTimelineViewProtocol.swift in Sources */, BB784A02BADB03C820617A46 /* TextRoomTimelineItem.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index ef95444bc..03d662476 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -34,7 +34,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, private var userSession: UserSessionProtocol? { didSet { userSessionObserver?.cancel() - if userSession != nil { configureNotificationManager() observeUserSessionChanges() diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 2948a7e29..8e5299ab8 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -41,6 +41,7 @@ final class AppSettings { case hasShownWelcomeScreen case swiftUITimelineEnabled case voiceMessageEnabled + case mentionsEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -248,4 +249,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.voiceMessageEnabled, defaultValue: false, storageType: .userDefaults(store)) var voiceMessageEnabled + + @UserPreference(key: UserDefaultsKeys.mentionsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var mentionsEnabled } diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index 7566bd15f..2e1df3e20 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -23,9 +23,9 @@ struct Application: App { private let appCoordinator: AppCoordinatorProtocol init() { - if Tests.isRunningUITests { + if ProcessInfo.isRunningUITests { appCoordinator = UITestsAppCoordinator() - } else if Tests.isRunningUnitTests { + } else if ProcessInfo.isRunningUnitTests { appCoordinator = UnitTestsAppCoordinator() } else { appCoordinator = AppCoordinator() @@ -59,6 +59,6 @@ struct Application: App { } private var shouldHideStatusBar: Bool { - Tests.isRunningUITests + ProcessInfo.isRunningUITests } } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index c07fce234..1550f3067 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -311,7 +311,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let timelineItemFactory = RoomTimelineItemFactory(userID: userID, mediaProvider: userSession.mediaProvider, - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL, + mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled)), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID), appSettings: appSettings) diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 15b8bba4a..7772aaaf1 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -261,7 +261,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private func presentHomeScreen() { let parameters = HomeScreenCoordinatorParameters(userSession: userSession, - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, + mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)), bugReportService: bugReportService, navigationStackCoordinator: detailNavigationStackCoordinator, selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher()) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 836e16f74..5c4395683 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.0.3 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.1 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT // swiftlint:disable all diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index d0c7a9146..7f100bfe6 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -23,6 +23,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { private let temporaryCodeBlockMarkingColor = UIColor.cyan private let linkColor = UIColor.blue private let permalinkBaseURL: URL + private let mentionBuilder: MentionBuilderProtocol private static var cache = LRUCache(countLimit: 1000) @@ -30,8 +31,9 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { cache.removeAllValues() } - init(permalinkBaseURL: URL) { + init(permalinkBaseURL: URL, mentionBuilder: MentionBuilderProtocol) { self.permalinkBaseURL = permalinkBaseURL + self.mentionBuilder = mentionBuilder } func fromPlain(_ string: String?) -> AttributedString? { @@ -45,6 +47,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { let mutableAttributedString = NSMutableAttributedString(string: string) addLinks(mutableAttributedString) + detectPermalinks(mutableAttributedString) removeLinkColors(mutableAttributedString) let result = try? AttributedString(mutableAttributedString, including: \.elementX) @@ -97,10 +100,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) removeDefaultForegroundColor(mutableAttributedString) addLinks(mutableAttributedString) - detectPermalinks(mutableAttributedString) - removeLinkColors(mutableAttributedString) replaceMarkedBlockquotes(mutableAttributedString) replaceMarkedCodeBlocks(mutableAttributedString) + detectPermalinks(mutableAttributedString) + removeLinkColors(mutableAttributedString) removeDTCoreTextArtifacts(mutableAttributedString) let result = try? AttributedString(mutableAttributedString, including: \.elementX) @@ -140,6 +143,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if let value = value as? UIColor, value == temporaryCodeBlockMarkingColor { attributedString.addAttribute(.backgroundColor, value: UIColor(.compound._bgCodeBlock) as Any, range: range) + // Codeblocks should not have links + attributedString.removeAttribute(.link, range: range) } } } @@ -177,7 +182,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { guard let matchRange = Range(match.range, in: string) else { return } - + var hasLink = false attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in if value != nil { @@ -196,7 +201,9 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { link.insert(contentsOf: "https://", at: link.startIndex) } - attributedString.addAttribute(.link, value: link as Any, range: match.range) + if let url = URL(string: link) { + attributedString.addAttribute(.link, value: url, range: match.range) + } } } @@ -206,7 +213,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if let url = value as? URL { switch PermalinkBuilder.detectPermalink(in: url, baseURL: permalinkBaseURL) { case .userIdentifier(let identifier): - attributedString.addAttributes([.MatrixUserID: identifier], range: range) + mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: identifier) case .roomIdentifier(let identifier): attributedString.addAttributes([.MatrixRoomID: identifier], range: range) case .roomAlias(let alias): @@ -280,3 +287,7 @@ extension NSAttributedString.Key { static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name) static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name) } + +protocol MentionBuilderProtocol { + func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) +} diff --git a/ElementX/Sources/Other/Pills/MentionBuilder.swift b/ElementX/Sources/Other/Pills/MentionBuilder.swift new file mode 100644 index 000000000..e62d4d9cc --- /dev/null +++ b/ElementX/Sources/Other/Pills/MentionBuilder.swift @@ -0,0 +1,49 @@ +// +// Copyright 2023 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 Foundation +import UIKit + +struct MentionBuilder: MentionBuilderProtocol { + // Can be removed when mentions are enabled by default + let mentionsEnabled: Bool + + func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) { + guard mentionsEnabled else { + attributedString.addAttributes([.MatrixUserID: userID], range: range) + return + } + + let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range) + let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body) + let blockquote = attributes[.MatrixBlockquote] + + let attachmentData = PillTextAttachmentData(type: .user(userID: userID), font: font) + guard let attachment = PillTextAttachment(attachmentData: attachmentData) else { + attributedString.addAttributes([.MatrixUserID: userID], range: range) + return + } + + var attributesToAdd: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID] + if let blockquote { + // mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute + attributesToAdd[.MatrixBlockquote] = blockquote + } + let attachmentString = NSMutableAttributedString(attachment: attachment) + attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length)) + attributedString.replaceCharacters(in: range, with: attachmentString) + } +} diff --git a/ElementX/Sources/Other/Pills/MessageText.swift b/ElementX/Sources/Other/Pills/MessageText.swift index ec87c9738..8e64f717a 100644 --- a/ElementX/Sources/Other/Pills/MessageText.swift +++ b/ElementX/Sources/Other/Pills/MessageText.swift @@ -15,21 +15,49 @@ // import SwiftUI -import UIKit final class MessageTextView: UITextView { + var roomContext: RoomScreenViewModel.Context? + var updateClosure: (() -> Void)? + // This prevents the magnifying glass from showing up override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { gestureRecognizer as? UILongPressGestureRecognizer == nil } + + func invalidateTextAttachmentsDisplay(update: Bool) { + attributedText.enumerateAttribute(.attachment, + in: NSRange(location: 0, length: attributedText.length), + options: []) { value, range, _ in + guard value != nil else { + return + } + self.layoutManager.invalidateDisplay(forCharacterRange: range) + if update { + updateClosure?() + } + } + } + + // Required to setup the first rendering of the pill view + override func layoutSubviews() { + invalidateTextAttachmentsDisplay(update: false) + super.layoutSubviews() + } } struct MessageText: UIViewRepresentable { @Environment(\.openURL) private var openURLAction: OpenURLAction - let attributedString: AttributedString + @EnvironmentObject private var viewModel: RoomScreenViewModel.Context + @State var attributedString: AttributedString func makeUIView(context: Context) -> MessageTextView { - let textView = MessageTextView() + // Need to use TextKit 1 for mentions + let textView = MessageTextView(usingTextLayoutManager: false) + textView.roomContext = viewModel + textView.updateClosure = { + attributedString = AttributedString(textView.attributedText) + } textView.isEditable = false textView.isScrollEnabled = false textView.adjustsFontForContentSizeCategory = true @@ -46,7 +74,7 @@ struct MessageText: UIViewRepresentable { textView.contentInsetAdjustmentBehavior = .never textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 - textView.textLayoutManager?.usesFontLeading = false + textView.layoutManager.usesFontLeading = false textView.backgroundColor = .clear textView.attributedText = NSAttributedString(attributedString) textView.delegate = context.coordinator @@ -94,9 +122,20 @@ struct MessageText_Previews: PreviewProvider, TestablePreview { container.font = UIFont.preferredFont(forTextStyle: .body) return container }() - + private static let attributedString = AttributedString("Hello World! Hello world! Hello world! Hello world! Hello World! Hellooooooooooooooooooooooo Woooooooooooooooooooooorld", attributes: defaultFontContainer) - private static let attributedStringWithAttachment = "Hello " + AttributedString(NSAttributedString(attachment: PillTextAttachment(data: Data(), ofType: InfoPlistReader.main.pillsUTType))) + " World!" + + private static let attributedStringWithAttachment: AttributedString = { + let testData = PillTextAttachmentData(type: .user(userID: "@alice:example.com"), font: .preferredFont(forTextStyle: .body)) + guard let attachment = PillTextAttachment(attachmentData: testData) else { + return AttributedString() + } + + var attributedString = "Hello test test test " + AttributedString(NSAttributedString(attachment: attachment)) + " World!" + attributedString + .mergeAttributes(defaultFontContainer) + return attributedString + }() private static let htmlStringWithQuote = """ @@ -106,28 +145,36 @@ struct MessageText_Previews: PreviewProvider, TestablePreview { private static let htmlStringWithList = "

This is a list

\n
    \n
  • One
  • \n
  • Two
  • \n
  • And number 3
  • \n
\n" - private static let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL) + private static let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)) + + static var attachmentPreview: some View { + MessageText(attributedString: attributedStringWithAttachment) + .border(Color.purple) + .environmentObject(RoomScreenViewModel.mock.context) + } static var previews: some View { MessageText(attributedString: attributedString) .border(Color.purple) .previewDisplayName("Custom Text") + .environmentObject(RoomScreenViewModel.mock.context) // For comparison Text(attributedString) .border(Color.purple) .previewDisplayName("SwiftUI Default Text") - MessageText(attributedString: attributedStringWithAttachment) - .border(Color.purple) + attachmentPreview .previewDisplayName("Custom Attachment") if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithQuote) { MessageText(attributedString: attributedString) .border(Color.purple) .previewDisplayName("With block quote") + .environmentObject(RoomScreenViewModel.mock.context) } if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithList) { MessageText(attributedString: attributedString) .border(Color.purple) .previewDisplayName("With list") + .environmentObject(RoomScreenViewModel.mock.context) } } } diff --git a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift index 8c72b5150..bd4ee2212 100644 --- a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift +++ b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift @@ -15,21 +15,52 @@ // import SwiftUI +import SwiftUIIntrospect import UIKit final class PillAttachmentViewProvider: NSTextAttachmentViewProvider { + private weak var messageTextView: MessageTextView? + // MARK: - Override + + override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { + 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 + tracksTextAttachmentViewBounds = true + } + + @MainActor override func loadView() { super.loadView() - guard textAttachment is PillTextAttachment else { - MXLog.failure("[PillAttachmentViewProvider]: attachment is missing or not of expected class") + guard let textAttachmentData = (textAttachment as? PillTextAttachment)?.pillData else { + MXLog.failure("[PillAttachmentViewProvider]: attachment is missing data or not of expected class") return } - - let view = PillView() + + let viewModel: PillContext + let imageProvider: ImageProviderProtocol? + if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests { + // The mock viewModel simulates the loading logic for testing purposes + viewModel = PillContext.mock(type: .loadUser) + imageProvider = MockMediaProvider() + } else if let roomContext = messageTextView?.roomContext { + viewModel = PillContext(roomContext: roomContext, data: textAttachmentData) + imageProvider = roomContext.imageProvider + } else { + MXLog.failure("[PillAttachmentViewProvider]: missing room context") + return + } + + let view = PillView(imageProvider: imageProvider, viewModel: viewModel) { [weak self] in + self?.messageTextView?.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 } } diff --git a/ElementX/Sources/Other/Pills/PillContext.swift b/ElementX/Sources/Other/Pills/PillContext.swift new file mode 100644 index 000000000..e6144298d --- /dev/null +++ b/ElementX/Sources/Other/Pills/PillContext.swift @@ -0,0 +1,110 @@ +// +// Copyright 2023 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 Combine +import Foundation + +@MainActor +final class PillContext: ObservableObject { + enum PillViewState { + case loading(contentID: String) + case loaded(contentID: String, name: String, avatarURL: URL?) + } + + @Published private var state: PillViewState + + var url: URL? { + switch state { + case .loading: + return nil + case .loaded(_, _, let url): + return url + } + } + + var name: String? { + switch state { + case .loading: + return nil + case .loaded(_, let name, _): + return name + } + } + + var displayText: String { + switch state { + case .loaded(_, let name, _): + return name + case .loading(let contentID): + return contentID + } + } + + var contentID: String { + switch state { + case .loaded(let contentID, _, _), .loading(let contentID): + return contentID + } + } + + private var cancellable: AnyCancellable? + + init(roomContext: RoomScreenViewModel.Context, data: PillTextAttachmentData) { + switch data.type { + case let .user(id): + if let profile = roomContext.viewState.members[id] { + state = .loaded(contentID: id, name: profile.displayName ?? id, avatarURL: profile.avatarURL) + } else { + state = .loading(contentID: id) + cancellable = roomContext.$viewState.sink { [weak self] viewState in + guard let self else { + return + } + if let profile = viewState.members[id] { + state = .loaded(contentID: id, name: profile.displayName ?? id, avatarURL: profile.avatarURL) + cancellable = nil + } + } + } + } + } +} + +extension PillContext { + enum MockType { + case loadUser + case loadedUser + } + + static func mock(type: MockType) -> PillContext { + let pillType: PillType + switch type { + case .loadUser: + pillType = .user(userID: "@test:test.com") + let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) + Task { + try? await Task.sleep(for: .seconds(2)) + viewModel.state = .loaded(contentID: "@test:test.com", name: "Test Longer Display Text", avatarURL: URL.documentsDirectory) + } + return viewModel + case .loadedUser: + pillType = .user(userID: "@test:test.com") + let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) + viewModel.state = .loaded(contentID: "@test:test.com", name: "Very Very Long Test Display Text", avatarURL: URL.documentsDirectory) + return viewModel + } + } +} diff --git a/ElementX/Sources/Other/Pills/PillTextAttachment.swift b/ElementX/Sources/Other/Pills/PillTextAttachment.swift index 6ceb61d0a..9a36e6aba 100644 --- a/ElementX/Sources/Other/Pills/PillTextAttachment.swift +++ b/ElementX/Sources/Other/Pills/PillTextAttachment.swift @@ -17,4 +17,27 @@ import UIKit /// Text attachment for pills display. -final class PillTextAttachment: NSTextAttachment { } +final class PillTextAttachment: NSTextAttachment { + convenience init?(attachmentData: PillTextAttachmentData) { + let encoder = JSONEncoder() + guard let encodedData = try? encoder.encode(attachmentData) else { return nil } + self.init(data: encodedData, ofType: InfoPlistReader.main.pillsUTType) + } + + var pillData: PillTextAttachmentData? { + guard let contents else { + return nil + } + let decoder = JSONDecoder() + return try? decoder.decode(PillTextAttachmentData.self, from: contents) + } + + override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { + var rect = super.attachmentBounds(for: textContainer, proposedLineFragment: lineFrag, glyphPosition: position, characterIndex: charIndex) + if let font = pillData?.font { + // Align the pill text vertically with the surrounding text. + rect.origin.y = font.descender + (font.lineHeight - rect.height) / 2.0 + } + return rect + } +} diff --git a/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift b/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift new file mode 100644 index 000000000..416468d9f --- /dev/null +++ b/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift @@ -0,0 +1,64 @@ +// +// Copyright 2023 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 Foundation +import UIKit + +enum PillType: Codable { + /// A pill that mentions a user + case user(userID: String) +} + +struct PillTextAttachmentData { + // MARK: - Properties + + /// Pill type + let type: PillType + + /// Font for the display name + let font: UIFont +} + +extension PillTextAttachmentData: Codable { + // MARK: - Codable + + enum CodingKeys: String, CodingKey { + case type + case font + } + + enum PillTextAttachmentDataError: Error { + case noFontData + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(PillType.self, forKey: .type) + let fontData = try container.decode(Data.self, forKey: .font) + if let font = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIFont.self, from: fontData) { + self.font = font + } else { + throw PillTextAttachmentDataError.noFontData + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false) + try container.encode(fontData, forKey: .font) + } +} diff --git a/ElementX/Sources/Other/Pills/PillView.swift b/ElementX/Sources/Other/Pills/PillView.swift index d4634bac8..f514eaaa5 100644 --- a/ElementX/Sources/Other/Pills/PillView.swift +++ b/ElementX/Sources/Other/Pills/PillView.swift @@ -17,21 +17,43 @@ import SwiftUI struct PillView: View { + let imageProvider: ImageProviderProtocol? + @ObservedObject var viewModel: PillContext + /// callback triggerd by changes in the display text + let didChangeText: () -> Void + var body: some View { - Button { - MXLog.info("TEXT ATTACHMENT TEST") - } label: { - HStack { - Image(asset: Asset.Images.appLogo) - .resizable() - .scaledToFit() - } + HStack(spacing: 4) { + LoadableAvatarImage(url: viewModel.url, name: viewModel.name, contentID: viewModel.contentID, avatarSize: .custom(24), imageProvider: imageProvider) + Text(viewModel.displayText) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textOnSolidPrimary) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + // for now design has defined no color so we will just use gray + .background(Capsule().foregroundColor(.gray)) + .frame(maxWidth: 235) + .onChange(of: viewModel.displayText) { _ in + didChangeText() } } } struct PillView_Previews: PreviewProvider, TestablePreview { + static let mockMediaProvider = MockMediaProvider() + + static var loading: some View { + PillView(imageProvider: mockMediaProvider, + viewModel: PillContext.mock(type: .loadUser)) { } + } + static var previews: some View { - PillView() + loading + .previewDisplayName("Loading") + PillView(imageProvider: mockMediaProvider, + viewModel: PillContext.mock(type: .loadedUser)) { } + .previewDisplayName("Loaded Long") } } diff --git a/ElementX/Sources/Other/Tests.swift b/ElementX/Sources/Other/ProcessInfo.swift similarity index 72% rename from ElementX/Sources/Other/Tests.swift rename to ElementX/Sources/Other/ProcessInfo.swift index fddda70b5..11fa261ac 100644 --- a/ElementX/Sources/Other/Tests.swift +++ b/ElementX/Sources/Other/ProcessInfo.swift @@ -16,11 +16,11 @@ import Foundation -public enum Tests { +extension ProcessInfo { /// Flag indicating whether the app is running the unit tests. static var isRunningUnitTests: Bool { #if DEBUG - ProcessInfo.processInfo.environment["IS_RUNNING_UNIT_TESTS"] == "1" + processInfo.environment["IS_RUNNING_UNIT_TESTS"] == "1" #else false #endif @@ -29,7 +29,7 @@ public enum Tests { /// Flag indicating whether the app is running the UI tests. static var isRunningUITests: Bool { #if DEBUG - ProcessInfo.processInfo.environment["UI_TESTS_SCREEN"] != nil + processInfo.environment["UI_TESTS_SCREEN"] != nil #else false #endif @@ -41,9 +41,9 @@ public enum Tests { } /// The identifier of the screen to be loaded when running UI tests. - static var screenID: UITestsScreenIdentifier? { + static var testScreenID: UITestsScreenIdentifier? { #if DEBUG - ProcessInfo.processInfo.environment["UI_TESTS_SCREEN"].flatMap(UITestsScreenIdentifier.init) + processInfo.environment["UI_TESTS_SCREEN"].flatMap(UITestsScreenIdentifier.init) #else nil #endif @@ -55,9 +55,17 @@ public enum Tests { } #if DEBUG - return ProcessInfo.processInfo.environment["UI_TESTS_DISABLE_TIMELINE_ACCESSIBILITY"] != nil + return processInfo.environment["UI_TESTS_DISABLE_TIMELINE_ACCESSIBILITY"] != nil #else return false #endif } + + static var isXcodePreview: Bool { + #if DEBUG + processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + #else + false + #endif + } } diff --git a/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift b/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift index a47799654..05c816244 100644 --- a/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift +++ b/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift @@ -23,13 +23,13 @@ public extension Animation { /// `noAnimation` if running tests, otherwise `default` animation if `UIAccessibility.isReduceMotionEnabled` is false static var elementDefault: Animation { - let animation: Animation = Tests.isRunningTests ? .noAnimation : .default + let animation: Animation = ProcessInfo.isRunningTests ? .noAnimation : .default return animation.disabledIfReduceMotionEnabled() } // `noAnimation` if running tests, otherwise `self` if `UIAccessibility.isReduceMotionEnabled` is false func disabledDuringTests() -> Self { - let animation: Animation = Tests.isRunningTests ? .noAnimation : self + let animation: Animation = ProcessInfo.isRunningTests ? .noAnimation : self return animation.disabledIfReduceMotionEnabled() } diff --git a/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift b/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift index 7c7e195da..e5b943b53 100644 --- a/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift +++ b/ElementX/Sources/Other/SwiftUI/Animation/ShimmerModifier.swift @@ -78,7 +78,8 @@ extension View { struct ShimmerOverlay_Previews: PreviewProvider, TestablePreview { static let viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: MockClientProxy(userID: ""), mediaProvider: MockMediaProvider()), - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, + mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)), selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index a6d7532b8..60dc4ab7d 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -338,7 +338,7 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview { mediaProvider: MockMediaProvider()) return HomeScreenViewModel(userSession: userSession, - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)), selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift index ee0dd7af4..dc84cb8bb 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift @@ -154,7 +154,7 @@ struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview { mediaProvider: MockMediaProvider()) return HomeScreenViewModel(userSession: userSession, - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)), selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index b9092a63d..5653ba1e1 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -188,7 +188,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview { mediaProvider: MockMediaProvider()) let viewModel = HomeScreenViewModel(userSession: userSession, - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, + mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)), selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift index 7c692f488..85f4deda2 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift @@ -201,7 +201,7 @@ struct FormattedBodyText_Previews: PreviewProvider, TestablePreview { "

This is a list

\n
    \n
  • One
  • \n
  • Two
  • \n
  • And number 3
  • \n
\n" ] - let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL) + let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)) ScrollView { VStack(alignment: .leading, spacing: 24.0) { @@ -234,6 +234,7 @@ private struct PreviewBubbleModifier: ViewModifier { .padding(timelineStyle == .bubbles ? 8 : 0) .background(timelineStyle == .bubbles ? Color.compound._bgBubbleOutgoing : nil) .cornerRadius(timelineStyle == .bubbles ? 12 : 0) + .environmentObject(RoomScreenViewModel.mock.context) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift index 9abb220db..367eeb1c9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift @@ -104,7 +104,7 @@ class TimelineTableViewController: UIViewController { // Prevents XCUITest from invoking the diffable dataSource's cellProvider // for each possible cell, causing layout issues - tableView.accessibilityElementsHidden = Tests.shouldDisableTimelineAccessibility + tableView.accessibilityElementsHidden = ProcessInfo.shouldDisableTimelineAccessibility scrollToBottomPublisher .sink { [weak self] _ in diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 9a9686331..9d8822fd8 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -50,6 +50,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var readReceiptsEnabled: Bool { get set } var swiftUITimelineEnabled: Bool { get set } var voiceMessageEnabled: Bool { get set } + var mentionsEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 2c0e548d7..05b355130 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -45,6 +45,11 @@ struct DeveloperOptionsScreen: View { Text("SwiftUI Timeline") Text("Resets on reboot") } + + Toggle(isOn: $context.mentionsEnabled) { + Text("Show user mentions") + Text("Requires app reboot") + } } Section("Room creation") { diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 5e40c4a91..b97d6a7c6 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -443,7 +443,8 @@ class ClientProxy: ClientProxyProtocol { .finish() let roomListService = syncService.roomListService() - let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .homeDirectory)) + let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL, + mentionBuilder: MentionBuilder(mentionsEnabled: appSettings.mentionsEnabled))) let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID), messageEventStringBuilder: roomMessageEventStringBuilder) roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index 9cb3860ac..ea7f55c83 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -97,7 +97,9 @@ enum RoomTimelineItemFixtures { isThreaded: false, sender: .init(id: "", displayName: "Helena"), content: .init(body: "", - formattedBody: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL).fromHTML("Hol' up
New home office set up!
That's amazing! Congrats 🥳"))) + formattedBody: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, + mentionBuilder: MentionBuilder(mentionsEnabled: ServiceLocator.shared.settings.mentionsEnabled)) + .fromHTML("Hol' up
New home office set up!
That's amazing! Congrats 🥳"))) ] /// A small chunk of events, containing 2 text items. diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 75e9f4203..256f2f562 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -50,7 +50,7 @@ class UITestsAppCoordinator: AppCoordinatorProtocol { delegate.window??.layer.speed = 0 } - guard let screenID = Tests.screenID else { fatalError("Unable to launch with unknown screen.") } + guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") } let mockScreen = MockScreen(id: screenID) navigationRootCoordinator.setRootCoordinator(mockScreen.coordinator) @@ -162,7 +162,7 @@ class MockScreen: Identifiable { let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"), mediaProvider: MockMediaProvider()) let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session, - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)), bugReportService: BugReportServiceMock(), navigationStackCoordinator: navigationStackCoordinator, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher())) diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 979b95b32..9f010763a 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -20,7 +20,7 @@ import UserNotifications class NotificationServiceExtension: UNNotificationServiceExtension { private let settings = NSESettings() - private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .homeDirectory))) + private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .homeDirectory, mentionBuilder: NSEMentionBuilder()))) private lazy var keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) private var handler: ((UNNotificationContent) -> Void)? diff --git a/NSE/Sources/Other/NSEMentionBuilder.swift b/NSE/Sources/Other/NSEMentionBuilder.swift new file mode 100644 index 000000000..543aa9c9c --- /dev/null +++ b/NSE/Sources/Other/NSEMentionBuilder.swift @@ -0,0 +1,23 @@ +// +// Copyright 2023 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 Foundation + +struct NSEMentionBuilder: MentionBuilderProtocol { + func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) { + attributedString.addAttributes([.MatrixUserID: userID], range: range) + } +} diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index b3050762a..1b7dd850d 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -18,7 +18,7 @@ import XCTest class AttributedStringBuilderTests: XCTestCase { - let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL) + let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)) let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) func testRenderHTMLStringWithHeaders() { @@ -413,6 +413,14 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes") } + func testUserMentionAtachment() { + let string = "https://matrix.to/#/@test:matrix.org" + let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) + XCTAssertNotNil(attributedStringFromHTML?.attachment) + let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) + XCTAssert(attributedStringFromPlain?.attachment.isNil == false) + } + // MARK: - Private private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) { diff --git a/UnitTests/Sources/AttributedStringTests.swift b/UnitTests/Sources/AttributedStringTests.swift index c779326d7..304973fd4 100644 --- a/UnitTests/Sources/AttributedStringTests.swift +++ b/UnitTests/Sources/AttributedStringTests.swift @@ -21,7 +21,7 @@ class AttributedStringTests: XCTestCase { func testReplacingFontWithPresentationIntent() { // Given a string parsed from HTML that contains specific fixed size fonts. let boldString = "Bold" - guard let originalString = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL) + guard let originalString = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)) .fromHTML("Normal \(boldString) Normal.") else { XCTFail("The attributed string should be built from the HTML.") return diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index aa069cbe4..962eec4c9 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -31,7 +31,7 @@ class HomeScreenViewModelTests: XCTestCase { clientProxy = MockClientProxy(userID: "@mock:client.com") viewModel = HomeScreenViewModel(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()), - attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL), + attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)), selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, diff --git a/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.1.png b/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.1.png index 5d893510d..1f7a2362b 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abd682ff45a39c88296804ac67c29c6425b3231a03002c8d0cb5b59df4464707 -size 212642 +oid sha256:b689d76c95de930e792bfbe6ffb86bb9e006958fa77a55ed0aba7ad69432530f +size 212484 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.2.png b/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.2.png index 5849de855..343603dc6 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.2.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_formattedBodyText.2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62be6e4e83d8797e26070862c8812366346594fec8e1fad8daddb0002611a879 -size 185793 +oid sha256:16ca3e2a15fd004ff1372409fac07bac16ff8b053c72a8e2acc9975afbe9957f +size 185944 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_messageText.Custom-Attachment.png b/UnitTests/__Snapshots__/PreviewTests/test_messageText.Custom-Attachment.png index 6895f5abd..2ec551aea 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_messageText.Custom-Attachment.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_messageText.Custom-Attachment.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7e5576967557d1cea6f5b50383c9db6e7749a0ba3a625c3bac987ccd97e0c81 -size 60539 +oid sha256:7757fa407df9869e19ef6d59c487e0ce8fb848fbb0f864ec328ecfc86bdc1834 +size 62544 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-block-quote.png b/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-block-quote.png index e0bb034ed..faacc14ce 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-block-quote.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-block-quote.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8f07d6150865cb0f7c4bec8743f552440d2e610fbba850675b0e28143fd83d4 -size 98236 +oid sha256:40267c3676ec3388535b82f1739a8f80ad19b36d38a5f02ecb2a86539b507092 +size 98225 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-list.png b/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-list.png index 5aa380df7..889ec1aa7 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-list.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_messageText.With-list.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86663f46166d096cc67fd32973e99dae7f72aa0d9c3b914dd600a57db54410de -size 74887 +oid sha256:f3d7d75000e46cd301244335d46367e03afda48b32328230b2bd0fbaf3e1abc9 +size 74667 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pillView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_pillView.1.png deleted file mode 100644 index 0dd33a5b4..000000000 --- a/UnitTests/__Snapshots__/PreviewTests/test_pillView.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71240349d0bfff7c43c230b5e13165c20b18bd20d9df92df7c7df7acf2aa9c61 -size 575946 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pillView.Loaded-Long.png b/UnitTests/__Snapshots__/PreviewTests/test_pillView.Loaded-Long.png new file mode 100644 index 000000000..2459c06db --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pillView.Loaded-Long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b28152066f759f0fb99c4bfab9e51bb915a4f9d12c555c05063a00b13eec1476 +size 65700 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pillView.Loading.png b/UnitTests/__Snapshots__/PreviewTests/test_pillView.Loading.png new file mode 100644 index 000000000..5d3581b2b --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pillView.Loading.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb935eb860e241bf52bc2b1bd45acf8361f661666721b3d512b6f5afd432cf70 +size 64239 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png index 6f562522d..9851e4d1d 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4ceb7aab76537608639630780df60331f7ba8ee6d775ff489c320e45773a73 -size 313849 +oid sha256:2c6e64db92199ab4f6239a2ede5240f4bc22fb1f533e455d9787910c14270ec4 +size 313456 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-RTL.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-RTL.png index 1d25fcdfc..1e1a8d5fe 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-RTL.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-RTL.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbe07e396c2259244e51a31274c4a490ad6c8084d56c34b3a9e1e15d7b992520 -size 341255 +oid sha256:19348f5dc147cbd87d1fbcdb9f228f18e00b29e6f6bf0a2d4c95a19e12424655 +size 341025 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-with-read-receipts.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-with-read-receipts.png index 555817603..d1b883044 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-with-read-receipts.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline-with-read-receipts.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a5e6199c9faae906e3d9ae844f6d92153d8bd57c2bae48af22b70902fe96d65 -size 337294 +oid sha256:1241ae3a54ad3f598748c86df59853a96b6266135816c5c3213b9b45f38b3272 +size 337109 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline.png index e270c49a7..a938add4a 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemBubbledStylerView.Mock-Timeline.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c21bd9280faed90b3442f883f40d0dd035172a6ac00025809a5c5c1742fdff6 -size 339621 +oid sha256:e6be01fcd027b50ca4b6eabf43c82c3530e23a939b22bd5279646d7a935f6989 +size 339436 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemPlainStylerView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemPlainStylerView.1.png index 3500faba3..3ccda25ff 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineItemPlainStylerView.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineItemPlainStylerView.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65613a6d61b256a3bbf0d9c08992dbd38a45449ab778e9152a00199e6b6cbd0b -size 378575 +oid sha256:5ed2fe6d8dbec14cc7f97d0ede2b86d0c1dbbbc559cf95a9fa2fdbbdbe8bd785 +size 378187 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png index 6f562522d..9851e4d1d 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4ceb7aab76537608639630780df60331f7ba8ee6d775ff489c320e45773a73 -size 313849 +oid sha256:2c6e64db92199ab4f6239a2ede5240f4bc22fb1f533e455d9787910c14270ec4 +size 313456 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png index 6f562522d..9851e4d1d 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4ceb7aab76537608639630780df60331f7ba8ee6d775ff489c320e45773a73 -size 313849 +oid sha256:2c6e64db92199ab4f6239a2ede5240f4bc22fb1f533e455d9787910c14270ec4 +size 313456 diff --git a/changelog.d/1804.feature b/changelog.d/1804.feature new file mode 100644 index 000000000..adb617e84 --- /dev/null +++ b/changelog.d/1804.feature @@ -0,0 +1 @@ +User mentions pills (behind a Feature Flag). \ No newline at end of file