diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 525ebb1a7..8919f9e8b 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -104,6 +104,7 @@ 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */; }; 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; }; 10D60D287025B71F4743A425 /* RoomDirectorySearchProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */; }; + 112C6F1C493B3F26AB22716A /* LinkMetadataProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6885D1340AA9C76063E26868 /* LinkMetadataProviderProtocol.swift */; }; 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; }; 114A8280D83146A26639C2A7 /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA2397174D0DC3918A7A8A7B /* AnalyticsConfiguration.swift */; }; 119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */; }; @@ -518,6 +519,7 @@ 5DB4334CBBA142376FF5FFEC /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; }; + 5E597B9959BDAE7A67DBD5B2 /* LinkMetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */; }; 5EB116B58533C9A0EBA22717 /* ThreadTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC2E7CEE6E9314FD69FE0ED9 /* ThreadTimelineScreen.swift */; }; 5EDBDE802761B5ECB54E6787 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711E5996016ABD6EAAEB58A /* LogLevel.swift */; }; 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; @@ -744,6 +746,7 @@ 8658F5034EAD7357CE7F9AC7 /* MatrixUserShareLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */; }; 865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; }; + 866FA35E7A2339EF8B6D91CA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6B6FFCE5B28AA03DD46F46 /* LinkPreviewView.swift */; }; 86769B62BAE17601B3AE1B60 /* TimelineMediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */; }; 8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 86DFA58FBBEB0AF671D2A1E1 /* HomeScreenKnockedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A103580EBA06155B1343EF16 /* HomeScreenKnockedCell.swift */; }; @@ -1637,6 +1640,7 @@ 1BA8082E26C77A2C587B34B3 /* MockTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTimelineController.swift; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; + 1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProvider.swift; sourceTree = ""; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = ""; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = ""; }; 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = ""; }; @@ -2030,6 +2034,7 @@ 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 682BC7BAF0EFEF512A8C5140 /* AuthenticationStartScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenBackgroundImage.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; + 6885D1340AA9C76063E26868 /* LinkMetadataProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProviderProtocol.swift; sourceTree = ""; }; 693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; 69A05E85E4872C3221C5C287 /* RemotePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreference.swift; sourceTree = ""; }; 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = ""; }; @@ -2294,6 +2299,7 @@ 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; 9C6624240FFD32B7F0834229 /* IdentityConfirmedScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenViewModel.swift; sourceTree = ""; }; 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCoordinatorTests.swift; sourceTree = ""; }; + 9C6B6FFCE5B28AA03DD46F46 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = ""; }; 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = ""; }; @@ -3024,6 +3030,7 @@ 92E99C57D7F92ED16F73282C /* ElementCall */, 39557ADF21345E18F3865B9E /* Emojis */, CA555F7C7CA382ACACF0D82B /* Keychain */, + DCF22D1E268F1A36EC3431E4 /* LinkMetadata */, 79E560F5113ED25D172E550C /* Media */, 6709362D60732DED2069AE0F /* MediaPlayer */, 6DE13A7AE6587B079F4049D7 /* Notification */, @@ -3544,6 +3551,7 @@ FEC4B431B0117BDEE697DB4A /* ComposerDisabledView.swift */, E2776E63E02719B20758EB78 /* EditRoomAddressListRow.swift */, 8F4F0AB250EFA7B71FB2BDB2 /* HorizontalHighlightGradient.swift */, + 9C6B6FFCE5B28AA03DD46F46 /* LinkPreviewView.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, C352359663A0E52BA20761EE /* LoadableImage.swift */, FFECCE59967018204876D0A5 /* LocationMarkerView.swift */, @@ -6038,6 +6046,15 @@ path = View; sourceTree = ""; }; + DCF22D1E268F1A36EC3431E4 /* LinkMetadata */ = { + isa = PBXGroup; + children = ( + 1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */, + 6885D1340AA9C76063E26868 /* LinkMetadataProviderProtocol.swift */, + ); + path = LinkMetadata; + sourceTree = ""; + }; DD96B3F20F354494DECBC4F7 /* View */ = { isa = PBXGroup; children = ( @@ -7802,6 +7819,9 @@ F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */, BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */, A9D349478F7D4A2B1E40CEF9 /* LegalInformationScreenViewModelProtocol.swift in Sources */, + 5E597B9959BDAE7A67DBD5B2 /* LinkMetadataProvider.swift in Sources */, + 112C6F1C493B3F26AB22716A /* LinkMetadataProviderProtocol.swift in Sources */, + 866FA35E7A2339EF8B6D91CA /* LinkPreviewView.swift in Sources */, 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */, D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */, 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 5eeb7c0eb..8e02fab43 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -689,6 +689,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg elementCallService: elementCallService, timelineControllerFactory: TimelineControllerFactory(), emojiProvider: EmojiProvider(appSettings: appSettings), + linkMetadataProvider: LinkMetadataProvider(), appMediator: appMediator, appSettings: appSettings, appHooks: appHooks, diff --git a/ElementX/Sources/Application/FlowCoordinatorProtocol.swift b/ElementX/Sources/Application/FlowCoordinatorProtocol.swift index e460813f6..233255128 100644 --- a/ElementX/Sources/Application/FlowCoordinatorProtocol.swift +++ b/ElementX/Sources/Application/FlowCoordinatorProtocol.swift @@ -25,6 +25,7 @@ struct CommonFlowParameters { let elementCallService: ElementCallServiceProtocol let timelineControllerFactory: TimelineControllerFactoryProtocol let emojiProvider: EmojiProviderProtocol + let linkMetadataProvider: LinkMetadataProviderProtocol let appMediator: AppMediatorProtocol let appSettings: AppSettings let appHooks: AppHooks diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index e703c8b63..1ee1523a8 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -62,6 +62,7 @@ final class AppSettings { case spacesEnabled case developerOptionsEnabled case nextGenHTMLParserEnabled + case linkPreviewsEnabled // Doug's tweaks 🔧 case hideUnreadMessagesBadge @@ -385,6 +386,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.nextGenHTMLParserEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) var nextGenHTMLParserEnabled + @UserPreference(key: UserDefaultsKeys.linkPreviewsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var linkPreviewsEnabled + @UserPreference(key: UserDefaultsKeys.developerOptionsEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) var developerOptionsEnabled } diff --git a/ElementX/Sources/FlowCoordinators/MediaEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/MediaEventsTimelineFlowCoordinator.swift index 7374f4fc5..7d9920f04 100644 --- a/ElementX/Sources/FlowCoordinators/MediaEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/MediaEventsTimelineFlowCoordinator.swift @@ -83,6 +83,7 @@ class MediaEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { appSettings: flowParameters.appSettings, analytics: flowParameters.analytics, emojiProvider: flowParameters.emojiProvider, + linkMetadataProvider: flowParameters.linkMetadataProvider, userIndicatorController: flowParameters.userIndicatorController, timelineControllerFactory: flowParameters.timelineControllerFactory) diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index 4354bbd05..aff2cdcb0 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -69,6 +69,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { appSettings: flowParameters.appSettings, analytics: flowParameters.analytics, emojiProvider: flowParameters.emojiProvider, + linkMetadataProvider: flowParameters.linkMetadataProvider, timelineControllerFactory: flowParameters.timelineControllerFactory, userIndicatorController: flowParameters.userIndicatorController)) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 43ab18e55..36eee2d44 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -543,6 +543,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProvider(), emojiProvider: flowParameters.emojiProvider, + linkMetadataProvider: flowParameters.linkMetadataProvider, completionSuggestionService: completionSuggestionService, ongoingCallRoomIDPublisher: flowParameters.ongoingCallRoomIDPublisher, appMediator: flowParameters.appMediator, @@ -639,6 +640,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProvider(), emojiProvider: flowParameters.emojiProvider, + linkMetadataProvider: flowParameters.linkMetadataProvider, completionSuggestionService: completionSuggestionService, appMediator: flowParameters.appMediator, appSettings: flowParameters.appSettings, diff --git a/ElementX/Sources/Other/SwiftUI/Views/LinkPreviewView.swift b/ElementX/Sources/Other/SwiftUI/Views/LinkPreviewView.swift new file mode 100644 index 000000000..04b05b2ee --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/LinkPreviewView.swift @@ -0,0 +1,58 @@ +// +// Copyright 2023, 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import LinkPresentation +import SwiftUI + +struct LinkPreviewView: UIViewRepresentable { + let url: URL + let metadata: LPLinkMetadata? + + func makeUIView(context: Context) -> LPLinkView { + let preview = LPLinkView(url: url) + + if let metadata { + preview.metadata = metadata + } + + return preview + } + + func updateUIView(_ uiView: LPLinkView, context: Context) { + if let metadata { + uiView.metadata = metadata + } + } + + func sizeThatFits(_ proposal: ProposedViewSize, uiView: LPLinkView, context: Context) -> CGSize? { + let width = proposal.width ?? uiView.intrinsicContentSize.width + let bestFit = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + + return CGSize(width: width, height: bestFit.height) + } +} + +struct LinkPreviewView_Previews: PreviewProvider { + static var previews: some View { + if let url = URL(string: "https://www.lunch.club") { + LinkPreviewView(url: url, metadata: appleMetadata) + .previewLayout(.sizeThatFits) + } + } + + private static var appleMetadata: LPLinkMetadata { + let metadata = LPLinkMetadata() + metadata.title = "Lunch club" + + if let url = Bundle.main.url(forResource: "preview_avatar_room", withExtension: "jpg") { + metadata.url = url + metadata.imageProvider = NSItemProvider(contentsOf: url) + } + + return metadata + } +} diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenCoordinator.swift index b8becdae1..6a9d9898a 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenCoordinator.swift @@ -18,6 +18,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters { let appSettings: AppSettings let analytics: AnalyticsService let emojiProvider: EmojiProviderProtocol + let linkMetadataProvider: LinkMetadataProviderProtocol let userIndicatorController: UserIndicatorControllerProtocol let timelineControllerFactory: TimelineControllerFactoryProtocol } @@ -49,6 +50,7 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol { appSettings: parameters.appSettings, analyticsService: parameters.analytics, emojiProvider: parameters.emojiProvider, + linkMetadataProvider: parameters.linkMetadataProvider, timelineControllerFactory: parameters.timelineControllerFactory) let filesTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, @@ -60,6 +62,7 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol { appSettings: parameters.appSettings, analyticsService: parameters.analytics, emojiProvider: parameters.emojiProvider, + linkMetadataProvider: parameters.linkMetadataProvider, timelineControllerFactory: parameters.timelineControllerFactory) viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel, diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift index 7bfce42a0..e752f943a 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift @@ -306,6 +306,7 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) } } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index ff5f4465e..1447d6912 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -17,6 +17,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters { let appSettings: AppSettings let analytics: AnalyticsService let emojiProvider: EmojiProviderProtocol + let linkMetadataProvider: LinkMetadataProviderProtocol let timelineControllerFactory: TimelineControllerFactoryProtocol let userIndicatorController: UserIndicatorControllerProtocol } @@ -54,6 +55,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { appSettings: parameters.appSettings, analyticsService: parameters.analytics, emojiProvider: parameters.emojiProvider, + linkMetadataProvider: parameters.linkMetadataProvider, timelineControllerFactory: parameters.timelineControllerFactory) } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 54df3cee5..502474486 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -78,6 +78,7 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 5bccef75c..012222610 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -19,6 +19,7 @@ struct RoomScreenCoordinatorParameters { let timelineController: TimelineControllerProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol let emojiProvider: EmojiProviderProtocol + let linkMetadataProvider: LinkMetadataProviderProtocol let completionSuggestionService: CompletionSuggestionServiceProtocol let ongoingCallRoomIDPublisher: CurrentValuePublisher let appMediator: AppMediatorProtocol @@ -86,6 +87,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { appSettings: parameters.appSettings, analyticsService: parameters.analytics, emojiProvider: parameters.emojiProvider, + linkMetadataProvider: parameters.linkMetadataProvider, timelineControllerFactory: parameters.timelineControllerFactory) let wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 9c80eba9e..bcaf90125 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -264,6 +264,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) return .init(room: roomViewModel, timeline: timelineViewModel) diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 214de97bb..64047363a 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -54,6 +54,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var knockingEnabled: Bool { get set } var nextGenHTMLParserEnabled: Bool { get set } + var linkPreviewsEnabled: 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 d5c2c2fd8..469fac597 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -63,6 +63,15 @@ struct DeveloperOptionsScreen: View { Text("Low priority filter") } } + + Section("Timeline") { + Toggle(isOn: $context.linkPreviewsEnabled) { + Text("Link previews") + Text("Follows the timeline media visibility settings.") + Text("Can leak the device IP address when loading link metadata.") + .foregroundStyle(.compound.textCriticalPrimary) + } + } Section("Join rules") { Toggle(isOn: $context.knockingEnabled) { diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift index 9a0525e5e..f8b8c960c 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift @@ -16,6 +16,7 @@ struct ThreadTimelineScreenCoordinatorParameters { let timelineController: TimelineControllerProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol let emojiProvider: EmojiProviderProtocol + let linkMetadataProvider: LinkMetadataProviderProtocol let completionSuggestionService: CompletionSuggestionServiceProtocol let appMediator: AppMediatorProtocol let appSettings: AppSettings @@ -65,6 +66,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { appSettings: parameters.appSettings, analyticsService: parameters.analytics, emojiProvider: parameters.emojiProvider, + linkMetadataProvider: parameters.linkMetadataProvider, timelineControllerFactory: parameters.timelineControllerFactory) let wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index c074a419e..0e7ed5c2f 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -41,6 +41,7 @@ class TimelineInteractionHandler { private let appSettings: AppSettings private let analyticsService: AnalyticsService private let emojiProvider: EmojiProviderProtocol + private let linkMetadataProvider: LinkMetadataProviderProtocol private let timelineControllerFactory: TimelineControllerFactoryProtocol private let pollInteractionHandler: PollInteractionHandlerProtocol @@ -67,6 +68,7 @@ class TimelineInteractionHandler { appSettings: AppSettings, analyticsService: AnalyticsService, emojiProvider: EmojiProviderProtocol, + linkMetadataProvider: LinkMetadataProviderProtocol, timelineControllerFactory: TimelineControllerFactoryProtocol) { self.roomProxy = roomProxy self.timelineController = timelineController @@ -78,6 +80,7 @@ class TimelineInteractionHandler { self.appSettings = appSettings self.analyticsService = analyticsService self.emojiProvider = emojiProvider + self.linkMetadataProvider = linkMetadataProvider self.timelineControllerFactory = timelineControllerFactory pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, @@ -590,6 +593,7 @@ class TimelineInteractionHandler { appSettings: appSettings, analyticsService: analyticsService, emojiProvider: emojiProvider, + linkMetadataProvider: linkMetadataProvider, timelineControllerFactory: timelineControllerFactory) return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel)) diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index c2944785f..6b53bc6fc 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -107,9 +107,12 @@ struct TimelineViewState: BindableState { var canCurrentUserPin = false var canCurrentUserKick = false var canCurrentUserBan = false + + var hideTimelineMedia: Bool + var isViewSourceEnabled: Bool var areThreadsEnabled: Bool - var hideTimelineMedia: Bool + var linkPreviewsEnabled: Bool let hasPredecessor: Bool @@ -131,6 +134,8 @@ struct TimelineViewState: BindableState { var emojiProvider: EmojiProviderProtocol + var linkMetadataProvider: LinkMetadataProviderProtocol? + var mapTilerConfiguration: MapTilerConfiguration var bindings: TimelineViewStateBindings diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 8d59f2161..dcfe08ca2 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -56,6 +56,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { appSettings: AppSettings, analyticsService: AnalyticsService, emojiProvider: EmojiProviderProtocol, + linkMetadataProvider: LinkMetadataProviderProtocol, timelineControllerFactory: TimelineControllerFactoryProtocol) { self.roomProxy = roomProxy self.timelineController = timelineController @@ -80,6 +81,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { appSettings: appSettings, analyticsService: analyticsService, emojiProvider: emojiProvider, + linkMetadataProvider: linkMetadataProvider, timelineControllerFactory: timelineControllerFactory) let hideTimelineMedia = switch userSession.clientProxy.timelineMediaVisibilityPublisher.value { @@ -95,12 +97,14 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { isDirectOneToOneRoom: roomProxy.isDirectOneToOneRoom, timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), ownUserID: roomProxy.ownUserID, + hideTimelineMedia: hideTimelineMedia, isViewSourceEnabled: appSettings.viewSourceEnabled, areThreadsEnabled: appSettings.threadsEnabled, - hideTimelineMedia: hideTimelineMedia, + linkPreviewsEnabled: appSettings.linkPreviewsEnabled, hasPredecessor: roomProxy.predecessorRoom != nil, pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs, emojiProvider: emojiProvider, + linkMetadataProvider: hideTimelineMedia ? nil : linkMetadataProvider, mapTilerConfiguration: appSettings.mapTilerConfiguration, bindings: .init(reactionsCollapsed: [:])), mediaProvider: userSession.mediaProvider) @@ -1007,6 +1011,7 @@ extension TimelineViewModel { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) } } diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift index 761d2051a..4ca36976f 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -52,6 +52,7 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) return mock }() diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index b34667488..4f53ff394 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -341,6 +341,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index 1f12b599a..ec3e41a8e 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -87,6 +87,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index b640951bd..122c7a2c6 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -97,6 +97,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) static var previews: some View { diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift index b4ce33668..2c525ddda 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift @@ -8,19 +8,62 @@ import Foundation import SwiftUI +import OrderedCollections + struct TextRoomTimelineView: View, TextBasedRoomTimelineViewProtocol { + static let maxLinkPreviewsToRender = 2 + + @Environment(\.timelineContext) private var context let timelineItem: TextRoomTimelineItem + @State private var linkMetadata: OrderedDictionary + + init(timelineItem: TextRoomTimelineItem, linkMetadata: OrderedDictionary = [:]) { + self.timelineItem = timelineItem + self.linkMetadata = linkMetadata + } + var body: some View { TimelineStyler(timelineItem: timelineItem) { - if let attributedString = timelineItem.content.formattedBody { - FormattedBodyText(attributedString: attributedString, - additionalWhitespacesCount: timelineItem.additionalWhitespaces(), - boostFontSize: timelineItem.shouldBoost) - } else { - FormattedBodyText(text: timelineItem.body, - additionalWhitespacesCount: timelineItem.additionalWhitespaces(), - boostFontSize: timelineItem.shouldBoost) + VStack(alignment: .leading, spacing: 8) { + if let attributedString = timelineItem.content.formattedBody { + FormattedBodyText(attributedString: attributedString, + additionalWhitespacesCount: timelineItem.additionalWhitespaces(), + boostFontSize: timelineItem.shouldBoost) + } else { + FormattedBodyText(text: timelineItem.body, + additionalWhitespacesCount: timelineItem.additionalWhitespaces(), + boostFontSize: timelineItem.shouldBoost) + } + + if context?.viewState.linkPreviewsEnabled ?? false { + VStack(spacing: 8) { + ForEach(linkMetadata.keys, id: \.absoluteString) { url in + let metadata = linkMetadata[url]?.metadata ?? context?.viewState.linkMetadataProvider?.metadataItems[url]?.metadata + LinkPreviewView(url: url, metadata: metadata) + } + } + .padding(.bottom, 16) + } + } + } + .task { await fetchLinkPreviews() } + } + + private func fetchLinkPreviews() async { + guard context?.viewState.linkPreviewsEnabled ?? false else { + return + } + + await withTaskGroup { taskGroup in + for url in timelineItem.links.prefix(Self.maxLinkPreviewsToRender) { + taskGroup.addTask { + if case let .success(metadata) = await context?.viewState.linkMetadataProvider?.fetchMetadataFor(url: url) { + await MainActor.run { + linkMetadata[url] = metadata + } + } + } } } } @@ -32,10 +75,12 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview { static var previews: some View { body.environmentObject(viewModel.context) .previewDisplayName("Bubble") + .previewLayout(.sizeThatFits) body .environmentObject(viewModel.context) .environment(\.layoutDirection, .rightToLeft) .previewDisplayName("Bubble RTL") + .previewLayout(.sizeThatFits) } static var body: some View { @@ -46,7 +91,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview { isOutgoing: false, senderId: "Bob")) - TextRoomTimelineView(timelineItem: itemWith(text: "Some other text", + TextRoomTimelineView(timelineItem: itemWith(text: "Check out this cool website: https://www.apple.com and also https://github.com for some great projects!", timestamp: .mock, isOutgoing: true, senderId: "Anne")) @@ -75,6 +120,12 @@ struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview { timestamp: .mock, isOutgoing: true, senderId: "Anne")) + + // HTML with links for testing + TextRoomTimelineView(timelineItem: itemWith(html: "Check out Apple's website and GitHub!", + timestamp: .mock, + isOutgoing: false, + senderId: "Bob")) } } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 956099e0b..376eee468 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -144,6 +144,7 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) static var previews: some View { diff --git a/ElementX/Sources/Services/LinkMetadata/LinkMetadataProvider.swift b/ElementX/Sources/Services/LinkMetadata/LinkMetadataProvider.swift new file mode 100644 index 000000000..fcd97ffc3 --- /dev/null +++ b/ElementX/Sources/Services/LinkMetadata/LinkMetadataProvider.swift @@ -0,0 +1,27 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import LinkPresentation + +class LinkMetadataProvider: LinkMetadataProviderProtocol { + private(set) var metadataItems = [URL: LinkMetadataProviderItem]() + + func fetchMetadataFor(url: URL) async -> Result { + if let item = metadataItems[url] { + return .success(item) + } + + do { + let metadata = try await LPMetadataProvider().startFetchingMetadata(for: url) + let item = LinkMetadataProviderItem(url: url, metadata: metadata) + metadataItems[url] = item + return .success(item) + } catch { + return .failure(error) + } + } +} diff --git a/ElementX/Sources/Services/LinkMetadata/LinkMetadataProviderProtocol.swift b/ElementX/Sources/Services/LinkMetadata/LinkMetadataProviderProtocol.swift new file mode 100644 index 000000000..2d03aaa26 --- /dev/null +++ b/ElementX/Sources/Services/LinkMetadata/LinkMetadataProviderProtocol.swift @@ -0,0 +1,20 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Foundation +import LinkPresentation + +struct LinkMetadataProviderItem { + let url: URL + let metadata: LPLinkMetadata? +} + +protocol LinkMetadataProviderProtocol { + var metadataItems: [URL: LinkMetadataProviderItem] { get } + + func fetchMetadataFor(url: URL) async -> Result +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift index b6dc99aad..596ed8704 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/TextRoomTimelineItem.swift @@ -5,6 +5,7 @@ // Please see LICENSE files in the repository root for full details. // +import Algorithms import UIKit struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable { @@ -28,4 +29,28 @@ struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable { var contentType: EventBasedMessageTimelineItemContentType { .text(content) } + + var links: [URL] { + guard let attributedString = content.formattedBody else { + return [] + } + + let links = attributedString.runs.compactMap { (run: AttributedString.Runs.Run) -> URL? in + if run.link == nil { + return nil + } + + guard run.elementX.eventOnRoomAlias == nil, + run.elementX.eventOnRoomID == nil, + run.elementX.roomAlias == nil, + run.elementX.roomID == nil, + run.elementX.userID == nil else { + return nil + } + + return run.link + } + + return Array(links.uniqued()) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift index ff3725955..ca6624a58 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift @@ -6,6 +6,8 @@ // import SwiftUI +import OrderedCollections + struct RoomTimelineItemView: View { @Environment(\.timelineContext) var context @ObservedObject var viewState: RoomTimelineItemViewState @@ -26,7 +28,7 @@ struct RoomTimelineItemView: View { @ViewBuilder private var timelineView: some View { switch viewState.type { case .text(let item): - TextRoomTimelineView(timelineItem: item) + TextRoomTimelineView(timelineItem: item, linkMetadata: linkMetadataForItem(item)) case .separator(let item): SeparatorRoomTimelineView(timelineItem: item) case .image(let item): @@ -64,13 +66,26 @@ struct RoomTimelineItemView: View { case .poll(let item): PollRoomTimelineView(timelineItem: item) case .voice(let item): - VoiceMessageRoomTimelineView(timelineItem: item, playerState: context?.viewState.audioPlayerStateProvider?(item.id) ?? AudioPlayerState(id: .timelineItemIdentifier(item.id), - title: L10n.commonVoiceMessage, - duration: 0)) + let playerState = context?.viewState.audioPlayerStateProvider?(item.id) ?? AudioPlayerState(id: .timelineItemIdentifier(item.id), + title: L10n.commonVoiceMessage, + duration: 0) + VoiceMessageRoomTimelineView(timelineItem: item, playerState: playerState) case .callInvite(let item): CallInviteRoomTimelineView(timelineItem: item) case .callNotification(let item): CallNotificationRoomTimelineView(timelineItem: item) } } + + private func linkMetadataForItem(_ item: TextRoomTimelineItem) -> OrderedDictionary { + var linkMetadata = OrderedDictionary() + for url in item.links.prefix(TextRoomTimelineView.maxLinkPreviewsToRender) { + if let item = context?.viewState.linkMetadataProvider?.metadataItems[url] { + linkMetadata[url] = item + } else { + linkMetadata[url] = LinkMetadataProviderItem(url: url, metadata: nil) + } + } + return linkMetadata + } } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 675e70839..f2d6ebc78 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -265,6 +265,7 @@ class MockScreen: Identifiable { timelineController: MockTimelineController(), mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -286,6 +287,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -307,6 +309,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -327,6 +330,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -350,6 +354,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -373,6 +378,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -395,6 +401,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -419,6 +426,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -442,6 +450,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -464,6 +473,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -500,6 +510,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -523,6 +534,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -546,6 +558,7 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaPlayerProvider: MediaPlayerProviderMock(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -594,6 +607,7 @@ class MockScreen: Identifiable { elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), emojiProvider: EmojiProvider(appSettings: appSettings), + linkMetadataProvider: LinkMetadataProvider(), appMediator: appMediator, appSettings: appSettings, appHooks: AppHooks(), @@ -762,6 +776,7 @@ class MockScreen: Identifiable { elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init(timelineController: timelineController)), emojiProvider: EmojiProvider(appSettings: appSettings), + linkMetadataProvider: LinkMetadataProvider(), appMediator: AppMediatorMock.default, appSettings: appSettings, appHooks: AppHooks(), diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPad-en-GB-0.png new file mode 100644 index 000000000..47f2ba219 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66983f10c29a66777044a4fb6047c2d83e188b017f43f9690fd6b3b235cef23e +size 18340 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPad-pseudo-0.png new file mode 100644 index 000000000..47f2ba219 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66983f10c29a66777044a4fb6047c2d83e188b017f43f9690fd6b3b235cef23e +size 18340 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..bd735dc39 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0971b1ca77cc049ee26de3698fc6345c59bd6137f153a5e7d232141712c3e70b +size 11563 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..bd735dc39 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/linkPreviewView.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0971b1ca77cc049ee26de3698fc6345c59bd6137f153a5e7d232141712c3e70b +size 11563 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-en-GB.png index e25a55214..aee4c0e1f 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc7e4b984e7d0a9d9f98f7d57713a5c79fd58449b958cd7b6e73b90617e0672c -size 168251 +oid sha256:1cb256b45c7615e1212fe94481166cb448657d5ccf4e88ed7adb459df16c5cca +size 190897 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-pseudo.png index 6897817ca..21af39147 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ddc1ca335f9ba8cb831c4f89830e28d0f2f0cb10d1af760699978738e8cb83 -size 174116 +oid sha256:5684e19073baf63e7318b51f33268c6aff3f0f633f31dfde42487f1647a3a7e1 +size 197880 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-en-GB.png index 93007c06e..1420182b4 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e934278b5e887273313127de4b8975fff58ceb5c3e2cdd77b0ecede0fd8969ee -size 127477 +oid sha256:71b151c85b92b4ff7333c856e400bd8e1a6c2a4805c854f42dba201f131ef4ac +size 176189 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-pseudo.png index 4f069ea0f..eab31a7dc 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-RTL-iPhone-16-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03ad377e66461582b568d9963aa075dc02ca1a091158564ddda26f793831ba16 -size 131449 +oid sha256:779226c1c239b0502c1506fc7802d6cecf5a1747418ffcf3f7fcc1f269da578a +size 181688 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-en-GB.png index e16ebbfbc..b6bd79811 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:445275865d4a492749e691e8c9314ab83d76d5f8ac9e4f1772b581e27b7421e3 -size 169498 +oid sha256:cae991fb50c5a4ad7890089db3ea0ef2492b31ddd1704dcbc637cb8f646de71f +size 192439 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-pseudo.png index f310dbb52..90476e444 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e900018eef7e7fa35b766f6c5b06d99b547d360013cf0ff3f28f549ffc84c95b -size 175241 +oid sha256:de0bc985887c1cf08570651ccddb3c3e0b55add43370f3da0fbd3bdc3f644148 +size 198851 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-en-GB.png index eece5bb42..6e502695a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b41bb83e83efc9236fec34bd546d21ca42a1652c949ceac870cc1aacf7564ba -size 126890 +oid sha256:3975d3376dda88953962e39df165fdda332551b12c03834972a5d7c516821ccd +size 176275 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-pseudo.png index 3f9d9e51a..e3928eff7 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/textRoomTimelineView.Bubble-iPhone-16-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f064f55b5fc411c9a6e10c82e75d752d5cb79ff751923d92e28fb7ca3691a38 -size 130454 +oid sha256:5450a64edb25f7a1af73545031b16678f0fee8e77d1b55fe257ee7f8fe4dede6 +size 181144 diff --git a/UnitTests/Sources/ChatsFlowCoordinatorTests.swift b/UnitTests/Sources/ChatsFlowCoordinatorTests.swift index 44207f009..559813e5e 100644 --- a/UnitTests/Sources/ChatsFlowCoordinatorTests.swift +++ b/UnitTests/Sources/ChatsFlowCoordinatorTests.swift @@ -38,6 +38,7 @@ class ChatsFlowCoordinatorTests: XCTestCase { elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: timelineControllerFactory, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index ed28741ca..994970086 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -26,6 +26,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) @@ -55,6 +56,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) @@ -77,6 +79,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) @@ -99,6 +102,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body))) @@ -120,6 +124,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomID("1"), font: .preferredFont(forTextStyle: .body))) @@ -145,6 +150,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body))) @@ -166,6 +172,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .roomAlias("#foundation-and-empire:matrix.org"), font: .preferredFont(forTextStyle: .body))) @@ -189,6 +196,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body))) @@ -210,6 +218,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomID("1")), font: .preferredFont(forTextStyle: .body))) @@ -235,6 +244,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body))) @@ -256,6 +266,7 @@ class PillContextTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .event(room: .roomAlias("#foundation-and-empire:matrix.org")), font: .preferredFont(forTextStyle: .body))) diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 30553e32f..36b70ad70 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -363,6 +363,7 @@ class RoomFlowCoordinatorTests: XCTestCase { elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: timelineControllerFactory, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(), diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 9148eb81e..36c2a2941 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -311,6 +311,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) return (viewModel, roomProxy, timelineProxy, timelineController) } @@ -336,6 +337,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let deferred = deferFulfillment(viewModel.context.$viewState) { value in @@ -359,6 +361,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferred = deferFulfillment(viewModel.context.$viewState) { value in @@ -394,6 +397,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferredState = deferFulfillment(viewModel.context.$viewState) { value in @@ -429,6 +433,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferredState = deferFulfillment(viewModel.context.$viewState) { value in @@ -470,6 +475,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs) @@ -497,6 +503,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferred = deferFulfillment(viewModel.context.$viewState) { value in @@ -534,6 +541,7 @@ class TimelineViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) } } diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 11cb10094..512d49f69 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -46,6 +46,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { elementCallService: ElementCallServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + linkMetadataProvider: LinkMetadataProvider(), appMediator: appMediator, appSettings: ServiceLocator.shared.settings, appHooks: AppHooks(),