/*
 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 "MXKSwiftHeader.h"

#import "MXKPreviewViewController.h"

// Constant used to determine whether an event is visible at the bottom of the tableview, based on its visible height
static const CGFloat kCellVisibilityMinimumHeight = 8.0;

@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.3s
    _resizeComposerAnimationDuration = 0.3;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    if (BuildSettings.newAppLayoutEnabled)
    {
        [self vc_setLargeTitleDisplayMode: UINavigationItemLargeTitleDisplayModeNever];
    }
    
    // Check whether the view controller has been pushed via storyboard
    if (!_bubblesTableView)
    {
        // Instantiate view controller objects
        [[[self class] nib] instantiateWithOwner:self options:nil];
    }
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated"
    // 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.0f
                                                                               constant:0.0f];
    #pragma clang diagnostic pop
    
    _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];

    [self.navigationController setToolbarHidden:YES animated:NO];
    
    // 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
    dispatch_async(dispatch_get_main_queue(), ^{
        [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 setPartialContent:roomDataSource.partialAttributedTextMessage];
    }
    
    if (!hasAppearedOnce)
    {
        hasAppearedOnce = YES;
    }
    
    //  Mark all messages as read when the room is displayed
    [self.roomDataSource.room.summary markAllAsReadLocally];
    
    [self updateCurrentEventIdAtTableBottom:YES];
    
    if (!self.isContextPreview)
    {
        [self.roomDataSource.room resetUnread];
    }
}

- (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;
    });
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
// 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;
    });
}
#pragma clang diagnostic pop

- (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.inputAccessoryViewForKeyboard.superview;
    }
    
    // Report the keyboard view in order to track keyboard frame changes
    self.keyboardView = keyboardView;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (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;
}
#pragma clang diagnostic pop

- (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.isPeeking)
        {
            // Remove the input toolbar 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 = [VectorL10n roomErrorTimelineEventNotFoundTitle];
                errorMessage = [VectorL10n roomErrorTimelineEventNotFound];
            }
            else
            {
                errorTitle = [VectorL10n roomErrorCannotLoadTimeline];
                errorMessage = mxError.error;
            }
        }
        else
        {
            errorTitle = [VectorL10n 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:[VectorL10n 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;
    }
    
    UserIndicatorCancel cancelIndicator = [self.userIndicatorStore presentLoadingWithLabel:[VectorL10n joining] isInteractionBlocking:YES];
    joinRoomRequest = [roomDataSource.room join:^{
        
        self->joinRoomRequest = nil;
        cancelIndicator();
        
        [self triggerInitialBackPagination];
        
        if (completion)
        {
            completion(MXKRoomViewControllerJoinRoomResultSuccess);
        }
        
    } failure:^(NSError *error) {
        cancelIndicator();
        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;
    }
    
    UserIndicatorCancel cancelIndicator = [self.userIndicatorStore presentLoadingWithLabel:[VectorL10n joining] isInteractionBlocking:YES];
    void (^success)(MXRoom *room) = ^(MXRoom *room) {
        
        self->joinRoomRequest = nil;
        cancelIndicator();
        
        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) {
        cancelIndicator();
        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 = [VectorL10n roomErrorJoinFailedEmptyRoom];
    }
    
    MXWeakify(self);
    [self->currentAlert dismissViewControllerAnimated:NO completion:nil];
    
    UIAlertController *errorAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomErrorJoinFailedTitle]
                                                                        message:msg
                                                                 preferredStyle:UIAlertControllerStyleAlert];
    
    [errorAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n 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
                  andLatestRoomState:nil
                  error:&error];
        if (error != MXKEventFormatterErrorNone)
        {
            reason = nil;
        }
    }
    
    if (!reason.length)
    {
        if (self.roomDataSource.room.isDirect)
        {
            reason = [VectorL10n roomLeftForDm];
        }
        else
        {
            reason = [VectorL10n 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
    UILabel *leftRoomReasonLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, self.view.frame.size.width - 20, 70)];
    leftRoomReasonLabel.numberOfLines = 0;
    leftRoomReasonLabel.text = reason;
    leftRoomReasonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    _bubblesTableView.tableHeaderView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 80)];
    [_bubblesTableView.tableHeaderView addSubview:leftRoomReasonLabel];
    [_bubblesTableView reloadData];
    
    _leftRoomReasonLabel = leftRoomReasonLabel;
    
    [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
{
    if ([self.titleView.class isEqual:roomTitleViewClass]) {
        return;
    }
    
    // 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];
    }
    
    self.navigationItem.titleView = 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.isPeeking)
    {
        // Do not show the input toolbar if the displayed timeline 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 instantiateRoomInputToolbarView];
        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.0f
                                                                                constant:0.0f]];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeTop
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeTop
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeLeading
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeLeading
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
        [_roomInputToolbarContainer addConstraint:[NSLayoutConstraint constraintWithItem:_roomInputToolbarContainer
                                                                               attribute:NSLayoutAttributeTrailing
                                                                               relatedBy:NSLayoutRelationEqual
                                                                                  toItem:inputToolbarView
                                                                               attribute:NSLayoutAttributeTrailing
                                                                              multiplier:1.0f
                                                                                constant:0.0f]];
    }
    
    [_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.0f
                                                                          constant:0.0f];
        
        
        NSLayoutConstraint* leadingConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                             attribute:NSLayoutAttributeLeading
                                                                             relatedBy:NSLayoutRelationEqual
                                                                                toItem:activitiesView
                                                                             attribute:NSLayoutAttributeLeading
                                                                            multiplier:1.0f
                                                                              constant:0.0f];
        
        NSLayoutConstraint* widthConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                           attribute:NSLayoutAttributeWidth
                                                                           relatedBy:NSLayoutRelationEqual
                                                                              toItem:activitiesView
                                                                           attribute:NSLayoutAttributeWidth
                                                                          multiplier:1.0f
                                                                            constant:0.0f];
        
        NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:_roomActivitiesContainer
                                                                            attribute:NSLayoutAttributeHeight
                                                                            relatedBy:NSLayoutRelationEqual
                                                                               toItem:activitiesView
                                                                            attribute:NSLayoutAttributeHeight
                                                                           multiplier:1.0f
                                                                             constant:0.0f];
        

        [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)sendAsIRCStyleCommandIfPossible:(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;

    NSString* kMXKSlashCmdChangeDisplayName = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeDisplayName];
    NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
    NSString* kMXKSlashCmdPartRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandPartRoom];
    NSString* kMXKSlashCmdChangeRoomTopic = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandChangeRoomTopic];

    
    if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]])
    {
        // 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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeDisplayName];
        }
    }
    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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
        }
    }
    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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandPartRoom];
        }
    }
    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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandChangeRoomTopic];
        }
    }
    else if ([string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandDiscardSession]])
    {
        [roomDataSource.mxSession.crypto discardOutboundGroupSessionForRoomWithRoomId:roomDataSource.roomId onComplete:^{
            MXLogDebug(@"[MXKRoomVC] Manually discarded outbound group session");
        }];
    }
    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:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandInviteUser]])
        {
            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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandInviteUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandKickUser]])
        {
            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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandKickUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandBanUser]])
        {
            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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandBanUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandUnbanUser]])
        {
            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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandUnbanUser];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandSetUserPowerLevel]])
        {
            // 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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandSetUserPowerLevel];
            }
        }
        else if ([cmd isEqualToString:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandResetUserPowerLevel]])
        {
            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 = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandResetUserPowerLevel];
            }
        }
        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

- (BOOL)canStopActivityIndicator {
    // Keep the loading wheel displayed while we are joining the room
    if (joinRoomRequest)
    {
        return NO;
    }
    
    // Check internal processes before stopping the loading wheel
    if (isPaginationInProgress || isInputToolbarProcessing)
    {
        // Keep activity indicator running
        return NO;
    }
    
    return [super canStopActivityIndicator];
}

#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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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;
    }
    
    UserIndicatorCancel cancelIndicator = [self.userIndicatorStore presentLoadingWithLabel:[VectorL10n loading] isInteractionBlocking:NO];
    
    // 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];
        }
        
        if (cancelIndicator) {
            cancelIndicator();
        }

    } 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;

        if (cancelIndicator) {
            cancelIndicator();
        }
    }];
}

- (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.5f;
    
    // Add the view and define edge constraints
    [self.view addSubview:eventDetailsView];
    
    self->eventDetailsView = eventDetailsView;
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated"
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:eventDetailsView
                                                          attribute:NSLayoutAttributeTop
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.topLayoutGuide
                                                          attribute:NSLayoutAttributeBottom
                                                         multiplier:1.0f
                                                           constant:10.0f]];
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:eventDetailsView
                                                          attribute:NSLayoutAttributeBottom
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:self.bottomLayoutGuide
                                                          attribute:NSLayoutAttributeTop
                                                         multiplier:1.0f
                                                           constant:-10.0f]];
    #pragma clang diagnostic pop
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
                                                          attribute:NSLayoutAttributeLeading
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:eventDetailsView
                                                          attribute:NSLayoutAttributeLeading
                                                         multiplier:1.0f
                                                           constant:-10.0f]];
    
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
                                                          attribute:NSLayoutAttributeTrailing
                                                          relatedBy:NSLayoutRelationEqual
                                                             toItem:eventDetailsView
                                                          attribute:NSLayoutAttributeTrailing
                                                         multiplier:1.0f
                                                           constant:10.0f]];
    [self.view setNeedsUpdateConstraints];
}

- (void)promptUserToResendEvent:(NSString *)eventId
{
    MXEvent *event = [roomDataSource eventWithEventId:eventId];
    
    MXLogDebug(@"[MXKRoomViewController] promptUserToResendEvent: %@", event);
    
    if (event && event.eventType == MXEventTypeRoomMessage)
    {
        NSString *msgtype = event.content[kMXMessageTypeKey];
        
        NSString* textMessage;
        if ([msgtype isEqualToString:kMXMessageTypeText])
        {
            textMessage = event.content[kMXMessageBodyKey];
        }
        
        // Show a confirmation popup to the end user
        if (currentAlert)
        {
            [currentAlert dismissViewControllerAnimated:NO completion:nil];
        }
        
        __weak typeof(self) weakSelf = self;
        
        UIAlertController *resendAlert = [UIAlertController alertControllerWithTitle:[VectorL10n resendMessage]
                                                                             message:textMessage
                                                                      preferredStyle:UIAlertControllerStyleAlert];
        
        [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction * action) {
                                                           
                                                           typeof(self) self = weakSelf;
                                                           self->currentAlert = nil;
                                                           
                                                       }]];
        
        [resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n 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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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) {
            // If we need to scroll to the bottom after the reload, layout refresh needs to be triggered,
            // otherwise contentSize of the table view will not be up-to-date
            // e.g. https://stackoverflow.com/a/31324129
            [_bubblesTableView layoutIfNeeded];
        }
    }
    
    if (shouldScrollToBottom)
    {
        [self scrollBubblesTableViewToBottomAnimated:NO];
    }
    
    return shouldScrollToBottom;
}

- (void)updateCurrentEventIdAtTableBottom:(BOOL)acknowledge
{
    // Do not update events if the controller is used as context menu preview.
    if (self.isContextPreview)
    {
        return;
    }
    
    // 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 += kCellVisibilityMinimumHeight;
        
        // 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.
                        if (self.navigationController.viewControllers.lastObject == self)
                        {
                            // Check if the selected event is eligible to be the new read marker position too
                            if (!bubbleData.collapsed && [self eligibleForReadMarkerUpdate:component.event])
                            {
                                BOOL updateRoomReadMarker = _updateRoomReadMarker && [self isEventPosteriorToCurrentReadMarker:component.event];
                                // Acknowledge this event and update the read marker if needed
                                [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateRoomReadMarker];
                            }
                            else
                            {
                                // Acknowledge only this event. The read marker is handled separately
                                [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:NO];

                                if (_updateRoomReadMarker)
                                {
                                    // Try to find the best event for the new read marker position
                                    [self updateReadMarkerEvent];
                                }
                            }
                        }
                    }
                    break;
                }
                // else we consider the previous cell.
            }
        }
    }
}

- (BOOL)eligibleForReadMarkerUpdate:(MXEvent *)event {
    // Prevent the readmarker to be placed on a relatesTo or a redaction event
    if (event.relatesTo || event.redacts)
    {
        return NO;
    }
    
    return YES;
}

- (BOOL)isEventPosteriorToCurrentReadMarker:(MXEvent *)event {
    if (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.
        return currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= event.originServerTs);
    }
    return YES;
}

/// Try to update the read marker by looking for an eligible event displayed at the bottom of the tableview
- (void)updateReadMarkerEvent
{
    // 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 += kCellVisibilityMinimumHeight;
    
    // 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)
        {
            continue;
        }

        if (![cell isKindOfClass:MXKRoomBubbleTableViewCell.class])
        {
            continue;
        }
        MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
        MXKRoomBubbleCellData *bubbleData = roomBubbleTableViewCell.bubbleData;
        if (!bubbleData)
        {
            continue;
        }
        
        // Prevent to place the read marker on a collapsed cell
        if (bubbleData.collapsed)
        {
            continue;
        }
        
        // 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];
            
            // Prevent the read marker to be placed on an unsupported event (e.g. redactions, reactions, ...)
            if (![self eligibleForReadMarkerUpdate:component.event])
            {
                continue;
            }
            
            // Check whether the bottom part of the component is visible.
            CGFloat pos = cell.frame.origin.y + bottomPositionY;
            if (pos <= contentBottomOffsetY)
            {
                // We found the component
                // Check whether the read marker must be updated.
                if ([self isEventPosteriorToCurrentReadMarker:component.event])
                {
                    // Move the read marker to this event
                    [roomDataSource.room moveReadMarkerToEventId:component.event.eventId];
                }
                
                return;
            }
            
            // Prepare the bottom position for the next component
            bottomPositionY = roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y;
        }
        
        // else we consider the previous cell.
    }
}

#pragma mark - MXKDataSourceDelegate

- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
    return nil;
}

- (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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
    {
        [self showAttachmentInCell:(MXKRoomBubbleTableViewCell *)cell];
    }
    else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnProgressView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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:[VectorL10n attachmentCancelDownload]
                                                                          preferredStyle:UIAlertControllerStyleAlert];
            
            [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n no]
                                                            style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction * action) {
                                                               
                                                               typeof(self) self = weakSelf;
                                                               self->currentAlert = nil;
                                                               
                                                           }]];
            
            [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n 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:[VectorL10n attachmentCancelUpload]
                                                                              preferredStyle:UIAlertControllerStyleAlert];
                
                [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n no]
                                                                style:UIAlertActionStyleDefault
                                                              handler:^(UIAlertAction * action) {
                                                                   
                                                                   typeof(self) self = weakSelf;
                                                                   self->currentAlert = nil;
                                                                   
                                                               }]];
                
                [cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n 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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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:[VectorL10n 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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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:[VectorL10n 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.partialAttributedTextMessage = inputToolbarView.attributedTextMessage;
    }
    
    [self handleTypingState:typing];
}

- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChanged:(CGFloat)height completion:(void (^)(BOOL finished))completion
{
    // This dispatch fixes a simultaneous accesses crash if this gets called twice quickly in succession
    dispatch_async(dispatch_get_main_queue(), ^{
        // 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];

            self->_roomInputToolbarContainerHeightConstraint.constant = height;
            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 sendAsIRCStyleCommandIfPossible: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)handleTypingState:(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;
    }
    
    [self sendTypingNotification:typing timeout:notificationTimeoutMS];
}

- (void)sendTypingNotification:(BOOL)typing timeout:(NSUInteger)notificationTimeoutMS
{
    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 handleTypingState: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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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:VectorL10n.attachmentUnsupportedPreviewTitle
                                                                                               message:VectorL10n.attachmentUnsupportedPreviewMessage
                                                                                        preferredStyle:UIAlertControllerStyleAlert];
                                MXWeakify(self);
                                [alert addAction:[UIAlertAction actionWithTitle:VectorL10n.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:[VectorL10n attachmentE2eKeysFilePrompt]
                                                                                     preferredStyle:UIAlertControllerStyleAlert];
                        
                        [keysPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n view]
                                                                       style:UIAlertActionStyleDefault
                                                                     handler:^(UIAlertAction * action) {
                                                                           
                                                                           // View file content
                                                                           if (weakSelf)
                                                                           {
                                                                               typeof(self) self = weakSelf;
                                                                               self->currentAlert = nil;
                                                                               
                                                                               viewAttachment();
                                                                           }
                                                                           
                                                                       }]];
                        
                        [keysPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n 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] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.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.3s delay is added to avoid flickering if the connection does not require to be restarted.
 */
- (void)managePullToKick:(UIScrollView *)scrollView
{
    // the current connection must be restarted
    if (roomDataSource.isLive && restartConnection)
    {
        // display at least 0.3s the spinner to show to the user that something is pending
        // else the UI is flickering
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            self->restartConnection = NO;
            
            if (![self canReconnect])
            {
                // if the event stream has not been restarted
                // hide the spinner
                [self removeReconnectingView];
            }
            // else wait that onSyncNotification is called.
        });
    }
}

#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
