mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
@room pill (#1834)
* all users mention pill + red higlight for own mentions * more tests * changelog * removed useless if let
This commit is contained in:
@@ -366,6 +366,7 @@
|
|||||||
754602A7B2AAD443C4228ED4 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; };
|
754602A7B2AAD443C4228ED4 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; };
|
||||||
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; };
|
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; };
|
||||||
764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */; };
|
764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */; };
|
||||||
|
767D366C40F1311CFA333763 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86376BEE425704AEE197CA54 /* PillContext.swift */; };
|
||||||
76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; };
|
76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; };
|
||||||
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
||||||
7719778A682FDAC21445E9C8 /* OnboardingLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */; };
|
7719778A682FDAC21445E9C8 /* OnboardingLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */; };
|
||||||
@@ -542,6 +543,7 @@
|
|||||||
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
|
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
|
||||||
A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; };
|
A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; };
|
||||||
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
|
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; };
|
||||||
|
A7BC01132AC6E83A009C9784 /* PillContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7BC01122AC6E83A009C9784 /* PillContextTests.swift */; };
|
||||||
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
||||||
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
|
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
|
||||||
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
|
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
|
||||||
@@ -716,7 +718,6 @@
|
|||||||
D871C8CF46950F959C9A62C3 /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54464351F170D570110AFCA /* WelcomeScreen.swift */; };
|
D871C8CF46950F959C9A62C3 /* WelcomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54464351F170D570110AFCA /* WelcomeScreen.swift */; };
|
||||||
D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; };
|
D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; };
|
||||||
D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.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 */; };
|
D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */; };
|
||||||
D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; };
|
D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; };
|
||||||
D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; };
|
D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; };
|
||||||
@@ -1114,7 +1115,6 @@
|
|||||||
4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
|
4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
|
||||||
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = "<group>"; };
|
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = "<group>"; };
|
||||||
4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = "<group>"; };
|
4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = "<group>"; };
|
||||||
4B203689DFD1431181A795F4 /* PillContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContext.swift; sourceTree = "<group>"; };
|
|
||||||
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
|
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
|
||||||
4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = "<group>"; };
|
4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = "<group>"; };
|
||||||
4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
@@ -1281,6 +1281,7 @@
|
|||||||
8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
|
8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = "<group>"; };
|
854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; };
|
85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
86376BEE425704AEE197CA54 /* PillContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContext.swift; sourceTree = "<group>"; };
|
||||||
86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModel.swift; sourceTree = "<group>"; };
|
86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1370,6 +1371,7 @@
|
|||||||
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
||||||
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
|
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
|
||||||
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
|
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
|
||||||
|
A7BC01122AC6E83A009C9784 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = "<group>"; };
|
||||||
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
|
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
|
||||||
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
|
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = "<group>"; };
|
A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -2755,6 +2757,7 @@
|
|||||||
7583EAC171059A86B767209F /* MediaProvider */,
|
7583EAC171059A86B767209F /* MediaProvider */,
|
||||||
7DBC911559934065993A5FF4 /* NotificationManager */,
|
7DBC911559934065993A5FF4 /* NotificationManager */,
|
||||||
1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */,
|
1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */,
|
||||||
|
A7BC01122AC6E83A009C9784 /* PillContextTests.swift */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3212,10 +3215,10 @@
|
|||||||
15748C254911E3654C93B0ED /* MentionBuilder.swift */,
|
15748C254911E3654C93B0ED /* MentionBuilder.swift */,
|
||||||
E1E0B4A34E69BD2132BEC521 /* MessageText.swift */,
|
E1E0B4A34E69BD2132BEC521 /* MessageText.swift */,
|
||||||
1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */,
|
1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */,
|
||||||
|
86376BEE425704AEE197CA54 /* PillContext.swift */,
|
||||||
9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */,
|
9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */,
|
||||||
913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */,
|
913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */,
|
||||||
7773CBFDBD458E0B7E270507 /* PillView.swift */,
|
7773CBFDBD458E0B7E270507 /* PillView.swift */,
|
||||||
4B203689DFD1431181A795F4 /* PillContext.swift */,
|
|
||||||
);
|
);
|
||||||
path = Pills;
|
path = Pills;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -4537,6 +4540,7 @@
|
|||||||
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
|
81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */,
|
||||||
FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */,
|
FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */,
|
||||||
7F02063FB3D1C3E5601471A1 /* WelcomeScreenScreenViewModelTests.swift in Sources */,
|
7F02063FB3D1C3E5601471A1 /* WelcomeScreenScreenViewModelTests.swift in Sources */,
|
||||||
|
A7BC01132AC6E83A009C9784 /* PillContextTests.swift in Sources */,
|
||||||
3116693C5EB476E028990416 /* XCTestCase.swift in Sources */,
|
3116693C5EB476E028990416 /* XCTestCase.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -4859,10 +4863,10 @@
|
|||||||
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */,
|
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */,
|
||||||
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */,
|
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */,
|
||||||
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */,
|
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */,
|
||||||
|
767D366C40F1311CFA333763 /* PillContext.swift in Sources */,
|
||||||
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */,
|
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */,
|
||||||
8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */,
|
8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */,
|
||||||
7E2BB42805C59DB57E95610F /* PillView.swift in Sources */,
|
7E2BB42805C59DB57E95610F /* PillView.swift in Sources */,
|
||||||
D9092786ACCFF72565AD7389 /* PillContext.swift in Sources */,
|
|
||||||
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */,
|
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */,
|
||||||
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */,
|
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */,
|
||||||
16CBD087038DE3815CDA512C /* PollMock.swift in Sources */,
|
16CBD087038DE3815CDA512C /* PollMock.swift in Sources */,
|
||||||
|
|||||||
@@ -143,8 +143,9 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
if let value = value as? UIColor,
|
if let value = value as? UIColor,
|
||||||
value == temporaryCodeBlockMarkingColor {
|
value == temporaryCodeBlockMarkingColor {
|
||||||
attributedString.addAttribute(.backgroundColor, value: UIColor(.compound._bgCodeBlock) as Any, range: range)
|
attributedString.addAttribute(.backgroundColor, value: UIColor(.compound._bgCodeBlock) as Any, range: range)
|
||||||
// Codeblocks should not have links
|
// Codeblocks should not have links and all users mentions
|
||||||
attributedString.removeAttribute(.link, range: range)
|
attributedString.removeAttribute(.link, range: range)
|
||||||
|
attributedString.removeAttribute(.MatrixAllUsersMention, range: range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,36 +174,40 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
|
|
||||||
let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: [])
|
let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: [])
|
||||||
matches.append(contentsOf: linkMatches)
|
matches.append(contentsOf: linkMatches)
|
||||||
guard matches.count > 0 else {
|
if matches.count > 0 {
|
||||||
return
|
// Sort the links by length so the longest one always takes priority
|
||||||
}
|
matches.sorted { $0.range.length > $1.range.length }.forEach { match in
|
||||||
|
guard let matchRange = Range(match.range, in: string) else {
|
||||||
// Sort the links by length so the longest one always takes priority
|
return
|
||||||
matches.sorted { $0.range.length > $1.range.length }.forEach { match in
|
}
|
||||||
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 {
|
||||||
var hasLink = false
|
hasLink = true
|
||||||
attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in
|
stop.pointee = true
|
||||||
if value != nil {
|
}
|
||||||
hasLink = true
|
}
|
||||||
stop.pointee = true
|
|
||||||
|
if hasLink {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if hasLink {
|
|
||||||
return
|
MatrixEntityRegex.allUsersRegex.matches(in: string, options: []).forEach { match in
|
||||||
}
|
if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) == nil {
|
||||||
|
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,6 +231,13 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attributedString.enumerateAttribute(.MatrixAllUsersMention, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
|
||||||
|
if let value = value as? Bool,
|
||||||
|
value {
|
||||||
|
mentionBuilder.handleAllUsersMention(for: attributedString, in: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) {
|
private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) {
|
||||||
@@ -286,8 +298,10 @@ extension NSAttributedString.Key {
|
|||||||
static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name)
|
static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name)
|
||||||
static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name)
|
static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name)
|
||||||
static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name)
|
static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name)
|
||||||
|
static let MatrixAllUsersMention: NSAttributedString.Key = .init(rawValue: AllUsersMentionAttribute.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol MentionBuilderProtocol {
|
protocol MentionBuilderProtocol {
|
||||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String)
|
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String)
|
||||||
|
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ enum RoomAliasAttribute: AttributedStringKey {
|
|||||||
public static var name = "MXRoomAliasAttribute"
|
public static var name = "MXRoomAliasAttribute"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AllUsersMentionAttribute: AttributedStringKey {
|
||||||
|
typealias Value = Bool
|
||||||
|
public static var name = "MXAllUsersMentionAttribute"
|
||||||
|
}
|
||||||
|
|
||||||
struct EventIDAttributeValue: Hashable {
|
struct EventIDAttributeValue: Hashable {
|
||||||
let roomID: String
|
let roomID: String
|
||||||
let eventID: String
|
let eventID: String
|
||||||
@@ -55,6 +60,8 @@ extension AttributeScopes {
|
|||||||
let roomAlias: RoomAliasAttribute
|
let roomAlias: RoomAliasAttribute
|
||||||
let eventID: EventIDAttribute
|
let eventID: EventIDAttribute
|
||||||
|
|
||||||
|
let allUsersMention: AllUsersMentionAttribute
|
||||||
|
|
||||||
let swiftUI: SwiftUIAttributes
|
let swiftUI: SwiftUIAttributes
|
||||||
let uiKit: UIKitAttributes
|
let uiKit: UIKitAttributes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ enum MatrixEntityRegex: String {
|
|||||||
case roomAlias
|
case roomAlias
|
||||||
case roomId
|
case roomId
|
||||||
case eventId
|
case eventId
|
||||||
|
case allUsers
|
||||||
|
|
||||||
var rawValue: String {
|
var rawValue: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -36,6 +37,8 @@ enum MatrixEntityRegex: String {
|
|||||||
return "![A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue
|
return "![A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue
|
||||||
case .eventId:
|
case .eventId:
|
||||||
return "\\$[a-z0-9_\\-\\/]+(:[a-z0-9]+\\.[a-z0-9]+)?"
|
return "\\$[a-z0-9_\\-\\/]+(:[a-z0-9]+\\.[a-z0-9]+)?"
|
||||||
|
case .allUsers:
|
||||||
|
return "@room"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ enum MatrixEntityRegex: String {
|
|||||||
static var roomAliasRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive)
|
static var roomAliasRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive)
|
||||||
static var roomIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive)
|
static var roomIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive)
|
||||||
static var eventIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive)
|
static var eventIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive)
|
||||||
|
static var allUsersRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.allUsers.rawValue)
|
||||||
static var linkRegex = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
static var linkRegex = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
|
|
||||||
@@ -87,4 +91,12 @@ enum MatrixEntityRegex: String {
|
|||||||
|
|
||||||
return match.range.length == identifier.count
|
return match.range.length == identifier.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func containsMatrixAllUsers(_ string: String) -> Bool {
|
||||||
|
guard allUsersRegex.firstMatch(in: string) != nil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,28 @@ struct MentionBuilder: MentionBuilderProtocol {
|
|||||||
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
|
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||||
attributedString.replaceCharacters(in: range, with: attachmentString)
|
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) {
|
||||||
|
guard mentionsEnabled else {
|
||||||
|
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: .allUsers, font: font)
|
||||||
|
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributesToAdd: [NSAttributedString.Key: Any] = [:]
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,21 +40,21 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewModel: PillContext
|
let context: PillContext
|
||||||
let imageProvider: ImageProviderProtocol?
|
let imageProvider: ImageProviderProtocol?
|
||||||
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
|
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
|
||||||
// The mock viewModel simulates the loading logic for testing purposes
|
// The mock viewModel simulates the loading logic for testing purposes
|
||||||
viewModel = PillContext.mock(type: .loadUser)
|
context = PillContext.mock(type: .loadUser(isOwn: false))
|
||||||
imageProvider = MockMediaProvider()
|
imageProvider = MockMediaProvider()
|
||||||
} else if let roomContext = messageTextView?.roomContext {
|
} else if let roomContext = messageTextView?.roomContext {
|
||||||
viewModel = PillContext(roomContext: roomContext, data: textAttachmentData)
|
context = PillContext(roomContext: roomContext, data: textAttachmentData)
|
||||||
imageProvider = roomContext.imageProvider
|
imageProvider = roomContext.imageProvider
|
||||||
} else {
|
} else {
|
||||||
MXLog.failure("[PillAttachmentViewProvider]: missing room context")
|
MXLog.failure("[PillAttachmentViewProvider]: missing room context")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let view = PillView(imageProvider: imageProvider, viewModel: viewModel) { [weak self] in
|
let view = PillView(imageProvider: imageProvider, context: context) { [weak self] in
|
||||||
self?.messageTextView?.invalidateTextAttachmentsDisplay(update: true)
|
self?.messageTextView?.invalidateTextAttachmentsDisplay(update: true)
|
||||||
}
|
}
|
||||||
let controller = UIHostingController(rootView: view)
|
let controller = UIHostingController(rootView: view)
|
||||||
|
|||||||
@@ -19,92 +19,72 @@ import Foundation
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class PillContext: ObservableObject {
|
final class PillContext: ObservableObject {
|
||||||
enum PillViewState {
|
struct PillViewState: Equatable {
|
||||||
case loading(contentID: String)
|
let contentID: String
|
||||||
case loaded(contentID: String, name: String, avatarURL: URL?)
|
let isOwnMention: Bool
|
||||||
|
let name: String?
|
||||||
|
let displayText: String
|
||||||
|
let avatarURL: URL?
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published private var state: PillViewState
|
@Published private(set) var viewState: 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?
|
private var cancellable: AnyCancellable?
|
||||||
|
|
||||||
init(roomContext: RoomScreenViewModel.Context, data: PillTextAttachmentData) {
|
init(roomContext: RoomScreenViewModel.Context, data: PillTextAttachmentData) {
|
||||||
switch data.type {
|
switch data.type {
|
||||||
case let .user(id):
|
case let .user(id):
|
||||||
|
let isOwnMention = id == roomContext.viewState.ownUserID
|
||||||
if let profile = roomContext.viewState.members[id] {
|
if let profile = roomContext.viewState.members[id] {
|
||||||
state = .loaded(contentID: id, name: profile.displayName ?? id, avatarURL: profile.avatarURL)
|
let name = profile.displayName ?? id
|
||||||
|
viewState = PillViewState(contentID: id, isOwnMention: isOwnMention, name: name, displayText: name, avatarURL: profile.avatarURL)
|
||||||
} else {
|
} else {
|
||||||
state = .loading(contentID: id)
|
viewState = PillViewState(contentID: id, isOwnMention: isOwnMention, name: nil, displayText: id, avatarURL: nil)
|
||||||
cancellable = roomContext.$viewState.sink { [weak self] viewState in
|
cancellable = roomContext.$viewState.sink { [weak self] viewState in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let profile = viewState.members[id] {
|
if let profile = viewState.members[id] {
|
||||||
state = .loaded(contentID: id, name: profile.displayName ?? id, avatarURL: profile.avatarURL)
|
let name = profile.displayName ?? id
|
||||||
|
self.viewState = PillViewState(contentID: id, isOwnMention: isOwnMention, name: name, displayText: name, avatarURL: profile.avatarURL)
|
||||||
cancellable = nil
|
cancellable = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .allUsers:
|
||||||
|
viewState = PillViewState(contentID: roomContext.viewState.roomID, isOwnMention: true, name: roomContext.viewState.roomTitle, displayText: "@room", avatarURL: roomContext.viewState.roomAvatarURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PillContext {
|
extension PillContext {
|
||||||
enum MockType {
|
enum MockType {
|
||||||
case loadUser
|
case loadUser(isOwn: Bool)
|
||||||
case loadedUser
|
case loadedUser(isOwn: Bool)
|
||||||
|
case allUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
static func mock(type: MockType) -> PillContext {
|
static func mock(type: MockType) -> PillContext {
|
||||||
|
let testID = "@test:test.com"
|
||||||
let pillType: PillType
|
let pillType: PillType
|
||||||
switch type {
|
switch type {
|
||||||
case .loadUser:
|
case .loadUser(let isOwn):
|
||||||
pillType = .user(userID: "@test:test.com")
|
pillType = .user(userID: testID)
|
||||||
let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||||
|
viewModel.viewState = PillViewState(contentID: testID, isOwnMention: isOwn, name: nil, displayText: testID, avatarURL: nil)
|
||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(for: .seconds(2))
|
try? await Task.sleep(for: .seconds(2))
|
||||||
viewModel.state = .loaded(contentID: "@test:test.com", name: "Test Longer Display Text", avatarURL: URL.documentsDirectory)
|
viewModel.viewState = PillViewState(contentID: "@test:test.com", isOwnMention: isOwn, name: nil, displayText: "Test Long Display Text", avatarURL: URL.documentsDirectory)
|
||||||
}
|
}
|
||||||
return viewModel
|
return viewModel
|
||||||
case .loadedUser:
|
case .loadedUser(let isOwn):
|
||||||
pillType = .user(userID: "@test:test.com")
|
pillType = .user(userID: "@test:test.com")
|
||||||
let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
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)
|
viewModel.viewState = PillViewState(contentID: "@test:test.com", isOwnMention: isOwn, name: nil, displayText: "Very Very Long Test Display Text", avatarURL: URL.documentsDirectory)
|
||||||
return viewModel
|
return viewModel
|
||||||
|
case .allUsers:
|
||||||
|
pillType = .allUsers
|
||||||
|
return PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import UIKit
|
|||||||
enum PillType: Codable {
|
enum PillType: Codable {
|
||||||
/// A pill that mentions a user
|
/// A pill that mentions a user
|
||||||
case user(userID: String)
|
case user(userID: String)
|
||||||
|
/// A pill that mentions all users in a room
|
||||||
|
case allUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PillTextAttachmentData {
|
struct PillTextAttachmentData {
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ import SwiftUI
|
|||||||
|
|
||||||
struct PillView: View {
|
struct PillView: View {
|
||||||
let imageProvider: ImageProviderProtocol?
|
let imageProvider: ImageProviderProtocol?
|
||||||
@ObservedObject var viewModel: PillContext
|
@ObservedObject var context: PillContext
|
||||||
/// callback triggerd by changes in the display text
|
/// callback triggerd by changes in the display text
|
||||||
let didChangeText: () -> Void
|
let didChangeText: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
LoadableAvatarImage(url: viewModel.url, name: viewModel.name, contentID: viewModel.contentID, avatarSize: .custom(24), imageProvider: imageProvider)
|
LoadableAvatarImage(url: context.viewState.avatarURL, name: context.viewState.name, contentID: context.viewState.contentID, avatarSize: .custom(24), imageProvider: imageProvider)
|
||||||
Text(viewModel.displayText)
|
Text(context.viewState.displayText)
|
||||||
.font(.compound.bodyLGSemibold)
|
.font(.compound.bodyLGSemibold)
|
||||||
.foregroundColor(.compound.textOnSolidPrimary)
|
.foregroundColor(.compound.textOnSolidPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@@ -33,9 +33,9 @@ struct PillView: View {
|
|||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
// for now design has defined no color so we will just use gray
|
// for now design has defined no color so we will just use gray
|
||||||
.background(Capsule().foregroundColor(.gray))
|
.background(Capsule().foregroundColor(context.viewState.isOwnMention ? .compound.bgCriticalPrimary : .gray))
|
||||||
.frame(maxWidth: 235)
|
.frame(maxWidth: 235)
|
||||||
.onChange(of: viewModel.displayText) { _ in
|
.onChange(of: context.viewState.displayText) { _ in
|
||||||
didChangeText()
|
didChangeText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,16 +44,21 @@ struct PillView: View {
|
|||||||
struct PillView_Previews: PreviewProvider, TestablePreview {
|
struct PillView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let mockMediaProvider = MockMediaProvider()
|
static let mockMediaProvider = MockMediaProvider()
|
||||||
|
|
||||||
static var loading: some View {
|
|
||||||
PillView(imageProvider: mockMediaProvider,
|
|
||||||
viewModel: PillContext.mock(type: .loadUser)) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
loading
|
PillView(imageProvider: mockMediaProvider,
|
||||||
|
context: PillContext.mock(type: .loadUser(isOwn: false))) { }
|
||||||
.previewDisplayName("Loading")
|
.previewDisplayName("Loading")
|
||||||
PillView(imageProvider: mockMediaProvider,
|
PillView(imageProvider: mockMediaProvider,
|
||||||
viewModel: PillContext.mock(type: .loadedUser)) { }
|
context: PillContext.mock(type: .loadUser(isOwn: true))) { }
|
||||||
|
.previewDisplayName("Loading Own")
|
||||||
|
PillView(imageProvider: mockMediaProvider,
|
||||||
|
context: PillContext.mock(type: .loadedUser(isOwn: false))) { }
|
||||||
.previewDisplayName("Loaded Long")
|
.previewDisplayName("Loaded Long")
|
||||||
|
PillView(imageProvider: mockMediaProvider,
|
||||||
|
context: PillContext.mock(type: .loadedUser(isOwn: true))) { }
|
||||||
|
.previewDisplayName("Loaded Long Own")
|
||||||
|
PillView(imageProvider: mockMediaProvider,
|
||||||
|
context: PillContext.mock(type: .allUsers)) { }
|
||||||
|
.previewDisplayName("All Users")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var timelineViewState = TimelineViewState() // check the doc before changing this
|
var timelineViewState = TimelineViewState() // check the doc before changing this
|
||||||
var swiftUITimelineEnabled = false
|
var swiftUITimelineEnabled = false
|
||||||
var longPressDisabledItemID: TimelineItemIdentifier?
|
var longPressDisabledItemID: TimelineItemIdentifier?
|
||||||
|
var ownUserID: String
|
||||||
|
|
||||||
var bindings: RoomScreenViewStateBindings
|
var bindings: RoomScreenViewStateBindings
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
timelineStyle: appSettings.timelineStyle,
|
timelineStyle: appSettings.timelineStyle,
|
||||||
readReceiptsEnabled: appSettings.readReceiptsEnabled,
|
readReceiptsEnabled: appSettings.readReceiptsEnabled,
|
||||||
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
|
||||||
|
ownUserID: roomProxy.ownUserID,
|
||||||
bindings: .init(reactionsCollapsed: [:])),
|
bindings: .init(reactionsCollapsed: [:])),
|
||||||
imageProvider: mediaProvider)
|
imageProvider: mediaProvider)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct NSEMentionBuilder: MentionBuilderProtocol {
|
struct NSEMentionBuilder: MentionBuilderProtocol {
|
||||||
|
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { }
|
||||||
|
|
||||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) {
|
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) {
|
||||||
attributedString.addAttributes([.MatrixUserID: userID], range: range)
|
attributedString.addAttributes([.MatrixUserID: userID], range: range)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,9 +418,59 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||||
XCTAssert(attributedStringFromPlain?.attachment.isNil == false)
|
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAllUsersMentionAttachment() {
|
||||||
|
let string = "@room"
|
||||||
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||||
|
checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 1)
|
||||||
|
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||||
|
checkAttachment(attributedString: attributedStringFromPlain, expectedRuns: 1)
|
||||||
|
|
||||||
|
let string2 = "Hello @room"
|
||||||
|
let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
|
||||||
|
checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 2)
|
||||||
|
let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2)
|
||||||
|
checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 2)
|
||||||
|
|
||||||
|
let string3 = "Hello @room how are you doing?"
|
||||||
|
let attributedStringFromHTML3 = attributedStringBuilder.fromHTML(string3)
|
||||||
|
checkAttachment(attributedString: attributedStringFromHTML3, expectedRuns: 3)
|
||||||
|
let attributedStringFromPlain3 = attributedStringBuilder.fromPlain(string3)
|
||||||
|
checkAttachment(attributedString: attributedStringFromPlain3, expectedRuns: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLinksHavePriorityOverAllUserMention() {
|
||||||
|
let string = "https://test@room.org"
|
||||||
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||||
|
checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: string, expectedRuns: 1)
|
||||||
|
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||||
|
checkLinkIn(attributedString: attributedStringFromPlain, expectedLink: string, expectedRuns: 1)
|
||||||
|
|
||||||
|
let string2 = "https://matrix.to/#/@roomusername:matrix.org"
|
||||||
|
let attributedStringFromHTML2 = attributedStringBuilder.fromHTML(string2)
|
||||||
|
checkLinkIn(attributedString: attributedStringFromHTML2, expectedLink: string2, expectedRuns: 1)
|
||||||
|
checkAttachment(attributedString: attributedStringFromHTML2, expectedRuns: 1)
|
||||||
|
let attributedStringFromPlain2 = attributedStringBuilder.fromPlain(string2)
|
||||||
|
checkLinkIn(attributedString: attributedStringFromPlain2, expectedLink: string2, expectedRuns: 1)
|
||||||
|
checkAttachment(attributedString: attributedStringFromPlain2, expectedRuns: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLinksAreIgnoredInCode() {
|
||||||
|
let htmlString = "<pre><code>test https://matrix.org test</code></pre>"
|
||||||
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
|
||||||
|
XCTAssert(attributedStringFromHTML?.runs.count == 1)
|
||||||
|
XCTAssertNil(attributedStringFromHTML?.link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAllUsersIsIgnoredInCode() {
|
||||||
|
let htmlString = "<pre><code>test @room test</code></pre>"
|
||||||
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
|
||||||
|
XCTAssert(attributedStringFromHTML?.runs.count == 1)
|
||||||
|
XCTAssertNil(attributedStringFromHTML?.attachment)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
||||||
@@ -438,4 +488,19 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTFail("Couldn't find expected value.")
|
XCTFail("Couldn't find expected value.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func checkAttachment(attributedString: AttributedString?, expectedRuns: Int) {
|
||||||
|
guard let attributedString else {
|
||||||
|
XCTFail("Could not build the attributed string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(attributedString.runs.count, expectedRuns)
|
||||||
|
|
||||||
|
for run in attributedString.runs where run.attachment != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTFail("Couldn't find expected value.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,4 +64,13 @@ class MatrixEntityRegexTests: XCTestCase {
|
|||||||
XCTAssertFalse(MatrixEntityRegex.isMatrixEventIdentifier("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg:"))
|
XCTAssertFalse(MatrixEntityRegex.isMatrixEventIdentifier("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg:"))
|
||||||
XCTAssertFalse(MatrixEntityRegex.isMatrixEventIdentifier("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg?"))
|
XCTAssertFalse(MatrixEntityRegex.isMatrixEventIdentifier("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg?"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAllUsers() {
|
||||||
|
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room"))
|
||||||
|
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a@rooma"))
|
||||||
|
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a @room a"))
|
||||||
|
XCTAssertFalse(MatrixEntityRegex.containsMatrixAllUsers("a @roaom a"))
|
||||||
|
XCTAssertFalse(MatrixEntityRegex.containsMatrixAllUsers("@roaom"))
|
||||||
|
XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room\n"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// 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 XCTest
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class PillContextTests: XCTestCase {
|
||||||
|
func testUser() async throws {
|
||||||
|
let id = "@test:matrix.org"
|
||||||
|
let proxyMock = RoomProxyMock(with: .init(displayName: "Test"))
|
||||||
|
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||||
|
proxyMock.members = subject.asCurrentValuePublisher()
|
||||||
|
let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
roomProxy: proxyMock,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||||
|
|
||||||
|
XCTAssertFalse(context.viewState.isOwnMention)
|
||||||
|
XCTAssertNil(context.viewState.avatarURL)
|
||||||
|
XCTAssertNil(context.viewState.name)
|
||||||
|
XCTAssertEqual(context.viewState.contentID, id)
|
||||||
|
XCTAssertEqual(context.viewState.displayText, id)
|
||||||
|
|
||||||
|
let name = "Mr. Test"
|
||||||
|
let avatarURL = URL(string: "https://test.jpg")
|
||||||
|
subject.send([RoomMemberProxyMock(with: .init(userID: id, displayName: name, avatarURL: avatarURL, membership: .join))])
|
||||||
|
await Task.yield()
|
||||||
|
|
||||||
|
XCTAssertFalse(context.viewState.isOwnMention)
|
||||||
|
XCTAssertEqual(context.viewState.avatarURL, avatarURL)
|
||||||
|
XCTAssertEqual(context.viewState.name, name)
|
||||||
|
XCTAssertEqual(context.viewState.contentID, id)
|
||||||
|
XCTAssertEqual(context.viewState.displayText, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOwnUser() async throws {
|
||||||
|
let id = "@test:matrix.org"
|
||||||
|
let proxyMock = RoomProxyMock(with: .init(displayName: "Test", ownUserID: id))
|
||||||
|
let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
||||||
|
proxyMock.members = subject.asCurrentValuePublisher()
|
||||||
|
let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
roomProxy: proxyMock,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
|
||||||
|
|
||||||
|
XCTAssertTrue(context.viewState.isOwnMention)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAllUsers() async throws {
|
||||||
|
let avatarURL = URL(string: "https://matrix.jpg")
|
||||||
|
let id = "test_room"
|
||||||
|
let displayName = "Test"
|
||||||
|
let proxyMock = RoomProxyMock(with: .init(id: id, displayName: displayName, avatarURL: avatarURL))
|
||||||
|
let mockController = MockRoomTimelineController()
|
||||||
|
mockController.roomProxy = proxyMock
|
||||||
|
let mock = RoomScreenViewModel(timelineController: mockController,
|
||||||
|
mediaProvider: MockMediaProvider(),
|
||||||
|
roomProxy: proxyMock,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analytics: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||||
|
|
||||||
|
XCTAssertTrue(context.viewState.isOwnMention)
|
||||||
|
XCTAssertEqual(context.viewState.avatarURL, avatarURL)
|
||||||
|
XCTAssertEqual(context.viewState.name, displayName)
|
||||||
|
XCTAssertEqual(context.viewState.contentID, id)
|
||||||
|
XCTAssertEqual(context.viewState.displayText, "@room")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:424b4e9eb2c8a83401fec9d8c8c737c825a622e025c443563c4a23a17976ab1d
|
||||||
|
size 62173
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c3cc84247a769c93950919a515079d0ff4a20293d9874df0905bcdf4264b78eb
|
||||||
|
size 66498
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:91680c015ee5ecbadadb799ff90f99a0112f00b7a95460d53bcd2d8f18177a1c
|
||||||
|
size 64987
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@room mention pill, and own mentions are red.
|
||||||
Reference in New Issue
Block a user