2021-12-03 10:47:24 +00:00
/ *
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations 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 .
* /
# define MXKROOMVIEWCONTROLLER_DEFAULT _TYPING _TIMEOUT _SEC 10
# define MXKROOMVIEWCONTROLLER_MESSAGES _TABLE _MINIMUM _HEIGHT 50
# import "MXKRoomViewController.h"
# import < MediaPlayer / MediaPlayer . h >
# import "MXKRoomBubbleTableViewCell.h"
# import "MXKSearchTableViewCell.h"
# import "MXKImageView.h"
# import "MXKRoomDataSourceManager.h"
# import "MXKRoomInputToolbarViewWithSimpleTextView.h"
# import "MXKConstants.h"
# import "MXKRoomBubbleCellData.h"
# import "MXKEncryptionKeysImportView.h"
# import "NSBundle+MatrixKit.h"
# import "MXKSlashCommands.h"
# import "MXKSwiftHeader.h"
# import "MXKPreviewViewController.h"
@ interface MXKRoomViewController ( ) < MXKPreviewViewControllerDelegate >
{
/ * *
YES once the view has appeared
* /
BOOL hasAppearedOnce ;
/ * *
YES if scrolling to bottom is in progress
* /
BOOL isScrollingToBottom ;
/ * *
Date of the last observed typing
* /
NSDate * lastTypingDate ;
/ * *
Local typing timout
* /
NSTimer * typingTimer ;
/ * *
YES when pagination is in progress .
* /
BOOL isPaginationInProgress ;
/ * *
The back pagination spinner view .
* /
UIView * backPaginationActivityView ;
/ * *
Store the height of the first bubble before back pagination .
* /
CGFloat backPaginationSavedFirstBubbleHeight ;
/ * *
Potential request in progress to join the selected room
* /
MXHTTPOperation * joinRoomRequest ;
/ * *
Text selection
* /
NSString * selectedText ;
/ * *
The class used to instantiate attachments viewer for image and video . .
* /
Class attachmentsViewerClass ;
/ * *
The class used to display event details .
* /
Class customEventDetailsViewClass ;
/ * *
The reconnection animated view .
* /
UIView * reconnectingView ;
/ * *
The view to import e2e keys .
* /
MXKEncryptionKeysImportView * importView ;
/ * *
The latest server sync date
* /
NSDate * latestServerSync ;
/ * *
The restart the event connnection
* /
BOOL restartConnection ;
}
/ * *
The eventId of the Attachment that was used to open the Attachments ViewController
* /
@ property ( nonatomic ) NSString * openedAttachmentEventId ;
/ * *
The eventId of the Attachment from which the Attachments ViewController was closed
* /
@ property ( nonatomic ) NSString * closedAttachmentEventId ;
@ property ( nonatomic ) UIImageView * openedAttachmentImageView ;
/ * *
Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room .
* /
@ property ( nonatomic , weak ) id mxSessionWillLeaveRoomNotificationObserver ;
/ * *
Observe UIApplicationDidBecomeActiveNotification to refresh bubbles when app leaves the background state .
* /
@ property ( nonatomic , weak ) id uiApplicationDidBecomeActiveNotificationObserver ;
/ * *
Observe UIMenuControllerDidHideMenuNotification to cancel text selection
* /
@ property ( nonatomic , weak ) id uiMenuControllerDidHideMenuNotificationObserver ;
/ * *
The attachments viewer for image and video .
* /
@ property ( nonatomic , weak ) MXKAttachmentsViewController * attachmentsViewer ;
@ end
@ implementation MXKRoomViewController
@ synthesize roomDataSource , titleView , inputToolbarView , activitiesView ;
# pragma mark - Class methods
+ ( UINib * ) nib
{
return [ UINib nibWithNibName : NSStringFromClass ( [ MXKRoomViewController class ] )
bundle : [ NSBundle bundleForClass : [ MXKRoomViewController class ] ] ] ;
}
+ ( instancetype ) roomViewController
{
return [ [ [ self class ] alloc ] initWithNibName : NSStringFromClass ( [ MXKRoomViewController class ] )
bundle : [ NSBundle bundleForClass : [ MXKRoomViewController class ] ] ] ;
}
# pragma mark -
- ( void ) finalizeInit
{
[ super finalizeInit ] ;
// Scroll to bottom the bubble history at first display
shouldScrollToBottomOnTableRefresh = YES ;
// Default pagination settings
_paginationThreshold = 300 ;
_paginationLimit = 30 ;
// Save progress text input by default
_saveProgressTextInput = YES ;
// Enable auto join option by default
_autoJoinInvitedRoom = YES ;
// Do not take ownership of room data source by default
_hasRoomDataSourceOwnership = NO ;
// Turn on the automatic events acknowledgement .
_eventsAcknowledgementEnabled = YES ;
// Do not update the read marker by default .
_updateRoomReadMarker = NO ;
// Center the table content on the initial event top by default .
_centerBubblesTableViewContentOnTheInitialEventBottom = NO ;
// Scroll to the bottom when a keyboard is presented
_scrollHistoryToTheBottomOnKeyboardPresentation = YES ;
// Keep visible the status bar by default .
isStatusBarHidden = NO ;
// By default actions button is shown in document preview
_allowActionsInDocumentPreview = YES ;
// By default the duration of the composer resizing is 0.3 s
_resizeComposerAnimationDuration = 0.3 ;
}
- ( void ) viewDidLoad
{
[ super viewDidLoad ] ;
// Check whether the view controller has been pushed via storyboard
if ( ! _bubblesTableView )
{
// Instantiate view controller objects
[ [ [ self class ] nib ] instantiateWithOwner : self options : nil ] ;
}
// Adjust bottom constraint of the input toolbar container in order to take into account potential tabBar
_roomInputToolbarContainerBottomConstraint . active = NO ;
_roomInputToolbarContainerBottomConstraint = [ NSLayoutConstraint constraintWithItem : self . bottomLayoutGuide
attribute : NSLayoutAttributeTop
relatedBy : NSLayoutRelationEqual
toItem : self . roomInputToolbarContainer
attribute : NSLayoutAttributeBottom
multiplier : 1.0 f
constant : 0.0 f ] ;
_roomInputToolbarContainerBottomConstraint . active = YES ;
[ self . view setNeedsUpdateConstraints ] ;
// Hide bubbles table by default in order to hide initial scrolling to the bottom
_bubblesTableView . hidden = YES ;
// Ensure that the titleView will be scaled when it will be required
// during a screen rotation for example .
_roomTitleViewContainer . autoresizingMask = UIViewAutoresizingFlexibleWidth ;
// Set default input toolbar view
[ self setRoomInputToolbarViewClass : MXKRoomInputToolbarViewWithSimpleTextView . class ] ;
// set the default extra
[ self setRoomActivitiesViewClass : MXKRoomActivitiesView . class ] ;
// Finalize table view configuration
[ self configureBubblesTableView ] ;
// Observe UIApplicationDidBecomeActiveNotification to refresh bubbles when app leaves the background state .
MXWeakify ( self ) ;
_uiApplicationDidBecomeActiveNotificationObserver = [ [ NSNotificationCenter defaultCenter ] addObserverForName : UIApplicationDidBecomeActiveNotification object : nil queue : [ NSOperationQueue mainQueue ] usingBlock : ^ ( NSNotification * notif ) {
MXStrongifyAndReturnIfNil ( self ) ;
if ( self -> roomDataSource . state = = MXKDataSourceStateReady && [ self -> roomDataSource tableView : self -> _bubblesTableView numberOfRowsInSection : 0 ] )
{
// Reload the full table
self . bubbleTableViewDisplayInTransition = YES ;
[ self reloadBubblesTable : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
}
} ] ;
if ( [ MXKAppSettings standardAppSettings ] . outboundGroupSessionKeyPreSharingStrategy = = MXKKeyPreSharingWhenEnteringRoom )
{
[ self shareEncryptionKeys ] ;
}
}
- ( BOOL ) prefersStatusBarHidden
{
// Return the current status bar visibility .
// Caution : Enable [ UIViewController prefersStatusBarHidden ] use at application level
// by turning on UIViewControllerBasedStatusBarAppearance in Info . plist .
return isStatusBarHidden ;
}
- ( void ) viewWillAppear : ( BOOL ) animated
{
[ super viewWillAppear : animated ] ;
// Observe server sync process at room data source level too
[ [ NSNotificationCenter defaultCenter ] addObserver : self selector : @ selector ( onMatrixSessionChange ) name : kMXKRoomDataSourceSyncStatusChanged object : nil ] ;
// Observe timeline failure
[ [ NSNotificationCenter defaultCenter ] addObserver : self selector : @ selector ( onTimelineError : ) name : kMXKRoomDataSourceTimelineError object : nil ] ;
// Observe the server sync
[ [ NSNotificationCenter defaultCenter ] addObserver : self selector : @ selector ( onSyncNotification ) name : kMXSessionDidSyncNotification object : nil ] ;
// Be sure to display the activity indicator during back pagination
if ( isPaginationInProgress )
{
[ self startActivityIndicator ] ;
}
// Finalize view controller appearance
[ self updateViewControllerAppearanceOnRoomDataSourceState ] ;
// no need to reload the tableview at this stage
// IOS is going to load it after calling this method
// so give a breath to scroll to the bottom if required
if ( shouldScrollToBottomOnTableRefresh )
{
self . bubbleTableViewDisplayInTransition = YES ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self scrollBubblesTableViewToBottomAnimated : NO ] ;
// Show bubbles table after initial scrolling to the bottom
// Patch : We need to delay this operation to wait for the end of scrolling .
dispatch_after ( dispatch_walltime ( DISPATCH_TIME _NOW , 0.3 * NSEC_PER _SEC ) , dispatch_get _main _queue ( ) , ^ {
self -> _bubblesTableView . hidden = NO ;
self . bubbleTableViewDisplayInTransition = NO ;
} ) ;
} ) ;
}
else
{
_bubblesTableView . hidden = NO ;
}
}
- ( void ) viewDidAppear : ( BOOL ) animated
{
[ super viewDidAppear : animated ] ;
// Remove the rounded bottom unsafe area of the iPhone X
_bubblesTableViewBottomConstraint . constant + = self . view . safeAreaInsets . bottom ;
if ( _saveProgressTextInput && roomDataSource )
{
// Retrieve the potential message partially typed during last room display .
// Note : We have to wait for viewDidAppear before updating growingTextView ( viewWillAppear is too early )
inputToolbarView . textMessage = roomDataSource . partialTextMessage ;
}
if ( ! hasAppearedOnce )
{
hasAppearedOnce = YES ;
}
// Mark all messages as read when the room is displayed
[ self . roomDataSource . room . summary markAllAsReadLocally ] ;
}
- ( void ) viewWillDisappear : ( BOOL ) animated
{
[ super viewWillDisappear : animated ] ;
[ [ NSNotificationCenter defaultCenter ] removeObserver : self name : kMXKRoomDataSourceSyncStatusChanged object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] removeObserver : self name : kMXKRoomDataSourceTimelineError object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] removeObserver : self name : kMXSessionDidSyncNotification object : nil ] ;
[ self removeReconnectingView ] ;
}
- ( void ) dealloc
{
if ( _mxSessionWillLeaveRoomNotificationObserver )
{
[ [ NSNotificationCenter defaultCenter ] removeObserver : _mxSessionWillLeaveRoomNotificationObserver ] ;
}
if ( _uiApplicationDidBecomeActiveNotificationObserver )
{
[ [ NSNotificationCenter defaultCenter ] removeObserver : _uiApplicationDidBecomeActiveNotificationObserver ] ;
}
if ( _uiMenuControllerDidHideMenuNotificationObserver )
{
[ [ NSNotificationCenter defaultCenter ] removeObserver : _uiMenuControllerDidHideMenuNotificationObserver ] ;
}
[ self destroy ] ;
}
- ( void ) didReceiveMemoryWarning
{
[ super didReceiveMemoryWarning ] ;
// Dispose of any resources that can be recreated .
}
- ( void ) viewWillTransitionToSize : ( CGSize ) size withTransitionCoordinator : ( id < UIViewControllerTransitionCoordinator > ) coordinator
{
isSizeTransitionInProgress = YES ;
shouldScrollToBottomOnTableRefresh = [ self isBubblesTableScrollViewAtTheBottom ] ;
[ super viewWillTransitionToSize : size withTransitionCoordinator : coordinator ] ;
dispatch_after ( dispatch_time ( DISPATCH_TIME _NOW , ( int64_t ) ( coordinator . transitionDuration * NSEC_PER _SEC ) ) , dispatch_get _main _queue ( ) , ^ {
if ( ! self . keyboardView )
{
[ self updateMessageTextViewFrame ] ;
}
// Force full table refresh to take into account cell width change .
self . bubbleTableViewDisplayInTransition = YES ;
[ self reloadBubblesTable : YES invalidateBubblesCellDataCache : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
self -> shouldScrollToBottomOnTableRefresh = NO ;
self -> isSizeTransitionInProgress = NO ;
} ) ;
}
// The 2 following methods are deprecated since iOS 8
- ( void ) willRotateToInterfaceOrientation : ( UIInterfaceOrientation ) toInterfaceOrientation duration : ( NSTimeInterval ) duration
{
isSizeTransitionInProgress = YES ;
shouldScrollToBottomOnTableRefresh = [ self isBubblesTableScrollViewAtTheBottom ] ;
[ super willRotateToInterfaceOrientation : toInterfaceOrientation duration : duration ] ;
}
- ( void ) didRotateFromInterfaceOrientation : ( UIInterfaceOrientation ) fromInterfaceOrientation
{
[ super didRotateFromInterfaceOrientation : fromInterfaceOrientation ] ;
if ( ! self . keyboardView )
{
[ self updateMessageTextViewFrame ] ;
}
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
// Force full table refresh to take into account cell width change .
self . bubbleTableViewDisplayInTransition = YES ;
[ self reloadBubblesTable : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
self -> shouldScrollToBottomOnTableRefresh = NO ;
self -> isSizeTransitionInProgress = NO ;
} ) ;
}
- ( void ) viewDidLayoutSubviews
{
[ super viewDidLayoutSubviews ] ;
CGFloat bubblesTableViewBottomConst = self . roomInputToolbarContainerBottomConstraint . constant + self . roomInputToolbarContainerHeightConstraint . constant + self . roomActivitiesContainerHeightConstraint . constant ;
if ( self . bubblesTableViewBottomConstraint . constant ! = bubblesTableViewBottomConst )
{
self . bubblesTableViewBottomConstraint . constant = bubblesTableViewBottomConst ;
}
}
# pragma mark - Override MXKViewController
- ( void ) onMatrixSessionChange
{
[ super onMatrixSessionChange ] ;
// Check dataSource state
if ( self . roomDataSource && ( self . roomDataSource . state = = MXKDataSourceStatePreparing || self . roomDataSource . serverSyncEventCount ) )
{
// dataSource is not ready , keep running the loading wheel
[ self startActivityIndicator ] ;
}
}
- ( void ) onKeyboardShowAnimationComplete
{
// Check first if the first responder belongs to title view
UIView * keyboardView = titleView . inputAccessoryView . superview ;
if ( ! keyboardView )
{
// Check whether the first responder is the input tool bar text composer
keyboardView = inputToolbarView . inputAccessoryView . superview ;
}
// Report the keyboard view in order to track keyboard frame changes
self . keyboardView = keyboardView ;
}
- ( void ) setKeyboardHeight : ( CGFloat ) keyboardHeight
{
// Deduce the bottom constraint for the input toolbar view ( Don ' t forget the potential tabBar )
CGFloat inputToolbarViewBottomConst = keyboardHeight - self . bottomLayoutGuide . length ;
// Check whether the keyboard is over the tabBar
if ( inputToolbarViewBottomConst < 0 )
{
inputToolbarViewBottomConst = 0 ;
}
// Update constraints
_roomInputToolbarContainerBottomConstraint . constant = inputToolbarViewBottomConst ;
_bubblesTableViewBottomConstraint . constant = inputToolbarViewBottomConst + _roomInputToolbarContainerHeightConstraint . constant + _roomActivitiesContainerHeightConstraint . constant ;
// Remove the rounded bottom unsafe area of the iPhone X
_bubblesTableViewBottomConstraint . constant + = self . view . safeAreaInsets . bottom ;
// Invalidate the current layout to take into account the new constraints in the next update cycle .
[ self . view setNeedsLayout ] ;
// Compute the visible area ( tableview + toolbar ) at the end of animation
CGFloat visibleArea = self . view . frame . size . height - _bubblesTableView . adjustedContentInset . top - keyboardHeight ;
// Deduce max height of the message text input by considering the minimum height of the table view .
inputToolbarView . maxHeight = visibleArea - MXKROOMVIEWCONTROLLER_MESSAGES _TABLE _MINIMUM _HEIGHT ;
// Check conditions before scrolling the tableview content when a new keyboard is presented .
if ( ( _scrollHistoryToTheBottomOnKeyboardPresentation || [ self isBubblesTableScrollViewAtTheBottom ] ) && ! super . keyboardHeight && keyboardHeight && ! currentAlert )
{
self . bubbleTableViewDisplayInTransition = YES ;
// Force here the layout update to scroll correctly the table content .
[ self . view layoutIfNeeded ] ;
[ self scrollBubblesTableViewToBottomAnimated : NO ] ;
self . bubbleTableViewDisplayInTransition = NO ;
}
else
{
[ self updateCurrentEventIdAtTableBottom : NO ] ;
}
super . keyboardHeight = keyboardHeight ;
}
- ( void ) destroy
{
if ( documentInteractionController )
{
[ documentInteractionController dismissPreviewAnimated : NO ] ;
[ documentInteractionController dismissMenuAnimated : NO ] ;
documentInteractionController = nil ;
}
if ( currentSharedAttachment )
{
[ currentSharedAttachment onShareEnded ] ;
currentSharedAttachment = nil ;
}
[ self dismissTemporarySubViews ] ;
_bubblesTableView . dataSource = nil ;
_bubblesTableView . delegate = nil ;
_bubblesTableView = nil ;
if ( roomDataSource . delegate = = self )
{
roomDataSource . delegate = nil ;
}
if ( _hasRoomDataSourceOwnership )
{
// Release the room data source
[ roomDataSource destroy ] ;
}
roomDataSource = nil ;
if ( titleView )
{
[ titleView removeFromSuperview ] ;
[ titleView destroy ] ;
titleView = nil ;
}
if ( inputToolbarView )
{
[ inputToolbarView removeFromSuperview ] ;
[ inputToolbarView destroy ] ;
inputToolbarView = nil ;
}
if ( activitiesView )
{
[ activitiesView removeFromSuperview ] ;
[ activitiesView destroy ] ;
activitiesView = nil ;
}
[ typingTimer invalidate ] ;
typingTimer = nil ;
if ( joinRoomRequest )
{
[ joinRoomRequest cancel ] ;
joinRoomRequest = nil ;
}
[ super destroy ] ;
}
# pragma mark -
- ( void ) configureBubblesTableView
{
// Set up table delegates
_bubblesTableView . delegate = self ;
_bubblesTableView . dataSource = roomDataSource ; // Note : data source may be nil here , it will be set during [ displayRoom : ] call .
// Observe kMXSessionWillLeaveRoomNotification to be notified if the user leaves the current room .
MXWeakify ( self ) ;
_mxSessionWillLeaveRoomNotificationObserver = [ [ NSNotificationCenter defaultCenter ] addObserverForName : kMXSessionWillLeaveRoomNotification object : nil queue : [ NSOperationQueue mainQueue ] usingBlock : ^ ( NSNotification * notif ) {
MXStrongifyAndReturnIfNil ( self ) ;
// Check whether the user will leave the current room
if ( notif . object = = self . mainSession )
{
NSString * roomId = notif . userInfo [ kMXSessionNotificationRoomIdKey ] ;
if ( roomId && [ roomId isEqualToString : self -> roomDataSource . roomId ] )
{
// Update view controller appearance
[ self leaveRoomOnEvent : notif . userInfo [ kMXSessionNotificationEventKey ] ] ;
}
}
} ] ;
}
- ( void ) updateMessageTextViewFrame
{
if ( ! self . keyboardView )
{
// Compute the visible area ( tableview + toolbar )
CGFloat visibleArea = self . view . frame . size . height - _bubblesTableView . adjustedContentInset . top ;
// Deduce max height of the message text input by considering the minimum height of the table view .
inputToolbarView . maxHeight = visibleArea - MXKROOMVIEWCONTROLLER_MESSAGES _TABLE _MINIMUM _HEIGHT ;
}
}
- ( CGFloat ) tableViewSafeAreaWidth
{
CGFloat safeAreaInsetsWidth ;
// Take safe area into account
safeAreaInsetsWidth = self . bubblesTableView . safeAreaInsets . left + self . bubblesTableView . safeAreaInsets . right ;
return self . bubblesTableView . frame . size . width - safeAreaInsetsWidth ;
}
# pragma mark - Public API
- ( void ) displayRoom : ( MXKRoomDataSource * ) dataSource
{
if ( roomDataSource )
{
if ( self . hasRoomDataSourceOwnership )
{
// Release the room data source
[ roomDataSource destroy ] ;
}
else if ( roomDataSource . delegate = = self )
{
roomDataSource . delegate = nil ;
}
roomDataSource = nil ;
[ self removeMatrixSession : self . mainSession ] ;
}
// Reset the current event id
currentEventIdAtTableBottom = nil ;
if ( dataSource )
{
if ( ! dataSource . isLive || dataSource . isPeeking )
{
// Remove the input toolbar if the displayed timeline is not a live one or in case of peeking .
// We do not let the user type message in this case .
[ self setRoomInputToolbarViewClass : nil ] ;
}
roomDataSource = dataSource ;
roomDataSource . delegate = self ;
roomDataSource . paginationLimitAroundInitialEvent = _paginationLimit ;
// Report the matrix session at view controller level to update UI according to session state
[ self addMatrixSession : roomDataSource . mxSession ] ;
if ( _bubblesTableView )
{
[ self dismissTemporarySubViews ] ;
// Set up table data source
_bubblesTableView . dataSource = roomDataSource ;
}
// When ready , do the initial back pagination
if ( roomDataSource . state = = MXKDataSourceStateReady )
{
[ self onRoomDataSourceReady ] ;
}
}
[ self updateViewControllerAppearanceOnRoomDataSourceState ] ;
}
- ( void ) onRoomDataSourceReady
{
// If the user is only invited , auto - join the room if this option is enabled
if ( roomDataSource . room . summary . membership = = MXMembershipInvite )
{
if ( _autoJoinInvitedRoom )
{
[ self joinRoom : nil ] ;
}
}
else
{
[ self triggerInitialBackPagination ] ;
}
}
- ( void ) updateViewControllerAppearanceOnRoomDataSourceState
{
// Update UI by considering dataSource state
if ( roomDataSource && roomDataSource . state = = MXKDataSourceStateReady )
{
[ self stopActivityIndicator ] ;
if ( titleView )
{
titleView . mxRoom = roomDataSource . room ;
titleView . editable = YES ;
titleView . hidden = NO ;
}
else
{
// set default title
self . navigationItem . title = roomDataSource . room . summary . displayname ;
}
// Show input tool bar
inputToolbarView . hidden = NO ;
}
else
{
// Update the title except if the room has just been left
if ( ! _leftRoomReasonLabel )
{
if ( roomDataSource && roomDataSource . state = = MXKDataSourceStatePreparing )
{
if ( titleView )
{
titleView . mxRoom = roomDataSource . room ;
titleView . hidden = ( ! titleView . mxRoom ) ;
}
else
{
self . navigationItem . title = roomDataSource . room . summary . displayname ;
}
}
else
{
if ( titleView )
{
titleView . mxRoom = nil ;
titleView . hidden = NO ;
}
else
{
self . navigationItem . title = nil ;
}
}
}
titleView . editable = NO ;
// Hide input tool bar
inputToolbarView . hidden = YES ;
}
// Finalize room title refresh
[ titleView refreshDisplay ] ;
if ( activitiesView )
{
// Hide by default the activity view when no room is displayed
activitiesView . hidden = ( roomDataSource = = nil ) ;
}
}
- ( void ) onTimelineError : ( NSNotification * ) notif
{
if ( notif . object = = roomDataSource )
{
[ self stopActivityIndicator ] ;
// Compute the message to display to the end user
NSString * errorTitle ;
NSString * errorMessage ;
NSError * error = notif . userInfo [ kMXKRoomDataSourceTimelineErrorErrorKey ] ;
if ( [ MXError isMXError : error ] )
{
MXError * mxError = [ [ MXError alloc ] initWithNSError : error ] ;
if ( [ mxError . errcode isEqualToString : kMXErrCodeStringNotFound ] )
{
errorTitle = [ MatrixKitL10n roomErrorTimelineEventNotFoundTitle ] ;
errorMessage = [ MatrixKitL10n roomErrorTimelineEventNotFound ] ;
}
else
{
errorTitle = [ MatrixKitL10n roomErrorCannotLoadTimeline ] ;
errorMessage = mxError . error ;
}
}
else
{
errorTitle = [ MatrixKitL10n roomErrorCannotLoadTimeline ] ;
}
// And show it
[ currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
__weak typeof ( self ) weakSelf = self ;
UIAlertController * errorAlert = [ UIAlertController alertControllerWithTitle : errorTitle
message : errorMessage
preferredStyle : UIAlertControllerStyleAlert ] ;
[ errorAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n ok ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
} ] ] ;
[ self presentViewController : errorAlert animated : YES completion : nil ] ;
currentAlert = errorAlert ;
}
}
- ( void ) joinRoom : ( void ( ^ ) ( MXKRoomViewControllerJoinRoomResult result ) ) completion
{
if ( joinRoomRequest ! = nil )
{
if ( completion )
{
completion ( MXKRoomViewControllerJoinRoomResultFailureJoinInProgress ) ;
}
return ;
}
[ self startActivityIndicator ] ;
joinRoomRequest = [ roomDataSource . room join : ^ {
self -> joinRoomRequest = nil ;
[ self stopActivityIndicator ] ;
[ self triggerInitialBackPagination ] ;
if ( completion )
{
completion ( MXKRoomViewControllerJoinRoomResultSuccess ) ;
}
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Failed to join room (%@)" , self -> roomDataSource . room . summary . displayname ) ;
[ self processRoomJoinFailureWithError : error completion : completion ] ;
} ] ;
}
- ( void ) joinRoomWithRoomIdOrAlias : ( NSString * ) roomIdOrAlias
viaServers : ( NSArray < NSString * > * ) viaServers
andSignUrl : ( NSString * ) signUrl
completion : ( void ( ^ ) ( MXKRoomViewControllerJoinRoomResult result ) ) completion
{
if ( joinRoomRequest ! = nil )
{
if ( completion )
{
completion ( MXKRoomViewControllerJoinRoomResultFailureJoinInProgress ) ;
}
return ;
}
[ self startActivityIndicator ] ;
void ( ^ success ) ( MXRoom * room ) = ^ ( MXRoom * room ) {
self -> joinRoomRequest = nil ;
[ self stopActivityIndicator ] ;
MXWeakify ( self ) ;
// The room is now part of the user ' s room
MXKRoomDataSourceManager * roomDataSourceManager = [ MXKRoomDataSourceManager sharedManagerForMatrixSession : self . mainSession ] ;
[ roomDataSourceManager roomDataSourceForRoom : room . roomId create : YES onComplete : ^ ( MXKRoomDataSource * newRoomDataSource ) {
MXStrongifyAndReturnIfNil ( self ) ;
// And can be displayed
[ self displayRoom : newRoomDataSource ] ;
if ( completion )
{
completion ( MXKRoomViewControllerJoinRoomResultSuccess ) ;
}
} ] ;
} ;
void ( ^ failure ) ( NSError * error ) = ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Failed to join room (%@)" , roomIdOrAlias ) ;
[ self processRoomJoinFailureWithError : error completion : completion ] ;
} ;
// Does the join need to be validated before ?
if ( signUrl )
{
joinRoomRequest = [ self . mainSession joinRoom : roomIdOrAlias viaServers : viaServers withSignUrl : signUrl success : success failure : failure ] ;
}
else
{
joinRoomRequest = [ self . mainSession joinRoom : roomIdOrAlias viaServers : viaServers success : success failure : failure ] ;
}
}
- ( void ) processRoomJoinFailureWithError : ( NSError * ) error completion : ( void ( ^ ) ( MXKRoomViewControllerJoinRoomResult result ) ) completion
{
self -> joinRoomRequest = nil ;
[ self stopActivityIndicator ] ;
// Show the error to the end user
NSString * msg = [ error . userInfo valueForKey : NSLocalizedDescriptionKey ] ;
// FIXME : We should hide this inside the SDK and expose it as a domain specific error
BOOL isRoomEmpty = [ msg isEqualToString : @ "No known servers" ] ;
if ( isRoomEmpty )
{
// minging kludge until https : // matrix . org / jira / browse / SYN -678 is fixed
// ' Error when trying to join an empty room should be more explicit '
msg = [ MatrixKitL10n roomErrorJoinFailedEmptyRoom ] ;
}
MXWeakify ( self ) ;
[ self -> currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
UIAlertController * errorAlert = [ UIAlertController alertControllerWithTitle : [ MatrixKitL10n roomErrorJoinFailedTitle ]
message : msg
preferredStyle : UIAlertControllerStyleAlert ] ;
[ errorAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n ok ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
MXStrongifyAndReturnIfNil ( self ) ;
self -> currentAlert = nil ;
if ( completion )
{
completion ( ( isRoomEmpty ? MXKRoomViewControllerJoinRoomResultFailureRoomEmpty : MXKRoomViewControllerJoinRoomResultFailureGeneric ) ) ;
}
} ] ] ;
[ self presentViewController : errorAlert animated : YES completion : nil ] ;
currentAlert = errorAlert ;
}
- ( void ) leaveRoomOnEvent : ( MXEvent * ) event
{
[ self dismissTemporarySubViews ] ;
NSString * reason = nil ;
if ( event )
{
MXKEventFormatterError error ;
reason = [ roomDataSource . eventFormatter stringFromEvent : event withRoomState : roomDataSource . roomState error : & error ] ;
if ( error ! = MXKEventFormatterErrorNone )
{
reason = nil ;
}
}
if ( ! reason . length )
{
if ( self . roomDataSource . room . isDirect )
{
reason = [ MatrixKitL10n roomLeftForDm ] ;
}
else
{
reason = [ MatrixKitL10n roomLeft ] ;
}
}
_bubblesTableView . dataSource = nil ;
_bubblesTableView . delegate = nil ;
if ( self . hasRoomDataSourceOwnership )
{
// Release the room data source
[ roomDataSource destroy ] ;
}
else if ( roomDataSource . delegate = = self )
{
roomDataSource . delegate = nil ;
}
roomDataSource = nil ;
// Add reason label
2021-12-14 17:33:37 +00:00
UILabel * leftRoomReasonLabel = [ [ UILabel alloc ] initWithFrame : CGRectMake ( 10 , 5 , self . view . frame . size . width - 20 , 70 ) ] ;
leftRoomReasonLabel . numberOfLines = 0 ;
leftRoomReasonLabel . text = reason ;
leftRoomReasonLabel . autoresizingMask = UIViewAutoresizingFlexibleWidth ;
2021-12-03 10:47:24 +00:00
_bubblesTableView . tableHeaderView = [ [ UILabel alloc ] initWithFrame : CGRectMake ( 0 , 0 , self . view . frame . size . width , 80 ) ] ;
2021-12-14 17:33:37 +00:00
[ _bubblesTableView . tableHeaderView addSubview : leftRoomReasonLabel ] ;
2021-12-03 10:47:24 +00:00
[ _bubblesTableView reloadData ] ;
2021-12-14 17:33:37 +00:00
_leftRoomReasonLabel = leftRoomReasonLabel ;
2021-12-03 10:47:24 +00:00
[ self updateViewControllerAppearanceOnRoomDataSourceState ] ;
}
- ( void ) setPaginationLimit : ( NSUInteger ) paginationLimit
{
_paginationLimit = paginationLimit ;
// Use the same value when loading messages around the initial event
roomDataSource . paginationLimitAroundInitialEvent = _paginationLimit ;
}
- ( void ) setRoomTitleViewClass : ( Class ) roomTitleViewClass
{
// Sanity check : accept only MXKRoomTitleView classes or sub - classes
NSParameterAssert ( [ roomTitleViewClass isSubclassOfClass : MXKRoomTitleView . class ] ) ;
// Remove potential title view
if ( titleView )
{
[ NSLayoutConstraint deactivateConstraints : titleView . constraints ] ;
[ titleView dismissKeyboard ] ;
[ titleView removeFromSuperview ] ;
[ titleView destroy ] ;
}
titleView = self . navigationItem . titleView = [ roomTitleViewClass roomTitleView ] ;
titleView . delegate = self ;
// Define directly the navigation titleView with the custom title view instance . Do not use anymore a container .
self . navigationItem . titleView = titleView ;
[ self updateViewControllerAppearanceOnRoomDataSourceState ] ;
}
- ( void ) setRoomInputToolbarViewClass : ( Class ) roomInputToolbarViewClass
{
if ( ! _roomInputToolbarContainer )
{
MXLogDebug ( @ "[MXKRoomVC] Set roomInputToolbarViewClass failed: container is missing" ) ;
return ;
}
// Remove potential toolbar
if ( inputToolbarView )
{
MXLogDebug ( @ "[MXKRoomVC] setRoomInputToolbarViewClass: Set inputToolbarView with class %@ to nil" , [ self . inputToolbarView class ] ) ;
[ NSLayoutConstraint deactivateConstraints : inputToolbarView . constraints ] ;
[ inputToolbarView dismissKeyboard ] ;
[ inputToolbarView removeFromSuperview ] ;
[ inputToolbarView destroy ] ;
inputToolbarView = nil ;
}
if ( roomDataSource && ( ! roomDataSource . isLive || roomDataSource . isPeeking ) )
{
// Do not show the input toolbar if the displayed timeline is not a live one , or in case of peeking .
// We do not let the user type message in this case .
roomInputToolbarViewClass = nil ;
}
if ( roomInputToolbarViewClass )
{
// Sanity check : accept only MXKRoomInputToolbarView classes or sub - classes
NSParameterAssert ( [ roomInputToolbarViewClass isSubclassOfClass : MXKRoomInputToolbarView . class ] ) ;
MXLogDebug ( @ "[MXKRoomVC] setRoomInputToolbarViewClass: Set inputToolbarView to class %@" , roomInputToolbarViewClass ) ;
id inputToolbarView = [ roomInputToolbarViewClass roomInputToolbarView ] ;
self -> inputToolbarView = inputToolbarView ;
self -> inputToolbarView . delegate = self ;
// Add the input toolbar view and define edge constraints
[ _roomInputToolbarContainer addSubview : inputToolbarView ] ;
[ _roomInputToolbarContainer addConstraint : [ NSLayoutConstraint constraintWithItem : _roomInputToolbarContainer
attribute : NSLayoutAttributeBottom
relatedBy : NSLayoutRelationEqual
toItem : inputToolbarView
attribute : NSLayoutAttributeBottom
multiplier : 1.0 f
constant : 0.0 f ] ] ;
[ _roomInputToolbarContainer addConstraint : [ NSLayoutConstraint constraintWithItem : _roomInputToolbarContainer
attribute : NSLayoutAttributeTop
relatedBy : NSLayoutRelationEqual
toItem : inputToolbarView
attribute : NSLayoutAttributeTop
multiplier : 1.0 f
constant : 0.0 f ] ] ;
[ _roomInputToolbarContainer addConstraint : [ NSLayoutConstraint constraintWithItem : _roomInputToolbarContainer
attribute : NSLayoutAttributeLeading
relatedBy : NSLayoutRelationEqual
toItem : inputToolbarView
attribute : NSLayoutAttributeLeading
multiplier : 1.0 f
constant : 0.0 f ] ] ;
[ _roomInputToolbarContainer addConstraint : [ NSLayoutConstraint constraintWithItem : _roomInputToolbarContainer
attribute : NSLayoutAttributeTrailing
relatedBy : NSLayoutRelationEqual
toItem : inputToolbarView
attribute : NSLayoutAttributeTrailing
multiplier : 1.0 f
constant : 0.0 f ] ] ;
}
[ _roomInputToolbarContainer setNeedsUpdateConstraints ] ;
}
- ( void ) setRoomActivitiesViewClass : ( Class ) roomActivitiesViewClass
{
if ( ! _roomActivitiesContainer )
{
MXLogDebug ( @ "[MXKRoomVC] Set RoomActivitiesViewClass failed: container is missing" ) ;
return ;
}
// Remove potential toolbar
if ( activitiesView )
{
[ NSLayoutConstraint deactivateConstraints : activitiesView . constraints ] ;
[ activitiesView removeFromSuperview ] ;
[ activitiesView destroy ] ;
activitiesView = nil ;
}
if ( roomActivitiesViewClass )
{
// Sanity check : accept only MXKRoomExtraInfoView classes or sub - classes
NSParameterAssert ( [ roomActivitiesViewClass isSubclassOfClass : MXKRoomActivitiesView . class ] ) ;
activitiesView = [ roomActivitiesViewClass roomActivitiesView ] ;
// Add the view and define edge constraints
activitiesView . translatesAutoresizingMaskIntoConstraints = NO ;
[ _roomActivitiesContainer addSubview : activitiesView ] ;
NSLayoutConstraint * topConstraint = [ NSLayoutConstraint constraintWithItem : _roomActivitiesContainer
attribute : NSLayoutAttributeTop
relatedBy : NSLayoutRelationEqual
toItem : activitiesView
attribute : NSLayoutAttributeTop
multiplier : 1.0 f
constant : 0.0 f ] ;
NSLayoutConstraint * leadingConstraint = [ NSLayoutConstraint constraintWithItem : _roomActivitiesContainer
attribute : NSLayoutAttributeLeading
relatedBy : NSLayoutRelationEqual
toItem : activitiesView
attribute : NSLayoutAttributeLeading
multiplier : 1.0 f
constant : 0.0 f ] ;
NSLayoutConstraint * widthConstraint = [ NSLayoutConstraint constraintWithItem : _roomActivitiesContainer
attribute : NSLayoutAttributeWidth
relatedBy : NSLayoutRelationEqual
toItem : activitiesView
attribute : NSLayoutAttributeWidth
multiplier : 1.0 f
constant : 0.0 f ] ;
NSLayoutConstraint * heightConstraint = [ NSLayoutConstraint constraintWithItem : _roomActivitiesContainer
attribute : NSLayoutAttributeHeight
relatedBy : NSLayoutRelationEqual
toItem : activitiesView
attribute : NSLayoutAttributeHeight
multiplier : 1.0 f
constant : 0.0 f ] ;
[ NSLayoutConstraint activateConstraints : @ [ topConstraint , leadingConstraint , widthConstraint , heightConstraint ] ] ;
// let the provide view to define a height .
// it could have no constrainst if there is no defined xib
_roomActivitiesContainerHeightConstraint . constant = activitiesView . height ;
// Listen to activities view change
activitiesView . delegate = self ;
}
else
{
_roomActivitiesContainerHeightConstraint . constant = 0 ;
}
_bubblesTableViewBottomConstraint . constant = _roomInputToolbarContainerBottomConstraint . constant + _roomInputToolbarContainerHeightConstraint . constant + _roomActivitiesContainerHeightConstraint . constant ;
[ _roomActivitiesContainer setNeedsUpdateConstraints ] ;
}
- ( void ) setAttachmentsViewerClass : ( Class ) theAttachmentsViewerClass
{
if ( theAttachmentsViewerClass )
{
// Sanity check : accept only MXKAttachmentsViewController classes or sub - classes
NSParameterAssert ( [ theAttachmentsViewerClass isSubclassOfClass : MXKAttachmentsViewController . class ] ) ;
}
attachmentsViewerClass = theAttachmentsViewerClass ;
}
- ( void ) setEventDetailsViewClass : ( Class ) eventDetailsViewClass
{
if ( eventDetailsViewClass )
{
// Sanity check : accept only MXKEventDetailsView classes or sub - classes
NSParameterAssert ( [ eventDetailsViewClass isSubclassOfClass : MXKEventDetailsView . class ] ) ;
}
customEventDetailsViewClass = eventDetailsViewClass ;
}
- ( BOOL ) isIRCStyleCommand : ( NSString * ) string
{
// Check whether the provided text may be an IRC - style command
if ( [ string hasPrefix : @ "/" ] = = NO || [ string hasPrefix : @ "//" ] = = YES )
{
return NO ;
}
// Parse command line
NSArray * components = [ string componentsSeparatedByString : @ " " ] ;
NSString * cmd = [ components objectAtIndex : 0 ] ;
NSUInteger index = 1 ;
// TODO : display an alert with the cmd usage in case of error or unrecognized cmd .
NSString * cmdUsage ;
if ( [ cmd isEqualToString : kMXKSlashCmdEmote ] )
{
// send message as an emote
[ self sendTextMessage : string ] ;
}
else if ( [ string hasPrefix : kMXKSlashCmdChangeDisplayName ] )
{
// Change display name
NSString * displayName ;
// Sanity check
if ( string . length > kMXKSlashCmdChangeDisplayName . length )
{
displayName = [ string substringFromIndex : kMXKSlashCmdChangeDisplayName . length + 1 ] ;
// Remove white space from both ends
displayName = [ displayName stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceCharacterSet ] ] ;
}
if ( displayName . length )
{
[ roomDataSource . mxSession . matrixRestClient setDisplayName : displayName success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Set displayName failed" ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /nick <display_name>" ;
}
}
else if ( [ string hasPrefix : kMXKSlashCmdJoinRoom ] )
{
// Join a room
NSString * roomAlias ;
// Sanity check
if ( string . length > kMXKSlashCmdJoinRoom . length )
{
roomAlias = [ string substringFromIndex : kMXKSlashCmdJoinRoom . length + 1 ] ;
// Remove white space from both ends
roomAlias = [ roomAlias stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceCharacterSet ] ] ;
}
// Check
if ( roomAlias . length )
{
// TODO : / join command does not support via parameters yet
[ roomDataSource . mxSession joinRoom : roomAlias viaServers : nil success : ^ ( MXRoom * room ) {
// Do nothing by default when we succeed to join the room
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Join roomAlias (%@) failed" , roomAlias ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /join <room_alias>" ;
}
}
else if ( [ string hasPrefix : kMXKSlashCmdPartRoom ] )
{
// Leave this room or another one
NSString * roomId ;
NSString * roomIdOrAlias ;
// Sanity check
if ( string . length > kMXKSlashCmdPartRoom . length )
{
roomIdOrAlias = [ string substringFromIndex : kMXKSlashCmdPartRoom . length + 1 ] ;
// Remove white space from both ends
roomIdOrAlias = [ roomIdOrAlias stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceCharacterSet ] ] ;
}
// Check
if ( roomIdOrAlias . length )
{
// Leave another room
if ( [ MXTools isMatrixRoomAlias : roomIdOrAlias ] )
{
// Convert the alias to a room ID
MXRoom * room = [ roomDataSource . mxSession roomWithAlias : roomIdOrAlias ] ;
if ( room )
{
roomId = room . roomId ;
}
}
else if ( [ MXTools isMatrixRoomIdentifier : roomIdOrAlias ] )
{
roomId = roomIdOrAlias ;
}
}
else
{
// Leave the current room
roomId = roomDataSource . roomId ;
}
if ( roomId . length )
{
[ roomDataSource . mxSession leaveRoom : roomId success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Part room_alias (%@ / %@) failed" , roomIdOrAlias , roomId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /part [<room_alias>]" ;
}
}
else if ( [ string hasPrefix : kMXKSlashCmdChangeRoomTopic ] )
{
// Change topic
NSString * topic ;
// Sanity check
if ( string . length > kMXKSlashCmdChangeRoomTopic . length )
{
topic = [ string substringFromIndex : kMXKSlashCmdChangeRoomTopic . length + 1 ] ;
// Remove white space from both ends
topic = [ topic stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceCharacterSet ] ] ;
}
if ( topic . length )
{
[ roomDataSource . room setTopic : topic success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Set topic failed" ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /topic <topic>" ;
}
}
else
{
// Retrieve userId
NSString * userId = nil ;
while ( index < components . count )
{
userId = [ components objectAtIndex : index + + ] ;
if ( userId . length )
{
// done
break ;
}
// reset
userId = nil ;
}
if ( [ cmd isEqualToString : kMXKSlashCmdInviteUser ] )
{
if ( userId )
{
// Invite the user
[ roomDataSource . room inviteUser : userId success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Invite user (%@) failed" , userId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /invite <userId>" ;
}
}
else if ( [ cmd isEqualToString : kMXKSlashCmdKickUser ] )
{
if ( userId )
{
// Retrieve potential reason
NSString * reason = nil ;
while ( index < components . count )
{
if ( reason )
{
reason = [ NSString stringWithFormat : @ "%@ %@" , reason , [ components objectAtIndex : index + + ] ] ;
}
else
{
reason = [ components objectAtIndex : index + + ] ;
}
}
// Kick the user
[ roomDataSource . room kickUser : userId reason : reason success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Kick user (%@) failed" , userId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /kick <userId> [<reason>]" ;
}
}
else if ( [ cmd isEqualToString : kMXKSlashCmdBanUser ] )
{
if ( userId )
{
// Retrieve potential reason
NSString * reason = nil ;
while ( index < components . count )
{
if ( reason )
{
reason = [ NSString stringWithFormat : @ "%@ %@" , reason , [ components objectAtIndex : index + + ] ] ;
}
else
{
reason = [ components objectAtIndex : index + + ] ;
}
}
// Ban the user
[ roomDataSource . room banUser : userId reason : reason success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Ban user (%@) failed" , userId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /ban <userId> [<reason>]" ;
}
}
else if ( [ cmd isEqualToString : kMXKSlashCmdUnbanUser ] )
{
if ( userId )
{
// Unban the user
[ roomDataSource . room unbanUser : userId success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Unban user (%@) failed" , userId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /unban <userId>" ;
}
}
else if ( [ cmd isEqualToString : kMXKSlashCmdSetUserPowerLevel ] )
{
// Retrieve power level
NSString * powerLevel = nil ;
while ( index < components . count )
{
powerLevel = [ components objectAtIndex : index + + ] ;
if ( powerLevel . length )
{
// done
break ;
}
// reset
powerLevel = nil ;
}
// Set power level
if ( userId && powerLevel )
{
// Set user power level
[ roomDataSource . room setPowerLevelOfUserWithUserID : userId powerLevel : [ powerLevel integerValue ] success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Set user power (%@) failed" , userId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /op <userId> <power level>" ;
}
}
else if ( [ cmd isEqualToString : kMXKSlashCmdResetUserPowerLevel ] )
{
if ( userId )
{
// Reset user power level
[ roomDataSource . room setPowerLevelOfUserWithUserID : userId powerLevel : 0 success : ^ {
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomVC] Reset user power (%@) failed" , userId ) ;
// Notify MatrixKit user
NSString * myUserId = self -> roomDataSource . mxSession . myUser . userId ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error userInfo : myUserId ? @ { kMXKErrorUserIdKey : myUserId } : nil ] ;
} ] ;
}
else
{
// Display cmd usage in text input as placeholder
cmdUsage = @ "Usage: /deop <userId>" ;
}
}
else
{
MXLogDebug ( @ "[MXKRoomVC] Unrecognised IRC-style command: %@" , string ) ;
// cmdUsage = [ NSString stringWithFormat : @ "Unrecognised IRC-style command: %@" , cmd ] ;
return NO ;
}
}
return YES ;
}
- ( void ) mention : ( MXRoomMember * ) roomMember
{
NSString * memberName = roomMember . displayname . length ? roomMember . displayname : roomMember . userId ;
if ( inputToolbarView . textMessage . length )
{
[ inputToolbarView pasteText : memberName ] ;
}
else if ( [ roomMember . userId isEqualToString : self . mainSession . myUser . userId ] )
{
// Prepare emote
inputToolbarView . textMessage = @ "/me " ;
}
else
{
// Bing the member
inputToolbarView . textMessage = [ NSString stringWithFormat : @ "%@: " , memberName ] ;
}
[ inputToolbarView becomeFirstResponder ] ;
}
- ( void ) dismissKeyboard
{
[ titleView dismissKeyboard ] ;
[ inputToolbarView dismissKeyboard ] ;
}
- ( BOOL ) isBubblesTableScrollViewAtTheBottom
{
if ( _bubblesTableView . contentSize . height )
{
// Check whether the most recent message is visible .
// Compute the max vertical position visible according to contentOffset
CGFloat maxPositionY = _bubblesTableView . contentOffset . y + ( _bubblesTableView . frame . size . height - _bubblesTableView . adjustedContentInset . bottom ) ;
// Be a bit less retrictive , consider the table view at the bottom even if the most recent message is partially hidden
maxPositionY + = 44 ;
BOOL isScrolledToBottom = ( maxPositionY >= _bubblesTableView . contentSize . height ) ;
// Consider the table view at the bottom if a scrolling to bottom is in progress too
return ( isScrolledToBottom || isScrollingToBottom ) ;
}
// Consider empty table view as at the bottom . Only do this after it has appeared .
// Returning YES here before the view has appeared allows calls to scrollBubblesTableViewToBottomAnimated
// before the view knows its final size , resulting in a position offset the second time a room is shown ( #4524 ) .
return hasAppearedOnce ;
}
- ( void ) scrollBubblesTableViewToBottomAnimated : ( BOOL ) animated
{
if ( _bubblesTableView . contentSize . height )
{
CGFloat visibleHeight = _bubblesTableView . frame . size . height - _bubblesTableView . adjustedContentInset . top - _bubblesTableView . adjustedContentInset . bottom ;
if ( visibleHeight < _bubblesTableView . contentSize . height )
{
CGFloat wantedOffsetY = _bubblesTableView . contentSize . height - visibleHeight - _bubblesTableView . adjustedContentInset . top ;
CGFloat currentOffsetY = _bubblesTableView . contentOffset . y ;
if ( wantedOffsetY ! = currentOffsetY )
{
isScrollingToBottom = YES ;
BOOL savedBubbleTableViewDisplayInTransition = self . isBubbleTableViewDisplayInTransition ;
self . bubbleTableViewDisplayInTransition = YES ;
[ self setBubbleTableViewContentOffset : CGPointMake ( 0 , wantedOffsetY ) animated : animated ] ;
self . bubbleTableViewDisplayInTransition = savedBubbleTableViewDisplayInTransition ;
}
else
{
// upateCurrentEventIdAtTableBottom must be called here ( it is usually called by the scrollview delegate at the end of scrolling ) .
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
else
{
[ self setBubbleTableViewContentOffset : CGPointMake ( 0 , - _bubblesTableView . adjustedContentInset . top ) animated : animated ] ;
}
shouldScrollToBottomOnTableRefresh = NO ;
}
}
- ( void ) dismissTemporarySubViews
{
[ self dismissKeyboard ] ;
if ( currentAlert )
{
[ currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
currentAlert = nil ;
}
if ( eventDetailsView )
{
[ eventDetailsView removeFromSuperview ] ;
eventDetailsView = nil ;
}
if ( _leftRoomReasonLabel )
{
[ _leftRoomReasonLabel removeFromSuperview ] ;
_leftRoomReasonLabel = nil ;
_bubblesTableView . tableHeaderView = nil ;
}
// Dispose potential keyboard view
self . keyboardView = nil ;
}
- ( void ) setBubbleTableViewContentOffset : ( CGPoint ) contentOffset animated : ( BOOL ) animated
{
if ( preventBubblesTableViewScroll )
{
return ;
}
[ self . bubblesTableView setContentOffset : contentOffset animated : animated ] ;
}
# pragma mark - properties
- ( void ) setBubbleTableViewDisplayInTransition : ( BOOL ) bubbleTableViewDisplayInTransition
{
if ( _bubbleTableViewDisplayInTransition ! = bubbleTableViewDisplayInTransition )
{
_bubbleTableViewDisplayInTransition = bubbleTableViewDisplayInTransition ;
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
- ( void ) setUpdateRoomReadMarker : ( BOOL ) updateRoomReadMarker
{
if ( _updateRoomReadMarker ! = updateRoomReadMarker )
{
_updateRoomReadMarker = updateRoomReadMarker ;
if ( updateRoomReadMarker = = YES )
{
if ( currentEventIdAtTableBottom )
{
[ self . roomDataSource . room moveReadMarkerToEventId : currentEventIdAtTableBottom ] ;
}
else
{
// Look for the last displayed event .
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
}
}
# pragma mark - activity indicator
- ( void ) stopActivityIndicator
{
// Keep the loading wheel displayed while we are joining the room
if ( joinRoomRequest )
{
return ;
}
// Check internal processes before stopping the loading wheel
if ( isPaginationInProgress || isInputToolbarProcessing )
{
// Keep activity indicator running
return ;
}
// Leave super decide
[ super stopActivityIndicator ] ;
}
# pragma mark - Pagination
- ( void ) triggerInitialBackPagination
{
// Trigger back pagination to fill all the screen
CGRect frame = [ [ UIScreen mainScreen ] bounds ] ;
MXWeakify ( self ) ;
isPaginationInProgress = YES ;
[ self startActivityIndicator ] ;
[ roomDataSource paginateToFillRect : frame
direction : MXTimelineDirectionBackwards
withMinRequestMessagesCount : _paginationLimit
success : ^ {
MXStrongifyAndReturnIfNil ( self ) ;
// Stop spinner
self -> isPaginationInProgress = NO ;
[ self stopActivityIndicator ] ;
self . bubbleTableViewDisplayInTransition = YES ;
// Reload table
[ self reloadBubblesTable : YES ] ;
if ( self -> roomDataSource . timeline . initialEventId )
{
// Center the table view to the cell that contains this event
NSInteger index = [ self -> roomDataSource indexOfCellDataWithEventId : self -> roomDataSource . timeline . initialEventId ] ;
if ( index ! = NSNotFound )
{
// Let iOS put the cell at the top of the table view
[ self . bubblesTableView scrollToRowAtIndexPath : [ NSIndexPath indexPathForRow : index inSection : 0 ] atScrollPosition : UITableViewScrollPositionTop animated : NO ] ;
// Apply an offset to move the targeted component at the center of the screen .
UITableViewCell * cell = [ self -> _bubblesTableView cellForRowAtIndexPath : [ NSIndexPath indexPathForRow : index inSection : 0 ] ] ;
CGPoint contentOffset = self -> _bubblesTableView . contentOffset ;
CGFloat firstVisibleContentRowOffset = self -> _bubblesTableView . contentOffset . y + self -> _bubblesTableView . adjustedContentInset . top ;
CGFloat lastVisibleContentRowOffset = self -> _bubblesTableView . frame . size . height - self -> _bubblesTableView . adjustedContentInset . bottom ;
CGFloat localPositionOfEvent = 0.0 ;
if ( [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
if ( self -> _centerBubblesTableViewContentOnTheInitialEventBottom )
{
localPositionOfEvent = [ roomBubbleTableViewCell bottomPositionOfEvent : self -> roomDataSource . timeline . initialEventId ] ;
}
else
{
localPositionOfEvent = [ roomBubbleTableViewCell topPositionOfEvent : self -> roomDataSource . timeline . initialEventId ] ;
}
}
contentOffset . y + = localPositionOfEvent - ( lastVisibleContentRowOffset / 2 - ( cell . frame . origin . y - firstVisibleContentRowOffset ) ) ;
// Sanity check
if ( contentOffset . y + lastVisibleContentRowOffset > self -> _bubblesTableView . contentSize . height )
{
contentOffset . y = self -> _bubblesTableView . contentSize . height - lastVisibleContentRowOffset ;
}
[ self setBubbleTableViewContentOffset : contentOffset animated : NO ] ;
// Update the read receipt and potentially the read marker .
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
self . bubbleTableViewDisplayInTransition = NO ;
}
failure : ^ ( NSError * error ) {
MXStrongifyAndReturnIfNil ( self ) ;
// Stop spinner
self -> isPaginationInProgress = NO ;
[ self stopActivityIndicator ] ;
self . bubbleTableViewDisplayInTransition = YES ;
// Reload table
[ self reloadBubblesTable : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
} ] ;
}
/ * *
Trigger an inconspicuous pagination .
The retrieved history is added discretely to the top or the bottom of bubbles table without change the current display .
@ param limit the maximum number of messages to retrieve .
@ param direction backwards or forwards .
* /
- ( void ) triggerPagination : ( NSUInteger ) limit direction : ( MXTimelineDirection ) direction
{
// Paginate only if possible
if ( isPaginationInProgress || roomDataSource . state ! = MXKDataSourceStateReady || NO = = [ roomDataSource . timeline canPaginate : direction ] )
{
return ;
}
// Store the current height of the first bubble ( if any )
backPaginationSavedFirstBubbleHeight = 0 ;
if ( direction = = MXTimelineDirectionBackwards && [ roomDataSource tableView : _bubblesTableView numberOfRowsInSection : 0 ] )
{
NSIndexPath * indexPath = [ NSIndexPath indexPathForRow : 0 inSection : 0 ] ;
backPaginationSavedFirstBubbleHeight = [ self tableView : _bubblesTableView heightForRowAtIndexPath : indexPath ] ;
}
isPaginationInProgress = YES ;
MXWeakify ( self ) ;
// Trigger pagination
[ roomDataSource paginate : limit direction : direction onlyFromStore : NO success : ^ ( NSUInteger addedCellNumber ) {
MXStrongifyAndReturnIfNil ( self ) ;
// We will adjust the vertical offset in order to unchange the current display ( pagination should be inconspicuous )
CGFloat verticalOffset = 0 ;
if ( direction = = MXTimelineDirectionBackwards )
{
// Compute the cumulative height of the added messages
for ( NSUInteger index = 0 ; index < addedCellNumber ; index + + )
{
NSIndexPath * indexPath = [ NSIndexPath indexPathForRow : index inSection : 0 ] ;
verticalOffset + = [ self tableView : self -> _bubblesTableView heightForRowAtIndexPath : indexPath ] ;
}
// Add delta of the height of the previous first cell ( if any )
if ( addedCellNumber < [ self -> roomDataSource tableView : self -> _bubblesTableView numberOfRowsInSection : 0 ] )
{
NSIndexPath * indexPath = [ NSIndexPath indexPathForRow : addedCellNumber inSection : 0 ] ;
verticalOffset + = ( [ self tableView : self -> _bubblesTableView heightForRowAtIndexPath : indexPath ] - self -> backPaginationSavedFirstBubbleHeight ) ;
}
self -> _bubblesTableView . tableHeaderView = self -> backPaginationActivityView = nil ;
}
else
{
self -> _bubblesTableView . tableFooterView = self -> reconnectingView = nil ;
}
// Trigger a full table reload . We could not only insert new cells related to pagination ,
// because some other changes may have been ignored during pagination ( see [ dataSource : didCellChange : ] ) .
self . bubbleTableViewDisplayInTransition = YES ;
// Disable temporarily scrolling and hide the scroll indicator during refresh to prevent flickering
[ self . bubblesTableView setShowsVerticalScrollIndicator : NO ] ;
[ self . bubblesTableView setScrollEnabled : NO ] ;
CGPoint contentOffset = self . bubblesTableView . contentOffset ;
BOOL hasBeenScrolledToBottom = [ self reloadBubblesTable : NO ] ;
if ( direction = = MXTimelineDirectionBackwards )
{
// Backwards pagination adds cells at the top of the tableview content .
// Vertical content offset needs to be updated ( except if the table has been scrolled to bottom )
if ( ( ! hasBeenScrolledToBottom && verticalOffset > 0 ) || direction = = MXTimelineDirectionForwards )
{
// Adjust vertical offset in order to compensate scrolling
contentOffset . y + = verticalOffset ;
[ self setBubbleTableViewContentOffset : contentOffset animated : NO ] ;
}
}
else
{
[ self setBubbleTableViewContentOffset : contentOffset animated : NO ] ;
}
// Restore scrolling and the scroll indicator
[ self . bubblesTableView setShowsVerticalScrollIndicator : YES ] ;
[ self . bubblesTableView setScrollEnabled : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
self -> isPaginationInProgress = NO ;
// Force the update of the current visual position
// Else there is a scroll jump on incoming message ( see https : // github . com / vector - im / vector - ios / issues / 79 )
if ( direction = = MXTimelineDirectionBackwards )
{
[ self updateCurrentEventIdAtTableBottom : NO ] ;
}
} failure : ^ ( NSError * error ) {
MXStrongifyAndReturnIfNil ( self ) ;
self . bubbleTableViewDisplayInTransition = YES ;
// Reload table on failure because some changes may have been ignored during pagination ( see [ dataSource : didCellChange : ] )
self -> isPaginationInProgress = NO ;
self -> _bubblesTableView . tableHeaderView = self -> backPaginationActivityView = nil ;
[ self reloadBubblesTable : NO ] ;
self . bubbleTableViewDisplayInTransition = NO ;
} ] ;
}
- ( void ) triggerAttachmentBackPagination : ( NSString * ) eventId
{
// Paginate only if possible
if ( NO = = [ roomDataSource . timeline canPaginate : MXTimelineDirectionBackwards ] && self . attachmentsViewer )
{
return ;
}
isPaginationInProgress = YES ;
MXWeakify ( self ) ;
// Trigger back pagination to find previous attachments
[ roomDataSource paginate : _paginationLimit direction : MXTimelineDirectionBackwards onlyFromStore : NO success : ^ ( NSUInteger addedCellNumber ) {
MXStrongifyAndReturnIfNil ( self ) ;
// Check whether attachments viewer is still visible
if ( self . attachmentsViewer )
{
// Check whether some older attachments have been added .
// Note : the stickers are excluded from the attachments list returned by the room datasource .
BOOL isDone = NO ;
NSArray * attachmentsWithThumbnail = self . roomDataSource . attachmentsWithThumbnail ;
if ( attachmentsWithThumbnail . count )
{
MXKAttachment * attachment = attachmentsWithThumbnail . firstObject ;
isDone = ! [ attachment . eventId isEqualToString : eventId ] ;
}
// Check whether pagination is still available
self . attachmentsViewer . complete = ( [ self -> roomDataSource . timeline canPaginate : MXTimelineDirectionBackwards ] = = NO ) ;
if ( isDone || self . attachmentsViewer . complete )
{
// Refresh the current attachments list .
[ self . attachmentsViewer displayAttachments : attachmentsWithThumbnail focusOn : nil ] ;
// Trigger a full table reload without scrolling . We could not only insert new cells related to back pagination ,
// because some other changes may have been ignored during back pagination ( see [ dataSource : didCellChange : ] ) .
self . bubbleTableViewDisplayInTransition = YES ;
self -> isPaginationInProgress = NO ;
[ self reloadBubblesTable : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
// Done
return ;
}
// Here a new back pagination is required
[ self triggerAttachmentBackPagination : eventId ] ;
}
else
{
// Trigger a full table reload without scrolling . We could not only insert new cells related to back pagination ,
// because some other changes may have been ignored during back pagination ( see [ dataSource : didCellChange : ] ) .
self . bubbleTableViewDisplayInTransition = YES ;
self -> isPaginationInProgress = NO ;
[ self reloadBubblesTable : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
}
} failure : ^ ( NSError * error ) {
MXStrongifyAndReturnIfNil ( self ) ;
// Reload table on failure because some changes may have been ignored during back pagination ( see [ dataSource : didCellChange : ] )
self . bubbleTableViewDisplayInTransition = YES ;
self -> isPaginationInProgress = NO ;
[ self reloadBubblesTable : YES ] ;
self . bubbleTableViewDisplayInTransition = NO ;
if ( self . attachmentsViewer )
{
// Force attachments update to cancel potential loading wheel
[ self . attachmentsViewer displayAttachments : self . attachmentsViewer . attachments focusOn : nil ] ;
}
} ] ;
}
# pragma mark - Post messages
- ( void ) sendTextMessage : ( NSString * ) msgTxt
{
// Let the datasource send it and manage the local echo
[ roomDataSource sendTextMessage : msgTxt success : nil failure : ^ ( NSError * error )
{
// Just log the error . The message will be displayed in red in the room history
MXLogDebug ( @ "[MXKRoomViewController] sendTextMessage failed." ) ;
} ] ;
}
# pragma mark - Event handling
- ( void ) showEventDetails : ( MXEvent * ) event
{
[ self dismissKeyboard ] ;
// Remove potential existing subviews
[ self dismissTemporarySubViews ] ;
MXKEventDetailsView * eventDetailsView ;
if ( customEventDetailsViewClass )
{
eventDetailsView = [ [ customEventDetailsViewClass alloc ] initWithEvent : event andMatrixSession : roomDataSource . mxSession ] ;
}
else
{
eventDetailsView = [ [ MXKEventDetailsView alloc ] initWithEvent : event andMatrixSession : roomDataSource . mxSession ] ;
}
// Add shadow on event details view
eventDetailsView . layer . cornerRadius = 5 ;
eventDetailsView . layer . shadowOffset = CGSizeMake ( 0 , 1 ) ;
eventDetailsView . layer . shadowOpacity = 0.5 f ;
// Add the view and define edge constraints
[ self . view addSubview : eventDetailsView ] ;
self -> eventDetailsView = eventDetailsView ;
[ self . view addConstraint : [ NSLayoutConstraint constraintWithItem : eventDetailsView
attribute : NSLayoutAttributeTop
relatedBy : NSLayoutRelationEqual
toItem : self . topLayoutGuide
attribute : NSLayoutAttributeBottom
multiplier : 1.0 f
constant : 10.0 f ] ] ;
[ self . view addConstraint : [ NSLayoutConstraint constraintWithItem : eventDetailsView
attribute : NSLayoutAttributeBottom
relatedBy : NSLayoutRelationEqual
toItem : self . bottomLayoutGuide
attribute : NSLayoutAttributeTop
multiplier : 1.0 f
constant : -10.0 f ] ] ;
[ self . view addConstraint : [ NSLayoutConstraint constraintWithItem : self . view
attribute : NSLayoutAttributeLeading
relatedBy : NSLayoutRelationEqual
toItem : eventDetailsView
attribute : NSLayoutAttributeLeading
multiplier : 1.0 f
constant : -10.0 f ] ] ;
[ self . view addConstraint : [ NSLayoutConstraint constraintWithItem : self . view
attribute : NSLayoutAttributeTrailing
relatedBy : NSLayoutRelationEqual
toItem : eventDetailsView
attribute : NSLayoutAttributeTrailing
multiplier : 1.0 f
constant : 10.0 f ] ] ;
[ self . view setNeedsUpdateConstraints ] ;
}
- ( void ) promptUserToResendEvent : ( NSString * ) eventId
{
MXEvent * event = [ roomDataSource eventWithEventId : eventId ] ;
MXLogDebug ( @ "[MXKRoomViewController] promptUserToResendEvent: %@" , event ) ;
if ( event && event . eventType = = MXEventTypeRoomMessage )
{
2021-12-13 08:04:30 +00:00
NSString * msgtype = event . content [ kMXMessageTypeKey ] ;
2021-12-03 10:47:24 +00:00
NSString * textMessage ;
if ( [ msgtype isEqualToString : kMXMessageTypeText ] )
{
2021-12-13 10:08:52 +00:00
textMessage = event . content [ kMXMessageBodyKey ] ;
2021-12-03 10:47:24 +00:00
}
// Show a confirmation popup to the end user
if ( currentAlert )
{
[ currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
}
__weak typeof ( self ) weakSelf = self ;
UIAlertController * resendAlert = [ UIAlertController alertControllerWithTitle : [ MatrixKitL10n resendMessage ]
message : textMessage
preferredStyle : UIAlertControllerStyleAlert ] ;
[ resendAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n cancel ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
} ] ] ;
[ resendAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n ok ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Let the datasource resend . It will manage local echo , etc .
[ self -> roomDataSource resendEventWithEventId : eventId success : nil failure : nil ] ;
} ] ] ;
[ self presentViewController : resendAlert animated : YES completion : nil ] ;
currentAlert = resendAlert ;
}
}
# pragma mark - bubbles table
- ( BOOL ) reloadBubblesTable : ( BOOL ) useBottomAnchor
{
return [ self reloadBubblesTable : useBottomAnchor invalidateBubblesCellDataCache : NO ] ;
}
- ( BOOL ) reloadBubblesTable : ( BOOL ) useBottomAnchor invalidateBubblesCellDataCache : ( BOOL ) invalidateBubblesCellDataCache
{
BOOL shouldScrollToBottom = shouldScrollToBottomOnTableRefresh ;
// When no size transition is in progress , check if the bottom of the content is currently visible .
// If this is the case , we will scroll automatically to the bottom after table refresh .
if ( ! isSizeTransitionInProgress && ! shouldScrollToBottom )
{
shouldScrollToBottom = [ self isBubblesTableScrollViewAtTheBottom ] ;
}
// Force bubblesCellData message recalculation if requested
if ( invalidateBubblesCellDataCache )
{
[ self . roomDataSource invalidateBubblesCellDataCache ] ;
}
// When scroll to bottom is not active , check whether we should keep the current event displayed at the bottom of the table
if ( ! shouldScrollToBottom && useBottomAnchor && currentEventIdAtTableBottom )
{
// Update content offset after refresh in order to keep visible the current event displayed at the bottom
[ _bubblesTableView reloadData ] ;
// Retrieve the new cell index of the event displayed previously at the bottom of table
NSInteger rowIndex = [ roomDataSource indexOfCellDataWithEventId : currentEventIdAtTableBottom ] ;
if ( rowIndex ! = NSNotFound )
{
// Retrieve the corresponding cell
UITableViewCell * cell = [ _bubblesTableView cellForRowAtIndexPath : [ NSIndexPath indexPathForRow : rowIndex inSection : 0 ] ] ;
UITableViewCell * cellTmp ;
if ( ! cell )
{
NSString * reuseIdentifier = [ self cellReuseIdentifierForCellData : [ roomDataSource cellDataAtIndex : rowIndex ] ] ;
// Create temporarily the cell ( this cell will released at the end , to be reusable )
// Do not pass in the indexPath when creating this cell , as there is a possible crash by dequeuing
// multiple cells for the same index path if rotating the device coincides with reloading the data .
cellTmp = [ _bubblesTableView dequeueReusableCellWithIdentifier : reuseIdentifier ] ;
cell = cellTmp ;
}
if ( cell )
{
CGFloat eventTopPosition = cell . frame . origin . y ;
CGFloat eventBottomPosition = eventTopPosition + cell . frame . size . height ;
// Compute accurate event positions in case of bubble with multiple components
if ( [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
NSArray * bubbleComponents = roomBubbleTableViewCell . bubbleData . bubbleComponents ;
if ( bubbleComponents . count > 1 )
{
// Check and update each component position
[ roomBubbleTableViewCell . bubbleData prepareBubbleComponentsPosition ] ;
NSInteger index = bubbleComponents . count - 1 ;
MXKRoomBubbleComponent * component = bubbleComponents [ index ] ;
if ( [ component . event . eventId isEqualToString : currentEventIdAtTableBottom ] )
{
eventTopPosition + = roomBubbleTableViewCell . msgTextViewTopConstraint . constant + component . position . y ;
}
else
{
while ( index - - )
{
MXKRoomBubbleComponent * previousComponent = bubbleComponents [ index ] ;
if ( [ previousComponent . event . eventId isEqualToString : currentEventIdAtTableBottom ] )
{
// Update top position if this is not the first component
if ( index )
{
eventTopPosition + = roomBubbleTableViewCell . msgTextViewTopConstraint . constant + previousComponent . position . y ;
}
eventBottomPosition = cell . frame . origin . y + roomBubbleTableViewCell . msgTextViewTopConstraint . constant + component . position . y ;
break ;
}
component = previousComponent ;
}
}
}
}
// Compute the offset of the content displayed at the bottom .
CGFloat contentBottomOffsetY = _bubblesTableView . contentOffset . y + ( _bubblesTableView . frame . size . height - _bubblesTableView . adjustedContentInset . bottom ) ;
if ( contentBottomOffsetY > _bubblesTableView . contentSize . height )
{
contentBottomOffsetY = _bubblesTableView . contentSize . height ;
}
// Check whether this event is no more displayed at the bottom
if ( ( contentBottomOffsetY <= eventTopPosition ) || ( eventBottomPosition < contentBottomOffsetY ) )
{
// Compute the top content offset to display again this event at the table bottom
CGFloat contentOffsetY = eventBottomPosition - ( _bubblesTableView . frame . size . height - _bubblesTableView . adjustedContentInset . bottom ) ;
// Check if there are enought data to fill the top
if ( contentOffsetY < - _bubblesTableView . adjustedContentInset . top )
{
// Scroll to the top
contentOffsetY = - _bubblesTableView . adjustedContentInset . top ;
}
CGPoint contentOffset = _bubblesTableView . contentOffset ;
contentOffset . y = contentOffsetY ;
[ self setBubbleTableViewContentOffset : contentOffset animated : NO ] ;
}
if ( cellTmp && [ cellTmp conformsToProtocol : @ protocol ( MXKCellRendering ) ] && [ cellTmp respondsToSelector : @ selector ( didEndDisplay ) ] )
{
// Release here resources , and restore reusable cells
[ ( id < MXKCellRendering > ) cellTmp didEndDisplay ] ;
}
}
}
}
else
{
// Do a full reload
[ _bubblesTableView reloadData ] ;
}
if ( shouldScrollToBottom )
{
[ self scrollBubblesTableViewToBottomAnimated : NO ] ;
}
return shouldScrollToBottom ;
}
- ( void ) updateCurrentEventIdAtTableBottom : ( BOOL ) acknowledge
{
// Update the identifier of the event displayed at the bottom of the table , except if a rotation or other size transition is in progress .
if ( ! isSizeTransitionInProgress && ! self . isBubbleTableViewDisplayInTransition )
{
// Compute the content offset corresponding to the line displayed at the table bottom ( just above the toolbar ) .
CGFloat contentBottomOffsetY = _bubblesTableView . contentOffset . y + ( _bubblesTableView . frame . size . height - _bubblesTableView . adjustedContentInset . bottom ) ;
if ( contentBottomOffsetY > _bubblesTableView . contentSize . height )
{
contentBottomOffsetY = _bubblesTableView . contentSize . height ;
}
// Be a bit less retrictive , consider visible an event at the bottom even if is partially hidden .
contentBottomOffsetY + = 8 ;
// Reset the current event id
currentEventIdAtTableBottom = nil ;
// Consider the visible cells ( starting by those displayed at the bottom )
NSArray * visibleCells = [ _bubblesTableView visibleCells ] ;
NSInteger index = visibleCells . count ;
UITableViewCell * cell ;
while ( index - - )
{
cell = visibleCells [ index ] ;
// Check whether the cell is actually visible
if ( cell && ( cell . frame . origin . y < contentBottomOffsetY ) )
{
if ( ! [ cell isKindOfClass : MXKTableViewCell . class ] )
{
continue ;
}
MXKCellData * cellData = ( ( MXKTableViewCell * ) cell ) . mxkCellData ;
// Only ' MXKRoomBubbleCellData ' is supported here for the moment .
if ( ! [ cellData isKindOfClass : MXKRoomBubbleCellData . class ] )
{
continue ;
}
MXKRoomBubbleCellData * bubbleData = ( MXKRoomBubbleCellData * ) cellData ;
// Check which bubble component is displayed at the bottom .
// For that update each component position .
[ bubbleData prepareBubbleComponentsPosition ] ;
NSArray * bubbleComponents = bubbleData . bubbleComponents ;
NSInteger componentIndex = bubbleComponents . count ;
CGFloat bottomPositionY = cell . frame . size . height ;
MXKRoomBubbleComponent * component ;
while ( componentIndex - - )
{
component = bubbleComponents [ componentIndex ] ;
if ( ! [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
continue ;
}
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
// Check whether the bottom part of the component is visible .
CGFloat pos = cell . frame . origin . y + bottomPositionY ;
if ( pos <= contentBottomOffsetY )
{
// We found the component
currentEventIdAtTableBottom = component . event . eventId ;
break ;
}
// Prepare the bottom position for the next component
bottomPositionY = roomBubbleTableViewCell . msgTextViewTopConstraint . constant + component . position . y ;
}
if ( currentEventIdAtTableBottom )
{
if ( acknowledge && self . isEventsAcknowledgementEnabled )
{
// Indicate to the homeserver that the user has read this event .
// Check whether the read marker must be updated .
BOOL updateReadMarker = _updateRoomReadMarker ;
if ( updateReadMarker && roomDataSource . room . accountData . readMarkerEventId )
{
MXEvent * currentReadMarkerEvent = [ roomDataSource . mxSession . store eventWithEventId : roomDataSource . room . accountData . readMarkerEventId inRoom : roomDataSource . roomId ] ;
if ( ! currentReadMarkerEvent )
{
currentReadMarkerEvent = [ roomDataSource eventWithEventId : roomDataSource . room . accountData . readMarkerEventId ] ;
}
// Update the read marker only if the current event is available , and the new event is posterior to it .
updateReadMarker = ( currentReadMarkerEvent && ( currentReadMarkerEvent . originServerTs <= component . event . originServerTs ) ) ;
}
[ roomDataSource . room acknowledgeEvent : component . event andUpdateReadMarker : updateReadMarker ] ;
}
break ;
}
// else we consider the previous cell .
}
}
}
}
# pragma mark - MXKDataSourceDelegate
- ( Class < MXKCellRendering > ) cellViewClassForCellData : ( MXKCellData * ) cellData
{
2022-01-13 15:50:17 +00:00
return nil ;
2021-12-03 10:47:24 +00:00
}
- ( NSString * ) cellReuseIdentifierForCellData : ( MXKCellData * ) cellData
{
Class class = [ self cellViewClassForCellData : cellData ] ;
if ( [ class respondsToSelector : @ selector ( defaultReuseIdentifier ) ] )
{
return [ class defaultReuseIdentifier ] ;
}
return nil ;
}
- ( void ) dataSource : ( MXKDataSource * ) dataSource didCellChange : ( id ) changes
{
UIApplication * sharedApplication = [ UIApplication performSelector : @ selector ( sharedApplication ) ] ;
if ( sharedApplication && sharedApplication . applicationState ! = UIApplicationStateActive )
{
// Do nothing at the UI level if the application do a sync in background
return ;
}
if ( isPaginationInProgress )
{
// Ignore these changes , the table will be full updated at the end of pagination .
return ;
}
if ( self . attachmentsViewer )
{
// Refresh the current attachments list without changing the current displayed attachment ( see focus = nil ) .
NSArray * attachmentsWithThumbnail = self . roomDataSource . attachmentsWithThumbnail ;
[ self . attachmentsViewer displayAttachments : attachmentsWithThumbnail focusOn : nil ] ;
}
self . bubbleTableViewDisplayInTransition = YES ;
CGPoint contentOffset = self . bubblesTableView . contentOffset ;
BOOL hasScrolledToTheBottom = [ self reloadBubblesTable : YES ] ;
// If the user is scrolling while we reload the data for a new incoming message for example ,
// there will be a jump in the table view display .
// Resetting the contentOffset after the reload fixes the issue .
if ( hasScrolledToTheBottom = = NO )
{
[ self setBubbleTableViewContentOffset : contentOffset animated : NO ] ;
}
self . bubbleTableViewDisplayInTransition = NO ;
}
- ( void ) dataSource : ( MXKDataSource * ) dataSource didStateChange : ( MXKDataSourceState ) state
{
[ self updateViewControllerAppearanceOnRoomDataSourceState ] ;
if ( state = = MXKDataSourceStateReady )
{
[ self onRoomDataSourceReady ] ;
}
}
- ( void ) dataSource : ( MXKDataSource * ) dataSource didRecognizeAction : ( NSString * ) actionIdentifier inCell : ( id < MXKCellRendering > ) cell userInfo : ( NSDictionary * ) userInfo
{
MXLogDebug ( @ "Gesture %@ has been recognized in %@. UserInfo: %@" , actionIdentifier , cell , userInfo ) ;
if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellTapOnMessageTextView ] )
{
MXLogDebug ( @ " -> A message has been tapped" ) ;
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellTapOnSenderNameLabel ] || [ actionIdentifier isEqualToString : kMXKRoomBubbleCellTapOnAvatarView ] )
{
// MXLogDebug ( @ " -> Name or avatar of %@ has been tapped" , userInfo [ kMXKRoomBubbleCellUserIdKey ] ) ;
// Add the member display name in text input
MXRoomMember * selectedRoomMember = [ roomDataSource . roomState . members memberWithUserId : userInfo [ kMXKRoomBubbleCellUserIdKey ] ] ;
if ( selectedRoomMember )
{
[ self mention : selectedRoomMember ] ;
}
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellTapOnDateTimeContainer ] )
{
roomDataSource . showBubblesDateTime = ! roomDataSource . showBubblesDateTime ;
MXLogDebug ( @ " -> Turn %@ cells date" , roomDataSource . showBubblesDateTime ? @ "ON" : @ "OFF" ) ;
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellTapOnAttachmentView ] && [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
[ self showAttachmentInCell : ( MXKRoomBubbleTableViewCell * ) cell ] ;
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellLongPressOnProgressView ] && [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
// Check if there is a download in progress , then offer to cancel it
NSString * downloadId = roomBubbleTableViewCell . bubbleData . attachment . downloadId ;
if ( [ MXMediaManager existingDownloaderWithIdentifier : downloadId ] )
{
if ( currentAlert )
{
[ currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
}
__weak __typeof ( self ) weakSelf = self ;
UIAlertController * cancelAlert = [ UIAlertController alertControllerWithTitle : nil
message : [ MatrixKitL10n attachmentCancelDownload ]
preferredStyle : UIAlertControllerStyleAlert ] ;
[ cancelAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n no ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
} ] ] ;
[ cancelAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n yes ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Get again the loader
MXMediaLoader * loader = [ MXMediaManager existingDownloaderWithIdentifier : downloadId ] ;
if ( loader )
{
[ loader cancel ] ;
}
// Hide the progress animation
roomBubbleTableViewCell . progressView . hidden = YES ;
} ] ] ;
[ self presentViewController : cancelAlert animated : YES completion : nil ] ;
currentAlert = cancelAlert ;
}
else if ( roomBubbleTableViewCell . bubbleData . attachment . eventSentState = = MXEventSentStatePreparing ||
roomBubbleTableViewCell . bubbleData . attachment . eventSentState = = MXEventSentStateEncrypting ||
roomBubbleTableViewCell . bubbleData . attachment . eventSentState = = MXEventSentStateUploading )
{
// Offer to cancel the upload in progress
// Upload id is stored in attachment url ( nasty trick )
NSString * uploadId = roomBubbleTableViewCell . bubbleData . attachment . contentURL ;
if ( [ MXMediaManager existingUploaderWithId : uploadId ] )
{
if ( currentAlert )
{
[ currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
}
__weak __typeof ( self ) weakSelf = self ;
UIAlertController * cancelAlert = [ UIAlertController alertControllerWithTitle : nil
message : [ MatrixKitL10n attachmentCancelUpload ]
preferredStyle : UIAlertControllerStyleAlert ] ;
[ cancelAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n no ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
} ] ] ;
[ cancelAlert addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n yes ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
// TODO cancel the attachment encryption if it is in progress .
// Get again the loader
MXMediaLoader * loader = [ MXMediaManager existingUploaderWithId : uploadId ] ;
if ( loader )
{
[ loader cancel ] ;
}
// Hide the progress animation
roomBubbleTableViewCell . progressView . hidden = YES ;
if ( weakSelf )
{
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Remove the outgoing message and its related cached file .
[ [ NSFileManager defaultManager ] removeItemAtPath : roomBubbleTableViewCell . bubbleData . attachment . cacheFilePath error : nil ] ;
[ [ NSFileManager defaultManager ] removeItemAtPath : roomBubbleTableViewCell . bubbleData . attachment . thumbnailCachePath error : nil ] ;
[ self . roomDataSource removeEventWithEventId : roomBubbleTableViewCell . bubbleData . attachment . eventId ] ;
}
} ] ] ;
[ self presentViewController : cancelAlert animated : YES completion : nil ] ;
currentAlert = cancelAlert ;
}
}
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellLongPressOnEvent ] && [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
[ self dismissKeyboard ] ;
MXEvent * selectedEvent = userInfo [ kMXKRoomBubbleCellEventKey ] ;
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
MXKAttachment * attachment = roomBubbleTableViewCell . bubbleData . attachment ;
if ( selectedEvent )
{
if ( currentAlert )
{
[ currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
currentAlert = nil ;
// Cancel potential text selection in other bubbles
for ( MXKRoomBubbleTableViewCell * bubble in self . bubblesTableView . visibleCells )
{
[ bubble highlightTextMessageForEvent : nil ] ;
}
}
__weak __typeof ( self ) weakSelf = self ;
UIAlertController * actionSheet = [ UIAlertController alertControllerWithTitle : nil message : nil preferredStyle : UIAlertControllerStyleActionSheet ] ;
// Add actions for a failed event
if ( selectedEvent . sentState = = MXEventSentStateFailed )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n resend ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Let the datasource resend . It will manage local echo , etc .
[ self . roomDataSource resendEventWithEventId : selectedEvent . eventId success : nil failure : nil ] ;
} ] ] ;
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n delete ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
[ self . roomDataSource removeEventWithEventId : selectedEvent . eventId ] ;
} ] ] ;
}
// Add actions for text message
if ( ! attachment )
{
// Highlight the select event
[ roomBubbleTableViewCell highlightTextMessageForEvent : selectedEvent . eventId ] ;
// Retrieved data related to the selected event
NSArray * components = roomBubbleTableViewCell . bubbleData . bubbleComponents ;
MXKRoomBubbleComponent * selectedComponent ;
for ( selectedComponent in components )
{
if ( [ selectedComponent . event . eventId isEqualToString : selectedEvent . eventId ] )
{
break ;
}
selectedComponent = nil ;
}
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n copy ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Cancel event highlighting
[ roomBubbleTableViewCell highlightTextMessageForEvent : nil ] ;
NSString * textMessage = selectedComponent . textMessage ;
if ( textMessage )
{
MXKPasteboardManager . shared . pasteboard . string = textMessage ;
}
else
{
MXLogDebug ( @ "[MXKRoomViewController] Copy text failed. Text is nil for room id/event id: %@/%@" , selectedComponent . event . roomId , selectedComponent . event . eventId ) ;
}
} ] ] ;
if ( [ MXKAppSettings standardAppSettings ] . messageDetailsAllowSharing )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n share ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Cancel event highlighting
[ roomBubbleTableViewCell highlightTextMessageForEvent : nil ] ;
NSArray * activityItems = [ NSArray arrayWithObjects : selectedComponent . textMessage , nil ] ;
UIActivityViewController * activityViewController = [ [ UIActivityViewController alloc ] initWithActivityItems : activityItems applicationActivities : nil ] ;
if ( activityViewController )
{
activityViewController . modalTransitionStyle = UIModalTransitionStyleCoverVertical ;
activityViewController . popoverPresentationController . sourceView = roomBubbleTableViewCell ;
activityViewController . popoverPresentationController . sourceRect = roomBubbleTableViewCell . bounds ;
[ self presentViewController : activityViewController animated : YES completion : nil ] ;
}
} ] ] ;
}
if ( components . count > 1 )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n selectAll ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
[ self selectAllTextMessageInCell : cell ] ;
} ] ] ;
}
}
else // Add action for attachment
{
if ( attachment . type = = MXKAttachmentTypeImage || attachment . type = = MXKAttachmentTypeVideo )
{
if ( [ MXKAppSettings standardAppSettings ] . messageDetailsAllowSaving )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n save ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
[ self startActivityIndicator ] ;
[ attachment save : ^ {
typeof ( self ) self = weakSelf ;
[ self stopActivityIndicator ] ;
} failure : ^ ( NSError * error ) {
typeof ( self ) self = weakSelf ;
[ self stopActivityIndicator ] ;
// Notify MatrixKit user
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error ] ;
} ] ;
// Start animation in case of download during attachment preparing
[ roomBubbleTableViewCell startProgressUI ] ;
} ] ] ;
}
}
if ( attachment . type ! = MXKAttachmentTypeSticker )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n copyButtonName ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
[ self startActivityIndicator ] ;
[ attachment copy : ^ {
typeof ( self ) self = weakSelf ;
[ self stopActivityIndicator ] ;
} failure : ^ ( NSError * error ) {
typeof ( self ) self = weakSelf ;
[ self stopActivityIndicator ] ;
// Notify MatrixKit user
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error ] ;
} ] ;
// Start animation in case of download during attachment preparing
[ roomBubbleTableViewCell startProgressUI ] ;
} ] ] ;
if ( [ MXKAppSettings standardAppSettings ] . messageDetailsAllowSharing )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n share ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
[ attachment prepareShare : ^ ( NSURL * fileURL ) {
typeof ( self ) self = weakSelf ;
self -> documentInteractionController = [ UIDocumentInteractionController interactionControllerWithURL : fileURL ] ;
[ self -> documentInteractionController setDelegate : self ] ;
self -> currentSharedAttachment = attachment ;
if ( ! [ self -> documentInteractionController presentOptionsMenuFromRect : self . view . frame inView : self . view animated : YES ] )
{
self -> documentInteractionController = nil ;
[ attachment onShareEnded ] ;
self -> currentSharedAttachment = nil ;
}
} failure : ^ ( NSError * error ) {
// Notify MatrixKit user
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error ] ;
} ] ;
// Start animation in case of download during attachment preparing
[ roomBubbleTableViewCell startProgressUI ] ;
} ] ] ;
}
}
// Check status of the selected event
if ( selectedEvent . sentState = = MXEventSentStatePreparing ||
selectedEvent . sentState = = MXEventSentStateEncrypting ||
selectedEvent . sentState = = MXEventSentStateUploading )
{
// Upload id is stored in attachment url ( nasty trick )
NSString * uploadId = roomBubbleTableViewCell . bubbleData . attachment . contentURL ;
if ( [ MXMediaManager existingUploaderWithId : uploadId ] )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n cancelUpload ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
// TODO cancel the attachment encryption if it is in progress .
// Cancel the loader
MXMediaLoader * loader = [ MXMediaManager existingUploaderWithId : uploadId ] ;
if ( loader )
{
[ loader cancel ] ;
}
// Hide the progress animation
roomBubbleTableViewCell . progressView . hidden = YES ;
if ( weakSelf )
{
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Remove the outgoing message and its related cached file .
[ [ NSFileManager defaultManager ] removeItemAtPath : roomBubbleTableViewCell . bubbleData . attachment . cacheFilePath error : nil ] ;
[ [ NSFileManager defaultManager ] removeItemAtPath : roomBubbleTableViewCell . bubbleData . attachment . thumbnailCachePath error : nil ] ;
[ self . roomDataSource removeEventWithEventId : selectedEvent . eventId ] ;
}
} ] ] ;
}
}
}
// Check status of the selected event
if ( selectedEvent . sentState = = MXEventSentStateSent )
{
// Check whether download is in progress
if ( selectedEvent . isMediaAttachment )
{
NSString * downloadId = roomBubbleTableViewCell . bubbleData . attachment . downloadId ;
if ( [ MXMediaManager existingDownloaderWithIdentifier : downloadId ] )
{
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n cancelDownload ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Get again the loader
MXMediaLoader * loader = [ MXMediaManager existingDownloaderWithIdentifier : downloadId ] ;
if ( loader )
{
[ loader cancel ] ;
}
// Hide the progress animation
roomBubbleTableViewCell . progressView . hidden = YES ;
} ] ] ;
}
}
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n showDetails ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Cancel event highlighting ( if any )
[ roomBubbleTableViewCell highlightTextMessageForEvent : nil ] ;
// Display event details
[ self showEventDetails : selectedEvent ] ;
} ] ] ;
}
[ actionSheet addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n cancel ]
style : UIAlertActionStyleCancel
handler : ^ ( UIAlertAction * action ) {
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Cancel event highlighting ( if any )
[ roomBubbleTableViewCell highlightTextMessageForEvent : nil ] ;
} ] ] ;
// Do not display empty action sheet
if ( actionSheet . actions . count > 1 )
{
[ actionSheet popoverPresentationController ] . sourceView = roomBubbleTableViewCell ;
[ actionSheet popoverPresentationController ] . sourceRect = roomBubbleTableViewCell . bounds ;
[ self presentViewController : actionSheet animated : YES completion : nil ] ;
currentAlert = actionSheet ;
}
}
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellLongPressOnAvatarView ] )
{
MXLogDebug ( @ " -> Avatar of %@ has been long pressed" , userInfo [ kMXKRoomBubbleCellUserIdKey ] ) ;
}
else if ( [ actionIdentifier isEqualToString : kMXKRoomBubbleCellUnsentButtonPressed ] )
{
MXEvent * selectedEvent = userInfo [ kMXKRoomBubbleCellEventKey ] ;
if ( selectedEvent )
{
// The user may want to resend it
[ self promptUserToResendEvent : selectedEvent . eventId ] ;
}
}
}
# pragma mark - Clipboard
- ( void ) selectAllTextMessageInCell : ( id < MXKCellRendering > ) cell
{
if ( ! [ MXKAppSettings standardAppSettings ] . messageDetailsAllowSharing )
{
return ;
}
if ( [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
selectedText = roomBubbleTableViewCell . bubbleData . textMessage ;
roomBubbleTableViewCell . allTextHighlighted = YES ;
// Display Menu ( dispatch is required here , else the attributed text change hides the menu )
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
MXWeakify ( self ) ;
self . uiMenuControllerDidHideMenuNotificationObserver = [ [ NSNotificationCenter defaultCenter ] addObserverForName : UIMenuControllerDidHideMenuNotification object : nil queue : [ NSOperationQueue mainQueue ] usingBlock : ^ ( NSNotification * notif ) {
MXStrongifyAndReturnIfNil ( self ) ;
// Deselect text
roomBubbleTableViewCell . allTextHighlighted = NO ;
self -> selectedText = nil ;
[ UIMenuController sharedMenuController ] . menuItems = nil ;
[ [ NSNotificationCenter defaultCenter ] removeObserver : self . uiMenuControllerDidHideMenuNotificationObserver ] ;
} ] ;
[ self becomeFirstResponder ] ;
UIMenuController * menu = [ UIMenuController sharedMenuController ] ;
menu . menuItems = @ [ [ [ UIMenuItem alloc ] initWithTitle : [ MatrixKitL10n share ] action : @ selector ( share : ) ] ] ;
[ menu setTargetRect : roomBubbleTableViewCell . messageTextView . frame inView : roomBubbleTableViewCell ] ;
[ menu setMenuVisible : YES animated : YES ] ;
} ) ;
}
}
- ( void ) copy : ( id ) sender
{
if ( selectedText )
{
MXKPasteboardManager . shared . pasteboard . string = selectedText ;
}
else
{
MXLogDebug ( @ "[MXKRoomViewController] Selected text copy failed. Selected text is nil" ) ;
}
}
- ( void ) share : ( id ) sender
{
if ( selectedText )
{
NSArray * activityItems = [ NSArray arrayWithObjects : selectedText , nil ] ;
UIActivityViewController * activityViewController = [ [ UIActivityViewController alloc ] initWithActivityItems : activityItems applicationActivities : nil ] ;
if ( activityViewController )
{
activityViewController . modalTransitionStyle = UIModalTransitionStyleCoverVertical ;
activityViewController . popoverPresentationController . sourceView = self . view ;
activityViewController . popoverPresentationController . sourceRect = self . view . bounds ;
[ self presentViewController : activityViewController animated : YES completion : nil ] ;
}
}
}
- ( BOOL ) canPerformAction : ( SEL ) action withSender : ( id ) sender
{
if ( selectedText . length && ( action = = @ selector ( copy : ) || action = = @ selector ( share : ) ) )
{
return YES ;
}
return NO ;
}
- ( BOOL ) canBecomeFirstResponder
{
return ( selectedText . length ! = 0 ) ;
}
# pragma mark - UITableView delegate
- ( CGFloat ) tableView : ( UITableView * ) tableView heightForRowAtIndexPath : ( NSIndexPath * ) indexPath
{
if ( tableView = = _bubblesTableView )
{
return [ roomDataSource cellHeightAtIndex : indexPath . row withMaximumWidth : self . tableViewSafeAreaWidth ] ;
}
return 0 ;
}
- ( void ) tableView : ( UITableView * ) tableView didSelectRowAtIndexPath : ( NSIndexPath * ) indexPath
{
if ( tableView = = _bubblesTableView )
{
// Dismiss keyboard when user taps on messages table view content
[ self dismissKeyboard ] ;
}
}
- ( 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 = = _bubblesTableView )
{
// Detect top bounce
if ( scrollView . contentOffset . y < - scrollView . adjustedContentInset . top )
{
// Shall we add back pagination spinner ?
if ( isPaginationInProgress && ! backPaginationActivityView )
{
UIActivityIndicatorView * spinner = [ [ UIActivityIndicatorView alloc ] initWithActivityIndicatorStyle : UIActivityIndicatorViewStyleGray ] ;
spinner . hidesWhenStopped = NO ;
spinner . backgroundColor = [ UIColor clearColor ] ;
[ spinner startAnimating ] ;
// no need to manage constraints here
// IOS defines them .
// since IOS7 the spinner is centered so need to create a background and add it .
_bubblesTableView . tableHeaderView = backPaginationActivityView = spinner ;
}
}
else
{
// Shall we add forward pagination spinner ?
if ( ! roomDataSource . isLive && isPaginationInProgress && scrollView . contentOffset . y + scrollView . frame . size . height > scrollView . contentSize . height + 64 && ! reconnectingView )
{
[ self addReconnectingView ] ;
}
else
{
[ self detectPullToKick : scrollView ] ;
}
}
}
}
- ( void ) scrollViewDidEndDragging : ( UIScrollView * ) scrollView willDecelerate : ( BOOL ) decelerate
{
if ( scrollView = = _bubblesTableView )
{
// if the user scrolls the history content without animation
// upateCurrentEventIdAtTableBottom must be called here ( without dispatch ) .
// else it will be done in scrollViewDidEndDecelerating
if ( ! decelerate )
{
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
}
- ( void ) scrollViewDidEndDecelerating : ( UIScrollView * ) scrollView
{
if ( scrollView = = _bubblesTableView )
{
// do not dispatch the upateCurrentEventIdAtTableBottom call
// else it might triggers weird UI lags .
[ self updateCurrentEventIdAtTableBottom : YES ] ;
[ self managePullToKick : scrollView ] ;
}
}
- ( void ) scrollViewDidEndScrollingAnimation : ( UIScrollView * ) scrollView
{
if ( scrollView = = _bubblesTableView )
{
// do not dispatch the upateCurrentEventIdAtTableBottom call
// else it might triggers weird UI lags .
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
- ( void ) scrollViewDidScroll : ( UIScrollView * ) scrollView
{
if ( scrollView = = _bubblesTableView )
{
BOOL wasScrollingToBottom = isScrollingToBottom ;
// Consider this callback to reset scrolling to bottom flag
isScrollingToBottom = NO ;
// shouldScrollToBottomOnTableRefresh is used to inhibit false detection of
// scrolling action from the user when the viewVC appears or rotates
if ( scrollView = = _bubblesTableView && scrollView . contentSize . height && ! shouldScrollToBottomOnTableRefresh )
{
// when the content size if smaller that the frame
// scrollViewDidEndDecelerating is not called
// so test it when the content offset goes back to the screen top .
if ( ( scrollView . contentSize . height < scrollView . frame . size . height ) && ( - scrollView . contentOffset . y = = scrollView . adjustedContentInset . top ) )
{
[ self managePullToKick : scrollView ] ;
}
// Trigger inconspicuous pagination when user scrolls toward the top
if ( scrollView . contentOffset . y < _paginationThreshold )
{
[ self triggerPagination : _paginationLimit direction : MXTimelineDirectionBackwards ] ;
}
// Enable forwards pagination when displaying non live timeline
else if ( ! roomDataSource . isLive && ! wasScrollingToBottom && ( ( scrollView . contentSize . height - scrollView . contentOffset . y - scrollView . frame . size . height ) < _paginationThreshold ) )
{
[ self triggerPagination : _paginationLimit direction : MXTimelineDirectionForwards ] ;
}
}
if ( wasScrollingToBottom )
{
// When scrolling to the bottom is performed without animation , ' scrollViewDidEndScrollingAnimation ' is not called .
// upateCurrentEventIdAtTableBottom must be called here ( without dispatch ) .
[ self updateCurrentEventIdAtTableBottom : YES ] ;
}
}
}
# pragma mark - MXKRoomTitleViewDelegate
- ( void ) roomTitleView : ( MXKRoomTitleView * ) titleView presentAlertController : ( UIAlertController * ) alertController
{
[ self dismissKeyboard ] ;
[ self presentViewController : alertController animated : YES completion : nil ] ;
}
- ( BOOL ) roomTitleViewShouldBeginEditing : ( MXKRoomTitleView * ) titleView
{
return YES ;
}
- ( void ) roomTitleView : ( MXKRoomTitleView * ) titleView isSaving : ( BOOL ) saving
{
if ( saving )
{
[ self startActivityIndicator ] ;
}
else
{
[ self stopActivityIndicator ] ;
}
}
# pragma mark - MXKRoomInputToolbarViewDelegate
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView hideStatusBar : ( BOOL ) isHidden
{
isStatusBarHidden = isHidden ;
// Trigger status bar update
[ self setNeedsStatusBarAppearanceUpdate ] ;
// Handle status bar with the historical method .
// TODO : remove this [ UIApplication statusBarHidden ] use ( deprecated since iOS 9 ) .
// Note : setting statusBarHidden does nothing if your application is using the default UIViewController - based status bar system .
UIApplication * sharedApplication = [ UIApplication performSelector : @ selector ( sharedApplication ) ] ;
if ( sharedApplication )
{
sharedApplication . statusBarHidden = isHidden ;
}
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView isTyping : ( BOOL ) typing
{
if ( _saveProgressTextInput && roomDataSource )
{
// Store the potential message partially typed in text input
roomDataSource . partialTextMessage = inputToolbarView . textMessage ;
}
[ self handleTypingNotification : typing ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView heightDidChanged : ( CGFloat ) height completion : ( void ( ^ ) ( BOOL finished ) ) completion
{
_roomInputToolbarContainerHeightConstraint . constant = height ;
// Update layout with animation
[ UIView animateWithDuration : self . resizeComposerAnimationDuration delay : 0 options : UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
animations : ^ {
// We will scroll to bottom if the bottom of the table is currently visible
BOOL shouldScrollToBottom = [ self isBubblesTableScrollViewAtTheBottom ] ;
CGFloat bubblesTableViewBottomConst = self -> _roomInputToolbarContainerBottomConstraint . constant + self -> _roomInputToolbarContainerHeightConstraint . constant + self -> _roomActivitiesContainerHeightConstraint . constant ;
self -> _bubblesTableViewBottomConstraint . constant = bubblesTableViewBottomConst ;
// Force to render the view
[ self . view layoutIfNeeded ] ;
if ( shouldScrollToBottom )
{
[ self scrollBubblesTableViewToBottomAnimated : NO ] ;
}
}
completion : ^ ( BOOL finished ) {
if ( completion )
{
completion ( finished ) ;
}
} ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView sendTextMessage : ( NSString * ) textMessage
{
// Handle potential IRC commands in typed string
if ( [ self isIRCStyleCommand : textMessage ] = = NO )
{
// Send text message in the current room
[ self sendTextMessage : textMessage ] ;
}
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView sendImage : ( UIImage * ) image
{
// Let the datasource send it and manage the local echo
[ roomDataSource sendImage : image success : nil failure : ^ ( NSError * error )
{
// Nothing to do . The image is marked as unsent in the room history by the datasource
MXLogDebug ( @ "[MXKRoomViewController] sendImage failed." ) ;
} ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView sendImage : ( NSData * ) imageData withMimeType : ( NSString * ) mimetype
{
// Let the datasource send it and manage the local echo
[ roomDataSource sendImage : imageData mimeType : mimetype success : nil failure : ^ ( NSError * error )
{
// Nothing to do . The image is marked as unsent in the room history by the datasource
MXLogDebug ( @ "[MXKRoomViewController] sendImage with mimetype failed." ) ;
} ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView sendVideo : ( NSURL * ) videoLocalURL withThumbnail : ( UIImage * ) videoThumbnail
{
AVURLAsset * videoAsset = [ AVURLAsset assetWithURL : videoLocalURL ] ;
[ self roomInputToolbarView : toolbarView sendVideoAsset : videoAsset withThumbnail : videoThumbnail ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView sendVideoAsset : ( AVAsset * ) videoAsset withThumbnail : ( UIImage * ) videoThumbnail
{
// Let the datasource send it and manage the local echo
[ roomDataSource sendVideoAsset : videoAsset withThumbnail : videoThumbnail success : nil failure : ^ ( NSError * error )
{
// Nothing to do . The video is marked as unsent in the room history by the datasource
MXLogDebug ( @ "[MXKRoomViewController] sendVideo failed." ) ;
} ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView sendFile : ( NSURL * ) fileLocalURL withMimeType : ( NSString * ) mimetype
{
// Let the datasource send it and manage the local echo
[ roomDataSource sendFile : fileLocalURL mimeType : mimetype success : nil failure : ^ ( NSError * error )
{
// Nothing to do . The file is marked as unsent in the room history by the datasource
MXLogDebug ( @ "[MXKRoomViewController] sendFile failed." ) ;
} ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView presentAlertController : ( UIAlertController * ) alertController
{
[ self dismissKeyboard ] ;
[ self presentViewController : alertController animated : YES completion : nil ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView presentViewController : ( UIViewController * ) viewControllerToPresent
{
[ self dismissKeyboard ] ;
[ self presentViewController : viewControllerToPresent animated : YES completion : nil ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView dismissViewControllerAnimated : ( BOOL ) flag completion : ( void ( ^ ) ( void ) ) completion
{
[ self dismissViewControllerAnimated : flag completion : completion ] ;
}
- ( void ) roomInputToolbarView : ( MXKRoomInputToolbarView * ) toolbarView updateActivityIndicator : ( BOOL ) isAnimating
{
isInputToolbarProcessing = isAnimating ;
if ( isAnimating )
{
[ self startActivityIndicator ] ;
}
else
{
[ self stopActivityIndicator ] ;
}
}
# pragma mark - Typing notification
- ( void ) handleTypingNotification : ( BOOL ) typing
{
NSUInteger notificationTimeoutMS = -1 ;
if ( typing )
{
// Check whether a typing event has been already reported to server ( We wait for the end of the local timout before considering this new event )
if ( typingTimer )
{
// Refresh date of the last observed typing
lastTypingDate = [ [ NSDate alloc ] init ] ;
return ;
}
// No typing event has been yet reported -> share encryption keys if requested
if ( [ MXKAppSettings standardAppSettings ] . outboundGroupSessionKeyPreSharingStrategy = = MXKKeyPreSharingWhenTyping )
{
[ self shareEncryptionKeys ] ;
}
// Launch a timer to prevent sending multiple typing notifications
NSTimeInterval timerTimeout = MXKROOMVIEWCONTROLLER_DEFAULT _TYPING _TIMEOUT _SEC ;
if ( lastTypingDate )
{
NSTimeInterval lastTypingAge = - [ lastTypingDate timeIntervalSinceNow ] ;
if ( lastTypingAge < timerTimeout )
{
// Subtract the time interval since last typing from the timer timeout
timerTimeout - = lastTypingAge ;
}
else
{
timerTimeout = 0 ;
}
}
else
{
// Keep date of this typing event
lastTypingDate = [ [ NSDate alloc ] init ] ;
}
if ( timerTimeout )
{
typingTimer = [ NSTimer scheduledTimerWithTimeInterval : timerTimeout target : self selector : @ selector ( typingTimeout : ) userInfo : self repeats : NO ] ;
// Compute the notification timeout in ms ( consider the double of the local typing timeout )
notificationTimeoutMS = 2000 * MXKROOMVIEWCONTROLLER_DEFAULT _TYPING _TIMEOUT _SEC ;
}
else
{
// This typing event is too old , we will ignore it
typing = NO ;
MXLogDebug ( @ "[MXKRoomVC] Ignore typing event (too old)" ) ;
}
}
else
{
// Cancel any typing timer
[ typingTimer invalidate ] ;
typingTimer = nil ;
// Reset last typing date
lastTypingDate = nil ;
}
MXWeakify ( self ) ;
// Send typing notification to server
[ roomDataSource . room sendTypingNotification : typing
timeout : notificationTimeoutMS
success : ^ {
MXStrongifyAndReturnIfNil ( self ) ;
// Reset last typing date
self -> lastTypingDate = nil ;
} failure : ^ ( NSError * error )
{
MXStrongifyAndReturnIfNil ( self ) ;
MXLogDebug ( @ "[MXKRoomVC] Failed to send typing notification (%d)" , typing ) ;
// Cancel timer ( if any )
[ self -> typingTimer invalidate ] ;
self -> typingTimer = nil ;
} ] ;
}
- ( IBAction ) typingTimeout : ( id ) sender
{
[ typingTimer invalidate ] ;
typingTimer = nil ;
// Check whether a new typing event has been observed
BOOL typing = ( lastTypingDate ! = nil ) ;
// Post a new typing notification
[ self handleTypingNotification : typing ] ;
}
# pragma mark - Attachment handling
- ( void ) showAttachmentInCell : ( UITableViewCell * ) cell
{
[ self dismissKeyboard ] ;
// Retrieve the attachment information from the associated cell data
if ( [ cell isKindOfClass : MXKTableViewCell . class ] )
{
MXKCellData * cellData = ( ( MXKTableViewCell * ) cell ) . mxkCellData ;
// Only ' MXKRoomBubbleCellData ' is supported here for the moment .
if ( [ cellData isKindOfClass : MXKRoomBubbleCellData . class ] )
{
MXKRoomBubbleCellData * bubbleData = ( MXKRoomBubbleCellData * ) cellData ;
MXKAttachment * selectedAttachment = bubbleData . attachment ;
if ( bubbleData . isAttachmentWithThumbnail )
{
// The attachments viewer is opened only on a valid attachment . It does not display the stickers .
if ( selectedAttachment . eventSentState = = MXEventSentStateSent && selectedAttachment . type ! = MXKAttachmentTypeSticker )
{
// Note : the stickers are presently excluded from the attachments list returned by the room dataSource .
NSArray * attachmentsWithThumbnail = self . roomDataSource . attachmentsWithThumbnail ;
MXKAttachmentsViewController * attachmentsViewer ;
// Present an attachment viewer
if ( attachmentsViewerClass )
{
attachmentsViewer = [ attachmentsViewerClass animatedAttachmentsViewControllerWithSourceViewController : self ] ;
}
else
{
attachmentsViewer = [ MXKAttachmentsViewController animatedAttachmentsViewControllerWithSourceViewController : self ] ;
}
attachmentsViewer . delegate = self ;
attachmentsViewer . complete = ( [ roomDataSource . timeline canPaginate : MXTimelineDirectionBackwards ] = = NO ) ;
attachmentsViewer . hidesBottomBarWhenPushed = YES ;
[ attachmentsViewer displayAttachments : attachmentsWithThumbnail focusOn : selectedAttachment . eventId ] ;
// Keep here the image view used to display the attachment in the selected cell .
// Note : Only ` MXKRoomBubbleTableViewCell` and ` MXKSearchTableViewCell` are supported for the moment .
if ( [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
self . openedAttachmentImageView = ( ( MXKRoomBubbleTableViewCell * ) cell ) . attachmentView . imageView ;
}
else if ( [ cell isKindOfClass : MXKSearchTableViewCell . class ] )
{
self . openedAttachmentImageView = ( ( MXKSearchTableViewCell * ) cell ) . attachmentImageView . imageView ;
}
self . openedAttachmentEventId = selectedAttachment . eventId ;
// "Initializing" closedAttachmentEventId so it is equal to openedAttachmentEventId at the beginning
self . closedAttachmentEventId = self . openedAttachmentEventId ;
if ( @ available ( iOS 13.0 , * ) )
{
attachmentsViewer . modalPresentationStyle = UIModalPresentationFullScreen ;
}
[ self presentViewController : attachmentsViewer animated : YES completion : nil ] ;
self . attachmentsViewer = attachmentsViewer ;
}
else
{
// Let ' s the application do something
MXLogDebug ( @ "[MXKRoomVC] showAttachmentInCell on an unsent media" ) ;
}
}
else if ( selectedAttachment . type = = MXKAttachmentTypeFile || selectedAttachment . type = = MXKAttachmentTypeAudio )
{
// Start activity indicator as feedback on file selection .
[ self startActivityIndicator ] ;
[ selectedAttachment prepareShare : ^ ( NSURL * fileURL ) {
[ self stopActivityIndicator ] ;
MXWeakify ( self ) ;
void ( ^ viewAttachment ) ( void ) = ^ ( ) {
MXStrongifyAndReturnIfNil ( self ) ;
if ( ! [ self canPreviewFileAttachment : selectedAttachment withLocalFileURL : fileURL ] )
{
// When we don ' t support showing a preview for a file , show a share
// sheet if allowed , otherwise display an error to inform the user .
if ( self . allowActionsInDocumentPreview )
{
UIActivityViewController * shareSheet = [ [ UIActivityViewController alloc ] initWithActivityItems : @ [ fileURL ]
applicationActivities : nil ] ;
MXWeakify ( self ) ;
shareSheet . completionWithItemsHandler = ^ ( UIActivityType activityType , BOOL completed , NSArray * returnedItems , NSError * activityError ) {
MXStrongifyAndReturnIfNil ( self ) ;
[ selectedAttachment onShareEnded ] ;
self -> currentSharedAttachment = nil ;
} ;
self -> currentSharedAttachment = selectedAttachment ;
[ self presentViewController : shareSheet animated : YES completion : nil ] ;
}
else
{
UIAlertController * alert = [ UIAlertController alertControllerWithTitle : MatrixKitL10n . attachmentUnsupportedPreviewTitle
message : MatrixKitL10n . attachmentUnsupportedPreviewMessage
preferredStyle : UIAlertControllerStyleAlert ] ;
MXWeakify ( self ) ;
[ alert addAction : [ UIAlertAction actionWithTitle : MatrixKitL10n . ok style : UIAlertActionStyleCancel handler : ^ ( UIAlertAction * _Nonnull action ) {
MXStrongifyAndReturnIfNil ( self ) ;
[ selectedAttachment onShareEnded ] ;
self -> currentAlert = nil ;
} ] ] ;
[ self presentViewController : alert animated : YES completion : nil ] ;
self -> currentAlert = alert ;
}
return ;
}
if ( self . allowActionsInDocumentPreview )
{
// We could get rid of this part of code and use only a MXKPreviewViewController
// Nevertheless , MXKRoomViewController is compliant to UIDocumentInteractionControllerDelegate
// and remove all this code could have effect on some custom implementations .
self -> documentInteractionController = [ UIDocumentInteractionController interactionControllerWithURL : fileURL ] ;
[ self -> documentInteractionController setDelegate : self ] ;
self -> currentSharedAttachment = selectedAttachment ;
if ( ! [ self -> documentInteractionController presentPreviewAnimated : YES ] )
{
if ( ! [ self -> documentInteractionController presentOptionsMenuFromRect : self . view . frame inView : self . view animated : YES ] )
{
self -> documentInteractionController = nil ;
[ selectedAttachment onShareEnded ] ;
self -> currentSharedAttachment = nil ;
}
}
}
else
{
self -> currentSharedAttachment = selectedAttachment ;
[ MXKPreviewViewController presentFrom : self fileUrl : fileURL allowActions : self . allowActionsInDocumentPreview delegate : self ] ;
}
} ;
if ( self -> roomDataSource . mxSession . crypto
&& [ selectedAttachment . contentInfo [ @ "mimetype" ] isEqualToString : @ "text/plain" ]
&& [ MXMegolmExportEncryption isMegolmKeyFile : fileURL ] )
{
// The file is a megolm key file
// Ask the user if they wants to view the file as a classic file attachment
// or open an import process
[ self -> currentAlert dismissViewControllerAnimated : NO completion : nil ] ;
__weak typeof ( self ) weakSelf = self ;
UIAlertController * keysPrompt = [ UIAlertController alertControllerWithTitle : @ ""
message : [ MatrixKitL10n attachmentE2eKeysFilePrompt ]
preferredStyle : UIAlertControllerStyleAlert ] ;
[ keysPrompt addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n view ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
// View file content
if ( weakSelf )
{
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
viewAttachment ( ) ;
}
} ] ] ;
[ keysPrompt addAction : [ UIAlertAction actionWithTitle : [ MatrixKitL10n attachmentE2eKeysImport ]
style : UIAlertActionStyleDefault
handler : ^ ( UIAlertAction * action ) {
if ( weakSelf )
{
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
// Show the keys import dialog
self -> importView = [ [ MXKEncryptionKeysImportView alloc ] initWithMatrixSession : self -> roomDataSource . mxSession ] ;
self -> currentAlert = self -> importView . alertController ;
[ self -> importView showInViewController : self toImportKeys : fileURL onComplete : ^ {
if ( weakSelf )
{
typeof ( self ) self = weakSelf ;
self -> currentAlert = nil ;
self -> importView = nil ;
}
} ] ;
}
} ] ] ;
[ self presentViewController : keysPrompt animated : YES completion : nil ] ;
self -> currentAlert = keysPrompt ;
}
else
{
viewAttachment ( ) ;
}
} failure : ^ ( NSError * error ) {
[ self stopActivityIndicator ] ;
// Notify MatrixKit user
[ [ NSNotificationCenter defaultCenter ] postNotificationName : kMXKErrorNotification object : error ] ;
} ] ;
if ( [ cell isKindOfClass : MXKRoomBubbleTableViewCell . class ] )
{
// Start animation in case of download
MXKRoomBubbleTableViewCell * roomBubbleTableViewCell = ( MXKRoomBubbleTableViewCell * ) cell ;
[ roomBubbleTableViewCell startProgressUI ] ;
}
}
}
}
}
- ( BOOL ) canPreviewFileAttachment : ( MXKAttachment * ) attachment withLocalFileURL : ( NSURL * ) localFileURL
{
// Sanity check .
if ( ! [ NSFileManager . defaultManager isReadableFileAtPath : localFileURL . path ] )
{
return NO ;
}
if ( UIDevice . currentDevice . systemVersion . floatValue >= 13 )
{
return YES ;
}
MXKUTI * attachmentUTI = attachment . uti ;
MXKUTI * fileUTI = [ [ MXKUTI alloc ] initWithLocalFileURL : localFileURL ] ;
if ( ! attachmentUTI || ! fileUTI )
{
return NO ;
}
NSArray < MXKUTI * > * unsupportedUTIs = @ [ MXKUTI . html , MXKUTI . xml , MXKUTI . svg ] ;
if ( [ attachmentUTI conformsToAnyOf : unsupportedUTIs ] || [ fileUTI conformsToAnyOf : unsupportedUTIs ] )
{
return NO ;
}
return YES ;
}
# pragma mark - MXKAttachmentsViewControllerDelegate
- ( BOOL ) attachmentsViewController : ( MXKAttachmentsViewController * ) attachmentsViewController paginateAttachmentBefore : ( NSString * ) eventId
{
[ self triggerAttachmentBackPagination : eventId ] ;
return [ self . roomDataSource . timeline canPaginate : MXTimelineDirectionBackwards ] ;
}
- ( void ) displayedNewAttachmentWithEventId : ( NSString * ) eventId {
self . closedAttachmentEventId = eventId ;
}
# pragma mark - MXKRoomActivitiesViewDelegate
- ( void ) didChangeHeight : ( MXKRoomActivitiesView * ) roomActivitiesView oldHeight : ( CGFloat ) oldHeight newHeight : ( CGFloat ) newHeight
{
// We will scroll to bottom if the bottom of the table is currently visible
BOOL shouldScrollToBottom = [ self isBubblesTableScrollViewAtTheBottom ] ;
// Apply height change to constraints
_roomActivitiesContainerHeightConstraint . constant = newHeight ;
_bubblesTableViewBottomConstraint . constant + = newHeight - oldHeight ;
// Force to render the view
[ self . view layoutIfNeeded ] ;
if ( shouldScrollToBottom )
{
[ self scrollBubblesTableViewToBottomAnimated : YES ] ;
}
}
# pragma mark - MXKPreviewViewControllerDelegate
- ( void ) previewViewControllerDidEndPreview : ( MXKPreviewViewController * ) controller
{
if ( currentSharedAttachment )
{
[ currentSharedAttachment onShareEnded ] ;
currentSharedAttachment = nil ;
}
}
# pragma mark - UIDocumentInteractionControllerDelegate
- ( UIViewController * ) documentInteractionControllerViewControllerForPreview : ( UIDocumentInteractionController * ) controller
{
return self ;
}
// Preview presented / dismissed on document . Use to set up any HI underneath .
- ( void ) documentInteractionControllerWillBeginPreview : ( UIDocumentInteractionController * ) controller
{
documentInteractionController = controller ;
}
- ( void ) documentInteractionControllerDidEndPreview : ( UIDocumentInteractionController * ) controller
{
documentInteractionController = nil ;
if ( currentSharedAttachment )
{
[ currentSharedAttachment onShareEnded ] ;
currentSharedAttachment = nil ;
}
}
- ( void ) documentInteractionControllerDidDismissOptionsMenu : ( UIDocumentInteractionController * ) controller
{
documentInteractionController = nil ;
if ( currentSharedAttachment )
{
[ currentSharedAttachment onShareEnded ] ;
currentSharedAttachment = nil ;
}
}
- ( void ) documentInteractionControllerDidDismissOpenInMenu : ( UIDocumentInteractionController * ) controller
{
documentInteractionController = nil ;
if ( currentSharedAttachment )
{
[ currentSharedAttachment onShareEnded ] ;
currentSharedAttachment = 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 : UIActivityIndicatorViewStyleGray ] ;
[ spinner sizeToFit ] ;
spinner . hidesWhenStopped = NO ;
spinner . backgroundColor = [ UIColor clearColor ] ;
[ spinner startAnimating ] ;
// no need to manage constraints here
// IOS defines them .
// since IOS7 the spinner is centered so need to create a background and add it .
_bubblesTableView . tableFooterView = reconnectingView = spinner ;
}
}
- ( void ) removeReconnectingView
{
if ( reconnectingView && ! restartConnection )
{
_bubblesTableView . tableFooterView = 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 ( roomDataSource . isLive && ! reconnectingView )
{
// detect if the user scrolls over the tableview bottom
restartConnection = (
( ( scrollView . contentSize . height < scrollView . frame . size . height ) && ( scrollView . contentOffset . y > 128 ) )
||
( ( scrollView . contentSize . height > scrollView . frame . size . height ) && ( scrollView . contentOffset . y + scrollView . frame . size . height ) > ( scrollView . contentSize . height + 128 ) ) ) ;
if ( restartConnection )
{
// wait that list decelerate to display / hide it
[ self addReconnectingView ] ;
}
}
}
/ * *
Restarts the current connection if it is required .
The 0.3 s 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 ( roomDataSource . isLive && restartConnection )
{
// display at least 0.3 s 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 .
} ) ;
}
}
# pragma mark - MXKSourceAttachmentAnimatorDelegate
- ( UIImageView * ) originalImageView {
if ( [ self . openedAttachmentEventId isEqualToString : self . closedAttachmentEventId ] ) {
return self . openedAttachmentImageView ;
}
return nil ;
}
- ( CGRect ) convertedFrameForOriginalImageView {
if ( [ self . openedAttachmentEventId isEqualToString : self . closedAttachmentEventId ] ) {
return [ self . openedAttachmentImageView convertRect : self . openedAttachmentImageView . frame toView : nil ] ;
}
// default frame which will be used if the user scrolls to other attachments in MXKAttachmentsViewController
return CGRectMake ( CGRectGetWidth ( self . view . frame ) / 2 , 0.0 , 0.0 , 0.0 ) ;
}
# pragma mark - Encryption key sharing
- ( void ) shareEncryptionKeys
{
__block NSString * roomId = roomDataSource . roomId ;
[ roomDataSource . mxSession . crypto ensureEncryptionInRoom : roomId success : ^ {
MXLogDebug ( @ "[MXKRoomViewController] Key shared for room: %@" , roomId ) ;
} failure : ^ ( NSError * error ) {
MXLogDebug ( @ "[MXKRoomViewController] Failed to share key for room %@: %@" , roomId , error ) ;
} ] ;
}
@ end