Mention in RTE (but without the pill yet) (#1863)

* suggestions in RTE, but they do not render as pills yet.

* remove unused code

* Update ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* PR suggestions

---------

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro 2023-10-09 14:26:53 +02:00 committed by GitHub
parent 855b08144c
commit f7bf4b6e20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 79 additions and 27 deletions

View File

@ -346,7 +346,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
emojiProvider: emojiProvider,
completionSuggestionService: completionSuggestionService)
completionSuggestionService: completionSuggestionService,
appSettings: appSettings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in

View File

@ -45,6 +45,7 @@ enum ComposerToolbarViewAction {
case handlePasteOrDrop(provider: NSItemProvider)
case enableTextFormatting
case composerAction(action: ComposerAction)
case selectedSuggestion(_ suggestion: SuggestionItem)
}
struct ComposerToolbarViewState: BindableState {

View File

@ -24,6 +24,8 @@ typealias ComposerToolbarViewModelType = StateStoreViewModel<ComposerToolbarView
final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerToolbarViewModelProtocol {
private let wysiwygViewModel: WysiwygComposerViewModel
private let completionSuggestionService: CompletionSuggestionServiceProtocol
private let appSettings: AppSettings
private let actionsSubject: PassthroughSubject<ComposerToolbarViewModelAction, Never> = .init()
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
@ -38,9 +40,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
private var currentLinkData: WysiwygLinkData?
init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol) {
init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, appSettings: AppSettings) {
self.wysiwygViewModel = wysiwygViewModel
self.completionSuggestionService = completionSuggestionService
self.appSettings = appSettings
super.init(initialViewState: ComposerToolbarViewState(areSuggestionsEnabled: completionSuggestionService.areSuggestionsEnabled, bindings: .init()), imageProvider: mediaProvider)
@ -121,6 +124,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
} else {
wysiwygViewModel.apply(action)
}
case .selectedSuggestion(let suggestion):
handleSuggestion(suggestion)
}
}
@ -150,6 +155,17 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
// MARK: - Private
private func handleSuggestion(_ suggestion: SuggestionItem) {
switch suggestion {
case let .user(item):
guard let url = try? PermalinkBuilder.permalinkTo(userIdentifier: item.id, baseURL: appSettings.permalinkBaseURL) else {
MXLog.error("Could not build user permalink")
return
}
wysiwygViewModel.setMention(url: url.absoluteString, name: item.displayName ?? item.id, mentionType: .user)
}
}
private func set(mode: RoomScreenComposerMode) {
guard mode != state.composerMode else { return }

View File

@ -19,6 +19,7 @@ import SwiftUI
struct CompletionSuggestionView: View {
let imageProvider: ImageProviderProtocol?
let items: [SuggestionItem]
let onTap: (SuggestionItem) -> Void
private enum Constants {
static let topPadding: CGFloat = 8.0
@ -63,7 +64,9 @@ struct CompletionSuggestionView: View {
private func list() -> some View {
List(items) { item in
Button { } label: {
Button {
onTap(item)
} label: {
switch item {
case .user(let mention):
MentionSuggestionItemView(imageProvider: imageProvider, item: mention)
@ -135,11 +138,11 @@ struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview {
VStack {
CompletionSuggestionView(imageProvider: MockMediaProvider(),
items: [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)),
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))])
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))]) { _ in }
}
VStack {
CompletionSuggestionView(imageProvider: MockMediaProvider(),
items: multipleItems)
items: multipleItems) { _ in }
}
}
}

View File

@ -49,7 +49,9 @@ struct ComposerToolbar: View {
}
private var suggestionView: some View {
CompletionSuggestionView(imageProvider: context.imageProvider, items: context.viewState.suggestions)
CompletionSuggestionView(imageProvider: context.imageProvider, items: context.viewState.suggestions) { suggestion in
context.send(viewAction: .selectedSuggestion(suggestion))
}
}
private var topBar: some View {
@ -194,7 +196,8 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
static let wysiwygViewModel = WysiwygComposerViewModel()
static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)),
mediaProvider: MockMediaProvider())
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings)
static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)),
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))]
@ -219,7 +222,8 @@ extension ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider())
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings)
return ComposerToolbar(context: composerViewModel.context,
wysiwygViewModel: wysiwygViewModel,
keyCommandHandler: { _ in false })

View File

@ -117,7 +117,8 @@ struct RoomAttachmentPicker: View {
struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
static let viewModel = ComposerToolbarViewModel(wysiwygViewModel: WysiwygComposerViewModel(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider())
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings)
static var previews: some View {
RoomAttachmentPicker(context: viewModel.context)

View File

@ -25,6 +25,7 @@ struct RoomScreenCoordinatorParameters {
let mediaProvider: MediaProviderProtocol
let emojiProvider: EmojiProviderProtocol
let completionSuggestionService: CompletionSuggestionServiceProtocol
let appSettings: AppSettings
}
enum RoomScreenCoordinatorAction {
@ -59,7 +60,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
mediaProvider: parameters.mediaProvider,
roomProxy: parameters.roomProxy,
appSettings: ServiceLocator.shared.settings,
appSettings: parameters.appSettings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
@ -67,7 +68,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
maxCompressedHeight: ComposerConstant.maxHeight,
maxExpandedHeight: ComposerConstant.maxHeight,
parserStyle: .elementX)
composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: parameters.completionSuggestionService, mediaProvider: parameters.mediaProvider)
composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider,
appSettings: parameters.appSettings)
}
// MARK: - Public

View File

@ -236,7 +236,8 @@ class MockScreen: Identifiable {
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@ -246,7 +247,8 @@ class MockScreen: Identifiable {
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@ -258,7 +260,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@ -270,7 +273,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@ -283,7 +287,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
@ -298,7 +303,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -313,7 +319,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -328,7 +335,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -344,7 +352,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -359,7 +368,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -374,7 +384,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -389,7 +400,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@ -404,7 +416,8 @@ class MockScreen: Identifiable {
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()))
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)

View File

@ -36,7 +36,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init())
viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: completionSuggestionServiceMock,
mediaProvider: MockMediaProvider())
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings)
}
func testComposerFocus() {
@ -105,16 +106,24 @@ class ComposerToolbarViewModelTests: XCTestCase {
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions))
viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: mockCompletionSuggestionService,
mediaProvider: MockMediaProvider())
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings)
XCTAssertEqual(viewModel.state.suggestions, suggestions)
}
func testSuggestionTrigger() {
wysiwygViewModel.setMarkdownContent("@test")
wysiwygViewModel.setMarkdownContent("#not_implemented_yey")
wysiwygViewModel.setMarkdownContent("#not_implemented_yay")
// The first one is nil because when initialised the view model is empty
XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil, .init(type: .user, text: "test"), nil])
}
func testSelectedSuggestion() {
let suggestion = SuggestionItem.user(item: .init(id: "@test:matrix.org", displayName: "Test", avatarURL: nil))
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
XCTAssertEqual(wysiwygViewModel.content.html, "<a data-mention-type=\"user\" href=\"https://matrix.to/#/@test:matrix.org\" contenteditable=\"false\">Test</a> ")
}
}