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 // The preview header
@property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer; @property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer;
@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; @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 // The jump to last unread banner
@property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer; @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, @interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate, ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate, 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 // The preview header
@ -223,8 +223,8 @@ static CGSize kThreadListBarButtonItemImageSize;
@property (nonatomic, strong) ShareManager *shareManager; @property (nonatomic, strong) ShareManager *shareManager;
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder; @property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator;
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; @property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView;
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; @property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
@ -416,7 +416,7 @@ static CGSize kThreadListBarButtonItemImageSize;
[self setupActions]; [self setupActions];
[self setupUserSuggestionViewIfNeeded]; [self setupCompletionSuggestionViewIfNeeded];
[self.topBannersStackView vc_removeAllSubviews]; [self.topBannersStackView vc_removeAllSubviews];
} }
@ -1088,12 +1088,12 @@ static CGSize kThreadListBarButtonItemImageSize;
[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
_voiceMessageController.roomId = dataSource.roomId; _voiceMessageController.roomId = dataSource.roomId;
_userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager _completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
room:dataSource.room room:dataSource.room
userID:self.roomDataSource.mxSession.myUserId]; userID:self.roomDataSource.mxSession.myUserId];
_userSuggestionCoordinator.delegate = self; _completionSuggestionCoordinator.delegate = self;
[self setupUserSuggestionViewIfNeeded]; [self setupCompletionSuggestionViewIfNeeded];
[self updateTopBanners]; [self updateTopBanners];
} }
@ -2726,13 +2726,13 @@ static CGSize kThreadListBarButtonItemImageSize;
} }
} }
- (void)setupUserSuggestionViewIfNeeded - (void)setupCompletionSuggestionViewIfNeeded
{ {
if(!self.isViewLoaded) { if(!self.isViewLoaded) {
return; return;
} }
UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable;
if (!suggestionsViewController) if (!suggestionsViewController)
{ {
@ -2742,12 +2742,12 @@ static CGSize kThreadListBarButtonItemImageSize;
[suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addChildViewController:suggestionsViewController]; [self addChildViewController:suggestionsViewController];
[self.userSuggestionContainerView addSubview:suggestionsViewController.view]; [self.completionSuggestionContainerView addSubview:suggestionsViewController.view];
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor],
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor],
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor],
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]];
[suggestionsViewController didMoveToParentViewController:self]; [suggestionsViewController didMoveToParentViewController:self];
} }
@ -5147,17 +5147,17 @@ static CGSize kThreadListBarButtonItemImageSize;
- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView - (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView
{ {
[self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; [self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage];
} }
- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern - (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 - (MXMediaManager *)mediaManager
@ -8059,9 +8059,9 @@ static CGSize kThreadListBarButtonItemImageSize;
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId]; [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
} }
#pragma mark - UserSuggestionCoordinatorBridgeDelegate #pragma mark - CompletionSuggestionCoordinatorBridgeDelegate
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator - (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
didRequestMentionForMember:(MXRoomMember *)member didRequestMentionForMember:(MXRoomMember *)member
textTrigger:(NSString *)textTrigger textTrigger:(NSString *)textTrigger
{ {
@ -8069,16 +8069,16 @@ static CGSize kThreadListBarButtonItemImageSize;
[self mention:member]; [self mention:member];
} }
- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator - (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator
textTrigger:(NSString *)textTrigger textTrigger:(NSString *)textTrigger
{ {
[self removeTriggerTextFromComposer:textTrigger]; [self removeTriggerTextFromComposer:textTrigger];
[self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]]; [self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]];
} }
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator - (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
didRequestCommand:(NSString *)command didRequestCommand:(NSString *)command
textTrigger:(NSString *)textTrigger textTrigger:(NSString *)textTrigger
{ {
[self removeTriggerTextFromComposer:textTrigger]; [self removeTriggerTextFromComposer:textTrigger];
[self setCommand:command]; [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]; [self.view layoutIfNeeded];
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,32 +17,32 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { enum MockCompletionSuggestionScreenState: MockScreenState, CaseIterable {
case multipleResults case multipleResults
private static var members: [RoomMembersProviderMember]! private static var members: [RoomMembersProviderMember]!
var screenType: Any.Type { var screenType: Any.Type {
UserSuggestionList.self CompletionSuggestionList.self
} }
var screenView: ([Any], AnyView) { var screenView: ([Any], AnyView) {
let service = UserSuggestionService(roomMemberProvider: self, commandProvider: self) let service = CompletionSuggestionService(roomMemberProvider: self, commandProvider: self)
let listViewModel = UserSuggestionViewModel(userSuggestionService: service) let listViewModel = CompletionSuggestionViewModel(completionSuggestionService: service)
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in let viewModel = CompletionSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
service.processTextMessage(textMessage) service.processTextMessage(textMessage)
} }
return ( return (
[service, listViewModel], [service, listViewModel],
AnyView(UserSuggestionListWithInput(viewModel: viewModel) AnyView(CompletionSuggestionListWithInput(viewModel: viewModel)
.environmentObject(AvatarViewModel.withMockedServices())) .environmentObject(AvatarViewModel.withMockedServices()))
) )
} }
} }
extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol {
var canMentionRoom: Bool { false } var canMentionRoom: Bool { false }
func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) { func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) {
@ -61,7 +61,7 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol {
} }
} }
extension MockUserSuggestionScreenState: CommandsProviderProtocol { extension MockCompletionSuggestionScreenState: CommandsProviderProtocol {
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) { func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
commands([ commands([
CommandsProviderCommand(name: "/ban"), 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 import Foundation
protocol UserSuggestionViewModelProtocol { protocol CompletionSuggestionViewModelProtocol {
/// Defines a shared context providing the ability to use a single `UserSuggestionViewModel` for multiple /// Defines a shared context providing the ability to use a single `CompletionSuggestionViewModel` for multiple
/// `UserSuggestionList` e.g. the list component can then be displayed seemlessly in both `RoomViewController` /// `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. /// UIKit hosted context, and in Rich-Text-Editor's SwiftUI fullscreen mode, without need to reload the data.
var sharedContext: UserSuggestionViewModelType.Context { get } var sharedContext: CompletionSuggestionViewModelType.Context { get }
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } var completion: ((CompletionSuggestionViewModelResult) -> Void)? { get set }
} }

View file

@ -20,40 +20,40 @@ import SwiftUI
import UIKit import UIKit
import WysiwygComposer import WysiwygComposer
protocol UserSuggestionCoordinatorDelegate: AnyObject { protocol CompletionSuggestionCoordinatorDelegate: AnyObject {
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) func completionSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: CompletionSuggestionCoordinator, textTrigger: String?)
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?) func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didRequestCommand command: String, textTrigger: String?)
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) func completionSuggestionCoordinator(_ coordinator: CompletionSuggestionCoordinator, didUpdateViewHeight height: CGFloat)
} }
struct UserSuggestionCoordinatorParameters { struct CompletionSuggestionCoordinatorParameters {
let mediaManager: MXMediaManager let mediaManager: MXMediaManager
let room: MXRoom let room: MXRoom
let userID: String let userID: String
} }
/// Wrapper around `UserSuggestionViewModelType.Context` to pass it through obj-c. /// Wrapper around `CompletionSuggestionViewModelType.Context` to pass it through obj-c.
final class UserSuggestionViewModelContextWrapper: NSObject { final class CompletionSuggestionViewModelContextWrapper: NSObject {
let context: UserSuggestionViewModelType.Context let context: CompletionSuggestionViewModelType.Context
init(context: UserSuggestionViewModelType.Context) { init(context: CompletionSuggestionViewModelType.Context) {
self.context = context self.context = context
} }
} }
final class UserSuggestionCoordinator: Coordinator, Presentable { final class CompletionSuggestionCoordinator: Coordinator, Presentable {
// MARK: - Properties // MARK: - Properties
// MARK: Private // MARK: Private
private let parameters: UserSuggestionCoordinatorParameters private let parameters: CompletionSuggestionCoordinatorParameters
private var userSuggestionHostingController: UIHostingController<AnyView> private var completionSuggestionHostingController: UIHostingController<AnyView>
private var userSuggestionService: UserSuggestionServiceProtocol private var completionSuggestionService: CompletionSuggestionServiceProtocol
private var userSuggestionViewModel: UserSuggestionViewModelProtocol private var completionSuggestionViewModel: CompletionSuggestionViewModelProtocol
private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider private var roomMemberProvider: CompletionSuggestionCoordinatorRoomMemberProvider
private var commandProvider: UserSuggestionCoordinatorCommandProvider private var commandProvider: CompletionSuggestionCoordinatorCommandProvider
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@ -63,57 +63,57 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
var completion: (() -> Void)? var completion: (() -> Void)?
weak var delegate: UserSuggestionCoordinatorDelegate? weak var delegate: CompletionSuggestionCoordinatorDelegate?
// MARK: - Setup // MARK: - Setup
init(parameters: UserSuggestionCoordinatorParameters) { init(parameters: CompletionSuggestionCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID) roomMemberProvider = CompletionSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID)
commandProvider = UserSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID) commandProvider = CompletionSuggestionCoordinatorCommandProvider(room: parameters.room, userID: parameters.userID)
userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider) completionSuggestionService = CompletionSuggestionService(roomMemberProvider: roomMemberProvider, commandProvider: commandProvider)
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context) let view = CompletionSuggestionList(viewModel: viewModel.context)
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
userSuggestionViewModel = viewModel completionSuggestionViewModel = viewModel
userSuggestionHostingController = VectorHostingController(rootView: view) completionSuggestionHostingController = VectorHostingController(rootView: view)
userSuggestionViewModel.completion = { [weak self] result in completionSuggestionViewModel.completion = { [weak self] result in
guard let self = self else { guard let self = self else {
return return
} }
switch result { switch result {
case .selectedItemWithIdentifier(let identifier): case .selectedItemWithIdentifier(let identifier):
if identifier == UserSuggestionID.room { if identifier == CompletionSuggestionUserID.room {
self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger) self.delegate?.completionSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.completionSuggestionService.currentTextTrigger)
return return
} }
if let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first { 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 { } 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 } guard let self = self else { return }
self.delegate?.userSuggestionCoordinator(self, self.delegate?.completionSuggestionCoordinator(self,
didUpdateViewHeight: self.calculateViewHeight()) didUpdateViewHeight: self.calculateViewHeight())
}.store(in: &cancellables) }.store(in: &cancellables)
} }
func processTextMessage(_ textMessage: String) { func processTextMessage(_ textMessage: String) {
userSuggestionService.processTextMessage(textMessage) completionSuggestionService.processTextMessage(textMessage)
} }
func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) { func processSuggestionPattern(_ suggestionPattern: SuggestionPattern?) {
userSuggestionService.processSuggestionPattern(suggestionPattern) completionSuggestionService.processSuggestionPattern(suggestionPattern)
} }
// MARK: - Public // MARK: - Public
@ -121,18 +121,18 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
func start() { } func start() { }
func toPresentable() -> UIViewController { func toPresentable() -> UIViewController {
userSuggestionHostingController completionSuggestionHostingController
} }
func sharedContext() -> UserSuggestionViewModelContextWrapper { func sharedContext() -> CompletionSuggestionViewModelContextWrapper {
UserSuggestionViewModelContextWrapper(context: userSuggestionViewModel.sharedContext) CompletionSuggestionViewModelContextWrapper(context: completionSuggestionViewModel.sharedContext)
} }
// MARK: - Private // MARK: - Private
private func calculateViewHeight() -> CGFloat { private func calculateViewHeight() -> CGFloat {
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) let viewModel = CompletionSuggestionViewModel(completionSuggestionService: completionSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context) let view = CompletionSuggestionList(viewModel: viewModel.context)
.environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
let controller = VectorHostingController(rootView: view) 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 room: MXRoom
private let userID: String private let userID: String
@ -194,7 +194,7 @@ private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderPr
self.roomMembers = joinedMembers self.roomMembers = joinedMembers
members(self.roomMembersToProviderMembers(joinedMembers)) members(self.roomMembersToProviderMembers(joinedMembers))
} failure: { error in } 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 room: MXRoom
private let userID: String 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 var name: String
} }
class UserSuggestionID: NSObject { class CompletionSuggestionUserID: NSObject {
/// A special case added for suggesting `@room` mentions. /// A special case added for suggesting `@room` mentions.
@objc static let room = "@room" @objc static let room = "@room"
} }
@ -42,17 +42,17 @@ protocol CommandsProviderProtocol {
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void)
} }
struct UserSuggestionServiceItem: UserSuggestionItemProtocol { struct CompletionSuggestionServiceUserItem: CompletionSuggestionUserItemProtocol {
let userId: String let userId: String
let displayName: String? let displayName: String?
let avatarUrl: String? let avatarUrl: String?
} }
struct CommandSuggestionServiceItem: CommandSuggestionItemProtocol { struct CompletionSuggestionServiceCommandItem: CompletionSuggestionCommandItemProtocol {
let name: String let name: String
} }
class UserSuggestionService: UserSuggestionServiceProtocol { class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
// MARK: - Properties // MARK: - Properties
// MARK: Private // MARK: Private
@ -60,13 +60,13 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
private let roomMemberProvider: RoomMembersProviderProtocol private let roomMemberProvider: RoomMembersProviderProtocol
private let commandProvider: CommandsProviderProtocol private let commandProvider: CommandsProviderProtocol
private var suggestionItems: [SuggestionItem] = [] private var suggestionItems: [CompletionSuggestionItem] = []
private let currentTextTriggerSubject = CurrentValueSubject<String?, Never>(nil) private let currentTextTriggerSubject = CurrentValueSubject<String?, Never>(nil)
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
// MARK: Public // MARK: Public
var items = CurrentValueSubject<[SuggestionItem], Never>([]) var items = CurrentValueSubject<[CompletionSuggestionItem], Never>([])
var currentTextTrigger: String? { var currentTextTrigger: String? {
currentTextTriggerSubject.value currentTextTriggerSubject.value
@ -93,7 +93,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
} }
} }
// MARK: - UserSuggestionServiceProtocol // MARK: - CompletionSuggestionServiceProtocol
func processTextMessage(_ textMessage: String?) { func processTextMessage(_ textMessage: String?) {
guard let textMessage = textMessage, guard let textMessage = textMessage,
@ -145,14 +145,14 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
} }
self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in 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 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 containedInUsername = completionSuggestionUserItem.userId.lowercased().contains(partialName.lowercased())
let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) let containedInDisplayName = (completionSuggestionUserItem.displayName ?? "").lowercased().contains(partialName.lowercased())
return (containedInUsername || containedInDisplayName) return (containedInUsername || containedInDisplayName)
}) })
@ -165,7 +165,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
guard let self else { return } guard let self else { return }
self.suggestionItems = commands.map { command in 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 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. /// Returns the array with an additional member that represents an `@room` mention.
func withRoom(_ canMentionRoom: Bool) -> Self { func withRoom(_ canMentionRoom: Bool) -> Self {
guard canMentionRoom else { return 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 Foundation
import WysiwygComposer import WysiwygComposer
protocol UserSuggestionItemProtocol: Avatarable { protocol CompletionSuggestionUserItemProtocol: Avatarable {
var userId: String { get } var userId: String { get }
var displayName: String? { get } var displayName: String? { get }
var avatarUrl: String? { get } var avatarUrl: String? { get }
} }
protocol CommandSuggestionItemProtocol { protocol CompletionSuggestionCommandItemProtocol {
var name: String { get } var name: String { get }
} }
enum SuggestionItem { enum CompletionSuggestionItem {
case command(value: CommandSuggestionItemProtocol) case command(value: CompletionSuggestionCommandItemProtocol)
case user(value: UserSuggestionItemProtocol) case user(value: CompletionSuggestionUserItemProtocol)
} }
protocol UserSuggestionServiceProtocol { protocol CompletionSuggestionServiceProtocol {
var items: CurrentValueSubject<[SuggestionItem], Never> { get } var items: CurrentValueSubject<[CompletionSuggestionItem], Never> { get }
var currentTextTrigger: String? { get } var currentTextTrigger: String? { get }
@ -44,7 +44,7 @@ protocol UserSuggestionServiceProtocol {
// MARK: Avatarable // MARK: Avatarable
extension UserSuggestionItemProtocol { extension CompletionSuggestionUserItemProtocol {
var mxContentUri: String? { var mxContentUri: String? {
avatarUrl avatarUrl
} }

View file

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

View file

@ -19,51 +19,53 @@ import XCTest
@testable import RiotSwiftUI @testable import RiotSwiftUI
class UserSuggestionServiceTests: XCTestCase { class CompletionSuggestionServiceTests: XCTestCase {
var service: UserSuggestionService! var service: CompletionSuggestionService!
var canMentionRoom = false var canMentionRoom = false
override func setUp() { override func setUp() {
service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false) service = CompletionSuggestionService(roomMemberProvider: self,
commandProvider: self,
shouldDebounce: false)
canMentionRoom = false canMentionRoom = false
} }
func testAlice() { func testAlice() {
service.processTextMessage("@Al") service.processTextMessage("@Al")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@al") service.processTextMessage("@al")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@ice") service.processTextMessage("@ice")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@Alice") service.processTextMessage("@Alice")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
service.processTextMessage("@alice:matrix.org") service.processTextMessage("@alice:matrix.org")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
} }
func testBob() { func testBob() {
service.processTextMessage("@ob") service.processTextMessage("@ob")
XCTAssertEqual(service.items.value.first?.displayName, "Bob") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob")
service.processTextMessage("@ob:") service.processTextMessage("@ob:")
XCTAssertEqual(service.items.value.first?.displayName, "Bob") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob")
service.processTextMessage("@b:matrix") service.processTextMessage("@b:matrix")
XCTAssertEqual(service.items.value.first?.displayName, "Bob") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Bob")
} }
func testBoth() { func testBoth() {
service.processTextMessage("@:matrix") service.processTextMessage("@:matrix")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
XCTAssertEqual(service.items.value.last?.displayName, "Bob") XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob")
service.processTextMessage("@.org") service.processTextMessage("@.org")
XCTAssertEqual(service.items.value.first?.displayName, "Alice") XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
XCTAssertEqual(service.items.value.last?.displayName, "Bob") XCTAssertEqual(service.items.value.last?.asUser?.displayName, "Bob")
} }
func testEmptyResult() { func testEmptyResult() {
@ -117,18 +119,18 @@ class UserSuggestionServiceTests: XCTestCase {
} }
func testRoomWithPower() { func testRoomWithPower() {
// Given a user without the power to mention a room. // Given a user with the power to mention a room.
canMentionRoom = true 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") service.processTextMessage("@ro")
// Then the completion for a room mention should be shown. // 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) { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
let users = [("Alice", "@alice:matrix.org"), let users = [("Alice", "@alice:matrix.org"),
("Bob", "@bob: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 import SwiftUI
struct UserSuggestionList: View { struct CompletionSuggestionList: View {
private enum Constants { private enum Constants {
static let topPadding: CGFloat = 8.0 static let topPadding: CGFloat = 8.0
static let listItemPadding: CGFloat = 4.0 static let listItemPadding: CGFloat = 4.0
@ -43,7 +43,7 @@ struct UserSuggestionList: View {
// MARK: Public // MARK: Public
@ObservedObject var viewModel: UserSuggestionViewModel.Context @ObservedObject var viewModel: CompletionSuggestionViewModel.Context
var showBackgroundShadow: Bool = true var showBackgroundShadow: Bool = true
var body: some View { var body: some View {
@ -51,7 +51,7 @@ struct UserSuggestionList: View {
EmptyView() EmptyView()
} else { } else {
ZStack { ZStack {
UserSuggestionListItem(content: UserSuggestionViewStateItem.user( CompletionSuggestionListItem(content: CompletionSuggestionViewStateItem.user(
id: "Prototype", id: "Prototype",
avatar: AvatarInput(mxContentUri: "", avatar: AvatarInput(mxContentUri: "",
matrixItemId: "", matrixItemId: "",
@ -79,7 +79,7 @@ struct UserSuggestionList: View {
Button { Button {
viewModel.send(viewAction: .selectedItem(item)) viewModel.send(viewAction: .selectedItem(item))
} label: { } label: {
UserSuggestionListItem(content: item) CompletionSuggestionListItem(content: item)
.modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id)) .modifier(ListItemPaddingModifier(isFirst: viewModel.viewState.items.first?.id == item.id))
} }
} }
@ -134,8 +134,8 @@ private struct BackgroundView<Content: View>: View {
// MARK: - Previews // MARK: - Previews
struct UserSuggestion_Previews: PreviewProvider { struct CompletionSuggestion_Previews: PreviewProvider {
static let stateRenderer = MockUserSuggestionScreenState.stateRenderer static let stateRenderer = MockCompletionSuggestionScreenState.stateRenderer
static var previews: some View { static var previews: some View {
stateRenderer.screenGroup() stateRenderer.screenGroup()
} }

View file

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

View file

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

View file

@ -29,7 +29,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) { var screenView: ([Any], AnyView) {
let viewModel: ComposerViewModel let viewModel: ComposerViewModel
let userSuggestionViewModel = MockUserSuggestionViewModel(initialViewState: UserSuggestionViewState(items: [])) let completionSuggestionViewModel = MockCompletionSuggestionViewModel(initialViewState: CompletionSuggestionViewState(items: []))
let bindings = ComposerBindings(focused: false) let bindings = ComposerBindings(focused: false)
switch self { switch self {
@ -67,7 +67,7 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
Spacer() Spacer()
Composer(viewModel: viewModel.context, Composer(viewModel: viewModel.context,
wysiwygViewModel: wysiwygviewModel, wysiwygViewModel: wysiwygviewModel,
userSuggestionSharedContext: userSuggestionViewModel.context, completionSuggestionSharedContext: completionSuggestionViewModel.context,
resizeAnimationDuration: 0.1, resizeAnimationDuration: 0.1,
sendMessageAction: { _ in }, sendMessageAction: { _ in },
showSendMediaActions: { }) 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 { final class CompletionSuggestionViewModelWrapper: NSObject {
let userSuggestionViewModel: UserSuggestionViewModel let completionSuggestionViewModel: CompletionSuggestionViewModel
init(_ userSuggestionViewModel: UserSuggestionViewModel) { init(_ completionSuggestionViewModel: CompletionSuggestionViewModel) {
self.userSuggestionViewModel = userSuggestionViewModel self.completionSuggestionViewModel = completionSuggestionViewModel
super.init() super.init()
} }
} }

View file

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