mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-27 19:56:57 +02:00
Merge branch 'develop' into steve/4734_room_stack
This commit is contained in:
@@ -16,7 +16,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-11
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -16,7 +16,7 @@ env:
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-11
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
## Changes in 1.6.5 (2021-10-14)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Upgrade MatrixKit version ([v0.16.7](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.7)).
|
||||
|
||||
|
||||
## Changes in 1.6.4 (2021-10-12)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 1.6.5
|
||||
CURRENT_PROJECT_VERSION = 1.6.5
|
||||
MARKETING_VERSION = 1.6.6
|
||||
CURRENT_PROJECT_VERSION = 1.6.6
|
||||
|
||||
@@ -13,7 +13,7 @@ use_frameworks!
|
||||
# - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI
|
||||
#
|
||||
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
|
||||
$matrixKitVersion = '= 0.16.6'
|
||||
$matrixKitVersion = '= 0.16.7'
|
||||
# $matrixKitVersion = :local
|
||||
# $matrixKitVersion = {'develop' => 'develop'}
|
||||
|
||||
|
||||
+13
-13
@@ -58,29 +58,29 @@ PODS:
|
||||
- MatomoTracker (7.4.1):
|
||||
- MatomoTracker/Core (= 7.4.1)
|
||||
- MatomoTracker/Core (7.4.1)
|
||||
- MatrixKit (0.16.6):
|
||||
- MatrixKit (0.16.7):
|
||||
- Down (~> 0.11.0)
|
||||
- DTCoreText (~> 1.6.25)
|
||||
- HPGrowingTextView (~> 1.1)
|
||||
- libPhoneNumber-iOS (~> 0.9.13)
|
||||
- MatrixKit/Core (= 0.16.6)
|
||||
- MatrixSDK (= 0.20.6)
|
||||
- MatrixKit/Core (0.16.6):
|
||||
- MatrixKit/Core (= 0.16.7)
|
||||
- MatrixSDK (= 0.20.7)
|
||||
- MatrixKit/Core (0.16.7):
|
||||
- Down (~> 0.11.0)
|
||||
- DTCoreText (~> 1.6.25)
|
||||
- HPGrowingTextView (~> 1.1)
|
||||
- libPhoneNumber-iOS (~> 0.9.13)
|
||||
- MatrixSDK (= 0.20.6)
|
||||
- MatrixSDK (0.20.6):
|
||||
- MatrixSDK/Core (= 0.20.6)
|
||||
- MatrixSDK/Core (0.20.6):
|
||||
- MatrixSDK (= 0.20.7)
|
||||
- MatrixSDK (0.20.7):
|
||||
- MatrixSDK/Core (= 0.20.7)
|
||||
- MatrixSDK/Core (0.20.7):
|
||||
- AFNetworking (~> 4.0.0)
|
||||
- GZIP (~> 1.3.0)
|
||||
- libbase58 (~> 0.1.4)
|
||||
- OLMKit (~> 3.2.5)
|
||||
- Realm (= 10.16.0)
|
||||
- SwiftyBeaver (= 1.9.5)
|
||||
- MatrixSDK/JingleCallStack (0.20.6):
|
||||
- MatrixSDK/JingleCallStack (0.20.7):
|
||||
- JitsiMeetSDK (= 3.10.2)
|
||||
- MatrixSDK/Core
|
||||
- OLMKit (3.2.5):
|
||||
@@ -124,7 +124,7 @@ DEPENDENCIES:
|
||||
- KeychainAccess (~> 4.2.2)
|
||||
- KTCenterFlowLayout (~> 1.3.1)
|
||||
- MatomoTracker (~> 7.4.1)
|
||||
- MatrixKit (= 0.16.6)
|
||||
- MatrixKit (= 0.16.7)
|
||||
- MatrixSDK
|
||||
- MatrixSDK/JingleCallStack
|
||||
- OLMKit
|
||||
@@ -204,8 +204,8 @@ SPEC CHECKSUMS:
|
||||
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||
MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb
|
||||
MatrixKit: 38f18f930af238abe44189909b16953e657d0ffc
|
||||
MatrixSDK: 37d6bc484fa90c39db46ed25fd3a4c707bb70452
|
||||
MatrixKit: d0346f60c7d0723066f6a3e94ebee789edc1f580
|
||||
MatrixSDK: 1d7a64d1e25f746e35157a68374b4282b5581188
|
||||
OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5
|
||||
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
|
||||
Realm: b6027801398f3743fc222f096faa85281b506e6c
|
||||
@@ -219,6 +219,6 @@ SPEC CHECKSUMS:
|
||||
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: c05591899cb3d6d02e404d4b48a02a1e6187d6bd
|
||||
PODFILE CHECKSUM: 3c829592a4e938c0248c7eb66e1aa9c4493b2334
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
||||
@@ -225,6 +225,7 @@ internal struct ImageAsset {
|
||||
internal typealias Image = UIImage
|
||||
#endif
|
||||
|
||||
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
|
||||
internal var image: Image {
|
||||
let bundle = BundleToken.bundle
|
||||
#if os(iOS) || os(tvOS)
|
||||
@@ -236,13 +237,25 @@ internal struct ImageAsset {
|
||||
let image = Image(named: name)
|
||||
#endif
|
||||
guard let result = image else {
|
||||
fatalError("Unable to load image named \(name).")
|
||||
fatalError("Unable to load image asset named \(name).")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
@available(iOS 8.0, tvOS 9.0, *)
|
||||
internal func image(compatibleWith traitCollection: UITraitCollection) -> Image {
|
||||
let bundle = BundleToken.bundle
|
||||
guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
|
||||
fatalError("Unable to load image asset named \(name).")
|
||||
}
|
||||
return result
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal extension ImageAsset.Image {
|
||||
@available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
|
||||
@available(macOS, deprecated,
|
||||
message: "This initializer is unsafe on macOS, please use the ImageAsset.image property")
|
||||
convenience init!(asset: ImageAsset) {
|
||||
|
||||
@@ -41,10 +41,22 @@ class VectorHostingController: UIHostingController<AnyView> {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = .clear
|
||||
|
||||
self.registerThemeServiceDidChangeThemeNotification()
|
||||
self.update(theme: self.theme)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// Fixes weird iOS 15 bug where the view no longer grows its enclosing host
|
||||
if #available(iOS 15.0, *) {
|
||||
self.view.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func registerThemeServiceDidChangeThemeNotification() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
|
||||
}
|
||||
@@ -54,8 +66,6 @@ class VectorHostingController: UIHostingController<AnyView> {
|
||||
}
|
||||
|
||||
private func update(theme: Theme) {
|
||||
self.view.backgroundColor = theme.headerBackgroundColor
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar {
|
||||
theme.applyStyle(onNavigationBar: navigationBar)
|
||||
}
|
||||
|
||||
@@ -377,6 +377,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
urlPreviewView = [URLPreviewView instantiate];
|
||||
urlPreviewView.preview = component.urlPreviewData;
|
||||
urlPreviewView.delegate = self;
|
||||
urlPreviewView.tag = index;
|
||||
|
||||
[temporaryViews addObject:urlPreviewView];
|
||||
|
||||
@@ -416,6 +417,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
|
||||
reactionsView = [BubbleReactionsView new];
|
||||
reactionsView.viewModel = bubbleReactionsViewModel;
|
||||
reactionsView.tag = index;
|
||||
[reactionsView updateWithTheme:ThemeService.shared.theme];
|
||||
|
||||
bubbleReactionsViewModel.viewModelDelegate = self;
|
||||
|
||||
@@ -137,7 +137,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, RoomParticipantsViewControllerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
|
||||
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
|
||||
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
|
||||
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate>
|
||||
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate>
|
||||
{
|
||||
|
||||
// The preview header
|
||||
@@ -249,6 +249,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
@property (nonatomic, strong) VoiceMessageController *voiceMessageController;
|
||||
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;
|
||||
|
||||
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator;
|
||||
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RoomViewController
|
||||
@@ -452,6 +455,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[self userInterfaceThemeDidChange];
|
||||
|
||||
}];
|
||||
|
||||
[self userInterfaceThemeDidChange];
|
||||
|
||||
// Observe URL preview updates.
|
||||
@@ -1019,6 +1023,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
|
||||
_voiceMessageController.roomId = dataSource.roomId;
|
||||
|
||||
_userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
|
||||
room:dataSource.room];
|
||||
_userSuggestionCoordinator.delegate = self;
|
||||
|
||||
[self setupUserSuggestionView];
|
||||
}
|
||||
|
||||
- (void)onRoomDataSourceReady
|
||||
@@ -2212,6 +2222,27 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupUserSuggestionView
|
||||
{
|
||||
if(!self.isViewLoaded) {
|
||||
MXLogError(@"Failed setting up user suggestions. View not loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable;
|
||||
[suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
|
||||
[self addChildViewController:suggestionsViewController];
|
||||
[self.userSuggestionContainerView 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],]];
|
||||
|
||||
[suggestionsViewController didMoveToParentViewController:self];
|
||||
}
|
||||
|
||||
#pragma mark - Jitsi
|
||||
|
||||
- (void)showJitsiCallWithWidget:(Widget*)widget
|
||||
@@ -4200,6 +4231,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
{
|
||||
[self cancelEventSelection];
|
||||
}
|
||||
|
||||
- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView *)toolbarView
|
||||
{
|
||||
[self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage];
|
||||
}
|
||||
|
||||
#pragma mark - MXKRoomMemberDetailsViewControllerDelegate
|
||||
|
||||
@@ -6517,4 +6553,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
|
||||
}
|
||||
|
||||
#pragma mark - UserSuggestionCoordinatorBridgeDelegate
|
||||
|
||||
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator
|
||||
didRequestMentionForMember:(MXRoomMember *)member
|
||||
textTrigger:(NSString *)textTrigger
|
||||
{
|
||||
if (textTrigger.length) {
|
||||
NSString *textMessage = [self.inputToolbarView textMessage];
|
||||
textMessage = [textMessage stringByReplacingOccurrencesOfString:textTrigger
|
||||
withString:@""
|
||||
options:NSBackwardsSearch | NSAnchoredSearch
|
||||
range:NSMakeRange(0, textMessage.length)];
|
||||
[self.inputToolbarView setTextMessage:textMessage];
|
||||
}
|
||||
|
||||
[self mention:member];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" 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="17703"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<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"/>
|
||||
@@ -32,6 +32,7 @@
|
||||
<outlet property="roomInputToolbarContainerHeightConstraint" destination="5eD-Fm-RDb" id="6ny-5w-1UA"/>
|
||||
<outlet property="scrollToBottomBadgeLabel" destination="QHs-rM-UU8" id="wk7-PQ-9Jm"/>
|
||||
<outlet property="scrollToBottomButton" destination="Ih9-EU-BOU" id="Wwg-gS-Sfp"/>
|
||||
<outlet property="userSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/>
|
||||
<outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
@@ -136,14 +137,14 @@
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</stackView>
|
||||
<button opaque="NO" alpha="0.0" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ih9-EU-BOU" userLabel="scroll Button">
|
||||
<rect key="frame" x="327" y="570" width="32" height="32"/>
|
||||
<rect key="frame" x="321" y="564" width="38" height="38"/>
|
||||
<state key="normal" image="scrolldown"/>
|
||||
<connections>
|
||||
<action selector="scrollToBottomAction:" destination="-1" eventType="touchUpInside" id="TOf-aY-J6a"/>
|
||||
</connections>
|
||||
</button>
|
||||
<label opaque="NO" userInteractionEnabled="NO" alpha="0.0" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QHs-rM-UU8" userLabel="scroll badge" customClass="BadgeLabel" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="334.5" y="562" width="17.5" height="16.5"/>
|
||||
<rect key="frame" x="336.5" y="557.5" width="7.5" height="13.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="11"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -160,6 +161,13 @@
|
||||
<constraint firstAttribute="height" id="E8v-l2-8eV"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oni-F4-X1U" userLabel="User suggestion container">
|
||||
<rect key="frame" x="0.0" y="626" width="375" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" placeholder="YES" id="1Cd-cT-gOr"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nLd-BP-JAE" userLabel="Room Input Toolbar Container">
|
||||
<rect key="frame" x="0.0" y="626" width="375" height="41"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
@@ -177,6 +185,7 @@
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="RoomVCView"/>
|
||||
<constraints>
|
||||
<constraint firstItem="QpJ-1u-4ii" firstAttribute="trailing" secondItem="oni-F4-X1U" secondAttribute="trailing" id="0Bn-nV-E8U"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BGD-sd-SQR" secondAttribute="bottom" constant="41" id="1SD-y2-oTg"/>
|
||||
<constraint firstItem="Xt7-83-dQh" firstAttribute="leading" secondItem="QpJ-1u-4ii" secondAttribute="leading" id="6lr-Tx-pEb"/>
|
||||
<constraint firstItem="QpJ-1u-4ii" firstAttribute="trailing" secondItem="Ih9-EU-BOU" secondAttribute="trailing" constant="16" id="6rq-lR-0sB"/>
|
||||
@@ -198,7 +207,10 @@
|
||||
<constraint firstItem="gt1-EO-UVY" firstAttribute="top" secondItem="QpJ-1u-4ii" secondAttribute="top" id="YfN-0Z-0bc"/>
|
||||
<constraint firstItem="54r-18-K1g" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="aR5-rp-1Cp"/>
|
||||
<constraint firstItem="gt1-EO-UVY" firstAttribute="bottom" secondItem="nLd-BP-JAE" secondAttribute="bottom" id="acJ-g8-R7x"/>
|
||||
<constraint firstItem="nLd-BP-JAE" firstAttribute="top" secondItem="oni-F4-X1U" secondAttribute="bottom" id="ave-fu-X1D"/>
|
||||
<constraint firstItem="BGD-sd-SQR" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="bFw-dg-qEr"/>
|
||||
<constraint firstItem="oni-F4-X1U" firstAttribute="top" relation="greaterThanOrEqual" secondItem="QpJ-1u-4ii" secondAttribute="top" id="hzP-Ee-xzI"/>
|
||||
<constraint firstItem="oni-F4-X1U" firstAttribute="leading" secondItem="QpJ-1u-4ii" secondAttribute="leading" id="k83-dd-hdL"/>
|
||||
<constraint firstAttribute="bottom" secondItem="nLd-BP-JAE" secondAttribute="bottom" id="omU-sm-3bK"/>
|
||||
<constraint firstItem="nLd-BP-JAE" firstAttribute="trailing" secondItem="QpJ-1u-4ii" secondAttribute="trailing" id="pRw-S0-6WL"/>
|
||||
<constraint firstItem="fmF-ad-erE" firstAttribute="top" secondItem="QpJ-1u-4ii" secondAttribute="top" id="qYq-nb-cu5"/>
|
||||
@@ -212,13 +224,13 @@
|
||||
</objects>
|
||||
<designables>
|
||||
<designable name="QHs-rM-UU8">
|
||||
<size key="intrinsicContentSize" width="17.5" height="16.5"/>
|
||||
<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"/>
|
||||
<image name="scrolldown" width="32" height="32"/>
|
||||
<image name="scrolldown" width="38" height="38"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
|
||||
@@ -40,6 +40,13 @@ typedef enum : NSUInteger
|
||||
*/
|
||||
- (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView*)toolbarView;
|
||||
|
||||
/**
|
||||
Inform the delegate that the text message has changed.
|
||||
|
||||
@param toolbarView the room input toolbar view
|
||||
*/
|
||||
- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView*)toolbarView;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
|
||||
@@ -157,6 +157,7 @@ const CGFloat kComposerContainerTrailingPadding = 12;
|
||||
|
||||
self.textView.text = textMessage;
|
||||
[self updateUIWithTextMessage:textMessage animated:YES];
|
||||
[self textViewDidChange:self.textView];
|
||||
}
|
||||
|
||||
- (NSString *)textMessage
|
||||
@@ -324,7 +325,7 @@ const CGFloat kComposerContainerTrailingPadding = 12;
|
||||
{
|
||||
NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text];
|
||||
[self updateUIWithTextMessage:newText animated:YES];
|
||||
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -340,6 +341,8 @@ const CGFloat kComposerContainerTrailingPadding = 12;
|
||||
{
|
||||
[self.delegate roomInputToolbarView:self isTyping:(self.textMessage.length > 0 ? YES : NO)];
|
||||
}
|
||||
|
||||
[self.delegate roomInputToolbarViewDidChangeTextMessage:self];
|
||||
}
|
||||
|
||||
- (void)textViewDidChangeHeight:(GrowingTextView *)textView height:(CGFloat)height
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@
|
||||
if (attrs[NSForegroundColorAttributeName])
|
||||
{
|
||||
UIColor *color = attrs[NSForegroundColorAttributeName];
|
||||
color = [color colorWithAlphaComponent:0.2];
|
||||
color = [color colorWithAlphaComponent:alpha];
|
||||
|
||||
NSMutableDictionary *newAttrs = [NSMutableDictionary dictionaryWithDictionary:attrs];
|
||||
newAttrs[NSForegroundColorAttributeName] = color;
|
||||
|
||||
@@ -47,7 +47,10 @@ class AvatarViewModel: InjectableObject, ObservableObject {
|
||||
stableColorIndex(matrixItemId: matrixItemId, colorCount: colorCount)
|
||||
)
|
||||
|
||||
guard let mxContentUri = mxContentUri else { return }
|
||||
guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize)
|
||||
.sink { completion in
|
||||
guard case let .failure(error) = completion else { return }
|
||||
|
||||
@@ -22,7 +22,8 @@ enum MockAppScreens {
|
||||
static let appScreens: [MockScreenState.Type] = [
|
||||
MockTemplateUserProfileScreenState.self,
|
||||
MockTemplateRoomListScreenState.self,
|
||||
MockTemplateRoomChatScreenState.self
|
||||
MockTemplateRoomChatScreenState.self,
|
||||
MockUserSuggestionScreenState.self
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// 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
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct RoundedCornerShape: Shape {
|
||||
|
||||
let radius: CGFloat
|
||||
let corners: UIRectCorner
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ class ViewModelContext<ViewState:BindableState, ViewAction>: ObservableObject {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
fileprivate let viewActions: PassthroughSubject<ViewAction, Never>
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
/*
|
||||
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
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol UserSuggestionCoordinatorDelegate: AnyObject {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class UserSuggestionCoordinator: Coordinator {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: UserSuggestionCoordinatorParameters
|
||||
|
||||
private var userSuggestionHostingController: UIViewController!
|
||||
private var userSuggestionService: UserSuggestionServiceProtocol!
|
||||
private var userSuggestionViewModel: UserSuggestionViewModelProtocol!
|
||||
|
||||
private var roomMembers: [MXRoomMember] = []
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: (() -> Void)?
|
||||
|
||||
weak var delegate: UserSuggestionCoordinatorDelegate?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: UserSuggestionCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
userSuggestionService = UserSuggestionService(roomMembersProvider: self)
|
||||
userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
|
||||
let view = UserSuggestionList(viewModel: userSuggestionViewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
|
||||
|
||||
userSuggestionHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
userSuggestionViewModel.completion = { [weak self] result in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .selectedItemWithIdentifier(let identifier):
|
||||
guard let member = self.roomMembers.filter({ $0.userId == identifier }).first else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processTextMessage(_ textMessage: String) {
|
||||
userSuggestionService.processTextMessage(textMessage)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.userSuggestionHostingController
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UserSuggestionCoordinator: RoomMembersProviderProtocol {
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
|
||||
parameters.room.members({ [weak self] roomMembers in
|
||||
guard let self = self, let joinedMembers = roomMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
self.roomMembers = joinedMembers
|
||||
members(self.roomMembersToProviderMembers(joinedMembers))
|
||||
}, lazyLoadedMembers: { [weak self] lazyRoomMembers in
|
||||
guard let self = self, let joinedMembers = lazyRoomMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
self.roomMembers = joinedMembers
|
||||
members(self.roomMembersToProviderMembers(joinedMembers))
|
||||
}, failure: { error in
|
||||
MXLog.error("[UserSuggestionCoordinator] Failed loading room with error: \(String(describing: error))")
|
||||
})
|
||||
}
|
||||
|
||||
private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] {
|
||||
roomMembers.map { RoomMembersProviderMember(userId: $0.userId, displayName: $0.displayname ?? "", avatarUrl: $0.avatarUrl ?? "") }
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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?)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
|
||||
private var _userSuggestionCoordinator: Any? = nil
|
||||
@available(iOS 14.0, *)
|
||||
fileprivate var userSuggestionCoordinator: UserSuggestionCoordinator {
|
||||
return _userSuggestionCoordinator as! UserSuggestionCoordinator
|
||||
}
|
||||
|
||||
weak var delegate: UserSuggestionCoordinatorBridgeDelegate?
|
||||
|
||||
init(mediaManager: MXMediaManager, room: MXRoom) {
|
||||
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room)
|
||||
if #available(iOS 14.0, *) {
|
||||
let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters)
|
||||
self._userSuggestionCoordinator = userSuggestionCoordinator
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
if #available(iOS 14.0, *) {
|
||||
userSuggestionCoordinator.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
func processTextMessage(_ textMessage: String) {
|
||||
if #available(iOS 14.0, *) {
|
||||
return self.userSuggestionCoordinator.processTextMessage(textMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController? {
|
||||
if #available(iOS 14.0, *) {
|
||||
return self.userSuggestionCoordinator.toPresentable()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) {
|
||||
delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
|
||||
struct UserSuggestionCoordinatorParameters {
|
||||
let mediaManager: MXMediaManager
|
||||
let room: MXRoom
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
enum UserSuggestionStateAction {
|
||||
case updateWithItems([UserSuggestionItemProtocol])
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
|
||||
@available(iOS 14, *)
|
||||
enum UserSuggestionViewAction {
|
||||
case selectedItem(UserSuggestionViewStateItem)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
|
||||
@available(iOS 14, *)
|
||||
enum UserSuggestionViewModelResult {
|
||||
case selectedItemWithIdentifier(String)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionViewStateItem: Identifiable {
|
||||
let id: String
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionViewState: BindableState {
|
||||
var items: [UserSuggestionViewStateItem]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
|
||||
case multipleResults
|
||||
|
||||
static private var members: [RoomMembersProviderMember]!
|
||||
|
||||
var screenType: Any.Type {
|
||||
MockUserSuggestionScreenState.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let service = UserSuggestionService(roomMembersProvider: self)
|
||||
let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service)
|
||||
|
||||
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
|
||||
service.processTextMessage(textMessage)
|
||||
}
|
||||
|
||||
return (
|
||||
[service, listViewModel],
|
||||
AnyView(UserSuggestionListWithInput(viewModel: viewModel)
|
||||
.addDependency(MockAvatarService.example))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension MockUserSuggestionScreenState: RoomMembersProviderProtocol {
|
||||
func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) {
|
||||
if Self.members == nil {
|
||||
Self.members = generateUsersWithCount(10)
|
||||
}
|
||||
|
||||
members(Self.members)
|
||||
}
|
||||
|
||||
private func generateUsersWithCount(_ count: UInt) -> [RoomMembersProviderMember] {
|
||||
return (0..<count).map { _ in
|
||||
let identifier = "@" + UUID().uuidString
|
||||
return RoomMembersProviderMember(userId: identifier, displayName: identifier, avatarUrl: "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct RoomMembersProviderMember {
|
||||
var userId: String
|
||||
var displayName: String
|
||||
var avatarUrl: String
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol RoomMembersProviderProtocol {
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionServiceItem: UserSuggestionItemProtocol {
|
||||
let userId: String
|
||||
let displayName: String?
|
||||
let avatarUrl: String?
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let roomMembersProvider: RoomMembersProviderProtocol
|
||||
|
||||
private var suggestionItems: [UserSuggestionItemProtocol] = []
|
||||
private let currentTextTriggerSubject = CurrentValueSubject<String?, Never>(nil)
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var items = CurrentValueSubject<[UserSuggestionItemProtocol], Never>([])
|
||||
|
||||
var currentTextTrigger: String? {
|
||||
currentTextTriggerSubject.value
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(roomMembersProvider: RoomMembersProviderProtocol) {
|
||||
self.roomMembersProvider = roomMembersProvider
|
||||
|
||||
currentTextTriggerSubject
|
||||
.debounce(for: 0.5, scheduler: RunLoop.main)
|
||||
.removeDuplicates()
|
||||
.sink { self.fetchAndFilterMembersForTextTrigger($0) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - UserSuggestionServiceProtocol
|
||||
|
||||
func processTextMessage(_ textMessage: String?) {
|
||||
guard let textMessage = textMessage,
|
||||
textMessage.count > 0,
|
||||
let lastComponent = textMessage.components(separatedBy: .whitespaces).last,
|
||||
lastComponent.prefix(while: { $0 == "@" }).count == 1 // Partial username should start with one and only one "@" character
|
||||
else {
|
||||
self.items.send([])
|
||||
self.currentTextTriggerSubject.send(nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.currentTextTriggerSubject.send(lastComponent)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func fetchAndFilterMembersForTextTrigger(_ textTrigger: String?) {
|
||||
guard var partialName = textTrigger else {
|
||||
return
|
||||
}
|
||||
|
||||
partialName.removeFirst() // remove the '@' prefix
|
||||
|
||||
roomMembersProvider.fetchMembers { [weak self] members in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.suggestionItems = members.map { member in
|
||||
UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)
|
||||
}
|
||||
|
||||
self.items.send(self.suggestionItems.filter({ userSuggestion in
|
||||
let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased())
|
||||
let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased())
|
||||
|
||||
return (containedInUsername || containedInDisplayName)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol UserSuggestionItemProtocol: Avatarable {
|
||||
var userId: String { get }
|
||||
var displayName: String? { get }
|
||||
var avatarUrl: String? { get }
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol UserSuggestionServiceProtocol {
|
||||
|
||||
var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get }
|
||||
|
||||
var currentTextTrigger: String? { get }
|
||||
|
||||
func processTextMessage(_ textMessage: String?)
|
||||
}
|
||||
|
||||
// MARK: Avatarable
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UserSuggestionItemProtocol {
|
||||
var mxContentUri: String? {
|
||||
avatarUrl
|
||||
}
|
||||
var matrixItemId: String {
|
||||
userId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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 XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class UserSuggestionUITests: MockScreenTest {
|
||||
|
||||
override class var screenType: MockScreenState.Type {
|
||||
return MockUserSuggestionScreenState.self
|
||||
}
|
||||
|
||||
override class func createTest() -> MockScreenTest {
|
||||
return UserSuggestionUITests(selector: #selector(verifyUserSuggestionScreen))
|
||||
}
|
||||
|
||||
func verifyUserSuggestionScreen() throws {
|
||||
XCTAssert(app.tables.firstMatch.exists)
|
||||
|
||||
let firstButton = app.tables.firstMatch.buttons.firstMatch
|
||||
_ = firstButton.waitForExistence(timeout: 10)
|
||||
XCTAssert(firstButton.identifier == "displayNameText-userIdText")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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 XCTest
|
||||
import Combine
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class UserSuggestionServiceTests: XCTestCase {
|
||||
|
||||
var service: UserSuggestionService?
|
||||
|
||||
override func setUp() {
|
||||
service = UserSuggestionService(roomMembersProvider: self)
|
||||
}
|
||||
|
||||
func testAlice() {
|
||||
service?.processTextMessage("@Al")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
|
||||
service?.processTextMessage("@al")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
|
||||
service?.processTextMessage("@ice")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
|
||||
service?.processTextMessage("@Alice")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
|
||||
service?.processTextMessage("@alice:matrix.org")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
}
|
||||
|
||||
func testBob() {
|
||||
service?.processTextMessage("@ob")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
|
||||
service?.processTextMessage("@ob:")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
|
||||
service?.processTextMessage("@b:matrix")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
}
|
||||
|
||||
func testBoth() {
|
||||
service?.processTextMessage("@:matrix")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
assert(service?.items.value.last?.displayName == "Bob")
|
||||
|
||||
service?.processTextMessage("@.org")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
assert(service?.items.value.last?.displayName == "Bob")
|
||||
}
|
||||
|
||||
func testEmptyResult() {
|
||||
service?.processTextMessage("Lorem ipsum idolor")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage("@")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage("@@")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage("alice@matrix.org")
|
||||
assert(service?.items.value.count == 0)
|
||||
}
|
||||
|
||||
func testStuff() {
|
||||
service?.processTextMessage("@@")
|
||||
assert(service?.items.value.count == 0)
|
||||
}
|
||||
|
||||
func testWhitespaces() {
|
||||
service?.processTextMessage("")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage(" ")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage("\n")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage(" \n ")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage("@A ")
|
||||
assert(service?.items.value.count == 0)
|
||||
|
||||
service?.processTextMessage(" @A ")
|
||||
assert(service?.items.value.count == 0)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UserSuggestionServiceTests: RoomMembersProviderProtocol {
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
|
||||
|
||||
let users = [("Alice", "@alice:matrix.org"),
|
||||
("Bob", "@bob:matrix.org")]
|
||||
|
||||
members(users.map({ user in
|
||||
RoomMembersProviderMember(identifier: user.1, displayName: user.0, avatarURL: "")
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionList: View {
|
||||
private struct Constants {
|
||||
static let topPadding: CGFloat = 8.0
|
||||
static let listItemPadding: CGFloat = 4.0
|
||||
static let lineSpacing: CGFloat = 10.0
|
||||
static let maxHeight: CGFloat = 300.0
|
||||
static let maxVisibleRows = 4
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
@State private var prototypeListItemFrame: CGRect = .zero
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: UserSuggestionViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
if viewModel.viewState.items.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
ZStack {
|
||||
UserSuggestionListItem(avatar: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Prototype"),
|
||||
displayName: "Prototype",
|
||||
userId: "Prototype")
|
||||
.background(ViewFrameReader(frame: $prototypeListItemFrame))
|
||||
.hidden()
|
||||
BackgroundView {
|
||||
List(viewModel.viewState.items) { item in
|
||||
Button {
|
||||
viewModel.send(viewAction: .selectedItem(item))
|
||||
} label: {
|
||||
UserSuggestionListItem(
|
||||
avatar: item.avatar,
|
||||
displayName: item.displayName,
|
||||
userId: item.id
|
||||
)
|
||||
.padding(.bottom, Constants.listItemPadding)
|
||||
.padding(.top, (viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding))
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.frame(height: min(Constants.maxHeight,
|
||||
min(contentHeightForRowCount(Constants.maxVisibleRows),
|
||||
contentHeightForRowCount(viewModel.viewState.items.count))))
|
||||
.id(UUID()) // Rebuild the whole list on item changes. Fixes performance issues.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func contentHeightForRowCount(_ count: Int) -> CGFloat {
|
||||
(prototypeListItemFrame.height + (Constants.listItemPadding * 2) + Constants.lineSpacing) * CGFloat(count) + Constants.topPadding
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private struct BackgroundView<Content: View>: View {
|
||||
|
||||
var content: () -> Content
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
private let shadowRadius: CGFloat = 20.0
|
||||
|
||||
init(@ViewBuilder content: @escaping () -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content()
|
||||
.background(theme.colors.background)
|
||||
.clipShape(RoundedCornerShape(radius: shadowRadius, corners: [.topLeft, .topRight]))
|
||||
.shadow(color: .black.opacity(0.20), radius: 20.0, x: 0.0, y: 3.0)
|
||||
.mask(Rectangle().padding(.init(top: -(shadowRadius * 2), leading: 0.0, bottom: 0.0, trailing: 0.0)))
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestion_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockUserSuggestionScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionListItem: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
let userId: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let avatar = avatar {
|
||||
AvatarImage(avatarData: avatar, size: .medium)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(displayName ?? "")
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.accessibility(identifier: "displayNameText")
|
||||
.lineLimit(1)
|
||||
Text(userId)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.accessibility(identifier: "userIdText")
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionHeader_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org")
|
||||
.addDependency(MockAvatarService.example)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionListWithInputViewModel {
|
||||
let listViewModel: UserSuggestionViewModelProtocol
|
||||
let callback: (String)->()
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionListWithInput: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var viewModel: UserSuggestionListWithInputViewModel
|
||||
@State private var inputText: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0.0) {
|
||||
UserSuggestionList(viewModel: viewModel.listViewModel.context)
|
||||
TextField("Search for user", text: $inputText)
|
||||
.background(Color.white)
|
||||
.onChange(of: inputText, perform:viewModel.callback)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding([.leading, .trailing])
|
||||
.onAppear {
|
||||
inputText = "@-" // Make the list show all available mock results
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionListWithInput_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockUserSuggestionScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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 SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias UserSuggestionViewModelType = StateStoreViewModel <UserSuggestionViewState,
|
||||
UserSuggestionStateAction,
|
||||
UserSuggestionViewAction>
|
||||
@available(iOS 14, *)
|
||||
class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let userSuggestionService: UserSuggestionServiceProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol {
|
||||
return UserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
}
|
||||
|
||||
private init(userSuggestionService: UserSuggestionServiceProtocol) {
|
||||
self.userSuggestionService = userSuggestionService
|
||||
super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService))
|
||||
setupItemsObserving()
|
||||
}
|
||||
|
||||
private func setupItemsObserving() {
|
||||
let updatePublisher = userSuggestionService.items
|
||||
.map(UserSuggestionStateAction.updateWithItems)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: updatePublisher)
|
||||
}
|
||||
|
||||
private static func defaultState(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewState {
|
||||
let viewStateItems = userSuggestionService.items.value.map { suggestionItem in
|
||||
return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName)
|
||||
}
|
||||
|
||||
return UserSuggestionViewState(items: viewStateItems)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: UserSuggestionViewAction) {
|
||||
switch viewAction {
|
||||
case .selectedItem(let item):
|
||||
completion?(.selectedItemWithIdentifier(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout UserSuggestionViewState, action: UserSuggestionStateAction) {
|
||||
switch action {
|
||||
case .updateWithItems(let items):
|
||||
state.items = items.map({ item in
|
||||
UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// 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
|
||||
|
||||
@available(iOS 14, *)
|
||||
protocol UserSuggestionViewModelProtocol {
|
||||
|
||||
static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol
|
||||
|
||||
var context: UserSuggestionViewModelType.Context { get }
|
||||
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set }
|
||||
}
|
||||
-1
@@ -55,7 +55,6 @@ final class TemplateUserProfileCoordinator: Coordinator {
|
||||
switch result {
|
||||
case .cancel, .done:
|
||||
self.completion?()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,5 +17,5 @@
|
||||
import Foundation
|
||||
|
||||
struct TemplateUserProfileCoordinatorParameters {
|
||||
let session: MXSession
|
||||
let session: MXSession
|
||||
}
|
||||
-1
@@ -17,7 +17,6 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
@available(iOS 14.0, *)
|
||||
|
||||
@@ -10,6 +10,7 @@ schemes:
|
||||
targets:
|
||||
RiotSwiftUI:
|
||||
- running
|
||||
- testing
|
||||
- profiling
|
||||
- analyzing
|
||||
- archiving
|
||||
@@ -18,6 +19,11 @@ schemes:
|
||||
run:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
test:
|
||||
config: Debug
|
||||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotSwiftUnitTests
|
||||
|
||||
targets:
|
||||
RiotSwiftUI:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Timeline: Selecting a message now correctly selects any reactions and URL previews too.
|
||||
@@ -0,0 +1 @@
|
||||
Build: Update to Xcode 12.5 in the Fastfile and macOS 11 in the GitHub actions.
|
||||
Reference in New Issue
Block a user