Rename UserSuggestion module as CompletionSuggestion

This commit is contained in:
aringenbach 2023-04-19 14:22:21 +02:00
parent 3cdbc26aed
commit 6d981004ed
24 changed files with 353 additions and 339 deletions

View file

@ -61,7 +61,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
// The preview header
@property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer;
@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint;
@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *userSuggestionContainerHeightConstraint;
@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *completionSuggestionContainerHeightConstraint;
// The jump to last unread banner
@property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer;

View file

@ -97,7 +97,7 @@ static CGSize kThreadListBarButtonItemImageSize;
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
{
// The preview header
@ -223,8 +223,8 @@ static CGSize kThreadListBarButtonItemImageSize;
@property (nonatomic, strong) ShareManager *shareManager;
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator;
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView;
@property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator;
@property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView;
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
@ -416,7 +416,7 @@ static CGSize kThreadListBarButtonItemImageSize;
[self setupActions];
[self setupUserSuggestionViewIfNeeded];
[self setupCompletionSuggestionViewIfNeeded];
[self.topBannersStackView vc_removeAllSubviews];
}
@ -1088,12 +1088,12 @@ static CGSize kThreadListBarButtonItemImageSize;
[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
_voiceMessageController.roomId = dataSource.roomId;
_userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
_completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
room:dataSource.room
userID:self.roomDataSource.mxSession.myUserId];
_userSuggestionCoordinator.delegate = self;
_completionSuggestionCoordinator.delegate = self;
[self setupUserSuggestionViewIfNeeded];
[self setupCompletionSuggestionViewIfNeeded];
[self updateTopBanners];
}
@ -2726,13 +2726,13 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
- (void)setupUserSuggestionViewIfNeeded
- (void)setupCompletionSuggestionViewIfNeeded
{
if(!self.isViewLoaded) {
return;
}
UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable;
UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable;
if (!suggestionsViewController)
{
@ -2742,12 +2742,12 @@ static CGSize kThreadListBarButtonItemImageSize;
[suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addChildViewController:suggestionsViewController];
[self.userSuggestionContainerView addSubview:suggestionsViewController.view];
[self.completionSuggestionContainerView addSubview:suggestionsViewController.view];
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor],
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor],
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor],
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]];
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor],
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor],
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor],
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]];
[suggestionsViewController didMoveToParentViewController:self];
}
@ -5147,17 +5147,17 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView
{
[self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage];
[self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage];
}
- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern
{
[self.userSuggestionCoordinator processSuggestionPattern:suggestionPattern];
[self.completionSuggestionCoordinator processSuggestionPattern:suggestionPattern];
}
- (UserSuggestionViewModelContextWrapper *)userSuggestionContext
- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext
{
return [self.userSuggestionCoordinator sharedContext];
return [self.completionSuggestionCoordinator sharedContext];
}
- (MXMediaManager *)mediaManager
@ -8059,9 +8059,9 @@ static CGSize kThreadListBarButtonItemImageSize;
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
}
#pragma mark - UserSuggestionCoordinatorBridgeDelegate
#pragma mark - CompletionSuggestionCoordinatorBridgeDelegate
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
didRequestMentionForMember:(MXRoomMember *)member
textTrigger:(NSString *)textTrigger
{
@ -8069,16 +8069,16 @@ static CGSize kThreadListBarButtonItemImageSize;
[self mention:member];
}
- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator
- (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator
textTrigger:(NSString *)textTrigger
{
[self removeTriggerTextFromComposer:textTrigger];
[self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]];
[self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]];
}
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator
didRequestCommand:(NSString *)command
textTrigger:(NSString *)textTrigger
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
didRequestCommand:(NSString *)command
textTrigger:(NSString *)textTrigger
{
[self removeTriggerTextFromComposer:textTrigger];
[self setCommand:command];
@ -8097,11 +8097,11 @@ static CGSize kThreadListBarButtonItemImageSize;
}
}
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height
{
if (self.userSuggestionContainerHeightConstraint.constant != height)
if (self.completionSuggestionContainerHeightConstraint.constant != height)
{
self.userSuggestionContainerHeightConstraint.constant = height;
self.completionSuggestionContainerHeightConstraint.constant = height;
[self.view layoutIfNeeded];
}

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -13,6 +12,8 @@
<connections>
<outlet property="bubblesTableView" destination="BGD-sd-SQR" id="OG4-Tw-Ovt"/>
<outlet property="bubblesTableViewBottomConstraint" destination="1SD-y2-oTg" id="n8D-hT-eqt"/>
<outlet property="completionSuggestionContainerHeightConstraint" destination="1Cd-cT-gOr" id="au5-3q-r54"/>
<outlet property="completionSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/>
<outlet property="inputBackgroundView" destination="Xt7-83-dQh" id="xoG-eb-zFB"/>
<outlet property="jumpToLastUnreadBanner" destination="S6r-bo-jxw" id="FSS-Be-E15"/>
<outlet property="jumpToLastUnreadBannerContainer" destination="S6H-Az-RCM" id="YlI-fu-OpT"/>
@ -32,8 +33,6 @@
<outlet property="scrollToBottomBadgeLabel" destination="QHs-rM-UU8" id="wk7-PQ-9Jm"/>
<outlet property="scrollToBottomButton" destination="Ih9-EU-BOU" id="Wwg-gS-Sfp"/>
<outlet property="topBannersStackView" destination="3z2-8P-wlg" id="uf5-gw-zWi"/>
<outlet property="userSuggestionContainerHeightConstraint" destination="1Cd-cT-gOr" id="au5-3q-r54"/>
<outlet property="userSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/>
<outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/>
<outletCollection property="toolbarContainerConstraints" destination="T1Y-r9-bYV" id="wax-9P-KGn"/>
<outletCollection property="toolbarContainerConstraints" destination="pRw-S0-6WL" id="q4S-0g-sqQ"/>
@ -48,20 +47,20 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="3z2-8P-wlg" userLabel="Top Banners Stack View">
<rect key="frame" x="0.0" y="0.0" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="20" width="375" height="0.0"/>
<constraints>
<constraint firstAttribute="height" priority="250" id="Y9P-Ek-wjg"/>
</constraints>
</stackView>
<tableView contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="BGD-sd-SQR">
<rect key="frame" x="0.0" y="0.0" width="375" height="626"/>
<rect key="frame" x="0.0" y="20" width="375" height="606"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="RoomVCBubblesTableView"/>
</userDefinedRuntimeAttributes>
</tableView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="54r-18-K1g">
<rect key="frame" x="0.0" y="0.0" width="375" height="368"/>
<rect key="frame" x="0.0" y="20" width="375" height="368"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="RoomVCPreviewHeaderContainer"/>
<constraints>
@ -69,7 +68,7 @@
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="fmF-ad-erE">
<rect key="frame" x="0.0" y="0.0" width="375" height="0.0"/>
<rect key="frame" x="0.0" y="20" width="375" height="0.0"/>
<subviews>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hB3-nR-MVR">
<rect key="frame" x="0.0" y="0.0" width="375" height="54"/>
@ -189,7 +188,7 @@
</constraints>
</view>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gt1-EO-UVY">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
@ -237,11 +236,6 @@
<point key="canvasLocation" x="136.80000000000001" y="152.47376311844079"/>
</view>
</objects>
<designables>
<designable name="QHs-rM-UU8">
<size key="intrinsicContentSize" width="7.5" height="13.5"/>
</designable>
</designables>
<resources>
<image name="new_close" width="16" height="16"/>
<image name="room_scroll_up" width="24" height="24"/>

View file

@ -22,7 +22,7 @@
@class RoomInputToolbarView;
@class LinkActionWrapper;
@class SuggestionPatternWrapper;
@class UserSuggestionViewModelContextWrapper;
@class CompletionSuggestionViewModelContextWrapper;
/**
Destination of the message in the composer
@ -84,7 +84,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
- (void)didDetectTextPattern: (SuggestionPatternWrapper *)suggestionPattern;
- (UserSuggestionViewModelContextWrapper *)userSuggestionContext;
- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext;
- (MXMediaManager *)mediaManager;

View file

@ -223,7 +223,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
let composer = Composer(
viewModel: viewModel.context,
wysiwygViewModel: wysiwygViewModel,
userSuggestionSharedContext: toolbarViewDelegate.userSuggestionContext().context,
completionSuggestionSharedContext: toolbarViewDelegate.completionSuggestionContext().context,
resizeAnimationDuration: Double(kResizeComposerAnimationDuration),
sendMessageAction: { [weak self] content in
guard let self = self else { return }

View file

@ -51,7 +51,7 @@ enum MockAppScreens {
MockStaticLocationViewingScreenState.self,
MockLocationSharingScreenState.self,
MockAnalyticsPromptScreenState.self,
MockUserSuggestionScreenState.self,
MockCompletionSuggestionScreenState.self,
MockPollEditFormScreenState.self,
MockSpaceCreationEmailInvitesScreenState.self,
MockSpaceSettingsScreenState.self,

View file

@ -16,15 +16,15 @@
import Foundation
enum UserSuggestionViewAction {
case selectedItem(UserSuggestionViewStateItem)
enum CompletionSuggestionViewAction {
case selectedItem(CompletionSuggestionViewStateItem)
}
enum UserSuggestionViewModelResult {
enum CompletionSuggestionViewModelResult {
case selectedItemWithIdentifier(String)
}
enum UserSuggestionViewStateItem: Identifiable {
enum CompletionSuggestionViewStateItem: Identifiable {
case command(name: String)
case user(id: String, avatar: AvatarInputProtocol?, displayName: String?)
@ -38,6 +38,6 @@ enum UserSuggestionViewStateItem: Identifiable {
}
}
struct UserSuggestionViewState: BindableState {
var items: [UserSuggestionViewStateItem]
struct CompletionSuggestionViewState: BindableState {
var items: [CompletionSuggestionViewStateItem]
}

View file

@ -17,32 +17,32 @@
import Foundation
import SwiftUI
enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
enum MockCompletionSuggestionScreenState: MockScreenState, CaseIterable {
case multipleResults
private static var members: [RoomMembersProviderMember]!
var screenType: Any.Type {
UserSuggestionList.self
CompletionSuggestionList.self
}
var screenView: ([Any], AnyView) {
let service = UserSuggestionService(roomMemberProvider: self, commandProvider: self)
let listViewModel = UserSuggestionViewModel(userSuggestionService: service)
let service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self)
let listViewModel = CompletionSuggestionViewModel(completionSuggestionService: service)
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
let viewModel = CompletionSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
service.processTextMessage(textMessage)
}
return (
[service, listViewModel],
AnyView(UserSuggestionListWithInput(viewModel: viewModel)
AnyView(CompletionSuggestionListWithInput(viewModel: viewModel)
.environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
extension MockUserSuggestionScreenState: RoomMembersProviderProtocol {
extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol {
var canMentionRoom: Bool { false }
func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) {
@ -61,7 +61,7 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol {
}
}
extension MockUserSuggestionScreenState: CommandsProviderProtocol {
extension MockCompletionSuggestionScreenState: CommandsProviderProtocol {
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
commands([
CommandsProviderCommand(name: "/ban"),

View file

@ -0,0 +1,77 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias CompletionSuggestionViewModelType = StateStoreViewModel<CompletionSuggestionViewState, CompletionSuggestionViewAction>
class CompletionSuggestionViewModel: CompletionSuggestionViewModelType, CompletionSuggestionViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let completionSuggestionService: CompletionSuggestionServiceProtocol
// MARK: Public
var sharedContext: CompletionSuggestionViewModelType.Context {
return self.context
}
var completion: ((CompletionSuggestionViewModelResult) -> Void)?
// MARK: - Setup
init(completionSuggestionService: CompletionSuggestionServiceProtocol) {
self.completionSuggestionService = completionSuggestionService
let items = completionSuggestionService.items.value.map { suggestionItem in
switch suggestionItem {
case .command(let completionSuggestionCommandItem):
return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name)
case .user(let completionSuggestionUserItem):
return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId,
avatar: completionSuggestionUserItem,
displayName: completionSuggestionUserItem.displayName)
}
}
super.init(initialViewState: CompletionSuggestionViewState(items: items))
completionSuggestionService.items.sink { [weak self] items in
self?.state.items = items.map { item in
switch item {
case .command(let completionSuggestionCommandItem):
return CompletionSuggestionViewStateItem.command(name: completionSuggestionCommandItem.name)
case .user(let completionSuggestionUserItem):
return CompletionSuggestionViewStateItem.user(id: completionSuggestionUserItem.userId,
avatar: completionSuggestionUserItem,
displayName: completionSuggestionUserItem.displayName)
}
}
}.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: CompletionSuggestionViewAction) {
switch viewAction {
case .selectedItem(let item):
completion?(.selectedItemWithIdentifier(item.id))
}
}
}

View file

@ -16,10 +16,10 @@
import Foundation
protocol UserSuggestionViewModelProtocol {
/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple
/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController`
protocol CompletionSuggestionViewModelProtocol {
/// Defines a shared context providing the ability to use a single `CompletionSuggestionViewModel` for multiple
/// `CompletionSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController`
/// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data.
var sharedContext: UserSuggestionViewModelType.Context { get }
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set }
var sharedContext: CompletionSuggestionViewModelType.Context { get }
var completion: ((CompletionSuggestionViewModelResult) -> Void)? { get set }
}

View file

@ -20,40 +20,40 @@ import SwiftUI
import UIKit
import WysiwygComposer
protocol UserSuggestionCoordinatorDelegate: AnyObject {
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?)
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?)
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat)
protocol CompletionSuggestionCoordinatorDelegate: AnyObject {
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?)
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?)
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat)
}
struct UserSuggestionCoordinatorParameters {
struct CompletionSuggestionCoordinatorParameters {
let mediaManager: MXMediaManager
let room: MXRoom
let userID: String
}
/// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c.
final class UserSuggestionViewModelContextWrapper: NSObject {
let context: UserSuggestionViewModelType.Context
/// Wrapper around `CompletionSuggestionViewModelType.Context` to pass it through obj-c.
final class CompletionSuggestionViewModelContextWrapper: NSObject {
let context: CompletionSuggestionViewModelType.Context
init(context: UserSuggestionViewModelType.Context) {
init(context: CompletionSuggestionViewModelType.Context) {
self.context = context
}
}
final class UserSuggestionCoordinator: Coordinator, Presentable {
final class CompletionSuggestionCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: UserSuggestionCoordinatorParameters
private let parameters: CompletionSuggestionCoordinatorParameters
private var userSuggestionHostingController: UIHostingController<AnyView>
private var userSuggestionService: UserSuggestionServiceProtocol
private var userSuggestionViewModel: UserSuggestionViewModelProtocol
private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider
private var commandProvider: UserSuggestionCoordinatorCommandProvider
private var completionSuggestionHostingController: UIHostingController<AnyView>
private var completionSuggestionService: CompletionSuggestionServiceProtocol
private var completionSuggestionViewModel: CompletionSuggestionViewModelProtocol
private var roomMemberProvider: CompletionSuggestionCoordinatorRoomMemberProvider
private var commandProvider: CompletionSuggestionCoordinatorCommandProvider
private var cancellables = Set<AnyCancellable>()
@ -63,57 +63,57 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
weak var delegate: UserSuggestionCoordinatorDelegate?
weak var delegate: CompletionSuggestionCoordinatorDelegate?
// MARK: - Setup
init(parameters: UserSuggestionCoordinatorParameters) {
init(parameters: CompletionSuggestionCoordinatorParameters) {
self.parameters = parameters
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID)
commandProvider = UserSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID)
userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider)
roomMemberProvider = CompletionSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID)
commandProvider = CompletionSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID)
completionSuggestionService = CompletionSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider)
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context)
let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService)
let view = CompletionSuggestionList(viewModel: viewModel.context)
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
userSuggestionViewModel = viewModel
userSuggestionHostingController = VectorHostingController(rootView: view)
completionSuggestionViewModel = viewModel
completionSuggestionHostingController = VectorHostingController(rootView: view)
userSuggestionViewModel.completion = { [weak self] result in
completionSuggestionViewModel.completion = { [weak self] result in
guard let self = self else {
return
}
switch result {
case .selectedItemWithIdentifier(let identifier):
if identifier == UserSuggestionID.room {
self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger)
if identifier == CompletionSuggestionUserID.room {
self.delegate?.completionSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.completionSuggestionService.currentTextTrigger)
return
}
if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first {
self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger)
self.delegate?.completionSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.completionSuggestionService.currentTextTrigger)
} else if let command = self.commandProvider.commands.filter({ $0 == identifier }).first {
self.delegate?.userSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.userSuggestionService.currentTextTrigger)
self.delegate?.completionSuggestionCoordinator(self, didRequestCommand: command, textTrigger: self.completionSuggestionService.currentTextTrigger)
}
}
}
userSuggestionService.items.sink { [weak self] _ in
completionSuggestionService.items.sink { [weak self] _ in
guard let self = self else { return }
self.delegate?.userSuggestionCoordinator(self,
self.delegate?.completionSuggestionCoordinator(self,
didUpdateViewHeight: self.calculateViewHeight())
}.store(in: &cancellables)
}
func processTextMessage(_ textMessage: String) {
userSuggestionService.processTextMessage(textMessage)
completionSuggestionService.processTextMessage(textMessage)
}
func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) {
userSuggestionService.processSuggestionPattern(suggestionPattern)
completionSuggestionService.processSuggestionPattern(suggestionPattern)
}
// MARK: - Public
@ -121,18 +121,18 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
func start() { }
func toPresentable() -> UIViewController {
userSuggestionHostingController
completionSuggestionHostingController
}
func sharedContext() -> UserSuggestionViewModelContextWrapper {
UserSuggestionViewModelContextWrapper(context: userSuggestionViewModel.sharedContext)
func sharedContext() -> CompletionSuggestionViewModelContextWrapper {
CompletionSuggestionViewModelContextWrapper(context: completionSuggestionViewModel.sharedContext)
}
// MARK: - Private
private func calculateViewHeight() -> CGFloat {
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context)
let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService)
let view = CompletionSuggestionList(viewModel: viewModel.context)
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
let controller = VectorHostingController(rootView: view)
@ -156,7 +156,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
}
}
private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
private class CompletionSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
private let room: MXRoom
private let userID: String
@ -194,7 +194,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr
self.roomMembers = joinedMembers
members(self.roomMembersToProviderMembers(joinedMembers))
} failure: { error in
MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error)
MXLog.error("[CompletionSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error)
}
}
@ -203,7 +203,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr
}
}
private class UserSuggestionCoordinatorCommandProvider: CommandsProviderProtocol {
private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderProtocol {
private let room: MXRoom
private let userID: String

View file

@ -0,0 +1,79 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@objc
protocol CompletionSuggestionCoordinatorBridgeDelegate: AnyObject {
func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinatorBridge, textTrigger: String?)
func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?)
func completionSuggestionCoordinatorBridge(_ coordinator: CompletionSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat)
}
@objcMembers
final class CompletionSuggestionCoordinatorBridge: NSObject {
private var _completionSuggestionCoordinator: Any?
fileprivate var completionSuggestionCoordinator: CompletionSuggestionCoordinator {
_completionSuggestionCoordinator as! CompletionSuggestionCoordinator
}
weak var delegate: CompletionSuggestionCoordinatorBridgeDelegate?
init(mediaManager: MXMediaManager, room: MXRoom, userID: String) {
let parameters = CompletionSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID)
let completionSuggestionCoordinator = CompletionSuggestionCoordinator(parameters: parameters)
_completionSuggestionCoordinator = completionSuggestionCoordinator
super.init()
completionSuggestionCoordinator.delegate = self
}
func processTextMessage(_ textMessage: String) {
completionSuggestionCoordinator.processTextMessage(textMessage)
}
func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) {
completionSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern)
}
func toPresentable() -> UIViewController? {
completionSuggestionCoordinator.toPresentable()
}
func sharedContext() -> CompletionSuggestionViewModelContextWrapper {
completionSuggestionCoordinator.sharedContext()
}
}
extension CompletionSuggestionCoordinatorBridge: CompletionSuggestionCoordinatorDelegate {
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) {
delegate?.completionSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger)
}
func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?) {
delegate?.completionSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger)
}
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) {
delegate?.completionSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger)
}
func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat) {
delegate?.completionSuggestionCoordinatorBridge(self, didUpdateViewHeight: height)
}
}

View file

@ -28,7 +28,7 @@ struct CommandsProviderCommand {
var name: String
}
class UserSuggestionID: NSObject {
class CompletionSuggestionUserID: NSObject {
/// A special case added for suggesting `@room` mentions.
@objc static let room = "@room"
}
@ -42,17 +42,17 @@ protocol CommandsProviderProtocol {
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void)
}
struct UserSuggestionServiceItem: UserSuggestionItemProtocol {
struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol {
let userId: String
let displayName: String?
let avatarUrl: String?
}
struct CommandSuggestionServiceItem: CommandSuggestionItemProtocol {
struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol {
let name: String
}
class UserSuggestionService: UserSuggestionServiceProtocol {
class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
// MARK: - Properties
// MARK: Private
@ -60,13 +60,13 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
private let roomMemberProvider: RoomMembersProviderProtocol
private let commandProvider: CommandsProviderProtocol
private var suggestionItems: [SuggestionItem] = []
private var suggestionItems: [CompletionSuggestionItem] = []
private let currentTextTriggerSubject = CurrentValueSubject<String?, Never>(nil)
private var cancellables = Set<AnyCancellable>()
// MARK: Public
var items = CurrentValueSubject<[SuggestionItem], Never>([])
var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([])
var currentTextTrigger: String? {
currentTextTriggerSubject.value
@ -93,7 +93,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
}
}
// MARK: - UserSuggestionServiceProtocol
// MARK: - CompletionSuggestionServiceProtocol
func processTextMessage(_ textMessage: String?) {
guard let textMessage = textMessage,
@ -145,14 +145,14 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
}
self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in
SuggestionItem.user(value: UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl))
CompletionSuggestionItem.user(value: CompletionSuggestionServiceUserItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl))
}
self.items.send(self.suggestionItems.filter { item in
guard case let .user(userSuggestion) = item else { return false }
guard case let .user(completionSuggestionUserItem) = item else { return false }
let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased())
let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased())
let containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(partialName.lowercased())
let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(partialName.lowercased())
return (containedInUsername || containedInDisplayName)
})
@ -165,7 +165,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
guard let self else { return }
self.suggestionItems = commands.map { command in
SuggestionItem.command(value: CommandSuggestionServiceItem(name: command.name))
CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(name: command.name))
}
self.items.send(self.suggestionItems.filter { item in
@ -184,6 +184,6 @@ extension Array where Element == RoomMembersProviderMember {
/// Returns the array with an additional member that represents an `@room` mention.
func withRoom(_ canMentionRoom: Bool) -> Self {
guard canMentionRoom else { return self }
return self + [RoomMembersProviderMember(userId: UserSuggestionID.room, displayName: "Everyone", avatarUrl: "")]
return self + [RoomMembersProviderMember(userId: CompletionSuggestionUserID.room, displayName: "Everyone", avatarUrl: "")]
}
}

View file

@ -18,23 +18,23 @@ import Combine
import Foundation
import WysiwygComposer
protocol UserSuggestionItemProtocol: Avatarable {
protocol CompletionSuggestionUserItemProtocol: Avatarable {
var userId: String { get }
var displayName: String? { get }
var avatarUrl: String? { get }
}
protocol CommandSuggestionItemProtocol {
protocol CompletionSuggestionCommandItemProtocol {
var name: String { get }
}
enum SuggestionItem {
case command(value: CommandSuggestionItemProtocol)
case user(value: UserSuggestionItemProtocol)
enum CompletionSuggestionItem {
case command(value: CompletionSuggestionCommandItemProtocol)
case user(value: CompletionSuggestionUserItemProtocol)
}
protocol UserSuggestionServiceProtocol {
var items: CurrentValueSubject<[SuggestionItem], Never> { get }
protocol CompletionSuggestionServiceProtocol {
var items: CurrentValueSubject<[CompletionSuggestionItem], Never> { get }
var currentTextTrigger: String? { get }
@ -44,7 +44,7 @@ protocol UserSuggestionServiceProtocol {
// MARK: Avatarable
extension UserSuggestionItemProtocol {
extension CompletionSuggestionUserItemProtocol {
var mxContentUri: String? {
avatarUrl
}

View file

@ -17,9 +17,9 @@
import RiotSwiftUI
import XCTest
class UserSuggestionUITests: MockScreenTestCase {
func testUserSuggestionScreen() throws {
app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title)
class CompletionSuggestionUITests: MockScreenTestCase {
func testCompletionSuggestionScreen() throws {
app.goToScreenWithIdentifier(MockCompletionSuggestionScreenState.multipleResults.title)
let firstButton = app.buttons["displayNameText-userIdText"].firstMatch
XCTAssert(firstButton.waitForExistence(timeout: 10))

View file

@ -19,51 +19,53 @@ import XCTest
@testable import RiotSwiftUI
class UserSuggestionServiceTests: XCTestCase {
var service: UserSuggestionService!
class CompletionSuggestionServiceTests: XCTestCase {
var service: CompletionSuggestionService!
var canMentionRoom = false
override func setUp() {
service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false)
service = CompletionSuggestionService(roomMemberProvider: self,
commandProvider: self,
shouldDebounce: false)
canMentionRoom = false
}
func testAlice() {
service.processTextMessage("@Al")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@al")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@ice")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@Alice")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@alice:matrix.org")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
}
func testBob() {
service.processTextMessage("@ob")
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob")
service.processTextMessage("@ob:")
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob")
service.processTextMessage("@b:matrix")
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob")
}
func testBoth() {
service.processTextMessage("@:matrix")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.last?.displayName, "Bob")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob")
service.processTextMessage("@.org")
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
XCTAssertEqual(service.items.value.last?.displayName, "Bob")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob")
}
func testEmptyResult() {
@ -117,18 +119,18 @@ class UserSuggestionServiceTests: XCTestCase {
}
func testRoomWithPower() {
// Given a user without the power to mention a room.
// Given a user with the power to mention a room.
canMentionRoom = true
// Given a user without the power to mention a room.
// Given a user with the power to mention a room.
service.processTextMessage("@ro")
// Then the completion for a room mention should be shown.
XCTAssertEqual(service.items.value.first?.userId, UserSuggestionID.room)
XCTAssertEqual(service.items.value.first?.asUser?.userId, CompletionSuggestionUserID.room)
}
}
extension UserSuggestionServiceTests: RoomMembersProviderProtocol {
extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol {
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
let users = [("Alice", "@alice:matrix.org"),
("Bob", "@bob:matrix.org")]
@ -138,3 +140,23 @@ extension UserSuggestionServiceTests: RoomMembersProviderProtocol {
})
}
}
extension CompletionSuggestionServiceTests: CommandsProviderProtocol {
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
let commandList = ["/ban", "/invite", "/join", "/me"]
commands(commandList.map { command in
CommandsProviderCommand(name: command)
})
}
}
extension CompletionSuggestionItem {
var asUser: CompletionSuggestionUserItemProtocol? {
if case let .user(value) = self { return value } else { return nil }
}
var asCommand: CompletionSuggestionCommandItemProtocol? {
if case let .command(value) = self { return value } else { return nil }
}
}

View file

@ -16,7 +16,7 @@
import SwiftUI
struct UserSuggestionList: View {
struct CompletionSuggestionList: View {
private enum Constants {
static let topPadding: CGFloat = 8.0
static let listItemPadding: CGFloat = 4.0
@ -43,7 +43,7 @@ struct UserSuggestionList: View {
// MARK: Public
@ObservedObject var viewModel: UserSuggestionViewModel.Context
@ObservedObject var viewModel: CompletionSuggestionViewModel.Context
var showBackgroundShadow: Bool = true
var body: some View {
@ -51,7 +51,7 @@ struct UserSuggestionList: View {
EmptyView()
} else {
ZStack {
UserSuggestionListItem(content: UserSuggestionViewStateItem.user(
CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user(
id: "Prototype",
avatar: AvatarInput(mxContentUri: "",
matrixItemId: "",
@ -79,7 +79,7 @@ struct UserSuggestionList: View {
Button {
viewModel.send(viewAction: .selectedItem(item))
} label: {
UserSuggestionListItem(content: item)
CompletionSuggestionListItem(content: item)
.modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id))
}
}
@ -134,8 +134,8 @@ private struct BackgroundView<Content: View>: View {
// MARK: - Previews
struct UserSuggestion_Previews: PreviewProvider {
static let stateRenderer = MockUserSuggestionScreenState.stateRenderer
struct CompletionSuggestion_Previews: PreviewProvider {
static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
}

View file

@ -16,7 +16,7 @@
import SwiftUI
struct UserSuggestionListItem: View {
struct CompletionSuggestionListItem: View {
// MARK: - Properties
// MARK: Private
@ -25,7 +25,7 @@ struct UserSuggestionListItem: View {
// MARK: Public
let content: UserSuggestionViewStateItem
let content: CompletionSuggestionViewStateItem
var body: some View {
HStack {
@ -59,9 +59,9 @@ struct UserSuggestionListItem: View {
// MARK: - Previews
struct UserSuggestionHeader_Previews: PreviewProvider {
struct CompletionSuggestionHeader_Previews: PreviewProvider {
static var previews: some View {
UserSuggestionListItem(content: UserSuggestionViewStateItem.user(
CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user(
id: "@alice:matrix.org",
avatar: MockAvatarInput.example,
displayName: "Alice"

View file

@ -16,24 +16,24 @@
import SwiftUI
struct UserSuggestionListWithInputViewModel {
let listViewModel: UserSuggestionViewModel
struct CompletionSuggestionListWithInputViewModel {
let listViewModel: CompletionSuggestionViewModel
let callback: (String) -> Void
}
struct UserSuggestionListWithInput: View {
struct CompletionSuggestionListWithInput: View {
// MARK: - Properties
// MARK: Private
// MARK: Public
var viewModel: UserSuggestionListWithInputViewModel
var viewModel: CompletionSuggestionListWithInputViewModel
@State private var inputText = ""
var body: some View {
VStack(spacing: 0.0) {
UserSuggestionList(viewModel: viewModel.listViewModel.context)
CompletionSuggestionList(viewModel: viewModel.listViewModel.context)
TextField("Search for user", text: $inputText)
.background(Color.white)
.onChange(of: inputText, perform: viewModel.callback)
@ -48,8 +48,8 @@ struct UserSuggestionListWithInput: View {
// MARK: - Previews
struct UserSuggestionListWithInput_Previews: PreviewProvider {
static let stateRenderer = MockUserSuggestionScreenState.stateRenderer
struct CompletionSuggestionListWithInput_Previews: PreviewProvider {
static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
}

View file

@ -29,7 +29,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) {
let viewModel: ComposerViewModel
let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: []))
let completionSuggestionViewModel = MockCompletionSuggestionViewModel(initialViewState: CompletionSuggestionViewState(items: []))
let bindings = ComposerBindings(focused: false)
switch self {
@ -67,7 +67,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
Spacer()
Composer(viewModel: viewModel.context,
wysiwygViewModel: wysiwygviewModel,
userSuggestionSharedContext: userSuggestionViewModel.context,
completionSuggestionSharedContext: completionSuggestionViewModel.context,
resizeAnimationDuration: 0.1,
sendMessageAction: { _ in },
showSendMediaActions: { })
@ -82,6 +82,4 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
}
}
private final class MockUserSuggestionViewModel: UserSuggestionViewModelType {
}
private final class MockCompletionSuggestionViewModel: CompletionSuggestionViewModelType { }

View file

@ -257,11 +257,11 @@ final class SuggestionPatternWrapper: NSObject {
}
}
final class UserSuggestionViewModelWrapper: NSObject {
let userSuggestionViewModel: UserSuggestionViewModel
final class CompletionSuggestionViewModelWrapper: NSObject {
let completionSuggestionViewModel: CompletionSuggestionViewModel
init(_ userSuggestionViewModel: UserSuggestionViewModel) {
self.userSuggestionViewModel = userSuggestionViewModel
init(_ completionSuggestionViewModel: CompletionSuggestionViewModel) {
self.completionSuggestionViewModel = completionSuggestionViewModel
super.init()
}
}

View file

@ -23,7 +23,7 @@ struct Composer: View {
// MARK: Private
@ObservedObject private var viewModel: ComposerViewModelType.Context
@ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel
private let userSuggestionSharedContext: UserSuggestionViewModelType.Context
private let completionSuggestionSharedContext: CompletionSuggestionViewModelType.Context
private let resizeAnimationDuration: Double
private let sendMessageAction: (WysiwygComposerContent) -> Void
@ -223,13 +223,13 @@ struct Composer: View {
init(
viewModel: ComposerViewModelType.Context,
wysiwygViewModel: WysiwygComposerViewModel,
userSuggestionSharedContext: UserSuggestionViewModelType.Context,
completionSuggestionSharedContext: CompletionSuggestionViewModelType.Context,
resizeAnimationDuration: Double,
sendMessageAction: @escaping (WysiwygComposerContent) -> Void,
showSendMediaActions: @escaping () -> Void) {
self.viewModel = viewModel
self.wysiwygViewModel = wysiwygViewModel
self.userSuggestionSharedContext = userSuggestionSharedContext
self.completionSuggestionSharedContext = completionSuggestionSharedContext
self.resizeAnimationDuration = resizeAnimationDuration
self.sendMessageAction = sendMessageAction
self.showSendMediaActions = showSendMediaActions
@ -256,7 +256,7 @@ struct Composer: View {
}
}
if wysiwygViewModel.maximised {
UserSuggestionList(viewModel: userSuggestionSharedContext, showBackgroundShadow: false)
CompletionSuggestionList(viewModel: completionSuggestionSharedContext, showBackgroundShadow: false)
}
}
.frame(height: composerHeight)

View file

@ -1,79 +0,0 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@objc
protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject {
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?)
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestCommand command: String, textTrigger: String?)
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat)
}
@objcMembers
final class UserSuggestionCoordinatorBridge: NSObject {
private var _userSuggestionCoordinator: Any?
fileprivate var userSuggestionCoordinator: UserSuggestionCoordinator {
_userSuggestionCoordinator as! UserSuggestionCoordinator
}
weak var delegate: UserSuggestionCoordinatorBridgeDelegate?
init(mediaManager: MXMediaManager, room: MXRoom, userID: String) {
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID)
let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters)
_userSuggestionCoordinator = userSuggestionCoordinator
super.init()
userSuggestionCoordinator.delegate = self
}
func processTextMessage(_ textMessage: String) {
userSuggestionCoordinator.processTextMessage(textMessage)
}
func processSuggestionPattern(_ suggestionPatternWrapper: SuggestionPatternWrapper) {
userSuggestionCoordinator.processSuggestionPattern(suggestionPatternWrapper.suggestionPattern)
}
func toPresentable() -> UIViewController? {
userSuggestionCoordinator.toPresentable()
}
func sharedContext() -> UserSuggestionViewModelContextWrapper {
userSuggestionCoordinator.sharedContext()
}
}
extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate {
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) {
delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger)
}
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) {
delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger)
}
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) {
delegate?.userSuggestionCoordinatorBridge(self, didRequestCommand: command, textTrigger: textTrigger)
}
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) {
delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height)
}
}

View file

@ -1,77 +0,0 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias UserSuggestionViewModelType = StateStoreViewModel<UserSuggestionViewState, UserSuggestionViewAction>
class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let userSuggestionService: UserSuggestionServiceProtocol
// MARK: Public
var sharedContext: UserSuggestionViewModelType.Context {
return self.context
}
var completion: ((UserSuggestionViewModelResult) -> Void)?
// MARK: - Setup
init(userSuggestionService: UserSuggestionServiceProtocol) {
self.userSuggestionService = userSuggestionService
let items = userSuggestionService.items.value.map { suggestionItem in
switch suggestionItem {
case .command(let commandSuggestionItem):
return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name)
case .user(let userSuggestionItem):
return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId,
avatar: userSuggestionItem,
displayName: userSuggestionItem.displayName)
}
}
super.init(initialViewState: UserSuggestionViewState(items: items))
userSuggestionService.items.sink { [weak self] items in
self?.state.items = items.map { item in
switch item {
case .command(let commandSuggestionItem):
return UserSuggestionViewStateItem.command(name: commandSuggestionItem.name)
case .user(let userSuggestionItem):
return UserSuggestionViewStateItem.user(id: userSuggestionItem.userId,
avatar: userSuggestionItem,
displayName: userSuggestionItem.displayName)
}
}
}.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: UserSuggestionViewAction) {
switch viewAction {
case .selectedItem(let item):
completion?(.selectedItemWithIdentifier(item.id))
}
}
}