Merge MatrixKit develop with commit hash: b85b736313bec0592bd1cabc68035d97f5331137

This commit is contained in:
SBiOSoftWhare
2021-12-03 11:47:24 +01:00
parent 475fe5552f
commit 1d9f01b5e3
475 changed files with 87437 additions and 0 deletions
@@ -0,0 +1,124 @@
/*
Copyright 2015 OpenMarket 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 <UIKit/UIKit.h>
#import "MXKTableViewController.h"
#import "MXKAccountManager.h"
#import "MXK3PID.h"
/**
*/
typedef void (^blockMXKAccountDetailsViewController_onReadyToLeave)(void);
/**
MXKAccountDetailsViewController instance may be used to display/edit the details of a matrix account.
Only one matrix session is handled by this view controller.
*/
@interface MXKAccountDetailsViewController : MXKTableViewController <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
{
@protected
/**
Section index
*/
NSInteger linkedEmailsSection;
NSInteger notificationsSection;
NSInteger configurationSection;
/**
The logout button
*/
UIButton *logoutButton;
/**
Linked email
*/
MXK3PID *submittedEmail;
UIButton *emailSubmitButton;
UITextField *emailTextField;
// Notifications
UISwitch *apnsNotificationsSwitch;
UISwitch *inAppNotificationsSwitch;
// The table cell with "Global Notification Settings" button
UIButton *notificationSettingsButton;
}
/**
The account displayed into the view controller.
*/
@property (nonatomic) MXKAccount *mxAccount;
/**
The default account picture displayed when no picture is defined.
*/
@property (nonatomic) UIImage *picturePlaceholder;
@property (nonatomic, readonly) IBOutlet UIButton *userPictureButton;
@property (nonatomic, readonly) IBOutlet UITextField *userDisplayName;
@property (nonatomic, readonly) IBOutlet UIButton *saveUserInfoButton;
@property (nonatomic, readonly) IBOutlet UIView *profileActivityIndicatorBgView;
@property (nonatomic, readonly) IBOutlet UIActivityIndicatorView *profileActivityIndicator;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKAccountDetailsViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `accountDetailsViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKAccountDetailsViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKAccountDetailsViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)accountDetailsViewController;
/**
Action registered on the following events:
- 'UIControlEventTouchUpInside' for each UIButton instance.
- 'UIControlEventValueChanged' for each UISwitch instance.
*/
- (IBAction)onButtonPressed:(id)sender;
/**
Action registered to handle text field edition
*/
- (IBAction)textFieldEditingChanged:(id)sender;
/**
Prompt user to save potential changes before leaving the view controller.
@param handler A block object called when the changes have been saved or discarded.
@return YES if no change is observed. NO when the user is prompted.
*/
- (BOOL)shouldLeave:(blockMXKAccountDetailsViewController_onReadyToLeave)handler;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKAccountDetailsViewController">
<connections>
<outlet property="profileActivityIndicator" destination="VAZ-Vd-dbs" id="Qia-8H-Opz"/>
<outlet property="profileActivityIndicatorBgView" destination="XNZ-KA-wLa" id="Ibb-Ru-b9S"/>
<outlet property="saveUserInfoButton" destination="Fcd-TW-6EN" id="iZF-7q-edr"/>
<outlet property="userDisplayName" destination="wTJ-d6-8LB" id="ney-e0-v1E"/>
<outlet property="userPictureButton" destination="Q5C-z0-PFC" id="inA-dL-Svu"/>
<outlet property="view" destination="ju7-p5-cp4" id="1a8-31-bad"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="ju7-p5-cp4">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="gTa-z3-gJI" userLabel="TableHeader">
<rect key="frame" x="0.0" y="0.0" width="375" height="115"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" clipsSubviews="YES" contentMode="scaleAspectFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q5C-z0-PFC" userLabel="PicturePickerButton">
<rect key="frame" x="10" y="10" width="75" height="75"/>
<color key="backgroundColor" red="0.89720267057418823" green="0.89720267057418823" blue="0.89720267057418823" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="75" id="h4a-sn-FGX"/>
<constraint firstAttribute="height" constant="75" id="u5z-VM-exL"/>
</constraints>
<state key="normal">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="ISw-2O-K3S"/>
</connections>
</button>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Display name" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="wTJ-d6-8LB">
<rect key="frame" x="95" y="23" width="270" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<action selector="textFieldEditingChanged:" destination="-1" eventType="editingChanged" id="ZaU-yY-z8l"/>
<outlet property="delegate" destination="-1" id="sGO-pB-sxI"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Fcd-TW-6EN">
<rect key="frame" x="95" y="73" width="270" height="30"/>
<state key="normal" title="Save changes">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="Krj-sL-BbO"/>
</connections>
</button>
<view hidden="YES" alpha="0.5" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XNZ-KA-wLa" userLabel="Spinner background view">
<rect key="frame" x="0.0" y="0.0" width="375" height="115"/>
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="VAZ-Vd-dbs">
<rect key="frame" x="169" y="39" width="37" height="37"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="wTJ-d6-8LB" firstAttribute="top" secondItem="gTa-z3-gJI" secondAttribute="top" constant="23" id="4Nx-Aq-DGZ"/>
<constraint firstItem="wTJ-d6-8LB" firstAttribute="trailing" secondItem="Fcd-TW-6EN" secondAttribute="trailing" id="9NO-pb-D26"/>
<constraint firstAttribute="centerY" secondItem="VAZ-Vd-dbs" secondAttribute="centerY" id="DnG-yj-mD2"/>
<constraint firstAttribute="trailing" secondItem="wTJ-d6-8LB" secondAttribute="trailing" constant="10" id="Ecb-G3-SzG"/>
<constraint firstItem="XNZ-KA-wLa" firstAttribute="top" secondItem="gTa-z3-gJI" secondAttribute="top" id="QMw-Un-h4D"/>
<constraint firstItem="Q5C-z0-PFC" firstAttribute="leading" secondItem="gTa-z3-gJI" secondAttribute="leading" constant="10" id="SBo-aU-k5D"/>
<constraint firstItem="Fcd-TW-6EN" firstAttribute="top" secondItem="wTJ-d6-8LB" secondAttribute="bottom" constant="20" id="SuW-K1-Ccy"/>
<constraint firstAttribute="centerX" secondItem="VAZ-Vd-dbs" secondAttribute="centerX" id="U9b-BV-8np"/>
<constraint firstItem="wTJ-d6-8LB" firstAttribute="leading" secondItem="Q5C-z0-PFC" secondAttribute="trailing" constant="10" id="hb2-WV-ygv"/>
<constraint firstItem="Q5C-z0-PFC" firstAttribute="top" secondItem="gTa-z3-gJI" secondAttribute="top" constant="10" id="mdK-nI-1Z6"/>
<constraint firstAttribute="trailing" secondItem="XNZ-KA-wLa" secondAttribute="trailing" id="pGA-4n-1Sl"/>
<constraint firstItem="XNZ-KA-wLa" firstAttribute="leading" secondItem="gTa-z3-gJI" secondAttribute="leading" id="sSy-en-QUN"/>
<constraint firstAttribute="bottom" secondItem="XNZ-KA-wLa" secondAttribute="bottom" id="ttc-fP-z4n"/>
<constraint firstItem="wTJ-d6-8LB" firstAttribute="leading" secondItem="Q5C-z0-PFC" secondAttribute="trailing" constant="10" id="tvA-Uj-PSe"/>
<constraint firstItem="wTJ-d6-8LB" firstAttribute="leading" secondItem="Fcd-TW-6EN" secondAttribute="leading" id="xag-mS-lrx"/>
</constraints>
</view>
<connections>
<outlet property="dataSource" destination="-1" id="KAH-DP-g22"/>
<outlet property="delegate" destination="-1" id="Uz1-g8-Ykk"/>
</connections>
</tableView>
</objects>
</document>
@@ -0,0 +1,26 @@
//
// Copyright 2020 The Matrix.org Foundation C.I.C
//
// 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 <UIKit/UIKit.h>
#import "MXKViewControllerActivityHandling.h"
NS_ASSUME_NONNULL_BEGIN
@interface MXKActivityHandlingViewController : UIViewController<MXKViewControllerActivityHandling>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,83 @@
//
// Copyright 2020 The Matrix.org Foundation C.I.C
//
// 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 "MXKActivityHandlingViewController.h"
@interface MXKActivityHandlingViewController ()
@end
@implementation MXKActivityHandlingViewController
@synthesize activityIndicator;
#pragma mark -
- (void)viewDidLoad
{
[super viewDidLoad];
// Add default activity indicator
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
activityIndicator.backgroundColor = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1.0];
activityIndicator.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
activityIndicator.hidesWhenStopped = YES;
CGRect frame = activityIndicator.frame;
frame.size.width += 30;
frame.size.height += 30;
activityIndicator.bounds = frame;
[activityIndicator.layer setCornerRadius:5];
activityIndicator.center = self.view.center;
[self.view addSubview:activityIndicator];
}
- (void)dealloc
{
if (activityIndicator)
{
[activityIndicator removeFromSuperview];
activityIndicator = nil;
}
}
#pragma mark - Activity indicator
- (void)startActivityIndicator
{
if (activityIndicator)
{
[self.view bringSubviewToFront:activityIndicator];
[activityIndicator startAnimating];
// Show the loading wheel after a delay so that if the caller calls stopActivityIndicator
// in a short future, the loading wheel will not be displayed to the end user.
activityIndicator.alpha = 0;
[UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
self->activityIndicator.alpha = 1;
} completion:^(BOOL finished)
{
}];
}
}
- (void)stopActivityIndicator
{
[activityIndicator stopAnimating];
}
@end
@@ -0,0 +1,123 @@
/*
Copyright 2015 OpenMarket 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 <MatrixSDK/MatrixSDK.h>
#import "MXKViewController.h"
#import "MXKAttachment.h"
#import "MXKAttachmentAnimator.h"
@protocol MXKAttachmentsViewControllerDelegate;
/**
This view controller is used to display attachments of a room.
Only one attachment is displayed at once, the user is able to swipe one by one the attachment.
*/
@interface MXKAttachmentsViewController : MXKViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UIDocumentInteractionControllerDelegate, MXKDestinationAttachmentAnimatorDelegate>
@property (nonatomic) IBOutlet UICollectionView *attachmentsCollection;
@property (nonatomic) IBOutlet UINavigationBar *navigationBar;
@property (unsafe_unretained, nonatomic) IBOutlet UIBarButtonItem *backButton;
/**
The attachments array.
*/
@property (nonatomic, readonly) NSArray *attachments;
/**
Tell whether all attachments have been retrieved from the room history (In that case no attachment can be added at the beginning of attachments array).
*/
@property (nonatomic) BOOL complete;
/**
The delegate notified when inputs are ready.
*/
@property (nonatomic, weak) id<MXKAttachmentsViewControllerDelegate> delegate;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKAttachmentsViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `roomViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKAttachmentsViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKAttachmentsViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)attachmentsViewController;
/**
Creates and returns a new `MXKAttachmentsViewController` object, also sets sets up environment for animated interactive transitions.
*/
+ (instancetype)animatedAttachmentsViewControllerWithSourceViewController:(UIViewController <MXKSourceAttachmentAnimatorDelegate> *)sourceViewController;
/**
Display attachments of a room.
The provided event id is used to select the attachment to display first. Use nil to unchange the current displayed attachment.
By default the first attachment is displayed.
If the back pagination spinner is currently displayed and provided event id is nil,
the viewer will display the first added attachment during back pagination.
@param attachmentArray the array of attachments (MXKAttachment instances).
@param eventId the identifier of the attachment to display first.
*/
- (void)displayAttachments:(NSArray*)attachmentArray focusOn:(NSString*)eventId;
/**
Action used to handle the `backButton` in the navigation bar.
*/
- (IBAction)onButtonPressed:(id)sender;
@end
@protocol MXKAttachmentsViewControllerDelegate <NSObject>
/**
Ask the delegate for more attachments.
This method is called only if 'complete' is NO.
When some attachments are available, the delegate update the attachmnet list by using
[MXKAttachmentsViewController displayAttachments: focusOn:].
When no new attachment is available, the delegate must update the property 'complete'.
@param attachmentsViewController the attachments view controller.
@param eventId the event identifier of the current first attachment.
@return a boolean which tells whether some new attachments may be added or not.
*/
- (BOOL)attachmentsViewController:(MXKAttachmentsViewController*)attachmentsViewController paginateAttachmentBefore:(NSString*)eventId;
@optional
/**
Informs the delegate that a new attachment has been shown
the parameter eventId is used by the delegate to identify the attachment
*/
- (void)displayedNewAttachmentWithEventId:(NSString *)eventId;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKAttachmentsViewController">
<connections>
<outlet property="attachmentsCollection" destination="LOp-cf-Sji" id="4Zx-KZ-fkh"/>
<outlet property="backButton" destination="VjO-K2-a0w" id="VlD-Iw-NT5"/>
<outlet property="navigationBar" destination="orb-yb-k6g" id="ILw-DO-IH2"/>
<outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleAspectFit" alwaysBounceHorizontal="YES" pagingEnabled="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="LOp-cf-Sji">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<collectionViewFlowLayout key="collectionViewLayout" scrollDirection="horizontal" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" id="RrA-v8-THJ">
<size key="itemSize" width="600" height="600"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<connections>
<outlet property="dataSource" destination="-1" id="gLX-KL-R8w"/>
<outlet property="delegate" destination="-1" id="z1d-ep-n4n"/>
</connections>
</collectionView>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="orb-yb-k6g">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<items>
<navigationItem id="lP4-eZ-AQ8">
<barButtonItem key="leftBarButtonItem" image="back_icon.png" style="plain" id="VjO-K2-a0w">
<connections>
<action selector="onButtonPressed:" destination="-1" id="PEa-WB-Up1"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="orb-yb-k6g" firstAttribute="top" secondItem="kHK-qN-rPN" secondAttribute="top" id="GGy-ix-LRO"/>
<constraint firstItem="LOp-cf-Sji" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="RQT-el-TH0"/>
<constraint firstAttribute="trailing" secondItem="LOp-cf-Sji" secondAttribute="trailing" id="Vgl-Wc-CI1"/>
<constraint firstAttribute="trailing" secondItem="orb-yb-k6g" secondAttribute="trailing" id="bFh-tc-Lub"/>
<constraint firstAttribute="bottom" secondItem="LOp-cf-Sji" secondAttribute="bottom" id="d22-2K-BEZ"/>
<constraint firstItem="LOp-cf-Sji" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="jEc-aG-zZn"/>
<constraint firstItem="orb-yb-k6g" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="toe-s8-clE"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<viewLayoutGuide key="safeArea" id="kHK-qN-rPN"/>
</view>
</objects>
<resources>
<image name="back_icon.png" width="13.5" height="23"/>
</resources>
</document>
@@ -0,0 +1,311 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
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 <UIKit/UIKit.h>
#import "MXKViewController.h"
#import "MXKAuthInputsView.h"
#import "MXKAuthenticationFallbackWebView.h"
@class MXKAuthenticationViewController;
/**
`MXKAuthenticationViewController` delegate.
*/
@protocol MXKAuthenticationViewControllerDelegate <NSObject>
/**
Tells the delegate the authentication process succeeded to add a new account.
@param authenticationViewController the `MXKAuthenticationViewController` instance.
@param userId the user id of the new added account.
*/
- (void)authenticationViewController:(MXKAuthenticationViewController *)authenticationViewController didLogWithUserId:(NSString*)userId;
@end
/**
This view controller should be used to manage registration or login flows with matrix homeserver.
Only the flow based on password is presently supported. Other flows should be added later.
You may add a delegate to be notified when a new account has been added successfully.
*/
@interface MXKAuthenticationViewController : MXKViewController <UITextFieldDelegate, MXKAuthInputsViewDelegate>
{
@protected
/**
Reference to any opened alert view.
*/
UIAlertController *alert;
/**
Tell whether the password has been reseted with success.
Used to return on login screen on submit button pressed.
*/
BOOL isPasswordReseted;
}
@property (weak, nonatomic) IBOutlet UIImageView *welcomeImageView;
@property (strong, nonatomic) IBOutlet UIScrollView *authenticationScrollView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *authScrollViewBottomConstraint;
@property (weak, nonatomic) IBOutlet UIView *contentView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *contentViewHeightConstraint;
@property (weak, nonatomic) IBOutlet UILabel *subTitleLabel;
@property (weak, nonatomic) IBOutlet UIView *authInputsContainerView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *authInputContainerViewHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *authInputContainerViewMinHeightConstraint;
@property (weak, nonatomic) IBOutlet UILabel *homeServerLabel;
@property (weak, nonatomic) IBOutlet UITextField *homeServerTextField;
@property (weak, nonatomic) IBOutlet UILabel *homeServerInfoLabel;
@property (weak, nonatomic) IBOutlet UIView *identityServerContainer;
@property (weak, nonatomic) IBOutlet UILabel *identityServerLabel;
@property (weak, nonatomic) IBOutlet UITextField *identityServerTextField;
@property (weak, nonatomic) IBOutlet UILabel *identityServerInfoLabel;
@property (weak, nonatomic) IBOutlet UIButton *submitButton;
@property (weak, nonatomic) IBOutlet UIButton *authSwitchButton;
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *authenticationActivityIndicator;
@property (weak, nonatomic) IBOutlet UIView *authenticationActivityIndicatorContainerView;
@property (weak, nonatomic) IBOutlet UILabel *noFlowLabel;
@property (weak, nonatomic) IBOutlet UIButton *retryButton;
@property (weak, nonatomic) IBOutlet UIView *authFallbackContentView;
// WKWebView is not available to be created from xib because of NSCoding support below iOS 11. So we're using a container view.
// See this: https://stackoverflow.com/questions/46221577/xcode-9-gm-wkwebview-nscoding-support-was-broken-in-previous-versions
@property (weak, nonatomic) IBOutlet UIView *authFallbackWebViewContainer;
@property (strong, nonatomic) MXKAuthenticationFallbackWebView *authFallbackWebView;
@property (weak, nonatomic) IBOutlet UIButton *cancelAuthFallbackButton;
/**
The current authentication type (MXKAuthenticationTypeLogin by default).
*/
@property (nonatomic) MXKAuthenticationType authType;
/**
The view in which authentication inputs are displayed (`MXKAuthInputsView-inherited` instance).
*/
@property (nonatomic) MXKAuthInputsView *authInputsView;
/**
The default homeserver url (nil by default).
*/
@property (nonatomic) NSString *defaultHomeServerUrl;
/**
The default identity server url (nil by default).
*/
@property (nonatomic) NSString *defaultIdentityServerUrl;
/**
Force a registration process based on a predefined set of parameters.
Use this property to pursue a registration from the next_link sent in an email validation email.
*/
@property (nonatomic) NSDictionary* externalRegistrationParameters;
/**
Use a login process based on the soft logout credentials.
*/
@property (nonatomic) MXCredentials *softLogoutCredentials;
/**
Enable/disable overall the user interaction option.
It is used during authentication process to prevent multiple requests.
*/
@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;
/**
The device name used to display it in the user's devices list (nil by default).
If nil, the device display name field is filled with a default string: "Mobile", "Tablet"...
*/
@property (nonatomic) NSString *deviceDisplayName;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKAuthenticationViewControllerDelegate> delegate;
/**
current ongoing MXHTTPOperation. Nil if none.
*/
@property (nonatomic, nullable, readonly) MXHTTPOperation *currentHttpOperation;
/**
Returns the `UINib` object initialized for a `MXKAuthenticationViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `authenticationViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKAuthenticationViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKAuthenticationViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)authenticationViewController;
/**
Register the MXKAuthInputsView class that will be used to display inputs for an authentication type.
By default the 'MXKAuthInputsPasswordBasedView' class is registered for 'MXKAuthenticationTypeLogin' authentication.
No class is registered for 'MXKAuthenticationTypeRegister' type.
No class is registered for 'MXKAuthenticationTypeForgotPassword' type.
@param authInputsViewClass a MXKAuthInputsView-inherited class.
@param authType the concerned authentication type
*/
- (void)registerAuthInputsViewClass:(Class)authInputsViewClass forAuthType:(MXKAuthenticationType)authType;
/**
Refresh login/register mechanism supported by the server and the application.
*/
- (void)refreshAuthenticationSession;
/**
Handle supported flows and associated information returned by the homeserver.
*/
- (void)handleAuthenticationSession:(MXAuthenticationSession *)authSession;
/**
Customize the MXHTTPClientOnUnrecognizedCertificate block that will be used to handle unrecognized certificate observed during authentication challenge from a server.
By default we prompt the user by displaying a fingerprint (SHA256) of the certificate. The user is then able to trust or not the certificate.
@param onUnrecognizedCertificateBlock the block that will be used to handle unrecognized certificate
*/
- (void)setOnUnrecognizedCertificateBlock:(MXHTTPClientOnUnrecognizedCertificate)onUnrecognizedCertificateBlock;
/**
Check whether the current username is already in use.
@param callback A block object called when the operation is completed.
*/
- (void)isUserNameInUse:(void (^)(BOOL isUserNameInUse))callback;
/**
Make a ping to the registration endpoint to detect a possible registration problem earlier.
@param callback A block object called when the operation is completed.
It provides a MXError to check to verify if the user can be registered.
*/
- (void)testUserRegistration:(void (^)(MXError *mxError))callback;
/**
Action registered on the following events:
- 'UIControlEventTouchUpInside' for each UIButton instance.
- 'UIControlEventValueChanged' for each UISwitch instance.
*/
- (IBAction)onButtonPressed:(id)sender;
/**
Set the homeserver url and force a new authentication session.
The default homeserver url is used when the provided url is nil.
@param homeServerUrl the homeserver url to use
*/
- (void)setHomeServerTextFieldText:(NSString *)homeServerUrl;
/**
Set the identity server url.
The default identity server url is used when the provided url is nil.
@param identityServerUrl the identity server url to use
*/
- (void)setIdentityServerTextFieldText:(NSString *)identityServerUrl;
/**
Fetch the identity server from the wellknown API of the selected homeserver.
and check if the HS requires an identity server.
*/
- (void)checkIdentityServer;
/**
Force dismiss keyboard
*/
- (void)dismissKeyboard;
/**
Cancel the current operation, and return to the initial step
*/
- (void)cancel;
/**
Handle the error received during an authentication request.
@param error the received error.
*/
- (void)onFailureDuringAuthRequest:(NSError *)error;
/**
Display a kMXErrCodeStringResourceLimitExceeded error received during an authentication
request.
@param errorDict the error data.
@param onAdminContactTapped a callback indicating if the user wants to contact their admin.
*/
- (void)showResourceLimitExceededError:(NSDictionary *)errorDict onAdminContactTapped:(void (^)(NSURL *adminContact))onAdminContactTapped;
/**
Handle the successful authentication request.
@param credentials the user's credentials.
*/
- (void)onSuccessfulLogin:(MXCredentials*)credentials;
/// Login with custom parameters
/// @param parameters Login parameters
- (void)loginWithParameters:(NSDictionary*)parameters;
/// Create an account with the given credentials
/// @param credentials Account credentials
- (void)createAccountWithCredentials:(MXCredentials *)credentials;
#pragma mark - Authentication Fallback
/**
Display the fallback URL within a webview.
*/
- (void)showAuthenticationFallBackView;
#pragma mark - Device rehydration
/**
Call this method at an appropriate time to attempt rehydrating from an existing dehydrated device
@param keyData Secret key data
@param credentials Account credentials
*/
- (void)attemptDeviceRehydrationWithKeyData:(NSData *)keyData credentials:(MXCredentials *)credentials;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKAuthenticationViewController">
<connections>
<outlet property="authFallbackContentView" destination="q1e-Wg-6t7" id="mR6-hS-5YB"/>
<outlet property="authFallbackWebViewContainer" destination="6Em-Jj-UPO" id="F5d-zg-2ED"/>
<outlet property="authInputContainerViewHeightConstraint" destination="e04-1Y-4gZ" id="5qa-cu-M8v"/>
<outlet property="authInputContainerViewMinHeightConstraint" destination="2s6-K4-8Vv" id="x5P-zJ-hA6"/>
<outlet property="authInputsContainerView" destination="xWb-IJ-v7F" id="DEo-Ji-wPT"/>
<outlet property="authScrollViewBottomConstraint" destination="g7o-eO-7Yr" id="kVv-Xp-aTT"/>
<outlet property="authSwitchButton" destination="FNe-kn-VlD" id="EhA-sr-kiZ"/>
<outlet property="authenticationActivityIndicator" destination="30E-gm-z6O" id="DDw-QJ-ND8"/>
<outlet property="authenticationScrollView" destination="OHV-KQ-Ww0" id="gyc-zq-fA1"/>
<outlet property="cancelAuthFallbackButton" destination="9qj-5c-Sfb" id="IH4-cc-kKx"/>
<outlet property="contentView" destination="rhx-dD-4EJ" id="XXc-2j-Gi6"/>
<outlet property="contentViewHeightConstraint" destination="6v6-fz-e8o" id="wHX-YX-3gE"/>
<outlet property="homeServerInfoLabel" destination="aLk-0i-Z0d" id="RRa-xP-oDQ"/>
<outlet property="homeServerLabel" destination="kHf-s1-cZa" id="nSI-4K-aQq"/>
<outlet property="homeServerTextField" destination="G8l-AP-iRs" id="F4s-Sf-TTZ"/>
<outlet property="identityServerInfoLabel" destination="NDg-U7-lEi" id="pLr-t8-MyU"/>
<outlet property="identityServerLabel" destination="5CT-Ht-Z3v" id="9h4-6J-n3X"/>
<outlet property="identityServerTextField" destination="PZC-Hd-Q6a" id="vKg-sd-dzJ"/>
<outlet property="noFlowLabel" destination="54b-4O-ip9" id="f18-H1-cQm"/>
<outlet property="retryButton" destination="wIH-Kd-r7q" id="42j-Ad-zVS"/>
<outlet property="subTitleLabel" destination="Bcs-TE-KZc" id="Gk1-7I-jnR"/>
<outlet property="submitButton" destination="k3J-Eg-itz" id="fiZ-wK-6YM"/>
<outlet property="view" destination="5rn-KE-plm" id="bFJ-yJ-vc0"/>
<outlet property="welcomeImageView" destination="d8r-TX-pwX" id="vzD-zK-EeC"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="5rn-KE-plm">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" keyboardDismissMode="onDrag" translatesAutoresizingMaskIntoConstraints="NO" id="OHV-KQ-Ww0">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rhx-dD-4EJ" userLabel="Content View">
<rect key="frame" x="0.0" y="0.0" width="600" height="640"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="d8r-TX-pwX" userLabel="Welcome Image View">
<rect key="frame" x="180" y="33" width="240" height="102"/>
<constraints>
<constraint firstAttribute="height" constant="102" id="1dJ-8w-oNT"/>
<constraint firstAttribute="width" constant="240" id="gA2-2L-JHF"/>
</constraints>
</imageView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Create account:" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bcs-TE-KZc">
<rect key="frame" x="88" y="147" width="110" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xWb-IJ-v7F" userLabel="AuthInputsContainerView">
<rect key="frame" x="150" y="170" width="300" height="180"/>
<subviews>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="30E-gm-z6O">
<rect key="frame" x="140" y="80" width="20" height="20"/>
</activityIndicatorView>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Currently we do not support authentication flows defined by this homeserver" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="0.0" translatesAutoresizingMaskIntoConstraints="NO" id="54b-4O-ip9" userLabel="noFlowLabel">
<rect key="frame" x="9" y="8" width="282" height="33.5"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wIH-Kd-r7q" userLabel="retryButton">
<rect key="frame" x="127.5" y="46.5" width="45" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="WtO-NT-ei8"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="45" id="YYb-6s-rgd"/>
</constraints>
<state key="normal" title="Retry">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="ZW9-lZ-tLj"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="54b-4O-ip9" secondAttribute="centerX" id="0bV-x1-MhX"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="2s6-K4-8Vv"/>
<constraint firstItem="54b-4O-ip9" firstAttribute="top" secondItem="xWb-IJ-v7F" secondAttribute="top" constant="8" id="Ddp-gU-nLY"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="54b-4O-ip9" secondAttribute="trailing" constant="8" id="VEQ-w9-9Ln"/>
<constraint firstAttribute="centerY" secondItem="30E-gm-z6O" secondAttribute="centerY" id="ctL-D3-bgP"/>
<constraint firstAttribute="height" constant="180" id="e04-1Y-4gZ"/>
<constraint firstItem="wIH-Kd-r7q" firstAttribute="top" secondItem="54b-4O-ip9" secondAttribute="bottom" constant="5" id="grf-0I-rwT"/>
<constraint firstAttribute="width" constant="300" id="lLi-9b-XMC"/>
<constraint firstItem="54b-4O-ip9" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="xWb-IJ-v7F" secondAttribute="leading" constant="8" id="phW-Yy-zN7"/>
<constraint firstAttribute="centerX" secondItem="wIH-Kd-r7q" secondAttribute="centerX" id="rTd-Qc-xrD"/>
<constraint firstAttribute="centerX" secondItem="30E-gm-z6O" secondAttribute="centerX" id="sSN-PO-Q6t"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="k3J-Eg-itz" userLabel="SubmitBtn">
<rect key="frame" x="272.5" y="360" width="55" height="33"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Submit">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="Ocd-Ag-6hf"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qYR-zu-cWD" userLabel="HomeServerView">
<rect key="frame" x="122" y="408" width="356" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Homeserver:" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kHf-s1-cZa">
<rect key="frame" x="0.0" y="6" width="100" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="sMT-6v-Bl2"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="URL (e.g. https://matrix.org)" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="G8l-AP-iRs">
<rect key="frame" x="106" y="0.0" width="250" height="30"/>
<constraints>
<constraint firstAttribute="width" priority="750" constant="250" id="Bus-fe-qDE"/>
<constraint firstAttribute="height" constant="30" id="n0r-1Y-JVb"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="-1" id="j11-Hz-zbw"/>
</connections>
</textField>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="G8l-AP-iRs" secondAttribute="bottom" id="5IT-R0-LOI"/>
<constraint firstItem="G8l-AP-iRs" firstAttribute="top" secondItem="qYR-zu-cWD" secondAttribute="top" id="Buu-5d-k19"/>
<constraint firstItem="kHf-s1-cZa" firstAttribute="top" secondItem="qYR-zu-cWD" secondAttribute="top" constant="6" id="L7i-5Z-s2o"/>
<constraint firstAttribute="trailing" secondItem="G8l-AP-iRs" secondAttribute="trailing" id="PLA-Aw-Pzy"/>
<constraint firstItem="G8l-AP-iRs" firstAttribute="leading" secondItem="kHf-s1-cZa" secondAttribute="trailing" constant="6" id="Q5p-aO-t6v"/>
<constraint firstItem="kHf-s1-cZa" firstAttribute="leading" secondItem="qYR-zu-cWD" secondAttribute="leading" id="cc8-Fb-gFT"/>
<constraint firstAttribute="bottom" secondItem="kHf-s1-cZa" secondAttribute="bottom" constant="6" id="nAb-iy-C9m"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Your homeserver stores all your conversations and account data" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="0.0" translatesAutoresizingMaskIntoConstraints="NO" id="aLk-0i-Z0d" userLabel="HomeServerInfoLabel">
<rect key="frame" x="106" y="438" width="388" height="16"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wgD-7Z-30S" userLabel="IdentityServerView">
<rect key="frame" x="115" y="462" width="370" height="30"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Identity server:" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5CT-Ht-Z3v">
<rect key="frame" x="0.0" y="6" width="114" height="18"/>
<constraints>
<constraint firstAttribute="width" constant="114" id="zee-RK-6DE"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="URL (e.g. https://matrix.org)" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="PZC-Hd-Q6a">
<rect key="frame" x="120" y="0.0" width="250" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="9Mz-m5-EB9"/>
<constraint firstAttribute="width" priority="750" constant="250" id="Qaq-Ul-hve"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="-1" id="nZF-by-6t6"/>
</connections>
</textField>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="PZC-Hd-Q6a" firstAttribute="leading" secondItem="5CT-Ht-Z3v" secondAttribute="trailing" constant="6" id="8JB-zn-QIC"/>
<constraint firstAttribute="trailing" secondItem="PZC-Hd-Q6a" secondAttribute="trailing" id="Jkm-DW-Ybe"/>
<constraint firstItem="5CT-Ht-Z3v" firstAttribute="leading" secondItem="wgD-7Z-30S" secondAttribute="leading" id="KVq-Db-bf1"/>
<constraint firstItem="5CT-Ht-Z3v" firstAttribute="top" secondItem="wgD-7Z-30S" secondAttribute="top" constant="6" id="NOf-dw-aCG"/>
<constraint firstAttribute="bottom" secondItem="5CT-Ht-Z3v" secondAttribute="bottom" constant="6" id="e1T-pC-1Hb"/>
<constraint firstAttribute="bottom" secondItem="PZC-Hd-Q6a" secondAttribute="bottom" id="nRU-cF-4sd"/>
<constraint firstItem="PZC-Hd-Q6a" firstAttribute="top" secondItem="wgD-7Z-30S" secondAttribute="top" id="wyY-ND-M2F"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" preferredMaxLayoutWidth="0.0" translatesAutoresizingMaskIntoConstraints="NO" id="NDg-U7-lEi" userLabel="IdentityServerInfoLabel">
<rect key="frame" x="24.5" y="492" width="551.5" height="31.5"/>
<string key="text">Matrix provides identity servers to track which emails etc. belong to which Matrix IDs. Only https://matrix.org currently exists.</string>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FNe-kn-VlD" userLabel="authSwitchButton">
<rect key="frame" x="247.5" y="535.5" width="105" height="30"/>
<state key="normal" title="Create account">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="Wso-Up-84h"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="k3J-Eg-itz" secondAttribute="centerX" id="16F-5a-sGz"/>
<constraint firstItem="aLk-0i-Z0d" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="rhx-dD-4EJ" secondAttribute="leading" constant="8" id="1RP-D4-oSf"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="NDg-U7-lEi" secondAttribute="trailing" constant="8" id="1wt-wg-IhV"/>
<constraint firstItem="NDg-U7-lEi" firstAttribute="top" secondItem="wgD-7Z-30S" secondAttribute="bottom" id="2ET-PY-wrQ"/>
<constraint firstAttribute="centerX" secondItem="aLk-0i-Z0d" secondAttribute="centerX" id="3A4-MZ-gtb"/>
<constraint firstAttribute="centerX" secondItem="qYR-zu-cWD" secondAttribute="centerX" id="4YF-AE-oU7"/>
<constraint firstAttribute="height" constant="640" id="6v6-fz-e8o"/>
<constraint firstItem="wgD-7Z-30S" firstAttribute="top" secondItem="aLk-0i-Z0d" secondAttribute="bottom" constant="8" id="DQl-gU-L2D"/>
<constraint firstItem="aLk-0i-Z0d" firstAttribute="top" secondItem="qYR-zu-cWD" secondAttribute="bottom" id="EwX-Q5-Hf9"/>
<constraint firstItem="qYR-zu-cWD" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="rhx-dD-4EJ" secondAttribute="leading" constant="8" id="HYA-Wo-irB"/>
<constraint firstAttribute="centerX" secondItem="xWb-IJ-v7F" secondAttribute="centerX" id="JWP-wi-ZDa"/>
<constraint firstAttribute="centerX" secondItem="NDg-U7-lEi" secondAttribute="centerX" id="R0H-hs-7wv"/>
<constraint firstItem="FNe-kn-VlD" firstAttribute="top" secondItem="NDg-U7-lEi" secondAttribute="bottom" constant="12" id="Uf5-oH-d3F"/>
<constraint firstItem="xWb-IJ-v7F" firstAttribute="top" secondItem="Bcs-TE-KZc" secondAttribute="bottom" constant="5" id="VkP-Hy-on9"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="qYR-zu-cWD" secondAttribute="trailing" constant="8" id="WKx-X9-rSk"/>
<constraint firstItem="qYR-zu-cWD" firstAttribute="top" secondItem="k3J-Eg-itz" secondAttribute="bottom" constant="15" id="YyO-m7-k5N"/>
<constraint firstItem="k3J-Eg-itz" firstAttribute="top" secondItem="xWb-IJ-v7F" secondAttribute="bottom" constant="10" id="ZWc-pM-S6Z"/>
<constraint firstItem="wgD-7Z-30S" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="rhx-dD-4EJ" secondAttribute="leading" constant="8" id="bhG-JM-5OL"/>
<constraint firstAttribute="centerX" secondItem="FNe-kn-VlD" secondAttribute="centerX" id="eY4-Si-CNK"/>
<constraint firstItem="xWb-IJ-v7F" firstAttribute="top" secondItem="d8r-TX-pwX" secondAttribute="bottom" constant="35" id="h11-Ty-Pmd"/>
<constraint firstAttribute="width" constant="600" placeholder="YES" id="hOT-Wn-hIb"/>
<constraint firstItem="xWb-IJ-v7F" firstAttribute="top" secondItem="rhx-dD-4EJ" secondAttribute="top" constant="170" id="khR-Uj-OTH"/>
<constraint firstAttribute="centerX" secondItem="Bcs-TE-KZc" secondAttribute="centerX" multiplier="2.1" id="ku1-W4-wTW"/>
<constraint firstAttribute="centerX" secondItem="d8r-TX-pwX" secondAttribute="centerX" id="l6k-EH-Yb8"/>
<constraint firstItem="NDg-U7-lEi" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="rhx-dD-4EJ" secondAttribute="leading" constant="8" id="n7a-Mq-ac1"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="wgD-7Z-30S" secondAttribute="trailing" constant="8" id="pdt-ux-dSI"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="aLk-0i-Z0d" secondAttribute="trailing" constant="8" id="szN-BO-i20"/>
<constraint firstAttribute="centerX" secondItem="wgD-7Z-30S" secondAttribute="centerX" id="ysX-l7-PIY"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="rhx-dD-4EJ" secondAttribute="bottom" id="0Ex-X9-7WC"/>
<constraint firstAttribute="trailing" secondItem="rhx-dD-4EJ" secondAttribute="trailing" id="Ztq-8U-wlX"/>
<constraint firstItem="rhx-dD-4EJ" firstAttribute="top" secondItem="OHV-KQ-Ww0" secondAttribute="top" id="ePD-me-ymh"/>
<constraint firstItem="rhx-dD-4EJ" firstAttribute="leading" secondItem="OHV-KQ-Ww0" secondAttribute="leading" id="h1J-cn-QL1"/>
</constraints>
</scrollView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="q1e-Wg-6t7" userLabel="Authentication Fallback ContentView">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9qj-5c-Sfb">
<rect key="frame" x="317" y="5" width="50" height="35"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="GMq-PF-fBv"/>
<constraint firstAttribute="height" constant="35" id="KfQ-gX-IHF"/>
</constraints>
<state key="normal" title="Cancel">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="Pxo-2q-AdE"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Em-Jj-UPO">
<rect key="frame" x="0.0" y="40" width="375" height="607"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="6Em-Jj-UPO" firstAttribute="top" secondItem="9qj-5c-Sfb" secondAttribute="bottom" id="1b1-7U-PeK"/>
<constraint firstAttribute="trailing" secondItem="9qj-5c-Sfb" secondAttribute="trailing" constant="8" id="AUi-MV-V60"/>
<constraint firstAttribute="bottom" secondItem="6Em-Jj-UPO" secondAttribute="bottom" id="PaB-bb-nat"/>
<constraint firstItem="6Em-Jj-UPO" firstAttribute="leading" secondItem="q1e-Wg-6t7" secondAttribute="leading" id="TBi-7x-M9c"/>
<constraint firstItem="9qj-5c-Sfb" firstAttribute="top" secondItem="q1e-Wg-6t7" secondAttribute="top" constant="5" id="ZKZ-pg-xzV"/>
<constraint firstAttribute="trailing" secondItem="6Em-Jj-UPO" secondAttribute="trailing" id="e1s-Hg-1Fa"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="OHV-KQ-Ww0" firstAttribute="leading" secondItem="5rn-KE-plm" secondAttribute="leading" id="1Rd-Il-6MW"/>
<constraint firstAttribute="bottom" secondItem="q1e-Wg-6t7" secondAttribute="bottom" id="8Bc-V7-iP3"/>
<constraint firstItem="q1e-Wg-6t7" firstAttribute="leading" secondItem="5rn-KE-plm" secondAttribute="leading" id="DiC-Ui-3Wk"/>
<constraint firstItem="OHV-KQ-Ww0" firstAttribute="top" secondItem="5rn-KE-plm" secondAttribute="top" constant="20" symbolic="YES" id="FGm-jm-8r0"/>
<constraint firstAttribute="trailing" secondItem="OHV-KQ-Ww0" secondAttribute="trailing" id="KAu-Wf-ojt"/>
<constraint firstAttribute="trailing" secondItem="q1e-Wg-6t7" secondAttribute="trailing" id="XIS-Jp-qZf"/>
<constraint firstAttribute="bottom" secondItem="OHV-KQ-Ww0" secondAttribute="bottom" id="g7o-eO-7Yr"/>
<constraint firstItem="q1e-Wg-6t7" firstAttribute="top" secondItem="5rn-KE-plm" secondAttribute="top" constant="20" id="sT2-HR-vou"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<point key="canvasLocation" x="138.40000000000001" y="152.47376311844079"/>
</view>
</objects>
</document>
@@ -0,0 +1,243 @@
/*
Copyright 2015 OpenMarket 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 <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKViewController.h"
#import "MXKImageView.h"
@class MXKCallViewController;
/**
Delegate for `MXKCallViewController` object
*/
@protocol MXKCallViewControllerDelegate <NSObject>
/**
Tells the delegate to dismiss the call view controller.
This callback is called when the user wants to go back into the app during a call or when the call is ended.
The delegate should check the state of the associated call to know the actual reason.
@param callViewController the call view controller.
@param completion the block to execute at the end of the operation.
*/
- (void)dismissCallViewController:(MXKCallViewController *)callViewController completion:(void (^)(void))completion;
/**
Tells the delegate that user tapped on hold call.
@param callViewController the call view controller.
*/
- (void)callViewControllerDidTapOnHoldCall:(MXKCallViewController *)callViewController;
@end
extern NSString *const kMXKCallViewControllerWillAppearNotification;
extern NSString *const kMXKCallViewControllerAppearedNotification;
extern NSString *const kMXKCallViewControllerWillDisappearNotification;
extern NSString *const kMXKCallViewControllerDisappearedNotification;
extern NSString *const kMXKCallViewControllerBackToAppNotification;
/**
'MXKCallViewController' instance displays a call. Only one matrix session is supported by this view controller.
*/
@interface MXKCallViewController : MXKViewController <MXCallDelegate, AVAudioPlayerDelegate>
@property (weak, nonatomic) IBOutlet MXKImageView *backgroundImageView;
@property (weak, nonatomic, readonly) IBOutlet UIView *localPreviewContainerView;
@property (weak, nonatomic, readonly) IBOutlet UIView *localPreviewVideoView;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *localPreviewActivityView;
@property (weak, nonatomic, readonly) IBOutlet UIView *onHoldCallContainerView;
@property (weak, nonatomic) IBOutlet MXKImageView *onHoldCallerImageView;
@property (weak, nonatomic, readonly) IBOutlet UIView *remotePreviewContainerView;
@property (weak, nonatomic) IBOutlet UIView *overlayContainerView;
@property (weak, nonatomic) IBOutlet UIView *callContainerView;
@property (weak, nonatomic) IBOutlet MXKImageView *callerImageView;
@property (weak, nonatomic) IBOutlet UIImageView *pausedIcon;
@property (weak, nonatomic) IBOutlet UILabel *callerNameLabel;
@property (weak, nonatomic) IBOutlet UILabel *callStatusLabel;
@property (weak, nonatomic) IBOutlet UIButton *resumeButton;
@property (weak, nonatomic) IBOutlet UIView *callToolBar;
@property (weak, nonatomic) IBOutlet UIButton *rejectCallButton;
@property (weak, nonatomic) IBOutlet UIButton *answerCallButton;
@property (weak, nonatomic) IBOutlet UIButton *endCallButton;
@property (weak, nonatomic) IBOutlet UIView *callControlContainerView;
@property (weak, nonatomic) IBOutlet UIButton *speakerButton;
@property (weak, nonatomic) IBOutlet UIButton *audioMuteButton;
@property (weak, nonatomic) IBOutlet UIButton *videoMuteButton;
@property (weak, nonatomic) IBOutlet UIButton *moreButtonForVoice;
@property (weak, nonatomic) IBOutlet UIButton *moreButtonForVideo;
@property (weak, nonatomic) IBOutlet UIButton *backToAppButton;
@property (weak, nonatomic) IBOutlet UIButton *cameraSwitchButton;
@property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *localPreviewContainerViewLeadingConstraint;
@property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *localPreviewContainerViewTopConstraint;
@property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *localPreviewContainerViewHeightConstraint;
@property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *localPreviewContainerViewWidthConstraint;
@property (weak, nonatomic) IBOutlet UIButton *transferButton;
/**
The default picture displayed when no picture is available.
*/
@property (nonatomic) UIImage *picturePlaceholder;
/**
The call status bar displayed on the top of the app during a call.
*/
@property (nonatomic, readonly) UIWindow *backToAppStatusWindow;
/**
Flag whether this call screen is displaying an alert right now.
*/
@property (nonatomic, readonly, getter=isDisplayingAlert) BOOL displayingAlert;
/**
The current call
*/
@property (nonatomic) MXCall *mxCall;
/**
The current call on hold
*/
@property (nonatomic) MXCall *mxCallOnHold;
/**
The current peer
*/
@property (nonatomic, readonly) MXUser *peer;
/**
The current peer of the call on hold
*/
@property (nonatomic, readonly) MXUser *peerOnHold;
/**
The delegate.
*/
@property (nonatomic, weak) id<MXKCallViewControllerDelegate> delegate;
/*
Specifies whether a ringtone must be played on incoming call.
It's important to set this value before you will set `mxCall` otherwise value of this property can has no effect.
Defaults to YES.
*/
@property (nonatomic) BOOL playRingtone;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKCallViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `roomViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKCallViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@param call a MXCall instance.
@return An initialized `MXKRoomViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)callViewController:(MXCall *)call;
/**
Return an audio file url based on the provided name.
@param soundName audio file name without extension.
@return a NSURL instance.
*/
- (NSURL*)audioURLWithName:(NSString *)soundName;
/**
Refresh the peer information in the call viewcontroller's view.
*/
- (void)updatePeerInfoDisplay;
/**
Adjust the layout of the preview container.
*/
- (void)updateLocalPreviewLayout;
/**
Show/Hide the overlay view.
@param isShown tell whether the overlay is shown or not.
*/
- (void)showOverlayContainer:(BOOL)isShown;
/**
Set up or teardown the promixity monitoring and enable/disable the idle timer according to call type, state & audio route.
*/
- (void)updateProximityAndSleep;
/**
Prepare and return the optional view displayed during incoming call notification.
Return nil by default
Subclasses may override this method to provide appropriate for their app view.
When this method is called peer and mxCall are valid so you can use them.
*/
- (UIView *)createIncomingCallView;
/**
Action registered on the event 'UIControlEventTouchUpInside' for each UIButton instance.
*/
- (IBAction)onButtonPressed:(id)sender;
/**
Default implementation presents an action sheet with proper options. Override to change the user interface.
*/
- (void)showAudioDeviceOptions;
/**
Default implementation makes the button selected for loud speakers and external device options, non-selected for built-in device.
*/
- (void)configureSpeakerButton;
#pragma mark - DTMF
/**
Default implementation does nothing. Override to show a dial pad and then use MXCall methods to send DTMF tones.
*/
- (void)openDialpad;
#pragma mark - Call Transfer
/**
Default implementation does nothing. Override to show a contact selection screen and then use MXCallManager methods to start the transfer.
*/
- (void)openCallTransfer;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,423 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKCallViewController">
<connections>
<outlet property="answerCallButton" destination="0DL-0m-uZo" id="iYW-vl-A5W"/>
<outlet property="audioMuteButton" destination="fxP-zM-kfT" id="J6M-is-5me"/>
<outlet property="backToAppButton" destination="nff-fB-sTq" id="UEn-bg-xuA"/>
<outlet property="backgroundImageView" destination="v1I-LH-wvv" id="HvS-WI-vnb"/>
<outlet property="callContainerView" destination="W2P-Hy-nV2" id="b0q-Lj-daE"/>
<outlet property="callControlContainerView" destination="nk9-Un-LVP" id="ZV3-qr-BtW"/>
<outlet property="callStatusLabel" destination="29y-MK-OWH" id="Kcn-MX-MUH"/>
<outlet property="callToolBar" destination="4h9-hF-BH8" id="2JU-Od-PBy"/>
<outlet property="callerImageView" destination="ImL-3N-2CJ" id="nkr-M1-2Be"/>
<outlet property="callerNameLabel" destination="IW8-8P-mS3" id="eaa-oo-l01"/>
<outlet property="endCallButton" destination="lVK-d8-Dqf" id="zgE-Go-bo6"/>
<outlet property="localPreviewActivityView" destination="Bhz-hS-5B6" id="2yA-xn-ITf"/>
<outlet property="localPreviewContainerView" destination="6gQ-zo-2Zw" id="6Ba-YZ-OmX"/>
<outlet property="localPreviewContainerViewHeightConstraint" destination="dZW-ZL-5rV" id="0qv-ne-6vn"/>
<outlet property="localPreviewContainerViewLeadingConstraint" destination="3Fs-0f-Aws" id="s4D-aK-1qx"/>
<outlet property="localPreviewContainerViewTopConstraint" destination="3kN-Qi-Klq" id="zT0-lf-noP"/>
<outlet property="localPreviewContainerViewWidthConstraint" destination="gyu-kv-SLy" id="h6P-sv-f69"/>
<outlet property="localPreviewVideoView" destination="Y9q-YD-ke6" id="X9G-e6-lDE"/>
<outlet property="moreButtonForVideo" destination="XCC-Xd-83F" id="exV-fE-Fcs"/>
<outlet property="moreButtonForVoice" destination="gp0-z0-MpD" id="Ksf-ku-aCv"/>
<outlet property="onHoldCallContainerView" destination="gYL-ur-SUZ" id="sWc-c5-84F"/>
<outlet property="overlayContainerView" destination="JAR-tn-sGN" id="09u-3G-1UA"/>
<outlet property="rejectCallButton" destination="cst-tR-uHV" id="Ust-PL-R6A"/>
<outlet property="remotePreviewContainerView" destination="Tjb-57-yB1" id="MaR-IC-ZKw"/>
<outlet property="resumeButton" destination="LXK-mz-xQl" id="FkJ-Ix-NuL"/>
<outlet property="speakerButton" destination="UHM-u9-ODN" id="PFN-R6-3b3"/>
<outlet property="transferButton" destination="WDH-nE-sZ3" id="ema-6K-xPO"/>
<outlet property="videoMuteButton" destination="QWZ-Xg-1VM" id="kbc-Ut-yse"/>
<outlet property="view" destination="iN0-l3-epB" id="JDF-cz-roW"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v1I-LH-wvv" userLabel="Background Image View" customClass="MXKImageView">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Tjb-57-yB1" userLabel="Remote Preview Container View">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<view hidden="YES" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6gQ-zo-2Zw" userLabel="Local Preview Container View">
<rect key="frame" x="20" y="20" width="100" height="120"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Y9q-YD-ke6" userLabel="Local Preview Video View">
<rect key="frame" x="0.0" y="0.0" width="100" height="120"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" hidesWhenStopped="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="Bhz-hS-5B6">
<rect key="frame" x="31.5" y="41.5" width="37" height="37"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" notEnabled="YES"/>
</accessibility>
<constraints>
<constraint firstAttribute="trailing" secondItem="Y9q-YD-ke6" secondAttribute="trailing" id="Vmf-vR-uO0"/>
<constraint firstAttribute="bottom" secondItem="Y9q-YD-ke6" secondAttribute="bottom" id="XkV-Gc-mGJ"/>
<constraint firstItem="Bhz-hS-5B6" firstAttribute="centerY" secondItem="6gQ-zo-2Zw" secondAttribute="centerY" id="dBH-9m-51R"/>
<constraint firstAttribute="height" constant="120" id="dZW-ZL-5rV"/>
<constraint firstAttribute="width" constant="100" id="gyu-kv-SLy"/>
<constraint firstItem="Y9q-YD-ke6" firstAttribute="leading" secondItem="6gQ-zo-2Zw" secondAttribute="leading" id="n2F-7p-0NR"/>
<constraint firstItem="Bhz-hS-5B6" firstAttribute="centerX" secondItem="6gQ-zo-2Zw" secondAttribute="centerX" id="vRe-Km-hqC"/>
<constraint firstItem="Y9q-YD-ke6" firstAttribute="top" secondItem="6gQ-zo-2Zw" secondAttribute="top" id="x3l-4B-yhP"/>
</constraints>
</view>
<view hidden="YES" opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gYL-ur-SUZ" userLabel="OnHold Call Container View">
<rect key="frame" x="255" y="20" width="100" height="120"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BnO-XU-rbO">
<rect key="frame" x="0.0" y="0.0" width="100" height="120"/>
<subviews>
<view contentMode="scaleAspectFill" translatesAutoresizingMaskIntoConstraints="NO" id="Yi0-Kz-f8n" userLabel="Caller Image View" customClass="MXKImageView">
<rect key="frame" x="0.0" y="0.0" width="100" height="120"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="call_paused_icon" translatesAutoresizingMaskIntoConstraints="NO" id="tDd-nl-o66">
<rect key="frame" x="40" y="50" width="20" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="4ZR-gY-TFU"/>
<constraint firstAttribute="height" constant="20" id="VOl-de-wx3"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="tDd-nl-o66" firstAttribute="centerY" secondItem="BnO-XU-rbO" secondAttribute="centerY" id="0WW-DK-ikE"/>
<constraint firstItem="Yi0-Kz-f8n" firstAttribute="top" secondItem="BnO-XU-rbO" secondAttribute="top" id="ATS-v3-un5"/>
<constraint firstAttribute="trailing" secondItem="Yi0-Kz-f8n" secondAttribute="trailing" id="Ems-Vb-x8T"/>
<constraint firstItem="tDd-nl-o66" firstAttribute="centerX" secondItem="BnO-XU-rbO" secondAttribute="centerX" id="KI3-Lj-zWM"/>
<constraint firstAttribute="bottom" secondItem="Yi0-Kz-f8n" secondAttribute="bottom" id="u9w-k7-Tu4"/>
<constraint firstItem="Yi0-Kz-f8n" firstAttribute="leading" secondItem="BnO-XU-rbO" secondAttribute="leading" id="yTR-nN-6l2"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" notEnabled="YES"/>
</accessibility>
<constraints>
<constraint firstItem="BnO-XU-rbO" firstAttribute="leading" secondItem="gYL-ur-SUZ" secondAttribute="leading" id="2tc-aB-SHR"/>
<constraint firstAttribute="width" constant="100" id="Wuk-6q-X0l"/>
<constraint firstAttribute="trailing" secondItem="BnO-XU-rbO" secondAttribute="trailing" id="YIx-kL-fSQ"/>
<constraint firstAttribute="height" constant="120" id="eQ5-RY-39A"/>
<constraint firstAttribute="bottom" secondItem="BnO-XU-rbO" secondAttribute="bottom" id="rRV-2N-Wrr"/>
<constraint firstItem="BnO-XU-rbO" firstAttribute="top" secondItem="gYL-ur-SUZ" secondAttribute="top" id="rlC-LI-TF2"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="8"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JAR-tn-sGN" userLabel="Overlay Container View">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nff-fB-sTq" userLabel="BackToApp">
<rect key="frame" x="20" y="5" width="40" height="28"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="28" id="U4K-iA-Ca2"/>
<constraint firstAttribute="width" constant="40" id="yAh-Gb-GtB"/>
</constraints>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<color key="tintColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="yKw-Aj-XkE"/>
</connections>
</button>
<view opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nk9-Un-LVP" userLabel="Call Control Container View">
<rect key="frame" x="0.0" y="329" width="375" height="55"/>
<subviews>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fxP-zM-kfT" userLabel="Audio Mute Btn">
<rect key="frame" x="16" y="0.0" width="56" height="55"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="CgE-f8-nPS"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QWZ-Xg-1VM" userLabel="Video Mute Btn">
<rect key="frame" x="88" y="0.0" width="55.5" height="55"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="yRT-8e-Sed"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UHM-u9-ODN" userLabel="Speaker Btn">
<rect key="frame" x="159.5" y="0.0" width="56" height="55"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="contentEdgeInsets" minX="0.0" minY="0.0" maxX="4" maxY="0.0"/>
<inset key="titleEdgeInsets" minX="-74" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="9Jd-Wv-foD"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="XCC-Xd-83F" userLabel="More Btn Video">
<rect key="frame" x="231.5" y="0.0" width="55.5" height="55"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="eev-cq-bkD"/>
</connections>
</button>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleAspectFit" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gp0-z0-MpD" userLabel="More Btn Voice">
<rect key="frame" x="303" y="0.0" width="56" height="55"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="lJr-dI-K66"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="XCC-Xd-83F" firstAttribute="leading" secondItem="UHM-u9-ODN" secondAttribute="trailing" constant="16" id="4qI-5H-xme"/>
<constraint firstItem="UHM-u9-ODN" firstAttribute="leading" secondItem="QWZ-Xg-1VM" secondAttribute="trailing" constant="16" id="4vU-TE-eHI"/>
<constraint firstItem="gp0-z0-MpD" firstAttribute="width" secondItem="fxP-zM-kfT" secondAttribute="width" id="65z-cx-uPN"/>
<constraint firstItem="gp0-z0-MpD" firstAttribute="top" secondItem="nk9-Un-LVP" secondAttribute="top" id="6Hn-cf-yrt"/>
<constraint firstItem="UHM-u9-ODN" firstAttribute="top" secondItem="nk9-Un-LVP" secondAttribute="top" id="9WM-jp-uJ2"/>
<constraint firstAttribute="bottom" secondItem="XCC-Xd-83F" secondAttribute="bottom" id="Mmc-f0-E3b"/>
<constraint firstItem="UHM-u9-ODN" firstAttribute="width" secondItem="fxP-zM-kfT" secondAttribute="width" id="QT2-3D-Rea"/>
<constraint firstAttribute="bottom" secondItem="fxP-zM-kfT" secondAttribute="bottom" id="Qly-U3-yZz"/>
<constraint firstAttribute="trailing" secondItem="gp0-z0-MpD" secondAttribute="trailing" constant="16" id="QnE-37-ZsF"/>
<constraint firstItem="QWZ-Xg-1VM" firstAttribute="leading" secondItem="fxP-zM-kfT" secondAttribute="trailing" constant="16" id="Tfa-tC-stW"/>
<constraint firstItem="gp0-z0-MpD" firstAttribute="leading" secondItem="XCC-Xd-83F" secondAttribute="trailing" constant="16" id="VFm-PI-KyK"/>
<constraint firstAttribute="bottom" secondItem="gp0-z0-MpD" secondAttribute="bottom" id="Y8l-mj-pQv"/>
<constraint firstItem="XCC-Xd-83F" firstAttribute="top" secondItem="nk9-Un-LVP" secondAttribute="top" id="daf-rp-QYh"/>
<constraint firstItem="XCC-Xd-83F" firstAttribute="width" secondItem="fxP-zM-kfT" secondAttribute="width" id="dfm-XQ-cI6"/>
<constraint firstAttribute="height" constant="55" id="eS5-qj-HTc"/>
<constraint firstAttribute="bottom" secondItem="UHM-u9-ODN" secondAttribute="bottom" id="g18-gQ-ckj"/>
<constraint firstItem="QWZ-Xg-1VM" firstAttribute="top" secondItem="nk9-Un-LVP" secondAttribute="top" id="g1k-AA-jae"/>
<constraint firstItem="QWZ-Xg-1VM" firstAttribute="width" secondItem="fxP-zM-kfT" secondAttribute="width" id="mrH-wc-BUa"/>
<constraint firstItem="fxP-zM-kfT" firstAttribute="leading" secondItem="nk9-Un-LVP" secondAttribute="leading" constant="16" id="ukn-jV-42y"/>
<constraint firstItem="fxP-zM-kfT" firstAttribute="top" secondItem="nk9-Un-LVP" secondAttribute="top" id="xKI-k3-zBC"/>
<constraint firstAttribute="bottom" secondItem="QWZ-Xg-1VM" secondAttribute="bottom" id="ygd-qE-SAS"/>
</constraints>
</view>
<view opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4h9-hF-BH8" userLabel="Call Bottom Bar View">
<rect key="frame" x="0.0" y="594" width="375" height="58"/>
<subviews>
<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lVK-d8-Dqf" userLabel="End Call Button">
<rect key="frame" x="15" y="4" width="345" height="50"/>
<color key="backgroundColor" red="0.75051294189999995" green="0.0078581154179999998" blue="0.015402332100000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<state key="normal" title="End Call">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="ZTw-gz-mwM"/>
</connections>
</button>
<button contentMode="center" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="wordWrap" translatesAutoresizingMaskIntoConstraints="NO" id="cst-tR-uHV" userLabel="Reject Call Button">
<rect key="frame" x="15" y="4" width="157.5" height="50"/>
<color key="backgroundColor" red="0.75051294191919193" green="0.0078581154183767592" blue="0.015402332100907003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title="Reject Call">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="njS-hU-ZzM"/>
</connections>
</button>
<button contentMode="center" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="wordWrap" translatesAutoresizingMaskIntoConstraints="NO" id="0DL-0m-uZo" userLabel="Answer Call Button">
<rect key="frame" x="202.5" y="4" width="157.5" height="50"/>
<color key="backgroundColor" red="0.0" green="0.72547743055555558" blue="0.0041503219763194587" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<state key="normal" title="Answer Call">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="gBy-fV-8DU"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="0DL-0m-uZo" secondAttribute="trailing" constant="15" id="8eS-wa-FjD"/>
<constraint firstItem="cst-tR-uHV" firstAttribute="top" secondItem="4h9-hF-BH8" secondAttribute="top" constant="4" id="8pa-OC-zyD"/>
<constraint firstAttribute="trailing" secondItem="lVK-d8-Dqf" secondAttribute="trailing" constant="15" id="EBB-PZ-ed6"/>
<constraint firstItem="0DL-0m-uZo" firstAttribute="top" secondItem="4h9-hF-BH8" secondAttribute="top" constant="4" id="RKq-zI-hBT"/>
<constraint firstItem="0DL-0m-uZo" firstAttribute="width" secondItem="cst-tR-uHV" secondAttribute="width" id="TuN-vE-uBt"/>
<constraint firstAttribute="bottom" secondItem="lVK-d8-Dqf" secondAttribute="bottom" constant="4" id="VQY-sr-2n1"/>
<constraint firstItem="lVK-d8-Dqf" firstAttribute="top" secondItem="4h9-hF-BH8" secondAttribute="top" constant="4" id="YKN-2x-fGu"/>
<constraint firstItem="lVK-d8-Dqf" firstAttribute="leading" secondItem="4h9-hF-BH8" secondAttribute="leading" constant="15" id="bIh-wJ-4Ax"/>
<constraint firstAttribute="centerY" secondItem="cst-tR-uHV" secondAttribute="centerY" id="byx-9k-bCx"/>
<constraint firstItem="0DL-0m-uZo" firstAttribute="leading" secondItem="cst-tR-uHV" secondAttribute="trailing" constant="30" id="hPu-76-1fr"/>
<constraint firstItem="cst-tR-uHV" firstAttribute="leading" secondItem="4h9-hF-BH8" secondAttribute="leading" constant="15" id="vUc-cr-gcl"/>
<constraint firstAttribute="height" constant="58" id="wE4-9n-mDx"/>
<constraint firstAttribute="centerY" secondItem="0DL-0m-uZo" secondAttribute="centerY" id="ydY-us-83K"/>
</constraints>
</view>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="W2P-Hy-nV2" userLabel="Call Container View">
<rect key="frame" x="0.0" y="40" width="375" height="259"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ImL-3N-2CJ" userLabel="Caller Image View" customClass="MXKImageView">
<rect key="frame" x="135.5" y="25" width="104" height="104"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="104" id="QfH-Wv-g9v"/>
<constraint firstAttribute="width" constant="104" id="vTq-Bb-ooO"/>
</constraints>
</view>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" text="Name" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="IW8-8P-mS3" userLabel="Caller Name Label">
<rect key="frame" x="0.0" y="134" width="375" height="32"/>
<fontDescription key="fontDescription" name="Helvetica" family="Helvetica" pointSize="27"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" text="-" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="12" translatesAutoresizingMaskIntoConstraints="NO" id="29y-MK-OWH" userLabel="Call Status Label">
<rect key="frame" x="0.0" y="171" width="375" height="15"/>
<fontDescription key="fontDescription" name="Helvetica" family="Helvetica" pointSize="13"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LXK-mz-xQl">
<rect key="frame" x="160" y="191" width="55" height="30"/>
<state key="normal" title="Resume"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="Ibj-GN-9lW"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="WDH-nE-sZ3" userLabel="Transfer">
<rect key="frame" x="159" y="229" width="57" height="30"/>
<state key="normal" title="Transfer"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="ZXg-sM-pdt"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="LXK-mz-xQl" firstAttribute="top" secondItem="29y-MK-OWH" secondAttribute="bottom" constant="5" id="4o3-Dw-noW"/>
<constraint firstItem="WDH-nE-sZ3" firstAttribute="top" secondItem="LXK-mz-xQl" secondAttribute="bottom" constant="8" id="DB6-wD-Btp"/>
<constraint firstItem="WDH-nE-sZ3" firstAttribute="centerX" secondItem="W2P-Hy-nV2" secondAttribute="centerX" id="IvV-zm-1J1"/>
<constraint firstItem="ImL-3N-2CJ" firstAttribute="top" secondItem="W2P-Hy-nV2" secondAttribute="top" constant="25" id="LYw-15-z57"/>
<constraint firstAttribute="trailing" secondItem="IW8-8P-mS3" secondAttribute="trailing" id="PsD-gD-a0S"/>
<constraint firstAttribute="trailing" secondItem="29y-MK-OWH" secondAttribute="trailing" id="Uo1-tj-IFA"/>
<constraint firstAttribute="centerX" secondItem="ImL-3N-2CJ" secondAttribute="centerX" id="XTc-Dz-fC7"/>
<constraint firstAttribute="height" constant="259" id="ZLw-UO-oa4"/>
<constraint firstItem="29y-MK-OWH" firstAttribute="top" secondItem="IW8-8P-mS3" secondAttribute="bottom" constant="5" id="af8-pz-JKx"/>
<constraint firstItem="IW8-8P-mS3" firstAttribute="top" secondItem="ImL-3N-2CJ" secondAttribute="bottom" constant="5" id="b1K-8G-BZc"/>
<constraint firstItem="29y-MK-OWH" firstAttribute="leading" secondItem="W2P-Hy-nV2" secondAttribute="leading" id="cOy-mL-qOS"/>
<constraint firstItem="LXK-mz-xQl" firstAttribute="centerX" secondItem="W2P-Hy-nV2" secondAttribute="centerX" id="ftJ-ZG-ZFw"/>
<constraint firstItem="IW8-8P-mS3" firstAttribute="leading" secondItem="W2P-Hy-nV2" secondAttribute="leading" id="peb-77-Gi5"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="nk9-Un-LVP" firstAttribute="leading" secondItem="JAR-tn-sGN" secondAttribute="leading" id="9wq-Y2-kZa"/>
<constraint firstItem="nff-fB-sTq" firstAttribute="top" secondItem="JAR-tn-sGN" secondAttribute="top" constant="5" id="C4F-59-09O"/>
<constraint firstItem="W2P-Hy-nV2" firstAttribute="leading" secondItem="JAR-tn-sGN" secondAttribute="leading" id="Dow-OM-vNZ"/>
<constraint firstItem="4h9-hF-BH8" firstAttribute="leading" secondItem="JAR-tn-sGN" secondAttribute="leading" id="Foo-x4-bMq"/>
<constraint firstItem="nk9-Un-LVP" firstAttribute="top" secondItem="W2P-Hy-nV2" secondAttribute="bottom" constant="30" id="NRK-rN-4S3"/>
<constraint firstAttribute="trailing" secondItem="W2P-Hy-nV2" secondAttribute="trailing" id="hPw-CT-hcl"/>
<constraint firstAttribute="trailing" secondItem="4h9-hF-BH8" secondAttribute="trailing" id="hi7-v3-bw5"/>
<constraint firstAttribute="bottom" secondItem="4h9-hF-BH8" secondAttribute="bottom" constant="15" id="hkd-BJ-xRf"/>
<constraint firstItem="nff-fB-sTq" firstAttribute="leading" secondItem="JAR-tn-sGN" secondAttribute="leading" constant="20" id="ubP-7v-uDn"/>
<constraint firstAttribute="trailing" secondItem="nk9-Un-LVP" secondAttribute="trailing" id="yES-yI-i3m"/>
<constraint firstItem="W2P-Hy-nV2" firstAttribute="top" secondItem="JAR-tn-sGN" secondAttribute="top" constant="40" id="yj4-Vr-mIA"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="gYL-ur-SUZ" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="20" id="2Vf-YX-syR"/>
<constraint firstItem="6gQ-zo-2Zw" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" id="3Fs-0f-Aws"/>
<constraint firstItem="6gQ-zo-2Zw" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="20" id="3kN-Qi-Klq"/>
<constraint firstAttribute="trailing" secondItem="JAR-tn-sGN" secondAttribute="trailing" id="IGf-YS-JEj"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="6gQ-zo-2Zw" secondAttribute="trailing" id="IZd-Uv-ucs"/>
<constraint firstAttribute="bottom" secondItem="JAR-tn-sGN" secondAttribute="bottom" id="KDu-Kr-vji"/>
<constraint firstAttribute="trailing" secondItem="gYL-ur-SUZ" secondAttribute="trailing" constant="20" id="TbF-td-23k"/>
<constraint firstItem="v1I-LH-wvv" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="VF9-js-1ub"/>
<constraint firstItem="JAR-tn-sGN" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="Z6W-gz-KKT"/>
<constraint firstItem="Tjb-57-yB1" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="aKy-Ol-bhc"/>
<constraint firstAttribute="bottom" secondItem="Tjb-57-yB1" secondAttribute="bottom" id="akt-pO-kWM"/>
<constraint firstItem="v1I-LH-wvv" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="cq1-0k-TFN"/>
<constraint firstAttribute="trailing" secondItem="v1I-LH-wvv" secondAttribute="trailing" id="eL2-a4-lJd"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="6gQ-zo-2Zw" secondAttribute="bottom" id="hS1-8y-3t8"/>
<constraint firstItem="Tjb-57-yB1" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="mW3-RA-hx2"/>
<constraint firstAttribute="bottom" secondItem="v1I-LH-wvv" secondAttribute="bottom" id="o10-x3-vgo"/>
<constraint firstItem="JAR-tn-sGN" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="pei-Ab-VHX"/>
<constraint firstAttribute="trailing" secondItem="Tjb-57-yB1" secondAttribute="trailing" id="rXn-dm-69F"/>
</constraints>
<point key="canvasLocation" x="-215.19999999999999" y="129.98500749625188"/>
</view>
</objects>
<resources>
<image name="call_paused_icon" width="24" height="24"/>
</resources>
</document>
@@ -0,0 +1,90 @@
/*
Copyright 2015 OpenMarket 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 <UIKit/UIKit.h>
#import "MXKTableViewController.h"
#import "MXKContact.h"
@class MXKContactDetailsViewController;
/**
`MXKContactDetailsViewController` delegate.
*/
@protocol MXKContactDetailsViewControllerDelegate <NSObject>
/**
Tells the delegate that the user wants to start chat with the contact by using the selected matrix id.
@param contactDetailsViewController the `MXKContactDetailsViewController` instance.
@param matrixId the selected matrix id of the contact.
@param completion the block to execute at the end of the operation (independently if it succeeded or not).
*/
- (void)contactDetailsViewController:(MXKContactDetailsViewController *)contactDetailsViewController startChatWithMatrixId:(NSString*)matrixId completion:(void (^)(void))completion;
@end
@interface MXKContactDetailsViewController : MXKTableViewController
@property (weak, nonatomic) IBOutlet UIButton *contactThumbnail;
@property (weak, nonatomic) IBOutlet UITextView *contactDisplayName;
/**
The default account picture displayed when no picture is defined.
*/
@property (nonatomic) UIImage *picturePlaceholder;
/**
The displayed contact
*/
@property (strong, nonatomic) MXKContact* contact;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKContactDetailsViewControllerDelegate> delegate;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKContactDetailsViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `contactDetailsViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKContactDetailsViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKContactDetailsViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)contactDetailsViewController;
/**
The contact's thumbnail is displayed inside a button. The following action is registered on
`UIControlEventTouchUpInside` event of this button.
*/
- (IBAction)onContactThumbnailPressed:(id)sender;
@end
@@ -0,0 +1,207 @@
/*
Copyright 2015 OpenMarket 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 "MXKContactDetailsViewController.h"
#import "MXKTableViewCellWithLabelAndButton.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
@interface MXKContactDetailsViewController ()
{
NSArray* matrixIDs;
}
@end
@implementation MXKContactDetailsViewController
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKContactDetailsViewController class])
bundle:[NSBundle bundleForClass:[MXKContactDetailsViewController class]]];
}
+ (instancetype)contactDetailsViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKContactDetailsViewController class])
bundle:[NSBundle bundleForClass:[MXKContactDetailsViewController class]]];
}
- (void)finalizeInit
{
[super finalizeInit];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_contactThumbnail)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
[self updatePictureButton:self.picturePlaceholder];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThumbnailUpdate:) name:kMXKContactThumbnailUpdateNotification object:nil];
// Force refresh
self.contact = _contact;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)destroy
{
matrixIDs = nil;
self.delegate = nil;
[super destroy];
}
#pragma mark -
- (void)setContact:(MXKContact *)contact
{
_contact = contact;
self.contactDisplayName.text = _contact.displayName;
// set the thumbnail info
[self.contactThumbnail.imageView setContentMode: UIViewContentModeScaleAspectFill];
[self.contactThumbnail.imageView setClipsToBounds:YES];
if (_contact.thumbnail)
{
[self updatePictureButton:_contact.thumbnail];
}
else
{
[self updatePictureButton:self.picturePlaceholder];
}
}
- (UIImage*)picturePlaceholder
{
return [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"default-profile"];
}
- (IBAction)onContactThumbnailPressed:(id)sender
{
// Do nothing by default
}
#pragma mark - UITableView datasource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
matrixIDs = _contact.matrixIdentifiers;
return matrixIDs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger row = indexPath.row;
MXKTableViewCellWithLabelAndButton *cell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithLabelAndButton defaultReuseIdentifier]];
if (!cell)
{
cell = [[MXKTableViewCellWithLabelAndButton alloc] init];
}
if (row < matrixIDs.count)
{
cell.mxkLabel.text = [matrixIDs objectAtIndex:row];
}
else
{
// should never happen
cell.mxkLabel.text = @"";
}
[cell.mxkButton setTitle:[MatrixKitL10n startChat] forState:UIControlStateNormal];
[cell.mxkButton setTitle:[MatrixKitL10n startChat] forState:UIControlStateHighlighted];
cell.mxkButton.tag = row;
[cell.mxkButton addTarget:self action:@selector(startChat:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
#pragma mark - Internals
- (void)updatePictureButton:(UIImage*)image
{
[self.contactThumbnail setImage:image forState:UIControlStateNormal];
[self.contactThumbnail setImage:image forState:UIControlStateHighlighted];
[self.contactThumbnail setImage:image forState:UIControlStateDisabled];
}
- (void)startChat:(UIButton*)sender
{
if (self.delegate && sender.tag < matrixIDs.count)
{
sender.enabled = NO;
[self.delegate contactDetailsViewController:self startChatWithMatrixId:[matrixIDs objectAtIndex:sender.tag] completion:^{
sender.enabled = YES;
}];
}
}
- (void)onThumbnailUpdate:(NSNotification *)notif
{
// sanity check
if ([notif.object isKindOfClass:[NSString class]])
{
NSString* contactID = notif.object;
if ([contactID isEqualToString:self.contact.contactID])
{
if (_contact.thumbnail)
{
[self updatePictureButton:_contact.thumbnail];
}
else
{
[self updatePictureButton:self.picturePlaceholder];
}
}
}
}
@end
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKContactDetailsViewController">
<connections>
<outlet property="contactDisplayName" destination="j7K-fD-zCi" id="YCz-xN-r7s"/>
<outlet property="contactThumbnail" destination="Q5C-z0-PFC" id="fmX-T6-ees"/>
<outlet property="view" destination="ju7-p5-cp4" id="1a8-31-bad"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="ju7-p5-cp4">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="gTa-z3-gJI" userLabel="TableHeader">
<rect key="frame" x="0.0" y="0.0" width="375" height="115"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" clipsSubviews="YES" contentMode="scaleAspectFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q5C-z0-PFC" userLabel="contact picture Button">
<rect key="frame" x="10" y="10" width="75" height="75"/>
<color key="backgroundColor" red="0.89720267057418823" green="0.89720267057418823" blue="0.89720267057418823" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="75" id="h4a-sn-FGX"/>
<constraint firstAttribute="height" constant="75" id="u5z-VM-exL"/>
</constraints>
<state key="normal">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onContactThumbnailPressed:" destination="-1" eventType="touchUpInside" id="7Xc-a3-N46"/>
</connections>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j7K-fD-zCi" userLabel="contact displayName">
<rect key="frame" x="95" y="10" width="270" height="95"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Q5C-z0-PFC" firstAttribute="leading" secondItem="gTa-z3-gJI" secondAttribute="leading" constant="10" id="SBo-aU-k5D"/>
<constraint firstItem="j7K-fD-zCi" firstAttribute="leading" secondItem="Q5C-z0-PFC" secondAttribute="trailing" constant="10" id="Tyv-6p-V4Q"/>
<constraint firstAttribute="trailing" secondItem="j7K-fD-zCi" secondAttribute="trailing" constant="10" id="Uc5-Z1-ISb"/>
<constraint firstAttribute="bottom" secondItem="j7K-fD-zCi" secondAttribute="bottom" constant="10" id="WPd-8P-yk2"/>
<constraint firstItem="Q5C-z0-PFC" firstAttribute="top" secondItem="gTa-z3-gJI" secondAttribute="top" constant="10" id="mdK-nI-1Z6"/>
<constraint firstItem="j7K-fD-zCi" firstAttribute="top" secondItem="gTa-z3-gJI" secondAttribute="top" constant="10" id="r1j-GD-Wko"/>
</constraints>
</view>
<connections>
<outlet property="dataSource" destination="-1" id="KAH-DP-g22"/>
<outlet property="delegate" destination="-1" id="Uz1-g8-Ykk"/>
</connections>
</tableView>
</objects>
</document>
@@ -0,0 +1,122 @@
/*
Copyright 2015 OpenMarket 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 <MatrixSDK/MatrixSDK.h>
#import "MXKTableViewController.h"
#import "MXKContactManager.h"
#import "MXKContact.h"
#import "MXKContactTableCell.h"
@class MXKContactListViewController;
/**
`MXKContactListViewController` delegate.
*/
@protocol MXKContactListViewControllerDelegate <NSObject>
/**
Tells the delegate that the user selected a contact.
@param contactListViewController the `MXKContactListViewController` instance.
@param contactId the id of the selected contact.
*/
- (void)contactListViewController:(MXKContactListViewController *)contactListViewController didSelectContact:(NSString*)contactId;
/**
Tells the delegate that the user tapped a contact thumbnail.
@param contactListViewController the `MXKContactListViewController` instance.
@param contactId the id of the tapped contact.
*/
- (void)contactListViewController:(MXKContactListViewController *)contactListViewController didTapContactThumbnail:(NSString*)contactId;
@end
/**
'MXKContactListViewController' instance displays constact list.
This view controller support multi sessions by collecting all matrix users (only one occurrence is kept by user).
*/
@interface MXKContactListViewController : MXKTableViewController <UINavigationControllerDelegate, UISearchBarDelegate, UIGestureRecognizerDelegate, MXKCellRenderingDelegate>
/**
The segmented control used to handle separatly matrix users and local contacts.
User's actions are handled by [MXKContactListViewController onSegmentValueChange:].
*/
@property (weak, nonatomic) IBOutlet UISegmentedControl* contactsControls;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKContactListViewControllerDelegate> delegate;
/**
Enable the search option by adding a navigation item in the navigation bar (YES by default).
Set NO this property to disable this option and hide the related bar button.
*/
@property (nonatomic) BOOL enableBarButtonSearch;
/**
Tell whether an action is already in progress.
*/
@property (nonatomic, readonly) BOOL hasPendingAction;
/**
The class used in creating new contact table cells.
Only MXKContactTableCell classes or sub-classes are accepted.
*/
@property (nonatomic) Class contactTableViewCellClass;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKContactListViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `contactListViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKContactListViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKContactListViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)contactListViewController;
/**
The action registered on 'value changed' event of the 'UISegmentedControl' contactControls.
*/
- (IBAction)onSegmentValueChange:(id)sender;
/**
Add a mask in overlay to prevent a new contact selection (used when an action is on progress).
*/
- (void)addPendingActionMask;
/**
Remove the potential overlay mask
*/
- (void)removePendingActionMask;
@end
@@ -0,0 +1,663 @@
/*
Copyright 2015 OpenMarket 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 "MXKContactListViewController.h"
#import "MXKSectionedContacts.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
@interface MXKContactListViewController ()
{
// YES -> only matrix users
// NO -> display local contacts
BOOL displayMatrixUsers;
// screenshot of the local contacts
NSArray* localContactsArray;
MXKSectionedContacts* sectionedLocalContacts;
// screenshot of the matrix users
NSArray* matrixContactsArray;
MXKSectionedContacts* sectionedMatrixContacts;
// Search
UIBarButtonItem *searchButton;
UISearchBar *contactsSearchBar;
NSMutableArray *filteredContacts;
MXKSectionedContacts* sectionedFilteredContacts;
BOOL searchBarShouldEndEditing;
BOOL ignoreSearchRequest;
NSString* latestSearchedPattern;
NSArray* collationTitles;
// mask view while processing a request
UIActivityIndicatorView * pendingMaskSpinnerView;
}
@end
@implementation MXKContactListViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKContactListViewController class])
bundle:[NSBundle bundleForClass:[MXKContactListViewController class]]];
}
+ (instancetype)contactListViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKContactListViewController class])
bundle:[NSBundle bundleForClass:[MXKContactListViewController class]]];
}
- (void)finalizeInit
{
[super finalizeInit];
_enableBarButtonSearch = YES;
// get the system collation titles
collationTitles = [[UILocalizedIndexedCollation currentCollation] sectionTitles];
}
- (void)dealloc
{
searchButton = nil;
}
- (void)destroy
{
[self removePendingActionMask];
[super destroy];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_contactsControls)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
// global init
displayMatrixUsers = (0 == self.contactsControls.selectedSegmentIndex);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactsRefresh:) name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactsRefresh:) name:kMXKContactManagerDidUpdateLocalContactsNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onContactsRefresh:) name:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil];
if (!_contactTableViewCellClass)
{
// Set default table view cell class
self.contactTableViewCellClass = [MXKContactTableCell class];
}
// Localize string
[_contactsControls setTitle:[MatrixKitL10n contactMxUsers] forSegmentAtIndex:0];
[_contactsControls setTitle:[MatrixKitL10n contactLocalContacts] forSegmentAtIndex:1];
// Apply search option in navigation bar
self.enableBarButtonSearch = _enableBarButtonSearch;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Restore search mechanism (if enabled)
ignoreSearchRequest = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
// Leave potential search session
if (contactsSearchBar)
{
[self searchBarCancelButtonClicked:contactsSearchBar];
}
}
- (void)scrollToTop
{
// stop any scrolling effect
[UIView setAnimationsEnabled:NO];
// before scrolling to the tableview top
self.tableView.contentOffset = CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top);
[UIView setAnimationsEnabled:YES];
}
#pragma mark -
-(void)setContactTableViewCellClass:(Class)contactTableViewCellClass
{
// Sanity check: accept only MXKContactTableCell classes or sub-classes
NSParameterAssert([contactTableViewCellClass isSubclassOfClass:MXKContactTableCell.class]);
_contactTableViewCellClass = contactTableViewCellClass;
[self.tableView registerClass:contactTableViewCellClass forCellReuseIdentifier:[contactTableViewCellClass defaultReuseIdentifier]];
}
- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch
{
_enableBarButtonSearch = enableBarButtonSearch;
if (enableBarButtonSearch)
{
if (!searchButton)
{
searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
}
// Add it in right bar items
NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems;
self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:searchButton] : @[searchButton];
}
else
{
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems];
[rightBarButtonItems removeObject:searchButton];
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
}
}
#pragma mark - Internals
- (void)updateSectionedLocalContacts:(BOOL)force
{
[self stopActivityIndicator];
MXKContactManager* sharedManager = [MXKContactManager sharedManager];
if (force || !localContactsArray)
{
localContactsArray = sharedManager.localContacts;
sectionedLocalContacts = [sharedManager getSectionedContacts:localContactsArray];
}
}
- (void)updateSectionedMatrixContacts:(BOOL)force
{
[self stopActivityIndicator];
MXKContactManager* sharedManager = [MXKContactManager sharedManager];
if (force || !matrixContactsArray)
{
matrixContactsArray = sharedManager.matrixContacts;
sectionedMatrixContacts = [sharedManager getSectionedContacts:matrixContactsArray];
}
}
- (BOOL)hasPendingAction
{
return nil != pendingMaskSpinnerView;
}
- (void)addPendingActionMask
{
// add a spinner above the tableview to avoid that the user tap on any other button
pendingMaskSpinnerView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
pendingMaskSpinnerView.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:0.5];
pendingMaskSpinnerView.frame = self.tableView.frame;
pendingMaskSpinnerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
// append it
[self.tableView.superview addSubview:pendingMaskSpinnerView];
// animate it
[pendingMaskSpinnerView startAnimating];
}
- (void)removePendingActionMask
{
if (pendingMaskSpinnerView)
{
[pendingMaskSpinnerView removeFromSuperview];
pendingMaskSpinnerView = nil;
[self.tableView reloadData];
}
}
#pragma mark - UITableView dataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSInteger sectionNb;
// search in progress
if (contactsSearchBar)
{
sectionNb = sectionedFilteredContacts.sectionedContacts.count;
if (!sectionNb)
{
// Keep at least one section to display the search bar
sectionNb = 1;
}
}
else if (displayMatrixUsers)
{
[self updateSectionedMatrixContacts:NO];
sectionNb = sectionedMatrixContacts.sectionedContacts.count;
}
else
{
[self updateSectionedLocalContacts:NO];
sectionNb = sectionedLocalContacts.sectionedContacts.count;
}
return sectionNb;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
MXKSectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts);
if (section < sectionedContacts.sectionedContacts.count)
{
return [sectionedContacts.sectionedContacts[section] count];
}
return 0;
}
- (NSString *)tableView:(UITableView *)aTableView titleForHeaderInSection:(NSInteger)section
{
if (contactsSearchBar)
{
// Hide section titles during search session
return nil;
}
MXKSectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts;
if (section < sectionedContacts.sectionTitles.count)
{
return (NSString*)[sectionedContacts.sectionTitles objectAtIndex:section];
}
return nil;
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)aTableView
{
// do not display the collation during a search
if (contactsSearchBar)
{
return nil;
}
return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
}
- (NSInteger)tableView:(UITableView *)aTableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
MXKSectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts;
NSInteger section = [sectionedContacts.sectionTitles indexOfObject:title];
// undefined title -> jump to the first valid non empty section
if (NSNotFound == section)
{
NSInteger systemCollationIndex = [collationTitles indexOfObject:title];
// find in the system collation
if (NSNotFound != systemCollationIndex)
{
systemCollationIndex--;
while ((systemCollationIndex >= 0) && (NSNotFound == section))
{
NSString* systemTitle = [collationTitles objectAtIndex:systemCollationIndex];
section = [sectionedContacts.sectionTitles indexOfObject:systemTitle];
systemCollationIndex--;
}
}
}
return section;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MXKContactTableCell* cell = [tableView dequeueReusableCellWithIdentifier:[_contactTableViewCellClass defaultReuseIdentifier] forIndexPath:indexPath];
cell.thumbnailDisplayBoxType = MXKTableViewCellDisplayBoxTypeCircle;
MXKSectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts);
MXKContact* contact = nil;
if (indexPath.section < sectionedContacts.sectionedContacts.count)
{
NSArray *thisSection = [sectionedContacts.sectionedContacts objectAtIndex:indexPath.section];
if (indexPath.row < thisSection.count)
{
contact = [thisSection objectAtIndex:indexPath.row];
}
}
if (contact)
{
cell.contactAccessoryViewType = MXKContactTableCellAccessoryMatrixIcon;
[cell render:contact];
cell.delegate = self;
}
return cell;
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MXKSectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts);
MXKContact* contact = nil;
if (indexPath.section < sectionedContacts.sectionedContacts.count)
{
NSArray *thisSection = [sectionedContacts.sectionedContacts objectAtIndex:indexPath.section];
if (indexPath.row < thisSection.count)
{
contact = [thisSection objectAtIndex:indexPath.row];
}
}
return [((Class<MXKCellRendering>)_contactTableViewCellClass) heightForCellData:contact withMaximumWidth:tableView.frame.size.width];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// In case of search, the section titles are hidden and the search bar is displayed in first section header.
if (contactsSearchBar)
{
if (section == 0)
{
return contactsSearchBar.frame.size.height;
}
return 0;
}
// Default section header height
return 22;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (contactsSearchBar && section == 0)
{
return contactsSearchBar;
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
MXKSectionedContacts* sectionedContacts = contactsSearchBar ? sectionedFilteredContacts : (displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts);
MXKContact* contact = nil;
if (indexPath.section < sectionedContacts.sectionedContacts.count)
{
NSArray *thisSection = [sectionedContacts.sectionedContacts objectAtIndex:indexPath.section];
if (indexPath.row < thisSection.count)
{
contact = [thisSection objectAtIndex:indexPath.row];
}
}
if (self.delegate) {
[self.delegate contactListViewController:self didSelectContact:contact.contactID];
}
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Release here resources, and restore reusable cells
if ([cell respondsToSelector:@selector(didEndDisplay)])
{
[(id<MXKCellRendering>)cell didEndDisplay];
}
}
#pragma mark - Actions
- (void)onContactsRefresh:(NSNotification *)notif
{
if ([notif.name isEqualToString:kMXKContactManagerDidUpdateMatrixContactsNotification])
{
[self updateSectionedMatrixContacts:YES];
}
else if ([notif.name isEqualToString:kMXKContactManagerDidUpdateLocalContactsNotification])
{
[self updateSectionedLocalContacts:YES];
}
else //if ([notif.name isEqualToString:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification])
{
// Consider here only global notifications, ignore notifications related to a specific contact.
if (notif.object)
{
return;
}
[self updateSectionedLocalContacts:YES];
}
if (contactsSearchBar)
{
latestSearchedPattern = nil;
[self searchBar:contactsSearchBar textDidChange:contactsSearchBar.text];
}
else
{
[self.tableView reloadData];
}
}
- (IBAction)onSegmentValueChange:(id)sender
{
if (sender == self.contactsControls)
{
displayMatrixUsers = (0 == self.contactsControls.selectedSegmentIndex);
// Leave potential search session
if (contactsSearchBar)
{
[self searchBarCancelButtonClicked:contactsSearchBar];
}
[self.tableView reloadData];
}
}
#pragma mark Search management
- (void)search:(id)sender
{
// The user may have pressed search button whereas the view controller was disappearing
if (ignoreSearchRequest)
{
return;
}
if (!contactsSearchBar)
{
MXKSectionedContacts* sectionedContacts = displayMatrixUsers ? sectionedMatrixContacts : sectionedLocalContacts;
// Check whether there are data in which search
if (sectionedContacts.sectionedContacts.count > 0)
{
// Create search bar
contactsSearchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 44)];
contactsSearchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
contactsSearchBar.showsCancelButton = YES;
contactsSearchBar.returnKeyType = UIReturnKeyDone;
contactsSearchBar.delegate = self;
searchBarShouldEndEditing = NO;
// init the table content
latestSearchedPattern = @"";
filteredContacts = [(displayMatrixUsers ? matrixContactsArray : localContactsArray) mutableCopy];
sectionedFilteredContacts = [[MXKContactManager sharedManager] getSectionedContacts:filteredContacts];
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
[self->contactsSearchBar becomeFirstResponder];
});
}
}
else
{
[self searchBarCancelButtonClicked:contactsSearchBar];
}
}
#pragma mark - UISearchBarDelegate
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar
{
searchBarShouldEndEditing = NO;
return YES;
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar
{
return searchBarShouldEndEditing;
}
- (NSArray*)patternsFromText:(NSString*)text
{
NSArray* items = [text componentsSeparatedByString:@" "];
if (items.count <= 1)
{
return items;
}
NSMutableArray* patterns = [[NSMutableArray alloc] init];
for (NSString* item in items)
{
if (item.length > 0)
{
[patterns addObject:item];
}
}
return patterns;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if ((contactsSearchBar == searchBar) && (![latestSearchedPattern isEqualToString:searchText]))
{
latestSearchedPattern = searchText;
// contacts
NSArray* contacts = displayMatrixUsers ? matrixContactsArray : localContactsArray;
// Update filtered list
if (searchText.length && contacts.count)
{
filteredContacts = [[NSMutableArray alloc] init];
NSArray* patterns = [self patternsFromText:searchText];
for(MXKContact* contact in contacts)
{
if ([contact matchedWithPatterns:patterns])
{
[filteredContacts addObject:contact];
}
}
}
else
{
filteredContacts = [contacts mutableCopy];
}
sectionedFilteredContacts = [[MXKContactManager sharedManager] getSectionedContacts:filteredContacts];
// Refresh display
[self.tableView reloadData];
[self scrollToTop];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
if (contactsSearchBar == searchBar)
{
// "Done" key has been pressed
searchBarShouldEndEditing = YES;
[contactsSearchBar resignFirstResponder];
}
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
if (contactsSearchBar == searchBar)
{
// Leave search
searchBarShouldEndEditing = YES;
[contactsSearchBar resignFirstResponder];
[contactsSearchBar removeFromSuperview];
contactsSearchBar = nil;
filteredContacts = nil;
sectionedFilteredContacts = nil;
latestSearchedPattern = nil;
[self.tableView reloadData];
[self scrollToTop];
}
}
#pragma mark - MXKCellRendering delegate
- (void)cell:(id<MXKCellRendering>)cell didRecognizeAction:(NSString*)actionIdentifier userInfo:(NSDictionary *)userInfo
{
if ([actionIdentifier isEqualToString:kMXKContactCellTapOnThumbnailView])
{
if (self.delegate) {
[self.delegate contactListViewController:self didTapContactThumbnail:userInfo[kMXKContactCellContactIdKey]];
}
}
}
- (BOOL)cell:(id<MXKCellRendering>)cell shouldDoAction:(NSString *)actionIdentifier userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue
{
// No such action yet on contacts
return defaultValue;
}
@end
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKContactListViewController">
<connections>
<outlet property="contactsControls" destination="75L-rc-grT" id="ocP-jP-5yq"/>
<outlet property="view" destination="bSe-Sh-aQf" id="4es-L5-nel"/>
</connections>
</placeholder>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="bSe-Sh-aQf">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="jGj-32-rtx">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" selected="YES" contentHorizontalAlignment="center" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="75L-rc-grT">
<rect key="frame" x="82" y="8" width="211" height="29"/>
<segments>
<segment title="Matrix Users"/>
<segment title="Local Contacts"/>
</segments>
<connections>
<action selector="onSegmentValueChange:" destination="-1" eventType="valueChanged" id="hSC-ZI-SSu"/>
</connections>
</segmentedControl>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="75L-rc-grT" firstAttribute="top" secondItem="jGj-32-rtx" secondAttribute="top" constant="8" id="KTm-E6-bIU"/>
<constraint firstAttribute="centerX" secondItem="75L-rc-grT" secondAttribute="centerX" id="chL-eY-CYt"/>
</constraints>
</view>
<connections>
<outlet property="dataSource" destination="-1" id="nhc-tM-1py"/>
<outlet property="delegate" destination="-1" id="9uq-Pc-RpB"/>
</connections>
</tableView>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
</objects>
</document>
@@ -0,0 +1,81 @@
/*
Copyright 2017 Vector Creations 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 <MatrixSDK/MatrixSDK.h>
#import "MXKTableViewController.h"
@class MXKCountryPickerViewController;
/**
`MXKCountryPickerViewController` delegate.
*/
@protocol MXKCountryPickerViewControllerDelegate <NSObject>
/**
Tells the delegate that the user selected a country.
@param countryPickerViewController the `MXKCountryPickerViewController` instance.
@param isoCountryCode the ISO 3166-1 country code representation.
*/
- (void)countryPickerViewController:(MXKCountryPickerViewController*)countryPickerViewController didSelectCountry:(NSString*)isoCountryCode;
@end
/**
'MXKCountryPickerViewController' instance displays the list of supported countries.
*/
@interface MXKCountryPickerViewController : MXKTableViewController <UISearchResultsUpdating>
/**
The searchController used to manage search.
*/
@property (nonatomic, strong) UISearchController *searchController;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKCountryPickerViewControllerDelegate> delegate;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKCountryPickerViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `countryPickerViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKCountryPickerViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKCountryPickerViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)countryPickerViewController;
/**
Show/Hide the international dialing code for each country (NO by default).
*/
@property (nonatomic) BOOL showCountryCallingCode;
@end
@@ -0,0 +1,299 @@
/*
Copyright 2017 Vector Creations 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 "MXKCountryPickerViewController.h"
@import libPhoneNumber_iOS;
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
NSString* const kMXKCountryPickerViewControllerCountryCellId = @"kMXKCountryPickerViewControllerCountryCellId";
@interface MXKCountryPickerViewController ()
{
NSMutableDictionary<NSString*, NSString*> *isoCountryCodesByCountryName;
NSArray<NSString*> *countryNames;
NSMutableArray<NSString*> *filteredCountryNames;
NSString *previousSearchPattern;
NSMutableDictionary<NSString*, NSNumber*> *callingCodesByCountryName;
}
@end
@implementation MXKCountryPickerViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKCountryPickerViewController class])
bundle:[NSBundle bundleForClass:[MXKCountryPickerViewController class]]];
}
+ (instancetype)countryPickerViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKCountryPickerViewController class])
bundle:[NSBundle bundleForClass:[MXKCountryPickerViewController class]]];
}
- (void)finalizeInit
{
[super finalizeInit];
NSArray *isoCountryCodes = [NSLocale ISOCountryCodes];
NSMutableArray<NSString*> *countries;
isoCountryCodesByCountryName = [NSMutableDictionary dictionaryWithCapacity:isoCountryCodes.count];
countries = [NSMutableArray arrayWithCapacity:isoCountryCodes.count];
NSLocale *local = [[NSLocale alloc] initWithLocaleIdentifier:[[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0]];
for (NSString *isoCountryCode in isoCountryCodes)
{
NSString *country = [local displayNameForKey:NSLocaleCountryCode value:isoCountryCode];
if (country)
{
[countries addObject: country];
isoCountryCodesByCountryName[country] = isoCountryCode;
}
}
countryNames = [countries sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
previousSearchPattern = nil;
filteredCountryNames = nil;
_showCountryCallingCode = NO;
}
- (void)destroy
{
[super destroy];
isoCountryCodesByCountryName = nil;
countryNames = nil;
filteredCountryNames = nil;
callingCodesByCountryName = nil;
previousSearchPattern = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!self.tableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
self.navigationItem.title = [MatrixKitL10n countryPickerTitle];
[self setupSearchController];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationItem.hidesSearchBarWhenScrolling = YES;
}
#pragma mark -
- (void)setShowCountryCallingCode:(BOOL)showCountryCallingCode
{
if (_showCountryCallingCode != showCountryCallingCode)
{
_showCountryCallingCode = showCountryCallingCode;
if (_showCountryCallingCode && !callingCodesByCountryName)
{
callingCodesByCountryName = [NSMutableDictionary dictionary];
for (NSString *countryName in countryNames)
{
NSString *isoCountryCode = isoCountryCodesByCountryName[countryName];
NSNumber *callingCode = [[NBPhoneNumberUtil sharedInstance] getCountryCodeForRegion:isoCountryCode];
callingCodesByCountryName[countryName] = callingCode;
}
}
[self.tableView reloadData];
}
}
#pragma mark - Private
- (void)setupSearchController
{
UISearchController *searchController = [[UISearchController alloc]
initWithSearchResultsController:nil];
searchController.dimsBackgroundDuringPresentation = NO;
searchController.hidesNavigationBarDuringPresentation = NO;
searchController.searchResultsUpdater = self;
self.navigationItem.searchController = searchController;
// Make the search bar visible on first view appearance
self.navigationItem.hidesSearchBarWhenScrolling = NO;
self.definesPresentationContext = YES;
self.searchController = searchController;
}
#pragma mark - UITableView dataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (filteredCountryNames)
{
return filteredCountryNames.count;
}
return countryNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:kMXKCountryPickerViewControllerCountryCellId];
if (!cell)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:kMXKCountryPickerViewControllerCountryCellId];
}
NSInteger index = indexPath.row;
NSString *countryName;
if (filteredCountryNames)
{
if (index < filteredCountryNames.count)
{
countryName = filteredCountryNames[index];
}
}
else if (index < countryNames.count)
{
countryName = countryNames[index];
}
if (countryName)
{
cell.textLabel.text = countryName;
if (self.showCountryCallingCode)
{
cell.detailTextLabel.text = [NSString stringWithFormat:@"+%@", [callingCodesByCountryName[countryName] stringValue]];
}
}
return cell;
}
#pragma mark - UITableView delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (self.delegate)
{
NSInteger index = indexPath.row;
NSString *countryName;
if (filteredCountryNames)
{
if (index < filteredCountryNames.count)
{
countryName = filteredCountryNames[index];
}
}
else if (index < countryNames.count)
{
countryName = countryNames[index];
}
if (countryName)
{
NSString *isoCountryCode = isoCountryCodesByCountryName[countryName];
[self.delegate countryPickerViewController:self didSelectCountry:isoCountryCode];
}
}
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchText = searchController.searchBar.text;
if (searchText.length)
{
searchText = [searchText lowercaseString];
if (previousSearchPattern && [searchText hasPrefix:previousSearchPattern])
{
for (NSUInteger index = 0; index < filteredCountryNames.count;)
{
NSString *countryName = [filteredCountryNames[index] lowercaseString];
if ([countryName hasPrefix:searchText] == NO)
{
[filteredCountryNames removeObjectAtIndex:index];
}
else
{
index++;
}
}
}
else
{
filteredCountryNames = [NSMutableArray array];
for (NSUInteger index = 0; index < countryNames.count; index++)
{
NSString *countryName = [countryNames[index] lowercaseString];
if ([countryName hasPrefix:searchText])
{
[filteredCountryNames addObject:countryNames[index]];
}
}
}
previousSearchPattern = searchText;
}
else
{
previousSearchPattern = nil;
filteredCountryNames = nil;
}
[self.tableView reloadData];
}
@end
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKCountryPickerViewController">
<connections>
<outlet property="view" destination="bSe-Sh-aQf" id="4es-L5-nel"/>
</connections>
</placeholder>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="bSe-Sh-aQf">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<outlet property="dataSource" destination="-1" id="nhc-tM-1py"/>
<outlet property="delegate" destination="-1" id="9uq-Pc-RpB"/>
</connections>
<point key="canvasLocation" x="-206" y="152"/>
</tableView>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
</objects>
</document>
@@ -0,0 +1,118 @@
/*
Copyright 2017 Vector Creations 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 <UIKit/UIKit.h>
#import "MXKViewController.h"
#import "MXKSessionGroupsDataSource.h"
@class MXKGroupListViewController;
/**
`MXKGroupListViewController` delegate.
*/
@protocol MXKGroupListViewControllerDelegate <NSObject>
/**
Tells the delegate that the user selected a group.
@param groupListViewController the `MXKGroupListViewController` instance.
@param group the selected group.
@param mxSession the matrix session in which the group is defined.
*/
- (void)groupListViewController:(MXKGroupListViewController *)groupListViewController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)mxSession;
@end
/**
This view controller displays a group list.
*/
@interface MXKGroupListViewController : MXKViewController <MXKDataSourceDelegate, UITableViewDelegate, UISearchBarDelegate>
{
@protected
/**
The fake top view displayed in case of vertical bounce.
*/
__weak UIView *topview;
}
@property (weak, nonatomic) IBOutlet UISearchBar *groupsSearchBar;
@property (weak, nonatomic) IBOutlet UITableView *groupsTableView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *groupsSearchBarTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *groupsSearchBarHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *groupsTableViewBottomConstraint;
/**
The current data source associated to the view controller.
*/
@property (nonatomic, readonly) MXKSessionGroupsDataSource *dataSource;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKGroupListViewControllerDelegate> delegate;
/**
Enable the search option by adding a navigation item in the navigation bar (YES by default).
Set NO this property to disable this option and hide the related bar button.
*/
@property (nonatomic) BOOL enableBarButtonSearch;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKGroupListViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `groupListViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKGroupListViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKGroupListViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)groupListViewController;
/**
Display the groups described in the provided data source.
Note: The provided data source will replace the current data source if any. The caller
should dispose properly this data source if it is not used anymore.
@param listDataSource the data source providing the groups list.
*/
- (void)displayList:(MXKSessionGroupsDataSource*)listDataSource;
/**
Refresh the groups table display.
*/
- (void)refreshGroupsTable;
/**
Hide/show the search bar at the top of the groups table view.
*/
- (void)hideSearchBar:(BOOL)hidden;
@end
@@ -0,0 +1,608 @@
/*
Copyright 2017 Vector Creations 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 "MXKGroupListViewController.h"
#import "MXKGroupTableViewCell.h"
#import "MXKTableViewHeaderFooterWithLabel.h"
@interface MXKGroupListViewController ()
{
/**
The data source providing UITableViewCells
*/
MXKSessionGroupsDataSource *dataSource;
/**
Search handling
*/
UIBarButtonItem *searchButton;
BOOL ignoreSearchRequest;
/**
The reconnection animated view.
*/
UIView* reconnectingView;
/**
The current table view header if any.
*/
UIView* tableViewHeaderView;
/**
The latest server sync date
*/
NSDate* latestServerSync;
/**
The restart the event connnection
*/
BOOL restartConnection;
}
@end
@implementation MXKGroupListViewController
@synthesize dataSource;
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKGroupListViewController class])
bundle:[NSBundle bundleForClass:[MXKGroupListViewController class]]];
}
+ (instancetype)groupListViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKGroupListViewController class])
bundle:[NSBundle bundleForClass:[MXKGroupListViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
_enableBarButtonSearch = YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_groupsTableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
// Adjust search bar Top constraint to take into account potential navBar.
if (_groupsSearchBarTopConstraint)
{
[NSLayoutConstraint deactivateConstraints:@[_groupsSearchBarTopConstraint]];
_groupsSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.groupsSearchBar
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
[NSLayoutConstraint activateConstraints:@[_groupsSearchBarTopConstraint]];
}
// Adjust table view Bottom constraint to take into account tabBar.
if (_groupsTableViewBottomConstraint)
{
[NSLayoutConstraint deactivateConstraints:@[_groupsTableViewBottomConstraint]];
_groupsTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.groupsTableView
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0f];
[NSLayoutConstraint activateConstraints:@[_groupsTableViewBottomConstraint]];
}
// Hide search bar by default
[self hideSearchBar:YES];
// Apply search option in navigation bar
self.enableBarButtonSearch = _enableBarButtonSearch;
// Add an accessory view to the search bar in order to retrieve keyboard view.
self.groupsSearchBar.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
// Finalize table view configuration
// Note: self-sizing cells and self-sizing section headers are enabled from the nib file.
self.groupsTableView.delegate = self;
self.groupsTableView.dataSource = dataSource; // Note: dataSource may be nil here
self.groupsTableView.estimatedSectionHeaderHeight = 30; // The value set in the nib seems not available for iOS version < 10.
// Set up classes to use for the cells and the section headers.
[self.groupsTableView registerNib:MXKGroupTableViewCell.nib forCellReuseIdentifier:MXKGroupTableViewCell.defaultReuseIdentifier];
[self.groupsTableView registerNib:MXKTableViewHeaderFooterWithLabel.nib forHeaderFooterViewReuseIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier];
// Add a top view which will be displayed in case of vertical bounce.
CGFloat height = self.groupsTableView.frame.size.height;
UIView *topview = [[UIView alloc] initWithFrame:CGRectMake(0,-height,self.groupsTableView.frame.size.width,height)];
topview.autoresizingMask = UIViewAutoresizingFlexibleWidth;
topview.backgroundColor = [UIColor groupTableViewBackgroundColor];
[self.groupsTableView addSubview:topview];
self->topview = topview;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Restore search mechanism (if enabled)
ignoreSearchRequest = NO;
// Observe the server sync
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncNotification) name:kMXSessionDidSyncNotification object:nil];
// Do a full reload
[self refreshGroupsTable];
// Refresh all groups summary
[self.dataSource refreshGroupsSummary:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
// Leave potential search session
if (!self.groupsSearchBar.isHidden)
{
[self searchBarCancelButtonClicked:self.groupsSearchBar];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidSyncNotification object:nil];
[self removeReconnectingView];
}
- (void)dealloc
{
self.groupsSearchBar.inputAccessoryView = nil;
searchButton = nil;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Override MXKViewController
- (void)onKeyboardShowAnimationComplete
{
// Report the keyboard view in order to track keyboard frame changes
self.keyboardView = _groupsSearchBar.inputAccessoryView.superview;
}
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
// Deduce the bottom constraint for the table view (Don't forget the potential tabBar)
CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
// Check whether the keyboard is over the tabBar
if (tableViewBottomConst < 0)
{
tableViewBottomConst = 0;
}
// Update constraints
_groupsTableViewBottomConstraint.constant = tableViewBottomConst;
// Force layout immediately to take into account new constraint
[self.view layoutIfNeeded];
}
- (void)destroy
{
self.groupsTableView.dataSource = nil;
self.groupsTableView.delegate = nil;
self.groupsTableView = nil;
dataSource.delegate = nil;
dataSource = nil;
_delegate = nil;
[topview removeFromSuperview];
topview = nil;
[super destroy];
}
#pragma mark -
- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch
{
_enableBarButtonSearch = enableBarButtonSearch;
if (enableBarButtonSearch)
{
if (!searchButton)
{
searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
}
// Add it in right bar items
NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems;
self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:searchButton] : @[searchButton];
}
else
{
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems];
[rightBarButtonItems removeObject:searchButton];
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
}
}
- (void)displayList:(MXKSessionGroupsDataSource *)listDataSource
{
// Cancel registration on existing dataSource if any
if (dataSource)
{
dataSource.delegate = nil;
// Remove associated matrix sessions
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
[self removeMatrixSession:mxSession];
}
}
dataSource = listDataSource;
dataSource.delegate = self;
// Report the matrix session at view controller level to update UI according to session state
[self addMatrixSession:listDataSource.mxSession];
if (self.groupsTableView)
{
// Set up table data source
self.groupsTableView.dataSource = dataSource;
}
}
- (void)refreshGroupsTable
{
// For now, do a simple full reload
[self.groupsTableView reloadData];
}
- (void)hideSearchBar:(BOOL)hidden
{
self.groupsSearchBar.hidden = hidden;
self.groupsSearchBarHeightConstraint.constant = hidden ? 0 : 44;
[self.view setNeedsUpdateConstraints];
}
#pragma mark - Action
- (IBAction)search:(id)sender
{
// The user may have pressed search button whereas the view controller was disappearing
if (ignoreSearchRequest)
{
return;
}
if (self.groupsSearchBar.isHidden)
{
// Check whether there are data in which search
if ([self.dataSource numberOfSectionsInTableView:self.groupsTableView])
{
[self hideSearchBar:NO];
// Create search bar
[self.groupsSearchBar becomeFirstResponder];
}
}
else
{
[self searchBarCancelButtonClicked: self.groupsSearchBar];
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
// Return the default group table view cell
return MXKGroupTableViewCell.class;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
// Return the default group table view cell
return MXKGroupTableViewCell.defaultReuseIdentifier;
}
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
// For now, do a simple full reload
[self refreshGroupsTable];
}
- (void)dataSource:(MXKDataSource *)dataSource didAddMatrixSession:(MXSession *)mxSession
{
[self addMatrixSession:mxSession];
}
- (void)dataSource:(MXKDataSource *)dataSource didRemoveMatrixSession:(MXSession *)mxSession
{
[self removeMatrixSession:mxSession];
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return tableView.estimatedRowHeight;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForHeaderInSection:(NSInteger)section
{
if (tableView.numberOfSections > 1)
{
return tableView.estimatedSectionHeaderHeight;
}
return 0;
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
// Refresh here the estimated row height
tableView.estimatedRowHeight = cell.frame.size.height;
}
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(nonnull UIView *)view forSection:(NSInteger)section
{
// Refresh here the estimated header height
tableView.estimatedSectionHeaderHeight = view.frame.size.height;
}
- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
MXKTableViewHeaderFooterWithLabel *sectionHeader;
if (tableView.numberOfSections > 1)
{
sectionHeader = [tableView dequeueReusableHeaderFooterViewWithIdentifier:MXKTableViewHeaderFooterWithLabel.defaultReuseIdentifier];
sectionHeader.mxkLabel.text = [self.dataSource tableView:tableView titleForHeaderInSection:section];
}
return sectionHeader;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_delegate)
{
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
if ([selectedCell conformsToProtocol:@protocol(MXKCellRendering)])
{
id<MXKCellRendering> cell = (id<MXKCellRendering>)selectedCell;
if ([cell respondsToSelector:@selector(renderedCellData)])
{
MXKCellData *cellData = cell.renderedCellData;
if ([cellData conformsToProtocol:@protocol(MXKGroupCellDataStoring)])
{
id<MXKGroupCellDataStoring> groupCellData = (id<MXKGroupCellDataStoring>)cellData;
[_delegate groupListViewController:self didSelectGroup:groupCellData.group inMatrixSession:self.mainSession];
}
}
}
}
// Hide the keyboard when user select a room
// do not hide the searchBar until the view controller disappear
// on tablets / iphone 6+, the user could expect to search again while looking at a room
[self.groupsSearchBar resignFirstResponder];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Release here resources, and restore reusable cells
if ([cell respondsToSelector:@selector(didEndDisplay)])
{
[(id<MXKCellRendering>)cell didEndDisplay];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Detect vertical bounce at the top of the tableview to trigger reconnection.
if (scrollView == _groupsTableView)
{
[self detectPullToKick:scrollView];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (scrollView == _groupsTableView)
{
[self managePullToKick:scrollView];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == _groupsTableView)
{
if (scrollView.contentOffset.y + scrollView.adjustedContentInset.top == 0)
{
[self managePullToKick:scrollView];
}
}
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
// Apply filter
if (searchText.length)
{
[self.dataSource searchWithPatterns:@[searchText]];
}
else
{
[self.dataSource searchWithPatterns:nil];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// "Done" key has been pressed
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
// Leave search
[searchBar resignFirstResponder];
[self hideSearchBar:YES];
self.groupsSearchBar.text = nil;
// Refresh display
[self.dataSource searchWithPatterns:nil];
}
#pragma mark - resync management
- (void)onSyncNotification
{
latestServerSync = [NSDate date];
MXWeakify(self);
// Refresh all groups summary
[self.dataSource refreshGroupsSummary:^{
MXStrongifyAndReturnIfNil(self);
[self removeReconnectingView];
}];
}
- (BOOL)canReconnect
{
// avoid restarting connection if some data has been received within 1 second (1000 : latestServerSync is null)
NSTimeInterval interval = latestServerSync ? [[NSDate date] timeIntervalSinceDate:latestServerSync] : 1000;
return (interval > 1) && [self.mainSession reconnect];
}
- (void)addReconnectingView
{
if (!reconnectingView)
{
UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f);
CGRect frame = spinner.frame;
frame.size.height = 80; // 80 * 0.75 = 60
spinner.bounds = frame;
spinner.color = [UIColor darkGrayColor];
spinner.hidesWhenStopped = NO;
spinner.backgroundColor = _groupsTableView.backgroundColor;
[spinner startAnimating];
// no need to manage constraints here, IOS defines them.
tableViewHeaderView = _groupsTableView.tableHeaderView;
_groupsTableView.tableHeaderView = reconnectingView = spinner;
}
}
- (void)removeReconnectingView
{
if (reconnectingView && !restartConnection)
{
_groupsTableView.tableHeaderView = tableViewHeaderView;
reconnectingView = nil;
}
}
/**
Detect if the current connection must be restarted.
The spinner is displayed until the overscroll ends (and scrollViewDidEndDecelerating is called).
*/
- (void)detectPullToKick:(UIScrollView *)scrollView
{
if (!reconnectingView)
{
// detect if the user scrolls over the tableview top
restartConnection = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top < -128);
if (restartConnection)
{
// wait that list decelerate to display / hide it
[self addReconnectingView];
}
}
}
/**
Restarts the current connection if it is required.
The 0.3s delay is added to avoid flickering if the connection does not require to be restarted.
*/
- (void)managePullToKick:(UIScrollView *)scrollView
{
// the current connection must be restarted
if (restartConnection)
{
// display at least 0.3s the spinner to show to the user that something is pending
// else the UI is flickering
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self->restartConnection = NO;
if (![self canReconnect])
{
// if the event stream has not been restarted
// hide the spinner
[self removeReconnectingView];
}
// else wait that onSyncNotification is called.
});
}
}
@end
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKGroupListViewController">
<connections>
<outlet property="groupsSearchBar" destination="Zbr-9e-VZh" id="lBZ-hu-yJo"/>
<outlet property="groupsSearchBarHeightConstraint" destination="jCU-cq-2OA" id="Nl4-Wn-L98"/>
<outlet property="groupsSearchBarTopConstraint" destination="aRg-Nz-enq" id="mDZ-YB-gRX"/>
<outlet property="groupsTableView" destination="orV-HH-88x" id="pZt-dT-XPy"/>
<outlet property="groupsTableViewBottomConstraint" destination="m4x-32-odR" id="4hx-dQ-8aj"/>
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" showsCancelButton="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zbr-9e-VZh">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="jCU-cq-2OA"/>
</constraints>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="-1" id="ejZ-kN-jKv"/>
</connections>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="70" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="30" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="orV-HH-88x">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="NaR-eJ-WMj"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="aRg-Nz-enq"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="fty-XB-tTr"/>
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="Zbr-9e-VZh" secondAttribute="bottom" id="lBa-P2-Vnx"/>
<constraint firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="m4x-32-odR"/>
<constraint firstAttribute="trailing" secondItem="Zbr-9e-VZh" secondAttribute="trailing" id="rKb-TZ-sap"/>
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="yBp-63-kZi"/>
</constraints>
</view>
</objects>
</document>
@@ -0,0 +1,104 @@
/*
Copyright 2017 Vector Creations 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 "MXKTableViewController.h"
@class MXKLanguagePickerViewController;
/**
`MXKLanguagePickerViewController` delegate.
*/
@protocol MXKLanguagePickerViewControllerDelegate <NSObject>
/**
Tells the delegate that the user has selected a language.
@param languagePickerViewController the `MXKLanguagePickerViewController` instance.
@param language the ISO language code. nil means use the language chosen by the OS.
*/
- (void)languagePickerViewController:(MXKLanguagePickerViewController*)languagePickerViewController didSelectLangugage:(NSString*)language;
@end
/**
'MXKLanguagePickerViewController' instance displays the list of languages.
For the moment, it displays only languages available in the application bundle.
*/
@interface MXKLanguagePickerViewController : MXKTableViewController <UISearchResultsUpdating>
/**
The searchController used to manage search.
*/
@property (nonatomic, strong) UISearchController *searchController;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKLanguagePickerViewControllerDelegate> delegate;
/**
The language marked in the list.
@"" by default.
*/
@property (nonatomic) NSString *selectedLanguage;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKLanguagePickerViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `listViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKLanguagePickerViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKLanguagePickerViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)languagePickerViewController;
/**
Get the description string of a language defined by its ISO country code.
The description is localised in this language.
@param language the ISO country code of the language (ex: "en").
@return its description (ex: "English").
*/
+ (NSString *)languageDescription:(NSString*)language;
/**
Get the localised description string of a language defined by its ISO country code.
@param language the ISO country code of the language (ex: "en").
@return its localised description (ex: "Anglais" on a device running in French).
*/
+ (NSString *)languageLocalisedDescription:(NSString *)language;
/**
Get the ISO country code of the language selected by the OS according to
the device language and languages available in the app bundle.
*/
+ (NSString *)defaultLanguage;
@end
@@ -0,0 +1,308 @@
/*
Copyright 2017 Vector Creations 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 "MXKLanguagePickerViewController.h"
@import libPhoneNumber_iOS;
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
NSString* const kMXKLanguagePickerViewControllerCellId = @"kMXKLanguagePickerViewControllerCellId";
NSString* const kMXKLanguagePickerCellDataKeyText = @"text";
NSString* const kMXKLanguagePickerCellDataKeyDetailText = @"detailText";
NSString* const kMXKLanguagePickerCellDataKeyLanguage = @"language";
@interface MXKLanguagePickerViewController ()
{
NSMutableArray<NSDictionary*> *cellDataArray;
NSMutableArray<NSDictionary*> *filteredCellDataArray;
NSString *previousSearchPattern;
}
@end
@implementation MXKLanguagePickerViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKLanguagePickerViewController class])
bundle:[NSBundle bundleForClass:[MXKLanguagePickerViewController class]]];
}
+ (instancetype)languagePickerViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKLanguagePickerViewController class])
bundle:[NSBundle bundleForClass:[MXKLanguagePickerViewController class]]];
}
+ (NSString *)languageDescription:(NSString *)language
{
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:language];
return [locale displayNameForKey:NSLocaleIdentifier value:language];
}
+ (NSString *)languageLocalisedDescription:(NSString *)language
{
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:[NSBundle mainBundle].preferredLocalizations.firstObject];
return [locale displayNameForKey:NSLocaleIdentifier value:language];
}
+ (NSString *)defaultLanguage
{
return [NSBundle mainBundle].preferredLocalizations.firstObject;
}
- (void)finalizeInit
{
[super finalizeInit];
cellDataArray = [NSMutableArray array];
filteredCellDataArray = nil;
previousSearchPattern = nil;
// Populate cellDataArray
// Start by the default language chosen by the OS
NSString *defaultLanguage = [MXKLanguagePickerViewController defaultLanguage];
NSString *languageDescription = [MatrixKitL10n languagePickerDefaultLanguage:[MXKLanguagePickerViewController languageDescription:defaultLanguage]];
[cellDataArray addObject:@{
kMXKLanguagePickerCellDataKeyText:languageDescription
}];
// Then, add languages available in the app bundle
NSArray<NSString *> *localizations = [[NSBundle mainBundle] localizations];
for (NSString *language in localizations)
{
// Do not duplicate the default lang
if (![language isEqualToString:defaultLanguage])
{
languageDescription = [MXKLanguagePickerViewController languageDescription:language];
NSString *localisedLanguageDescription = [MXKLanguagePickerViewController languageLocalisedDescription:language];
// Capitalise the description in the language locale
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:language];
languageDescription = [languageDescription capitalizedStringWithLocale:locale];
localisedLanguageDescription = [localisedLanguageDescription capitalizedStringWithLocale:locale];
if (languageDescription)
{
[cellDataArray addObject:@{
kMXKLanguagePickerCellDataKeyText: languageDescription,
kMXKLanguagePickerCellDataKeyDetailText: localisedLanguageDescription,
kMXKLanguagePickerCellDataKeyLanguage: language
}];
}
}
}
// Default to "" in order to differentiate it from nil
_selectedLanguage = @"";
}
- (void)destroy
{
[super destroy];
cellDataArray = nil;
filteredCellDataArray = nil;
previousSearchPattern = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!self.tableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
[self setupSearchController];
self.navigationItem.title = [MatrixKitL10n languagePickerTitle];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.navigationItem.hidesSearchBarWhenScrolling = YES;
}
#pragma mark - Private
- (void)setupSearchController
{
UISearchController *searchController = [[UISearchController alloc]
initWithSearchResultsController:nil];
searchController.dimsBackgroundDuringPresentation = NO;
searchController.hidesNavigationBarDuringPresentation = NO;
searchController.searchResultsUpdater = self;
// Search bar is hidden for the moment, uncomment following line to enable it.
// TODO: Enable it once we have enough translations to fill pages and pages
// self.navigationItem.searchController = searchController;
// Make the search bar visible on first view appearance
self.navigationItem.hidesSearchBarWhenScrolling = NO;
self.definesPresentationContext = YES;
self.searchController = searchController;
}
#pragma mark - UITableView dataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (filteredCellDataArray)
{
return filteredCellDataArray.count;
}
return cellDataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:kMXKLanguagePickerViewControllerCellId];
if (!cell)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kMXKLanguagePickerViewControllerCellId];
}
NSInteger index = indexPath.row;
NSDictionary *itemCellData;
if (filteredCellDataArray)
{
if (index < filteredCellDataArray.count)
{
itemCellData = filteredCellDataArray[index];
}
}
else if (index < cellDataArray.count)
{
itemCellData = cellDataArray[index];
}
if (itemCellData)
{
cell.textLabel.text = itemCellData[kMXKLanguagePickerCellDataKeyText];
cell.detailTextLabel.text = itemCellData[kMXKLanguagePickerCellDataKeyDetailText];
// Mark the cell with the selected language
if (_selectedLanguage == itemCellData[kMXKLanguagePickerCellDataKeyLanguage] || [_selectedLanguage isEqualToString:itemCellData[kMXKLanguagePickerCellDataKeyLanguage]])
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
return cell;
}
#pragma mark - UITableView delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (self.delegate)
{
NSInteger index = indexPath.row;
NSString *language;
if (filteredCellDataArray)
{
if (index < filteredCellDataArray.count)
{
language = filteredCellDataArray[index][kMXKLanguagePickerCellDataKeyLanguage];
}
}
else if (index < cellDataArray.count)
{
language = cellDataArray[index][kMXKLanguagePickerCellDataKeyLanguage];
}
[self.delegate languagePickerViewController:self didSelectLangugage:language];
}
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchText = searchController.searchBar.text;
if (searchText.length)
{
searchText = [searchText lowercaseString];
if (previousSearchPattern && [searchText hasPrefix:previousSearchPattern])
{
for (NSUInteger index = 0; index < filteredCellDataArray.count;)
{
NSString *text = [filteredCellDataArray[index][kMXKLanguagePickerCellDataKeyText] lowercaseString];
if ([text hasPrefix:searchText] == NO)
{
[filteredCellDataArray removeObjectAtIndex:index];
}
else
{
index++;
}
}
}
else
{
filteredCellDataArray = [NSMutableArray array];
for (NSUInteger index = 0; index < cellDataArray.count; index++)
{
NSString *text = [cellDataArray[index][kMXKLanguagePickerCellDataKeyText] lowercaseString];
if ([text hasPrefix:searchText])
{
[filteredCellDataArray addObject:cellDataArray[index]];
}
}
}
previousSearchPattern = searchText;
}
else
{
previousSearchPattern = nil;
filteredCellDataArray = nil;
}
}
@end
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKLanguagePickerViewController">
<connections>
<outlet property="view" destination="bSe-Sh-aQf" id="4es-L5-nel"/>
</connections>
</placeholder>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="bSe-Sh-aQf">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<outlet property="dataSource" destination="-1" id="nhc-tM-1py"/>
<outlet property="delegate" destination="-1" id="9uq-Pc-RpB"/>
</connections>
<point key="canvasLocation" x="140" y="154"/>
</tableView>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
</objects>
</document>
@@ -0,0 +1,34 @@
/*
Copyright 2015 OpenMarket 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 <MatrixSDK/MatrixSDK.h>
#import "MXKTableViewController.h"
#import "MXKAccount.h"
/**
'MXKNotificationSettingsViewController' instance may be used to display the notification settings (account's push rules).
Presently only the Global notification settings are supported.
*/
@interface MXKNotificationSettingsViewController : MXKTableViewController
/**
The account who owns the displayed notification settings.
*/
@property (nonatomic) MXKAccount *mxAccount;
@end
@@ -0,0 +1,637 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 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 "MXKNotificationSettingsViewController.h"
#import "MXKTableViewCellWithButton.h"
#import "MXKPushRuleTableViewCell.h"
#import "MXKPushRuleCreationTableViewCell.h"
#import "MXKTableViewCellWithTextView.h"
#import "MXKConstants.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
#define MXKNOTIFICATIONSETTINGS_SECTION_INTRO_INDEX 0
#define MXKNOTIFICATIONSETTINGS_SECTION_PER_WORD_INDEX 1
#define MXKNOTIFICATIONSETTINGS_SECTION_PER_ROOM_INDEX 2
#define MXKNOTIFICATIONSETTINGS_SECTION_PER_SENDER_INDEX 3
#define MXKNOTIFICATIONSETTINGS_SECTION_OTHERS_INDEX 4
#define MXKNOTIFICATIONSETTINGS_SECTION_DEFAULT_INDEX 5
#define MXKNOTIFICATIONSETTINGS_SECTION_COUNT 6
@interface MXKNotificationSettingsViewController ()
{
/**
Handle master rule state
*/
UIButton *ruleMasterButton;
BOOL areAllDisabled;
/**
*/
NSInteger contentRuleCreationIndex;
NSInteger roomRuleCreationIndex;
NSInteger senderRuleCreationIndex;
/**
Predefined rules index
*/
NSInteger ruleContainsUserNameIndex;
NSInteger ruleContainsDisplayNameIndex;
NSInteger ruleOneToOneRoomIndex;
NSInteger ruleInviteForMeIndex;
NSInteger ruleMemberEventIndex;
NSInteger ruleCallIndex;
NSInteger ruleSuppressBotsNotificationsIndex;
/**
Notification center observers
*/
id notificationCenterWillUpdateObserver;
id notificationCenterDidUpdateObserver;
id notificationCenterDidFailObserver;
}
@end
@implementation MXKNotificationSettingsViewController
- (void)finalizeInit
{
[super finalizeInit];
}
- (void)dealloc
{
ruleMasterButton = nil;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)destroy
{
if (notificationCenterWillUpdateObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:notificationCenterWillUpdateObserver];
notificationCenterWillUpdateObserver = nil;
}
if (notificationCenterDidUpdateObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:notificationCenterDidUpdateObserver];
notificationCenterDidUpdateObserver = nil;
}
if (notificationCenterDidFailObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:notificationCenterDidFailObserver];
notificationCenterDidFailObserver = nil;
}
[super destroy];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (_mxAccount)
{
[self startActivityIndicator];
// Refresh existing notification rules
[_mxAccount.mxSession.notificationCenter refreshRules:^{
[self stopActivityIndicator];
[self.tableView reloadData];
} failure:^(NSError *error) {
[self stopActivityIndicator];
}];
notificationCenterWillUpdateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXNotificationCenterWillUpdateRules object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self startActivityIndicator];
}];
notificationCenterDidUpdateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXNotificationCenterDidUpdateRules object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self stopActivityIndicator];
[self.tableView reloadData];
}];
notificationCenterDidFailObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXNotificationCenterDidFailRulesUpdate object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[self stopActivityIndicator];
// Notify MatrixKit user
NSString *myUserId = self.mxAccount.mxCredentials.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:note.userInfo[kMXNotificationCenterErrorKey] userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}];
}
// Refresh display
[self.tableView reloadData];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (notificationCenterWillUpdateObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:notificationCenterWillUpdateObserver];
notificationCenterWillUpdateObserver = nil;
}
if (notificationCenterDidUpdateObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:notificationCenterDidUpdateObserver];
notificationCenterDidUpdateObserver = nil;
}
if (notificationCenterDidFailObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:notificationCenterDidFailObserver];
notificationCenterDidFailObserver = nil;
}
}
#pragma mark - Actions
- (IBAction)onButtonPressed:(id)sender
{
if (sender == ruleMasterButton)
{
// Swap enable state for all noticiations
MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterDisableAllNotificationsRuleID];
if (pushRule)
{
[_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled];
}
}
}
#pragma mark - UITableView data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Check master rule state
MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterDisableAllNotificationsRuleID];
if (pushRule.enabled)
{
areAllDisabled = YES;
return 1;
}
else
{
areAllDisabled = NO;
return MXKNOTIFICATIONSETTINGS_SECTION_COUNT;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger count = 0;
if (section == MXKNOTIFICATIONSETTINGS_SECTION_INTRO_INDEX)
{
count = 2;
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_PER_WORD_INDEX)
{
// A first cell will display a user information
count = 1;
// Only removable content rules are listed in this section (we ignore here predefined rules)
for (MXPushRule *pushRule in _mxAccount.mxSession.notificationCenter.rules.global.content)
{
if (!pushRule.isDefault)
{
count++;
}
}
// Add one item to suggest new rule creation
contentRuleCreationIndex = count ++;
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_PER_ROOM_INDEX)
{
count = _mxAccount.mxSession.notificationCenter.rules.global.room.count;
// Add one item to suggest new rule creation
roomRuleCreationIndex = count ++;
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_PER_SENDER_INDEX)
{
count = _mxAccount.mxSession.notificationCenter.rules.global.sender.count;
// Add one item to suggest new rule creation
senderRuleCreationIndex = count ++;
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_OTHERS_INDEX)
{
ruleContainsUserNameIndex = ruleContainsDisplayNameIndex = ruleOneToOneRoomIndex = ruleInviteForMeIndex = ruleMemberEventIndex = ruleCallIndex = ruleSuppressBotsNotificationsIndex = -1;
// Check whether each predefined rule is supported
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterContainUserNameRuleID])
{
ruleContainsUserNameIndex = count++;
}
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterContainDisplayNameRuleID])
{
ruleContainsDisplayNameIndex = count++;
}
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterOneToOneRoomRuleID])
{
ruleOneToOneRoomIndex = count++;
}
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterInviteMeRuleID])
{
ruleInviteForMeIndex = count++;
}
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterMemberEventRuleID])
{
ruleMemberEventIndex = count++;
}
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterCallRuleID])
{
ruleCallIndex = count++;
}
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterSuppressBotsNotificationsRuleID])
{
ruleSuppressBotsNotificationsIndex = count++;
}
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_DEFAULT_INDEX)
{
if ([_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterAllOtherRoomMessagesRuleID])
{
count = 1;
}
}
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = nil;
NSInteger rowIndex = indexPath.row;
if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_INTRO_INDEX)
{
if (indexPath.row == 0)
{
MXKTableViewCellWithButton *masterBtnCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]];
if (!masterBtnCell)
{
masterBtnCell = [[MXKTableViewCellWithButton alloc] init];
}
if (areAllDisabled)
{
[masterBtnCell.mxkButton setTitle:[MatrixKitL10n notificationSettingsEnableNotifications] forState:UIControlStateNormal];
[masterBtnCell.mxkButton setTitle:[MatrixKitL10n notificationSettingsEnableNotifications] forState:UIControlStateHighlighted];
}
else
{
[masterBtnCell.mxkButton setTitle:[MatrixKitL10n notificationSettingsDisableAll] forState:UIControlStateNormal];
[masterBtnCell.mxkButton setTitle:[MatrixKitL10n notificationSettingsDisableAll] forState:UIControlStateHighlighted];
}
[masterBtnCell.mxkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
ruleMasterButton = masterBtnCell.mxkButton;
cell = masterBtnCell;
}
else
{
MXKTableViewCellWithTextView *introCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithTextView defaultReuseIdentifier]];
if (!introCell)
{
introCell = [[MXKTableViewCellWithTextView alloc] init];
}
if (areAllDisabled)
{
introCell.mxkTextView.text = [MatrixKitL10n notificationSettingsEnableNotificationsWarning];
introCell.mxkTextView.backgroundColor = [UIColor redColor];
}
else
{
introCell.mxkTextView.text = [MatrixKitL10n notificationSettingsGlobalInfo];
introCell.mxkTextView.backgroundColor = [UIColor clearColor];
}
introCell.mxkTextView.font = [UIFont systemFontOfSize:14];
cell = introCell;
}
}
else if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_PER_WORD_INDEX)
{
if (rowIndex == 0)
{
MXKTableViewCellWithTextView *introCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithTextView defaultReuseIdentifier]];
if (!introCell)
{
introCell = [[MXKTableViewCellWithTextView alloc] init];
}
introCell.mxkTextView.text = [MatrixKitL10n notificationSettingsPerWordInfo];
introCell.mxkTextView.font = [UIFont systemFontOfSize:14];
cell = introCell;
}
else if (rowIndex == contentRuleCreationIndex)
{
MXKPushRuleCreationTableViewCell *pushRuleCreationCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleCreationTableViewCell defaultReuseIdentifier]];
if (!pushRuleCreationCell)
{
pushRuleCreationCell = [[MXKPushRuleCreationTableViewCell alloc] init];
}
pushRuleCreationCell.mxSession = _mxAccount.mxSession;
pushRuleCreationCell.mxPushRuleKind = MXPushRuleKindContent;
cell = pushRuleCreationCell;
}
else
{
// Only removable content rules are listed in this section
NSInteger count = 0;
for (MXPushRule *pushRule in _mxAccount.mxSession.notificationCenter.rules.global.content)
{
if (!pushRule.isDefault)
{
count++;
if (count == rowIndex)
{
MXKPushRuleTableViewCell *pushRuleCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleTableViewCell defaultReuseIdentifier]];
if (!pushRuleCell)
{
pushRuleCell = [[MXKPushRuleTableViewCell alloc] init];
}
pushRuleCell.mxSession = _mxAccount.mxSession;
pushRuleCell.mxPushRule = pushRule;
cell = pushRuleCell;
break;
}
}
}
}
}
else if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_PER_ROOM_INDEX)
{
if (rowIndex == roomRuleCreationIndex)
{
MXKPushRuleCreationTableViewCell *pushRuleCreationCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleCreationTableViewCell defaultReuseIdentifier]];
if (!pushRuleCreationCell)
{
pushRuleCreationCell = [[MXKPushRuleCreationTableViewCell alloc] init];
}
pushRuleCreationCell.mxSession = _mxAccount.mxSession;
pushRuleCreationCell.mxPushRuleKind = MXPushRuleKindRoom;
cell = pushRuleCreationCell;
}
else if (rowIndex < _mxAccount.mxSession.notificationCenter.rules.global.room.count)
{
MXKPushRuleTableViewCell *pushRuleCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleTableViewCell defaultReuseIdentifier]];
if (!pushRuleCell)
{
pushRuleCell = [[MXKPushRuleTableViewCell alloc] init];
}
pushRuleCell.mxSession = _mxAccount.mxSession;
pushRuleCell.mxPushRule = [_mxAccount.mxSession.notificationCenter.rules.global.room objectAtIndex:rowIndex];
cell = pushRuleCell;
}
}
else if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_PER_SENDER_INDEX)
{
if (rowIndex == senderRuleCreationIndex)
{
MXKPushRuleCreationTableViewCell *pushRuleCreationCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleCreationTableViewCell defaultReuseIdentifier]];
if (!pushRuleCreationCell)
{
pushRuleCreationCell = [[MXKPushRuleCreationTableViewCell alloc] init];
}
pushRuleCreationCell.mxSession = _mxAccount.mxSession;
pushRuleCreationCell.mxPushRuleKind = MXPushRuleKindSender;
cell = pushRuleCreationCell;
}
else if (rowIndex < _mxAccount.mxSession.notificationCenter.rules.global.sender.count)
{
MXKPushRuleTableViewCell *pushRuleCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleTableViewCell defaultReuseIdentifier]];
if (!pushRuleCell)
{
pushRuleCell = [[MXKPushRuleTableViewCell alloc] init];
}
pushRuleCell.mxSession = _mxAccount.mxSession;
pushRuleCell.mxPushRule = [_mxAccount.mxSession.notificationCenter.rules.global.sender objectAtIndex:rowIndex];
cell = pushRuleCell;
}
}
else if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_OTHERS_INDEX)
{
MXPushRule *pushRule;
NSString *ruleDescription;
if (rowIndex == ruleContainsUserNameIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterContainUserNameRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsContainMyUserName];
}
if (rowIndex == ruleContainsDisplayNameIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterContainDisplayNameRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsContainMyDisplayName];
}
if (rowIndex == ruleOneToOneRoomIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterOneToOneRoomRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsJustSentToMe];
}
if (rowIndex == ruleInviteForMeIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterInviteMeRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsInviteToANewRoom];
}
if (rowIndex == ruleMemberEventIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterMemberEventRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsPeopleJoinLeaveRooms];
}
if (rowIndex == ruleCallIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterCallRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsReceiveACall];
}
if (rowIndex == ruleSuppressBotsNotificationsIndex)
{
pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterSuppressBotsNotificationsRuleID];
ruleDescription = [MatrixKitL10n notificationSettingsSuppressFromBots];
}
if (pushRule)
{
MXKPushRuleTableViewCell *pushRuleCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleTableViewCell defaultReuseIdentifier]];
if (!pushRuleCell)
{
pushRuleCell = [[MXKPushRuleTableViewCell alloc] init];
}
pushRuleCell.mxSession = _mxAccount.mxSession;
pushRuleCell.mxPushRule = pushRule;
pushRuleCell.ruleDescription.text = ruleDescription;
cell = pushRuleCell;
}
}
else if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_DEFAULT_INDEX)
{
MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterAllOtherRoomMessagesRuleID];
if (pushRule)
{
MXKPushRuleTableViewCell *pushRuleCell = [tableView dequeueReusableCellWithIdentifier:[MXKPushRuleTableViewCell defaultReuseIdentifier]];
if (!pushRuleCell)
{
pushRuleCell = [[MXKPushRuleTableViewCell alloc] init];
}
pushRuleCell.mxSession = _mxAccount.mxSession;
pushRuleCell.mxPushRule = pushRule;
pushRuleCell.ruleDescription.text = [MatrixKitL10n notificationSettingsNotifyAllOther];
cell = pushRuleCell;
}
}
else
{
// Return a fake cell to prevent app from crashing.
cell = [[UITableViewCell alloc] init];
}
return cell;
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_INTRO_INDEX && indexPath.row == 1)
{
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, MAXFLOAT)];
textView.font = [UIFont systemFontOfSize:14];
textView.text = areAllDisabled ? [MatrixKitL10n notificationSettingsEnableNotificationsWarning] : [MatrixKitL10n notificationSettingsGlobalInfo];
CGSize contentSize = [textView sizeThatFits:textView.frame.size];
return contentSize.height + 1;
}
if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_PER_WORD_INDEX)
{
if (indexPath.row == 0)
{
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, MAXFLOAT)];
textView.font = [UIFont systemFontOfSize:14];
textView.text = [MatrixKitL10n notificationSettingsPerWordInfo];
CGSize contentSize = [textView sizeThatFits:textView.frame.size];
return contentSize.height + 1;
}
else if (indexPath.row == contentRuleCreationIndex)
{
return 120;
}
}
if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_PER_ROOM_INDEX && indexPath.row == roomRuleCreationIndex)
{
return 120;
}
if (indexPath.section == MXKNOTIFICATIONSETTINGS_SECTION_PER_SENDER_INDEX && indexPath.row == senderRuleCreationIndex)
{
return 120;
}
return 50;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (section != MXKNOTIFICATIONSETTINGS_SECTION_INTRO_INDEX)
{
return 30;
}
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *sectionHeader = [[UIView alloc] initWithFrame:[tableView rectForHeaderInSection:section]];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
UILabel *sectionLabel = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, sectionHeader.frame.size.width - 10, sectionHeader.frame.size.height - 10)];
sectionLabel.font = [UIFont boldSystemFontOfSize:16];
sectionLabel.backgroundColor = [UIColor clearColor];
[sectionHeader addSubview:sectionLabel];
if (section == MXKNOTIFICATIONSETTINGS_SECTION_PER_WORD_INDEX)
{
sectionLabel.text = [MatrixKitL10n notificationSettingsPerWordNotifications];
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_PER_ROOM_INDEX)
{
sectionLabel.text = [MatrixKitL10n notificationSettingsPerRoomNotifications];
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_PER_SENDER_INDEX)
{
sectionLabel.text = [MatrixKitL10n notificationSettingsPerSenderNotifications];
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_OTHERS_INDEX)
{
sectionLabel.text = [MatrixKitL10n notificationSettingsOtherAlerts];
}
else if (section == MXKNOTIFICATIONSETTINGS_SECTION_DEFAULT_INDEX)
{
sectionLabel.text = [MatrixKitL10n notificationSettingsByDefault];
}
return sectionHeader;
}
@end
@@ -0,0 +1,74 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C
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 <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol MXKPreviewViewControllerDelegate;
/**
@brief A view controller that previews, opens, or prints files whose file format cannot be handled directly by your app.
Use this class to present an appropriate user interface for previewing, opening, copying, or printing a specified file. For example, an email program might use this class to allow the user to preview attachments and open them in other apps.
After presenting its user interface, a document interaction controller handles all interactions needed to support file preview and menu display.
Unlike UIDocumentInteractionController, this view controller aims to be modal presented.
*/
@interface MXKPreviewViewController : UINavigationController
/**
@brief presents a new instance of MXKPreviewViewController as modal.
@param presenting view controller that presents the MXKPreviewViewController
@param fileUrl URL of the file. This URL should point to a local file.
@param allowActions YES to display actions Button. NO otherwise
@param delegate delegate (optional) that receives some events about the lifecycle of the MXKPreviewViewController
@return the instance of MXKPreviewViewController
*/
+ (MXKPreviewViewController *)presentFrom:(nonnull UIViewController *)presenting
fileUrl: (nonnull NSURL *)fileUrl
allowActions: (BOOL)allowActions
delegate: (nullable id<MXKPreviewViewControllerDelegate>)delegate;
@end
/**
A set of methods you can implement to respond to messages from a preview controller.
*/
@protocol MXKPreviewViewControllerDelegate <NSObject>
@optional
/**
The MXKPreviewViewController will present the preview
@param controller the instance of MXKPreviewViewController
*/
- (void)previewViewControllerWillBeginPreview:(MXKPreviewViewController *)controller;
/**
The MXKPreviewViewController did end presenting the preview
@param controller the instance of MXKPreviewViewController
*/
- (void)previewViewControllerDidEndPreview:(MXKPreviewViewController *)controller;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,104 @@
/*
Copyright 2020 Vector Creations 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 "MXKPreviewViewController.h"
@import QuickLook;
@interface MXKPreviewViewController () <QLPreviewControllerDataSource>
/// A specialized view controller for previewing an item.
@property (nonatomic, weak) QLPreviewController *previewController;
/// URL of the file to preview
@property (nonatomic, strong) NSURL *fileURL;
/// YES to display actions Button. NO otherwise
@property (nonatomic) BOOL allowActions;
@property (nonatomic, weak) id<MXKPreviewViewControllerDelegate> previewDelegate;
@end
@implementation MXKPreviewViewController
+ (MXKPreviewViewController *)presentFrom:(UIViewController *)presenting fileUrl:(NSURL *)fileUrl allowActions:(BOOL)allowActions delegate:(nullable id<MXKPreviewViewControllerDelegate>)delegate
{
MXKPreviewViewController *previewController = [[MXKPreviewViewController alloc] initWithFileUrl: fileUrl allowActions: allowActions];
previewController.previewDelegate = delegate;
if ([delegate respondsToSelector:@selector(previewViewControllerWillBeginPreview:)]) {
[delegate previewViewControllerWillBeginPreview:previewController];
}
[presenting presentViewController:previewController animated:YES completion:^{
}];
return previewController;
}
- (instancetype)initWithFileUrl: (NSURL *)fileUrl allowActions: (BOOL)allowActions
{
QLPreviewController *previewController = [[QLPreviewController alloc] init];
self = [super initWithRootViewController:previewController];
self.previewController = previewController;
if (self)
{
self.modalPresentationStyle = UIModalPresentationFullScreen;
self.fileURL = fileUrl;
self.allowActions = allowActions;
self.previewController.dataSource = self;
self.previewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneAction:)];
}
return self;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!self.allowActions)
{
NSMutableArray *items = [NSMutableArray arrayWithArray: self.previewController.navigationItem.rightBarButtonItems];
if (items.count > 0)
{
[items removeObjectAtIndex:0];
}
self.previewController.navigationItem.rightBarButtonItems = items;
}
}
- (IBAction)doneAction:(id)sender
{
[self dismissViewControllerAnimated:YES completion:^{
if ([self.previewDelegate respondsToSelector:@selector(previewViewControllerDidEndPreview:)]) {
[self.previewDelegate previewViewControllerDidEndPreview:self];
}
}];
}
#pragma mark - QLPreviewControllerDataSource
- (NSInteger)numberOfPreviewItemsInPreviewController:(nonnull QLPreviewController *)controller
{
return self.fileURL ? 1 : 0;
}
- (nonnull id<QLPreviewItem>)previewController:(nonnull QLPreviewController *)controller previewItemAtIndex:(NSInteger)index
{
return self.fileURL;
}
@end
@@ -0,0 +1,129 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations 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 <UIKit/UIKit.h>
#import "MXKViewController.h"
#import "MXKRecentsDataSource.h"
@class MXKRecentListViewController;
/**
`MXKRecentListViewController` delegate.
*/
@protocol MXKRecentListViewControllerDelegate <NSObject>
/**
Tells the delegate that the user selected a room.
@param recentListViewController the `MXKRecentListViewController` instance.
@param roomId the id of the selected room.
@param mxSession the matrix session in which the room is defined.
*/
- (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString*)roomId inMatrixSession:(MXSession*)mxSession;
/**
Tells the delegate that the user selected a suggested room.
@param recentListViewController the `MXKRecentListViewController` instance.
@param childInfo the `MXSpaceChildInfo` instance that describes the selected room.
*/
-(void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo;
@end
/**
This view controller displays a room list.
*/
@interface MXKRecentListViewController : MXKViewController <MXKDataSourceDelegate, UITableViewDelegate, UISearchBarDelegate>
{
@protected
/**
The fake top view displayed in case of vertical bounce.
*/
__weak UIView *topview;
}
@property (weak, nonatomic) IBOutlet UISearchBar *recentsSearchBar;
@property (weak, nonatomic) IBOutlet UITableView *recentsTableView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *recentsSearchBarTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *recentsSearchBarHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *recentsTableViewBottomConstraint;
/**
The current data source associated to the view controller.
*/
@property (nonatomic, readonly) MXKRecentsDataSource *dataSource;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKRecentListViewControllerDelegate> delegate;
/**
Enable the search option by adding a navigation item in the navigation bar (YES by default).
Set NO this property to disable this option and hide the related bar button.
*/
@property (nonatomic) BOOL enableBarButtonSearch;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKRecentListViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `recentListViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKRecentListViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKRecentListViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)recentListViewController;
/**
Display the recents described in the provided data source.
Note1: The provided data source will replace the current data source if any. The caller
should dispose properly this data source if it is not used anymore.
Note2: You may provide here a MXKInterleavedRecentsDataSource instance to display interleaved recents.
@param listDataSource the data source providing the recents list.
*/
- (void)displayList:(MXKRecentsDataSource*)listDataSource;
/**
Refresh the recents table display.
*/
- (void)refreshRecentsTable;
/**
Hide/show the search bar at the top of the recents table view.
*/
- (void)hideSearchBar:(BOOL)hidden;
@end
@@ -0,0 +1,624 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations 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 "MXKRecentListViewController.h"
#import "MXKRoomDataSourceManager.h"
#import "MXKInterleavedRecentsDataSource.h"
#import "MXKInterleavedRecentTableViewCell.h"
#import "MXKSwiftHeader.h"
@interface MXKRecentListViewController ()
{
/**
The data source providing UITableViewCells
*/
MXKRecentsDataSource *dataSource;
/**
Search handling
*/
UIBarButtonItem *searchButton;
BOOL ignoreSearchRequest;
/**
The reconnection animated view.
*/
__weak UIView* reconnectingView;
/**
The current table view header if any.
*/
UIView* tableViewHeaderView;
/**
The latest server sync date
*/
NSDate* latestServerSync;
/**
The restart the event connnection
*/
BOOL restartConnection;
}
@end
@implementation MXKRecentListViewController
@synthesize dataSource;
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKRecentListViewController class])
bundle:[NSBundle bundleForClass:[MXKRecentListViewController class]]];
}
+ (instancetype)recentListViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKRecentListViewController class])
bundle:[NSBundle bundleForClass:[MXKRecentListViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
_enableBarButtonSearch = YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_recentsTableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
// Adjust search bar Top constraint to take into account potential navBar.
if (_recentsSearchBarTopConstraint)
{
_recentsSearchBarTopConstraint.active = NO;
_recentsSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.recentsSearchBar
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
_recentsSearchBarTopConstraint.active = YES;
}
// Adjust table view Bottom constraint to take into account tabBar.
if (_recentsTableViewBottomConstraint)
{
_recentsTableViewBottomConstraint.active = NO;
_recentsTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.recentsTableView
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0f];
_recentsTableViewBottomConstraint.active = YES;
}
// Hide search bar by default
[self hideSearchBar:YES];
// Apply search option in navigation bar
self.enableBarButtonSearch = _enableBarButtonSearch;
// Add an accessory view to the search bar in order to retrieve keyboard view.
self.recentsSearchBar.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
// Finalize table view configuration
self.recentsTableView.delegate = self;
self.recentsTableView.dataSource = dataSource; // Note: dataSource may be nil here
// Set up classes to use for cells
[self.recentsTableView registerNib:MXKRecentTableViewCell.nib forCellReuseIdentifier:MXKRecentTableViewCell.defaultReuseIdentifier];
// Consider here the specific case where interleaved recents are supported
[self.recentsTableView registerNib:MXKInterleavedRecentTableViewCell.nib forCellReuseIdentifier:MXKInterleavedRecentTableViewCell.defaultReuseIdentifier];
// Add a top view which will be displayed in case of vertical bounce.
CGFloat height = self.recentsTableView.frame.size.height;
UIView *topview = [[UIView alloc] initWithFrame:CGRectMake(0,-height,self.recentsTableView.frame.size.width,height)];
topview.autoresizingMask = UIViewAutoresizingFlexibleWidth;
topview.backgroundColor = [UIColor groupTableViewBackgroundColor];
[self.recentsTableView addSubview:topview];
self->topview = topview;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Restore search mechanism (if enabled)
ignoreSearchRequest = NO;
// Observe server sync at room data source level too
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionChange) name:kMXKRoomDataSourceSyncStatusChanged object:nil];
// Observe the server sync
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onSyncNotification) name:kMXSessionDidSyncNotification object:nil];
// Do a full reload
[self refreshRecentsTable];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
// Leave potential search session
if (!self.recentsSearchBar.isHidden)
{
[self searchBarCancelButtonClicked:self.recentsSearchBar];
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidSyncNotification object:nil];
[self removeReconnectingView];
}
- (void)dealloc
{
self.recentsSearchBar.inputAccessoryView = nil;
searchButton = nil;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Override MXKViewController
- (void)onMatrixSessionChange
{
[super onMatrixSessionChange];
// Check whether no server sync is in progress in room data sources
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
if ([MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession].isServerSyncInProgress)
{
// sync is in progress for at least one data source, keep running the loading wheel
[self.activityIndicator startAnimating];
break;
}
}
}
- (void)onKeyboardShowAnimationComplete
{
// Report the keyboard view in order to track keyboard frame changes
self.keyboardView = _recentsSearchBar.inputAccessoryView.superview;
}
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
// Deduce the bottom constraint for the table view (Don't forget the potential tabBar)
CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
// Check whether the keyboard is over the tabBar
if (tableViewBottomConst < 0)
{
tableViewBottomConst = 0;
}
// Update constraints
_recentsTableViewBottomConstraint.constant = tableViewBottomConst;
// Force layout immediately to take into account new constraint
[self.view layoutIfNeeded];
}
- (void)destroy
{
self.recentsTableView.dataSource = nil;
self.recentsTableView.delegate = nil;
self.recentsTableView = nil;
dataSource.delegate = nil;
dataSource = nil;
_delegate = nil;
[topview removeFromSuperview];
topview = nil;
[super destroy];
}
#pragma mark -
- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch
{
_enableBarButtonSearch = enableBarButtonSearch;
if (enableBarButtonSearch)
{
if (!searchButton)
{
searchButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
}
// Add it in right bar items
NSArray *rightBarButtonItems = self.navigationItem.rightBarButtonItems;
self.navigationItem.rightBarButtonItems = rightBarButtonItems ? [rightBarButtonItems arrayByAddingObject:searchButton] : @[searchButton];
}
else
{
NSMutableArray *rightBarButtonItems = [NSMutableArray arrayWithArray: self.navigationItem.rightBarButtonItems];
[rightBarButtonItems removeObject:searchButton];
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
}
}
- (void)displayList:(MXKRecentsDataSource *)listDataSource
{
// Cancel registration on existing dataSource if any
if (dataSource)
{
dataSource.delegate = nil;
// Remove associated matrix sessions
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
[self removeMatrixSession:mxSession];
}
}
dataSource = listDataSource;
dataSource.delegate = self;
// Report all matrix sessions at view controller level to update UI according to sessions state
NSArray *mxSessions = listDataSource.mxSessions;
for (MXSession *mxSession in mxSessions)
{
[self addMatrixSession:mxSession];
}
if (self.recentsTableView)
{
// Set up table data source
self.recentsTableView.dataSource = dataSource;
}
}
- (void)refreshRecentsTable
{
// For now, do a simple full reload
[self.recentsTableView reloadData];
}
- (void)hideSearchBar:(BOOL)hidden
{
self.recentsSearchBar.hidden = hidden;
self.recentsSearchBarHeightConstraint.constant = hidden ? 0 : 44;
[self.view setNeedsUpdateConstraints];
}
#pragma mark - Action
- (IBAction)search:(id)sender
{
// The user may have pressed search button whereas the view controller was disappearing
if (ignoreSearchRequest)
{
return;
}
if (self.recentsSearchBar.isHidden)
{
// Check whether there are data in which search
if ([self.dataSource numberOfSectionsInTableView:self.recentsTableView])
{
[self hideSearchBar:NO];
// Create search bar
[self.recentsSearchBar becomeFirstResponder];
}
}
else
{
[self searchBarCancelButtonClicked: self.recentsSearchBar];
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
// Consider here the specific case where interleaved recents are supported
if ([dataSource isKindOfClass:MXKInterleavedRecentsDataSource.class])
{
return MXKInterleavedRecentTableViewCell.class;
}
// Return the default recent table view cell
return MXKRecentTableViewCell.class;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
// Consider here the specific case where interleaved recents are supported
if ([dataSource isKindOfClass:MXKInterleavedRecentsDataSource.class])
{
return MXKInterleavedRecentTableViewCell.defaultReuseIdentifier;
}
// Return the default recent table view cell
return MXKRecentTableViewCell.defaultReuseIdentifier;
}
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
// For now, do a simple full reload
[self refreshRecentsTable];
}
- (void)dataSource:(MXKDataSource *)dataSource didAddMatrixSession:(MXSession *)mxSession
{
[self addMatrixSession:mxSession];
}
- (void)dataSource:(MXKDataSource *)dataSource didRemoveMatrixSession:(MXSession *)mxSession
{
[self removeMatrixSession:mxSession];
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [dataSource cellHeightAtIndexPath:indexPath];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
// Section header is required only when several recent lists are displayed.
if (self.dataSource.displayedRecentsDataSourcesCount > 1)
{
return 35;
}
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
// Let dataSource provide the section header.
return [dataSource viewForHeaderInSection:section withFrame:[tableView rectForHeaderInSection:section]];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_delegate)
{
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
if ([selectedCell conformsToProtocol:@protocol(MXKCellRendering)])
{
id<MXKCellRendering> cell = (id<MXKCellRendering>)selectedCell;
if ([cell respondsToSelector:@selector(renderedCellData)])
{
MXKCellData *cellData = cell.renderedCellData;
if ([cellData conformsToProtocol:@protocol(MXKRecentCellDataStoring)])
{
id<MXKRecentCellDataStoring> recentCellData = (id<MXKRecentCellDataStoring>)cellData;
if (recentCellData.isSuggestedRoom)
{
[_delegate recentListViewController:self
didSelectSuggestedRoom:recentCellData.roomSummary.spaceChildInfo];
}
else
{
[_delegate recentListViewController:self
didSelectRoom:recentCellData.roomIdentifier
inMatrixSession:recentCellData.mxSession];
}
}
}
}
}
// Hide the keyboard when user select a room
// do not hide the searchBar until the view controller disappear
// on tablets / iphone 6+, the user could expect to search again while looking at a room
[self.recentsSearchBar resignFirstResponder];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Release here resources, and restore reusable cells
if ([cell respondsToSelector:@selector(didEndDisplay)])
{
[(id<MXKCellRendering>)cell didEndDisplay];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Detect vertical bounce at the top of the tableview to trigger reconnection.
if (scrollView == _recentsTableView)
{
[self detectPullToKick:scrollView];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (scrollView == _recentsTableView)
{
[self managePullToKick:scrollView];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == _recentsTableView)
{
if (scrollView.contentOffset.y + scrollView.adjustedContentInset.top == 0)
{
[self managePullToKick:scrollView];
}
}
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
// Apply filter
if (searchText.length)
{
[self.dataSource searchWithPatterns:@[searchText]];
}
else
{
[self.dataSource searchWithPatterns:nil];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// "Done" key has been pressed
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
// Leave search
[searchBar resignFirstResponder];
[self hideSearchBar:YES];
self.recentsSearchBar.text = nil;
// Refresh display
[self.dataSource searchWithPatterns:nil];
}
#pragma mark - resync management
- (void)onSyncNotification
{
latestServerSync = [NSDate date];
[self removeReconnectingView];
}
- (BOOL)canReconnect
{
// avoid restarting connection if some data has been received within 1 second (1000 : latestServerSync is null)
NSTimeInterval interval = latestServerSync ? [[NSDate date] timeIntervalSinceDate:latestServerSync] : 1000;
return (interval > 1) && [self.mainSession reconnect];
}
- (void)addReconnectingView
{
if (!reconnectingView)
{
UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f);
CGRect frame = spinner.frame;
frame.size.height = 80; // 80 * 0.75 = 60
spinner.bounds = frame;
spinner.color = [UIColor darkGrayColor];
spinner.hidesWhenStopped = NO;
spinner.backgroundColor = _recentsTableView.backgroundColor;
[spinner startAnimating];
// no need to manage constraints here, IOS defines them.
tableViewHeaderView = _recentsTableView.tableHeaderView;
_recentsTableView.tableHeaderView = reconnectingView = spinner;
}
}
- (void)removeReconnectingView
{
if (reconnectingView && !restartConnection)
{
_recentsTableView.tableHeaderView = tableViewHeaderView;
reconnectingView = nil;
}
}
/**
Detect if the current connection must be restarted.
The spinner is displayed until the overscroll ends (and scrollViewDidEndDecelerating is called).
*/
- (void)detectPullToKick:(UIScrollView *)scrollView
{
if (!reconnectingView)
{
// detect if the user scrolls over the tableview top
restartConnection = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top < -128);
if (restartConnection)
{
// wait that list decelerate to display / hide it
[self addReconnectingView];
}
}
}
/**
Restarts the current connection if it is required.
The 0.3s delay is added to avoid flickering if the connection does not require to be restarted.
*/
- (void)managePullToKick:(UIScrollView *)scrollView
{
// the current connection must be restarted
if (restartConnection)
{
// display at least 0.3s the spinner to show to the user that something is pending
// else the UI is flickering
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self->restartConnection = NO;
if (![self canReconnect])
{
// if the event stream has not been restarted
// hide the spinner
[self removeReconnectingView];
}
// else wait that onSyncNotification is called.
});
}
}
@end
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKRecentListViewController">
<connections>
<outlet property="recentsSearchBar" destination="Zbr-9e-VZh" id="AvH-q6-dOM"/>
<outlet property="recentsSearchBarHeightConstraint" destination="jCU-cq-2OA" id="w76-xb-0xq"/>
<outlet property="recentsSearchBarTopConstraint" destination="aRg-Nz-enq" id="gI4-54-oEm"/>
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
<outlet property="recentsTableViewBottomConstraint" destination="m4x-32-odR" id="8JQ-c9-1Tq"/>
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" showsCancelButton="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zbr-9e-VZh">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="jCU-cq-2OA"/>
</constraints>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="-1" id="ejZ-kN-jKv"/>
</connections>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="orV-HH-88x">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="NaR-eJ-WMj"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="aRg-Nz-enq"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="fty-XB-tTr"/>
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="Zbr-9e-VZh" secondAttribute="bottom" id="lBa-P2-Vnx"/>
<constraint firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="m4x-32-odR"/>
<constraint firstAttribute="trailing" secondItem="Zbr-9e-VZh" secondAttribute="trailing" id="rKb-TZ-sap"/>
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="yBp-63-kZi"/>
</constraints>
</view>
</objects>
</document>
@@ -0,0 +1,212 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations 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 <UIKit/UIKit.h>
#import "MXKViewController.h"
#import "MXKImageView.h"
/**
Available actions on room member
*/
typedef enum : NSUInteger
{
MXKRoomMemberDetailsActionInvite,
MXKRoomMemberDetailsActionLeave,
MXKRoomMemberDetailsActionKick,
MXKRoomMemberDetailsActionBan,
MXKRoomMemberDetailsActionUnban,
MXKRoomMemberDetailsActionIgnore,
MXKRoomMemberDetailsActionUnignore,
MXKRoomMemberDetailsActionSetDefaultPowerLevel,
MXKRoomMemberDetailsActionSetModerator,
MXKRoomMemberDetailsActionSetAdmin,
MXKRoomMemberDetailsActionSetCustomPowerLevel,
MXKRoomMemberDetailsActionStartChat,
MXKRoomMemberDetailsActionStartVoiceCall,
MXKRoomMemberDetailsActionStartVideoCall,
MXKRoomMemberDetailsActionMention,
MXKRoomMemberDetailsActionSecurity,
MXKRoomMemberDetailsActionSecurityInformation
} MXKRoomMemberDetailsAction;
@class MXKRoomMemberDetailsViewController;
/**
`MXKRoomMemberDetailsViewController` delegate.
*/
@protocol MXKRoomMemberDetailsViewControllerDelegate <NSObject>
/**
Tells the delegate that the user wants to start a one-to-one chat with the room member.
@param roomMemberDetailsViewController the `MXKRoomMemberDetailsViewController` instance.
@param matrixId the member's matrix id
@param completion the block to execute at the end of the operation (independently if it succeeded or not).
*/
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString*)matrixId completion:(void (^)(void))completion;
@optional
/**
Tells the delegate that the user wants to mention the room member.
@discussion the `MXKRoomMemberDetailsViewController` instance is withdrawn automatically.
@param roomMemberDetailsViewController the `MXKRoomMemberDetailsViewController` instance.
@param member the room member to mention.
*/
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController mention:(MXRoomMember*)member;
/**
Tells the delegate that the user wants to place a voip call with the room member.
@param roomMemberDetailsViewController the `MXKRoomMemberDetailsViewController` instance.
@param matrixId the member's matrix id
@param isVideoCall the type of the call: YES for video call / NO for voice call.
*/
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController placeVoipCallWithMemberId:(NSString*)matrixId andVideo:(BOOL)isVideoCall;
@end
/**
Whereas the main item of this view controller is a table view, the 'MXKRoomMemberDetailsViewController' class inherits
from 'MXKViewController' instead of 'MXKTableViewController' in order to ease the customization.
Indeed some items like header may be added at the same level than the table.
*/
@interface MXKRoomMemberDetailsViewController : MXKViewController <UITableViewDelegate, UITableViewDataSource>
{
@protected
/**
Current alert (if any).
*/
UIAlertController *currentAlert;
/**
List of the allowed actions on this member.
*/
NSMutableArray<NSNumber*> *actionsArray;
}
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet MXKImageView *memberThumbnail;
@property (weak, nonatomic) IBOutlet UITextView *roomMemberMatrixInfo;
/**
The default account picture displayed when no picture is defined.
*/
@property (nonatomic) UIImage *picturePlaceholder;
/**
The displayed member and the corresponding room
*/
@property (nonatomic, readonly) MXRoomMember *mxRoomMember;
@property (nonatomic, readonly) MXRoom *mxRoom;
@property (nonatomic, readonly) MXEventTimeline *mxRoomLiveTimeline;
/**
Enable mention option. NO by default
*/
@property (nonatomic) BOOL enableMention;
/**
Enable voip call (voice/video). NO by default
*/
@property (nonatomic) BOOL enableVoipCall;
/**
Enable leave this room. YES by default
*/
@property (nonatomic) BOOL enableLeave;
/**
Tell whether an action is already in progress.
*/
@property (nonatomic, readonly) BOOL hasPendingAction;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKRoomMemberDetailsViewControllerDelegate> delegate;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKRoomMemberDetailsViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `roomMemberDetailsViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKRoomMemberDetailsViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKRoomMemberDetailsViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)roomMemberDetailsViewController;
/**
Set the room member to display. Provide the actual room in order to handle member changes.
@param roomMember the matrix room member
@param room the matrix room to which this member belongs.
*/
- (void)displayRoomMember:(MXRoomMember*)roomMember withMatrixRoom:(MXRoom*)room;
/**
Refresh the member information.
*/
- (void)updateMemberInfo;
/**
The following method is registered on `UIControlEventTouchUpInside` event for all displayed action buttons.
The start chat and mention options are transferred to the delegate.
All the other actions are handled by the current implementation.
If the delegate responds to selector: @selector(roomMemberDetailsViewController:placeVoipCallWithMemberId:andVideo:), the voip options
are transferred to the delegate.
*/
- (IBAction)onActionButtonPressed:(id)sender;
/**
Set the power level of the room member
@param value the value to set.
@param promptUser prompt the user if they ops a member with the same power level.
*/
- (void)setPowerLevel:(NSInteger)value promptUser:(BOOL)promptUser;
/**
Add a mask in overlay to prevent a new contact selection (used when an action is on progress).
*/
- (void)addPendingActionMask;
/**
Remove the potential overlay mask
*/
- (void)removePendingActionMask;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKRoomMemberDetailsViewController">
<connections>
<outlet property="memberThumbnail" destination="aCC-Nn-76o" id="gyJ-vS-syH"/>
<outlet property="roomMemberMatrixInfo" destination="j7K-fD-zCi" id="2o5-bf-JBu"/>
<outlet property="tableView" destination="hJP-2r-QxY" id="XZr-oR-4yU"/>
<outlet property="view" destination="Df0-2N-edY" id="SKP-AJ-qf0"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="Df0-2N-edY">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="hJP-2r-QxY">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="OcA-Yr-ljR" userLabel="TableHeader">
<rect key="frame" x="0.0" y="0.0" width="375" height="115"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aCC-Nn-76o" customClass="MXKImageView">
<rect key="frame" x="10" y="10" width="75" height="75"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="75" id="MrR-4z-5Ya"/>
<constraint firstAttribute="width" secondItem="aCC-Nn-76o" secondAttribute="height" multiplier="1:1" id="lwc-Yc-EDh"/>
</constraints>
</view>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j7K-fD-zCi" userLabel="member matrix info">
<rect key="frame" x="95" y="10" width="270" height="95"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="j7K-fD-zCi" secondAttribute="trailing" constant="10" id="1bD-6F-bvn"/>
<constraint firstItem="j7K-fD-zCi" firstAttribute="leading" secondItem="aCC-Nn-76o" secondAttribute="trailing" constant="10" id="OJB-qV-vae"/>
<constraint firstItem="aCC-Nn-76o" firstAttribute="top" secondItem="OcA-Yr-ljR" secondAttribute="top" constant="10" id="XHc-5o-3sR"/>
<constraint firstItem="aCC-Nn-76o" firstAttribute="leading" secondItem="OcA-Yr-ljR" secondAttribute="leading" constant="10" id="Xb5-Q8-eer"/>
<constraint firstItem="j7K-fD-zCi" firstAttribute="top" secondItem="OcA-Yr-ljR" secondAttribute="top" constant="10" id="hyH-j6-iVj"/>
<constraint firstAttribute="bottom" secondItem="j7K-fD-zCi" secondAttribute="bottom" constant="10" id="iP9-p3-Fgv"/>
</constraints>
</view>
<connections>
<outlet property="dataSource" destination="-1" id="rmb-eW-4fJ"/>
<outlet property="delegate" destination="-1" id="3xv-qb-1cH"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="hJP-2r-QxY" secondAttribute="trailing" id="8EI-re-vO2"/>
<constraint firstItem="hJP-2r-QxY" firstAttribute="leading" secondItem="Df0-2N-edY" secondAttribute="leading" id="Az3-yV-wXV"/>
<constraint firstItem="hJP-2r-QxY" firstAttribute="top" secondItem="Df0-2N-edY" secondAttribute="top" id="DHS-BM-205"/>
<constraint firstAttribute="bottom" secondItem="hJP-2r-QxY" secondAttribute="bottom" id="Dih-o9-6aN"/>
</constraints>
</view>
</objects>
</document>
@@ -0,0 +1,116 @@
/*
Copyright 2015 OpenMarket 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 <UIKit/UIKit.h>
#import "MXKViewController.h"
#import "MXKRoomMemberListDataSource.h"
@class MXKRoomMemberListViewController;
/**
`MXKRoomMemberListViewController` delegate.
*/
@protocol MXKRoomMemberListViewControllerDelegate <NSObject>
/**
Tells the delegate that the user selected a member.
@param roomMemberListViewController the `MXKRoomMemberListViewController` instance.
@param member the selected member.
*/
- (void)roomMemberListViewController:(MXKRoomMemberListViewController *)roomMemberListViewController didSelectMember:(MXRoomMember*)member;
@end
/**
This view controller displays members of a room. Only one matrix session is handled by this view controller.
*/
@interface MXKRoomMemberListViewController : MXKViewController <MXKDataSourceDelegate, UITableViewDelegate, UISearchBarDelegate>
{
@protected
/**
Used to auto scroll at the top when search session is started or cancelled.
*/
BOOL shouldScrollToTopOnRefresh;
}
@property (weak, nonatomic) IBOutlet UISearchBar *membersSearchBar;
@property (weak, nonatomic) IBOutlet UITableView *membersTableView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *membersSearchBarTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *membersSearchBarHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *membersTableViewBottomConstraint;
/**
The current data source associated to the view controller.
*/
@property (nonatomic, readonly) MXKRoomMemberListDataSource *dataSource;
/**
The delegate for the view controller.
*/
@property (nonatomic, weak) id<MXKRoomMemberListViewControllerDelegate> delegate;
/**
Enable the search in room members list according to the member's display name (YES by default).
Set NO this property to disable this option and hide the related bar button.
*/
@property (nonatomic) BOOL enableMemberSearch;
/**
Enable the invitation of a new member (YES by default).
Set NO this property to disable this option and hide the related bar button.
*/
@property (nonatomic) BOOL enableMemberInvitation;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKRoomMemberListViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `roomMemberListViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKRoomMemberListViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKRoomMemberListViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)roomMemberListViewController;
/**
Display the members list.
@param listDataSource the data source providing the members list.
*/
- (void)displayList:(MXKRoomMemberListDataSource*)listDataSource;
/**
Scroll the members list to the top.
@param animated YES to animate the transition at a constant velocity to the new offset, NO to make the transition immediate.
*/
- (void)scrollToTop:(BOOL)animated;
@end
@@ -0,0 +1,571 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 "MXKRoomMemberListViewController.h"
#import "MXKRoomMemberTableViewCell.h"
#import "MXKConstants.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
@interface MXKRoomMemberListViewController ()
{
/**
The data source providing UITableViewCells
*/
MXKRoomMemberListDataSource *dataSource;
/**
Timer used to update members presence
*/
NSTimer* presenceUpdateTimer;
/**
Optional bar buttons
*/
UIBarButtonItem *searchBarButton;
UIBarButtonItem *addBarButton;
/**
The current displayed alert (if any).
*/
UIAlertController *currentAlert;
/**
Search bar
*/
BOOL ignoreSearchRequest;
/**
Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
*/
id leaveRoomNotificationObserver;
}
@end
@implementation MXKRoomMemberListViewController
@synthesize dataSource;
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKRoomMemberListViewController class])
bundle:[NSBundle bundleForClass:[MXKRoomMemberListViewController class]]];
}
+ (instancetype)roomMemberListViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKRoomMemberListViewController class])
bundle:[NSBundle bundleForClass:[MXKRoomMemberListViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
// Enable both bar button by default.
_enableMemberInvitation = YES;
_enableMemberSearch = YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!self.membersTableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
// Adjust Top and Bottom constraints to take into account potential navBar and tabBar.
[NSLayoutConstraint deactivateConstraints:@[_membersSearchBarTopConstraint, _membersTableViewBottomConstraint]];
_membersSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.membersSearchBar
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
_membersTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.membersTableView
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0f];
[NSLayoutConstraint activateConstraints:@[_membersSearchBarTopConstraint, _membersTableViewBottomConstraint]];
// Hide search bar by default
self.membersSearchBar.hidden = YES;
self.membersSearchBarHeightConstraint.constant = 0;
[self.view setNeedsUpdateConstraints];
searchBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(search:)];
addBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(inviteNewMember:)];
// Refresh bar button display.
[self refreshUIBarButtons];
// Add an accessory view to the search bar in order to retrieve keyboard view.
self.membersSearchBar.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
// Finalize table view configuration
self.membersTableView.delegate = self;
self.membersTableView.dataSource = dataSource; // Note datasource may be nil here.
// Set up default table view cell class
[self.membersTableView registerNib:MXKRoomMemberTableViewCell.nib forCellReuseIdentifier:MXKRoomMemberTableViewCell.defaultReuseIdentifier];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Check whether the user still belongs to the room's members.
if (self.dataSource && [self.mainSession roomWithRoomId:self.dataSource.roomId])
{
[self refreshUIBarButtons];
// Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
leaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif)
{
// Check whether the user will leave the room related to the displayed member list
if (notif.object == self.mainSession)
{
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
if (roomId && [roomId isEqualToString:self.dataSource.roomId])
{
// We remove the current view controller.
[self withdrawViewControllerAnimated:YES completion:nil];
}
}
}];
}
else
{
// We remove the current view controller.
[self withdrawViewControllerAnimated:YES completion:nil];
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Restore search mechanism (if enabled)
ignoreSearchRequest = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
if (leaveRoomNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:leaveRoomNotificationObserver];
leaveRoomNotificationObserver = nil;
}
// Leave potential search session
if (!self.membersSearchBar.isHidden)
{
[self searchBarCancelButtonClicked:self.membersSearchBar];
}
}
- (void)dealloc
{
self.membersSearchBar.inputAccessoryView = nil;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Override MXKTableViewController
- (void)onKeyboardShowAnimationComplete
{
// Report the keyboard view in order to track keyboard frame changes
self.keyboardView = _membersSearchBar.inputAccessoryView.superview;
}
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
// Deduce the bottom constraint for the table view (Don't forget the potential tabBar)
CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
// Check whether the keyboard is over the tabBar
if (tableViewBottomConst < 0)
{
tableViewBottomConst = 0;
}
// Update constraints
_membersTableViewBottomConstraint.constant = tableViewBottomConst;
// Force layout immediately to take into account new constraint
[self.view layoutIfNeeded];
}
- (void)destroy
{
if (presenceUpdateTimer)
{
[presenceUpdateTimer invalidate];
presenceUpdateTimer = nil;
}
self.membersTableView.dataSource = nil;
self.membersTableView.delegate = nil;
self.membersTableView = nil;
dataSource.delegate = nil;
dataSource = nil;
if (currentAlert)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
currentAlert = nil;
}
searchBarButton = nil;
addBarButton = nil;
_delegate = nil;
[super destroy];
}
#pragma mark - Internal methods
- (void)updateMembersActivityInfo
{
for (id memberCell in self.membersTableView.visibleCells)
{
if ([memberCell respondsToSelector:@selector(updateActivityInfo)])
{
[memberCell updateActivityInfo];
}
}
}
#pragma mark - UIBarButton handling
- (void)setEnableMemberSearch:(BOOL)enableMemberSearch
{
_enableMemberSearch = enableMemberSearch;
[self refreshUIBarButtons];
}
- (void)setEnableMemberInvitation:(BOOL)enableMemberInvitation
{
_enableMemberInvitation = enableMemberInvitation;
[self refreshUIBarButtons];
}
- (void)refreshUIBarButtons
{
MXRoom *mxRoom = [self.mainSession roomWithRoomId:dataSource.roomId];
MXWeakify(self);
[mxRoom state:^(MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
BOOL showInvitationOption = self.enableMemberInvitation;
if (showInvitationOption && self->dataSource)
{
// Check conditions to be able to invite someone
NSInteger oneSelfPowerLevel = [roomState.powerLevels powerLevelOfUserWithUserID:self.mainSession.myUser.userId];
if (oneSelfPowerLevel < [roomState.powerLevels invite])
{
showInvitationOption = NO;
}
}
if (showInvitationOption)
{
if (self.enableMemberSearch)
{
self.navigationItem.rightBarButtonItems = @[self->searchBarButton, self->addBarButton];
}
else
{
self.navigationItem.rightBarButtonItems = @[self->addBarButton];
}
}
else if (self.enableMemberSearch)
{
self.navigationItem.rightBarButtonItems = @[self->searchBarButton];
}
else
{
self.navigationItem.rightBarButtonItems = nil;
}
}];
}
#pragma mark -
- (void)displayList:(MXKRoomMemberListDataSource *)listDataSource
{
if (dataSource)
{
dataSource.delegate = nil;
dataSource = nil;
[self removeMatrixSession:self.mainSession];
}
dataSource = listDataSource;
dataSource.delegate = self;
// Report the matrix session at view controller level to update UI according to session state
[self addMatrixSession:dataSource.mxSession];
if (self.membersTableView)
{
// Set up table data source
self.membersTableView.dataSource = dataSource;
}
}
- (void)scrollToTop:(BOOL)animated
{
[self.membersTableView setContentOffset:CGPointMake(-self.membersTableView.adjustedContentInset.left, -self.membersTableView.adjustedContentInset.top) animated:animated];
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
// Return the default member table view cell
return MXKRoomMemberTableViewCell.class;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
// Consider the default member table view cell
return MXKRoomMemberTableViewCell.defaultReuseIdentifier;
}
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
if (presenceUpdateTimer)
{
[presenceUpdateTimer invalidate];
presenceUpdateTimer = nil;
}
// For now, do a simple full reload
[self.membersTableView reloadData];
if (shouldScrollToTopOnRefresh)
{
[self scrollToTop:NO];
shouldScrollToTopOnRefresh = NO;
}
// Place a timer to update members's activity information
presenceUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(updateMembersActivityInfo) userInfo:self repeats:YES];
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [dataSource cellHeightAtIndex:indexPath.row];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (_delegate)
{
id<MXKRoomMemberCellDataStoring> cellData = [dataSource cellDataAtIndex:indexPath.row];
[_delegate roomMemberListViewController:self didSelectMember:cellData.roomMember];
}
[tableView deselectRowAtIndexPath:indexPath animated:NO];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 0;
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Release here resources, and restore reusable cells
if ([cell respondsToSelector:@selector(didEndDisplay)])
{
[(id<MXKCellRendering>)cell didEndDisplay];
}
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
// Apply filter
shouldScrollToTopOnRefresh = YES;
if (searchText.length)
{
[self.dataSource searchWithPatterns:@[searchText]];
}
else
{
[self.dataSource searchWithPatterns:nil];
}
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// "Done" key has been pressed
[searchBar resignFirstResponder];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
// Leave search
[searchBar resignFirstResponder];
self.membersSearchBar.hidden = YES;
self.membersSearchBarHeightConstraint.constant = 0;
[self.view setNeedsUpdateConstraints];
self.membersSearchBar.text = nil;
// Refresh display
shouldScrollToTopOnRefresh = YES;
[self.dataSource searchWithPatterns:nil];
}
#pragma mark - Actions
- (void)search:(id)sender
{
// The user may have pressed search button whereas the view controller was disappearing
if (ignoreSearchRequest)
{
return;
}
if (self.membersSearchBar.isHidden)
{
// Check whether there are data in which search
if ([self.dataSource tableView:self.membersTableView numberOfRowsInSection:0])
{
self.membersSearchBar.hidden = NO;
self.membersSearchBarHeightConstraint.constant = 44;
[self.view setNeedsUpdateConstraints];
// Create search bar
[self.membersSearchBar becomeFirstResponder];
}
}
else
{
[self searchBarCancelButtonClicked: self.membersSearchBar];
}
}
- (void)inviteNewMember:(id)sender
{
__weak typeof(self) weakSelf = self;
if (currentAlert)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
}
// Ask for userId to invite
currentAlert = [UIAlertController alertControllerWithTitle:[MatrixKitL10n userIdTitle] message:nil preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n cancel]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField)
{
textField.secureTextEntry = NO;
textField.placeholder = [MatrixKitL10n userIdPlaceholder];
}];
[currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n invite]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
NSString *userId = [self->currentAlert textFields].firstObject.text;
self->currentAlert = nil;
if (userId.length)
{
MXRoom *mxRoom = [self.mainSession roomWithRoomId:self.dataSource.roomId];
if (mxRoom)
{
[mxRoom inviteUser:userId success:^{
} failure:^(NSError *error) {
MXLogDebug(@"[MXKRoomVC] Invite %@ failed", userId);
// Notify MatrixKit user
NSString *myUserId = self.mainSession.myUser.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}];
}
}
}
}]];
[self presentViewController:currentAlert animated:YES completion:nil];
}
@end
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKRoomMemberListViewController">
<connections>
<outlet property="membersSearchBar" destination="Zbr-9e-VZh" id="cnj-ol-yU0"/>
<outlet property="membersSearchBarHeightConstraint" destination="jCU-cq-2OA" id="xFA-RP-Mdl"/>
<outlet property="membersSearchBarTopConstraint" destination="aRg-Nz-enq" id="cKu-YM-PQP"/>
<outlet property="membersTableView" destination="orV-HH-88x" id="G7I-A9-ZjX"/>
<outlet property="membersTableViewBottomConstraint" destination="m4x-32-odR" id="F7E-uT-fJX"/>
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" showsCancelButton="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zbr-9e-VZh">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="jCU-cq-2OA"/>
</constraints>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="-1" id="BNF-PO-yKJ"/>
</connections>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="orV-HH-88x">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="NaR-eJ-WMj"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="aRg-Nz-enq"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="fty-XB-tTr"/>
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="Zbr-9e-VZh" secondAttribute="bottom" id="lBa-P2-Vnx"/>
<constraint firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="m4x-32-odR"/>
<constraint firstAttribute="trailing" secondItem="Zbr-9e-VZh" secondAttribute="trailing" id="rKb-TZ-sap"/>
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="yBp-63-kZi"/>
</constraints>
</view>
</objects>
</document>
@@ -0,0 +1,81 @@
/*
Copyright 2015 OpenMarket 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 <MatrixSDK/MatrixSDK.h>
#import "MXKTableViewController.h"
/**
This view controller displays the room settings.
*/
@interface MXKRoomSettingsViewController : MXKTableViewController
{
@protected
// the dedicated room
MXRoom* mxRoom;
// the room state
MXRoomState* mxRoomState;
}
/**
The dedicated roomId.
*/
@property (nonatomic, readonly) NSString *roomId;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKRoomSettingsViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `roomViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKRoomSettingsViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKRoomSettingsViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)roomSettingsViewController;
/**
Set the dedicated session and the room Id
*/
- (void)initWithSession:(MXSession*)session andRoomId:(NSString*)roomId;
/**
Refresh the displayed room settings. By default this method reload the table view.
@discusion You may override this method to handle the table refresh.
*/
- (void)refreshRoomSettings;
/**
Updates the display with a new room state.
@param newRoomState the new room state.
*/
- (void)updateRoomState:(MXRoomState*)newRoomState;
@end
@@ -0,0 +1,215 @@
/*
Copyright 2015 OpenMarket 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 "MXKRoomSettingsViewController.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
@interface MXKRoomSettingsViewController()
{
// the room events listener
id roomListener;
// Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
id leaveRoomNotificationObserver;
// Observe kMXRoomDidFlushDataNotification to take into account the updated room state when the room history is flushed.
id roomDidFlushDataNotificationObserver;
}
@end
@implementation MXKRoomSettingsViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKRoomSettingsViewController class])
bundle:[NSBundle bundleForClass:[MXKRoomSettingsViewController class]]];
}
+ (instancetype)roomSettingsViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKRoomSettingsViewController class])
bundle:[NSBundle bundleForClass:[MXKRoomSettingsViewController class]]];
}
#pragma mark -
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self refreshRoomSettings];
}
#pragma mark - Override MXKTableViewController
- (void)finalizeInit
{
[super finalizeInit];
}
- (void)destroy
{
if (roomListener)
{
MXWeakify(self);
[mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) {
MXStrongifyAndReturnIfNil(self);
[liveTimeline removeListener:self->roomListener];
self->roomListener = nil;
}];
}
if (leaveRoomNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:leaveRoomNotificationObserver];
leaveRoomNotificationObserver = nil;
}
if (roomDidFlushDataNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:roomDidFlushDataNotificationObserver];
roomDidFlushDataNotificationObserver = nil;
}
mxRoom = nil;
mxRoomState = nil;
[super destroy];
}
- (void)onMatrixSessionStateDidChange:(NSNotification *)notif;
{
// Check this is our Matrix session that has changed
if (notif.object == self.mainSession)
{
[super onMatrixSessionStateDidChange:notif];
// refresh when the session sync is done.
if (MXSessionStateRunning == self.mainSession.state)
{
[self refreshRoomSettings];
}
}
}
#pragma mark - Public API
/**
Set the dedicated session and the room Id
*/
- (void)initWithSession:(MXSession*)mxSession andRoomId:(NSString*)roomId
{
// Update the matrix session
if (self.mainSession)
{
[self removeMatrixSession:self.mainSession];
}
mxRoom = nil;
// Sanity checks
if (mxSession && roomId)
{
[self addMatrixSession:mxSession];
// Report the room identifier
_roomId = roomId;
mxRoom = [mxSession roomWithRoomId:roomId];
}
if (mxRoom)
{
// Register a listener to handle messages related to room name, topic...
MXWeakify(self);
[mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) {
MXStrongifyAndReturnIfNil(self);
self->roomListener = [liveTimeline listenToEventsOfTypes:@[kMXEventTypeStringRoomName, kMXEventTypeStringRoomTopic, kMXEventTypeStringRoomAliases, kMXEventTypeStringRoomAvatar, kMXEventTypeStringRoomPowerLevels, kMXEventTypeStringRoomCanonicalAlias, kMXEventTypeStringRoomJoinRules, kMXEventTypeStringRoomGuestAccess, kMXEventTypeStringRoomHistoryVisibility] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
// Consider only live events
if (direction == MXTimelineDirectionForwards)
{
[self updateRoomState:liveTimeline.state];
}
}];
// Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room.
self->leaveRoomNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionWillLeaveRoomNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
// Check whether the user will leave the room related to the displayed participants
if (notif.object == self.mainSession)
{
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
if (roomId && [roomId isEqualToString:self.roomId])
{
// We remove the current view controller.
[self withdrawViewControllerAnimated:YES completion:nil];
}
}
}];
// Observe room history flush (sync with limited timeline, or state event redaction)
self->roomDidFlushDataNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomDidFlushDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXRoom *room = notif.object;
if (self.mainSession == room.mxSession && [self.roomId isEqualToString:room.roomId])
{
// The existing room history has been flushed during server sync. Take into account the updated room state.
[self updateRoomState:liveTimeline.state];
}
}];
[self updateRoomState:liveTimeline.state];
}];
}
self.title = [MatrixKitL10n roomDetailsTitle];
}
- (void)refreshRoomSettings
{
[self.tableView reloadData];
}
- (void)updateRoomState:(MXRoomState*)newRoomState
{
mxRoomState = newRoomState.copy;
[self refreshRoomSettings];
}
#pragma mark - UITableViewDataSource
// empty by default
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
@end
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKRoomSettingsViewController">
<connections>
<outlet property="view" destination="ju7-p5-cp4" id="eNV-3R-aIp"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="24" sectionFooterHeight="24" id="ju7-p5-cp4">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<outlet property="dataSource" destination="-1" id="BWQ-3I-rCG"/>
<outlet property="delegate" destination="-1" id="lmH-mW-1fL"/>
</connections>
</tableView>
</objects>
</document>
@@ -0,0 +1,440 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
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 <MatrixSDK/MatrixSDK.h>
#import "MXKViewController.h"
#import "MXKRoomDataSource.h"
#import "MXKRoomTitleView.h"
#import "MXKRoomInputToolbarView.h"
#import "MXKRoomActivitiesView.h"
#import "MXKEventDetailsView.h"
#import "MXKAttachmentsViewController.h"
#import "MXKAttachmentAnimator.h"
typedef NS_ENUM(NSUInteger, MXKRoomViewControllerJoinRoomResult) {
MXKRoomViewControllerJoinRoomResultSuccess,
MXKRoomViewControllerJoinRoomResultFailureRoomEmpty,
MXKRoomViewControllerJoinRoomResultFailureJoinInProgress,
MXKRoomViewControllerJoinRoomResultFailureGeneric
};
/**
This view controller displays messages of a room. Only one matrix session is handled by this view controller.
*/
@interface MXKRoomViewController : MXKViewController <MXKDataSourceDelegate, MXKRoomTitleViewDelegate, MXKRoomInputToolbarViewDelegate, UITableViewDelegate, UIDocumentInteractionControllerDelegate, MXKAttachmentsViewControllerDelegate, MXKRoomActivitiesViewDelegate, MXKSourceAttachmentAnimatorDelegate>
{
@protected
/**
The identifier of the current event displayed at the bottom of the table (just above the toolbar).
Use to anchor the message displayed at the bottom during table refresh.
*/
NSString *currentEventIdAtTableBottom;
/**
Boolean value used to scroll to bottom the bubble history after refresh.
*/
BOOL shouldScrollToBottomOnTableRefresh;
/**
Potential event details view.
*/
__weak MXKEventDetailsView *eventDetailsView;
/**
Current alert (if any).
*/
__weak UIAlertController *currentAlert;
/**
The document interaction Controller used to share attachment
*/
UIDocumentInteractionController *documentInteractionController;
/**
The current shared attachment.
*/
MXKAttachment *currentSharedAttachment;
/**
The potential text input placeholder is saved when it is replaced temporarily
*/
NSString *savedInputToolbarPlaceholder;
/**
Tell whether the input toolbar required to run an animation indicator.
*/
BOOL isInputToolbarProcessing;
/**
Tell whether a device rotation is in progress
*/
BOOL isSizeTransitionInProgress;
/**
The current visibility of the status bar in this view controller.
*/
BOOL isStatusBarHidden;
/**
YES to prevent `bubblesTableView` scrolling when calling -[setBubbleTableViewContentOffset:animated:]
*/
BOOL preventBubblesTableViewScroll;
}
/**
The current data source associated to the view controller.
*/
@property (nonatomic, readonly) MXKRoomDataSource *roomDataSource;
/**
Flag indicating if this instance has the memory ownership of its `roomDataSource`.
If YES, it will release it on [self destroy] call;
Default is NO.
*/
@property (nonatomic) BOOL hasRoomDataSourceOwnership;
/**
Tell whether the bubbles table view display is in transition. Its display is not warranty during the transition.
*/
@property (nonatomic, getter=isBubbleTableViewDisplayInTransition) BOOL bubbleTableViewDisplayInTransition;
/**
Tell whether the automatic events acknowledgement (based on read receipt) is enabled.
Default is YES.
*/
@property (nonatomic, getter=isEventsAcknowledgementEnabled) BOOL eventsAcknowledgementEnabled;
/**
Tell whether the room read marker must be updated when an event is acknowledged with a read receipt.
Default is NO.
*/
@property (nonatomic) BOOL updateRoomReadMarker;
/**
When the room view controller displays a room data source based on a timeline with an initial event,
the bubble table view content is scrolled by default to display the top of this event at the center of the screen
the first time it appears.
Use this property to force the table view to center its content on the bottom part of the event.
Default is NO.
*/
@property (nonatomic) BOOL centerBubblesTableViewContentOnTheInitialEventBottom;
/**
The current title view defined into the view controller.
*/
@property (nonatomic, weak, readonly) MXKRoomTitleView* titleView;
/**
The current input toolbar view defined into the view controller.
*/
@property (nonatomic, weak, readonly) MXKRoomInputToolbarView* inputToolbarView;
/**
The current extra info view defined into the view controller.
*/
@property (nonatomic, readonly) MXKRoomActivitiesView* activitiesView;
/**
The threshold used to trigger inconspicuous back pagination, or forwards pagination
for non live timeline. A pagination is triggered when the vertical content offset
is lower this threshold.
Default is 300.
*/
@property (nonatomic) NSUInteger paginationThreshold;
/**
The maximum number of messages to retrieve during a pagination. Default is 30.
*/
@property (nonatomic) NSUInteger paginationLimit;
/**
Enable/disable saving of the current typed text in message composer when view disappears.
The message composer is prefilled with this text when the room is opened again.
This property value is YES by default.
*/
@property BOOL saveProgressTextInput;
/**
The invited rooms can be automatically joined when the data source is ready.
This property enable/disable this option. Its value is YES by default.
*/
@property BOOL autoJoinInvitedRoom;
/**
Tell whether the room history is automatically scrolled to the most recent messages
when a keyboard is presented. YES by default.
This option is ignored when an alert is presented.
*/
@property BOOL scrollHistoryToTheBottomOnKeyboardPresentation;
/**
YES (default) to show actions button in document preview. NO otherwise.
*/
@property BOOL allowActionsInDocumentPreview;
/**
Duration of the animation in case of the composer needs to be resized (default 0.3s)
*/
@property NSTimeInterval resizeComposerAnimationDuration;
/**
This object is defined when the displayed room is left. It is added into the bubbles table header.
This label is used to display the reason why the room has been left.
*/
@property (nonatomic, weak, readonly) UILabel *leftRoomReasonLabel;
@property (weak, nonatomic) IBOutlet UITableView *bubblesTableView;
@property (weak, nonatomic) IBOutlet UIView *roomTitleViewContainer;
@property (weak, nonatomic) IBOutlet UIView *roomInputToolbarContainer;
@property (weak, nonatomic) IBOutlet UIView *roomActivitiesContainer;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bubblesTableViewTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bubblesTableViewBottomConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomActivitiesContainerHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerBottomConstraint;
#pragma mark - Class methods
/**
Returns the `UINib` object initialized for a `MXKRoomViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
@discussion You may override this method to provide a customized nib. If you do,
you should also override `roomViewController` to return your
view controller loaded from your custom nib.
*/
+ (UINib *)nib;
/**
Creates and returns a new `MXKRoomViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKRoomViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)roomViewController;
/**
Display a room.
@param dataSource the data source .
*/
- (void)displayRoom:(MXKRoomDataSource*)dataSource;
/**
This method is called when the associated data source is ready.
By default this operation triggers the initial back pagination when the user is an actual
member of the room (membership = join).
The invited rooms are automatically joined during this operation if 'autoJoinInvitedRoom' is YES.
When the room is successfully joined, an initial back pagination is triggered too.
Else nothing is done for the invited rooms.
Override it to customize the view controller behavior when the data source is ready.
*/
- (void)onRoomDataSourceReady;
/**
Update view controller appearance according to the state of its associated data source.
This method is called in the following use cases:
- on data source change (see `[MXKRoomViewController displayRoom:]`).
- on data source state change (see `[MXKDataSourceDelegate dataSource:didStateChange:]`)
- when view did appear.
The default implementation:
- show input toolbar view if the dataSource is defined and ready (`MXKDataSourceStateReady`), hide toolbar in others use cases.
- stop activity indicator if the dataSource is defined and ready (`MXKDataSourceStateReady`).
- update view controller title with room information.
Override it to customize view appearance according to data source state.
*/
- (void)updateViewControllerAppearanceOnRoomDataSourceState;
/**
This method is called when the associated data source has encountered an error on the timeline.
Override it to customize the view controller behavior.
@param notif the notification data sent with kMXKRoomDataSourceTimelineError notif.
*/
- (void)onTimelineError:(NSNotification *)notif;
/**
Join the current displayed room.
This operation fails if the user has already joined the room, or if the data source is not ready.
It fails if a join request is already running too.
@param completion the block to execute at the end of the operation.
You may specify nil for this parameter.
*/
- (void)joinRoom:(void(^)(MXKRoomViewControllerJoinRoomResult result))completion;
/**
Join a room with a room id or an alias.
This operation fails if the user has already joined the room, or if the data source is not ready,
or if the access to the room is forbidden to the user.
It fails if a join request is already running too.
@param roomIdOrAlias the id or the alias of the room to join.
@param viaServers The server names to try and join through in addition to those that are automatically chosen. It is optional and can be nil.
@param signUrl the signurl paramater passed with a 3PID invitation. It is optional and can be nil.
@param completion the block to execute at the end of the operation.
You may specify nil for this parameter.
*/
- (void)joinRoomWithRoomIdOrAlias:(NSString*)roomIdOrAlias
viaServers:(NSArray<NSString*>*)viaServers
andSignUrl:(NSString*)signUrl
completion:(void(^)(MXKRoomViewControllerJoinRoomResult result))completion;
/**
Update view controller appearance when the user is about to leave the displayed room.
This method is called when the user will leave the current room (see `kMXSessionWillLeaveRoomNotification`).
The default implementation:
- discard `roomDataSource`
- hide input toolbar view
- freeze the room title display
- add a label (`leftRoomReasonLabel`) in bubbles table header to display the reason why the room has been left.
Override it to customize view appearance, or to withdraw the view controller.
@param event the MXEvent responsible for the leaving.
*/
- (void)leaveRoomOnEvent:(MXEvent*)event;
/**
Register the class used to instantiate the title view which will handle the room name display.
The resulting view is added into 'roomTitleViewContainer' view, which must be defined before calling this method.
Note: By default the room name is displayed by using 'navigationItem.title' field of the view controller.
@param roomTitleViewClass a MXKRoomTitleView-inherited class.
*/
- (void)setRoomTitleViewClass:(Class)roomTitleViewClass;
/**
Register the class used to instantiate the input toolbar view which will handle message composer
and attachments selection for the room.
The resulting view is added into 'roomInputToolbarContainer' view, which must be defined before calling this method.
@param roomInputToolbarViewClass a MXKRoomInputToolbarView-inherited class, or nil to remove the current view.
*/
- (void)setRoomInputToolbarViewClass:(Class)roomInputToolbarViewClass;
/**
Register the class used to instantiate the extra info view.
The resulting view is added into 'roomActivitiesContainer' view, which must be defined before calling this method.
@param roomActivitiesViewClass a MXKRoomActivitiesViewClass-inherited class, or nil to remove the current view.
*/
- (void)setRoomActivitiesViewClass:(Class)roomActivitiesViewClass;
/**
Register the class used to instantiate the viewer dedicated to the attachments with thumbnail.
By default 'MXKAttachmentsViewController' class is used.
@param attachmentsViewerClass a MXKAttachmentsViewController-inherited class, or nil to restore the default class.
*/
- (void)setAttachmentsViewerClass:(Class)attachmentsViewerClass;
/**
Register the view class used to display the details of an event.
MXKEventDetailsView is used by default.
@param eventDetailsViewClass a MXKEventDetailsView-inherited class.
*/
- (void)setEventDetailsViewClass:(Class)eventDetailsViewClass;
/**
Detect and process potential IRC command in provided string.
@param string to analyse
@return YES if IRC style command has been detected and interpreted.
*/
- (BOOL)isIRCStyleCommand:(NSString*)string;
/**
Mention the member display name in the current text of the message composer.
The message composer becomes then the first responder.
*/
- (void)mention:(MXRoomMember*)roomMember;
/**
Force to dismiss keyboard if any
*/
- (void)dismissKeyboard;
/**
Tell whether the most recent message of the room history is visible.
*/
- (BOOL)isBubblesTableScrollViewAtTheBottom;
/**
Scroll the room history until the most recent message.
*/
- (void)scrollBubblesTableViewToBottomAnimated:(BOOL)animated;
/**
Dismiss the keyboard and all the potential subviews.
*/
- (void)dismissTemporarySubViews;
/**
Display a popup with the event detais.
@param event the event to inspect.
*/
- (void)showEventDetails:(MXEvent *)event;
/**
Present the attachments viewer by displaying the attachment of the provided cell.
@param cell the table view cell with attachment
*/
- (void)showAttachmentInCell:(UITableViewCell*)cell;
/**
Force a refresh of the room history display.
You should not call this method directly.
You may override it in inherited 'MXKRoomViewController' class.
@param useBottomAnchor tells whether the updated history must keep display the same event at the bottom.
@return a boolean value which tells whether the table has been scrolled to the bottom.
*/
- (BOOL)reloadBubblesTable:(BOOL)useBottomAnchor;
/**
Sets the offset from the content `bubblesTableView`'s origin. Take into account `preventBubblesTableViewScroll` value.
@param contentOffset Offset from the content `bubblesTableView`s origin.
@param animated YES to animate the transition.
*/
- (void)setBubbleTableViewContentOffset:(CGPoint)contentOffset animated:(BOOL)animated;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKRoomViewController">
<connections>
<outlet property="bubblesTableView" destination="BGD-sd-SQR" id="OG4-Tw-Ovt"/>
<outlet property="bubblesTableViewBottomConstraint" destination="Ksk-39-kfi" id="CTo-Ux-4NP"/>
<outlet property="bubblesTableViewTopConstraint" destination="X14-4s-uGM" id="Hic-6h-N05"/>
<outlet property="roomActivitiesContainer" destination="XX4-n6-hCm" id="uD0-ab-8s8"/>
<outlet property="roomActivitiesContainerHeightConstraint" destination="E8v-l2-8eV" id="ebD-oV-ttx"/>
<outlet property="roomInputToolbarContainer" destination="nLd-BP-JAE" id="1dp-P1-0js"/>
<outlet property="roomInputToolbarContainerBottomConstraint" destination="kQ6-Cg-FMi" id="nHr-fR-XnV"/>
<outlet property="roomInputToolbarContainerHeightConstraint" destination="5eD-Fm-RDb" id="6ny-5w-1UA"/>
<outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="BGD-sd-SQR">
<rect key="frame" x="0.0" y="0.0" width="375" height="626"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XX4-n6-hCm" userLabel="Activities Container">
<rect key="frame" x="0.0" y="606" width="375" height="20"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="E8v-l2-8eV"/>
</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" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="41" id="5eD-Fm-RDb"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="BGD-sd-SQR" secondAttribute="trailing" id="0la-ok-MBr"/>
<constraint firstItem="nLd-BP-JAE" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="4Q7-hr-rqi"/>
<constraint firstAttribute="bottom" secondItem="BGD-sd-SQR" secondAttribute="bottom" constant="41" id="Ksk-39-kfi"/>
<constraint firstItem="XX4-n6-hCm" firstAttribute="bottom" secondItem="nLd-BP-JAE" secondAttribute="top" id="QO8-nF-xys"/>
<constraint firstItem="XX4-n6-hCm" firstAttribute="width" secondItem="iN0-l3-epB" secondAttribute="width" id="WhE-lH-ZtR"/>
<constraint firstItem="BGD-sd-SQR" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="X14-4s-uGM"/>
<constraint firstAttribute="trailing" secondItem="nLd-BP-JAE" secondAttribute="trailing" id="YAu-gd-ItG"/>
<constraint firstItem="XX4-n6-hCm" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="csl-KT-4s9"/>
<constraint firstItem="BGD-sd-SQR" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="haP-Kv-OLI"/>
<constraint firstAttribute="bottom" secondItem="nLd-BP-JAE" secondAttribute="bottom" id="kQ6-Cg-FMi"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
</view>
</objects>
</document>
@@ -0,0 +1,75 @@
/*
Copyright 2015 OpenMarket 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 <UIKit/UIKit.h>
#import "MXKViewController.h"
#import "MXKSearchDataSource.h"
/**
This view controller handles search server side. Only one matrix session is handled by this view controller.
According to its dataSource configuration the search can be done all user's rooms or a set of rooms.
*/
@interface MXKSearchViewController : MXKViewController <UITableViewDelegate, MXKDataSourceDelegate, UISearchBarDelegate>
@property (weak, nonatomic) IBOutlet UISearchBar *searchSearchBar;
@property (weak, nonatomic) IBOutlet UITableView *searchTableView;
@property (weak, nonatomic) IBOutlet UILabel *noResultsLabel;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchSearchBarTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchSearchBarHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *searchTableViewBottomConstraint;
/**
The current data source associated to the view controller.
*/
@property (nonatomic, readonly) MXKSearchDataSource *dataSource;
/**
Enable the search option by adding a navigation item in the navigation bar (YES by default).
Set NO this property to disable this option and hide the related bar button.
*/
@property (nonatomic) BOOL enableBarButtonSearch;
/**
If YES, the table view will scroll at the bottom on the next data source refresh.
It comes back to NO after each refresh.
*/
@property (nonatomic) BOOL shouldScrollToBottomOnRefresh;
#pragma mark - Class methods
/**
Creates and returns a new `MXKSearchViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `MXKSearchViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)searchViewController;
/**
Display the search results described in the provided data source.
Note: The provided data source replaces the current data source if any. The current
data source is released.
@param searchDataSource the data source providing the search results.
*/
- (void)displaySearch:(MXKSearchDataSource*)searchDataSource;
@end
@@ -0,0 +1,423 @@
/*
Copyright 2015 OpenMarket 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 "MXKSearchViewController.h"
#import "MXKSearchTableViewCell.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
@interface MXKSearchViewController ()
{
/**
Optional bar buttons
*/
UIBarButtonItem *searchBarButton;
/**
Search handling
*/
BOOL ignoreSearchRequest;
}
@end
@implementation MXKSearchViewController
@synthesize dataSource, shouldScrollToBottomOnRefresh;
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass([MXKSearchViewController class])
bundle:[NSBundle bundleForClass:[MXKSearchViewController class]]];
}
+ (instancetype)searchViewController
{
return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKSearchViewController class])
bundle:[NSBundle bundleForClass:[MXKSearchViewController class]]];
}
#pragma mark -
- (void)finalizeInit
{
[super finalizeInit];
_enableBarButtonSearch = YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Check whether the view controller has been pushed via storyboard
if (!_searchTableView)
{
// Instantiate view controller objects
[[[self class] nib] instantiateWithOwner:self options:nil];
}
// Adjust Top and Bottom constraints to take into account potential navBar and tabBar.
[NSLayoutConstraint deactivateConstraints:@[_searchSearchBarTopConstraint, _searchTableViewBottomConstraint]];
_searchSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.searchSearchBar
attribute:NSLayoutAttributeTop
multiplier:1.0f
constant:0.0f];
_searchTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.searchTableView
attribute:NSLayoutAttributeBottom
multiplier:1.0f
constant:0.0f];
[NSLayoutConstraint activateConstraints:@[_searchSearchBarTopConstraint, _searchTableViewBottomConstraint]];
// Hide search bar by default
self.searchSearchBar.hidden = YES;
self.searchSearchBarHeightConstraint.constant = 0;
[self.view setNeedsUpdateConstraints];
self.noResultsLabel.text = [MatrixKitL10n searchNoResults];
self.noResultsLabel.hidden = YES;
searchBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(showSearchBar:)];
// Apply search option in navigation bar
self.enableBarButtonSearch = _enableBarButtonSearch;
// Finalize table view configuration
_searchTableView.delegate = self;
_searchTableView.dataSource = dataSource; // Note: dataSource may be nil here
// Set up classes to use for cells
[self.searchTableView registerNib:MXKSearchTableViewCell.nib forCellReuseIdentifier:MXKSearchTableViewCell.defaultReuseIdentifier];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Restore search mechanism (if enabled)
ignoreSearchRequest = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// The user may still press search button whereas the view disappears
ignoreSearchRequest = YES;
}
#pragma mark - Override MXKViewController
- (void)onKeyboardShowAnimationComplete
{
// Report the keyboard view in order to track keyboard frame changes
self.keyboardView = _searchSearchBar.inputAccessoryView.superview;
}
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
// Deduce the bottom constraint for the table view (Don't forget the potential tabBar)
CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
// Check whether the keyboard is over the tabBar
if (tableViewBottomConst < 0)
{
tableViewBottomConst = 0;
}
// Update constraints
_searchTableViewBottomConstraint.constant = tableViewBottomConst;
// Force layout immediately to take into account new constraint
[self.view layoutIfNeeded];
}
- (void)destroy
{
_searchTableView.dataSource = nil;
_searchTableView.delegate = nil;
_searchTableView = nil;
dataSource.delegate = nil;
[dataSource destroy];
dataSource = nil;
[super destroy];
}
#pragma mark -
- (void)displaySearch:(MXKSearchDataSource*)searchDataSource
{
// Cancel registration on existing dataSource if any
if (dataSource)
{
dataSource.delegate = nil;
// Remove associated matrix sessions
[self removeMatrixSession:dataSource.mxSession];
[dataSource destroy];
}
dataSource = searchDataSource;
dataSource.delegate = self;
// Report the related matrix sessions at view controller level to update UI according to sessions state
[self addMatrixSession:searchDataSource.mxSession];
if (_searchTableView)
{
// Set up table data source
_searchTableView.dataSource = dataSource;
}
}
#pragma mark - UIBarButton handling
- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch
{
_enableBarButtonSearch = enableBarButtonSearch;
[self refreshUIBarButtons];
}
- (void)refreshUIBarButtons
{
if (_enableBarButtonSearch)
{
self.navigationItem.rightBarButtonItems = @[searchBarButton];
}
else
{
self.navigationItem.rightBarButtonItems = nil;
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
return MXKSearchTableViewCell.class;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
return MXKSearchTableViewCell.defaultReuseIdentifier;
}
- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
__block CGPoint tableViewOffset;
if (!shouldScrollToBottomOnRefresh)
{
// Store current tableview scrolling point to restore it after [UITableView reloadData]
// This avoids unexpected scrolling for the user
tableViewOffset = _searchTableView.contentOffset;
}
[_searchTableView reloadData];
if (shouldScrollToBottomOnRefresh)
{
[self scrollToBottomAnimated:NO];
shouldScrollToBottomOnRefresh = NO;
}
else
{
// Restore the user scrolling point by computing the offset introduced by new cells
// New cells are always introduced at the top of the table
NSIndexSet *insertedIndexes = (NSIndexSet*)changes;
// Get each new cell height
[insertedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
MXKCellData* cellData = [self.dataSource cellDataAtIndex:idx];
Class<MXKCellRendering> class = [self cellViewClassForCellData:cellData];
tableViewOffset.y += [class heightForCellData:cellData withMaximumWidth:self->_searchTableView.frame.size.width];
}];
[_searchTableView setContentOffset:tableViewOffset animated:NO];
}
self.title = [NSString stringWithFormat:@"%@ (%tu)", self.dataSource.searchText, self.dataSource.serverCount];
}
- (void)dataSource:(MXKDataSource*)dataSource2 didStateChange:(MXKDataSourceState)state
{
// MXKSearchDataSource comes back to the `MXKDataSourceStatePreparing` when searching
if (state == MXKDataSourceStatePreparing)
{
_noResultsLabel.hidden = YES;
[self startActivityIndicator];
}
else
{
[self stopActivityIndicator];
// Display "No Results" if a search is active with an empty result
if (dataSource.searchText.length && ![dataSource tableView:_searchTableView numberOfRowsInSection:0])
{
_noResultsLabel.hidden = NO;
_searchTableView.hidden = YES;
}
else
{
_noResultsLabel.hidden = YES;
_searchTableView.hidden = NO;
}
}
}
#pragma mark - UITableView delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MXKCellData *cellData = [dataSource cellDataAtIndex:indexPath.row];
Class<MXKCellRendering> class = [self cellViewClassForCellData:cellData];
return [class heightForCellData:cellData withMaximumWidth:tableView.frame.size.width];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Must be implemented at app level
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
// Release here resources, and restore reusable cells
if ([cell respondsToSelector:@selector(didEndDisplay)])
{
[(id<MXKCellRendering>)cell didEndDisplay];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Detect vertical bounce at the top of the tableview to trigger pagination
if (scrollView == _searchTableView)
{
// paginate ?
if (scrollView.contentOffset.y < -64)
{
[self triggerBackPagination];
}
}
}
#pragma mark - UISearchBarDelegate
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
// "Done" key has been pressed
[searchBar resignFirstResponder];
// Apply filter
if (searchBar.text.length)
{
shouldScrollToBottomOnRefresh = YES;
[dataSource searchMessages:searchBar.text force:NO];
}
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
// Leave search
[searchBar resignFirstResponder];
self.searchSearchBar.hidden = YES;
self.searchSearchBarHeightConstraint.constant = 0;
[self.view setNeedsUpdateConstraints];
self.searchSearchBar.text = nil;
}
#pragma mark - Actions
- (void)showSearchBar:(id)sender
{
// The user may have pressed search button whereas the view controller was disappearing
if (ignoreSearchRequest)
{
return;
}
if (self.searchSearchBar.isHidden)
{
self.searchSearchBar.hidden = NO;
self.searchSearchBarHeightConstraint.constant = 44;
[self.view setNeedsUpdateConstraints];
[self.searchSearchBar becomeFirstResponder];
}
else
{
[self searchBarCancelButtonClicked: self.searchSearchBar];
}
}
#pragma mark - Private methods
- (void)triggerBackPagination
{
// Paginate only if possible
if (NO == dataSource.canPaginate)
{
return;
}
[dataSource paginateBack];
}
- (void)scrollToBottomAnimated:(BOOL)animated
{
if (_searchTableView.contentSize.height)
{
CGFloat visibleHeight = _searchTableView.frame.size.height - _searchTableView.adjustedContentInset.top - _searchTableView.adjustedContentInset.bottom;
if (visibleHeight < _searchTableView.contentSize.height)
{
CGFloat wantedOffsetY = _searchTableView.contentSize.height - visibleHeight - _searchTableView.adjustedContentInset.top;
CGFloat currentOffsetY = _searchTableView.contentOffset.y;
if (wantedOffsetY != currentOffsetY)
{
[_searchTableView setContentOffset:CGPointMake(0, wantedOffsetY) animated:animated];
}
}
else
{
_searchTableView.contentOffset = CGPointMake(0, - _searchTableView.adjustedContentInset.top);
}
}
}
@end
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MXKSearchViewController">
<connections>
<outlet property="noResultsLabel" destination="R0k-Za-T1q" id="B08-vF-6Fy"/>
<outlet property="searchSearchBar" destination="Zbr-9e-VZh" id="cnj-ol-yU0"/>
<outlet property="searchSearchBarHeightConstraint" destination="jCU-cq-2OA" id="xFA-RP-Mdl"/>
<outlet property="searchSearchBarTopConstraint" destination="aRg-Nz-enq" id="cKu-YM-PQP"/>
<outlet property="searchTableView" destination="orV-HH-88x" id="G7I-A9-ZjX"/>
<outlet property="searchTableViewBottomConstraint" destination="m4x-32-odR" id="F7E-uT-fJX"/>
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" showsCancelButton="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zbr-9e-VZh">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="jCU-cq-2OA"/>
</constraints>
<textInputTraits key="textInputTraits" returnKeyType="done"/>
<connections>
<outlet property="delegate" destination="-1" id="BNF-PO-yKJ"/>
</connections>
</searchBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="orV-HH-88x">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</tableView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Results" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R0k-Za-T1q">
<rect key="frame" x="15" y="60" width="375" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="RRM-CN-Pq2"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="NaR-eJ-WMj"/>
<constraint firstItem="R0k-Za-T1q" firstAttribute="top" secondItem="Zbr-9e-VZh" secondAttribute="bottom" constant="16" id="UC4-55-9yg"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="aRg-Nz-enq"/>
<constraint firstItem="R0k-Za-T1q" firstAttribute="trailing" secondItem="Zbr-9e-VZh" secondAttribute="trailing" constant="15" id="bvY-5p-dck"/>
<constraint firstItem="Zbr-9e-VZh" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="fty-XB-tTr"/>
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="Zbr-9e-VZh" secondAttribute="bottom" id="lBa-P2-Vnx"/>
<constraint firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="m4x-32-odR"/>
<constraint firstItem="R0k-Za-T1q" firstAttribute="leading" secondItem="Zbr-9e-VZh" secondAttribute="leading" constant="15" id="oCw-HZ-Aqi"/>
<constraint firstAttribute="trailing" secondItem="Zbr-9e-VZh" secondAttribute="trailing" id="rKb-TZ-sap"/>
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="yBp-63-kZi"/>
</constraints>
</view>
</objects>
</document>
@@ -0,0 +1,27 @@
/*
Copyright 2015 OpenMarket 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 "MXKViewControllerHandling.h"
/**
MXKViewController extends UITableViewController to handle requirements for
any matrixKit table view controllers (see MXKViewControllerHandling protocol).
*/
@interface MXKTableViewController : UITableViewController <MXKViewControllerHandling>
@end
@@ -0,0 +1,574 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 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 "MXKTableViewController.h"
#import "UIViewController+MatrixKit.h"
#import "MXSession+MatrixKit.h"
@interface MXKTableViewController ()
{
/**
Array of `MXSession` instances.
*/
NSMutableArray *mxSessionArray;
/**
Keep reference on the pushed view controllers to release them correctly
*/
NSMutableArray *childViewControllers;
}
@end
@implementation MXKTableViewController
@synthesize defaultBarTintColor, enableBarTintColorStatusChange;
@synthesize barTitleColor;
@synthesize mainSession;
@synthesize activityIndicator, rageShakeManager;
@synthesize childViewControllers;
#pragma mark -
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
[self finalizeInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
[self finalizeInit];
}
return self;
}
- (void)finalizeInit
{
// Set default properties values
defaultBarTintColor = nil;
barTitleColor = nil;
enableBarTintColorStatusChange = YES;
rageShakeManager = nil;
mxSessionArray = [NSMutableArray array];
childViewControllers = [NSMutableArray array];
}
#pragma mark -
- (void)viewDidLoad
{
[super viewDidLoad];
// Add default activity indicator
activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
activityIndicator.backgroundColor = [UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1.0];
activityIndicator.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
CGRect frame = activityIndicator.frame;
frame.size.width += 30;
frame.size.height += 30;
activityIndicator.bounds = frame;
[activityIndicator.layer setCornerRadius:5];
activityIndicator.center = self.view.center;
[self.view addSubview:activityIndicator];
}
- (void)dealloc
{
if (activityIndicator)
{
[activityIndicator removeFromSuperview];
activityIndicator = nil;
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.rageShakeManager)
{
[self.rageShakeManager cancel:self];
}
// Update UI according to mxSession state, and add observer (if need)
if (mxSessionArray.count)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil];
}
[self onMatrixSessionChange];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
[activityIndicator stopAnimating];
if (self.rageShakeManager)
{
[self.rageShakeManager cancel:self];
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
MXLogDebug(@"[MXKTableViewController] %@ viewDidAppear", self.class);
// Release properly pushed and/or presented view controller
if (childViewControllers.count)
{
for (id viewController in childViewControllers)
{
if ([viewController isKindOfClass:[UINavigationController class]])
{
UINavigationController *navigationController = (UINavigationController*)viewController;
for (id subViewController in navigationController.viewControllers)
{
if ([subViewController respondsToSelector:@selector(destroy)])
{
[subViewController destroy];
}
}
}
else if ([viewController respondsToSelector:@selector(destroy)])
{
[viewController destroy];
}
}
[childViewControllers removeAllObjects];
}
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
MXLogDebug(@"[MXKTableViewController] %@ viewDidDisappear", self.class);
}
- (void)setEnableBarTintColorStatusChange:(BOOL)enable
{
if (enableBarTintColorStatusChange != enable)
{
enableBarTintColorStatusChange = enable;
[self onMatrixSessionChange];
}
}
- (void)setDefaultBarTintColor:(UIColor *)barTintColor
{
defaultBarTintColor = barTintColor;
if (enableBarTintColorStatusChange)
{
// Force update by taking into account the matrix session state.
[self onMatrixSessionChange];
}
else
{
// Set default tintColor
self.navigationController.navigationBar.barTintColor = defaultBarTintColor;
self.mxk_mainNavigationController.navigationBar.barTintColor = defaultBarTintColor;
}
}
- (void)setBarTitleColor:(UIColor *)titleColor
{
barTitleColor = titleColor;
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
UINavigationController *mainNavigationController = self.mxk_mainNavigationController;
// Set navigation bar title color
NSDictionary<NSString *,id> *titleTextAttributes = self.navigationController.navigationBar.titleTextAttributes;
if (titleTextAttributes)
{
NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithDictionary:titleTextAttributes];
textAttributes[NSForegroundColorAttributeName] = barTitleColor;
self.navigationController.navigationBar.titleTextAttributes = textAttributes;
}
else if (barTitleColor)
{
self.navigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: barTitleColor};
}
if (mainNavigationController)
{
titleTextAttributes = mainNavigationController.navigationBar.titleTextAttributes;
if (titleTextAttributes)
{
NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithDictionary:titleTextAttributes];
textAttributes[NSForegroundColorAttributeName] = barTitleColor;
mainNavigationController.navigationBar.titleTextAttributes = textAttributes;
}
else if (barTitleColor)
{
mainNavigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: barTitleColor};
}
}
}
- (void)setView:(UIView *)view
{
[super setView:view];
// Keep the activity indicator (if any)
if (view && activityIndicator)
{
[self.view addSubview:activityIndicator];
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Keep ref on destinationViewController
[childViewControllers addObject:segue.destinationViewController];
}
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
{
// Keep ref on presented view controller
[childViewControllers addObject:viewControllerToPresent];
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
}
#pragma mark -
- (void)addMatrixSession:(MXSession*)mxSession
{
if (!mxSession || mxSession.state == MXSessionStateClosed)
{
return;
}
if (!mxSessionArray.count)
{
[mxSessionArray addObject:mxSession];
// Add matrix sessions observer on first added session
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil];
}
else if ([mxSessionArray indexOfObject:mxSession] == NSNotFound)
{
[mxSessionArray addObject:mxSession];
}
// Force update
[self onMatrixSessionChange];
}
- (void)removeMatrixSession:(MXSession*)mxSession
{
if (!mxSession)
{
return;
}
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
if (index != NSNotFound)
{
[mxSessionArray removeObjectAtIndex:index];
if (!mxSessionArray.count)
{
// Remove matrix sessions observer
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
}
}
// Force update
[self onMatrixSessionChange];
}
- (NSArray*)mxSessions
{
return [NSArray arrayWithArray:mxSessionArray];
}
- (MXSession*)mainSession
{
// We consider the first added session as the main one.
if (mxSessionArray.count)
{
return [mxSessionArray firstObject];
}
return nil;
}
#pragma mark -
- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
{
// Check whether the view controller is embedded inside a navigation controller.
if (self.navigationController)
{
[self popViewController:self navigationController:self.navigationController animated:animated completion:completion];
}
else
{
// Suppose here the view controller has been presented modally. We dismiss it
[self dismissViewControllerAnimated:animated completion:completion];
}
}
- (void)popViewController:(UIViewController*)viewController navigationController:(UINavigationController*)navigationController animated:(BOOL)animated completion:(void (^)(void))completion
{
// We pop the view controller (except if it is the root view controller).
NSUInteger index = [navigationController.viewControllers indexOfObject:viewController];
if (index != NSNotFound)
{
if (index > 0)
{
UIViewController *previousViewController = [navigationController.viewControllers objectAtIndex:(index - 1)];
[navigationController popToViewController:previousViewController animated:animated];
if (completion)
{
completion();
}
}
else
{
// Check whether the navigation controller is embedded inside a navigation controller, to pop it.
if (navigationController.navigationController)
{
[self popViewController:navigationController navigationController:navigationController.navigationController animated:animated completion:completion];
}
else
{
// Remove the root view controller
navigationController.viewControllers = @[];
// Suppose here the navigation controller has been presented modally. We dismiss it
[navigationController dismissViewControllerAnimated:animated completion:completion];
}
}
}
}
- (void)destroy
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
mxSessionArray = nil;
childViewControllers = nil;
}
#pragma mark - Sessions handling
- (void)onMatrixSessionStateDidChange:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
if (index != NSNotFound)
{
if (mxSession.state == MXSessionStateClosed)
{
// Call here the dedicated method which may be overridden
[self removeMatrixSession:mxSession];
}
else
{
[self onMatrixSessionChange];
}
}
}
- (void)onMatrixSessionChange
{
// This method is called to refresh view controller appearance on session state change,
// It is called when the view will appear to update session array by removing closed sessions.
// Indeed 'kMXSessionStateDidChangeNotification' are observed only when the view controller is visible.
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
UINavigationController *mainNavigationController = self.mxk_mainNavigationController;
if (mxSessionArray.count)
{
// Check each session state
UIColor *barTintColor = defaultBarTintColor;
BOOL allHomeserverNotReachable = YES;
BOOL isActivityInProgress = NO;
for (NSUInteger index = 0; index < mxSessionArray.count;)
{
MXSession *mxSession = mxSessionArray[index];
// Remove here closed sessions
if (mxSession.state == MXSessionStateClosed)
{
// Call here the dedicated method which may be overridden.
// This method will call again [onMatrixSessionChange] when session is removed.
[self removeMatrixSession:mxSession];
return;
}
else
{
if (mxSession.state == MXSessionStateHomeserverNotReachable)
{
barTintColor = [UIColor orangeColor];
}
else
{
allHomeserverNotReachable = NO;
isActivityInProgress = mxSession.shouldShowActivityIndicator;
}
index ++;
}
}
// Check whether the navigation bar color depends on homeserver reachability.
if (enableBarTintColorStatusChange)
{
// The navigation bar tintColor reflects the matrix homeserver reachability status.
if (allHomeserverNotReachable)
{
self.navigationController.navigationBar.barTintColor = [UIColor redColor];
if (mainNavigationController)
{
mainNavigationController.navigationBar.barTintColor = [UIColor redColor];
}
}
else
{
self.navigationController.navigationBar.barTintColor = barTintColor;
if (mainNavigationController)
{
mainNavigationController.navigationBar.barTintColor = barTintColor;
}
}
}
// Run activity indicator if need
if (isActivityInProgress)
{
[self startActivityIndicator];
}
else
{
[self stopActivityIndicator];
}
}
else
{
// Hide potential activity indicator
[self stopActivityIndicator];
// Check whether the navigation bar color depends on homeserver reachability.
if (enableBarTintColorStatusChange)
{
// Restore default tintColor
self.navigationController.navigationBar.barTintColor = defaultBarTintColor;
if (mainNavigationController)
{
mainNavigationController.navigationBar.barTintColor = defaultBarTintColor;
}
}
}
}
#pragma mark - Activity indicator
- (void)startActivityIndicator
{
if (activityIndicator)
{
// Keep centering the loading wheel
CGPoint center = self.view.center;
center.y += self.tableView.contentOffset.y - self.tableView.adjustedContentInset.top;
activityIndicator.center = center;
[self.view bringSubviewToFront:activityIndicator];
[activityIndicator startAnimating];
// Show the loading wheel after a delay so that if the caller calls stopActivityIndicator
// in a short future, the loading wheel will not be displayed to the end user.
activityIndicator.alpha = 0;
[UIView animateWithDuration:0.3 delay:0.3 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
self->activityIndicator.alpha = 1;
} completion:^(BOOL finished)
{
}];
}
}
- (void)stopActivityIndicator
{
// Check whether all conditions are satisfied before stopping loading wheel
BOOL isActivityInProgress = NO;
for (MXSession *mxSession in mxSessionArray)
{
if (mxSession.shouldShowActivityIndicator)
{
isActivityInProgress = YES;
}
}
if (!isActivityInProgress)
{
[activityIndicator stopAnimating];
}
}
#pragma mark - Shake handling
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake && self.rageShakeManager)
{
[self.rageShakeManager startShaking:self];
}
}
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
[self motionEnded:motion withEvent:event];
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (self.rageShakeManager)
{
[self.rageShakeManager stopShaking:self];
}
}
- (BOOL)canBecomeFirstResponder
{
return (self.rageShakeManager != nil);
}
@end
@@ -0,0 +1,52 @@
/*
Copyright 2015 OpenMarket 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 "MXKViewControllerHandling.h"
#import "MXKActivityHandlingViewController.h"
/**
MXKViewController extends UIViewController to handle requirements for
any matrixKit view controllers (see MXKViewControllerHandling protocol).
This class provides some methods to ease keyboard handling.
*/
@interface MXKViewController : MXKActivityHandlingViewController <MXKViewControllerHandling>
#pragma mark - Keyboard handling
/**
Call when keyboard display animation is complete.
Override this method to set the actual keyboard view in 'keyboardView' property.
The 'MXKViewController' instance will then observe the keyboard frame changes, and update 'keyboardHeight' property.
*/
- (void)onKeyboardShowAnimationComplete;
/**
The current keyboard view (This field is nil when keyboard is dismissed).
This property should be set when keyboard display animation is complete to track keyboard frame changes.
*/
@property (nonatomic) UIView *keyboardView;
/**
The current keyboard height (This field is 0 when keyboard is dismissed).
*/
@property CGFloat keyboardHeight;
@end
@@ -0,0 +1,657 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 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 "MXKViewController.h"
#import "UIViewController+MatrixKit.h"
#import "MXSession+MatrixKit.h"
const CGFloat MXKViewControllerMaxExternalKeyboardHeight = 80;
@interface MXKViewController ()
{
/**
Array of `MXSession` instances.
*/
NSMutableArray *mxSessionArray;
/**
Keep reference on the pushed view controllers to release them correctly
*/
NSMutableArray *childViewControllers;
}
@end
@implementation MXKViewController
@synthesize defaultBarTintColor, enableBarTintColorStatusChange;
@synthesize barTitleColor;
@synthesize mainSession;
@synthesize rageShakeManager;
@synthesize childViewControllers;
#pragma mark -
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
[self finalizeInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
[self finalizeInit];
}
return self;
}
- (void)finalizeInit
{
// Set default properties values
defaultBarTintColor = nil;
barTitleColor = nil;
enableBarTintColorStatusChange = YES;
rageShakeManager = nil;
mxSessionArray = [NSMutableArray array];
childViewControllers = [NSMutableArray array];
}
#pragma mark -
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.rageShakeManager)
{
[self.rageShakeManager cancel:self];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
// Update UI according to mxSession state, and add observer (if need)
if (mxSessionArray.count)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil];
}
[self onMatrixSessionChange];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
[self.activityIndicator stopAnimating];
if (self.rageShakeManager)
{
[self.rageShakeManager cancel:self];
}
// Remove keyboard view (if any)
self.keyboardView = nil;
self.keyboardHeight = 0;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
MXLogDebug(@"[MXKViewController] %@ viewDidAppear", self.class);
// Release properly pushed and/or presented view controller
if (childViewControllers.count)
{
for (id viewController in childViewControllers)
{
if ([viewController isKindOfClass:[UINavigationController class]])
{
UINavigationController *navigationController = (UINavigationController*)viewController;
for (id subViewController in navigationController.viewControllers)
{
if ([subViewController respondsToSelector:@selector(destroy)])
{
[subViewController destroy];
}
}
}
else if ([viewController respondsToSelector:@selector(destroy)])
{
[viewController destroy];
}
}
[childViewControllers removeAllObjects];
}
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
MXLogDebug(@"[MXKViewController] %@ viewDidDisappear", self.class);
}
- (void)setEnableBarTintColorStatusChange:(BOOL)enable
{
if (enableBarTintColorStatusChange != enable)
{
enableBarTintColorStatusChange = enable;
[self onMatrixSessionChange];
}
}
- (void)setDefaultBarTintColor:(UIColor *)barTintColor
{
defaultBarTintColor = barTintColor;
if (enableBarTintColorStatusChange)
{
// Force update by taking into account the matrix session state.
[self onMatrixSessionChange];
}
else
{
// Set default tintColor
self.navigationController.navigationBar.barTintColor = defaultBarTintColor;
self.mxk_mainNavigationController.navigationBar.barTintColor = defaultBarTintColor;
}
}
- (void)setBarTitleColor:(UIColor *)titleColor
{
barTitleColor = titleColor;
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
UINavigationController *mainNavigationController = self.mxk_mainNavigationController;
// Set navigation bar title color
NSDictionary<NSString *,id> *titleTextAttributes = self.navigationController.navigationBar.titleTextAttributes;
if (titleTextAttributes)
{
NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithDictionary:titleTextAttributes];
textAttributes[NSForegroundColorAttributeName] = barTitleColor;
self.navigationController.navigationBar.titleTextAttributes = textAttributes;
}
else if (barTitleColor)
{
self.navigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: barTitleColor};
}
if (mainNavigationController)
{
titleTextAttributes = mainNavigationController.navigationBar.titleTextAttributes;
if (titleTextAttributes)
{
NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithDictionary:titleTextAttributes];
textAttributes[NSForegroundColorAttributeName] = barTitleColor;
mainNavigationController.navigationBar.titleTextAttributes = textAttributes;
}
else if (barTitleColor)
{
mainNavigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: barTitleColor};
}
}
}
- (void)setView:(UIView *)view
{
[super setView:view];
// Keep the activity indicator (if any)
if (self.activityIndicator)
{
self.activityIndicator.center = self.view.center;
[self.view addSubview:self.activityIndicator];
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Keep ref on destinationViewController
[childViewControllers addObject:segue.destinationViewController];
}
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
{
// Keep ref on presented view controller
[childViewControllers addObject:viewControllerToPresent];
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
}
#pragma mark -
- (void)addMatrixSession:(MXSession*)mxSession
{
if (!mxSession || mxSession.state == MXSessionStateClosed)
{
return;
}
if (!mxSessionArray.count)
{
[mxSessionArray addObject:mxSession];
// Add matrix sessions observer on first added session
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil];
}
else if ([mxSessionArray indexOfObject:mxSession] == NSNotFound)
{
[mxSessionArray addObject:mxSession];
}
// Force update
[self onMatrixSessionChange];
}
- (void)removeMatrixSession:(MXSession*)mxSession
{
if (!mxSession)
{
return;
}
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
if (index != NSNotFound)
{
[mxSessionArray removeObjectAtIndex:index];
if (!mxSessionArray.count)
{
// Remove matrix sessions observer
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
}
}
// Force update
[self onMatrixSessionChange];
}
- (NSArray*)mxSessions
{
return [NSArray arrayWithArray:mxSessionArray];
}
- (MXSession*)mainSession
{
// We consider the first added session as the main one.
if (mxSessionArray.count)
{
return [mxSessionArray firstObject];
}
return nil;
}
#pragma mark -
- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
{
// Check whether the view controller is embedded inside a navigation controller.
if (self.navigationController)
{
[self popViewController:self navigationController:self.navigationController animated:animated completion:completion];
}
else
{
// Suppose here the view controller has been presented modally. We dismiss it
[self dismissViewControllerAnimated:animated completion:completion];
}
}
- (void)popViewController:(UIViewController*)viewController navigationController:(UINavigationController*)navigationController animated:(BOOL)animated completion:(void (^)(void))completion
{
// We pop the view controller (except if it is the root view controller).
NSUInteger index = [navigationController.viewControllers indexOfObject:viewController];
if (index != NSNotFound)
{
if (index > 0)
{
UIViewController *previousViewController = [navigationController.viewControllers objectAtIndex:(index - 1)];
[navigationController popToViewController:previousViewController animated:animated];
if (completion)
{
completion();
}
}
else
{
// Check whether the navigation controller is embedded inside a navigation controller, to pop it.
if (navigationController.navigationController)
{
[self popViewController:navigationController navigationController:navigationController.navigationController animated:animated completion:completion];
}
else
{
// Remove the root view controller
navigationController.viewControllers = @[];
// Suppose here the navigation controller has been presented modally. We dismiss it
[navigationController dismissViewControllerAnimated:animated completion:completion];
}
}
}
}
- (void)destroy
{
// Remove properly keyboard view (remove related key observers)
self.keyboardView = nil;
// Remove observers
[[NSNotificationCenter defaultCenter] removeObserver:self];
mxSessionArray = nil;
childViewControllers = nil;
}
#pragma mark - Sessions handling
- (void)onMatrixSessionStateDidChange:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
if (index != NSNotFound)
{
if (mxSession.state == MXSessionStateClosed)
{
// Call here the dedicated method which may be overridden
[self removeMatrixSession:mxSession];
}
else
{
[self onMatrixSessionChange];
}
}
}
- (void)onMatrixSessionChange
{
// This method is called to refresh view controller appearance on session state change,
// It is called when the view will appear to update session array by removing closed sessions.
// Indeed 'kMXSessionStateDidChangeNotification' are observed only when the view controller is visible.
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
UINavigationController *mainNavigationController = self.mxk_mainNavigationController;
if (mxSessionArray.count)
{
// Check each session state.
UIColor *barTintColor = defaultBarTintColor;
BOOL allHomeserverNotReachable = YES;
BOOL isActivityInProgress = NO;
for (NSUInteger index = 0; index < mxSessionArray.count;)
{
MXSession *mxSession = mxSessionArray[index];
// Remove here closed sessions
if (mxSession.state == MXSessionStateClosed)
{
// Call here the dedicated method which may be overridden.
// This method will call again [onMatrixSessionChange] when session is removed.
[self removeMatrixSession:mxSession];
return;
}
else
{
if (mxSession.state == MXSessionStateHomeserverNotReachable)
{
barTintColor = [UIColor orangeColor];
}
else
{
allHomeserverNotReachable = NO;
isActivityInProgress = mxSession.shouldShowActivityIndicator;
}
index++;
}
}
// Check whether the navigation bar color depends on homeserver reachability.
if (enableBarTintColorStatusChange)
{
// The navigation bar tintColor reflects the matrix homeserver reachability status.
if (allHomeserverNotReachable)
{
self.navigationController.navigationBar.barTintColor = [UIColor redColor];
if (mainNavigationController)
{
mainNavigationController.navigationBar.barTintColor = [UIColor redColor];
}
}
else
{
self.navigationController.navigationBar.barTintColor = barTintColor;
if (mainNavigationController)
{
mainNavigationController.navigationBar.barTintColor = barTintColor;
}
}
}
// Run activity indicator if need
if (isActivityInProgress)
{
[self startActivityIndicator];
}
else
{
[self stopActivityIndicator];
}
}
else
{
// Hide potential activity indicator
[self stopActivityIndicator];
// Check whether the navigation bar color depends on homeserver reachability.
if (enableBarTintColorStatusChange)
{
// Restore default tintColor
self.navigationController.navigationBar.barTintColor = defaultBarTintColor;
if (mainNavigationController)
{
mainNavigationController.navigationBar.barTintColor = defaultBarTintColor;
}
}
}
}
#pragma mark - Activity indicator
- (void)stopActivityIndicator
{
// Check whether all conditions are satisfied before stopping loading wheel
BOOL isActivityInProgress = NO;
for (MXSession *mxSession in mxSessionArray)
{
if (mxSession.shouldShowActivityIndicator)
{
isActivityInProgress = YES;
}
}
if (!isActivityInProgress)
{
[super stopActivityIndicator];
}
}
#pragma mark - Shake handling
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (motion == UIEventSubtypeMotionShake && self.rageShakeManager)
{
[self.rageShakeManager startShaking:self];
}
}
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
[self motionEnded:motion withEvent:event];
}
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
if (self.rageShakeManager)
{
[self.rageShakeManager stopShaking:self];
}
}
- (BOOL)canBecomeFirstResponder
{
return (self.rageShakeManager != nil);
}
#pragma mark - Keyboard handling
- (void)onKeyboardShowAnimationComplete
{
// Do nothing here - `MXKViewController-inherited` instance must override this method.
}
- (void)setKeyboardView:(UIView *)keyboardView
{
// Remove previous keyboardView if any
if (_keyboardView)
{
// Restore UIKeyboardWillShowNotification observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
// Remove keyboard view observers
[_keyboardView removeObserver:self forKeyPath:NSStringFromSelector(@selector(frame))];
[_keyboardView removeObserver:self forKeyPath:NSStringFromSelector(@selector(center))];
_keyboardView = nil;
}
if (keyboardView)
{
// Add observers to detect keyboard drag down
[keyboardView addObserver:self forKeyPath:NSStringFromSelector(@selector(frame)) options:0 context:nil];
[keyboardView addObserver:self forKeyPath:NSStringFromSelector(@selector(center)) options:0 context:nil];
// Remove UIKeyboardWillShowNotification observer to ignore this notification until keyboard is dismissed.
// Note: UIKeyboardWillShowNotification may be triggered several times before keyboard is dismissed,
// because the keyboard height is updated (switch to a Chinese keyboard for example).
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
_keyboardView = keyboardView;
}
}
- (void)onKeyboardWillShow:(NSNotification *)notif
{
MXLogDebug(@"[MXKViewController] %@ onKeyboardWillShow", self.class);
// Get the keyboard size
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
CGRect endRect = rectVal.CGRectValue;
// IOS 8 triggers some unexpected keyboard events
if ((endRect.size.height == 0) || (endRect.size.width == 0))
{
return;
}
// Detect if an external keyboard is used
CGRect keyboard = [self.view convertRect:endRect fromView:self.view.window];
CGFloat height = self.view.frame.size.height;
BOOL hasExternalKeyboard = keyboard.size.height <= MXKViewControllerMaxExternalKeyboardHeight;
// Get the animation info
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
UIViewAnimationCurve animationCurve = curveValue.intValue;
// The duration is ignored but it is better to define it
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// Apply keyboard animation
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
if (!hasExternalKeyboard)
{
// Set the new virtual keyboard height by checking screen orientation
self.keyboardHeight = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
}
else
{
// The virtual keyboard is not shown on the screen but its toolbar is still displayed.
// Manage the height of this one
self.keyboardHeight = height - keyboard.origin.y;
}
} completion:^(BOOL finished)
{
[self onKeyboardShowAnimationComplete];
}];
}
- (void)onKeyboardWillHide:(NSNotification *)notif
{
MXLogDebug(@"[MXKViewController] %@ onKeyboardWillHide", self.class);
// Remove keyboard view
self.keyboardView = nil;
// Get the animation info
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
UIViewAnimationCurve animationCurve = curveValue.intValue;
// the duration is ignored but it is better to define it
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// Apply keyboard animation
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
self.keyboardHeight = 0;
} completion:^(BOOL finished)
{
}];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ((object == _keyboardView) && ([keyPath isEqualToString:NSStringFromSelector(@selector(frame))] || [keyPath isEqualToString:NSStringFromSelector(@selector(center))]))
{
// The keyboard view has been modified (Maybe the user drag it down), we update the input toolbar bottom constraint to adjust layout.
// Compute keyboard height (on IOS 8 and later, the screen size is oriented)
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
self.keyboardHeight = screenSize.height - _keyboardView.frame.origin.y;
}
}
@end
@@ -0,0 +1,50 @@
//
// Copyright 2021 The Matrix.org Foundation C.I.C
//
// 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.
//
#ifndef MXKViewControllerActivityHandling_h
#define MXKViewControllerActivityHandling_h
/**
`MXKViewControllerActivityHandling` defines a protocol to handle requirements for
all matrixKit view controllers and table view controllers.
It manages the following points:
- stop/start activity indicator according to the state of the associated matrix sessions.
*/
@protocol MXKViewControllerActivityHandling <NSObject>
/**
Activity indicator view.
By default this activity indicator is centered inside the view controller view. It automatically
starts if `shouldShowActivityIndicator `returns true for the session.
It is stopped on other states.
Set nil to disable activity indicator animation.
*/
@property (nonatomic) UIActivityIndicatorView *activityIndicator;
/**
Bring the activity indicator to the front and start it.
*/
- (void)startActivityIndicator;
/**
Stop the activity indicator if all conditions are satisfied.
*/
- (void)stopActivityIndicator;
@end
#endif /* MXKViewControllerActivityHandling_h */
@@ -0,0 +1,148 @@
/*
Copyright 2015 OpenMarket 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/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKResponderRageShaking.h"
#import "MXKViewControllerActivityHandling.h"
/**
`MXKViewControllerHandling` defines a protocol to handle requirements for
all matrixKit view controllers and table view controllers.
It manages the following points:
- matrix sessions handling, one or more sessions are supported.
- stop/start activity indicator according to the state of the associated matrix sessions.
- update view appearance on matrix session state change.
- support rage shake mechanism (depend on `rageShakeManager` property).
*/
@protocol MXKViewControllerHandling <MXKViewControllerActivityHandling>
/**
The default navigation bar tint color (nil by default).
*/
@property (nonatomic) UIColor *defaultBarTintColor;
/**
The color of the title in the navigation bar (nil by default).
*/
@property (nonatomic) UIColor *barTitleColor;
/**
Enable the change of the navigation bar tint color according to the matrix homeserver reachability status (YES by default).
Set NO this property to disable navigation tint color change.
*/
@property (nonatomic) BOOL enableBarTintColorStatusChange;
/**
List of associated matrix sessions (empty by default).
This property is used to update view appearance according to the session(s) state.
*/
@property (nonatomic, readonly) NSArray* mxSessions;
/**
The first associated matrix session is considered as the main session (nil by default).
*/
@property (nonatomic, readonly) MXSession *mainSession;
/**
Keep reference on the pushed and/or presented view controllers.
*/
@property (nonatomic, readonly) NSArray *childViewControllers;
/**
An object implementing the `MXKResponderRageShaking` protocol.
The view controller uses this object (if any) to report beginning and end of potential
rage shake when it is the first responder.
This property is nil by default.
*/
@property (nonatomic) id<MXKResponderRageShaking> rageShakeManager;
/**
Called during UIViewController initialization to set the default
properties values (see [initWithNibName:bundle:] and [initWithCoder:]).
You should not call this method directly.
Subclasses can override this method as needed to customize the initialization.
*/
- (void)finalizeInit;
/**
Add a matrix session in the list of associated sessions (see 'mxSessions' property).
The session is ignored if its state is 'MXSessionStateClosed'.
In other case, the session is stored, and an observer on 'kMXSessionStateDidChangeNotification' is added if it's not already done.
A session is automatically removed when its state returns to 'MXSessionStateClosed'.
@param mxSession a Matrix session.
*/
- (void)addMatrixSession:(MXSession*)mxSession;
/**
Remove a matrix session from the list of associated sessions (see 'mxSessions' property).
Remove the session. The 'kMXSessionStateDidChangeNotification' observer is removed if there is no more matrix session.
@param mxSession a Matrix session.
*/
- (void)removeMatrixSession:(MXSession*)mxSession;
/**
The method specified as notification selector during 'kMXSessionStateDidChangeNotification' observer creation.
By default this method consider ONLY notifications related to associated sessions (see 'mxSessions' property).
A session is automatically removed from the list when its state is 'MXSessionStateClosed'. Else [self onMatrixSessionChange] is called.
Override it to handle state change on associated sessions AND others.
*/
- (void)onMatrixSessionStateDidChange:(NSNotification *)notif;
/**
This method is called on the following matrix session changes:
- a new session is added.
- a session is removed.
- the state of an associated session changed (according to `MXSessionStateDidChangeNotification`).
This method is called to refresh the display when the view controller will appear too.
By default view controller appearance is updated according to the state of associated sessions:
- starts activity indicator as soon as when `shouldShowActivityIndicator `returns true for the session.
- switches in red the navigation bar tintColor when all sessions are in `MXSessionStateHomeserverNotReachable` state.
- switches in orange the navigation bar tintColor when at least one session is in `MXSessionStateHomeserverNotReachable` state.
Override it to customize view appearance according to associated session(s).
*/
- (void)onMatrixSessionChange;
/**
Pop or dismiss the view controller. It depends if the view controller is embedded inside a navigation controller or not.
@param animated YES to animate the transition.
@param completion the block to execute after the view controller is popped or dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
*/
- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion;
/**
Dispose of any resources, and remove event observers.
*/
- (void)destroy;
@end
@@ -0,0 +1,69 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2018 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 "MXKViewController.h"
#import <WebKit/WebKit.h>
/**
'MXKWebViewViewController' instance is used to display a webview.
*/
@interface MXKWebViewViewController : MXKViewController <WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler>
{
@protected
/**
The back button displayed as the right bar button item.
*/
UIBarButtonItem *backButton;
@public
/**
The content of this screen is fully displayed by this webview
*/
WKWebView *webView;
}
/**
Init 'MXKWebViewViewController' instance with a web content url.
@param URL the url to open
*/
- (id)initWithURL:(NSString*)URL;
/**
Init 'MXKWebViewViewController' instance with a local HTML file path.
@param localHTMLFile The path of the local HTML file.
*/
- (id)initWithLocalHTMLFile:(NSString*)localHTMLFile;
/**
Route javascript logs to NSLog.
*/
- (void)enableDebug;
/**
Define the web content url to open
Dont use this property to load local HTML files, instead use 'localHTMLFile'.
*/
@property (nonatomic) NSString *URL;
/**
Define the local HTML file path to load
*/
@property (nonatomic) NSString *localHTMLFile;
@end
@@ -0,0 +1,349 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
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 "MXKWebViewViewController.h"
#import "NSBundle+MatrixKit.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import "MXKSwiftHeader.h"
NSString *const kMXKWebViewViewControllerPostMessageJSLog = @"jsLog";
// Override console.* logs methods to send WebKit postMessage events to native code.
// Note: this code has a minimal support of multiple parameters in console.log()
NSString *const kMXKWebViewViewControllerJavaScriptEnableLog =
@"console.debug = console.log; console.info = console.log; console.warn = console.log; console.error = console.log;" \
@"console.log = function() {" \
@" var msg = arguments[0];" \
@" for (var i = 1; i < arguments.length; i++) {" \
@" msg += ' ' + arguments[i];" \
@" }" \
@" window.webkit.messageHandlers.%@.postMessage(msg);" \
@"};";
@interface MXKWebViewViewController ()
{
BOOL enableDebug;
// Right buttons bar state before loading the webview
NSArray<UIBarButtonItem *> *originalRightBarButtonItems;
}
@end
@implementation MXKWebViewViewController
- (instancetype)init
{
self = [super init];
if (self)
{
enableDebug = NO;
}
return self;
}
- (id)initWithURL:(NSString*)URL
{
self = [self init];
if (self)
{
_URL = URL;
}
return self;
}
- (id)initWithLocalHTMLFile:(NSString*)localHTMLFile
{
self = [self init];
if (self)
{
_localHTMLFile = localHTMLFile;
}
return self;
}
- (void)enableDebug
{
// We can only call addScriptMessageHandler on a given message only once
if (enableDebug)
{
return;
}
enableDebug = YES;
// Redirect all console.* logging methods into a WebKit postMessage event with name "jsLog"
[webView.configuration.userContentController addScriptMessageHandler:self name:kMXKWebViewViewControllerPostMessageJSLog];
NSString *javaScriptString = [NSString stringWithFormat:kMXKWebViewViewControllerJavaScriptEnableLog, kMXKWebViewViewControllerPostMessageJSLog];
[webView evaluateJavaScript:javaScriptString completionHandler:nil];
}
- (void)finalizeInit
{
[super finalizeInit];
}
- (void)viewDidLoad
{
[super viewDidLoad];
originalRightBarButtonItems = self.navigationItem.rightBarButtonItems;
// Init the webview
webView = [[WKWebView alloc] initWithFrame:self.view.frame];
webView.backgroundColor= [UIColor whiteColor];
webView.navigationDelegate = self;
webView.UIDelegate = self;
[webView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:webView];
// Force webview in full width (to handle auto-layout in case of screen rotation)
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:webView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:webView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0];
// Force webview in full height
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:webView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.topLayoutGuide
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:0];
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:webView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.bottomLayoutGuide
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
[NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
backButton = [[UIBarButtonItem alloc] initWithTitle:[MatrixKitL10n back] style:UIBarButtonItemStylePlain target:self action:@selector(goBack)];
if (_URL.length)
{
self.URL = _URL;
}
else if (_localHTMLFile.length)
{
self.localHTMLFile = _localHTMLFile;
}
}
- (void)destroy
{
if (webView)
{
webView.navigationDelegate = nil;
[webView stopLoading];
[webView removeFromSuperview];
webView = nil;
}
backButton = nil;
_URL = nil;
_localHTMLFile = nil;
[super destroy];
}
- (void)dealloc
{
[self destroy];
}
- (void)setURL:(NSString *)URL
{
[webView stopLoading];
_URL = URL;
_localHTMLFile = nil;
if (URL.length)
{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]];
[webView loadRequest:request];
}
}
- (void)setLocalHTMLFile:(NSString *)localHTMLFile
{
[webView stopLoading];
_localHTMLFile = localHTMLFile;
_URL = nil;
if (localHTMLFile.length)
{
NSString* htmlString = [NSString stringWithContentsOfFile:localHTMLFile encoding:NSUTF8StringEncoding error:nil];
[webView loadHTMLString:htmlString baseURL:nil];
}
}
- (void)goBack
{
if (webView.canGoBack)
{
[webView goBack];
}
else if (_localHTMLFile.length)
{
// Reload local html file
self.localHTMLFile = _localHTMLFile;
}
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
// Handle back button visibility here
BOOL canGoBack = webView.canGoBack;
if (_localHTMLFile.length && !canGoBack)
{
// Check whether the current content is not the local html file
canGoBack = (![webView.URL.absoluteString isEqualToString:@"about:blank"]);
}
if (canGoBack)
{
self.navigationItem.rightBarButtonItem = backButton;
}
else
{
// Reset the original state
self.navigationItem.rightBarButtonItems = originalRightBarButtonItems;
}
}
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler
{
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
// We handle here only the server trust authentication.
// We fallback to the default logic for other cases.
if (protectionSpace.authenticationMethod != NSURLAuthenticationMethodServerTrust || !protectionSpace.serverTrust)
{
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}
SecTrustRef serverTrust = [protectionSpace serverTrust];
// Check first whether there are some pinned certificates (certificate included in the bundle).
NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"cer" inDirectory:@"."];
if (paths.count)
{
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSString *path in paths)
{
NSData *certificateData = [NSData dataWithContentsOfFile:path];
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// Only use these certificates to pin against, and do not trust the built-in anchor certificates.
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
}
else
{
// Check whether some certificates have been trusted by the user (self-signed certificates support).
NSSet<NSData *> *certificates = [MXAllowedCertificates sharedInstance].certificates;
if (certificates.count)
{
NSMutableArray *allowedCertificates = [NSMutableArray array];
for (NSData *certificateData in certificates)
{
[allowedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// Add all the allowed certificates to the chain of trust
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)allowedCertificates);
// Reenable trusting the built-in anchor certificates in addition to those passed in via the SecTrustSetAnchorCertificates API.
SecTrustSetAnchorCertificatesOnly(serverTrust, false);
}
}
// Re-evaluate the trust policy
SecTrustResultType secresult = kSecTrustResultInvalid;
if (SecTrustEvaluate(serverTrust, &secresult) != errSecSuccess)
{
// Reject the server auth if an error occurs
completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil);
}
else
{
switch (secresult)
{
case kSecTrustResultUnspecified: // The OS trusts this certificate implicitly.
case kSecTrustResultProceed: // The user explicitly told the OS to trust it.
{
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
break;
}
default:
{
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
break;
}
}
}
}
#pragma mark - WKUIDelegate
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(nonnull WKWebViewConfiguration *)configuration forNavigationAction:(nonnull WKNavigationAction *)navigationAction windowFeatures:(nonnull WKWindowFeatures *)windowFeatures
{
// Make sure we open links with `target="_blank"` within this webview
if (!navigationAction.targetFrame.isMainFrame)
{
[webView loadRequest:navigationAction.request];
}
return nil;
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:kMXKWebViewViewControllerPostMessageJSLog])
{
MXLogDebug(@"-- JavaScript: %@", message.body);
}
}
@end