mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add support for sharing URLs and text. (#3546)
* Add support for sharing URLs and text. * Fix a bug where a stored draft would overwrite the shared text. * Add tests.
This commit is contained in:
@@ -23,17 +23,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: appSettings)
|
||||
wysiwygViewModel = WysiwygComposerViewModel()
|
||||
completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init())
|
||||
draftServiceMock = ComposerDraftServiceMock()
|
||||
viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: completionSuggestionServiceMock,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: draftServiceMock)
|
||||
|
||||
viewModel.context.composerFormattingEnabled = true
|
||||
setUpViewModel()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
@@ -340,7 +330,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
return .success(nil)
|
||||
}
|
||||
|
||||
viewModel.loadDraft()
|
||||
await viewModel.loadDraft()
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertTrue(viewModel.state.composerEmpty)
|
||||
@@ -356,7 +346,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
htmlText: nil,
|
||||
draftType: .newMessage))
|
||||
}
|
||||
viewModel.loadDraft()
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
@@ -373,7 +363,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
htmlText: "<strong>Hello</strong> world!",
|
||||
draftType: .newMessage))
|
||||
}
|
||||
viewModel.loadDraft()
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertTrue(viewModel.context.composerFormattingEnabled)
|
||||
@@ -391,7 +381,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
htmlText: nil,
|
||||
draftType: .edit(eventID: "testID")))
|
||||
}
|
||||
viewModel.loadDraft()
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
@@ -424,7 +414,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
return .success(.init(details: loadedReply,
|
||||
isThreaded: true))
|
||||
}
|
||||
viewModel.loadDraft()
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [draftExpectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
@@ -464,7 +454,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
return .success(.init(details: loadedReply,
|
||||
isThreaded: true))
|
||||
}
|
||||
viewModel.loadDraft()
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [draftExpectation], timeout: 10)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
@@ -622,6 +612,45 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.process(viewAction: .sendMessage)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRestoreDoesntOverwriteInitialText() async {
|
||||
let sharedText = "Some shared text"
|
||||
let expectation = expectation(description: "Wait for draft to be restored")
|
||||
expectation.isInverted = true
|
||||
setUpViewModel(initialText: sharedText) {
|
||||
defer { expectation.fulfill() }
|
||||
return .success(.init(plainText: "Hello world!",
|
||||
htmlText: nil,
|
||||
draftType: .newMessage))
|
||||
}
|
||||
viewModel.context.composerFormattingEnabled = false
|
||||
await viewModel.loadDraft()
|
||||
|
||||
await fulfillment(of: [expectation], timeout: 1)
|
||||
XCTAssertFalse(viewModel.context.composerFormattingEnabled)
|
||||
XCTAssertEqual(viewModel.state.composerMode, .default)
|
||||
XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: sharedText))
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setUpViewModel(initialText: String? = nil, loadDraftClosure: (() async -> Result<ComposerDraftProxy?, ComposerDraftServiceError>)? = nil) {
|
||||
wysiwygViewModel = WysiwygComposerViewModel()
|
||||
completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init())
|
||||
draftServiceMock = ComposerDraftServiceMock()
|
||||
if let loadDraftClosure {
|
||||
draftServiceMock.loadDraftClosure = loadDraftClosure
|
||||
}
|
||||
|
||||
viewModel = ComposerToolbarViewModel(initialText: initialText,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: completionSuggestionServiceMock,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: draftServiceMock)
|
||||
viewModel.context.composerFormattingEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private extension MentionSuggestionItem {
|
||||
|
||||
@@ -218,7 +218,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareRoute() async throws {
|
||||
func testShareMediaRoute() async throws {
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
@@ -243,6 +243,31 @@ class RoomFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRoute() async throws {
|
||||
await setupRoomFlowCoordinator()
|
||||
|
||||
try await process(route: .room(roomID: "1", via: []))
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: "1", text: "Important text")
|
||||
try await process(route: .share(sharePayload))
|
||||
|
||||
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
|
||||
try await process(route: .childRoom(roomID: "2", via: []))
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator)
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
|
||||
|
||||
try await process(route: .share(sharePayload))
|
||||
|
||||
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
|
||||
XCTAssertNil(navigationStackCoordinator.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func process(route: AppRoute) async throws {
|
||||
|
||||
@@ -242,7 +242,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
"A new timeline should be created for the same room ID, so that the screen isn't stale while loading.")
|
||||
}
|
||||
|
||||
func testShareRouteWithoutRoom() async throws {
|
||||
func testShareMediaRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil))
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
|
||||
@@ -253,7 +253,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareRouteWithRoom() async throws {
|
||||
func testShareMediaRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1"))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
@@ -265,6 +265,29 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithoutRoom() async throws {
|
||||
try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil))
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedState: .shareExtensionRoomList(sharePayload: sharePayload))
|
||||
|
||||
XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator)
|
||||
}
|
||||
|
||||
func testShareTextRouteWithRoom() async throws {
|
||||
try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1"))
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
|
||||
let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text")
|
||||
try await process(route: .share(sharePayload),
|
||||
expectedState: .roomList(selectedRoomID: "2"))
|
||||
|
||||
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||
XCTAssertNil(splitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws {
|
||||
|
||||
Reference in New Issue
Block a user