Merge branch 'develop' into steve/4734_room_stack

This commit is contained in:
SBiOSoftWhare
2021-10-14 16:21:03 +02:00
41 changed files with 1235 additions and 36 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ env:
jobs:
build:
name: Build
runs-on: macos-latest
runs-on: macos-11
steps:
- uses: actions/checkout@v2
+1 -1
View File
@@ -16,7 +16,7 @@ env:
jobs:
tests:
name: Tests
runs-on: macos-latest
runs-on: macos-11
steps:
- uses: actions/checkout@v2
+7
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+14 -1
View File
@@ -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;
+55 -1
View File
@@ -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
+18 -6
View File
@@ -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
View File
@@ -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 ?? "") }
}
}
@@ -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)
}
}
@@ -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]
}
@@ -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)
})
}
}
}
@@ -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 }
}
@@ -55,7 +55,6 @@ final class TemplateUserProfileCoordinator: Coordinator {
switch result {
case .cancel, .done:
self.completion?()
break
}
}
}
@@ -17,5 +17,5 @@
import Foundation
struct TemplateUserProfileCoordinatorParameters {
let session: MXSession
let session: MXSession
}
@@ -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, *)
+6
View File
@@ -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:
+1
View File
@@ -0,0 +1 @@
Timeline: Selecting a message now correctly selects any reactions and URL previews too.
+1
View File
@@ -0,0 +1 @@
Build: Update to Xcode 12.5 in the Fastfile and macOS 11 in the GitHub actions.