mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Implement link previews for text messages using Apple's LinkPreview framework (#4520)
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
|
||||
1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProvider.swift; sourceTree = "<group>"; };
|
||||
1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = "<group>"; };
|
||||
1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = "<group>"; };
|
||||
1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
@@ -2030,6 +2034,7 @@
|
||||
68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = "<group>"; };
|
||||
682BC7BAF0EFEF512A8C5140 /* AuthenticationStartScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenBackgroundImage.swift; sourceTree = "<group>"; };
|
||||
6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = "<group>"; };
|
||||
6885D1340AA9C76063E26868 /* LinkMetadataProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
||||
69A05E85E4872C3221C5C287 /* RemotePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreference.swift; sourceTree = "<group>"; };
|
||||
69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = "<group>"; };
|
||||
@@ -2294,6 +2299,7 @@
|
||||
9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
|
||||
9C6624240FFD32B7F0834229 /* IdentityConfirmedScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||
9C6B6FFCE5B28AA03DD46F46 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
DCF22D1E268F1A36EC3431E4 /* LinkMetadata */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */,
|
||||
6885D1340AA9C76063E26868 /* LinkMetadataProviderProtocol.swift */,
|
||||
);
|
||||
path = LinkMetadata;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
@@ -689,6 +689,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
elementCallService: elementCallService,
|
||||
timelineControllerFactory: TimelineControllerFactory(),
|
||||
emojiProvider: EmojiProvider(appSettings: appSettings),
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
appMediator: appMediator,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+3
@@ -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,
|
||||
|
||||
+1
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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()))
|
||||
}()
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ struct RoomScreenCoordinatorParameters {
|
||||
let timelineController: TimelineControllerProtocol
|
||||
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let linkMetadataProvider: LinkMetadataProviderProtocol
|
||||
let completionSuggestionService: CompletionSuggestionServiceProtocol
|
||||
let ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
@@ -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 { }
|
||||
|
||||
+9
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
@@ -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()))
|
||||
}()
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
+1
@@ -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 {
|
||||
|
||||
+60
-9
@@ -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<URL, LinkMetadataProviderItem>
|
||||
|
||||
init(timelineItem: TextRoomTimelineItem, linkMetadata: OrderedDictionary<URL, LinkMetadataProviderItem> = [:]) {
|
||||
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 <a href=\"https://www.apple.com\">Apple's website</a> and <a href=\"https://github.com\">GitHub</a>!",
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<LinkMetadataProviderItem, Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LinkMetadataProviderItem, Error>
|
||||
}
|
||||
+25
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<URL, LinkMetadataProviderItem> {
|
||||
var linkMetadata = OrderedDictionary<URL, LinkMetadataProviderItem>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66983f10c29a66777044a4fb6047c2d83e188b017f43f9690fd6b3b235cef23e
|
||||
size 18340
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66983f10c29a66777044a4fb6047c2d83e188b017f43f9690fd6b3b235cef23e
|
||||
size 18340
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0971b1ca77cc049ee26de3698fc6345c59bd6137f153a5e7d232141712c3e70b
|
||||
size 11563
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0971b1ca77cc049ee26de3698fc6345c59bd6137f153a5e7d232141712c3e70b
|
||||
size 11563
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc7e4b984e7d0a9d9f98f7d57713a5c79fd58449b958cd7b6e73b90617e0672c
|
||||
size 168251
|
||||
oid sha256:1cb256b45c7615e1212fe94481166cb448657d5ccf4e88ed7adb459df16c5cca
|
||||
size 190897
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c7ddc1ca335f9ba8cb831c4f89830e28d0f2f0cb10d1af760699978738e8cb83
|
||||
size 174116
|
||||
oid sha256:5684e19073baf63e7318b51f33268c6aff3f0f633f31dfde42487f1647a3a7e1
|
||||
size 197880
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e934278b5e887273313127de4b8975fff58ceb5c3e2cdd77b0ecede0fd8969ee
|
||||
size 127477
|
||||
oid sha256:71b151c85b92b4ff7333c856e400bd8e1a6c2a4805c854f42dba201f131ef4ac
|
||||
size 176189
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:03ad377e66461582b568d9963aa075dc02ca1a091158564ddda26f793831ba16
|
||||
size 131449
|
||||
oid sha256:779226c1c239b0502c1506fc7802d6cecf5a1747418ffcf3f7fcc1f269da578a
|
||||
size 181688
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:445275865d4a492749e691e8c9314ab83d76d5f8ac9e4f1772b581e27b7421e3
|
||||
size 169498
|
||||
oid sha256:cae991fb50c5a4ad7890089db3ea0ef2492b31ddd1704dcbc637cb8f646de71f
|
||||
size 192439
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e900018eef7e7fa35b766f6c5b06d99b547d360013cf0ff3f28f549ffc84c95b
|
||||
size 175241
|
||||
oid sha256:de0bc985887c1cf08570651ccddb3c3e0b55add43370f3da0fbd3bdc3f644148
|
||||
size 198851
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5b41bb83e83efc9236fec34bd546d21ca42a1652c949ceac870cc1aacf7564ba
|
||||
size 126890
|
||||
oid sha256:3975d3376dda88953962e39df165fdda332551b12c03834972a5d7c516821ccd
|
||||
size 176275
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f064f55b5fc411c9a6e10c82e75d752d5cb79ff751923d92e28fb7ca3691a38
|
||||
size 130454
|
||||
oid sha256:5450a64edb25f7a1af73545031b16678f0fee8e77d1b55fe257ee7f8fe4dede6
|
||||
size 181144
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user