8340 lines
337 KiB
Objective-C
8340 lines
337 KiB
Objective-C
/*
|
|
Copyright 2018-2024 New Vector Ltd.
|
|
Copyright 2017 Vector Creations Ltd
|
|
Copyright 2014 OpenMarket Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
@import MobileCoreServices;
|
|
|
|
#import "RoomViewController.h"
|
|
|
|
#import "RoomDataSource.h"
|
|
#import "RoomBubbleCellData.h"
|
|
|
|
#import "RoomInputToolbarView.h"
|
|
#import "DisabledRoomInputToolbarView.h"
|
|
|
|
#import "RoomActivitiesView.h"
|
|
|
|
#import "AttachmentsViewController.h"
|
|
|
|
#import "EventDetailsView.h"
|
|
|
|
#import "RoomAvatarTitleView.h"
|
|
#import "ExpandedRoomTitleView.h"
|
|
#import "SimpleRoomTitleView.h"
|
|
#import "PreviewRoomTitleView.h"
|
|
|
|
#import "RoomMemberDetailsViewController.h"
|
|
#import "ContactDetailsViewController.h"
|
|
|
|
#import "SegmentedViewController.h"
|
|
#import "RoomSettingsViewController.h"
|
|
|
|
#import "RoomFilesViewController.h"
|
|
|
|
#import "RoomSearchViewController.h"
|
|
|
|
#import "UsersDevicesViewController.h"
|
|
|
|
#import "ReadReceiptsViewController.h"
|
|
|
|
#import "JitsiViewController.h"
|
|
|
|
#import "RoomEmptyBubbleCell.h"
|
|
#import "RoomMembershipExpandedBubbleCell.h"
|
|
#import "MXKRoomBubbleTableViewCell+Riot.h"
|
|
|
|
#import "AvatarGenerator.h"
|
|
#import "Tools.h"
|
|
#import "WidgetManager.h"
|
|
#import "ShareManager.h"
|
|
|
|
#import "GBDeviceInfo_iOS.h"
|
|
|
|
#import "RoomEncryptedDataBubbleCell.h"
|
|
#import "EncryptionInfoView.h"
|
|
|
|
#import "MXRoom+Riot.h"
|
|
|
|
#import "IntegrationManagerViewController.h"
|
|
#import "WidgetPickerViewController.h"
|
|
#import "StickerPickerViewController.h"
|
|
|
|
#import "EventFormatter.h"
|
|
|
|
#import "SettingsViewController.h"
|
|
#import "SecurityViewController.h"
|
|
|
|
#import "TypingUserInfo.h"
|
|
|
|
#import "MXSDKOptions.h"
|
|
|
|
#import "RoomTimelineCellProvider.h"
|
|
|
|
#import "GeneratedInterface-Swift.h"
|
|
|
|
NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification";
|
|
NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification";
|
|
const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
|
static const int kThreadListBarButtonItemTag = 99;
|
|
static UIEdgeInsets kThreadListBarButtonItemContentInsetsNoDot;
|
|
static UIEdgeInsets kThreadListBarButtonItemContentInsetsDot;
|
|
static CGSize kThreadListBarButtonItemImageSize;
|
|
|
|
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
|
|
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
|
|
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
|
|
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, CompletionSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, ThreadsBetaCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate, RoomParticipantsInviteCoordinatorBridgePresenterDelegate, RoomInputToolbarViewDelegate, ComposerCreateActionListBridgePresenterDelegate>
|
|
{
|
|
|
|
// The preview header
|
|
__weak PreviewRoomTitleView *previewHeader;
|
|
|
|
// The user taps on a user id contained in a message
|
|
MXKContact *selectedContact;
|
|
|
|
// List of members who are typing in the room.
|
|
NSArray *currentTypingUsers;
|
|
|
|
// Typing notifications listener.
|
|
__weak id typingNotifListener;
|
|
|
|
// The position of the first touch down event stored in case of scrolling when the expanded header is visible.
|
|
CGPoint startScrollingPoint;
|
|
|
|
// Missed discussions badge
|
|
NSUInteger missedDiscussionsCount;
|
|
NSUInteger missedHighlightCount;
|
|
UILabel *missedDiscussionsBadgeLabel;
|
|
UIView *missedDiscussionsDotView;
|
|
|
|
// Potential encryption details view.
|
|
__weak EncryptionInfoView *encryptionInfoView;
|
|
|
|
// The list of unknown devices that prevent outgoing messages from being sent
|
|
MXUsersDevicesMap<MXDeviceInfo*> *unknownDevices;
|
|
|
|
// Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar.
|
|
__weak id kAppDelegateDidTapStatusBarNotificationObserver;
|
|
|
|
// Observe kAppDelegateNetworkStatusDidChangeNotification to handle network status change.
|
|
__weak id kAppDelegateNetworkStatusDidChangeNotificationObserver;
|
|
|
|
// Observers to manage MXSession state (and sync errors)
|
|
__weak id kMXSessionStateDidChangeObserver;
|
|
|
|
// Observers to manage ongoing conference call banner
|
|
__weak id kMXCallStateDidChangeObserver;
|
|
__weak id kMXCallManagerConferenceStartedObserver;
|
|
__weak id kMXCallManagerConferenceFinishedObserver;
|
|
|
|
// Observers to manage widgets
|
|
__weak id kMXKWidgetManagerDidUpdateWidgetObserver;
|
|
|
|
// Observer kMXRoomSummaryDidChangeNotification to keep updated the missed discussion count
|
|
__weak id mxRoomSummaryDidChangeObserver;
|
|
|
|
// Observer for removing the re-request explanation/waiting dialog
|
|
__weak id mxEventDidDecryptNotificationObserver;
|
|
|
|
// The table view cell in which the read marker is displayed (nil by default).
|
|
MXKRoomBubbleTableViewCell *readMarkerTableViewCell;
|
|
|
|
// Tell whether the view controller is appeared or not.
|
|
BOOL isAppeared;
|
|
|
|
// A flag indicating whether a room has been left
|
|
BOOL isRoomLeft;
|
|
|
|
// The last known frame of the view used to detect whether size-related layout change is needed
|
|
CGRect lastViewBounds;
|
|
|
|
// Tell whether the room has a Jitsi call or not.
|
|
BOOL hasJitsiCall;
|
|
|
|
// The right bar button items back up.
|
|
NSArray<UIBarButtonItem *> *rightBarButtonItems;
|
|
|
|
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
|
|
__weak id kThemeServiceDidChangeThemeNotificationObserver;
|
|
|
|
// Observe URL preview updates to refresh cells.
|
|
__weak id URLPreviewDidUpdateNotificationObserver;
|
|
|
|
// Listener for `m.room.tombstone` event type
|
|
__weak id tombstoneEventNotificationsListener;
|
|
|
|
// Homeserver notices
|
|
MXServerNotices *serverNotices;
|
|
|
|
// Formatted body parser for events
|
|
FormattedBodyParser *formattedBodyParser;
|
|
|
|
// Time to display notification content in the timeline
|
|
MXTaskProfile *notificationTaskProfile;
|
|
|
|
// Observe kMXEventTypeStringRoomMember events
|
|
__weak id roomMemberEventListener;
|
|
}
|
|
|
|
@property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView;
|
|
|
|
|
|
@property (nonatomic, strong) RoomContextualMenuViewController *roomContextualMenuViewController;
|
|
@property (nonatomic, strong) RoomContextualMenuPresenter *roomContextualMenuPresenter;
|
|
@property (nonatomic, strong) MXKErrorAlertPresentation *errorPresenter;
|
|
@property (nonatomic, strong) NSAttributedString *textMessageBeforeEditing;
|
|
@property (nonatomic, strong) NSString *htmlTextBeforeEditing;
|
|
@property (nonatomic, strong) EditHistoryCoordinatorBridgePresenter *editHistoryPresenter;
|
|
@property (nonatomic, strong) MXKDocumentPickerPresenter *documentPickerPresenter;
|
|
@property (nonatomic, strong) EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter;
|
|
@property (nonatomic, strong) ReactionHistoryCoordinatorBridgePresenter *reactionHistoryCoordinatorBridgePresenter;
|
|
@property (nonatomic, strong) CameraPresenter *cameraPresenter;
|
|
@property (nonatomic, strong) MediaPickerCoordinatorBridgePresenter *mediaPickerPresenter;
|
|
@property (nonatomic, strong) RoomMessageURLParser *roomMessageURLParser;
|
|
@property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter;
|
|
@property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter;
|
|
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
|
|
@property (nonatomic, strong) RoomParticipantsInviteCoordinatorBridgePresenter *participantsInvitePresenter;
|
|
@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter;
|
|
@property (nonatomic, strong) ThreadsBetaCoordinatorBridgePresenter *threadsBetaBridgePresenter;
|
|
@property (nonatomic, strong) SlidingModalPresenter *threadsNoticeModalPresenter;
|
|
@property (nonatomic, strong) ComposerCreateActionListBridgePresenter *composerCreateActionListBridgePresenter;
|
|
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
|
|
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
|
|
@property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden;
|
|
|
|
@property (nonatomic, strong) VoiceMessageController *voiceMessageController;
|
|
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;
|
|
|
|
@property (nonatomic, strong) ShareManager *shareManager;
|
|
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
|
|
|
|
@property (nonatomic, strong) CompletionSuggestionCoordinatorBridge *completionSuggestionCoordinator;
|
|
@property (nonatomic, weak) IBOutlet UIView *completionSuggestionContainerView;
|
|
|
|
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
|
|
|
|
// The direct chat target user. The room timeline is presented without an actual room until the direct chat is created
|
|
@property (nonatomic, nullable, strong) MXUser *directChatTargetUser;
|
|
|
|
// When layout of the screen changes (e.g. height), we no longer know whether
|
|
// to autoscroll to the bottom again or not. Instead we need to capture the
|
|
// scroll state just before the layout change, and restore it after the layout.
|
|
@property (nonatomic) BOOL wasScrollAtBottomBeforeLayout;
|
|
|
|
// Check if we should wait for other participants
|
|
@property (nonatomic, readonly) BOOL shouldWaitForOtherParticipants;
|
|
|
|
@end
|
|
|
|
@implementation RoomViewController
|
|
@synthesize roomPreviewData;
|
|
|
|
#pragma mark - Class methods
|
|
|
|
+ (void)initialize
|
|
{
|
|
kThreadListBarButtonItemContentInsetsNoDot = UIEdgeInsetsMake(0, 8, 0, 8);
|
|
kThreadListBarButtonItemContentInsetsDot = UIEdgeInsetsMake(0, 8, 6, 8);
|
|
kThreadListBarButtonItemImageSize = CGSizeMake(21, 21);
|
|
}
|
|
|
|
+ (UINib *)nib
|
|
{
|
|
return [UINib nibWithNibName:NSStringFromClass(self.class)
|
|
bundle:[NSBundle bundleForClass:self.class]];
|
|
}
|
|
|
|
+ (instancetype)roomViewController
|
|
{
|
|
RoomViewController *controller = [[[self class] alloc] initWithNibName:NSStringFromClass(self.class)
|
|
bundle:[NSBundle bundleForClass:self.class]];
|
|
controller.displayConfiguration = [RoomDisplayConfiguration default];
|
|
return controller;
|
|
}
|
|
|
|
+ (instancetype)instantiateWithConfiguration:(RoomDisplayConfiguration *)configuration
|
|
{
|
|
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
|
|
NSString *storyboardId = [NSString stringWithFormat:@"%@StoryboardId", self.className];
|
|
RoomViewController *controller = [storyboard instantiateViewControllerWithIdentifier:storyboardId];
|
|
controller.displayConfiguration = configuration;
|
|
return controller;
|
|
}
|
|
|
|
+ (NSString *)className
|
|
{
|
|
NSString *result = NSStringFromClass(self.class);
|
|
if ([result containsString:@"."])
|
|
{
|
|
result = [result componentsSeparatedByString:@"."].lastObject;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
|
|
{
|
|
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
|
if (self)
|
|
{
|
|
// Disable auto join
|
|
self.autoJoinInvitedRoom = NO;
|
|
|
|
// Disable auto scroll to bottom on keyboard presentation
|
|
self.scrollHistoryToTheBottomOnKeyboardPresentation = NO;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
|
{
|
|
self = [super initWithCoder:aDecoder];
|
|
if (self)
|
|
{
|
|
// Disable auto join
|
|
self.autoJoinInvitedRoom = NO;
|
|
|
|
// Disable auto scroll to bottom on keyboard presentation
|
|
self.scrollHistoryToTheBottomOnKeyboardPresentation = NO;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)finalizeInit
|
|
{
|
|
[super finalizeInit];
|
|
|
|
[self registerPillAttachmentViewProviderIfNeeded];
|
|
self.resizeComposerAnimationDuration = kResizeComposerAnimationDuration;
|
|
|
|
// Setup `MXKViewControllerHandling` properties
|
|
self.enableBarTintColorStatusChange = NO;
|
|
self.rageShakeManager = [RageShakeManager sharedManager];
|
|
formattedBodyParser = [FormattedBodyParser new];
|
|
self.eventMenuBuilder = [EventMenuBuilder new];
|
|
|
|
_showMissedDiscussionsBadge = YES;
|
|
_scrollToBottomHidden = YES;
|
|
_isWaitingForOtherParticipants = NO;
|
|
|
|
// Listen to the event sent state changes
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeSentState:) name:kMXEventDidChangeSentStateNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeIdentifier:) name:kMXEventDidChangeIdentifierNotification object:nil];
|
|
|
|
// Show / hide actions button in document preview according BuildSettings
|
|
self.allowActionsInDocumentPreview = BuildSettings.messageDetailsAllowShare;
|
|
|
|
_voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider];
|
|
self.voiceMessageController.delegate = self;
|
|
}
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
// Register first customized cell view classes used to render bubbles
|
|
[[RoomTimelineConfiguration shared].currentStyle.cellProvider registerCellsForTableView:self.bubblesTableView];
|
|
|
|
[self vc_removeBackTitle];
|
|
|
|
// Display leftBarButtonItems or leftBarButtonItem to the right of the Back button
|
|
self.navigationItem.leftItemsSupplementBackButton = YES;
|
|
|
|
[self setupRemoveJitsiWidgetRemoveView];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Replace the default input toolbar view.
|
|
// Note: this operation will force the layout of subviews. That is why cell view classes must be registered before.
|
|
[self updateRoomInputToolbarViewClassIfNeeded];
|
|
});
|
|
|
|
// set extra area
|
|
[self setRoomActivitiesViewClass:RoomActivitiesView.class];
|
|
|
|
// Custom the attachmnet viewer
|
|
[self setAttachmentsViewerClass:AttachmentsViewController.class];
|
|
|
|
// Custom the event details view
|
|
[self setEventDetailsViewClass:EventDetailsView.class];
|
|
|
|
// Prepare missed dicussion badge (if any)
|
|
self.showMissedDiscussionsBadge = _showMissedDiscussionsBadge;
|
|
|
|
// Refresh the waiting for other participants state
|
|
[self refreshWaitForOtherParticipantsState];
|
|
|
|
// Set up the room title view according to the data source (if any)
|
|
[self refreshRoomTitle];
|
|
|
|
// Refresh tool bar if the room data source is set.
|
|
if (self.roomDataSource)
|
|
{
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
|
|
self.roomContextualMenuPresenter = [RoomContextualMenuPresenter new];
|
|
self.errorPresenter = [MXKErrorAlertPresentation new];
|
|
self.roomMessageURLParser = [RoomMessageURLParser new];
|
|
|
|
self.jumpToLastUnreadLabel.text = [VectorL10n roomJumpToFirstUnread];
|
|
|
|
MXWeakify(self);
|
|
|
|
// Observe user interface theme change.
|
|
kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self userInterfaceThemeDidChange];
|
|
|
|
}];
|
|
|
|
[self userInterfaceThemeDidChange];
|
|
|
|
// Observe URL preview updates.
|
|
[self registerURLPreviewNotifications];
|
|
|
|
[self setupActions];
|
|
|
|
[self setupCompletionSuggestionViewIfNeeded];
|
|
|
|
[self.topBannersStackView vc_removeAllSubviews];
|
|
}
|
|
|
|
- (void)userInterfaceThemeDidChange
|
|
{
|
|
// Consider the main navigation controller if the current view controller is embedded inside a split view controller.
|
|
UINavigationController *mainNavigationController = self.navigationController;
|
|
if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
|
|
{
|
|
mainNavigationController = self.splitViewController.viewControllers.firstObject;
|
|
}
|
|
|
|
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
|
|
if (mainNavigationController)
|
|
{
|
|
[ThemeService.shared.theme applyStyleOnNavigationBar:mainNavigationController.navigationBar];
|
|
}
|
|
|
|
// Keep navigation bar transparent in some cases
|
|
if (!self.previewHeaderContainer.hidden)
|
|
{
|
|
self.navigationController.navigationBar.translucent = YES;
|
|
mainNavigationController.navigationBar.translucent = YES;
|
|
}
|
|
|
|
[self.inputToolbarView customizeViewRendering];
|
|
|
|
self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor;
|
|
|
|
[self.removeJitsiWidgetView updateWithTheme:ThemeService.shared.theme];
|
|
|
|
// Prepare jump to last unread banner
|
|
self.jumpToLastUnreadImageView.tintColor = ThemeService.shared.theme.tintColor;
|
|
self.jumpToLastUnreadLabel.textColor = ThemeService.shared.theme.textPrimaryColor;
|
|
|
|
self.previewHeaderContainer.backgroundColor = ThemeService.shared.theme.headerBackgroundColor;
|
|
|
|
// Check the table view style to select its bg color.
|
|
self.bubblesTableView.backgroundColor = ((self.bubblesTableView.style == UITableViewStylePlain) ? ThemeService.shared.theme.backgroundColor : ThemeService.shared.theme.headerBackgroundColor);
|
|
self.bubblesTableView.separatorColor = ThemeService.shared.theme.lineBreakColor;
|
|
self.view.backgroundColor = self.bubblesTableView.backgroundColor;
|
|
|
|
if (self.bubblesTableView.dataSource)
|
|
{
|
|
[self.bubblesTableView reloadData];
|
|
}
|
|
|
|
[self.scrollToBottomButton vc_addShadowWithColor:ThemeService.shared.theme.shadowColor
|
|
offset:CGSizeMake(0, 4)
|
|
radius:6
|
|
opacity:0.2];
|
|
|
|
self.inputBackgroundView.backgroundColor = [ThemeService.shared.theme.backgroundColor colorWithAlphaComponent:0.98];
|
|
|
|
if (ThemeService.shared.isCurrentThemeDark)
|
|
{
|
|
[self.scrollToBottomButton setImage:AssetImages.scrolldownDark.image forState:UIControlStateNormal];
|
|
|
|
self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.navigation;
|
|
[self.jumpToLastUnreadBanner vc_removeShadow];
|
|
self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.quarterlyContent;
|
|
if (self.maximisedToolbarDimmingView) {
|
|
self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.29];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self.scrollToBottomButton setImage:AssetImages.scrolldown.image forState:UIControlStateNormal];
|
|
|
|
self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.background;
|
|
[self.jumpToLastUnreadBanner vc_addShadowWithColor:ThemeService.shared.theme.shadowColor
|
|
offset:CGSizeMake(0, 4)
|
|
radius:8
|
|
opacity:0.1];
|
|
self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.tertiaryContent;
|
|
if (self.maximisedToolbarDimmingView) {
|
|
self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.12];
|
|
}
|
|
}
|
|
|
|
self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor;
|
|
|
|
[self updateThreadListBarButtonBadgeWith:self.mainSession.threadingService];
|
|
|
|
[self.liveLocationSharingBannerView updateWithTheme:ThemeService.shared.theme];
|
|
|
|
[self setNeedsStatusBarAppearanceUpdate];
|
|
}
|
|
|
|
- (UIStatusBarStyle)preferredStatusBarStyle
|
|
{
|
|
return ThemeService.shared.theme.statusBarStyle;
|
|
}
|
|
|
|
- (void)didReceiveMemoryWarning
|
|
{
|
|
[super didReceiveMemoryWarning];
|
|
// Dispose of any resources that can be recreated.
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[super viewWillAppear:animated];
|
|
|
|
// Refresh the room title view
|
|
[self refreshRoomTitle];
|
|
|
|
// refresh remove Jitsi widget view
|
|
[self refreshRemoveJitsiWidgetView];
|
|
|
|
// Refresh tool bar if the room data source is set.
|
|
if (self.roomDataSource)
|
|
{
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
|
|
// Reset typing notification in order to remove the allocated space
|
|
if ([self.roomDataSource isKindOfClass:RoomDataSource.class])
|
|
{
|
|
[((RoomDataSource*)self.roomDataSource) resetTypingNotification];
|
|
}
|
|
|
|
[self listenTypingNotifications];
|
|
[self listenCallNotifications];
|
|
[self listenWidgetNotifications];
|
|
[self listenTombstoneEventNotifications];
|
|
[self listenMXSessionStateChangeNotifications];
|
|
|
|
MXWeakify(self);
|
|
|
|
// Observe kAppDelegateDidTapStatusBarNotification.
|
|
kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self setBubbleTableViewContentOffset:CGPointMake(-self.bubblesTableView.adjustedContentInset.left, -self.bubblesTableView.adjustedContentInset.top) animated:YES];
|
|
}];
|
|
|
|
if ([self.roomDataSource.roomId isEqualToString:[LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush])
|
|
{
|
|
[self startActivityIndicator];
|
|
[self.roomDataSource reload];
|
|
[LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush = nil;
|
|
|
|
notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameNotificationsOpenEvent];
|
|
}
|
|
|
|
[self updateTopBanners];
|
|
|
|
self.bubblesTableView.clipsToBounds = NO;
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
[super viewWillDisappear:animated];
|
|
|
|
// hide action
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
[self removeTypingNotificationsListener];
|
|
|
|
if (self.customizedRoomDataSource)
|
|
{
|
|
// Cancel potential selected event (to leave edition mode)
|
|
if (self.customizedRoomDataSource.selectedEventId)
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
}
|
|
[self cancelEventHighlight];
|
|
|
|
// Hide preview header to restore navigation bar settings
|
|
[self showPreviewHeader:NO];
|
|
|
|
if (kAppDelegateDidTapStatusBarNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver];
|
|
kAppDelegateDidTapStatusBarNotificationObserver = nil;
|
|
}
|
|
|
|
[self removeCallNotificationsListeners];
|
|
[self removeWidgetNotificationsListeners];
|
|
[self removeTombstoneEventNotificationsListener];
|
|
[self removeMXSessionStateChangeNotificationsListener];
|
|
|
|
// Re-enable the read marker display, and disable its update.
|
|
self.roomDataSource.showReadMarker = YES;
|
|
self.updateRoomReadMarker = NO;
|
|
isAppeared = NO;
|
|
|
|
[VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices];
|
|
[VoiceBroadcastRecorderProvider.shared pauseRecording];
|
|
[VoiceBroadcastPlaybackProvider.shared pausePlaying];
|
|
|
|
// Stop the loading indicator even if the session is still in progress
|
|
[self stopLoadingUserIndicator];
|
|
|
|
[self setMaximisedToolbarIsHiddenIfNeeded: YES];
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated
|
|
{
|
|
[super viewDidAppear:animated];
|
|
|
|
// Screen tracking
|
|
MXRoomSummary *summary = [self.mainSession roomWithRoomId:self.roomDataSource.roomId].summary;
|
|
if (!summary || !summary.isJoined)
|
|
{
|
|
[AnalyticsScreenTracker trackScreen: AnalyticsScreenRoomPreview];
|
|
}
|
|
else
|
|
{
|
|
[AnalyticsScreenTracker trackScreen: AnalyticsScreenRoom];
|
|
}
|
|
|
|
isAppeared = YES;
|
|
[self checkReadMarkerVisibility];
|
|
|
|
if (self.roomDataSource)
|
|
{
|
|
// Set visible room id
|
|
[AppDelegate theDelegate].visibleRoomId = self.roomDataSource.roomId;
|
|
}
|
|
|
|
MXWeakify(self);
|
|
|
|
// Observe network reachability
|
|
kAppDelegateNetworkStatusDidChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateNetworkStatusDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
|
|
}];
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
|
|
// Observe missed notifications
|
|
mxRoomSummaryDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomSummaryDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXRoomSummary *roomSummary = notif.object;
|
|
|
|
if ([roomSummary.roomId isEqualToString:self.roomDataSource.roomId])
|
|
{
|
|
[self refreshMissedDiscussionsCount:NO];
|
|
}
|
|
}];
|
|
[self refreshMissedDiscussionsCount:YES];
|
|
self.keyboardHeight = MAX(self.keyboardHeight, 0);
|
|
|
|
if (hasJitsiCall &&
|
|
!self.isRoomHavingAJitsiCall)
|
|
{
|
|
// the room had a Jitsi call before, but not now
|
|
hasJitsiCall = NO;
|
|
[self reloadBubblesTable:YES];
|
|
}
|
|
|
|
self.showSettingsInitially = NO;
|
|
|
|
if (!RiotSettings.shared.threadsNoticeDisplayed && RiotSettings.shared.enableThreads)
|
|
{
|
|
[self showThreadsNotice];
|
|
}
|
|
|
|
if (self.saveProgressTextInput && self.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)
|
|
[self.inputToolbarView setPartialContent:self.roomDataSource.partialAttributedTextMessage];
|
|
}
|
|
|
|
[self setMaximisedToolbarIsHiddenIfNeeded: NO];
|
|
}
|
|
|
|
- (void)viewDidDisappear:(BOOL)animated
|
|
{
|
|
[super viewDidDisappear:animated];
|
|
|
|
// Hide contextual menu if needed
|
|
[self hideContextualMenuAnimated:NO];
|
|
|
|
// Reset visible room id
|
|
[AppDelegate theDelegate].visibleRoomId = nil;
|
|
|
|
if (kAppDelegateNetworkStatusDidChangeNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateNetworkStatusDidChangeNotificationObserver];
|
|
kAppDelegateNetworkStatusDidChangeNotificationObserver = nil;
|
|
}
|
|
|
|
if (mxRoomSummaryDidChangeObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:mxRoomSummaryDidChangeObserver];
|
|
mxRoomSummaryDidChangeObserver = nil;
|
|
}
|
|
|
|
if (mxEventDidDecryptNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver];
|
|
mxEventDidDecryptNotificationObserver = nil;
|
|
}
|
|
|
|
if (self.isRoomHavingAJitsiCall)
|
|
{
|
|
hasJitsiCall = YES;
|
|
[self reloadBubblesTable:YES];
|
|
}
|
|
}
|
|
|
|
- (void)viewWillLayoutSubviews {
|
|
[super viewWillLayoutSubviews];
|
|
self.wasScrollAtBottomBeforeLayout = self.isBubblesTableScrollViewAtTheBottom;
|
|
}
|
|
|
|
- (void)viewDidLayoutSubviews
|
|
{
|
|
[super viewDidLayoutSubviews];
|
|
BOOL didViewChangeBounds = !CGRectEqualToRect(lastViewBounds, self.view.bounds);
|
|
lastViewBounds = self.view.bounds;
|
|
|
|
UIEdgeInsets contentInset = self.bubblesTableView.contentInset;
|
|
contentInset.bottom = self.view.safeAreaInsets.bottom;
|
|
self.bubblesTableView.contentInset = contentInset;
|
|
|
|
// Check here whether a subview has been added or removed
|
|
if (encryptionInfoView)
|
|
{
|
|
if (!encryptionInfoView.superview)
|
|
{
|
|
// Reset
|
|
encryptionInfoView = nil;
|
|
|
|
// Reload the full table to take into account a potential change on a device status.
|
|
[self.bubblesTableView reloadData];
|
|
}
|
|
}
|
|
|
|
if (eventDetailsView)
|
|
{
|
|
if (!eventDetailsView.superview)
|
|
{
|
|
// Reset
|
|
eventDetailsView = nil;
|
|
}
|
|
}
|
|
|
|
// Check whether the preview header is visible
|
|
if (previewHeader)
|
|
{
|
|
if (previewHeader.mainHeaderContainer.isHidden)
|
|
{
|
|
// Check here the main background height to display a correct navigation bar background.
|
|
CGRect frame = self.navigationController.navigationBar.frame;
|
|
|
|
CGFloat mainHeaderBackgroundHeight = frame.size.height + (frame.origin.y > 0 ? frame.origin.y : 0);
|
|
|
|
if (previewHeader.mainHeaderBackgroundHeightConstraint.constant != mainHeaderBackgroundHeight)
|
|
{
|
|
previewHeader.mainHeaderBackgroundHeightConstraint.constant = mainHeaderBackgroundHeight;
|
|
|
|
// Force the layout of previewHeader to update the position of 'bottomBorderView' which
|
|
// is used to define the actual height of the preview container.
|
|
[previewHeader layoutIfNeeded];
|
|
}
|
|
}
|
|
|
|
self.edgesForExtendedLayout = UIRectEdgeAll;
|
|
|
|
// Adjust the top constraint of the bubbles table
|
|
CGRect frame = previewHeader.bottomBorderView.frame;
|
|
self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height;
|
|
|
|
self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top;
|
|
}
|
|
else
|
|
{
|
|
// In non expanded header mode, the navigation bar is opaque
|
|
// The table view must not display behind it
|
|
self.edgesForExtendedLayout = UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight;
|
|
}
|
|
|
|
// re-scroll to the bottom, if at bottom before the most recent layout
|
|
if (self.wasScrollAtBottomBeforeLayout && didViewChangeBounds)
|
|
{
|
|
self.wasScrollAtBottomBeforeLayout = NO;
|
|
[self scrollBubblesTableViewToBottomAnimated:NO];
|
|
}
|
|
|
|
[self refreshMissedDiscussionsCount:YES];
|
|
}
|
|
|
|
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
|
|
{
|
|
if ([self.titleView isKindOfClass:RoomTitleView.class])
|
|
{
|
|
RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
|
|
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation))
|
|
{
|
|
[roomTitleView updateLayoutForOrientation:UIInterfaceOrientationPortrait];
|
|
}
|
|
else
|
|
{
|
|
[roomTitleView updateLayoutForOrientation:UIInterfaceOrientationLandscapeLeft];
|
|
}
|
|
}
|
|
|
|
// Hide the expanded header or the preview in case of iPad and iPhone 6 plus.
|
|
// On these devices, the display mode of the splitviewcontroller may change during screen rotation.
|
|
// It may correspond to an overlay mode in portrait and a side-by-side mode in landscape.
|
|
// This display mode change involves a change at the navigation bar level.
|
|
// If we don't hide the header, the navigation bar is in a wrong state after rotation. FIXME: Find a way to keep visible the header on rotation.
|
|
if ([GBDeviceInfo deviceInfo].family == GBDeviceFamilyiPad || [GBDeviceInfo deviceInfo].displayInfo.display >= GBDeviceDisplay5p5Inch)
|
|
{
|
|
// Hide the preview header (if any) before rotating (It will be restored by `refreshRoomTitle` call if this is still a room preview).
|
|
[self showPreviewHeader:NO];
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((coordinator.transitionDuration + 0.5) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
// Let [self refreshRoomTitle] refresh this title view correctly
|
|
[self refreshRoomTitle];
|
|
|
|
});
|
|
}
|
|
else if (previewHeader)
|
|
{
|
|
// Refresh here the preview header according to the coming screen orientation.
|
|
|
|
// Retrieve the affine transform indicating the amount of rotation being applied to the interface.
|
|
// This transform is the identity transform when no rotation is applied.
|
|
// Otherwise, it is a transform that applies a 90 degree, -90 degree, or 180 degree rotation.
|
|
CGAffineTransform transform = coordinator.targetTransform;
|
|
|
|
// Consider here only the transform that applies a +/- 90 degree.
|
|
if (transform.b * transform.c == -1)
|
|
{
|
|
UIInterfaceOrientation currentScreenOrientation = [[UIApplication sharedApplication] statusBarOrientation];
|
|
BOOL isLandscapeOriented = YES;
|
|
|
|
switch (currentScreenOrientation)
|
|
{
|
|
case UIInterfaceOrientationLandscapeRight:
|
|
case UIInterfaceOrientationLandscapeLeft:
|
|
{
|
|
// We leave here landscape orientation
|
|
isLandscapeOriented = NO;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
[self refreshPreviewHeader:isLandscapeOriented];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((coordinator.transitionDuration + 0.5) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
// Refresh the room title at the end of the transition to take into account the potential changes during the transition.
|
|
// For example the display of a preview header is ignored during transition.
|
|
[self refreshRoomTitle];
|
|
|
|
});
|
|
}
|
|
|
|
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
}
|
|
|
|
#pragma mark - Accessibility
|
|
|
|
// Handle scrolling when VoiceOver is on because it does not work well if we let the system do:
|
|
// VoiceOver loses the focus on the tableview
|
|
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction
|
|
{
|
|
BOOL canScroll = YES;
|
|
|
|
// Scroll by one page
|
|
CGFloat tableViewHeight = self.bubblesTableView.frame.size.height;
|
|
|
|
CGPoint offset = self.bubblesTableView.contentOffset;
|
|
switch (direction)
|
|
{
|
|
case UIAccessibilityScrollDirectionUp:
|
|
offset.y -= tableViewHeight;
|
|
break;
|
|
|
|
case UIAccessibilityScrollDirectionDown:
|
|
offset.y += tableViewHeight;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (offset.y < 0 && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards])
|
|
{
|
|
// Can't paginate more. Let's stick on the first item
|
|
UIView *focusedView = [self firstCellWithAccessibilityDataInCells:self.bubblesTableView.visibleCells.objectEnumerator];
|
|
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, focusedView);
|
|
canScroll = NO;
|
|
}
|
|
else if (offset.y > self.bubblesTableView.contentSize.height - tableViewHeight
|
|
&& ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards])
|
|
{
|
|
// Can't paginate more. Let's stick on the last item with accessibility
|
|
UIView *focusedView = [self firstCellWithAccessibilityDataInCells:self.bubblesTableView.visibleCells.reverseObjectEnumerator];
|
|
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, focusedView);
|
|
canScroll = NO;
|
|
}
|
|
else
|
|
{
|
|
// Disable VoiceOver while scrolling
|
|
self.bubblesTableView.accessibilityElementsHidden = YES;
|
|
|
|
[self setBubbleTableViewContentOffset:offset animated:NO];
|
|
|
|
NSEnumerator<UITableViewCell*> *cells;
|
|
if (direction == UIAccessibilityScrollDirectionUp)
|
|
{
|
|
cells = self.bubblesTableView.visibleCells.objectEnumerator;
|
|
}
|
|
else
|
|
{
|
|
cells = self.bubblesTableView.visibleCells.reverseObjectEnumerator;
|
|
}
|
|
UIView *cell = [self firstCellWithAccessibilityDataInCells:cells];
|
|
|
|
self.bubblesTableView.accessibilityElementsHidden = NO;
|
|
|
|
// Force VoiceOver to focus on a visible item
|
|
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, cell);
|
|
}
|
|
|
|
// If we cannot scroll, let VoiceOver indicates the border
|
|
return canScroll;
|
|
}
|
|
|
|
- (UIView*)firstCellWithAccessibilityDataInCells:(NSEnumerator<UITableViewCell*>*)cells
|
|
{
|
|
UIView *view;
|
|
|
|
for (UITableViewCell *cell in cells)
|
|
{
|
|
if (![cell isKindOfClass:[RoomEmptyBubbleCell class]])
|
|
{
|
|
view = cell;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return view;
|
|
}
|
|
|
|
|
|
#pragma mark - Override MXKRoomViewController
|
|
|
|
- (void)addMatrixSession:(MXSession *)mxSession
|
|
{
|
|
[super addMatrixSession:mxSession];
|
|
|
|
[mxSession.threadingService addDelegate:self];
|
|
[self updateThreadListBarButtonBadgeWith:mxSession.threadingService];
|
|
}
|
|
|
|
- (void)removeMatrixSession:(MXSession *)mxSession
|
|
{
|
|
[mxSession.threadingService removeDelegate:self];
|
|
|
|
[super removeMatrixSession:mxSession];
|
|
}
|
|
|
|
- (void)onMatrixSessionChange
|
|
{
|
|
[super onMatrixSessionChange];
|
|
|
|
// Re-enable the read marker display, and disable its update.
|
|
self.roomDataSource.showReadMarker = YES;
|
|
self.updateRoomReadMarker = NO;
|
|
}
|
|
|
|
#pragma mark - Loading indicators
|
|
|
|
- (BOOL)providesCustomActivityIndicator {
|
|
return YES;
|
|
}
|
|
|
|
// Override of a legacy method to determine whether to use a newer implementation instead.
|
|
// Will be removed in the future https://github.com/vector-im/element-ios/issues/5608
|
|
- (void)startActivityIndicator {
|
|
[self.delegate roomViewControllerDidStartLoading:self];
|
|
}
|
|
|
|
// Override of a legacy method to determine whether to use a newer implementation instead.
|
|
// Will be removed in the future https://github.com/vector-im/element-ios/issues/5608
|
|
- (void)stopActivityIndicator
|
|
{
|
|
if (notificationTaskProfile)
|
|
{
|
|
// Consider here we have displayed the message corresponding to the notification
|
|
[MXSDKOptions.sharedInstance.profiler stopMeasuringTaskWithProfile:notificationTaskProfile];
|
|
notificationTaskProfile = nil;
|
|
}
|
|
// The legacy super implementation of `stopActivityIndicator` contains a number of checks grouped under `canStopActivityIndicator`
|
|
// to determine whether the indicator can be stopped or not (and the method should thus rather be called `stopActivityIndicatorIfPossible`).
|
|
// Since the newer indicators are not calling super implementation, the check for `canStopActivityIndicator` has to be performed manually.
|
|
if ([self canStopActivityIndicator]) {
|
|
[self stopLoadingUserIndicator];
|
|
}
|
|
}
|
|
|
|
- (void)stopLoadingUserIndicator
|
|
{
|
|
[self.delegate roomViewControllerDidStopLoading:self];
|
|
}
|
|
|
|
- (void)displayRoom:(MXKRoomDataSource *)dataSource
|
|
{
|
|
// Remove potential preview Data
|
|
if (roomPreviewData)
|
|
{
|
|
roomPreviewData = nil;
|
|
[self removeMatrixSession:self.mainSession];
|
|
}
|
|
|
|
// Set potential discussion target user to nil, now use the dataSource to populate the view
|
|
self.directChatTargetUser = nil;
|
|
|
|
// Enable the read marker display, and disable its update.
|
|
dataSource.showReadMarker = YES;
|
|
self.updateRoomReadMarker = NO;
|
|
|
|
[super displayRoom:dataSource];
|
|
|
|
self.customizedRoomDataSource = nil;
|
|
|
|
if (self.roomDataSource)
|
|
{
|
|
[self listenToServerNotices];
|
|
|
|
self.eventsAcknowledgementEnabled = YES;
|
|
|
|
// Store ref on customized room data source
|
|
if ([dataSource isKindOfClass:RoomDataSource.class])
|
|
{
|
|
self.customizedRoomDataSource = (RoomDataSource*)dataSource;
|
|
}
|
|
|
|
// Set room title view
|
|
[self refreshRoomTitle];
|
|
|
|
// Stop any pending voice broadcast if needed
|
|
[self stopUncompletedVoiceBroadcastIfNeeded];
|
|
}
|
|
else
|
|
{
|
|
self.navigationItem.rightBarButtonItem.enabled = NO;
|
|
}
|
|
|
|
[self refreshRoomInputToolbar];
|
|
|
|
[VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary];
|
|
_voiceMessageController.roomId = dataSource.roomId;
|
|
|
|
_completionSuggestionCoordinator = [[CompletionSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
|
|
room:dataSource.room
|
|
userID:self.roomDataSource.mxSession.myUserId];
|
|
_completionSuggestionCoordinator.delegate = self;
|
|
|
|
[self setupCompletionSuggestionViewIfNeeded];
|
|
|
|
[self updateRoomInputToolbarViewClassIfNeeded];
|
|
|
|
[self updateTopBanners];
|
|
}
|
|
|
|
- (void)onRoomDataSourceReady
|
|
{
|
|
// Handle here invitation
|
|
if (self.roomDataSource.room.summary.membership == MXMembershipInvite)
|
|
{
|
|
self.navigationItem.rightBarButtonItem.enabled = NO;
|
|
|
|
// Show preview header
|
|
[self showPreviewHeader:YES];
|
|
}
|
|
|
|
[super onRoomDataSourceReady];
|
|
}
|
|
|
|
- (void)updateViewControllerAppearanceOnRoomDataSourceState
|
|
{
|
|
[super updateViewControllerAppearanceOnRoomDataSourceState];
|
|
|
|
if (self.isRoomPreview)
|
|
{
|
|
self.navigationItem.rightBarButtonItem.enabled = NO;
|
|
|
|
// Remove input tool bar if any
|
|
if (self.inputToolbarView)
|
|
{
|
|
[super setRoomInputToolbarViewClass:nil];
|
|
}
|
|
|
|
if (previewHeader)
|
|
{
|
|
previewHeader.mxRoom = self.roomDataSource.room;
|
|
|
|
// Force the layout of subviews (some constraints may have been updated)
|
|
[self forceLayoutRefresh];
|
|
}
|
|
}
|
|
else if (self.isNewDirectChat)
|
|
{
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
else
|
|
{
|
|
[self showPreviewHeader:NO];
|
|
|
|
self.navigationItem.rightBarButtonItem.enabled = (self.roomDataSource != nil);
|
|
|
|
self.titleView.editable = NO;
|
|
|
|
if (self.roomDataSource)
|
|
{
|
|
// Update the input toolbar class and update the layout
|
|
[self updateRoomInputToolbarViewClassIfNeeded];
|
|
|
|
self.inputToolbarView.hidden = (self.roomDataSource.state != MXKDataSourceStateReady);
|
|
|
|
// Restore room activities view if none
|
|
if (!self.activitiesView)
|
|
{
|
|
// And the extra area
|
|
[self setRoomActivitiesViewClass:RoomActivitiesView.class];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)leaveRoomOnEvent:(MXEvent*)event
|
|
{
|
|
// Force a simple title view initialised with the current room before leaving actually the room.
|
|
[self setRoomTitleViewClass:SimpleRoomTitleView.class];
|
|
self.titleView.editable = NO;
|
|
self.titleView.mxRoom = self.roomDataSource.room;
|
|
|
|
// Hide the potential read marker banner.
|
|
self.jumpToLastUnreadBannerContainer.hidden = YES;
|
|
|
|
[super leaveRoomOnEvent:event];
|
|
[self notifyDelegateOnLeaveRoomIfNecessary];
|
|
}
|
|
|
|
|
|
+ (Class) mainToolbarClass
|
|
{
|
|
if (RiotSettings.shared.enableWysiwygComposer)
|
|
{
|
|
return WysiwygInputToolbarView.class;
|
|
}
|
|
else
|
|
{
|
|
return RoomInputToolbarView.class;
|
|
}
|
|
}
|
|
|
|
// Set the input toolbar according to the current display
|
|
- (void)updateRoomInputToolbarViewClassIfNeeded
|
|
{
|
|
Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];
|
|
|
|
// If RTE is enabled, delay the toolbar setup until `completionSuggestionCoordinator` is ready.
|
|
if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && _completionSuggestionCoordinator == nil)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BOOL shouldDismissContextualMenu = NO;
|
|
|
|
// Check the user has enough power to post message
|
|
if (self.roomDataSource.roomState)
|
|
{
|
|
MXRoomPowerLevels *powerLevels = self.roomDataSource.roomState.powerLevels;
|
|
NSInteger userPowerLevel = [powerLevels powerLevelOfUserWithUserID:self.mainSession.myUser.userId];
|
|
|
|
BOOL canSend = (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsMessage:kMXEventTypeStringRoomMessage]);
|
|
BOOL isRoomObsolete = self.roomDataSource.roomState.isObsolete;
|
|
BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded];
|
|
|
|
if (isRoomObsolete || isResourceLimitExceeded || _isWaitingForOtherParticipants)
|
|
{
|
|
roomInputToolbarViewClass = nil;
|
|
shouldDismissContextualMenu = YES;
|
|
}
|
|
else if (!canSend)
|
|
{
|
|
roomInputToolbarViewClass = DisabledRoomInputToolbarView.class;
|
|
shouldDismissContextualMenu = YES;
|
|
}
|
|
}
|
|
|
|
// Do not show toolbar in case of preview
|
|
if (self.isRoomPreview)
|
|
{
|
|
roomInputToolbarViewClass = nil;
|
|
shouldDismissContextualMenu = YES;
|
|
}
|
|
|
|
if (shouldDismissContextualMenu)
|
|
{
|
|
[self hideContextualMenuAnimated:NO];
|
|
}
|
|
|
|
// Change inputToolbarView class only if given class is different from current one
|
|
if (!self.inputToolbarView || ![self.inputToolbarView isMemberOfClass:roomInputToolbarViewClass])
|
|
{
|
|
[super setRoomInputToolbarViewClass:roomInputToolbarViewClass];
|
|
if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) {
|
|
id<RoomInputToolbarViewProtocol> inputToolbar = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
|
|
[inputToolbar setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView];
|
|
}
|
|
|
|
[self updateInputToolBarViewHeight];
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
}
|
|
|
|
// Get the height of the current room input toolbar
|
|
- (CGFloat)inputToolbarHeight
|
|
{
|
|
CGFloat height = 0;
|
|
|
|
if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) {
|
|
id<RoomInputToolbarViewProtocol> inputToolbar = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
|
|
height = inputToolbar.toolbarHeight;
|
|
}
|
|
else if ([self.inputToolbarView isKindOfClass:DisabledRoomInputToolbarView.class])
|
|
{
|
|
height = ((DisabledRoomInputToolbarView*)self.inputToolbarView).mainToolbarMinHeightConstraint.constant;
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
- (void)setRoomActivitiesViewClass:(Class)roomActivitiesViewClass
|
|
{
|
|
// Do not show room activities in case of preview (FIXME: show it when live events will be supported during peeking)
|
|
if (self.isRoomPreview)
|
|
{
|
|
roomActivitiesViewClass = nil;
|
|
}
|
|
|
|
[super setRoomActivitiesViewClass:roomActivitiesViewClass];
|
|
|
|
if (!self.isActivitiesViewExpanded)
|
|
{
|
|
self.roomActivitiesContainerHeightConstraint.constant = 0;
|
|
}
|
|
}
|
|
|
|
- (BOOL)sendAsIRCStyleCommandIfPossible:(NSString*)string
|
|
{
|
|
// Override the default behavior for `/join` command in order to open automatically the joined room
|
|
|
|
NSString* kMXKSlashCmdJoinRoom = [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandJoinRoom];
|
|
|
|
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)
|
|
{
|
|
Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerSlashCommand;
|
|
|
|
// TODO: /join command does not support via parameters yet
|
|
[self.mainSession joinRoom:roomAlias viaServers:nil success:^(MXRoom *room) {
|
|
|
|
[self showRoomWithId:room.roomId];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[RoomVC] Join roomAlias (%@) failed", roomAlias);
|
|
//Alert user
|
|
[self showError:error];
|
|
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// Display cmd usage in text input as placeholder
|
|
self.inputToolbarView.placeholder = [MXKSlashCommandsHelper commandUsageFor:MXKSlashCommandJoinRoom];
|
|
}
|
|
return YES;
|
|
}
|
|
return [super sendAsIRCStyleCommandIfPossible:string];
|
|
}
|
|
|
|
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
|
|
{
|
|
[super setKeyboardHeight:keyboardHeight];
|
|
|
|
self.inputToolbarView.maxHeight = round(([UIScreen mainScreen].bounds.size.height - keyboardHeight) * 0.7);
|
|
|
|
// Make the activity indicator follow the keyboard
|
|
// At runtime, this creates a smooth animation
|
|
CGPoint activityIndicatorCenter = self.activityIndicator.center;
|
|
activityIndicatorCenter.y = self.view.center.y - keyboardHeight / 2;
|
|
self.activityIndicator.center = activityIndicatorCenter;
|
|
}
|
|
|
|
- (void)dismissTemporarySubViews
|
|
{
|
|
[super dismissTemporarySubViews];
|
|
|
|
if (encryptionInfoView)
|
|
{
|
|
[encryptionInfoView removeFromSuperview];
|
|
encryptionInfoView = nil;
|
|
}
|
|
}
|
|
|
|
- (void)setBubbleTableViewDisplayInTransition:(BOOL)bubbleTableViewDisplayInTransition
|
|
{
|
|
if (self.isBubbleTableViewDisplayInTransition != bubbleTableViewDisplayInTransition)
|
|
{
|
|
[super setBubbleTableViewDisplayInTransition:bubbleTableViewDisplayInTransition];
|
|
|
|
// Refresh additional displays when the table is ready.
|
|
if (!bubbleTableViewDisplayInTransition && !self.bubblesTableView.isHidden)
|
|
{
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshRoomTitle];
|
|
|
|
[self checkReadMarkerVisibility];
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)sendTextMessage:(NSString*)msgTxt
|
|
{
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if (readyToSend)
|
|
{
|
|
// The event modified is always fetch from the actual data source
|
|
MXEvent *eventModified = [self.roomDataSource eventWithEventId:self.customizedRoomDataSource.selectedEventId];
|
|
|
|
// In the case the event is a reply or and edit, and it's done on a non-live timeline
|
|
// we have to fetch live timeline in order to display the event properly
|
|
[self setupRoomDataSourceToResolveEvent:^(MXKRoomDataSource *roomDataSource) {
|
|
if (self.inputToolBarSendMode == RoomInputToolbarViewSendModeReply && eventModified)
|
|
{
|
|
[roomDataSource sendReplyToEvent:eventModified withTextMessage: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.");
|
|
}];
|
|
}
|
|
else if (self.inputToolBarSendMode == RoomInputToolbarViewSendModeEdit && eventModified)
|
|
{
|
|
[roomDataSource replaceTextMessageForEvent:eventModified withTextMessage:msgTxt success:nil failure:^(NSError *error) {
|
|
// Just log the error. The message will be displayed in red
|
|
MXLogDebug(@"[MXKRoomViewController] sendTextMessage failed.");
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// 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.");
|
|
}];
|
|
}
|
|
|
|
if (self.customizedRoomDataSource.selectedEventId)
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
}];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)setupRoomDataSourceToResolveEvent: (void (^)(MXKRoomDataSource *roomDataSource))onComplete
|
|
{
|
|
// If the event occur on timeline not live, use the live data source to resolve event
|
|
BOOL isLive = self.roomDataSource.isLive;
|
|
if (!isLive)
|
|
{
|
|
if (self.roomDataSourceLive == nil)
|
|
{
|
|
MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];
|
|
|
|
[roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId
|
|
create:YES
|
|
onComplete:^(MXKRoomDataSource *roomDataSource) {
|
|
self.roomDataSourceLive = roomDataSource;
|
|
[self.roomDataSourceLive finalizeInitialization];
|
|
onComplete(self.roomDataSourceLive);
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
onComplete(self.roomDataSourceLive);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
onComplete(self.roomDataSource);
|
|
}
|
|
}
|
|
|
|
- (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]);
|
|
|
|
MXKRoomTitleView *titleView = [roomTitleViewClass roomTitleView];
|
|
[self setValue:titleView forKey:@"titleView"];
|
|
titleView.delegate = self;
|
|
titleView.mxRoom = self.roomDataSource.room;
|
|
titleView.mxUser = self.directChatTargetUser;
|
|
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:titleView];
|
|
|
|
if ([titleView isKindOfClass:RoomTitleView.class])
|
|
{
|
|
RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
|
|
missedDiscussionsBadgeLabel = roomTitleView.missedDiscussionsBadgeLabel;
|
|
missedDiscussionsDotView = roomTitleView.dotView;
|
|
[roomTitleView updateLayoutForOrientation:[UIApplication sharedApplication].statusBarOrientation];
|
|
}
|
|
|
|
[self updateViewControllerAppearanceOnRoomDataSourceState];
|
|
|
|
[self updateTitleViewEncryptionDecoration];
|
|
}
|
|
|
|
- (void)destroy
|
|
{
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
if (self.customizedRoomDataSource)
|
|
{
|
|
self.customizedRoomDataSource.selectedEventId = nil;
|
|
self.customizedRoomDataSource = nil;
|
|
}
|
|
|
|
[self removeTypingNotificationsListener];
|
|
|
|
if (kThemeServiceDidChangeThemeNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kThemeServiceDidChangeThemeNotificationObserver];
|
|
kThemeServiceDidChangeThemeNotificationObserver = nil;
|
|
}
|
|
if (kAppDelegateDidTapStatusBarNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateDidTapStatusBarNotificationObserver];
|
|
kAppDelegateDidTapStatusBarNotificationObserver = nil;
|
|
}
|
|
if (kAppDelegateNetworkStatusDidChangeNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kAppDelegateNetworkStatusDidChangeNotificationObserver];
|
|
kAppDelegateNetworkStatusDidChangeNotificationObserver = nil;
|
|
}
|
|
if (mxRoomSummaryDidChangeObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:mxRoomSummaryDidChangeObserver];
|
|
mxRoomSummaryDidChangeObserver = nil;
|
|
}
|
|
if (mxEventDidDecryptNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver];
|
|
mxEventDidDecryptNotificationObserver = nil;
|
|
}
|
|
if (URLPreviewDidUpdateNotificationObserver)
|
|
{
|
|
[NSNotificationCenter.defaultCenter removeObserver:URLPreviewDidUpdateNotificationObserver];
|
|
}
|
|
|
|
[self removeCallNotificationsListeners];
|
|
[self removeWidgetNotificationsListeners];
|
|
[self removeTombstoneEventNotificationsListener];
|
|
[self removeMXSessionStateChangeNotificationsListener];
|
|
[self removeServerNoticesListener];
|
|
|
|
if (previewHeader)
|
|
{
|
|
// Here [destroy] is called before [viewWillDisappear:]
|
|
MXLogDebug(@"[RoomVC] destroyed whereas it is still visible");
|
|
|
|
[previewHeader removeFromSuperview];
|
|
previewHeader = nil;
|
|
|
|
// Hide preview header container to ignore [self showPreviewHeader:NO] call (if any).
|
|
self.previewHeaderContainer.hidden = YES;
|
|
}
|
|
|
|
roomPreviewData = nil;
|
|
|
|
missedDiscussionsBadgeLabel = nil;
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:nil];
|
|
|
|
[self waitForOtherParticipant:NO];
|
|
|
|
[super destroy];
|
|
}
|
|
|
|
#pragma mark - Start DM
|
|
|
|
/**
|
|
Create a direct chat with given user.
|
|
*/
|
|
- (void)createDiscussionWithUser:(MXUser*)user completion:(void (^)(BOOL success))onComplete
|
|
{
|
|
[self startActivityIndicator];
|
|
|
|
[[AppDelegate theDelegate] createDirectChatWithUserId:user.userId completion:^(NSString *roomId) {
|
|
if (roomId)
|
|
{
|
|
MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];
|
|
[roomDataSourceManager roomDataSourceForRoom:roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) {
|
|
[self stopActivityIndicator];
|
|
[self setRoomInputToolbarViewClass:nil];
|
|
[self displayRoom:roomDataSource];
|
|
|
|
onComplete(YES);
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
[self stopActivityIndicator];
|
|
onComplete(NO);
|
|
}
|
|
}];
|
|
}
|
|
|
|
/**
|
|
Create the discussion if needed
|
|
*/
|
|
- (void)createDiscussionIfNeeded:(void (^)(BOOL readyToSend))onComplete
|
|
{
|
|
void(^completion)(BOOL) = ^(BOOL readyToSend) {
|
|
self.inputToolbarView.userInteractionEnabled = true;
|
|
if (onComplete) {
|
|
onComplete(readyToSend);
|
|
}
|
|
};
|
|
|
|
if (self.directChatTargetUser)
|
|
{
|
|
// Disable the input tool bar during this operation. This prevents us from creating several discussions, or
|
|
// trying to send several invites.
|
|
self.inputToolbarView.userInteractionEnabled = false;
|
|
|
|
[self createDiscussionWithUser:self.directChatTargetUser completion:completion];
|
|
}
|
|
else
|
|
{
|
|
completion(YES);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
-(void)setActivitiesViewExpanded:(BOOL)activitiesViewExpanded
|
|
{
|
|
if (_activitiesViewExpanded != activitiesViewExpanded)
|
|
{
|
|
_activitiesViewExpanded = activitiesViewExpanded;
|
|
|
|
self.roomActivitiesContainerHeightConstraint.constant = activitiesViewExpanded ? 53 : 0;
|
|
[super roomInputToolbarView:self.inputToolbarView heightDidChanged:[self inputToolbarHeight] completion:nil];
|
|
}
|
|
}
|
|
|
|
- (void)setScrollToBottomHidden:(BOOL)scrollToBottomHidden
|
|
{
|
|
if (_scrollToBottomHidden != scrollToBottomHidden)
|
|
{
|
|
_scrollToBottomHidden = scrollToBottomHidden;
|
|
}
|
|
|
|
if (!_scrollToBottomHidden && [self.roomDataSource isKindOfClass:RoomDataSource.class])
|
|
{
|
|
RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
|
|
if (roomDataSource.currentTypingUsers && !roomDataSource.currentTypingUsers.count)
|
|
{
|
|
[roomDataSource resetTypingNotification];
|
|
[self.bubblesTableView reloadData];
|
|
}
|
|
}
|
|
|
|
[UIView animateWithDuration:.2 animations:^{
|
|
self.scrollToBottomBadgeLabel.alpha = (scrollToBottomHidden || !self.scrollToBottomBadgeLabel.text) ? 0 : 1;
|
|
self.scrollToBottomButton.alpha = scrollToBottomHidden ? 0 : 1;
|
|
}];
|
|
}
|
|
|
|
- (void)setMissedDiscussionsBadgeHidden:(BOOL)missedDiscussionsBadgeHidden{
|
|
_missedDiscussionsBadgeHidden = missedDiscussionsBadgeHidden;
|
|
|
|
missedDiscussionsBadgeLabel.hidden = missedDiscussionsBadgeHidden;
|
|
missedDiscussionsDotView.hidden = missedDiscussionsBadgeHidden;
|
|
}
|
|
|
|
- (BOOL)shouldShowLiveLocationSharingBannerView
|
|
{
|
|
return self.customizedRoomDataSource.isCurrentUserSharingActiveLocation;
|
|
}
|
|
|
|
#pragma mark - Wait for 3rd party invitee
|
|
|
|
- (void)setIsWaitingForOtherParticipants:(BOOL)isWaitingForOtherParticipants
|
|
{
|
|
if (_isWaitingForOtherParticipants == isWaitingForOtherParticipants)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isWaitingForOtherParticipants = isWaitingForOtherParticipants;
|
|
[self updateRoomInputToolbarViewClassIfNeeded];
|
|
|
|
if (_isWaitingForOtherParticipants)
|
|
{
|
|
if (self->roomMemberEventListener == nil)
|
|
{
|
|
MXWeakify(self);
|
|
self->roomMemberEventListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomMember] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if (direction != MXTimelineDirectionForwards)
|
|
{
|
|
return;
|
|
}
|
|
[self refreshWaitForOtherParticipantsState];
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (self->roomMemberEventListener != nil)
|
|
{
|
|
[self.roomDataSource.room removeListener:self->roomMemberEventListener];
|
|
self->roomMemberEventListener = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)shouldWaitForOtherParticipants
|
|
{
|
|
MXRoomState *roomState = self.roomDataSource.roomState;
|
|
BOOL isDirect = self.roomDataSource.room.isDirect;
|
|
|
|
// Wait for the other participant only if it is a direct encrypted room with only one member waiting for a third party guest.
|
|
return (isDirect && roomState.isEncrypted && roomState.membersCount.members == 1 && roomState.thirdPartyInvites.count > 0);
|
|
}
|
|
|
|
- (void)refreshWaitForOtherParticipantsState
|
|
{
|
|
[self waitForOtherParticipant:self.shouldWaitForOtherParticipants];
|
|
}
|
|
|
|
#pragma mark - Internals
|
|
|
|
- (UIBarButtonItem *)videoCallBarButtonItem
|
|
{
|
|
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:AssetImages.videoCall.image
|
|
style:UIBarButtonItemStylePlain
|
|
target:self
|
|
action:@selector(onVideoCallPressed:)];
|
|
item.accessibilityLabel = [VectorL10n roomAccessibilityVideoCall];
|
|
|
|
return item;
|
|
}
|
|
|
|
- (UIBarButtonItem *)joinJitsiBarButtonItem
|
|
{
|
|
CallTileActionButton *button = [CallTileActionButton new];
|
|
[button setImage:AssetImages.callVideoIcon.image
|
|
forState:UIControlStateNormal];
|
|
[button setTitle:[VectorL10n roomJoinGroupCall]
|
|
forState:UIControlStateNormal];
|
|
[button addTarget:self
|
|
action:@selector(onVideoCallPressed:)
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
button.contentEdgeInsets = UIEdgeInsetsMake(4, 12, 4, 12);
|
|
|
|
UIBarButtonItem *item;
|
|
|
|
if (RiotSettings.shared.enableThreads)
|
|
{
|
|
// Add some spacing when there is a threads button
|
|
UIView *buttonContainer = [[UIView alloc] initWithFrame:CGRectZero];
|
|
[buttonContainer vc_addSubViewMatchingParent:button withInsets:UIEdgeInsetsMake(0, 0, 0, -12)];
|
|
|
|
item = [[UIBarButtonItem alloc] initWithCustomView:buttonContainer];
|
|
}
|
|
else
|
|
{
|
|
item = [[UIBarButtonItem alloc] initWithCustomView:button];
|
|
}
|
|
|
|
item.accessibilityLabel = [VectorL10n roomAccessibilityVideoCall];
|
|
|
|
return item;
|
|
}
|
|
|
|
- (UIBarButtonItem *)threadMoreBarButtonItem
|
|
{
|
|
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:AssetImages.roomContextMenuMore.image
|
|
style:UIBarButtonItemStylePlain
|
|
target:self
|
|
action:@selector(onButtonPressed:)];
|
|
item.accessibilityLabel = [VectorL10n roomAccessibilityThreadMore];
|
|
|
|
return item;
|
|
}
|
|
|
|
- (UIBarButtonItem *)threadListBarButtonItem
|
|
{
|
|
UIButton *button = [UIButton new];
|
|
button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot;
|
|
button.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
|
[button setImage:[AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize]
|
|
forState:UIControlStateNormal];
|
|
[button addTarget:self
|
|
action:@selector(onThreadListTapped:)
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
button.accessibilityLabel = [VectorL10n roomAccessibilityThreads];
|
|
|
|
UIBarButtonItem *result = [[UIBarButtonItem alloc] initWithCustomView:button];
|
|
result.tag = kThreadListBarButtonItemTag;
|
|
return result;
|
|
}
|
|
|
|
- (void)setupRemoveJitsiWidgetRemoveView
|
|
{
|
|
if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self.removeJitsiWidgetView = [RemoveJitsiWidgetView instantiate];
|
|
self.removeJitsiWidgetView.delegate = self;
|
|
|
|
[self.removeJitsiWidgetContainer vc_addSubViewMatchingParent:self.removeJitsiWidgetView];
|
|
|
|
self.removeJitsiWidgetContainer.hidden = YES;
|
|
|
|
[self refreshRemoveJitsiWidgetView];
|
|
}
|
|
|
|
- (void)forceLayoutRefresh
|
|
{
|
|
// Sanity check: check whether the table view data source is set.
|
|
if (self.bubblesTableView.dataSource)
|
|
{
|
|
[self.view layoutIfNeeded];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isRoomPreview
|
|
{
|
|
if (self.isContextPreview)
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
// Check first whether some preview data are defined.
|
|
if (roomPreviewData)
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
if (self.roomDataSource && self.roomDataSource.state == MXKDataSourceStateReady && self.roomDataSource.room.summary.membership == MXMembershipInvite)
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
// Indicates if a new direct chat with a target user (without associated room) is occuring.
|
|
- (BOOL)isNewDirectChat
|
|
{
|
|
return self.directChatTargetUser != nil;
|
|
}
|
|
|
|
- (BOOL)isEncryptionEnabled
|
|
{
|
|
return self.roomDataSource.room.summary.isEncrypted && self.mainSession.crypto != nil;
|
|
}
|
|
|
|
- (BOOL)supportCallOption
|
|
{
|
|
if (!self.displayConfiguration.callsEnabled)
|
|
{
|
|
return NO;
|
|
}
|
|
BOOL callOptionAllowed = (self.roomDataSource.room.isDirect && RiotSettings.shared.roomScreenAllowVoIPForDirectRoom) || (!self.roomDataSource.room.isDirect && RiotSettings.shared.roomScreenAllowVoIPForNonDirectRoom);
|
|
return callOptionAllowed && BuildSettings.allowVoIPUsage && self.roomDataSource.mxSession.callManager && self.roomDataSource.room.summary.membersCount.joined >= 2;
|
|
}
|
|
|
|
- (BOOL)isCallActive
|
|
{
|
|
MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId];
|
|
|
|
return (callInRoom && callInRoom.state != MXCallStateEnded)
|
|
|| self.customizedRoomDataSource.jitsiWidget;
|
|
}
|
|
|
|
- (BOOL)canSendStateEventWithType:(MXEventTypeString)eventTypeString
|
|
{
|
|
MXRoomPowerLevels *powerLevels = [self.roomDataSource.roomState powerLevels];
|
|
NSInteger requiredPower = [powerLevels minimumPowerLevelForSendingEventAsStateEvent:eventTypeString];
|
|
NSInteger myPower = [powerLevels powerLevelOfUserWithUserID:self.roomDataSource.mxSession.myUserId];
|
|
return myPower >= requiredPower;
|
|
}
|
|
|
|
/**
|
|
Returns a flag for the current user whether it's privileged to add/remove Jitsi widgets to this room.
|
|
*/
|
|
- (BOOL)canEditJitsiWidget
|
|
{
|
|
return [self canSendStateEventWithType:kWidgetModularEventTypeString];
|
|
}
|
|
|
|
- (void)registerURLPreviewNotifications
|
|
{
|
|
MXWeakify(self);
|
|
|
|
URLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Ensure this is the correct room
|
|
if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomDataSource.roomId])
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the indexPath for the updated cell.
|
|
NSString *updatedEventId = notification.userInfo[@"eventId"];
|
|
NSInteger updatedEventIndex = [self.roomDataSource indexOfCellDataWithEventId:updatedEventId];
|
|
NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForRow:updatedEventIndex inSection:0];
|
|
|
|
// Store the content size and offset before reloading the cell
|
|
CGFloat originalContentSize = self.bubblesTableView.contentSize.height;
|
|
CGPoint contentOffset = self.bubblesTableView.contentOffset;
|
|
|
|
// Only update the content offset if the cell is visible or above the current visible cells.
|
|
BOOL shouldUpdateContentOffset = NO;
|
|
NSIndexPath *lastVisibleIndexPath = [self.bubblesTableView indexPathsForVisibleRows].lastObject;
|
|
if (lastVisibleIndexPath && updatedIndexPath.row < lastVisibleIndexPath.row)
|
|
{
|
|
shouldUpdateContentOffset = YES;
|
|
}
|
|
|
|
// Note: Despite passing in the index path, this reloads the whole table.
|
|
[self dataSource:self.roomDataSource didCellChange:updatedIndexPath];
|
|
|
|
// Update the content offset to include any changes to the scroll view's height.
|
|
if (shouldUpdateContentOffset)
|
|
{
|
|
CGFloat delta = self.bubblesTableView.contentSize.height - originalContentSize;
|
|
contentOffset.y += delta;
|
|
|
|
self.bubblesTableView.contentOffset = contentOffset;
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)refreshRoomTitle
|
|
{
|
|
NSMutableArray *rightBarButtonItems = nil;
|
|
|
|
// Set the right room title view
|
|
if (self.isRoomPreview)
|
|
{
|
|
[self showPreviewHeader:YES];
|
|
}
|
|
else if (self.roomDataSource)
|
|
{
|
|
[self showPreviewHeader:NO];
|
|
|
|
if (self.roomDataSource.isLive)
|
|
{
|
|
rightBarButtonItems = [NSMutableArray new];
|
|
BOOL hasCustomJoinButton = NO;
|
|
|
|
if (self.supportCallOption)
|
|
{
|
|
if (self.roomDataSource.room.summary.membersCount.joined == 2
|
|
&& self.roomDataSource.room.isDirect
|
|
&& !self.mainSession.vc_homeserverConfiguration.jitsi.useFor1To1Calls)
|
|
{
|
|
// voice call button for Matrix call
|
|
UIBarButtonItem *itemVoice = [[UIBarButtonItem alloc] initWithImage:AssetImages.voiceCallHangonIcon.image
|
|
style:UIBarButtonItemStylePlain
|
|
target:self
|
|
action:@selector(onVoiceCallPressed:)];
|
|
itemVoice.accessibilityLabel = [VectorL10n roomAccessibilityCall];
|
|
itemVoice.enabled = !self.isCallActive;
|
|
[rightBarButtonItems addObject:itemVoice];
|
|
|
|
// video call button for Matrix call
|
|
UIBarButtonItem *itemVideo = [self videoCallBarButtonItem];
|
|
itemVideo.enabled = !self.isCallActive;
|
|
[rightBarButtonItems addObject:itemVideo];
|
|
}
|
|
else
|
|
{
|
|
// video call button for Jitsi call
|
|
if (self.isCallActive)
|
|
{
|
|
if (self.isRoomHavingAJitsiCall)
|
|
{
|
|
// show a disabled call button
|
|
UIBarButtonItem *item = [self videoCallBarButtonItem];
|
|
item.enabled = NO;
|
|
[rightBarButtonItems addObject:item];
|
|
}
|
|
else
|
|
{
|
|
UIBarButtonItem *item = [self joinJitsiBarButtonItem];
|
|
[rightBarButtonItems addObject:item];
|
|
|
|
hasCustomJoinButton = YES;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// show a video call button
|
|
// item will still be enabled, and when tapped an alert will be displayed to the user
|
|
UIBarButtonItem *item = [self videoCallBarButtonItem];
|
|
if (!self.canEditJitsiWidget)
|
|
{
|
|
item.image = [AssetImages.videoCall.image vc_withAlpha:0.3];
|
|
}
|
|
[rightBarButtonItems addObject:item];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ([self widgetsCount:NO])
|
|
{
|
|
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:AssetImages.integrationsIcon.image
|
|
style:UIBarButtonItemStylePlain
|
|
target:self
|
|
action:@selector(onIntegrationsPressed:)];
|
|
item.accessibilityLabel = [VectorL10n roomAccessibilityIntegrations];
|
|
if (hasCustomJoinButton)
|
|
{
|
|
item.imageInsets = UIEdgeInsetsMake(0, -5, 0, -5);
|
|
item.landscapeImagePhoneInsets = UIEdgeInsetsMake(0, -5, 0, -5);
|
|
}
|
|
[rightBarButtonItems addObject:item];
|
|
}
|
|
}
|
|
|
|
// Do not change title view class here if the expanded header is visible.
|
|
[self setRoomTitleViewClass:RoomTitleView.class];
|
|
((RoomTitleView*)self.titleView).tapGestureDelegate = self;
|
|
|
|
MXKImageView *userPictureView = ((RoomTitleView*)self.titleView).pictureView;
|
|
|
|
// Set user picture in input toolbar
|
|
if (userPictureView)
|
|
{
|
|
[self.roomDataSource.room.summary setRoomAvatarImageIn:userPictureView];
|
|
}
|
|
|
|
[self refreshMissedDiscussionsCount:YES];
|
|
|
|
if (RiotSettings.shared.enableThreads && !_isWaitingForOtherParticipants)
|
|
{
|
|
if (self.roomDataSource.threadId)
|
|
{
|
|
// in a thread
|
|
if (rightBarButtonItems == nil)
|
|
{
|
|
rightBarButtonItems = [NSMutableArray new];
|
|
}
|
|
UIBarButtonItem *itemThreadMore = [self threadMoreBarButtonItem];
|
|
[rightBarButtonItems insertObject:itemThreadMore atIndex:0];
|
|
}
|
|
else
|
|
{
|
|
// in a regular timeline
|
|
UIBarButtonItem *itemThreadList = [self threadListBarButtonItem];
|
|
[self updateThreadListBarButtonItem:itemThreadList
|
|
with:self.mainSession.threadingService];
|
|
[rightBarButtonItems insertObject:itemThreadList atIndex:0];
|
|
}
|
|
}
|
|
}
|
|
else if (self.isNewDirectChat)
|
|
{
|
|
[self showPreviewHeader:NO];
|
|
|
|
[self setRoomTitleViewClass:RoomTitleView.class];
|
|
MXKImageView *userPictureView = ((RoomTitleView*)self.titleView).pictureView;
|
|
|
|
// Set user picture in input toolbar
|
|
if (userPictureView)
|
|
{
|
|
[userPictureView vc_setRoomAvatarImageWith:self.directChatTargetUser.avatarUrl
|
|
roomId:self.directChatTargetUser.userId
|
|
displayName:self.directChatTargetUser.displayname ?: self.directChatTargetUser.userId
|
|
mediaManager:self.mainSession.mediaManager];
|
|
}
|
|
}
|
|
|
|
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
|
|
}
|
|
|
|
- (void)updateInputToolBarVisibility
|
|
{
|
|
BOOL hideInputToolBar = NO;
|
|
|
|
if (self.roomDataSource)
|
|
{
|
|
hideInputToolBar = (self.roomDataSource.state != MXKDataSourceStateReady);
|
|
}
|
|
|
|
self.inputToolbarView.hidden = hideInputToolBar;
|
|
}
|
|
|
|
- (void)refreshRoomInputToolbar
|
|
{
|
|
MXKImageView *userPictureView;
|
|
|
|
// Show or hide input tool bar
|
|
[self updateInputToolBarVisibility];
|
|
|
|
// Check whether the input toolbar is ready before updating it.
|
|
if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
id<RoomInputToolbarViewProtocol> roomInputToolbarView = (id<RoomInputToolbarViewProtocol>) self.inputToolbarView;
|
|
|
|
// Update encryption decoration if needed
|
|
[self updateEncryptionDecorationForRoomInputToolbar:roomInputToolbarView];
|
|
|
|
// Update actions when the input toolbar refreshed
|
|
[self setupActions];
|
|
|
|
// Update placeholder and hide voice message view
|
|
if (self.isNewDirectChat)
|
|
{
|
|
[self setInputToolBarSendMode:RoomInputToolbarViewSendModeCreateDM forEventWithId:nil];
|
|
[roomInputToolbarView setVoiceMessageToolbarView:nil];
|
|
}
|
|
}
|
|
else if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:DisabledRoomInputToolbarView.class])
|
|
{
|
|
DisabledRoomInputToolbarView *roomInputToolbarView = (DisabledRoomInputToolbarView*)self.inputToolbarView;
|
|
|
|
// Get user picture view in input toolbar
|
|
userPictureView = roomInputToolbarView.pictureView;
|
|
|
|
// For the moment, there is only one reason to use `DisabledRoomInputToolbarView`
|
|
[roomInputToolbarView setDisabledReason:[VectorL10n roomDoNotHavePermissionToPost]];
|
|
}
|
|
|
|
// Set user picture in input toolbar
|
|
if (userPictureView)
|
|
{
|
|
UIImage *preview = [AvatarGenerator generateAvatarForMatrixItem:self.mainSession.myUser.userId withDisplayName:self.mainSession.myUser.displayname];
|
|
|
|
// Suppose the avatar is stored unencrypted on the Matrix media repository.
|
|
userPictureView.enableInMemoryCache = YES;
|
|
[userPictureView setImageURI:self.mainSession.myUser.avatarUrl
|
|
withType:nil
|
|
andImageOrientation:UIImageOrientationUp
|
|
toFitViewSize:userPictureView.frame.size
|
|
withMethod:MXThumbnailingMethodCrop
|
|
previewImage:preview
|
|
mediaManager:self.mainSession.mediaManager];
|
|
[userPictureView.layer setCornerRadius:userPictureView.frame.size.width / 2];
|
|
userPictureView.clipsToBounds = YES;
|
|
}
|
|
}
|
|
|
|
- (void)setInputToolBarSendMode:(RoomInputToolbarViewSendMode)sendMode forEventWithId:(NSString *)eventId
|
|
{
|
|
if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
MXKRoomInputToolbarView <RoomInputToolbarViewProtocol> *roomInputToolbarView = (MXKRoomInputToolbarView <RoomInputToolbarViewProtocol> *) self.inputToolbarView;
|
|
if (eventId)
|
|
{
|
|
MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
|
|
MXRoomMember * roomMember = [self.roomDataSource.roomState.members memberWithUserId:event.sender];
|
|
if (roomMember.displayname.length)
|
|
{
|
|
roomInputToolbarView.eventSenderDisplayName = roomMember.displayname;
|
|
}
|
|
else
|
|
{
|
|
roomInputToolbarView.eventSenderDisplayName = event.sender;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
roomInputToolbarView.eventSenderDisplayName = nil;
|
|
}
|
|
roomInputToolbarView.sendMode = sendMode;
|
|
}
|
|
}
|
|
|
|
- (RoomInputToolbarViewSendMode)inputToolBarSendMode
|
|
{
|
|
RoomInputToolbarViewSendMode sendMode = RoomInputToolbarViewSendModeSend;
|
|
if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:[RoomInputToolbarView class]])
|
|
{
|
|
RoomInputToolbarView *roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView;
|
|
sendMode = roomInputToolbarView.sendMode;
|
|
}
|
|
|
|
return sendMode;
|
|
}
|
|
|
|
- (void)onSwipeGesture:(UISwipeGestureRecognizer*)swipeGestureRecognizer
|
|
{
|
|
UIView *view = swipeGestureRecognizer.view;
|
|
|
|
if (view == self.activitiesView)
|
|
{
|
|
// Dismiss the keyboard when user swipes down on activities view.
|
|
[self.inputToolbarView dismissKeyboard];
|
|
}
|
|
}
|
|
|
|
- (void)updateInputToolBarViewHeight
|
|
{
|
|
// Update the inputToolBar height.
|
|
CGFloat height = [self inputToolbarHeight];
|
|
// Disable animation during the update
|
|
[UIView setAnimationsEnabled:NO];
|
|
[self roomInputToolbarView:self.inputToolbarView heightDidChanged:height completion:nil];
|
|
[UIView setAnimationsEnabled:YES];
|
|
}
|
|
|
|
- (UIImage*)roomEncryptionBadgeImage
|
|
{
|
|
UIImage *encryptionIcon;
|
|
|
|
if (self.isEncryptionEnabled)
|
|
{
|
|
RoomEncryptionTrustLevel roomEncryptionTrustLevel = ((RoomDataSource*)self.roomDataSource).encryptionTrustLevel;
|
|
|
|
encryptionIcon = [EncryptionTrustLevelBadgeImageHelper roomBadgeImageFor:roomEncryptionTrustLevel];
|
|
}
|
|
|
|
return encryptionIcon;
|
|
}
|
|
|
|
- (void)updateInputToolbarEncryptionDecoration
|
|
{
|
|
if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
id<RoomInputToolbarViewProtocol> roomInputToolbarView = (id<RoomInputToolbarViewProtocol>)self.inputToolbarView;
|
|
[self updateEncryptionDecorationForRoomInputToolbar:roomInputToolbarView];
|
|
}
|
|
}
|
|
|
|
- (void)updateTitleViewEncryptionDecoration
|
|
{
|
|
if (![self.titleView isKindOfClass:[RoomTitleView class]])
|
|
{
|
|
return;
|
|
}
|
|
|
|
RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
|
|
roomTitleView.badgeImageView.image = self.roomEncryptionBadgeImage;
|
|
}
|
|
|
|
- (void)updateEncryptionDecorationForRoomInputToolbar:(id<RoomInputToolbarViewProtocol>)roomInputToolbarView
|
|
{
|
|
roomInputToolbarView.isEncryptionEnabled = self.isEncryptionEnabled;
|
|
}
|
|
|
|
- (void)handleLongPressFromCell:(id<MXKCellRendering>)cell withTappedEvent:(MXEvent*)event
|
|
{
|
|
if (event && !self.customizedRoomDataSource.selectedEventId)
|
|
{
|
|
[self showContextualMenuForEvent:event fromSingleTapGesture:NO cell:cell animated:YES];
|
|
}
|
|
}
|
|
|
|
- (void)showReactionHistoryForEventId:(NSString*)eventId animated:(BOOL)animated
|
|
{
|
|
if (self.reactionHistoryCoordinatorBridgePresenter.isPresenting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReactionHistoryCoordinatorBridgePresenter *presenter = [[ReactionHistoryCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:eventId];
|
|
presenter.delegate = self;
|
|
|
|
[presenter presentFrom:self animated:animated];
|
|
|
|
self.reactionHistoryCoordinatorBridgePresenter = presenter;
|
|
}
|
|
|
|
- (void)showCameraControllerAnimated:(BOOL)animated
|
|
{
|
|
CameraPresenter *cameraPresenter = [CameraPresenter new];
|
|
cameraPresenter.delegate = self;
|
|
[cameraPresenter presentCameraFrom:self with:@[MXKUTI.image, MXKUTI.movie] animated:YES];
|
|
|
|
self.cameraPresenter = cameraPresenter;
|
|
}
|
|
|
|
|
|
- (void)showMediaPickerAnimated:(BOOL)animated
|
|
{
|
|
MediaPickerCoordinatorBridgePresenter *mediaPickerPresenter = [[MediaPickerCoordinatorBridgePresenter alloc] initWithSession:self.mainSession mediaUTIs:@[MXKUTI.image, MXKUTI.movie] allowsMultipleSelection:YES];
|
|
mediaPickerPresenter.delegate = self;
|
|
|
|
UIView *sourceView;
|
|
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
|
|
{
|
|
sourceView = ((RoomInputToolbarView*)self.inputToolbarView).attachMediaButton;
|
|
}
|
|
else
|
|
{
|
|
sourceView = self.inputToolbarView;
|
|
}
|
|
|
|
[mediaPickerPresenter presentFrom:self sourceView:sourceView sourceRect:sourceView.bounds animated:YES];
|
|
|
|
self.mediaPickerPresenter = mediaPickerPresenter;
|
|
}
|
|
|
|
- (void)showRoomCreationModal
|
|
{
|
|
[self.roomCreationModalCoordinatorBridgePresenter dismissWithAnimated:NO completion:nil];
|
|
|
|
self.roomCreationModalCoordinatorBridgePresenter = [[RoomCreationModalCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomState:self.roomDataSource.roomState];
|
|
self.roomCreationModalCoordinatorBridgePresenter.delegate = self;
|
|
[self.roomCreationModalCoordinatorBridgePresenter presentFrom:self animated:YES];
|
|
}
|
|
|
|
- (void)showMemberDetails:(MXRoomMember *)member
|
|
{
|
|
if (!member)
|
|
{
|
|
return;
|
|
}
|
|
RoomMemberDetailsViewController *memberViewController = [RoomMemberDetailsViewController roomMemberDetailsViewController];
|
|
|
|
// Set delegate to handle action on member (start chat, mention)
|
|
memberViewController.delegate = self;
|
|
memberViewController.enableMention = (self.inputToolbarView != nil);
|
|
memberViewController.enableVoipCall = NO;
|
|
|
|
[memberViewController displayRoomMember:member withMatrixRoom:self.roomDataSource.room];
|
|
|
|
[self.navigationController pushViewController:memberViewController animated:YES];
|
|
}
|
|
|
|
- (void)showRoomAvatarChange
|
|
{
|
|
[self showRoomInfoWithInitialSection:RoomInfoSectionChangeAvatar animated:YES];
|
|
}
|
|
|
|
- (void)showAddParticipants
|
|
{
|
|
self.participantsInvitePresenter = [[RoomParticipantsInviteCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId];
|
|
self.participantsInvitePresenter.delegate = self;
|
|
[self.participantsInvitePresenter presentFrom:self animated:YES];
|
|
}
|
|
|
|
- (void)showRoomTopicChange
|
|
{
|
|
[self showRoomInfoWithInitialSection:RoomInfoSectionChangeTopic animated:YES];
|
|
}
|
|
|
|
- (void)showRoomInfo
|
|
{
|
|
[self showRoomInfoWithInitialSection:RoomInfoSectionNone animated:YES];
|
|
}
|
|
|
|
- (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection animated:(BOOL)animated
|
|
{
|
|
RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection canAddParticipants: !self.isWaitingForOtherParticipants];
|
|
|
|
self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters];
|
|
|
|
self.roomInfoCoordinatorBridgePresenter.delegate = self;
|
|
[self.roomInfoCoordinatorBridgePresenter pushFrom:self.navigationController animated:animated];
|
|
}
|
|
|
|
- (void)setupActions {
|
|
|
|
if (![self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
return;
|
|
}
|
|
|
|
RoomInputToolbarView *roomInputView = ((RoomInputToolbarView *) self.inputToolbarView);
|
|
MXWeakify(self);
|
|
NSMutableArray *actionItems = [NSMutableArray new];
|
|
if (RiotSettings.shared.roomScreenAllowMediaLibraryAction)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionMediaLibrary.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self showMediaPickerAnimated:YES];
|
|
}]];
|
|
}
|
|
if (RiotSettings.shared.roomScreenAllowStickerAction && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionSticker.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self roomInputToolbarViewPresentStickerPicker];
|
|
}]];
|
|
}
|
|
if (RiotSettings.shared.roomScreenAllowFilesAction)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionFile.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self roomInputToolbarViewDidTapFileUpload];
|
|
}]];
|
|
}
|
|
if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLive.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self roomInputToolbarViewDidTapVoiceBroadcast];
|
|
}]];
|
|
}
|
|
if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionPoll.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self];
|
|
}]];
|
|
}
|
|
if (BuildSettings.locationSharingEnabled && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLocation.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self];
|
|
}]];
|
|
}
|
|
if (RiotSettings.shared.roomScreenAllowCameraAction)
|
|
{
|
|
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionCamera.image andAction:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
|
|
((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
|
|
}
|
|
[self showCameraControllerAnimated:YES];
|
|
}]];
|
|
}
|
|
roomInputView.actionsBar.actionItems = actionItems;
|
|
}
|
|
|
|
- (NSString *)textInputContextIdentifier
|
|
{
|
|
return self.roomDataSource.roomId;
|
|
}
|
|
|
|
- (void)roomInputToolbarViewPresentStickerPicker
|
|
{
|
|
// Search for the sticker picker widget in the user account
|
|
Widget *widget = [[WidgetManager sharedManager] userWidgets:self.roomDataSource.mxSession ofTypes:@[kWidgetTypeStickerPicker]].firstObject;
|
|
|
|
if (widget)
|
|
{
|
|
// Display the widget
|
|
[widget widgetUrl:^(NSString * _Nonnull widgetUrl) {
|
|
|
|
StickerPickerViewController *stickerPickerVC = [[StickerPickerViewController alloc] initWithUrl:widgetUrl forWidget:widget];
|
|
|
|
stickerPickerVC.roomDataSource = self.roomDataSource;
|
|
|
|
[self.navigationController pushViewController:stickerPickerVC animated:YES];
|
|
} failure:^(NSError * _Nonnull error) {
|
|
|
|
MXLogDebug(@"[RoomVC] Cannot display widget %@", widget);
|
|
[self showError:error];
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// The Sticker picker widget is not installed yet. Propose the user to install it
|
|
MXWeakify(self);
|
|
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
NSString *alertMessage = [NSString stringWithFormat:@"%@\n%@",
|
|
[VectorL10n widgetStickerPickerNoStickerpacksAlert],
|
|
[VectorL10n widgetStickerPickerNoStickerpacksAlertAddNow]];
|
|
|
|
UIAlertController *installPrompt = [UIAlertController alertControllerWithTitle:nil
|
|
message:alertMessage
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[installPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n no]
|
|
style:UIAlertActionStyleCancel
|
|
handler:^(UIAlertAction * action)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
|
|
}]];
|
|
|
|
[installPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n yes]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
|
|
// Show the sticker picker settings screen
|
|
IntegrationManagerViewController *modularVC = [[IntegrationManagerViewController alloc]
|
|
initForMXSession:self.roomDataSource.mxSession
|
|
inRoom:self.roomDataSource.roomId
|
|
screen:[IntegrationManagerViewController screenForWidget:kWidgetTypeStickerPicker]
|
|
widgetId:nil];
|
|
|
|
[self presentViewController:modularVC animated:NO completion:nil];
|
|
}]];
|
|
|
|
[installPrompt mxk_setAccessibilityIdentifier:@"RoomVCStickerPickerAlert"];
|
|
[self presentViewController:installPrompt animated:YES completion:nil];
|
|
currentAlert = installPrompt;
|
|
}
|
|
}
|
|
|
|
- (void)roomInputToolbarViewDidTapFileUpload
|
|
{
|
|
MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new];
|
|
documentPickerPresenter.delegate = self;
|
|
|
|
NSArray<MXKUTI*> *allowedUTIs = @[MXKUTI.data];
|
|
[documentPickerPresenter presentDocumentPickerWith:allowedUTIs from:self animated:YES completion:nil];
|
|
|
|
self.documentPickerPresenter = documentPickerPresenter;
|
|
}
|
|
|
|
- (void)roomInputToolbarViewDidTapVoiceBroadcast
|
|
{
|
|
// Check first the room permission
|
|
if (![self canSendStateEventWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastPermissionDeniedMessage]];
|
|
return;
|
|
}
|
|
|
|
MXSession* session = self.roomDataSource.mxSession;
|
|
// Check whether the user is not already broadcasting here or in another room
|
|
if (session.voiceBroadcastService)
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastAlreadyInProgressMessage]];
|
|
return;
|
|
}
|
|
|
|
// Prevents listening a VB when recording a new one
|
|
[VoiceBroadcastPlaybackProvider.shared pausePlaying];
|
|
|
|
// Check connectivity
|
|
if ([AppDelegate theDelegate].isOffline)
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]];
|
|
return;
|
|
}
|
|
|
|
// Request the voice broadcast service to start recording - No service is returned if someone else is already broadcasting in the room
|
|
[session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) {
|
|
if (voiceBroadcastService) {
|
|
[voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { } failure:^(NSError * _Nonnull error) {
|
|
[self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]];
|
|
[session tearDownVoiceBroadcastService];
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastBlockedBySomeoneElseMessage]];
|
|
}
|
|
}];
|
|
}
|
|
|
|
/**
|
|
Send a video asset via the room input toolbar prompting the user for the conversion preset to use
|
|
if the `showMediaCompressionPrompt` setting has been enabled.
|
|
@param videoAsset The video asset to send
|
|
@param isPhotoLibraryAsset Whether the asset was picked from the user's photo library.
|
|
*/
|
|
- (void)sendVideoAsset:(AVAsset *)videoAsset isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
|
|
{
|
|
if (![self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (RiotSettings.shared.showMediaCompressionPrompt)
|
|
{
|
|
// Show the video conversion prompt for the user to select what size video they would like to send.
|
|
UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset
|
|
withCompletion:^(NSString *presetName) {
|
|
// When the preset name is missing, the user cancelled.
|
|
if (!presetName)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Set the chosen preset and send the video (conversion takes place in the SDK).
|
|
[MXSDKOptions sharedInstance].videoConversionPresetName = presetName;
|
|
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
[self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}];
|
|
|
|
UIView *sourceView;
|
|
|
|
if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
|
|
{
|
|
sourceView = ((RoomInputToolbarView*)self.inputToolbarView).attachMediaButton;
|
|
}
|
|
else
|
|
{
|
|
sourceView = self.inputToolbarView;
|
|
}
|
|
|
|
compressionPrompt.popoverPresentationController.sourceView = sourceView;
|
|
compressionPrompt.popoverPresentationController.sourceRect = sourceView.bounds;
|
|
|
|
[self presentViewController:compressionPrompt animated:YES completion:nil];
|
|
}
|
|
else
|
|
{
|
|
// Otherwise default to 1080p and send the video.
|
|
[MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
|
|
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
[self.inputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)showRoomWithId:(NSString*)roomId
|
|
{
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate roomViewController:self showRoomWithId:roomId eventId:nil];
|
|
}
|
|
else
|
|
{
|
|
[[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:self.roomDataSource.mxSession];
|
|
}
|
|
}
|
|
|
|
- (void)leaveRoom
|
|
{
|
|
[self startActivityIndicator];
|
|
|
|
[self.roomDataSource.room leave:^{
|
|
|
|
[self stopActivityIndicator];
|
|
[self notifyDelegateOnLeaveRoomIfNecessary];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
[self stopActivityIndicator];
|
|
MXLogDebug(@"[RoomVC] Failed to reject an invited room (%@) failed", self.roomDataSource.room.roomId);
|
|
|
|
}];
|
|
}
|
|
|
|
- (void)notifyDelegateOnLeaveRoomIfNecessary {
|
|
if (isRoomLeft) {
|
|
return;
|
|
}
|
|
isRoomLeft = YES;
|
|
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate roomViewControllerDidLeaveRoom:self];
|
|
}
|
|
else
|
|
{
|
|
[[AppDelegate theDelegate] restoreInitialDisplay:^{}];
|
|
}
|
|
}
|
|
|
|
- (void)roomPreviewDidTapCancelAction
|
|
{
|
|
// Decline this invitation = leave this page
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate roomViewControllerPreviewDidTapCancel:self];
|
|
}
|
|
else
|
|
{
|
|
[[AppDelegate theDelegate] restoreInitialDisplay:^{}];
|
|
}
|
|
}
|
|
|
|
- (void)startChatWithUserId:(NSString *)userId completion:(void (^)(void))completion
|
|
{
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate roomViewController:self startChatWithUserId:userId completion:completion];
|
|
}
|
|
else
|
|
{
|
|
[[AppDelegate theDelegate] showNewDirectChat:userId withMatrixSession:self.mainSession completion:completion];
|
|
}
|
|
}
|
|
|
|
- (void)showError:(NSError*)error
|
|
{
|
|
[[AppDelegate theDelegate] showErrorAsAlert:error];
|
|
}
|
|
|
|
- (UIAlertController*)showAlertWithTitle:(NSString*)title message:(NSString*)message
|
|
{
|
|
return [[AppDelegate theDelegate] showAlertWithTitle:title message:message];
|
|
}
|
|
|
|
- (ScreenPresentationParameters*)buildUniversalLinkPresentationParameters
|
|
{
|
|
return [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:BuildSettings.allowSplitViewDetailsScreenStacking sender:self sourceView:nil];
|
|
}
|
|
|
|
- (BOOL)handleUniversalLinkURL:(NSURL*)url
|
|
{
|
|
ScreenPresentationParameters *screenParameters = [self buildUniversalLinkPresentationParameters];
|
|
UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithUrl:url
|
|
presentationParameters:screenParameters];
|
|
return [self handleUniversalLinkWithParameters:parameters];
|
|
}
|
|
|
|
- (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)url
|
|
{
|
|
ScreenPresentationParameters *screenParameters = [self buildUniversalLinkPresentationParameters];
|
|
UniversalLink *universalLink = [[UniversalLink alloc] initWithUrl:url];
|
|
UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment
|
|
universalLink:universalLink
|
|
presentationParameters:screenParameters];
|
|
return [self handleUniversalLinkWithParameters:parameters];
|
|
}
|
|
|
|
- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters
|
|
{
|
|
Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerTimeline;
|
|
|
|
if (self.delegate)
|
|
{
|
|
return [self.delegate roomViewController:self handleUniversalLinkWithParameters:parameters];
|
|
}
|
|
else
|
|
{
|
|
return [[AppDelegate theDelegate] handleUniversalLinkWithParameters:parameters];
|
|
}
|
|
}
|
|
|
|
- (void)setupCompletionSuggestionViewIfNeeded
|
|
{
|
|
if(!self.isViewLoaded) {
|
|
return;
|
|
}
|
|
|
|
UIViewController *suggestionsViewController = self.completionSuggestionCoordinator.toPresentable;
|
|
|
|
if (!suggestionsViewController)
|
|
{
|
|
return;
|
|
}
|
|
|
|
[suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO];
|
|
|
|
[self addChildViewController:suggestionsViewController];
|
|
[self.completionSuggestionContainerView addSubview:suggestionsViewController.view];
|
|
|
|
[NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.topAnchor],
|
|
[suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.leadingAnchor],
|
|
[suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.trailingAnchor],
|
|
[suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.completionSuggestionContainerView.bottomAnchor],]];
|
|
|
|
[suggestionsViewController didMoveToParentViewController:self];
|
|
}
|
|
|
|
- (void)updateTopBanners
|
|
{
|
|
[self.view bringSubviewToFront:self.topBannersStackView];
|
|
|
|
[self updateLiveLocationBannerViewVisibility];
|
|
}
|
|
|
|
- (void)showEmojiPickerForEventId:(NSString *)eventId
|
|
{
|
|
EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter = [[EmojiPickerCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:eventId];
|
|
emojiPickerCoordinatorBridgePresenter.delegate = self;
|
|
|
|
NSInteger cellRow = [self.roomDataSource indexOfCellDataWithEventId:eventId];
|
|
|
|
UIView *sourceView;
|
|
CGRect sourceRect = CGRectNull;
|
|
|
|
if (cellRow >= 0)
|
|
{
|
|
NSIndexPath *cellIndexPath = [NSIndexPath indexPathForRow:cellRow inSection:0];
|
|
UITableViewCell *cell = [self.bubblesTableView cellForRowAtIndexPath:cellIndexPath];
|
|
sourceView = cell;
|
|
|
|
if ([cell isKindOfClass:[MXKRoomBubbleTableViewCell class]])
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
|
|
NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:eventId];
|
|
sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
|
|
}
|
|
|
|
}
|
|
|
|
[emojiPickerCoordinatorBridgePresenter presentFrom:self sourceView:sourceView sourceRect:sourceRect animated:YES];
|
|
self.emojiPickerCoordinatorBridgePresenter = emojiPickerCoordinatorBridgePresenter;
|
|
}
|
|
|
|
#pragma mark - Jitsi
|
|
|
|
- (void)showJitsiCallWithWidget:(Widget*)widget
|
|
{
|
|
[[AppDelegate theDelegate].callPresenter displayJitsiCallWithWidget:widget];
|
|
}
|
|
|
|
- (void)endActiveJitsiCall
|
|
{
|
|
[[AppDelegate theDelegate].callPresenter endActiveJitsiCall];
|
|
}
|
|
|
|
- (BOOL)isRoomHavingAJitsiCall
|
|
{
|
|
return [self isRoomHavingAJitsiCallForWidgetId:self.roomDataSource.roomId];
|
|
}
|
|
|
|
- (BOOL)isRoomHavingAJitsiCallForWidgetId:(NSString*)widgetId
|
|
{
|
|
return [[AppDelegate theDelegate].callPresenter.jitsiVC.widget.roomId isEqualToString:widgetId];
|
|
}
|
|
|
|
#pragma mark - Dialpad
|
|
|
|
- (void)openDialpad
|
|
{
|
|
DialpadViewController *controller = [DialpadViewController instantiateWithConfiguration:[DialpadConfiguration default]];
|
|
controller.delegate = self;
|
|
self.customSizedPresentationController = [[CustomSizedPresentationController alloc] initWithPresentedViewController:controller presentingViewController:self];
|
|
self.customSizedPresentationController.dismissOnBackgroundTap = NO;
|
|
self.customSizedPresentationController.cornerRadius = 16;
|
|
|
|
controller.transitioningDelegate = self.customSizedPresentationController;
|
|
[self presentViewController:controller animated:YES completion:nil];
|
|
}
|
|
|
|
#pragma mark - DialpadViewControllerDelegate
|
|
|
|
- (void)dialpadViewControllerDidTapCall:(DialpadViewController *)viewController withPhoneNumber:(NSString *)phoneNumber
|
|
{
|
|
if (self.mainSession.callManager && phoneNumber.length > 0)
|
|
{
|
|
[self startActivityIndicator];
|
|
|
|
[viewController dismissViewControllerAnimated:YES completion:^{
|
|
MXWeakify(self);
|
|
[self.mainSession.callManager placeCallAgainst:phoneNumber withVideo:NO success:^(MXCall * _Nonnull call) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
self.customSizedPresentationController = nil;
|
|
|
|
// do nothing extra here. UI will be handled automatically by the CallService.
|
|
} failure:^(NSError * _Nullable error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
}];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)dialpadViewControllerDidTapClose:(DialpadViewController *)viewController
|
|
{
|
|
[viewController dismissViewControllerAnimated:YES completion:nil];
|
|
self.customSizedPresentationController = nil;
|
|
}
|
|
|
|
#pragma mark - Hide/Show preview header
|
|
|
|
- (void)showPreviewHeader:(BOOL)isVisible
|
|
{
|
|
if (self.previewHeaderContainer && self.previewHeaderContainer.isHidden == isVisible)
|
|
{
|
|
// Check conditions before making the preview room header visible.
|
|
// This operation is ignored if a screen rotation is in progress,
|
|
// or if the view controller is not embedded inside a split view controller yet.
|
|
if (isVisible && (isSizeTransitionInProgress == YES || !self.splitViewController))
|
|
{
|
|
MXLogDebug(@"[RoomVC] Show preview header ignored");
|
|
return;
|
|
}
|
|
|
|
if (isVisible)
|
|
{
|
|
PreviewRoomTitleView *previewHeader = [PreviewRoomTitleView roomTitleView];
|
|
previewHeader.delegate = self;
|
|
previewHeader.tapGestureDelegate = self;
|
|
previewHeader.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[self.previewHeaderContainer addSubview:previewHeader];
|
|
|
|
self->previewHeader = previewHeader;
|
|
|
|
// Force preview header in full width
|
|
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
|
|
attribute:NSLayoutAttributeLeading
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.previewHeaderContainer
|
|
attribute:NSLayoutAttributeLeading
|
|
multiplier:1.0
|
|
constant:0];
|
|
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
|
|
attribute:NSLayoutAttributeTrailing
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.previewHeaderContainer
|
|
attribute:NSLayoutAttributeTrailing
|
|
multiplier:1.0
|
|
constant:0];
|
|
// Vertical constraints are required for iOS > 8
|
|
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
|
|
attribute:NSLayoutAttributeTop
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.previewHeaderContainer
|
|
attribute:NSLayoutAttributeTop
|
|
multiplier:1.0
|
|
constant:0];
|
|
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:previewHeader
|
|
attribute:NSLayoutAttributeBottom
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.previewHeaderContainer
|
|
attribute:NSLayoutAttributeBottom
|
|
multiplier:1.0
|
|
constant:0];
|
|
|
|
[NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
|
|
|
|
if (roomPreviewData)
|
|
{
|
|
previewHeader.roomPreviewData = roomPreviewData;
|
|
}
|
|
else if (self.roomDataSource)
|
|
{
|
|
previewHeader.mxRoom = self.roomDataSource.room;
|
|
}
|
|
|
|
self.previewHeaderContainer.hidden = NO;
|
|
|
|
// Finalize preview header display according to the screen orientation
|
|
[self refreshPreviewHeader:UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation])];
|
|
}
|
|
else
|
|
{
|
|
[previewHeader removeFromSuperview];
|
|
previewHeader = nil;
|
|
|
|
self.previewHeaderContainer.hidden = YES;
|
|
|
|
// Consider the main navigation controller if the current view controller is embedded inside a split view controller.
|
|
UINavigationController *mainNavigationController = self.navigationController;
|
|
if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
|
|
{
|
|
mainNavigationController = self.splitViewController.viewControllers.firstObject;
|
|
}
|
|
|
|
// Set a default title view class without handling tap gesture (Let [self refreshRoomTitle] refresh this view correctly).
|
|
[self setRoomTitleViewClass:RoomTitleView.class];
|
|
|
|
// Remove the shadow image used to hide the bottom border of the navigation bar when the preview header is displayed
|
|
[mainNavigationController.navigationBar setShadowImage:nil];
|
|
[mainNavigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
|
|
|
|
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
|
|
animations:^{
|
|
|
|
self.bubblesTableViewTopConstraint.constant = 0;
|
|
|
|
// Force to render the view
|
|
[self forceLayoutRefresh];
|
|
|
|
}
|
|
completion:^(BOOL finished){
|
|
}];
|
|
}
|
|
}
|
|
|
|
// Consider the main navigation controller if the current view controller is embedded inside a split view controller.
|
|
UINavigationController *mainNavigationController = self.navigationController;
|
|
if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
|
|
{
|
|
mainNavigationController = self.splitViewController.viewControllers.firstObject;
|
|
}
|
|
mainNavigationController.navigationBar.translucent = isVisible;
|
|
self.navigationController.navigationBar.translucent = isVisible;
|
|
}
|
|
|
|
- (void)refreshPreviewHeader:(BOOL)isLandscapeOriented
|
|
{
|
|
if (previewHeader)
|
|
{
|
|
if (isLandscapeOriented
|
|
&& [GBDeviceInfo deviceInfo].family != GBDeviceFamilyiPad)
|
|
{
|
|
CGRect frame = self.navigationController.navigationBar.frame;
|
|
|
|
previewHeader.mainHeaderContainer.hidden = YES;
|
|
previewHeader.mainHeaderBackgroundHeightConstraint.constant = frame.size.height + (frame.origin.y > 0 ? frame.origin.y : 0);
|
|
|
|
[self setRoomTitleViewClass:RoomTitleView.class];
|
|
// We don't want to handle tap gesture here
|
|
|
|
// Remove details icon
|
|
RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView;
|
|
|
|
// Set preview data to provide the room name
|
|
roomTitleView.roomPreviewData = roomPreviewData;
|
|
}
|
|
else
|
|
{
|
|
previewHeader.mainHeaderContainer.hidden = NO;
|
|
previewHeader.mainHeaderBackgroundHeightConstraint.constant = previewHeader.mainHeaderContainer.frame.size.height;
|
|
|
|
if ([previewHeader isKindOfClass:PreviewRoomTitleView.class])
|
|
{
|
|
// In case of preview, update the header height so that we can
|
|
// display as much as possible the room topic in this header.
|
|
// Note: the header height is handled by the previewHeader.mainHeaderBackgroundHeightConstraint.
|
|
PreviewRoomTitleView *previewRoomTitleView = (PreviewRoomTitleView *)previewHeader;
|
|
|
|
// Compute the height required to display all the room topic
|
|
CGSize sizeThatFitsTextView = [previewRoomTitleView.roomTopic sizeThatFits:CGSizeMake(previewRoomTitleView.roomTopic.frame.size.width, MAXFLOAT)];
|
|
|
|
// Increase the preview header height according to the room topic height
|
|
// but limit it in order to let room for room messages at the screen bottom.
|
|
// This free space depends on the device.
|
|
// On an iphone 5 screen, the room topic height cannot be more than 50px.
|
|
// Then, on larger screen, we can allow it a bit more height but we
|
|
// apply a factor to give more priority to the display of more messages.
|
|
CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
|
|
CGFloat maxRoomTopicHeight = 50 + (screenHeight - 568) / 3;
|
|
|
|
CGFloat additionalHeight = MIN(maxRoomTopicHeight, sizeThatFitsTextView.height)
|
|
- previewRoomTitleView.roomTopic.frame.size.height;
|
|
|
|
previewHeader.mainHeaderBackgroundHeightConstraint.constant += additionalHeight;
|
|
}
|
|
|
|
[self setRoomTitleViewClass:RoomAvatarTitleView.class];
|
|
// Note the avatar title view does not define tap gesture.
|
|
|
|
previewHeader.roomAvatar.alpha = 0.0;
|
|
|
|
// Set the avatar provided in preview data
|
|
if (roomPreviewData.roomAvatarUrl)
|
|
{
|
|
previewHeader.roomAvatarURL = roomPreviewData.roomAvatarUrl;
|
|
}
|
|
else if (roomPreviewData.roomId && roomPreviewData.roomName)
|
|
{
|
|
previewHeader.roomAvatarPlaceholder = [AvatarGenerator generateAvatarForMatrixItem:roomPreviewData.roomId withDisplayName:roomPreviewData.roomName];
|
|
}
|
|
else
|
|
{
|
|
previewHeader.roomAvatarPlaceholder = [MXKTools paintImage:AssetImages.placeholder.image
|
|
withColor:ThemeService.shared.theme.tintColor];
|
|
}
|
|
}
|
|
|
|
// Force the layout of previewHeader to update the position of 'bottomBorderView' which is used
|
|
// to define the actual height of the preview container.
|
|
[previewHeader layoutIfNeeded];
|
|
CGRect frame = previewHeader.bottomBorderView.frame;
|
|
self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height;
|
|
|
|
// Consider the main navigation controller if the current view controller is embedded inside a split view controller.
|
|
UINavigationController *mainNavigationController = self.navigationController;
|
|
if (self.splitViewController.isCollapsed && self.splitViewController.viewControllers.count)
|
|
{
|
|
mainNavigationController = self.splitViewController.viewControllers.firstObject;
|
|
}
|
|
|
|
// When the preview header is displayed, we hide the bottom border of the navigation bar (the shadow image).
|
|
// The default shadow image is nil. When non-nil, this property represents a custom shadow image to show instead
|
|
// of the default. For a custom shadow image to be shown, a custom background image must also be set with the
|
|
// setBackgroundImage:forBarMetrics: method. If the default background image is used, then the default shadow
|
|
// image will be used regardless of the value of this property.
|
|
UIImage *shadowImage = [[UIImage alloc] init];
|
|
[mainNavigationController.navigationBar setShadowImage:shadowImage];
|
|
[mainNavigationController.navigationBar setBackgroundImage:shadowImage forBarMetrics:UIBarMetricsDefault];
|
|
|
|
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn
|
|
animations:^{
|
|
|
|
self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top;
|
|
|
|
self->previewHeader.roomAvatar.alpha = 1;
|
|
|
|
// Force to render the view
|
|
[self forceLayoutRefresh];
|
|
|
|
}
|
|
completion:^(BOOL finished){
|
|
}];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Preview
|
|
|
|
- (void)displayRoomPreview:(RoomPreviewData *)previewData
|
|
{
|
|
// Release existing room data source or preview
|
|
[self displayRoom:nil];
|
|
|
|
if (previewData)
|
|
{
|
|
self.eventsAcknowledgementEnabled = NO;
|
|
|
|
[self addMatrixSession:previewData.mxSession];
|
|
|
|
roomPreviewData = previewData;
|
|
|
|
[self refreshRoomTitle];
|
|
|
|
if (roomPreviewData.roomDataSource)
|
|
{
|
|
[super displayRoom:roomPreviewData.roomDataSource];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - New discussion
|
|
|
|
- (void)displayNewDirectChatWithTargetUser:(nonnull MXUser*)directChatTargetUser session:(nonnull MXSession*)session
|
|
{
|
|
// `[displayRoom:]` may require the session, setting it here before calling it
|
|
[self addMatrixSession:session];
|
|
|
|
// Release existing room data source or preview
|
|
[self displayRoom:nil];
|
|
|
|
self.directChatTargetUser = directChatTargetUser;
|
|
|
|
self.eventsAcknowledgementEnabled = NO;
|
|
|
|
[self refreshRoomTitle];
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
|
|
#pragma mark - MXKDataSourceDelegate
|
|
|
|
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
|
|
{
|
|
RoomTimelineCellIdentifier cellIdentifier = [self cellIdentifierForCellData:cellData andRoomDataSource:self.customizedRoomDataSource];
|
|
|
|
RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared];
|
|
|
|
return [timelineConfiguration.currentStyle.cellProvider cellViewClassForCellIdentifier:cellIdentifier];;
|
|
}
|
|
|
|
- (RoomTimelineCellIdentifier)cellIdentifierForCellData:(MXKCellData*)cellData andRoomDataSource:(RoomDataSource *)customizedRoomDataSource;
|
|
{
|
|
// Sanity check
|
|
if (![cellData conformsToProtocol:@protocol(MXKRoomBubbleCellDataStoring)])
|
|
{
|
|
return RoomTimelineCellIdentifierUnknown;
|
|
}
|
|
|
|
BOOL showEncryptionBadge = NO;
|
|
RoomTimelineCellIdentifier cellIdentifier;
|
|
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = (id<MXKRoomBubbleCellDataStoring>)cellData;
|
|
|
|
MXKRoomBubbleCellData *roomBubbleCellData;
|
|
|
|
if ([bubbleData isKindOfClass:MXKRoomBubbleCellData.class])
|
|
{
|
|
roomBubbleCellData = (MXKRoomBubbleCellData*)bubbleData;
|
|
showEncryptionBadge = roomBubbleCellData.containsBubbleComponentWithEncryptionBadge;
|
|
}
|
|
|
|
// Select the suitable table view cell class, by considering first the empty bubble cell.
|
|
if (bubbleData.hasNoDisplay)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierEmpty;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreationIntro)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierRoomCreationIntro;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierRoomPredecessor;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval)
|
|
{
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationIncomingRequestApprovalWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationIncomingRequestApproval;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationRequest)
|
|
{
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationRequestStatusWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationRequestStatus;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagKeyVerificationConclusion)
|
|
{
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierKeyVerificationConclusionWithPaginationTitle : RoomTimelineCellIdentifierKeyVerificationConclusion;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagMembership)
|
|
{
|
|
if (bubbleData.collapsed)
|
|
{
|
|
if (bubbleData.nextCollapsableCellData)
|
|
{
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipCollapsedWithPaginationTitle : RoomTimelineCellIdentifierMembershipCollapsed;
|
|
}
|
|
else
|
|
{
|
|
// Use a normal membership cell for a single membership event
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipWithPaginationTitle : RoomTimelineCellIdentifierMembership;
|
|
}
|
|
}
|
|
else if (bubbleData.collapsedAttributedTextMessage)
|
|
{
|
|
// The cell (and its series) is not collapsed but this cell is the first
|
|
// of the series. So, use the cell with the "collapse" button.
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipExpandedWithPaginationTitle : RoomTimelineCellIdentifierMembershipExpanded;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierMembershipWithPaginationTitle : RoomTimelineCellIdentifierMembership;
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagRoomCreateConfiguration)
|
|
{
|
|
cellIdentifier = bubbleData.isPaginationFirstBubble ? RoomTimelineCellIdentifierRoomCreationCollapsedWithPaginationTitle : RoomTimelineCellIdentifierRoomCreationCollapsed;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagCall)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierDirectCallStatus;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierGroupCallStatus;
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagRTCCallNotify)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierMatrixRTCCall;
|
|
}
|
|
else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage || bubbleData.attachment.type == MXKAttachmentTypeAudio)
|
|
{
|
|
if (bubbleData.isIncoming)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessageWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessageWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessage;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessageWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessageWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessage;
|
|
}
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagPoll)
|
|
{
|
|
if (bubbleData.isIncoming)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingPollWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingPollWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingPoll;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingPollWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingPollWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingPoll;
|
|
}
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagLocation || bubbleData.tag == RoomBubbleCellDataTagLiveLocation)
|
|
{
|
|
if (bubbleData.isIncoming)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingLocationWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingLocationWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingLocation;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingLocationWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingLocationWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingLocation;
|
|
}
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastPlayback)
|
|
{
|
|
if (bubbleData.isIncoming)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback;
|
|
}
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastRecord)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder;
|
|
}
|
|
}
|
|
|
|
else if (roomBubbleCellData.getFirstBubbleComponentWithDisplay.event.isEmote)
|
|
{
|
|
if (bubbleData.isIncoming)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierIncomingEmoteWithPaginationTitleWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingEmoteWithPaginationTitle;
|
|
}
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingEmoteWithoutSenderInfo;
|
|
}
|
|
else if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncryptedWithoutSenderName : RoomTimelineCellIdentifierIncomingEmoteWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingEmoteEncrypted : RoomTimelineCellIdentifierIncomingEmote;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierOutgoingEmoteWithPaginationTitleWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingEmoteWithPaginationTitle;
|
|
}
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingEmoteWithoutSenderInfo;
|
|
}
|
|
else if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncryptedWithoutSenderName : RoomTimelineCellIdentifierOutgoingEmoteWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingEmoteEncrypted : RoomTimelineCellIdentifierOutgoingEmote;
|
|
}
|
|
}
|
|
}
|
|
else if (bubbleData.isIncoming)
|
|
{
|
|
if (bubbleData.isAttachmentWithThumbnail)
|
|
{
|
|
// Check whether the provided celldata corresponds to a selected sticker
|
|
if (customizedRoomDataSource.selectedEventId && (bubbleData.attachment.type == MXKAttachmentTypeSticker) && [bubbleData.attachment.eventId isEqualToString:customizedRoomDataSource.selectedEventId])
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierSelectedSticker;
|
|
}
|
|
else if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingAttachmentWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingAttachmentWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncrypted : RoomTimelineCellIdentifierIncomingAttachment;
|
|
}
|
|
}
|
|
else if (bubbleData.isAttachment)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitleWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingTextMessageWithPaginationTitle;
|
|
}
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderInfo;
|
|
}
|
|
else if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncryptedWithoutSenderName : RoomTimelineCellIdentifierIncomingTextMessageWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingTextMessageEncrypted : RoomTimelineCellIdentifierIncomingTextMessage;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Handle here outgoing bubbles
|
|
if (bubbleData.isAttachmentWithThumbnail)
|
|
{
|
|
// Check whether the provided celldata corresponds to a selected sticker
|
|
if (customizedRoomDataSource.selectedEventId && (bubbleData.attachment.type == MXKAttachmentTypeSticker) && [bubbleData.attachment.eventId isEqualToString:customizedRoomDataSource.selectedEventId])
|
|
{
|
|
cellIdentifier = RoomTimelineCellIdentifierSelectedSticker;
|
|
}
|
|
else if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingAttachmentWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingAttachmentWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncrypted : RoomTimelineCellIdentifierOutgoingAttachment;
|
|
}
|
|
}
|
|
else if (bubbleData.isAttachment)
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle;
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData.isPaginationFirstBubble)
|
|
{
|
|
if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitleWithoutSenderName : RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitleWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingTextMessageWithPaginationTitle;
|
|
}
|
|
}
|
|
else if (bubbleData.shouldHideSenderInformation)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderInfo;
|
|
}
|
|
else if (bubbleData.shouldHideSenderName)
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncryptedWithoutSenderName : RoomTimelineCellIdentifierOutgoingTextMessageWithoutSenderName;
|
|
}
|
|
else
|
|
{
|
|
cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingTextMessageEncrypted : RoomTimelineCellIdentifierOutgoingTextMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
return cellIdentifier;
|
|
}
|
|
|
|
#pragma mark - MXKDataSource delegate
|
|
|
|
- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary *)userInfo
|
|
{
|
|
// Handle here user actions on bubbles for Vector app
|
|
if (self.customizedRoomDataSource)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData;
|
|
|
|
if ([cell isKindOfClass:[MXKRoomBubbleTableViewCell class]])
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
|
|
bubbleData = roomBubbleTableViewCell.bubbleData;
|
|
}
|
|
|
|
|
|
if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAvatarView])
|
|
{
|
|
MXRoomMember *member = [self.roomDataSource.roomState.members memberWithUserId:userInfo[kMXKRoomBubbleCellUserIdKey]];
|
|
[self showMemberDetails:member];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnAvatarView])
|
|
{
|
|
// Add the member display name in text input
|
|
MXRoomMember *roomMember = [self.roomDataSource.roomState.members memberWithUserId:userInfo[kMXKRoomBubbleCellUserIdKey]];
|
|
if (roomMember)
|
|
{
|
|
[self mention:roomMember];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellStopShareButtonPressed])
|
|
{
|
|
NSString *beaconInfoEventId;
|
|
|
|
if ([bubbleData isKindOfClass:[RoomBubbleCellData class]])
|
|
{
|
|
RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleData;
|
|
beaconInfoEventId = roomBubbleCellData.beaconInfoSummary.id;
|
|
}
|
|
|
|
[self.delegate roomViewControllerDidStopLiveLocationSharing:self beaconInfoEventId:beaconInfoEventId];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellRetryShareButtonPressed])
|
|
{
|
|
MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
if (selectedEvent)
|
|
{
|
|
// TODO: - Implement retry live location action
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnMessageTextView] || [actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnContentView])
|
|
{
|
|
// Retrieve the tapped event
|
|
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
|
|
// Check whether a selection already exist or not
|
|
if (self.customizedRoomDataSource.selectedEventId)
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagLiveLocation)
|
|
{
|
|
[self.delegate roomViewController:self didRequestLiveLocationPresentationForBubbleData:bubbleData];
|
|
}
|
|
else if (tappedEvent)
|
|
{
|
|
if (tappedEvent.eventType == MXEventTypeRoomCreate)
|
|
{
|
|
// Handle tap on RoomPredecessorBubbleCell
|
|
MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:tappedEvent.content];
|
|
NSString *predecessorRoomId = createContent.roomPredecessorInfo.roomId;
|
|
|
|
if (predecessorRoomId)
|
|
{
|
|
// Show predecessor room
|
|
Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerTombstone;
|
|
[self showRoomWithId:predecessorRoomId];
|
|
}
|
|
else
|
|
{
|
|
// Show contextual menu on single tap if bubble is not collapsed
|
|
if (bubbleData.collapsed)
|
|
{
|
|
// Do nothing here as we display room creation modal only if the user taps on the room name
|
|
}
|
|
else
|
|
{
|
|
[self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES];
|
|
}
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagCall)
|
|
{
|
|
if ([bubbleData isKindOfClass:[RoomBubbleCellData class]])
|
|
{
|
|
// post notification `RoomCallTileTapped`
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RoomCallTileTappedNotification object:bubbleData];
|
|
|
|
preventBubblesTableViewScroll = YES;
|
|
[self selectEventWithId:tappedEvent.eventId];
|
|
}
|
|
}
|
|
else if (bubbleData.tag == RoomBubbleCellDataTagGroupCall)
|
|
{
|
|
if ([bubbleData isKindOfClass:[RoomBubbleCellData class]])
|
|
{
|
|
// post notification `RoomGroupCallTileTapped`
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RoomGroupCallTileTappedNotification object:bubbleData];
|
|
|
|
preventBubblesTableViewScroll = YES;
|
|
[self selectEventWithId:tappedEvent.eventId];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Show contextual menu on single tap if bubble is not collapsed
|
|
if (bubbleData.collapsed)
|
|
{
|
|
[self selectEventWithId:tappedEvent.eventId];
|
|
}
|
|
else
|
|
{
|
|
if (tappedEvent.location) {
|
|
[_delegate roomViewController:self didRequestLocationPresentationForEvent:tappedEvent bubbleData:bubbleData];
|
|
} else {
|
|
[self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnOverlayContainer])
|
|
{
|
|
// Cancel the current event selection
|
|
[self cancelEventSelection];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellRiotEditButtonPressed])
|
|
{
|
|
[self dismissKeyboard];
|
|
|
|
MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
|
|
if (selectedEvent)
|
|
{
|
|
[self showContextualMenuForEvent:selectedEvent fromSingleTapGesture:YES cell:cell animated:YES];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellKeyVerificationIncomingRequestAcceptPressed])
|
|
{
|
|
NSString *eventId = userInfo[kMXKRoomBubbleCellEventIdKey];
|
|
|
|
RoomDataSource *roomDataSource = (RoomDataSource*)self.roomDataSource;
|
|
|
|
[roomDataSource acceptVerificationRequestForEventId:eventId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
[self showError:error];
|
|
}];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed])
|
|
{
|
|
NSString *eventId = userInfo[kMXKRoomBubbleCellEventIdKey];
|
|
|
|
RoomDataSource *roomDataSource = (RoomDataSource*)self.roomDataSource;
|
|
|
|
[roomDataSource declineVerificationRequestForEventId:eventId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
[self showError:error];
|
|
}];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView])
|
|
{
|
|
if (((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventSentState == MXEventSentStateFailed)
|
|
{
|
|
// Shortcut: when clicking on an unsent media, show the action sheet to resend it
|
|
NSString *eventId = ((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventId;
|
|
MXEvent *selectedEvent = [self.roomDataSource eventWithEventId:eventId];
|
|
|
|
if (selectedEvent)
|
|
{
|
|
[self dataSource:dataSource didRecognizeAction:kMXKRoomBubbleCellRiotEditButtonPressed inCell:cell userInfo:@{kMXKRoomBubbleCellEventKey:selectedEvent}];
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[RoomViewController] didRecognizeAction:inCell:userInfo tap on attachment with event state MXEventSentStateFailed. Selected event is nil for event id %@", eventId);
|
|
}
|
|
}
|
|
else if (((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.type == MXKAttachmentTypeSticker)
|
|
{
|
|
// We don't open the attachments viewer when the user taps on a sticker.
|
|
// We consider this tap like a selection.
|
|
|
|
// Check whether a selection already exist or not
|
|
if (self.customizedRoomDataSource.selectedEventId)
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
else
|
|
{
|
|
// Highlight this event in displayed message
|
|
[self selectEventWithId:((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventId];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Keep default implementation
|
|
[super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kRoomEncryptedDataBubbleCellTapOnEncryptionIcon])
|
|
{
|
|
// Retrieve the tapped event
|
|
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
|
|
if (tappedEvent)
|
|
{
|
|
[self showEncryptionInformation:tappedEvent];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnReceiptsContainer])
|
|
{
|
|
MXKReceiptSendersContainer *container = userInfo[kMXKRoomBubbleCellReceiptsContainerKey];
|
|
[ReadReceiptsViewController openInViewController:self fromContainer:container withSession:self.mainSession];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kRoomMembershipExpandedBubbleCellTapOnCollapseButton])
|
|
{
|
|
// Reset the selection before collapsing
|
|
self.customizedRoomDataSource.selectedEventId = nil;
|
|
|
|
[self.roomDataSource collapseRoomBubble:((MXKRoomBubbleTableViewCell*)cell).bubbleData collapsed:YES];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent])
|
|
{
|
|
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
|
|
if (!bubbleData.collapsed)
|
|
{
|
|
[self handleLongPressFromCell:cell withTappedEvent:tappedEvent];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnReactionView])
|
|
{
|
|
NSString *tappedEventId = userInfo[kMXKRoomBubbleCellEventIdKey];
|
|
if (tappedEventId)
|
|
{
|
|
[self showReactionHistoryForEventId:tappedEventId animated:YES];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAddReaction])
|
|
{
|
|
NSString *tappedEventId = userInfo[kMXKRoomBubbleCellEventIdKey];
|
|
if (tappedEventId)
|
|
{
|
|
[self showEmojiPickerForEventId:tappedEventId];
|
|
}
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.callBackAction])
|
|
{
|
|
MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
|
|
|
|
[self placeCallWithVideo2:eventContent.isVideoCall];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.declineAction])
|
|
{
|
|
MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
|
|
|
|
MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId];
|
|
[call hangup];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.answerAction])
|
|
{
|
|
MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
|
|
|
|
MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId];
|
|
[call answer];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomDirectCallStatusCell.endCallAction])
|
|
{
|
|
MXEvent *callInviteEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
MXCallInviteEventContent *eventContent = [MXCallInviteEventContent modelFromJSON:callInviteEvent.content];
|
|
|
|
MXCall *call = [self.mainSession.callManager callWithCallId:eventContent.callId];
|
|
[call hangup];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomGroupCallStatusCell.joinAction] ||
|
|
[actionIdentifier isEqualToString:RoomGroupCallStatusCell.answerAction])
|
|
{
|
|
MXWeakify(self);
|
|
|
|
// Check app permissions first
|
|
[MXKTools checkAccessForCall:YES
|
|
manualChangeMessageForAudio:[VectorL10n microphoneAccessNotGrantedForCall:AppInfo.current.displayName]
|
|
manualChangeMessageForVideo:[VectorL10n cameraAccessNotGrantedForCall:AppInfo.current.displayName]
|
|
showPopUpInViewController:self completionHandler:^(BOOL granted) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if (granted)
|
|
{
|
|
// Present the Jitsi view controller
|
|
Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
|
|
if (jitsiWidget)
|
|
{
|
|
[self showJitsiCallWithWidget:jitsiWidget];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[RoomVC] didRecognizeAction:inCell:userInfo Warning: The application does not have the permission to join/answer the group call");
|
|
}
|
|
}];
|
|
|
|
MXEvent *widgetEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent
|
|
inMatrixSession:self.customizedRoomDataSource.mxSession];
|
|
[[JitsiService shared] resetDeclineForWidgetWithId:widget.widgetId];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomGroupCallStatusCell.leaveAction])
|
|
{
|
|
[self endActiveJitsiCall];
|
|
[self reloadBubblesTable:YES];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomGroupCallStatusCell.declineAction])
|
|
{
|
|
MXEvent *widgetEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent
|
|
inMatrixSession:self.customizedRoomDataSource.mxSession];
|
|
[[JitsiService shared] declineWidgetWithId:widget.widgetId];
|
|
[self reloadBubblesTable:YES];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAvatarView])
|
|
{
|
|
[self showRoomAvatarChange];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAddParticipants])
|
|
{
|
|
[self showAddParticipants];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnAddTopic])
|
|
{
|
|
[self showRoomTopicChange];
|
|
}
|
|
else if ([actionIdentifier isEqualToString:RoomCreationIntroCell.tapOnRoomName])
|
|
{
|
|
[self showRoomCreationModal];
|
|
}
|
|
else
|
|
{
|
|
// Keep default implementation for other actions
|
|
[super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Keep default implementation for other actions
|
|
[super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
|
|
}
|
|
}
|
|
|
|
// Display the additiontal event actions menu
|
|
- (void)showAdditionalActionsMenuForEvent:(MXEvent*)selectedEvent inCell:(id<MXKCellRendering>)cell animated:(BOOL)animated
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
|
MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
|
|
|
|
BOOL isJitsiCallEvent = NO;
|
|
switch (selectedEvent.eventType) {
|
|
case MXEventTypeCustom:
|
|
if ([selectedEvent.type isEqualToString:kWidgetMatrixEventTypeString]
|
|
|| [selectedEvent.type isEqualToString:kWidgetModularEventTypeString])
|
|
{
|
|
Widget *widget = [[Widget alloc] initWithWidgetEvent:selectedEvent inMatrixSession:self.roomDataSource.mxSession];
|
|
if ([widget.type isEqualToString:kWidgetTypeJitsiV1] ||
|
|
[widget.type isEqualToString:kWidgetTypeJitsiV2])
|
|
{
|
|
isJitsiCallEvent = YES;
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
[self.eventMenuBuilder reset];
|
|
|
|
MXWeakify(self);
|
|
|
|
BOOL showThreadOption = [self showThreadOptionForEvent:selectedEvent];
|
|
if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell])
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
|
MXKRoomBubbleCellData *cellData = roomBubbleTableViewCell.bubbleData;
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCopy
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self copyEvent:selectedEvent inCell:cell withCellData:cellData];
|
|
}]];
|
|
}
|
|
|
|
// Add actions for a failed event
|
|
if (selectedEvent.sentState == MXEventSentStateFailed)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeRetrySending
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n retry]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Let the datasource resend. It will manage local echo, etc.
|
|
[self.roomDataSource resendEventWithEventId:selectedEvent.eventId success:nil failure:nil];
|
|
}]];
|
|
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeRemove
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
|
|
style:UIAlertActionStyleDestructive
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
|
|
}]];
|
|
}
|
|
|
|
// View in room action
|
|
if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId])
|
|
{
|
|
// if in the thread and selected event is the root event
|
|
// add "View in room" action
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewInRoom
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self.delegate roomViewController:self
|
|
showRoomWithId:self.roomDataSource.roomId
|
|
eventId:selectedEvent.eventId];
|
|
}]];
|
|
}
|
|
|
|
// Add actions for text message
|
|
if (!attachment)
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
|
|
// Check status of the selected event
|
|
if (selectedEvent.sentState == MXEventSentStatePreparing ||
|
|
selectedEvent.sentState == MXEventSentStateEncrypting ||
|
|
selectedEvent.sentState == MXEventSentStateSending)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
self->currentAlert = nil;
|
|
|
|
// Cancel and remove the outgoing message
|
|
[self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId];
|
|
[self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
|
|
|
|
[self cancelEventSelection];
|
|
}]];
|
|
}
|
|
|
|
if (selectedEvent.sentState == MXEventSentStateSent &&
|
|
!selectedEvent.isTimelinePollEvent &&
|
|
// Forwarding of live-location shares still to be implemented
|
|
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
|
|
}]];
|
|
}
|
|
|
|
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && !selectedEvent.isTimelinePollEvent &&
|
|
selectedEvent.eventType != MXEventTypeBeaconInfo)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
UIActivityViewController *activityViewController = nil;
|
|
if (selectedEvent.location) {
|
|
activityViewController = [self.delegate roomViewController:self locationShareActivityViewControllerForEvent:selectedEvent];
|
|
}
|
|
|
|
if (activityViewController == nil && selectedComponent.textMessage) {
|
|
NSArray *activityItems = @[selectedComponent.textMessage];
|
|
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];
|
|
}
|
|
}]];
|
|
}
|
|
}
|
|
else // Add action for attachment
|
|
{
|
|
// Forwarding for already sent attachments
|
|
if (selectedEvent.sentState == MXEventSentStateSent && (attachment.type == MXKAttachmentTypeFile ||
|
|
attachment.type == MXKAttachmentTypeImage ||
|
|
attachment.type == MXKAttachmentTypeVideo ||
|
|
attachment.type == MXKAttachmentTypeVoiceMessage)) {
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
|
|
}]];
|
|
}
|
|
|
|
if (BuildSettings.messageDetailsAllowSave)
|
|
{
|
|
if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeSaveMedia
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self startActivityIndicator];
|
|
|
|
MXWeakify(self);
|
|
[attachment save:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
//Alert user
|
|
[self showError: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 ||
|
|
selectedEvent.sentState == MXEventSentStateSending)
|
|
{
|
|
// Upload id is stored in attachment url (nasty trick)
|
|
NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL;
|
|
if ([MXMediaManager existingUploaderWithId:uploadId])
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Get again the loader
|
|
MXMediaLoader *loader = [MXMediaManager existingUploaderWithId:uploadId];
|
|
if (loader)
|
|
{
|
|
[loader cancel];
|
|
}
|
|
// Hide the progress animation
|
|
roomBubbleTableViewCell.progressView.hidden = YES;
|
|
|
|
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];
|
|
|
|
// Cancel and remove the outgoing message
|
|
[self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId];
|
|
[self.roomDataSource removeEventWithEventId:selectedEvent.eventId];
|
|
|
|
[self cancelEventSelection];
|
|
}]];
|
|
}
|
|
}
|
|
|
|
if (attachment.type != MXKAttachmentTypeSticker)
|
|
{
|
|
if (BuildSettings.messageDetailsAllowShare)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self startActivityIndicator];
|
|
|
|
MXWeakify(self);
|
|
[attachment prepareShare:^(NSURL *fileURL) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self stopActivityIndicator];
|
|
|
|
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) {
|
|
[self showError:error];
|
|
[self stopActivityIndicator];
|
|
}];
|
|
|
|
// Start animation in case of download during attachment preparing
|
|
[roomBubbleTableViewCell startProgressUI];
|
|
}]];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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])
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelDownloading
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Get again the loader
|
|
MXMediaLoader *loader = [MXMediaManager existingDownloaderWithIdentifier:downloadId];
|
|
if (loader)
|
|
{
|
|
[loader cancel];
|
|
}
|
|
// Hide the progress animation
|
|
roomBubbleTableViewCell.progressView.hidden = YES;
|
|
}]];
|
|
}
|
|
}
|
|
|
|
if (BuildSettings.messageDetailsAllowPermalink)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypePermalink
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Create a matrix.to permalink that is common to all matrix clients
|
|
NSString *permalink = [MXTools permalinkToEvent:selectedEvent.eventId inRoom:selectedEvent.roomId];
|
|
NSURL *url = [NSURL URLWithString:permalink];
|
|
|
|
if (url)
|
|
{
|
|
MXKPasteboardManager.shared.pasteboard.URL = url;
|
|
[self.view vc_toastWithMessage:VectorL10n.roomEventCopyLinkInfo
|
|
image:AssetImages.linkIcon.image
|
|
duration:2.0
|
|
position:ToastPositionBottom
|
|
additionalMargin:self.roomInputToolbarContainerHeightConstraint.constant];
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[RoomViewController] Contextual menu permalink action failed. Permalink is nil room id/event id: %@/%@", selectedEvent.roomId, selectedEvent.eventId);
|
|
}
|
|
}]];
|
|
}
|
|
|
|
if (BuildSettings.messageDetailsAllowViewSource)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewSource
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Display event details
|
|
[self showEventDetails:selectedEvent];
|
|
}]];
|
|
|
|
|
|
// Add "View Decrypted Source" for e2ee event we can decrypt
|
|
if (selectedEvent.isEncrypted && selectedEvent.clearEvent)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewDecryptedSource
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Display clear event details
|
|
[self showEventDetails:selectedEvent.clearEvent];
|
|
}]];
|
|
}
|
|
}
|
|
|
|
// Do not allow to redact the event that enabled encryption (m.room.encryption)
|
|
// because it breaks everything
|
|
if (selectedEvent.eventType != MXEventTypeRoomEncryption)
|
|
{
|
|
NSString *title;
|
|
EventMenuItemType itemType;
|
|
if (selectedEvent.eventType == MXEventTypePollStart)
|
|
{
|
|
title = [VectorL10n roomEventActionRemovePoll];
|
|
itemType = EventMenuItemTypeRemovePoll;
|
|
}
|
|
else
|
|
{
|
|
title = [VectorL10n roomEventActionRedact];
|
|
itemType = EventMenuItemTypeRemove;
|
|
}
|
|
|
|
[self.eventMenuBuilder addItemWithType:itemType
|
|
action:[UIAlertAction actionWithTitle:title
|
|
style:UIAlertActionStyleDestructive
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
[self startActivityIndicator];
|
|
|
|
NSArray<NSString *>* relationTypes = nil;
|
|
// If it's a voice broadcast, delete the selected event and all related events.
|
|
if (selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
|
|
relationTypes = @[MXEventRelationTypeReference];
|
|
}
|
|
|
|
MXWeakify(self);
|
|
[self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:relationTypes reason:nil success:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId);
|
|
//Alert user
|
|
[self showError:error];
|
|
}];
|
|
}]];
|
|
}
|
|
|
|
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId])
|
|
{
|
|
if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId])
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeEndPoll
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self.delegate roomViewController:self endPollWithEventIdentifier:selectedEvent.eventId];
|
|
|
|
[self hideContextualMenuAnimated:YES];
|
|
}]];
|
|
}
|
|
}
|
|
|
|
// Add reaction history if event contains reactions
|
|
if (roomBubbleTableViewCell.bubbleData.reactions[selectedEvent.eventId].aggregatedReactionsWithNonZeroCount)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeReactionHistory
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Show reaction history
|
|
[self showReactionHistoryForEventId:selectedEvent.eventId animated:YES];
|
|
}]];
|
|
}
|
|
|
|
if (![selectedEvent.sender isEqualToString:self.mainSession.myUserId] && RiotSettings.shared.roomContextualMenuShowReportContentOption)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeReport
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
|
|
style:UIAlertActionStyleDestructive
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Prompt user to enter a description of the problem content.
|
|
UIAlertController *reportReasonAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomEventActionReportPromptReason]
|
|
message:nil
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[reportReasonAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
textField.secureTextEntry = NO;
|
|
textField.placeholder = nil;
|
|
textField.keyboardType = UIKeyboardTypeDefault;
|
|
}];
|
|
|
|
MXWeakify(self);
|
|
[reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
NSString *text = [self->currentAlert textFields].firstObject.text;
|
|
self->currentAlert = nil;
|
|
|
|
[self startActivityIndicator];
|
|
|
|
MXWeakify(self);
|
|
[self.roomDataSource.room reportEvent:selectedEvent.eventId score:-100 reason:text success:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self stopActivityIndicator];
|
|
|
|
// Prompt user to ignore content from this user
|
|
UIAlertController *ignoreUserAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomEventActionReportPromptIgnoreUser]
|
|
message:nil
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
MXWeakify(self);
|
|
[ignoreUserAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n yes] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
|
|
[self startActivityIndicator];
|
|
|
|
MXWeakify(self);
|
|
// Add the user to the blacklist: ignored users
|
|
[self.mainSession ignoreUsers:@[selectedEvent.sender] success:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
MXLogDebug(@"[RoomVC] Ignore user (%@) failed", selectedEvent.sender);
|
|
//Alert user
|
|
[self showError:error];
|
|
}];
|
|
}]];
|
|
|
|
[ignoreUserAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n no] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[self presentViewController:ignoreUserAlert animated:YES completion:nil];
|
|
self->currentAlert = ignoreUserAlert;
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
MXLogDebug(@"[RoomVC] Report event (%@) failed", selectedEvent.eventId);
|
|
//Alert user
|
|
[self showError:error];
|
|
|
|
}];
|
|
}]];
|
|
|
|
[reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[self presentViewController:reportReasonAlert animated:YES completion:nil];
|
|
self->currentAlert = reportReasonAlert;
|
|
}]];
|
|
}
|
|
|
|
if (!isJitsiCallEvent && self.roomDataSource.room.summary.isEncrypted)
|
|
{
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewEncryption
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelEventSelection];
|
|
|
|
// Display encryption details
|
|
[self showEncryptionInformation:selectedEvent];
|
|
}]];
|
|
}
|
|
}
|
|
|
|
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancel
|
|
action:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
|
style:UIAlertActionStyleCancel
|
|
handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self hideContextualMenuAnimated:YES];
|
|
}]];
|
|
|
|
// Do not display empty action sheet
|
|
if (!self.eventMenuBuilder.isEmpty)
|
|
{
|
|
UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
|
|
|
// build actions and add them to the alert
|
|
NSArray<UIAlertAction*> *actions = [self.eventMenuBuilder build];
|
|
for (UIAlertAction *action in actions)
|
|
{
|
|
[actionsMenu addAction:action];
|
|
}
|
|
|
|
NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:selectedEvent.eventId];
|
|
|
|
CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
|
|
|
|
[actionsMenu mxk_setAccessibilityIdentifier:@"RoomVCEventMenuAlert"];
|
|
[actionsMenu popoverPresentationController].sourceView = roomBubbleTableViewCell;
|
|
[actionsMenu popoverPresentationController].sourceRect = sourceRect;
|
|
[self dismissKeyboard];
|
|
[self presentViewController:actionsMenu animated:animated completion:nil];
|
|
currentAlert = actionsMenu;
|
|
}
|
|
}
|
|
|
|
- (void)presentEventForwardingDialogForSelectedEvent:(MXEvent *)selectedEvent
|
|
{
|
|
ForwardingShareItemSender *shareItemSender = [[ForwardingShareItemSender alloc] initWithEvent:selectedEvent];
|
|
self.shareManager = [[ShareManager alloc] initWithShareItemSender:shareItemSender
|
|
type:ShareManagerTypeForward
|
|
session:self.mainSession];
|
|
|
|
MXWeakify(self);
|
|
[self.shareManager setCompletionCallback:^(ShareManagerResult result) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if ([self.presentedViewController isEqual:self.shareManager.mainViewController])
|
|
{
|
|
[self dismissViewControllerAnimated:YES completion:nil];
|
|
}
|
|
self.shareManager = nil;
|
|
}];
|
|
|
|
[self presentViewController:self.shareManager.mainViewController animated:YES completion:nil];
|
|
}
|
|
|
|
- (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue
|
|
{
|
|
BOOL shouldDoAction = defaultValue;
|
|
|
|
if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellShouldInteractWithURL])
|
|
{
|
|
// Try to catch universal link supported by the app
|
|
NSURL *url = userInfo[kMXKRoomBubbleCellUrl];
|
|
// Retrieve the type of interaction expected with the URL (See UITextItemInteraction)
|
|
NSNumber *urlItemInteractionValue = userInfo[kMXKRoomBubbleCellUrlItemInteraction];
|
|
|
|
RoomMessageURLType roomMessageURLType = RoomMessageURLTypeUnknown;
|
|
|
|
if (url)
|
|
{
|
|
roomMessageURLType = [self.roomMessageURLParser parseURL:url];
|
|
}
|
|
|
|
// When a link refers to a room alias/id, a user id or an event id, the non-ASCII characters (like '#' in room alias) has been escaped
|
|
// to be able to convert it into a legal URL string.
|
|
NSString *absoluteURLString = [url.absoluteString stringByRemovingPercentEncoding];
|
|
|
|
// If the link can be open it by the app, let it do
|
|
if ([Tools isUniversalLink:url])
|
|
{
|
|
shouldDoAction = NO;
|
|
|
|
[self handleUniversalLinkURL:url];
|
|
}
|
|
// Open a detail screen about the clicked user
|
|
else if ([MXTools isMatrixUserIdentifier:absoluteURLString])
|
|
{
|
|
shouldDoAction = NO;
|
|
|
|
NSString *userId = absoluteURLString;
|
|
|
|
MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:userId];
|
|
if (member)
|
|
{
|
|
// Use the room member detail VC for room members
|
|
[self showMemberDetails:member];
|
|
}
|
|
else
|
|
{
|
|
// Use the contact detail VC for other users
|
|
MXUser *user = [self.roomDataSource.room.mxSession userWithUserId:userId];
|
|
if (user)
|
|
{
|
|
selectedContact = [[MXKContact alloc] initMatrixContactWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) andMatrixID:user.userId];
|
|
}
|
|
else
|
|
{
|
|
selectedContact = [[MXKContact alloc] initMatrixContactWithDisplayName:userId andMatrixID:userId];
|
|
}
|
|
[self performSegueWithIdentifier:@"showContactDetails" sender:self];
|
|
}
|
|
}
|
|
// Open the clicked room
|
|
else if ([MXTools isMatrixRoomIdentifier:absoluteURLString] || [MXTools isMatrixRoomAlias:absoluteURLString])
|
|
{
|
|
shouldDoAction = NO;
|
|
|
|
NSString *roomIdOrAlias = absoluteURLString;
|
|
|
|
// Create a permalink to open or preview the room.
|
|
NSString *permalink = [MXTools permalinkToRoom:roomIdOrAlias];
|
|
NSURL *permalinkURL = [NSURL URLWithString:permalink];
|
|
|
|
[self handleUniversalLinkURL:permalinkURL];
|
|
}
|
|
else if ([absoluteURLString hasPrefix:EventFormatterOnReRequestKeysLinkAction])
|
|
{
|
|
NSArray<NSString*> *arguments = [absoluteURLString componentsSeparatedByString:EventFormatterLinkActionSeparator];
|
|
if (arguments.count > 1)
|
|
{
|
|
NSString *eventId = arguments[1];
|
|
MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
|
|
|
|
if (event)
|
|
{
|
|
[self reRequestKeysAndShowExplanationAlert:event];
|
|
}
|
|
}
|
|
}
|
|
else if ([absoluteURLString hasPrefix:EventFormatterEditedEventLinkAction])
|
|
{
|
|
NSArray<NSString*> *arguments = [absoluteURLString componentsSeparatedByString:EventFormatterLinkActionSeparator];
|
|
if (arguments.count > 1)
|
|
{
|
|
NSString *eventId = arguments[1];
|
|
[self showEditHistoryForEventId:eventId animated:YES];
|
|
}
|
|
shouldDoAction = NO;
|
|
}
|
|
else if (url && urlItemInteractionValue)
|
|
{
|
|
// Fallback case for external links
|
|
switch (urlItemInteractionValue.integerValue) {
|
|
case UITextItemInteractionInvokeDefaultAction:
|
|
{
|
|
switch (roomMessageURLType) {
|
|
case RoomMessageURLTypeAppleDataDetector:
|
|
// Keep the default OS behavior on single tap when UITextView data detector detect a known type.
|
|
shouldDoAction = YES;
|
|
break;
|
|
case RoomMessageURLTypeDummy:
|
|
// Do nothing for dummy links
|
|
shouldDoAction = NO;
|
|
break;
|
|
case RoomMessageURLTypeHttp:
|
|
shouldDoAction = YES;
|
|
break;
|
|
default:
|
|
{
|
|
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
URLValidationResult *result = [URLValidator validateTappedURL:url in:tappedEvent];
|
|
if (result.shouldShowConfirmationAlert)
|
|
{
|
|
[self showDifferentURLsAlertFor:url
|
|
visibleURLString:result.visibleURLString];
|
|
return NO;
|
|
}
|
|
// Try to open the link
|
|
[[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) {
|
|
if (!success)
|
|
{
|
|
[self showUnableToOpenLinkErrorAlert];
|
|
}
|
|
}];
|
|
shouldDoAction = NO;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case UITextItemInteractionPresentActions:
|
|
{
|
|
if (roomMessageURLType == RoomMessageURLTypeHttp) {
|
|
shouldDoAction = YES;
|
|
} else {
|
|
// Retrieve the tapped event
|
|
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
|
|
|
if (tappedEvent)
|
|
{
|
|
// Long press on link, present room contextual menu.
|
|
[self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:NO cell:cell animated:YES];
|
|
}
|
|
|
|
shouldDoAction = NO;
|
|
}
|
|
}
|
|
break;
|
|
case UITextItemInteractionPreview:
|
|
// Force touch on link, let MXKRoomBubbleTableViewCell UITextView use default peek and pop behavior.
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self showUnableToOpenLinkErrorAlert];
|
|
}
|
|
}
|
|
|
|
return shouldDoAction;
|
|
}
|
|
|
|
- (void)selectEventWithId:(NSString*)eventId
|
|
{
|
|
[self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeSend showTimestamp:YES];
|
|
}
|
|
|
|
- (void)selectEventWithId:(NSString*)eventId inputToolBarSendMode:(RoomInputToolbarViewSendMode)inputToolBarSendMode showTimestamp:(BOOL)showTimestamp
|
|
{
|
|
[self setInputToolBarSendMode:inputToolBarSendMode forEventWithId:eventId];
|
|
|
|
self.customizedRoomDataSource.showBubbleDateTimeOnSelection = showTimestamp;
|
|
self.customizedRoomDataSource.selectedEventId = eventId;
|
|
|
|
// Force table refresh
|
|
[self dataSource:self.roomDataSource didCellChange:nil];
|
|
}
|
|
|
|
- (void)cancelEventSelection
|
|
{
|
|
[self setInputToolBarSendMode:RoomInputToolbarViewSendModeSend forEventWithId:nil];
|
|
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
self.customizedRoomDataSource.showBubbleDateTimeOnSelection = YES;
|
|
self.customizedRoomDataSource.selectedEventId = nil;
|
|
self.customizedRoomDataSource.highlightedEventId = nil;
|
|
|
|
[self restoreTextMessageBeforeEditing];
|
|
|
|
// Force table refresh
|
|
[self dataSource:self.roomDataSource didCellChange:nil];
|
|
}
|
|
|
|
- (void)showUnableToOpenLinkErrorAlert
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n error]
|
|
message:[VectorL10n roomMessageUnableOpenLinkErrorMessage]];
|
|
}
|
|
|
|
- (void)editEventContentWithId:(NSString*)eventId
|
|
{
|
|
MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
|
|
|
|
if ([self inputToolbarConformsToHtmlToolbarViewProtocol])
|
|
{
|
|
MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *htmlInputToolBarView = (MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *) self.inputToolbarView;
|
|
self.htmlTextBeforeEditing = htmlInputToolBarView.htmlContent;
|
|
htmlInputToolBarView.htmlContent = [self.customizedRoomDataSource editableHtmlTextMessageFor:event];
|
|
}
|
|
else if ([self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
self.textMessageBeforeEditing = self.inputToolbarView.attributedTextMessage;
|
|
self.inputToolbarView.attributedTextMessage = [self.customizedRoomDataSource editableAttributedTextMessageFor:event];
|
|
}
|
|
|
|
[self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeEdit showTimestamp:YES];
|
|
}
|
|
|
|
- (void)restoreTextMessageBeforeEditing
|
|
{
|
|
|
|
|
|
if (self.htmlTextBeforeEditing && [self inputToolbarConformsToHtmlToolbarViewProtocol])
|
|
{
|
|
MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *htmlInputToolBarView = (MXKRoomInputToolbarView <HtmlRoomInputToolbarViewProtocol> *) self.inputToolbarView;
|
|
htmlInputToolBarView.htmlContent = self.htmlTextBeforeEditing;
|
|
}
|
|
else if (self.textMessageBeforeEditing && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
self.inputToolbarView.attributedTextMessage = self.textMessageBeforeEditing;
|
|
}
|
|
|
|
self.textMessageBeforeEditing = nil;
|
|
self.htmlTextBeforeEditing = nil;
|
|
}
|
|
|
|
- (BOOL)inputToolbarConformsToHtmlToolbarViewProtocol
|
|
{
|
|
return [self.inputToolbarView conformsToProtocol:@protocol(HtmlRoomInputToolbarViewProtocol)];
|
|
}
|
|
|
|
- (BOOL)inputToolbarConformsToToolbarViewProtocol
|
|
{
|
|
return [self.inputToolbarView conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)];
|
|
}
|
|
|
|
- (void)showDifferentURLsAlertFor:(NSURL *)url visibleURLString:(NSString *)visibleURLString
|
|
{
|
|
// urls are different, show confirmation alert
|
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n externalLinkConfirmationTitle] message:[VectorL10n externalLinkConfirmationMessage:visibleURLString :url.absoluteString] preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
UIAlertAction *continueAction = [UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
// Try to open the link
|
|
[[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) {
|
|
if (!success)
|
|
{
|
|
[self showUnableToOpenLinkErrorAlert];
|
|
}
|
|
}];
|
|
}];
|
|
|
|
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:nil];
|
|
|
|
[alert addAction:continueAction];
|
|
[alert addAction:cancelAction];
|
|
|
|
[self presentViewController:alert animated:YES completion:nil];
|
|
}
|
|
|
|
#pragma mark - RoomDataSourceDelegate
|
|
|
|
- (void)roomDataSourceDidUpdateEncryptionTrustLevel:(RoomDataSource *)roomDataSource
|
|
{
|
|
[self updateInputToolbarEncryptionDecoration];
|
|
[self updateTitleViewEncryptionDecoration];
|
|
}
|
|
|
|
- (void)roomDataSource:(RoomDataSource *)roomDataSource didTapThread:(id<MXThreadProtocol>)thread
|
|
{
|
|
[self openThreadWithId:thread.id];
|
|
|
|
[Analytics.shared trackInteraction:AnalyticsUIElementRoomThreadSummaryItem];
|
|
}
|
|
|
|
- (void)roomDataSourceDidUpdateCurrentUserSharingLocationStatus:(RoomDataSource *)roomDataSource
|
|
{
|
|
[self updateLiveLocationBannerViewVisibility];
|
|
}
|
|
|
|
#pragma mark - Segues
|
|
|
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
|
|
{
|
|
// Keep ref on destinationViewController
|
|
[super prepareForSegue:segue sender:sender];
|
|
|
|
id pushedViewController = [segue destinationViewController];
|
|
|
|
if ([[segue identifier] isEqualToString:@"showRoomSearch"])
|
|
{
|
|
// Dismiss keyboard
|
|
[self dismissKeyboard];
|
|
|
|
RoomSearchViewController* roomSearchViewController = (RoomSearchViewController*)pushedViewController;
|
|
// Add the current data source to be able to search messages.
|
|
roomSearchViewController.roomDataSource = self.roomDataSource;
|
|
}
|
|
else if ([[segue identifier] isEqualToString:@"showContactDetails"])
|
|
{
|
|
if (selectedContact)
|
|
{
|
|
ContactDetailsViewController *contactDetailsViewController = segue.destinationViewController;
|
|
contactDetailsViewController.enableVoipCall = NO;
|
|
contactDetailsViewController.contact = selectedContact;
|
|
|
|
selectedContact = nil;
|
|
}
|
|
}
|
|
else if ([[segue identifier] isEqualToString:@"showUnknownDevices"])
|
|
{
|
|
if (unknownDevices)
|
|
{
|
|
UsersDevicesViewController *usersDevicesViewController = (UsersDevicesViewController *)segue.destinationViewController.childViewControllers.firstObject;
|
|
[usersDevicesViewController displayUsersDevices:unknownDevices andMatrixSession:self.roomDataSource.mxSession onComplete:nil];
|
|
|
|
unknownDevices = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - VoIP
|
|
|
|
- (void)placeCallWithVideo:(BOOL)video
|
|
{
|
|
__weak __typeof(self) weakSelf = self;
|
|
|
|
// Check app permissions first
|
|
[MXKTools checkAccessForCall:video
|
|
manualChangeMessageForAudio:[VectorL10n microphoneAccessNotGrantedForCall:AppInfo.current.displayName]
|
|
manualChangeMessageForVideo:[VectorL10n cameraAccessNotGrantedForCall:AppInfo.current.displayName]
|
|
showPopUpInViewController:self completionHandler:^(BOOL granted) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
|
|
if (granted)
|
|
{
|
|
if (video)
|
|
{
|
|
[self placeCallWithVideo2:video];
|
|
}
|
|
else if (self.mainSession.callManager.supportsPSTN)
|
|
{
|
|
[self showVoiceCallActionSheet];
|
|
}
|
|
else
|
|
{
|
|
[self placeCallWithVideo2:NO];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"RoomViewController: Warning: The application does not have the permission to place the call");
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)showVoiceCallActionSheet
|
|
{
|
|
// Ask the user the kind of the call: voice or dialpad?
|
|
UIAlertController *callActionSheet = [UIAlertController alertControllerWithTitle:nil
|
|
message:nil
|
|
preferredStyle:UIAlertControllerStyleActionSheet];
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
[callActionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n roomPlaceVoiceCall]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
[self placeCallWithVideo2:NO];
|
|
}
|
|
|
|
}]];
|
|
|
|
[callActionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n roomOpenDialpad]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
[self openDialpad];
|
|
}
|
|
|
|
}]];
|
|
|
|
[callActionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
|
style:UIAlertActionStyleCancel
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
}
|
|
|
|
}]];
|
|
|
|
[callActionSheet popoverPresentationController].barButtonItem = self.navigationItem.rightBarButtonItems.firstObject;
|
|
[callActionSheet popoverPresentationController].permittedArrowDirections = UIPopoverArrowDirectionUp;
|
|
[self presentViewController:callActionSheet animated:YES completion:nil];
|
|
currentAlert = callActionSheet;
|
|
}
|
|
|
|
- (void)placeCallWithVideo2:(BOOL)video
|
|
{
|
|
Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
|
|
if (jitsiWidget)
|
|
{
|
|
// If there is already a Jitsi call, join it
|
|
[self showJitsiCallWithWidget:jitsiWidget];
|
|
}
|
|
else
|
|
{
|
|
if (self.roomDataSource.room.summary.membersCount.joined == 2
|
|
&& self.roomDataSource.room.isDirect
|
|
&& !self.mainSession.vc_homeserverConfiguration.jitsi.useFor1To1Calls)
|
|
{
|
|
// Matrix call
|
|
[self.roomDataSource.room placeCallWithVideo:video success:nil failure:nil];
|
|
}
|
|
else
|
|
{
|
|
// Jitsi call
|
|
if (self.canEditJitsiWidget)
|
|
{
|
|
// User has right to add a Jitsi widget
|
|
// Create the Jitsi widget and open it directly
|
|
[self startActivityIndicator];
|
|
|
|
MXWeakify(self);
|
|
|
|
[[WidgetManager sharedManager] createJitsiWidgetInRoom:self.roomDataSource.room
|
|
withVideo:video
|
|
success:^(Widget *jitsiWidget)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
[self showJitsiCallWithWidget:jitsiWidget];
|
|
}
|
|
failure:^(NSError *error)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
[self showJitsiErrorAsAlert:error];
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// Insufficient privileges to add a Jitsi widget
|
|
MXWeakify(self);
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
UIAlertController *unprivilegedAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomNoPrivilegesToCreateGroupCall]
|
|
message:nil
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[unprivilegedAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[unprivilegedAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"];
|
|
[self presentViewController:unprivilegedAlert animated:YES completion:nil];
|
|
currentAlert = unprivilegedAlert;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)hangupCall
|
|
{
|
|
MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId];
|
|
if (callInRoom)
|
|
{
|
|
[callInRoom hangup];
|
|
}
|
|
else if (self.isRoomHavingAJitsiCall)
|
|
{
|
|
[self endActiveJitsiCall];
|
|
[self reloadBubblesTable:YES];
|
|
}
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
|
|
#pragma mark - MXKRoomInputToolbarViewDelegate
|
|
|
|
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView isTyping:(BOOL)typing
|
|
{
|
|
[super roomInputToolbarView:toolbarView isTyping:typing];
|
|
|
|
// TODO: Improve so we don't save partial message twice.
|
|
RoomInputToolbarView *inputToolbar = (RoomInputToolbarView *)toolbarView;
|
|
|
|
if (self.saveProgressTextInput && self.roomDataSource && inputToolbar)
|
|
{
|
|
// Store the potential message partially typed in text input
|
|
self.roomDataSource.partialAttributedTextMessage = inputToolbar.attributedTextMessage;
|
|
}
|
|
|
|
// Cancel potential selected event (to leave edition mode)
|
|
NSString *selectedEventId = self.customizedRoomDataSource.selectedEventId;
|
|
if (typing && selectedEventId && ![self.roomDataSource canReplyToEventWithId:selectedEventId])
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
}
|
|
|
|
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChanged:(CGFloat)height completion:(void (^)(BOOL finished))completion
|
|
{
|
|
if (self.roomInputToolbarContainerHeightConstraint.constant != height)
|
|
{
|
|
[super roomInputToolbarView:toolbarView heightDidChanged:height completion:^(BOOL finished) {
|
|
|
|
if (completion)
|
|
{
|
|
completion (finished);
|
|
}
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)roomInputToolbarViewDidTapCancel:(MXKRoomInputToolbarView<RoomInputToolbarViewProtocol>*)toolbarView
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
|
|
- (void)roomInputToolbarViewDidChangeTextMessage:(RoomInputToolbarView *)toolbarView
|
|
{
|
|
[self.completionSuggestionCoordinator processTextMessage:toolbarView.textMessage];
|
|
}
|
|
|
|
- (void)didDetectTextPattern:(SuggestionPatternWrapper *)suggestionPattern
|
|
{
|
|
[self.completionSuggestionCoordinator processSuggestionPattern:suggestionPattern];
|
|
}
|
|
|
|
- (CompletionSuggestionViewModelContextWrapper *)completionSuggestionContext
|
|
{
|
|
return [self.completionSuggestionCoordinator sharedContext];
|
|
}
|
|
|
|
- (MXMediaManager *)mediaManager
|
|
{
|
|
return self.mainSession.mediaManager;
|
|
}
|
|
|
|
- (void)roomInputToolbarViewDidOpenActionMenu:(RoomInputToolbarView*)toolbarView
|
|
{
|
|
// Consider opening the action menu as beginning to type and share encryption keys if requested.
|
|
if ([MXKAppSettings standardAppSettings].outboundGroupSessionKeyPreSharingStrategy == MXKKeyPreSharingWhenTyping)
|
|
{
|
|
[self shareEncryptionKeys];
|
|
}
|
|
}
|
|
|
|
- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendFormattedTextMessage:(NSString *)formattedTextMessage withRawText:(NSString *)rawText
|
|
{
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (readyToSend) {
|
|
[self sendFormattedTextMessage:rawText htmlMsg:formattedTextMessage];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView sendCommand:(NSString *)commandText
|
|
{
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (readyToSend) {
|
|
if (![self sendAsIRCStyleCommandIfPossible:commandText])
|
|
{
|
|
// Display an error for unknown command
|
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
|
|
message:[VectorL10n roomCommandErrorUnknownCommand]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:nil]];
|
|
[self presentViewController:alert animated:YES completion:nil];
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)toolbarView
|
|
{
|
|
NSMutableArray *actionItems = [NSMutableArray new];
|
|
if (RiotSettings.shared.roomScreenAllowMediaLibraryAction)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionPhotoLibrary)];
|
|
}
|
|
if (RiotSettings.shared.roomScreenAllowStickerAction && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionStickers)];
|
|
}
|
|
if (RiotSettings.shared.roomScreenAllowFilesAction)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionAttachments)];
|
|
}
|
|
if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionVoiceBroadcast)];
|
|
}
|
|
if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionPolls)];
|
|
}
|
|
if (BuildSettings.locationSharingEnabled && !self.isNewDirectChat)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionLocation)];
|
|
}
|
|
if (RiotSettings.shared.roomScreenAllowCameraAction)
|
|
{
|
|
[actionItems addObject:@(ComposerCreateActionCamera)];
|
|
}
|
|
|
|
self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems
|
|
wysiwygEnabled:RiotSettings.shared.enableWysiwygComposer
|
|
textFormattingEnabled:RiotSettings.shared.enableWysiwygTextFormatting];
|
|
self.composerCreateActionListBridgePresenter.delegate = self;
|
|
[self.composerCreateActionListBridgePresenter presentFrom:self animated:YES];
|
|
}
|
|
|
|
- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendAttributedTextMessage:(NSAttributedString *)attributedTextMessage
|
|
{
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (readyToSend) {
|
|
BOOL isMessageAHandledCommand = NO;
|
|
// "/me" command is supported with Pills in RoomDataSource.
|
|
if (![attributedTextMessage.string hasPrefix:[MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]])
|
|
{
|
|
// Other commands currently work with identifiers (e.g. ban, invite, op, etc).
|
|
NSString *message;
|
|
if (@available(iOS 15.0, *))
|
|
{
|
|
message = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage mode:PillsReplacementTextModeIdentifier];
|
|
}
|
|
else
|
|
{
|
|
message = attributedTextMessage.string;
|
|
}
|
|
// Try to send the slash command
|
|
isMessageAHandledCommand = [self sendAsIRCStyleCommandIfPossible:message];
|
|
}
|
|
|
|
if (!isMessageAHandledCommand)
|
|
{
|
|
[self sendAttributedTextMessage:attributedTextMessage];
|
|
}
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)roomInputToolbarView:(MXKRoomInputToolbarView *)toolbarView shouldStorePartialContent:(NSAttributedString *)partialAttributedTextMessage
|
|
{
|
|
self.roomDataSource.partialAttributedTextMessage = partialAttributedTextMessage;
|
|
}
|
|
|
|
#pragma mark - MXKRoomMemberDetailsViewControllerDelegate
|
|
|
|
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion
|
|
{
|
|
[self startChatWithUserId:matrixId completion:completion];
|
|
}
|
|
|
|
- (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController mention:(MXRoomMember*)member
|
|
{
|
|
[self mention:member];
|
|
}
|
|
|
|
#pragma mark - Action
|
|
|
|
- (IBAction)onVoiceCallPressed:(id)sender
|
|
{
|
|
// Manage case of a Voice broadcast listening -> Pause Voice broadcast playback
|
|
[VoiceBroadcastPlaybackProvider.shared pausePlaying];
|
|
|
|
if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) {
|
|
[[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle
|
|
message:VectorL10n.voiceBroadcastVoipCannotStartDescription];
|
|
}
|
|
else if (self.isCallActive)
|
|
{
|
|
[self hangupCall];
|
|
}
|
|
else
|
|
{
|
|
[self placeCallWithVideo:NO];
|
|
}
|
|
}
|
|
|
|
- (IBAction)onVideoCallPressed:(id)sender
|
|
{
|
|
// Manage case of a Voice broadcast listening -> Pause Voice broadcast playback
|
|
[VoiceBroadcastPlaybackProvider.shared pausePlaying];
|
|
|
|
if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) {
|
|
[[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle
|
|
message:VectorL10n.voiceBroadcastVoipCannotStartDescription];
|
|
} else {
|
|
[self placeCallWithVideo:YES];
|
|
}
|
|
}
|
|
|
|
- (IBAction)onThreadListTapped:(id)sender
|
|
{
|
|
self.threadsBridgePresenter = [self.delegate threadsCoordinatorForRoomViewController:self threadId:nil];
|
|
self.threadsBridgePresenter.delegate = self;
|
|
[self.threadsBridgePresenter pushFrom:self.navigationController animated:YES];
|
|
|
|
[Analytics.shared trackInteraction:AnalyticsUIElementRoomThreadListButton];
|
|
}
|
|
|
|
- (IBAction)onIntegrationsPressed:(id)sender
|
|
{
|
|
WidgetPickerViewController *widgetPicker = [[WidgetPickerViewController alloc] initForMXSession:self.roomDataSource.mxSession
|
|
inRoom:self.roomDataSource.roomId];
|
|
|
|
[widgetPicker showInViewController:self];
|
|
}
|
|
|
|
- (void)scrollToBottomAction:(id)sender
|
|
{
|
|
[self goBackToLive];
|
|
}
|
|
|
|
- (IBAction)onButtonPressed:(id)sender
|
|
{
|
|
if (sender == self.jumpToLastUnreadButton)
|
|
{
|
|
// Dismiss potential keyboard.
|
|
[self dismissKeyboard];
|
|
NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId;
|
|
NSString *threadId = self.roomDataSource.threadId;
|
|
[self reloadRoomWihtEventId:eventId threadId:threadId forceUpdateRoomMarker:YES];
|
|
}
|
|
else if (sender == self.resetReadMarkerButton)
|
|
{
|
|
// Move the read marker to the current read receipt position.
|
|
[self.roomDataSource.room forgetReadMarker];
|
|
|
|
// Hide the banner
|
|
self.jumpToLastUnreadBannerContainer.hidden = YES;
|
|
}
|
|
}
|
|
|
|
- (void)handleReportRoom
|
|
{
|
|
// Prompt user to enter a description of the problem content.
|
|
UIAlertController *reportReasonAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomActionReportPromptReason]
|
|
message:nil
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[reportReasonAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
textField.secureTextEntry = NO;
|
|
textField.placeholder = nil;
|
|
textField.keyboardType = UIKeyboardTypeDefault;
|
|
}];
|
|
|
|
MXWeakify(self);
|
|
[reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
NSString *text = [self->currentAlert textFields].firstObject.text;
|
|
self->currentAlert = nil;
|
|
|
|
[self startActivityIndicator];
|
|
|
|
[self.roomDataSource.mxSession.matrixRestClient reportRoom:self.roomDataSource.roomId reason:text success:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
|
|
MXLogDebug(@"[RoomVC] Report room (%@) failed", self.roomDataSource.roomId);
|
|
//Alert user
|
|
[self showError:error];
|
|
}];
|
|
}]];
|
|
|
|
[reportReasonAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[self presentViewController:reportReasonAlert animated:YES completion:nil];
|
|
self->currentAlert = reportReasonAlert;
|
|
}
|
|
|
|
#pragma mark - UITableViewDelegate
|
|
|
|
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
cell.backgroundColor = ThemeService.shared.theme.backgroundColor;
|
|
|
|
// Update the selected background view
|
|
if (ThemeService.shared.theme.selectedBackgroundColor)
|
|
{
|
|
cell.selectedBackgroundView = [[UIView alloc] init];
|
|
cell.selectedBackgroundView.backgroundColor = ThemeService.shared.theme.selectedBackgroundColor;
|
|
}
|
|
else
|
|
{
|
|
if (tableView.style == UITableViewStylePlain)
|
|
{
|
|
cell.selectedBackgroundView = nil;
|
|
}
|
|
else
|
|
{
|
|
cell.selectedBackgroundView.backgroundColor = nil;
|
|
}
|
|
}
|
|
|
|
if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
|
|
if (roomBubbleTableViewCell.readMarkerView)
|
|
{
|
|
readMarkerTableViewCell = roomBubbleTableViewCell;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self checkReadMarkerVisibility];
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
|
|
{
|
|
if (cell == readMarkerTableViewCell)
|
|
{
|
|
readMarkerTableViewCell = nil;
|
|
}
|
|
|
|
[super tableView:tableView didEndDisplayingCell:cell forRowAtIndexPath:indexPath];
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
|
{
|
|
[super scrollViewDidScroll:scrollView];
|
|
|
|
[self checkReadMarkerVisibility];
|
|
|
|
// Switch back to the live mode when the user scrolls to the bottom of the non live timeline.
|
|
if (!self.roomDataSource.isLive && ![self isRoomPreview] && !self.isNewDirectChat)
|
|
{
|
|
CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom;
|
|
if (contentBottomPosY >= self.bubblesTableView.contentSize.height && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards])
|
|
{
|
|
[self goBackToLive];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
|
|
{
|
|
if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewWillBeginDragging:)])
|
|
{
|
|
[super scrollViewWillBeginDragging:scrollView];
|
|
}
|
|
|
|
[self cancelEventHighlight];
|
|
}
|
|
|
|
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
|
|
{
|
|
if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)])
|
|
{
|
|
[super scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
|
|
}
|
|
|
|
if (decelerate == NO)
|
|
{
|
|
// Handle swipe on expanded header
|
|
[self onScrollViewDidEndScrolling:scrollView];
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
}
|
|
else
|
|
{
|
|
// Dispatch async the expanded header handling in order to let the deceleration go first.
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
// Handle swipe on expanded header
|
|
[self onScrollViewDidEndScrolling:scrollView];
|
|
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
|
|
{
|
|
if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndDecelerating:)])
|
|
{
|
|
[super scrollViewDidEndDecelerating:scrollView];
|
|
}
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
}
|
|
|
|
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
|
|
{
|
|
if ([MXKRoomViewController instancesRespondToSelector:@selector(scrollViewDidEndScrollingAnimation:)])
|
|
{
|
|
[super scrollViewDidEndScrollingAnimation:scrollView];
|
|
}
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
}
|
|
|
|
- (void)onScrollViewDidEndScrolling:(UIScrollView *)scrollView
|
|
{
|
|
|
|
}
|
|
|
|
#pragma mark - MXKRoomTitleViewDelegate
|
|
|
|
- (BOOL)roomTitleViewShouldBeginEditing:(MXKRoomTitleView*)titleView
|
|
{
|
|
// Disable room name edition
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - RoomTitleViewTapGestureDelegate
|
|
|
|
- (void)roomTitleView:(RoomTitleView*)titleView recognizeTapGesture:(UITapGestureRecognizer*)tapGestureRecognizer
|
|
{
|
|
UIView *tappedView = tapGestureRecognizer.view;
|
|
|
|
if (tappedView == titleView.titleMask)
|
|
{
|
|
[self showRoomInfo];
|
|
}
|
|
else if (tappedView == previewHeader.rightButton)
|
|
{
|
|
// 'Join' button has been pressed
|
|
if (!roomPreviewData)
|
|
{
|
|
[self joinRoom:^(MXKRoomViewControllerJoinRoomResult result) {
|
|
switch (result)
|
|
{
|
|
case MXKRoomViewControllerJoinRoomResultSuccess:
|
|
[self refreshRoomTitle];
|
|
break;
|
|
case MXKRoomViewControllerJoinRoomResultFailureRoomEmpty:
|
|
[self declineRoomInvitation];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}];
|
|
|
|
return;
|
|
}
|
|
|
|
// Attempt to join the room (keep reference on the potential eventId, the preview data will be removed automatically in case of success).
|
|
NSString *eventId = roomPreviewData.eventId;
|
|
|
|
// We promote here join by room alias instead of room id when an alias is available.
|
|
NSString *roomIdOrAlias = roomPreviewData.roomId;
|
|
|
|
if (roomPreviewData.roomCanonicalAlias.length)
|
|
{
|
|
roomIdOrAlias = roomPreviewData.roomCanonicalAlias;
|
|
}
|
|
else if (roomPreviewData.roomAliases.count)
|
|
{
|
|
roomIdOrAlias = roomPreviewData.roomAliases.firstObject;
|
|
}
|
|
|
|
// Note in case of simple link to a room the signUrl param is nil
|
|
[self joinRoomWithRoomIdOrAlias:roomIdOrAlias viaServers:roomPreviewData.viaServers
|
|
andSignUrl:roomPreviewData.emailInvitation.signUrl
|
|
completion:^(MXKRoomViewControllerJoinRoomResult result) {
|
|
|
|
switch (result)
|
|
{
|
|
case MXKRoomViewControllerJoinRoomResultSuccess:
|
|
{
|
|
// If an event was specified, replace the datasource by a non live datasource showing the event
|
|
if (eventId)
|
|
{
|
|
MXWeakify(self);
|
|
[RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
|
|
initialEventId:eventId
|
|
threadId:self.roomDataSource.threadId
|
|
andMatrixSession:self.mainSession
|
|
onComplete:^(id roomDataSource) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[roomDataSource finalizeInitialization];
|
|
((RoomDataSource*)roomDataSource).markTimelineInitialEvent = YES;
|
|
|
|
[self displayRoom:roomDataSource];
|
|
|
|
self.hasRoomDataSourceOwnership = YES;
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// Enable back the text input
|
|
[self setRoomInputToolbarViewClass:[RoomViewController mainToolbarClass]];
|
|
[self updateInputToolBarViewHeight];
|
|
|
|
// And the extra area
|
|
[self setRoomActivitiesViewClass:RoomActivitiesView.class];
|
|
|
|
[self refreshRoomTitle];
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
break;
|
|
}
|
|
case MXKRoomViewControllerJoinRoomResultFailureRoomEmpty:
|
|
[self declineRoomInvitation];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}];
|
|
}
|
|
else if (tappedView == previewHeader.leftButton)
|
|
{
|
|
[self presentDeclineOptionsFromView:tappedView];
|
|
}
|
|
else if (tappedView == previewHeader.reportButton)
|
|
{
|
|
[self handleReportRoom];
|
|
}
|
|
}
|
|
|
|
- (void)presentDeclineOptionsFromView:(UIView *)view
|
|
{
|
|
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:[VectorL10n roomPreviewDeclineInvitationOptions]
|
|
message:nil
|
|
preferredStyle:UIAlertControllerStyleActionSheet];
|
|
[actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n decline]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * _Nonnull action) {
|
|
[self declineRoomInvitation];
|
|
}]];
|
|
[actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n ignoreUser]
|
|
style:UIAlertActionStyleDestructive
|
|
handler:^(UIAlertAction * _Nonnull action) {
|
|
[self ignoreInviteSender];
|
|
}]];
|
|
[actionSheet addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
|
style:UIAlertActionStyleCancel
|
|
handler:nil]];
|
|
actionSheet.popoverPresentationController.sourceView = view;
|
|
[self presentViewController:actionSheet animated:YES completion:nil];
|
|
}
|
|
|
|
- (void)declineRoomInvitation
|
|
{
|
|
// 'Decline' button has been pressed
|
|
if (roomPreviewData)
|
|
{
|
|
[self roomPreviewDidTapCancelAction];
|
|
}
|
|
else
|
|
{
|
|
[self startActivityIndicator];
|
|
MXWeakify(self);
|
|
[self.roomDataSource.room leave:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self stopActivityIndicator];
|
|
[self popToHomeViewController];
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self stopActivityIndicator];
|
|
MXLogDebug(@"[RoomVC] Failed to reject an invited room (%@) failed", self.roomDataSource.room.roomId);
|
|
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)ignoreInviteSender
|
|
{
|
|
[self startActivityIndicator];
|
|
MXWeakify(self);
|
|
[self.roomDataSource.room ignoreInviteSender:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self stopActivityIndicator];
|
|
[self popToHomeViewController];
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self stopActivityIndicator];
|
|
MXLogDebug(@"[RoomVC] Failed to ignore inviter in room (%@)", self.roomDataSource.room.roomId);
|
|
}];
|
|
}
|
|
|
|
- (void)popToHomeViewController
|
|
{
|
|
// We remove the current view controller.
|
|
// Pop to homes view controller
|
|
[[AppDelegate theDelegate] restoreInitialDisplay:^{}];
|
|
}
|
|
|
|
#pragma mark - Typing management
|
|
|
|
- (void)removeTypingNotificationsListener
|
|
{
|
|
if (self.roomDataSource)
|
|
{
|
|
// Remove the previous live listener
|
|
if (typingNotifListener)
|
|
{
|
|
MXWeakify(self);
|
|
[self.roomDataSource.room liveTimeline:^(id<MXEventTimeline> liveTimeline) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[liveTimeline removeListener:self->typingNotifListener];
|
|
self->typingNotifListener = nil;
|
|
}];
|
|
}
|
|
}
|
|
|
|
currentTypingUsers = nil;
|
|
}
|
|
|
|
- (void)listenTypingNotifications
|
|
{
|
|
if (self.roomDataSource)
|
|
{
|
|
// Add typing notification listener
|
|
MXWeakify(self);
|
|
self->typingNotifListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringTypingNotification] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Handle only live events
|
|
if (direction == MXTimelineDirectionForwards)
|
|
{
|
|
// Retrieve typing users list
|
|
NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self.roomDataSource.room.typingUsers];
|
|
// Remove typing info for the current user
|
|
NSUInteger index = [typingUsers indexOfObject:self.mainSession.myUser.userId];
|
|
if (index != NSNotFound)
|
|
{
|
|
[typingUsers removeObjectAtIndex:index];
|
|
}
|
|
|
|
// Ignore this notification if both arrays are empty
|
|
if (self->currentTypingUsers.count || typingUsers.count)
|
|
{
|
|
self->currentTypingUsers = typingUsers;
|
|
[self refreshActivitiesViewDisplay];
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Retrieve the current typing users list
|
|
NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self.roomDataSource.room.typingUsers];
|
|
// Remove typing info for the current user
|
|
NSUInteger index = [typingUsers indexOfObject:self.mainSession.myUser.userId];
|
|
if (index != NSNotFound)
|
|
{
|
|
[typingUsers removeObjectAtIndex:index];
|
|
}
|
|
currentTypingUsers = typingUsers;
|
|
[self refreshActivitiesViewDisplay];
|
|
}
|
|
}
|
|
|
|
- (void)refreshTypingNotification
|
|
{
|
|
RoomDataSource *roomDataSource = (RoomDataSource *) self.roomDataSource;
|
|
BOOL needsUpdate = currentTypingUsers.count != roomDataSource.currentTypingUsers.count;
|
|
|
|
NSMutableArray *typingUsers = [NSMutableArray new];
|
|
for (NSUInteger i = 0 ; i < currentTypingUsers.count ; i++) {
|
|
NSString *userId = currentTypingUsers[i];
|
|
MXRoomMember* member = [self.roomDataSource.roomState.members memberWithUserId:userId];
|
|
TypingUserInfo *userInfo;
|
|
if (member)
|
|
{
|
|
userInfo = [[TypingUserInfo alloc] initWithMember: member];
|
|
}
|
|
else
|
|
{
|
|
userInfo = [[TypingUserInfo alloc] initWithUserId: userId];
|
|
}
|
|
[typingUsers addObject:userInfo];
|
|
needsUpdate = needsUpdate || userInfo.userId != ((MXRoomMember *) roomDataSource.currentTypingUsers[i]).userId;
|
|
}
|
|
|
|
if (needsUpdate)
|
|
{
|
|
// BOOL needsReload = roomDataSource.currentTypingUsers == nil;
|
|
// Quick fix for https://github.com/vector-im/element-ios/issues/4230
|
|
BOOL needsReload = YES;
|
|
roomDataSource.currentTypingUsers = typingUsers;
|
|
if (needsReload)
|
|
{
|
|
[self.bubblesTableView reloadData];
|
|
}
|
|
else
|
|
{
|
|
NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
|
|
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:count - 1 inSection:0];
|
|
[self.bubblesTableView reloadRowsAtIndexPaths:@[lastIndexPath] withRowAnimation:UITableViewRowAnimationFade];
|
|
}
|
|
|
|
if (self.isScrollToBottomHidden
|
|
&& !self.bubblesTableView.isDragging
|
|
&& !self.bubblesTableView.isDecelerating)
|
|
{
|
|
NSInteger count = [self.bubblesTableView numberOfRowsInSection:0];
|
|
if (count)
|
|
{
|
|
[self scrollBubblesTableViewToBottomAnimated:YES];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Call notifications management
|
|
|
|
- (void)removeCallNotificationsListeners
|
|
{
|
|
if (kMXCallStateDidChangeObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kMXCallStateDidChangeObserver];
|
|
kMXCallStateDidChangeObserver = nil;
|
|
}
|
|
if (kMXCallManagerConferenceStartedObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kMXCallManagerConferenceStartedObserver];
|
|
kMXCallManagerConferenceStartedObserver = nil;
|
|
}
|
|
if (kMXCallManagerConferenceFinishedObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kMXCallManagerConferenceFinishedObserver];
|
|
kMXCallManagerConferenceFinishedObserver = nil;
|
|
}
|
|
}
|
|
|
|
- (void)listenCallNotifications
|
|
{
|
|
MXWeakify(self);
|
|
|
|
kMXCallStateDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallStateDidChange object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXCall *call = notif.object;
|
|
if ([call.room.roomId isEqualToString:self.customizedRoomDataSource.roomId])
|
|
{
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
}];
|
|
kMXCallManagerConferenceStartedObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerConferenceStarted object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
NSString *roomId = notif.object;
|
|
if ([roomId isEqualToString:self.customizedRoomDataSource.roomId])
|
|
{
|
|
[self refreshActivitiesViewDisplay];
|
|
}
|
|
}];
|
|
kMXCallManagerConferenceFinishedObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerConferenceFinished object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
NSString *roomId = notif.object;
|
|
if ([roomId isEqualToString:self.customizedRoomDataSource.roomId])
|
|
{
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshRoomInputToolbar];
|
|
}
|
|
}];
|
|
}
|
|
|
|
|
|
#pragma mark - Server notices management
|
|
|
|
- (void)removeServerNoticesListener
|
|
{
|
|
if (serverNotices)
|
|
{
|
|
[serverNotices close];
|
|
serverNotices = nil;
|
|
}
|
|
}
|
|
|
|
- (void)listenToServerNotices
|
|
{
|
|
if (!serverNotices)
|
|
{
|
|
serverNotices = [[MXServerNotices alloc] initWithMatrixSession:self.roomDataSource.mxSession];
|
|
serverNotices.delegate = self;
|
|
}
|
|
}
|
|
|
|
- (void)serverNoticesDidChangeState:(MXServerNotices *)serverNotices
|
|
{
|
|
[self refreshActivitiesViewDisplay];
|
|
}
|
|
|
|
#pragma mark - Widget notifications management
|
|
|
|
- (void)removeWidgetNotificationsListeners
|
|
{
|
|
if (kMXKWidgetManagerDidUpdateWidgetObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kMXKWidgetManagerDidUpdateWidgetObserver];
|
|
kMXKWidgetManagerDidUpdateWidgetObserver = nil;
|
|
}
|
|
}
|
|
|
|
- (void)listenWidgetNotifications
|
|
{
|
|
if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXWeakify(self);
|
|
|
|
kMXKWidgetManagerDidUpdateWidgetObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kWidgetManagerDidUpdateWidgetNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
Widget *widget = notif.object;
|
|
if (widget.mxSession == self.roomDataSource.mxSession
|
|
&& [widget.roomId isEqualToString:self.customizedRoomDataSource.roomId])
|
|
{
|
|
// Call button update
|
|
[self refreshRoomTitle];
|
|
// Remove Jitsi widget view update
|
|
[self refreshRemoveJitsiWidgetView];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)showJitsiErrorAsAlert:(NSError*)error
|
|
{
|
|
// Customise the error for permission issues
|
|
if ([error.domain isEqualToString:WidgetManagerErrorDomain] && error.code == WidgetManagerErrorCodeNotEnoughPower)
|
|
{
|
|
error = [NSError errorWithDomain:error.domain
|
|
code:error.code
|
|
userInfo:@{
|
|
NSLocalizedDescriptionKey: [VectorL10n roomConferenceCallNoPower]
|
|
}];
|
|
}
|
|
|
|
// Alert user
|
|
[self showError:error];
|
|
}
|
|
|
|
- (NSUInteger)widgetsCount:(BOOL)includeUserWidgets
|
|
{
|
|
if (!self.displayConfiguration.integrationsEnabled)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
NSUInteger widgetsCount = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsiV1, kWidgetTypeJitsiV2]
|
|
inRoom:self.roomDataSource.room
|
|
withRoomState:self.roomDataSource.roomState].count;
|
|
if (includeUserWidgets)
|
|
{
|
|
widgetsCount += [[WidgetManager sharedManager] userWidgets:self.roomDataSource.room.mxSession].count;
|
|
}
|
|
|
|
return widgetsCount;
|
|
}
|
|
|
|
#pragma mark - Unreachable Network Handling
|
|
|
|
- (void)refreshActivitiesViewDisplay
|
|
{
|
|
if ([self.activitiesView isKindOfClass:RoomActivitiesView.class])
|
|
{
|
|
RoomActivitiesView *roomActivitiesView = (RoomActivitiesView*)self.activitiesView;
|
|
|
|
// Reset gesture recognizers
|
|
while (roomActivitiesView.gestureRecognizers.count)
|
|
{
|
|
[roomActivitiesView removeGestureRecognizer:roomActivitiesView.gestureRecognizers[0]];
|
|
}
|
|
|
|
if ([self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded])
|
|
{
|
|
self.activitiesViewExpanded = YES;
|
|
[roomActivitiesView showResourceLimitExceededError:self.roomDataSource.mxSession.syncError.userInfo onAdminContactTapped:^(NSURL *adminContactURL) {
|
|
[[UIApplication sharedApplication] vc_open:adminContactURL completionHandler:^(BOOL success) {
|
|
if (!success)
|
|
{
|
|
MXLogDebug(@"[RoomVC] refreshActivitiesViewDisplay: adminContact(%@) cannot be opened", adminContactURL);
|
|
}
|
|
}];
|
|
}];
|
|
}
|
|
else if ([AppDelegate theDelegate].isOffline)
|
|
{
|
|
// Doing nothing here as the offline notification is now handled by the AppCoordinator
|
|
}
|
|
else if (self.customizedRoomDataSource.roomState.isObsolete)
|
|
{
|
|
self.activitiesViewExpanded = YES;
|
|
MXWeakify(self);
|
|
[roomActivitiesView displayRoomReplacementWithRoomLinkTappedHandler:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXEvent *stoneTombEvent = [self.customizedRoomDataSource.roomState stateEventsWithType:kMXEventTypeStringRoomTombStone].lastObject;
|
|
|
|
NSString *replacementRoomId = self.customizedRoomDataSource.roomState.tombStoneContent.replacementRoomId;
|
|
if ([self.roomDataSource.mxSession roomWithRoomId:replacementRoomId])
|
|
{
|
|
// Open the room if it is already joined
|
|
[self showRoomWithId:replacementRoomId];
|
|
}
|
|
else
|
|
{
|
|
// Else auto join it via the server that sent the event
|
|
MXLogDebug(@"[RoomVC] Auto join an upgraded room: %@ -> %@. Sender: %@", self.customizedRoomDataSource.roomState.roomId,
|
|
replacementRoomId, stoneTombEvent.sender);
|
|
|
|
NSString *viaSenderServer = [MXTools serverNameInMatrixIdentifier:stoneTombEvent.sender];
|
|
|
|
if (viaSenderServer)
|
|
{
|
|
[self startActivityIndicator];
|
|
[self.roomDataSource.mxSession joinRoom:replacementRoomId viaServers:@[viaSenderServer] success:^(MXRoom *room) {
|
|
[self stopActivityIndicator];
|
|
|
|
[self showRoomWithId:replacementRoomId];
|
|
|
|
} failure:^(NSError *error) {
|
|
[self stopActivityIndicator];
|
|
|
|
MXLogDebug(@"[RoomVC] Failed to join an upgraded room. Error: %@",
|
|
error);
|
|
[self showError:error];
|
|
}];
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
else if ([self checkUnsentMessages] == NO)
|
|
{
|
|
// Show "scroll to bottom" icon when the most recent message is not visible,
|
|
// or when the timelime is not live (this icon is used to go back to live).
|
|
// Note: we check if `currentEventIdAtTableBottom` is set to know whether the table has been rendered at least once.
|
|
if (!self.roomDataSource.isLive || (currentEventIdAtTableBottom && [self isBubblesTableScrollViewAtTheBottom] == NO))
|
|
{
|
|
if (self.roomDataSource.room)
|
|
{
|
|
// Retrieve the unread messages count on the current thread
|
|
NSUInteger unreadCount = [self.mainSession.store
|
|
localUnreadEventCount:self.roomDataSource.room.roomId
|
|
threadId:self.roomDataSource.threadId ?: kMXEventTimelineMain
|
|
withTypeIn:self.mainSession.unreadEventTypes];
|
|
|
|
self.scrollToBottomBadgeLabel.text = unreadCount ? [NSString stringWithFormat:@"%lu", unreadCount] : nil;
|
|
self.scrollToBottomHidden = NO;
|
|
}
|
|
else
|
|
{
|
|
// will be here for left rooms
|
|
self.scrollToBottomBadgeLabel.text = nil;
|
|
self.scrollToBottomHidden = YES;
|
|
}
|
|
}
|
|
else if (serverNotices.usageLimit && serverNotices.usageLimit.isServerNoticeUsageLimit)
|
|
{
|
|
self.scrollToBottomHidden = YES;
|
|
self.activitiesViewExpanded = YES;
|
|
[roomActivitiesView showResourceUsageLimitNotice:serverNotices.usageLimit onAdminContactTapped:^(NSURL *adminContactURL) {
|
|
[[UIApplication sharedApplication] vc_open:adminContactURL completionHandler:^(BOOL success) {
|
|
if (!success)
|
|
{
|
|
MXLogDebug(@"[RoomVC] refreshActivitiesViewDisplay: adminContact(%@) cannot be opened", adminContactURL);
|
|
}
|
|
}];
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
self.scrollToBottomHidden = YES;
|
|
self.activitiesViewExpanded = NO;
|
|
[self refreshTypingNotification];
|
|
}
|
|
}
|
|
|
|
// Recognize swipe downward to dismiss keyboard if any
|
|
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(onSwipeGesture:)];
|
|
[swipe setNumberOfTouchesRequired:1];
|
|
[swipe setDirection:UISwipeGestureRecognizerDirectionDown];
|
|
[roomActivitiesView addGestureRecognizer:swipe];
|
|
}
|
|
}
|
|
|
|
- (void)goBackToLive
|
|
{
|
|
if (self.roomDataSource.isLive)
|
|
{
|
|
// Enable the read marker display, and disable its update (in order to not mark as read all the new messages by default).
|
|
self.roomDataSource.showReadMarker = YES;
|
|
self.updateRoomReadMarker = NO;
|
|
|
|
[self scrollBubblesTableViewToBottomAnimated:YES];
|
|
|
|
[self cancelEventHighlight];
|
|
}
|
|
else
|
|
{
|
|
MXWeakify(self);
|
|
|
|
void(^continueBlock)(MXKRoomDataSource *, BOOL) = ^(MXKRoomDataSource *roomDataSource, BOOL hasRoomDataSourceOwnership){
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[roomDataSource finalizeInitialization];
|
|
|
|
// Scroll to bottom the bubble history on the display refresh.
|
|
self->shouldScrollToBottomOnTableRefresh = YES;
|
|
|
|
[self displayRoom:roomDataSource];
|
|
|
|
// Set the room view controller has the data source ownership here.
|
|
self.hasRoomDataSourceOwnership = hasRoomDataSourceOwnership;
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
|
|
if (self.saveProgressTextInput)
|
|
{
|
|
// Restore the potential message partially typed before jump to last unread messages.
|
|
[self.inputToolbarView setPartialContent:roomDataSource.partialAttributedTextMessage];
|
|
}
|
|
};
|
|
|
|
if (self.roomDataSource.threadId)
|
|
{
|
|
[ThreadDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
|
|
initialEventId:nil
|
|
threadId:self.roomDataSource.threadId
|
|
andMatrixSession:self.mainSession
|
|
onComplete:^(ThreadDataSource *threadDataSource)
|
|
{
|
|
continueBlock(threadDataSource, YES);
|
|
}];
|
|
}
|
|
else if (self.roomDataSource.roomId)
|
|
{
|
|
if (self.isContextPreview)
|
|
{
|
|
[RoomPreviewDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
|
|
threadId:nil
|
|
andMatrixSession:self.mainSession
|
|
onComplete:^(RoomPreviewDataSource *roomDataSource)
|
|
{
|
|
continueBlock(roomDataSource, YES);
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// Switch back to the room live timeline managed by MXKRoomDataSourceManager
|
|
MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession];
|
|
|
|
[roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId
|
|
create:YES
|
|
onComplete:^(MXKRoomDataSource *roomDataSource) {
|
|
continueBlock(roomDataSource, NO);
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Missed discussions handling
|
|
|
|
- (void)refreshMissedDiscussionsCount:(BOOL)force
|
|
{
|
|
// Ignore this action when no room is displayed
|
|
if (!self.showMissedDiscussionsBadge || !self.roomDataSource || !missedDiscussionsBadgeLabel
|
|
|| [UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPhone
|
|
|| ([[UIScreen mainScreen] nativeBounds].size.height > 2532 && UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)))
|
|
{
|
|
self.missedDiscussionsBadgeHidden = YES;
|
|
return;
|
|
}
|
|
|
|
self.missedDiscussionsBadgeHidden = NO;
|
|
|
|
NSUInteger highlightCount = 0;
|
|
NSUInteger missedCount = [[AppDelegate theDelegate].masterTabBarController missedDiscussionsCount];
|
|
|
|
// Compute the missed notifications count of the current room by considering its notification mode in Riot.
|
|
NSUInteger roomNotificationCount = self.roomDataSource.room.summary.notificationCount;
|
|
if (self.roomDataSource.room.isMentionsOnly)
|
|
{
|
|
// Only the highlighted missed messages must be considered here.
|
|
roomNotificationCount = self.roomDataSource.room.summary.highlightCount;
|
|
}
|
|
|
|
// Remove the current room from the missed discussion counter.
|
|
if (missedCount && roomNotificationCount)
|
|
{
|
|
missedCount--;
|
|
}
|
|
|
|
if (missedCount)
|
|
{
|
|
// Compute the missed highlight count
|
|
highlightCount = [[AppDelegate theDelegate].masterTabBarController missedHighlightDiscussionsCount];
|
|
if (highlightCount && self.roomDataSource.room.summary.highlightCount)
|
|
{
|
|
// Remove the current room from the missed highlight counter
|
|
highlightCount--;
|
|
}
|
|
}
|
|
|
|
if (force || missedDiscussionsCount != missedCount || missedHighlightCount != highlightCount)
|
|
{
|
|
missedDiscussionsCount = missedCount;
|
|
missedHighlightCount = highlightCount;
|
|
|
|
if (missedCount)
|
|
{
|
|
// Refresh missed discussions count label
|
|
if (missedCount > 99)
|
|
{
|
|
missedDiscussionsBadgeLabel.text = @"99+";
|
|
}
|
|
else
|
|
{
|
|
missedDiscussionsBadgeLabel.text = [NSString stringWithFormat:@"%tu", missedCount];
|
|
}
|
|
|
|
missedDiscussionsDotView.alpha = highlightCount == 0 ? 0 : 1;
|
|
}
|
|
else
|
|
{
|
|
missedDiscussionsBadgeLabel.text = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Unsent Messages Handling
|
|
|
|
-(BOOL)checkUnsentMessages
|
|
{
|
|
MXRoomSummarySentStatus sentStatus = MXRoomSummarySentStatusOk;
|
|
if ([self.activitiesView isKindOfClass:RoomActivitiesView.class])
|
|
{
|
|
sentStatus = self.roomDataSource.room.summary.sentStatus;
|
|
|
|
if (sentStatus != MXRoomSummarySentStatusOk)
|
|
{
|
|
NSString *notification = sentStatus == MXRoomSummarySentStatusSentFailedDueToUnknownDevices ?
|
|
[VectorL10n roomUnsentMessagesUnknownDevicesNotification] :
|
|
[VectorL10n roomUnsentMessagesNotification];
|
|
|
|
MXWeakify(self);
|
|
RoomActivitiesView *roomActivitiesView = (RoomActivitiesView*) self.activitiesView;
|
|
self.activitiesViewExpanded = YES;
|
|
[roomActivitiesView displayUnsentMessagesNotification:notification withResendLink:^{
|
|
|
|
[self resendAllUnsentMessages];
|
|
|
|
} andCancelLink:^{
|
|
|
|
[self cancelAllUnsentMessages];
|
|
|
|
} andIconTapGesture:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (self->currentAlert)
|
|
{
|
|
[self->currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
}
|
|
|
|
MXWeakify(self);
|
|
UIAlertController *resendAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
|
|
|
[resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomResendUnsentMessages]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self resendAllUnsentMessages];
|
|
self->currentAlert = nil;
|
|
|
|
}]];
|
|
|
|
[resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomDeleteUnsentMessages]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self cancelAllUnsentMessages];
|
|
self->currentAlert = nil;
|
|
|
|
}]];
|
|
|
|
[resendAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
|
style:UIAlertActionStyleCancel
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
self->currentAlert = nil;
|
|
|
|
}]];
|
|
|
|
[resendAlert mxk_setAccessibilityIdentifier:@"RoomVCUnsentMessagesMenuAlert"];
|
|
[resendAlert popoverPresentationController].sourceView = roomActivitiesView;
|
|
[resendAlert popoverPresentationController].sourceRect = roomActivitiesView.bounds;
|
|
[self presentViewController:resendAlert animated:YES completion:nil];
|
|
self->currentAlert = resendAlert;
|
|
|
|
}];
|
|
}
|
|
}
|
|
|
|
return sentStatus != MXRoomSummarySentStatusOk;
|
|
}
|
|
|
|
- (void)eventDidChangeSentState:(NSNotification *)notif
|
|
{
|
|
// We are only interested by event that has just failed in their encryption
|
|
// because of unknown devices in the room
|
|
MXEvent *event = notif.object;
|
|
if (event.sentState == MXEventSentStateFailed &&
|
|
[event.roomId isEqualToString:self.roomDataSource.roomId]
|
|
&& [event.sentError.domain isEqualToString:MXEncryptingErrorDomain]
|
|
&& event.sentError.code == MXEncryptingErrorUnknownDeviceCode
|
|
&& !unknownDevices) // Show the alert once in case of resending several events
|
|
{
|
|
__weak __typeof(self) weakSelf = self;
|
|
|
|
[self dismissTemporarySubViews];
|
|
|
|
// List all unknown devices
|
|
unknownDevices = [[MXUsersDevicesMap alloc] init];
|
|
|
|
NSArray<MXEvent*> *outgoingMsgs = self.roomDataSource.room.outgoingMessages;
|
|
for (MXEvent *event in outgoingMsgs)
|
|
{
|
|
if (event.sentState == MXEventSentStateFailed
|
|
&& [event.sentError.domain isEqualToString:MXEncryptingErrorDomain]
|
|
&& event.sentError.code == MXEncryptingErrorUnknownDeviceCode)
|
|
{
|
|
MXUsersDevicesMap<MXDeviceInfo*> *eventUnknownDevices = event.sentError.userInfo[MXEncryptingErrorUnknownDeviceDevicesKey];
|
|
|
|
[unknownDevices addEntriesFromMap:eventUnknownDevices];
|
|
}
|
|
}
|
|
|
|
UIAlertController *unknownDevicesAlert = [UIAlertController alertControllerWithTitle:[VectorL10n unknownDevicesAlertTitle]
|
|
message:[VectorL10n unknownDevicesAlert]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[unknownDevicesAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n unknownDevicesVerify]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
[self performSegueWithIdentifier:@"showUnknownDevices" sender:self];
|
|
}
|
|
|
|
}]];
|
|
|
|
[unknownDevicesAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n unknownDevicesSendAnyway]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
// Acknowledge the existence of all devices
|
|
self->unknownDevices = nil;
|
|
|
|
// And resend pending messages
|
|
[self resendAllUnsentMessages];
|
|
}
|
|
|
|
}]];
|
|
|
|
[unknownDevicesAlert mxk_setAccessibilityIdentifier:@"RoomVCUnknownDevicesAlert"];
|
|
[self presentViewController:unknownDevicesAlert animated:YES completion:nil];
|
|
currentAlert = unknownDevicesAlert;
|
|
}
|
|
}
|
|
|
|
- (void)eventDidChangeIdentifier:(NSNotification *)notif
|
|
{
|
|
MXEvent *event = notif.object;
|
|
NSString *previousId = notif.userInfo[kMXEventIdentifierKey];
|
|
|
|
if ([self.customizedRoomDataSource.selectedEventId isEqualToString:previousId])
|
|
{
|
|
MXLogDebug(@"[RoomVC] eventDidChangeIdentifier: Update selectedEventId");
|
|
self.customizedRoomDataSource.selectedEventId = event.eventId;
|
|
}
|
|
}
|
|
|
|
|
|
- (void)resendAllUnsentMessages
|
|
{
|
|
// List unsent event ids
|
|
NSArray *outgoingMsgs = self.roomDataSource.room.outgoingMessages;
|
|
NSMutableArray *failedEventIds = [NSMutableArray arrayWithCapacity:outgoingMsgs.count];
|
|
|
|
for (MXEvent *event in outgoingMsgs)
|
|
{
|
|
if (event.sentState == MXEventSentStateFailed)
|
|
{
|
|
[failedEventIds addObject:event.eventId];
|
|
}
|
|
}
|
|
|
|
// Launch iterative operation
|
|
[self resendFailedEvent:0 inArray:failedEventIds];
|
|
}
|
|
|
|
- (void)resendFailedEvent:(NSUInteger)index inArray:(NSArray*)failedEventIds
|
|
{
|
|
if (index < failedEventIds.count)
|
|
{
|
|
NSString *failedEventId = failedEventIds[index];
|
|
NSUInteger nextIndex = index + 1;
|
|
|
|
// Let the datasource resend. It will manage local echo, etc.
|
|
[self.roomDataSource resendEventWithEventId:failedEventId success:^(NSString *eventId) {
|
|
|
|
[self resendFailedEvent:nextIndex inArray:failedEventIds];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
[self resendFailedEvent:nextIndex inArray:failedEventIds];
|
|
|
|
}];
|
|
|
|
return;
|
|
}
|
|
|
|
// Refresh activities view
|
|
[self refreshActivitiesViewDisplay];
|
|
}
|
|
|
|
- (void)cancelAllUnsentMessages
|
|
{
|
|
UIAlertController *cancelAlert = [UIAlertController alertControllerWithTitle:[VectorL10n roomUnsentMessagesCancelTitle]
|
|
message:[VectorL10n roomUnsentMessagesCancelMessage]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
MXWeakify(self);
|
|
[cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[cancelAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n delete] style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
// Remove unsent event ids
|
|
for (NSUInteger index = 0; index < self.roomDataSource.room.outgoingMessages.count;)
|
|
{
|
|
MXEvent *event = self.roomDataSource.room.outgoingMessages[index];
|
|
if (event.sentState == MXEventSentStateFailed)
|
|
{
|
|
[self.roomDataSource removeEventWithEventId:event.eventId];
|
|
}
|
|
else
|
|
{
|
|
index ++;
|
|
}
|
|
}
|
|
|
|
[self refreshActivitiesViewDisplay];
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[self presentViewController:cancelAlert animated:YES completion:nil];
|
|
currentAlert = cancelAlert;
|
|
}
|
|
|
|
# pragma mark - Encryption Information view
|
|
|
|
- (void)showEncryptionInformation:(MXEvent *)event
|
|
{
|
|
[self dismissKeyboard];
|
|
|
|
// Remove potential existing subviews
|
|
[self dismissTemporarySubViews];
|
|
|
|
EncryptionInfoView *encryptionInfoView = [[EncryptionInfoView alloc] initWithEvent:event andMatrixSession:self.roomDataSource.mxSession];
|
|
|
|
// Add shadow on added view
|
|
encryptionInfoView.layer.cornerRadius = 5;
|
|
encryptionInfoView.layer.shadowOffset = CGSizeMake(0, 1);
|
|
encryptionInfoView.layer.shadowOpacity = 0.5f;
|
|
|
|
// Add the view and define edge constraints
|
|
[self.view addSubview:encryptionInfoView];
|
|
|
|
self->encryptionInfoView = encryptionInfoView;
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated"
|
|
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:encryptionInfoView
|
|
attribute:NSLayoutAttributeTop
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:self.topLayoutGuide
|
|
attribute:NSLayoutAttributeBottom
|
|
multiplier:1.0f
|
|
constant:10.0f]];
|
|
|
|
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:encryptionInfoView
|
|
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:encryptionInfoView
|
|
attribute:NSLayoutAttributeLeading
|
|
multiplier:1.0f
|
|
constant:-10.0f]];
|
|
|
|
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view
|
|
attribute:NSLayoutAttributeTrailing
|
|
relatedBy:NSLayoutRelationEqual
|
|
toItem:encryptionInfoView
|
|
attribute:NSLayoutAttributeTrailing
|
|
multiplier:1.0f
|
|
constant:10.0f]];
|
|
[self.view setNeedsUpdateConstraints];
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Read marker handling
|
|
|
|
- (void)checkReadMarkerVisibility
|
|
{
|
|
if (readMarkerTableViewCell && isAppeared && !self.isBubbleTableViewDisplayInTransition)
|
|
{
|
|
// Check whether the read marker is visible
|
|
CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top;
|
|
CGFloat readMarkerViewPosY = readMarkerTableViewCell.frame.origin.y + readMarkerTableViewCell.readMarkerView.frame.origin.y;
|
|
if (contentTopPosY <= readMarkerViewPosY)
|
|
{
|
|
// Compute the max vertical position visible according to contentOffset
|
|
CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom;
|
|
if (readMarkerViewPosY <= contentBottomPosY)
|
|
{
|
|
// Launch animation
|
|
[self animateReadMarkerView];
|
|
|
|
// Disable the read marker display when it has been rendered once.
|
|
self.roomDataSource.showReadMarker = NO;
|
|
[self refreshJumpToLastUnreadBannerDisplay];
|
|
|
|
// Update the read marker position according the events acknowledgement in this view controller.
|
|
self.updateRoomReadMarker = YES;
|
|
|
|
if (self.roomDataSource.isLive)
|
|
{
|
|
// Move the read marker to the current read receipt position.
|
|
[self.roomDataSource.room forgetReadMarker];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)animateReadMarkerView
|
|
{
|
|
// Check whether the cell with the read marker is known and if the marker is not animated yet.
|
|
|
|
if (!readMarkerTableViewCell || readMarkerTableViewCell.readMarkerView.isHidden == NO)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RoomBubbleCellData *cellData = (RoomBubbleCellData*)readMarkerTableViewCell.bubbleData;
|
|
|
|
id<RoomTimelineCellDecorator> cellDecorator = [RoomTimelineConfiguration shared].currentStyle.cellDecorator;
|
|
|
|
[cellDecorator dissmissReadMarkerViewForCell:readMarkerTableViewCell
|
|
cellData:cellData
|
|
animated:YES
|
|
completion:^{
|
|
|
|
self->readMarkerTableViewCell = nil;
|
|
}];
|
|
}
|
|
|
|
- (void)refreshRemoveJitsiWidgetView
|
|
{
|
|
if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking)
|
|
{
|
|
Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
|
|
|
|
if (jitsiWidget && self.canEditJitsiWidget)
|
|
{
|
|
[self.removeJitsiWidgetView reset];
|
|
self.removeJitsiWidgetContainer.hidden = NO;
|
|
self.removeJitsiWidgetView.delegate = self;
|
|
}
|
|
else
|
|
{
|
|
self.removeJitsiWidgetContainer.hidden = YES;
|
|
self.removeJitsiWidgetView.delegate = nil;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self.removeJitsiWidgetView reset];
|
|
self.removeJitsiWidgetContainer.hidden = YES;
|
|
self.removeJitsiWidgetView.delegate = self;
|
|
}
|
|
}
|
|
|
|
- (void)refreshJumpToLastUnreadBannerDisplay
|
|
{
|
|
// This banner is only displayed when the room timeline is in live (and no peeking).
|
|
// Check whether the read marker exists and has not been rendered yet.
|
|
if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId)
|
|
{
|
|
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
|
|
return [evaluatedObject isKindOfClass:MXKRoomBubbleTableViewCell.class];
|
|
}];
|
|
NSArray *visibleCells = [[self.bubblesTableView visibleCells] filteredArrayUsingPredicate:predicate];
|
|
UITableViewCell *cell = visibleCells.firstObject;
|
|
if (cell)
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
|
|
// Check whether the read marker is inside the first displayed cell.
|
|
if (roomBubbleTableViewCell.readMarkerView)
|
|
{
|
|
// The read marker display is still enabled (see roomDataSource.showReadMarker flag),
|
|
// this means the read marker was not been visible yet.
|
|
// We show the banner if the marker is located in the top hidden part of the cell.
|
|
CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top;
|
|
CGFloat readMarkerViewPosY = roomBubbleTableViewCell.frame.origin.y + roomBubbleTableViewCell.readMarkerView.frame.origin.y;
|
|
self.jumpToLastUnreadBannerContainer.hidden = (contentTopPosY < readMarkerViewPosY);
|
|
}
|
|
else
|
|
{
|
|
// Check whether the read marker event is anterior to the first event displayed in the first rendered cell.
|
|
MXKRoomBubbleComponent *component = roomBubbleTableViewCell.bubbleData.bubbleComponents.firstObject;
|
|
MXEvent *firstDisplayedEvent = component.event;
|
|
MXEvent *currentReadMarkerEvent = [self.roomDataSource.mxSession.store eventWithEventId:self.roomDataSource.room.accountData.readMarkerEventId inRoom:self.roomDataSource.roomId];
|
|
|
|
if (!currentReadMarkerEvent || (currentReadMarkerEvent.originServerTs < firstDisplayedEvent.originServerTs))
|
|
{
|
|
self.jumpToLastUnreadBannerContainer.hidden = NO;
|
|
}
|
|
else
|
|
{
|
|
self.jumpToLastUnreadBannerContainer.hidden = YES;
|
|
|
|
// Force the read marker position in order to not depend on the read marker animation (https://github.com/vector-im/element-ios/issues/7420)
|
|
self.updateRoomReadMarker = YES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.jumpToLastUnreadBannerContainer.hidden = YES;
|
|
|
|
// Initialize the read marker if it does not exist yet, only in case of live timeline.
|
|
if (!self.roomDataSource.room.accountData.readMarkerEventId && self.roomDataSource.isLive && !self.roomDataSource.isPeeking)
|
|
{
|
|
// Move the read marker to the current read receipt position by default.
|
|
[self.roomDataSource.room forgetReadMarker];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - ContactsTableViewControllerDelegate
|
|
|
|
- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
// Invite ?
|
|
NSString *promptMsg = [VectorL10n roomParticipantsInvitePromptMsg:contact.displayName];
|
|
UIAlertController *invitePrompt = [UIAlertController alertControllerWithTitle:[VectorL10n roomParticipantsInvitePromptTitle]
|
|
message:promptMsg
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[invitePrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
|
style:UIAlertActionStyleCancel
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
}
|
|
|
|
}]];
|
|
|
|
[invitePrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n invite]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
// Sanity check
|
|
if (!weakSelf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
MXSession* session = self.roomDataSource.mxSession;
|
|
NSString* roomId = self.roomDataSource.roomId;
|
|
MXRoom *room = [session roomWithRoomId:roomId];
|
|
|
|
NSArray *identifiers = contact.matrixIdentifiers;
|
|
NSString *participantId;
|
|
|
|
if (identifiers.count)
|
|
{
|
|
participantId = identifiers.firstObject;
|
|
|
|
// Invite this user if a room is defined
|
|
[room inviteUser:participantId success:^{
|
|
|
|
// Refresh display by removing the contacts picker
|
|
[contactsTableViewController withdrawViewControllerAnimated:YES completion:nil];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[RoomVC] Invite %@ failed", participantId);
|
|
// Alert user
|
|
[self showError:error];
|
|
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
if (contact.emailAddresses.count)
|
|
{
|
|
// This is a local contact, consider the first email by default.
|
|
// TODO: Prompt the user to select the right email.
|
|
MXKEmail *email = contact.emailAddresses.firstObject;
|
|
participantId = email.emailAddress;
|
|
}
|
|
else
|
|
{
|
|
// This is the text filled by the user.
|
|
participantId = contact.displayName;
|
|
}
|
|
|
|
// Is it an email or a Matrix user ID?
|
|
if ([MXTools isEmailAddress:participantId])
|
|
{
|
|
[room inviteUserByEmail:participantId success:^{
|
|
|
|
// Refresh display by removing the contacts picker
|
|
[contactsTableViewController withdrawViewControllerAnimated:YES completion:nil];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[RoomVC] Invite be email %@ failed", participantId);
|
|
// Alert user
|
|
if ([error.domain isEqualToString:kMXRestClientErrorDomain]
|
|
&& error.code == MXRestClientErrorMissingIdentityServer)
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n errorInvite3pidWithNoIdentityServer] message:nil];
|
|
}
|
|
else
|
|
{
|
|
[self showError:error];
|
|
}
|
|
}];
|
|
}
|
|
else //if ([MXTools isMatrixUserIdentifier:participantId])
|
|
{
|
|
[room inviteUser:participantId success:^{
|
|
|
|
// Refresh display by removing the contacts picker
|
|
[contactsTableViewController withdrawViewControllerAnimated:YES completion:nil];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[RoomVC] Invite %@ failed", participantId);
|
|
// Alert user
|
|
[self showError:error];
|
|
|
|
}];
|
|
}
|
|
}
|
|
|
|
}]];
|
|
|
|
[invitePrompt mxk_setAccessibilityIdentifier:@"RoomVCInviteAlert"];
|
|
[self presentViewController:invitePrompt animated:YES completion:nil];
|
|
currentAlert = invitePrompt;
|
|
}
|
|
|
|
#pragma mark - Re-request encryption keys
|
|
|
|
- (void)reRequestKeysAndShowExplanationAlert:(MXEvent*)event
|
|
{
|
|
MXWeakify(self);
|
|
__block UIAlertController *alert;
|
|
|
|
// Force device verification if session has cross-signing activated and device is not yet verified
|
|
if (self.mainSession.crypto.crossSigning && self.mainSession.crypto.crossSigning.state == MXCrossSigningStateCrossSigningExists)
|
|
{
|
|
[self presentReviewUnverifiedSessionsAlert];
|
|
return;
|
|
}
|
|
|
|
// Make the re-request
|
|
[self.mainSession.crypto reRequestRoomKeyForEvent:event];
|
|
|
|
// Observe kMXEventDidDecryptNotification to remove automatically the dialog
|
|
// if the user has shared the keys from another device
|
|
mxEventDidDecryptNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXEventDidDecryptNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXEvent *decryptedEvent = notif.object;
|
|
|
|
if ([decryptedEvent.eventId isEqualToString:event.eventId])
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self->mxEventDidDecryptNotificationObserver];
|
|
self->mxEventDidDecryptNotificationObserver = nil;
|
|
|
|
if (self->currentAlert == alert)
|
|
{
|
|
[self->currentAlert dismissViewControllerAnimated:YES completion:nil];
|
|
self->currentAlert = nil;
|
|
}
|
|
}
|
|
}];
|
|
|
|
// Show the explanation dialog
|
|
alert = [UIAlertController alertControllerWithTitle:VectorL10n.rerequestKeysAlertTitle
|
|
message:[VectorL10n e2eRoomKeyRequestMessage:AppInfo.current.displayName]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self->mxEventDidDecryptNotificationObserver];
|
|
self->mxEventDidDecryptNotificationObserver = nil;
|
|
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[self presentViewController:alert animated:YES completion:nil];
|
|
currentAlert = alert;
|
|
}
|
|
|
|
- (void)presentReviewUnverifiedSessionsAlert
|
|
{
|
|
MXLogDebug(@"[MasterTabBarController] presentReviewUnverifiedSessionsAlertWithSession");
|
|
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n keyVerificationAlertTitle]
|
|
message:[VectorL10n keyVerificationAlertBody]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
[self showSettingsSecurityScreen];
|
|
}]];
|
|
|
|
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n later]
|
|
style:UIAlertActionStyleCancel
|
|
handler:nil]];
|
|
|
|
[self presentViewController:alert animated:YES completion:nil];
|
|
|
|
currentAlert = alert;
|
|
}
|
|
|
|
- (void)showSettingsSecurityScreen
|
|
{
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate roomViewController:self showCompleteSecurityForSession:self.mainSession];
|
|
}
|
|
else
|
|
{
|
|
[[AppDelegate theDelegate] presentCompleteSecurityForSession: self.mainSession];
|
|
}
|
|
}
|
|
|
|
#pragma mark Tombstone event
|
|
|
|
- (void)listenTombstoneEventNotifications
|
|
{
|
|
// Room is already obsolete do not listen to tombstone event
|
|
if (self.roomDataSource.roomState.isObsolete)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXWeakify(self);
|
|
|
|
tombstoneEventNotificationsListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomTombStone] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Update activitiesView with room replacement information
|
|
[self refreshActivitiesViewDisplay];
|
|
// Hide inputToolbarView
|
|
[self updateRoomInputToolbarViewClassIfNeeded];
|
|
}];
|
|
}
|
|
|
|
- (void)removeTombstoneEventNotificationsListener
|
|
{
|
|
if (self.roomDataSource)
|
|
{
|
|
// Remove the previous live listener
|
|
if (tombstoneEventNotificationsListener)
|
|
{
|
|
[self.roomDataSource.room removeListener:tombstoneEventNotificationsListener];
|
|
tombstoneEventNotificationsListener = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark MXSession state change
|
|
|
|
- (void)listenMXSessionStateChangeNotifications
|
|
{
|
|
MXWeakify(self);
|
|
|
|
kMXSessionStateDidChangeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:self.roomDataSource.mxSession queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (self.roomDataSource.mxSession.state == MXSessionStateSyncError
|
|
|| self.roomDataSource.mxSession.state == MXSessionStateRunning)
|
|
{
|
|
[self refreshActivitiesViewDisplay];
|
|
|
|
// update inputToolbarView
|
|
[self updateRoomInputToolbarViewClassIfNeeded];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)removeMXSessionStateChangeNotificationsListener
|
|
{
|
|
if (kMXSessionStateDidChangeObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:kMXSessionStateDidChangeObserver];
|
|
kMXSessionStateDidChangeObserver = nil;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Contextual Menu
|
|
|
|
- (NSArray<RoomContextualMenuItem*>*)contextualMenuItemsForEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
|
|
{
|
|
if (event.sentState == MXEventSentStateFailed)
|
|
{
|
|
return @[
|
|
[self resendMenuItemWithEvent:event],
|
|
[self deleteMenuItemWithEvent:event],
|
|
[self editMenuItemWithEvent:event],
|
|
[self copyMenuItemWithEvent:event andCell:cell]
|
|
];
|
|
}
|
|
|
|
BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates)
|
|
|| (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages);
|
|
BOOL showThreadOption = [self showThreadOptionForEvent:event];
|
|
|
|
NSMutableArray<RoomContextualMenuItem*> *items = [NSMutableArray arrayWithCapacity:5];
|
|
|
|
[items addObject:[self replyMenuItemWithEvent:event]];
|
|
if (showThreadOption)
|
|
{
|
|
// add "Thread" option only if not already in a thread
|
|
[items addObject:[self replyInThreadMenuItemWithEvent:event]];
|
|
}
|
|
[items addObject:[self editMenuItemWithEvent:event]];
|
|
if (!showThreadOption)
|
|
{
|
|
[items addObject:[self copyMenuItemWithEvent:event andCell:cell]];
|
|
}
|
|
if (showMoreOption)
|
|
{
|
|
[items addObject:[self moreMenuItemWithEvent:event andCell:cell]];
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
- (void)showContextualMenuForEvent:(MXEvent*)event fromSingleTapGesture:(BOOL)usedSingleTapGesture cell:(id<MXKCellRendering>)cell animated:(BOOL)animated
|
|
{
|
|
if (self.roomContextualMenuPresenter.isPresenting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NSString *selectedEventId = event.eventId;
|
|
|
|
NSArray<RoomContextualMenuItem*>* contextualMenuItems = [self contextualMenuItemsForEvent:event andCell:cell];
|
|
ReactionsMenuViewModel *reactionsMenuViewModel;
|
|
CGRect bubbleComponentFrameInOverlayView = CGRectNull;
|
|
|
|
if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && [self.roomDataSource canReactToEventWithId:event.eventId])
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
|
|
MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData;
|
|
NSArray *bubbleComponents = bubbleCellData.bubbleComponents;
|
|
|
|
NSInteger foundComponentIndex = [bubbleCellData bubbleComponentIndexForEventId:event.eventId];
|
|
CGRect bubbleComponentFrame;
|
|
|
|
if (bubbleComponents.count > 0)
|
|
{
|
|
NSInteger selectedComponentIndex = foundComponentIndex != NSNotFound ? foundComponentIndex : 0;
|
|
bubbleComponentFrame = [roomBubbleTableViewCell surroundingFrameInTableViewForComponentIndex:selectedComponentIndex];
|
|
}
|
|
else
|
|
{
|
|
bubbleComponentFrame = roomBubbleTableViewCell.frame;
|
|
}
|
|
|
|
bubbleComponentFrameInOverlayView = [self.bubblesTableView convertRect:bubbleComponentFrame toView:self.overlayContainerView];
|
|
|
|
NSString *roomId = self.roomDataSource.roomId;
|
|
MXAggregations *aggregations = self.mainSession.aggregations;
|
|
MXAggregatedReactions *aggregatedReactions = [aggregations aggregatedReactionsOnEvent:selectedEventId inRoom:roomId];
|
|
|
|
reactionsMenuViewModel = [[ReactionsMenuViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:selectedEventId];
|
|
reactionsMenuViewModel.coordinatorDelegate = self;
|
|
}
|
|
|
|
if (!self.roomContextualMenuViewController)
|
|
{
|
|
self.roomContextualMenuViewController = [RoomContextualMenuViewController instantiate];
|
|
self.roomContextualMenuViewController.delegate = self;
|
|
}
|
|
|
|
[self.roomContextualMenuViewController updateWithContextualMenuItems:contextualMenuItems reactionsMenuViewModel:reactionsMenuViewModel];
|
|
|
|
[self enableOverlayContainerUserInteractions:YES];
|
|
|
|
[self.roomContextualMenuPresenter presentWithRoomContextualMenuViewController:self.roomContextualMenuViewController
|
|
from:self
|
|
on:self.overlayContainerView
|
|
contentToReactFrame:bubbleComponentFrameInOverlayView
|
|
fromSingleTapGesture:usedSingleTapGesture
|
|
animated:animated
|
|
completion:^{
|
|
}];
|
|
|
|
preventBubblesTableViewScroll = YES;
|
|
[self selectEventWithId:selectedEventId];
|
|
}
|
|
|
|
- (void)hideContextualMenuAnimated:(BOOL)animated
|
|
{
|
|
[self hideContextualMenuAnimated:animated completion:nil];
|
|
}
|
|
|
|
- (void)hideContextualMenuAnimated:(BOOL)animated completion:(void(^)(void))completion
|
|
{
|
|
[self hideContextualMenuAnimated:animated cancelEventSelection:YES completion:completion];
|
|
}
|
|
|
|
- (void)hideContextualMenuAnimated:(BOOL)animated cancelEventSelection:(BOOL)cancelEventSelection completion:(void(^)(void))completion
|
|
{
|
|
if (!self.roomContextualMenuPresenter.isPresenting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (cancelEventSelection)
|
|
{
|
|
[self cancelEventSelection];
|
|
}
|
|
|
|
preventBubblesTableViewScroll = NO;
|
|
|
|
[self.roomContextualMenuPresenter hideContextualMenuWithAnimated:animated completion:^{
|
|
[self enableOverlayContainerUserInteractions:NO];
|
|
|
|
if (completion)
|
|
{
|
|
completion();
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)enableOverlayContainerUserInteractions:(BOOL)enableOverlayContainerUserInteractions
|
|
{
|
|
self.inputToolbarView.editable = !enableOverlayContainerUserInteractions;
|
|
self.bubblesTableView.scrollsToTop = !enableOverlayContainerUserInteractions;
|
|
self.overlayContainerView.userInteractionEnabled = enableOverlayContainerUserInteractions;
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)resendMenuItemWithEvent:(MXEvent*)event
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *resendMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionResend];
|
|
resendMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
|
|
[self cancelEventSelection];
|
|
[self.roomDataSource resendEventWithEventId:event.eventId success:nil failure:nil];
|
|
};
|
|
|
|
return resendMenuItem;
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)deleteMenuItemWithEvent:(MXEvent*)event
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *deleteMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionDelete];
|
|
deleteMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXWeakify(self);
|
|
[self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
UIAlertController *deleteConfirmation = [UIAlertController alertControllerWithTitle:[VectorL10n roomEventActionDeleteConfirmationTitle]
|
|
message:[VectorL10n roomEventActionDeleteConfirmationMessage]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[deleteConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
}]];
|
|
|
|
[deleteConfirmation addAction:[UIAlertAction actionWithTitle:[VectorL10n delete] style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) {
|
|
[self.roomDataSource removeEventWithEventId:event.eventId];
|
|
}]];
|
|
|
|
[self presentViewController:deleteConfirmation animated:YES completion:nil];
|
|
self->currentAlert = deleteConfirmation;
|
|
}];
|
|
};
|
|
|
|
return deleteMenuItem;
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)editMenuItemWithEvent:(MXEvent*)event
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit];
|
|
|
|
switch (event.eventType) {
|
|
case MXEventTypePollStart: {
|
|
editMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:nil];
|
|
[self.delegate roomViewController:self didRequestEditForPollWithStartEvent:event];
|
|
};
|
|
|
|
editMenuItem.isEnabled = [self.delegate roomViewController:self canEditPollWithEventIdentifier:event.eventId];
|
|
|
|
break;
|
|
}
|
|
default: {
|
|
editMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
|
|
[self editEventContentWithId:event.eventId];
|
|
|
|
// And display the keyboard
|
|
[self.inputToolbarView becomeFirstResponder];
|
|
};
|
|
|
|
editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId];
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return editMenuItem;
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)copyMenuItemWithEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *copyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionCopy];
|
|
copyMenuItem.isEnabled = [self canCopyEvent:event andCell:cell];
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
|
MXKRoomBubbleCellData *cellData = roomBubbleTableViewCell.bubbleData;
|
|
copyMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self copyEvent:event inCell:cell withCellData:cellData];
|
|
};
|
|
|
|
return copyMenuItem;
|
|
}
|
|
|
|
- (BOOL)canCopyEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
|
MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
|
|
|
|
BOOL result = !attachment || attachment.type != MXKAttachmentTypeSticker;
|
|
|
|
if (attachment && !BuildSettings.messageDetailsAllowCopyMedia)
|
|
{
|
|
result = NO;
|
|
}
|
|
|
|
if (result)
|
|
{
|
|
switch (event.eventType) {
|
|
case MXEventTypeRoomMessage:
|
|
{
|
|
NSString *messageType = event.content[kMXMessageTypeKey];
|
|
|
|
if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest])
|
|
{
|
|
result = NO;
|
|
}
|
|
break;
|
|
}
|
|
case MXEventTypeKeyVerificationStart:
|
|
case MXEventTypeKeyVerificationAccept:
|
|
case MXEventTypeKeyVerificationKey:
|
|
case MXEventTypeKeyVerificationMac:
|
|
case MXEventTypeKeyVerificationDone:
|
|
case MXEventTypeKeyVerificationCancel:
|
|
case MXEventTypePollStart:
|
|
case MXEventTypePollEnd:
|
|
case MXEventTypeBeaconInfo:
|
|
result = NO;
|
|
break;
|
|
case MXEventTypeCustom:
|
|
if ([event.type isEqualToString:kWidgetMatrixEventTypeString]
|
|
|| [event.type isEqualToString:kWidgetModularEventTypeString])
|
|
{
|
|
Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:self.roomDataSource.mxSession];
|
|
if ([widget.type isEqualToString:kWidgetTypeJitsiV1] ||
|
|
[widget.type isEqualToString:kWidgetTypeJitsiV2])
|
|
{
|
|
result = NO;
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (void)copyEvent:(MXEvent*)event inCell:(id<MXKCellRendering>)cell withCellData:(MXKRoomBubbleCellData *)cellData
|
|
{
|
|
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
|
MXKAttachment *attachment = cellData.attachment;
|
|
|
|
if (!attachment)
|
|
{
|
|
NSArray *components = cellData.bubbleComponents;
|
|
MXKRoomBubbleComponent *selectedComponent;
|
|
for (selectedComponent in components)
|
|
{
|
|
if ([selectedComponent.event.eventId isEqualToString:event.eventId])
|
|
{
|
|
break;
|
|
}
|
|
selectedComponent = nil;
|
|
}
|
|
|
|
NSAttributedString *attributedTextMessage = selectedComponent.attributedTextMessage;
|
|
|
|
if (attributedTextMessage)
|
|
{
|
|
if (@available(iOS 15.0, *))
|
|
{
|
|
MXKPasteboardManager.shared.pasteboard.string = [PillsFormatter stringByReplacingPillsIn:attributedTextMessage
|
|
mode:PillsReplacementTextModeMarkdown];
|
|
}
|
|
else
|
|
{
|
|
MXKPasteboardManager.shared.pasteboard.string = attributedTextMessage.string;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[RoomViewController] Contextual menu copy failed. Text is nil for room id/event id: %@/%@", selectedComponent.event.roomId, selectedComponent.event.eventId);
|
|
}
|
|
|
|
[self hideContextualMenuAnimated:YES];
|
|
}
|
|
else if (attachment.type != MXKAttachmentTypeSticker)
|
|
{
|
|
[self hideContextualMenuAnimated:YES completion:^{
|
|
[self startActivityIndicator];
|
|
|
|
[attachment copy:^{
|
|
|
|
[self stopActivityIndicator];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
[self stopActivityIndicator];
|
|
|
|
//Alert user
|
|
[self showError:error];
|
|
}];
|
|
|
|
// Start animation in case of download during attachment preparing
|
|
[roomBubbleTableViewCell startProgressUI];
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)replyMenuItemWithEvent:(MXEvent*)event
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *replyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReply];
|
|
replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio;
|
|
replyMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
|
|
[self selectEventWithId:event.eventId inputToolBarSendMode:RoomInputToolbarViewSendModeReply showTimestamp:NO];
|
|
|
|
// And display the keyboard
|
|
[self.inputToolbarView becomeFirstResponder];
|
|
};
|
|
|
|
return replyMenuItem;
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)replyInThreadMenuItemWithEvent:(MXEvent*)event
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *item = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReplyInThread];
|
|
item.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio;
|
|
item.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
|
|
|
|
if (RiotSettings.shared.enableThreads)
|
|
{
|
|
[self openThreadWithId:event.eventId];
|
|
}
|
|
else
|
|
{
|
|
[self showThreadsBetaForEvent:event];
|
|
}
|
|
};
|
|
|
|
return item;
|
|
}
|
|
|
|
- (RoomContextualMenuItem *)moreMenuItemWithEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
|
|
{
|
|
MXWeakify(self);
|
|
|
|
RoomContextualMenuItem *moreMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionMore];
|
|
moreMenuItem.action = ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self hideContextualMenuAnimated:YES completion:nil];
|
|
[self showAdditionalActionsMenuForEvent:event inCell:cell animated:YES];
|
|
};
|
|
|
|
return moreMenuItem;
|
|
}
|
|
|
|
#pragma mark - Threads
|
|
|
|
- (BOOL)showThreadOptionForEvent:(MXEvent*)event
|
|
{
|
|
return !self.roomDataSource.threadId
|
|
&& !event.threadId
|
|
&& (RiotSettings.shared.enableThreads || self.mainSession.store.supportedMatrixVersions.supportsThreads);
|
|
}
|
|
|
|
- (void)showThreadsNotice
|
|
{
|
|
if (!self.threadsNoticeModalPresenter)
|
|
{
|
|
self.threadsNoticeModalPresenter = [SlidingModalPresenter new];
|
|
}
|
|
|
|
[self.threadsNoticeModalPresenter dismissWithAnimated:NO completion:nil];
|
|
|
|
ThreadsNoticeViewController *threadsNoticeVC = [ThreadsNoticeViewController instantiate];
|
|
|
|
MXWeakify(self);
|
|
|
|
threadsNoticeVC.didTapDoneButton = ^{
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self.threadsNoticeModalPresenter dismissWithAnimated:YES completion:^{
|
|
RiotSettings.shared.threadsNoticeDisplayed = YES;
|
|
}];
|
|
};
|
|
|
|
[self.threadsNoticeModalPresenter present:threadsNoticeVC
|
|
from:self.presentedViewController?:self
|
|
animated:YES
|
|
options:SlidingModalPresenter.SpanningOption
|
|
completion:nil];
|
|
}
|
|
|
|
- (void)showThreadsBetaForEvent:(MXEvent *)event
|
|
{
|
|
if (self.threadsBetaBridgePresenter)
|
|
{
|
|
[self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.threadsBetaBridgePresenter = nil;
|
|
}
|
|
|
|
self.threadsBetaBridgePresenter = [[ThreadsBetaCoordinatorBridgePresenter alloc] initWithThreadId:event.eventId
|
|
infoText:VectorL10n.threadsBetaInformation
|
|
additionalText:nil];
|
|
self.threadsBetaBridgePresenter.delegate = self;
|
|
|
|
[self.threadsBetaBridgePresenter presentFrom:self.presentedViewController?:self animated:YES];
|
|
}
|
|
|
|
- (void)openThreadWithId:(NSString *)threadId
|
|
{
|
|
if (self.threadsBridgePresenter)
|
|
{
|
|
[self.threadsBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.threadsBridgePresenter = nil;
|
|
}
|
|
|
|
self.threadsBridgePresenter = [self.delegate threadsCoordinatorForRoomViewController:self threadId:threadId];
|
|
self.threadsBridgePresenter.delegate = self;
|
|
[self.threadsBridgePresenter pushFrom:self.navigationController animated:YES];
|
|
}
|
|
|
|
- (void)highlightAndDisplayEvent:(NSString *)eventId completion:(void (^)(void))completion
|
|
{
|
|
NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId];
|
|
if (row == NSNotFound)
|
|
{
|
|
// event with eventId is not loaded into data source yet, load another data source and display it
|
|
[self startActivityIndicator];
|
|
MXWeakify(self);
|
|
[RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
|
|
initialEventId:eventId
|
|
threadId:nil
|
|
andMatrixSession:self.roomDataSource.mxSession
|
|
onComplete:^(RoomDataSource *roomDataSource) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[roomDataSource finalizeInitialization];
|
|
[self stopActivityIndicator];
|
|
roomDataSource.markTimelineInitialEvent = YES;
|
|
[self displayRoom:roomDataSource];
|
|
// Give the data source ownership to the room view controller.
|
|
self.hasRoomDataSourceOwnership = YES;
|
|
if (completion)
|
|
{
|
|
completion();
|
|
}
|
|
}];
|
|
return;
|
|
}
|
|
|
|
NSMutableArray<NSIndexPath *> *rowsToReload = [[NSMutableArray alloc] init];
|
|
// Get the current hightlighted event because we will need to reload it
|
|
NSString *currentHiglightedEventId = self.customizedRoomDataSource.highlightedEventId;
|
|
if (currentHiglightedEventId)
|
|
{
|
|
NSInteger currentHiglightedRow = [self.roomDataSource indexOfCellDataWithEventId:currentHiglightedEventId];
|
|
if (currentHiglightedRow != NSNotFound)
|
|
{
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:currentHiglightedRow inSection:0];
|
|
if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath])
|
|
{
|
|
[rowsToReload addObject:indexPath];
|
|
}
|
|
}
|
|
}
|
|
|
|
self.customizedRoomDataSource.highlightedEventId = eventId;
|
|
|
|
// Add the new highligted event to the list of rows to reload
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
|
|
BOOL indexPathIsVisible = [[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath];
|
|
if (indexPathIsVisible)
|
|
{
|
|
[rowsToReload addObject:indexPath];
|
|
}
|
|
|
|
// Reload rows
|
|
if (rowsToReload.count > 0)
|
|
{
|
|
[self.bubblesTableView reloadRowsAtIndexPaths:rowsToReload
|
|
withRowAnimation:UITableViewRowAnimationNone];
|
|
}
|
|
|
|
// Scroll to the newly highlighted row
|
|
if (indexPathIsVisible || [self.bubblesTableView vc_hasIndexPath:indexPath])
|
|
{
|
|
[self.bubblesTableView scrollToRowAtIndexPath:indexPath
|
|
atScrollPosition:UITableViewScrollPositionMiddle
|
|
animated:YES];
|
|
}
|
|
|
|
if (completion)
|
|
{
|
|
completion();
|
|
}
|
|
}
|
|
|
|
- (void)cancelEventHighlight
|
|
{
|
|
// if data source is highlighting an event, dismiss the highlight when user dragges the table view
|
|
if (self.customizedRoomDataSource.highlightedEventId)
|
|
{
|
|
NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:self.customizedRoomDataSource.highlightedEventId];
|
|
if (row == NSNotFound)
|
|
{
|
|
self.customizedRoomDataSource.highlightedEventId = nil;
|
|
return;
|
|
}
|
|
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
|
|
if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath])
|
|
{
|
|
self.customizedRoomDataSource.highlightedEventId = nil;
|
|
[self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath]
|
|
withRowAnimation:UITableViewRowAnimationAutomatic];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)updateThreadListBarButtonBadgeWith:(MXThreadingService *)service
|
|
{
|
|
[self updateThreadListBarButtonItem:nil with:service];
|
|
}
|
|
|
|
- (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXThreadingService *)service
|
|
{
|
|
if (!service || _isWaitingForOtherParticipants)
|
|
{
|
|
return;
|
|
}
|
|
|
|
__block NSInteger replaceIndex = NSNotFound;
|
|
[self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem * _Nonnull item, NSUInteger index, BOOL * _Nonnull stop)
|
|
{
|
|
if (item.tag == kThreadListBarButtonItemTag)
|
|
{
|
|
replaceIndex = index;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
|
|
if (!barButtonItem && replaceIndex == NSNotFound)
|
|
{
|
|
// there is no thread list bar button item, and not provided another to update
|
|
// ignore
|
|
return;
|
|
}
|
|
|
|
UIBarButtonItem *threadListBarButtonItem = barButtonItem ?: [self threadListBarButtonItem];
|
|
UIButton *button = (UIButton *)threadListBarButtonItem.customView;
|
|
|
|
MXThreadNotificationsCount *notificationsCount = [service notificationsCountForRoom:self.roomDataSource.roomId];
|
|
|
|
UIImage *buttonIcon = [AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize];
|
|
[button setImage:buttonIcon forState:UIControlStateNormal];
|
|
button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot;
|
|
|
|
if (notificationsCount.notificationsNumber > 0)
|
|
{
|
|
BadgeLabel *badgeLabel = [[BadgeLabel alloc] init];
|
|
badgeLabel.text = notificationsCount.notificationsNumber > 99 ? @"99+" : [NSString stringWithFormat:@"%lu", notificationsCount.notificationsNumber];
|
|
id<Theme> theme = ThemeService.shared.theme;
|
|
badgeLabel.font = theme.fonts.caption1SB;
|
|
badgeLabel.textColor = theme.colors.navigation;
|
|
badgeLabel.badgeColor = notificationsCount.numberOfHighlightedThreads ? theme.colors.alert : theme.colors.secondaryContent;
|
|
[button addSubview:badgeLabel];
|
|
|
|
[badgeLabel layoutIfNeeded];
|
|
|
|
badgeLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
[badgeLabel.centerYAnchor constraintEqualToAnchor:button.centerYAnchor
|
|
constant:badgeLabel.bounds.size.height - buttonIcon.size.height / 2].active = YES;
|
|
[badgeLabel.centerXAnchor constraintEqualToAnchor:button.centerXAnchor
|
|
constant:badgeLabel.bounds.size.width + buttonIcon.size.width / 2].active = YES;
|
|
}
|
|
|
|
if (replaceIndex == NSNotFound)
|
|
{
|
|
// there is no thread list bar button item, this was only an update
|
|
return;
|
|
}
|
|
|
|
UIBarButtonItem *originalItem = self.navigationItem.rightBarButtonItems[replaceIndex];
|
|
UIButton *originalButton = (UIButton *)originalItem.customView;
|
|
if ([originalButton imageForState:UIControlStateNormal] == [button imageForState:UIControlStateNormal]
|
|
&& UIEdgeInsetsEqualToEdgeInsets(originalButton.contentEdgeInsets, button.contentEdgeInsets))
|
|
{
|
|
// no need to replace, it's the same
|
|
return;
|
|
}
|
|
NSMutableArray<UIBarButtonItem*> *items = [self.navigationItem.rightBarButtonItems mutableCopy];
|
|
items[replaceIndex] = threadListBarButtonItem;
|
|
self.navigationItem.rightBarButtonItems = items;
|
|
}
|
|
|
|
#pragma mark - RoomContextualMenuViewControllerDelegate
|
|
|
|
- (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController
|
|
{
|
|
[self hideContextualMenuAnimated:YES];
|
|
}
|
|
|
|
#pragma mark - ReactionsMenuViewModelCoordinatorDelegate
|
|
|
|
- (void)reactionsMenuViewModel:(ReactionsMenuViewModel *)viewModel didAddReaction:(NSString *)reaction forEventId:(NSString *)eventId
|
|
{
|
|
MXWeakify(self);
|
|
|
|
[self hideContextualMenuAnimated:YES completion:^{
|
|
|
|
[self.roomDataSource addReaction:reaction forEventId:eventId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
|
|
}];
|
|
}];
|
|
}
|
|
|
|
- (void)reactionsMenuViewModel:(ReactionsMenuViewModel *)viewModel didRemoveReaction:(NSString *)reaction forEventId:(NSString *)eventId
|
|
{
|
|
MXWeakify(self);
|
|
|
|
[self hideContextualMenuAnimated:YES completion:^{
|
|
|
|
[self.roomDataSource removeReaction:reaction forEventId:eventId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
|
|
}];
|
|
|
|
}];
|
|
}
|
|
|
|
- (void)reactionsMenuViewModelDidTapMoreReactions:(ReactionsMenuViewModel *)viewModel forEventId:(NSString *)eventId
|
|
{
|
|
[self hideContextualMenuAnimated:YES];
|
|
|
|
[self showEmojiPickerForEventId:eventId];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)showEditHistoryForEventId:(NSString*)eventId animated:(BOOL)animated
|
|
{
|
|
MXEvent *event = [self.roomDataSource eventWithEventId:eventId];
|
|
EditHistoryCoordinatorBridgePresenter *presenter = [[EditHistoryCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession event:event];
|
|
|
|
presenter.delegate = self;
|
|
[presenter presentFrom:self animated:animated];
|
|
|
|
self.editHistoryPresenter = presenter;
|
|
}
|
|
|
|
#pragma mark - EditHistoryCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)editHistoryCoordinatorBridgePresenterDelegateDidComplete:(EditHistoryCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.editHistoryPresenter = nil;
|
|
}
|
|
|
|
#pragma mark - DocumentPickerPresenterDelegate
|
|
|
|
- (void)documentPickerPresenterWasCancelled:(MXKDocumentPickerPresenter *)presenter
|
|
{
|
|
self.documentPickerPresenter = nil;
|
|
}
|
|
|
|
- (void)documentPickerPresenter:(MXKDocumentPickerPresenter *)presenter didPickDocumentsAt:(NSURL *)url
|
|
{
|
|
self.documentPickerPresenter = nil;
|
|
|
|
MXKUTI *fileUTI = [[MXKUTI alloc] initWithLocalFileURL:url];
|
|
NSString *mimeType = fileUTI.mimeType;
|
|
|
|
if (fileUTI.isImage)
|
|
{
|
|
NSData *imageData = [[NSData alloc] initWithContentsOfURL:url];
|
|
|
|
[self sendImage:imageData mimeType:mimeType];
|
|
}
|
|
else if (fileUTI.isVideo)
|
|
{
|
|
[self sendVideo:url];
|
|
}
|
|
else if (fileUTI.isFile)
|
|
{
|
|
[self sendFile:url mimeType:mimeType];
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[MXKRoomViewController] File upload using MIME type %@ is not supported.", mimeType);
|
|
|
|
[self showAlertWithTitle:[VectorL10n fileUploadErrorTitle]
|
|
message:[VectorL10n fileUploadErrorUnsupportedFileTypeMessage]];
|
|
}
|
|
}
|
|
|
|
- (void)sendImage:(NSData *)imageData mimeType:(NSString *)mimeType {
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if (readyToSend)
|
|
{
|
|
// Let the datasource send it and manage the local echo
|
|
[self.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 failed.");
|
|
}];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)sendVideo:(NSURL * _Nonnull)url {
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if (readyToSend)
|
|
{
|
|
// Let the datasource send it and manage the local echo
|
|
[(RoomDataSource*)self.roomDataSource sendVideo:url 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.");
|
|
}];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)sendFile:(NSURL * _Nonnull)url mimeType:(NSString *)mimeType {
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
MXWeakify(self);
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
if (readyToSend)
|
|
{
|
|
// Let the datasource send it and manage the local echo
|
|
[self.roomDataSource sendFile:url 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.");
|
|
}];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
#pragma mark - EmojiPickerCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)emojiPickerCoordinatorBridgePresenter:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didAddEmoji:(NSString *)emoji forEventId:(NSString *)eventId
|
|
{
|
|
MXWeakify(self);
|
|
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
|
|
[self.roomDataSource addReaction:emoji forEventId:eventId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
|
|
}];
|
|
}];
|
|
self.emojiPickerCoordinatorBridgePresenter = nil;
|
|
}
|
|
|
|
- (void)emojiPickerCoordinatorBridgePresenter:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didRemoveEmoji:(NSString *)emoji forEventId:(NSString *)eventId
|
|
{
|
|
MXWeakify(self);
|
|
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
|
|
|
|
[self.roomDataSource removeReaction:emoji forEventId:eventId success:^{
|
|
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
|
|
}];
|
|
}];
|
|
self.emojiPickerCoordinatorBridgePresenter = nil;
|
|
}
|
|
|
|
- (void)emojiPickerCoordinatorBridgePresenterDidCancel:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.emojiPickerCoordinatorBridgePresenter = nil;
|
|
}
|
|
|
|
#pragma mark - ReactionHistoryCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)reactionHistoryCoordinatorBridgePresenterDelegateDidClose:(ReactionHistoryCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
|
|
self.reactionHistoryCoordinatorBridgePresenter = nil;
|
|
}];
|
|
}
|
|
|
|
#pragma mark - CameraPresenterDelegate
|
|
|
|
- (void)cameraPresenterDidCancel:(CameraPresenter *)cameraPresenter
|
|
{
|
|
[cameraPresenter dismissWithAnimated:YES completion:nil];
|
|
self.cameraPresenter = nil;
|
|
}
|
|
|
|
- (void)cameraPresenter:(CameraPresenter *)cameraPresenter didSelectImage:(UIImage *)image
|
|
{
|
|
[cameraPresenter dismissWithAnimated:YES completion:nil];
|
|
self.cameraPresenter = nil;
|
|
|
|
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
|
|
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
[self.inputToolbarView sendSelectedImage:imageData
|
|
withMimeType:MXKUTI.jpeg.mimeType
|
|
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
|
|
isPhotoLibraryAsset:NO];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)cameraPresenter:(CameraPresenter *)cameraPresenter didSelectVideoAt:(NSURL *)url
|
|
{
|
|
[cameraPresenter dismissWithAnimated:YES completion:nil];
|
|
self.cameraPresenter = nil;
|
|
|
|
AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url];
|
|
[self sendVideoAsset:selectedVideo isPhotoLibraryAsset:NO];
|
|
}
|
|
|
|
#pragma mark - MediaPickerCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)mediaPickerCoordinatorBridgePresenterDidCancel:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.mediaPickerPresenter = nil;
|
|
}
|
|
|
|
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectImageData:(NSData *)imageData withUTI:(MXKUTI *)uti
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.mediaPickerPresenter = nil;
|
|
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
[self.inputToolbarView sendSelectedImage:imageData
|
|
withMimeType:uti.mimeType
|
|
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
|
|
isPhotoLibraryAsset:YES];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectVideo:(AVAsset *)videoAsset
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.mediaPickerPresenter = nil;
|
|
|
|
[self sendVideoAsset:videoAsset isPhotoLibraryAsset:YES];
|
|
}
|
|
|
|
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectAssets:(NSArray<PHAsset *> *)assets
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.mediaPickerPresenter = nil;
|
|
|
|
// Set a 1080p video conversion preset as compression mode only has an effect on the images.
|
|
[MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
|
|
|
|
// Create before sending the message in case of a discussion (direct chat)
|
|
[self createDiscussionIfNeeded:^(BOOL readyToSend) {
|
|
if (readyToSend && [self inputToolbarConformsToToolbarViewProtocol])
|
|
{
|
|
[self.inputToolbarView sendSelectedAssets:assets withCompressionMode:MediaCompressionHelper.defaultCompressionMode];
|
|
}
|
|
// Errors are handled at the request level. This should be improved in case of code rewriting.
|
|
}];
|
|
}
|
|
|
|
#pragma mark - RoomCreationModalCoordinatorBridgePresenter
|
|
|
|
- (void)roomCreationModalCoordinatorBridgePresenterDelegateDidComplete:(RoomCreationModalCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.roomCreationModalCoordinatorBridgePresenter = nil;
|
|
}
|
|
|
|
#pragma mark - RoomInfoCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)roomInfoCoordinatorBridgePresenterDelegateDidComplete:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
|
|
self.roomInfoCoordinatorBridgePresenter = nil;
|
|
}
|
|
|
|
- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didRequestMentionForMember:(MXRoomMember *)member
|
|
{
|
|
[self mention:member];
|
|
}
|
|
|
|
- (void)roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[self notifyDelegateOnLeaveRoomIfNecessary];
|
|
}
|
|
|
|
- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didReplaceRoomWithReplacementId:(NSString *)roomId
|
|
{
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate roomViewController:self didReplaceRoomWithReplacementId:roomId];
|
|
}
|
|
else
|
|
{
|
|
ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews:NO];
|
|
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:nil mxSession:self.mainSession presentationParameters:presentationParameters showSettingsInitially:YES];
|
|
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
|
|
}
|
|
}
|
|
|
|
- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator
|
|
viewEventInTimeline:(MXEvent *)event
|
|
{
|
|
[self.navigationController popToViewController:self animated:true];
|
|
[self reloadRoomWihtEventId:event.eventId threadId:event.threadId forceUpdateRoomMarker:NO];
|
|
}
|
|
|
|
- (void)roomInfoCoordinatorBridgePresenterDidRequestReportRoom:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[self handleReportRoom];
|
|
}
|
|
|
|
-(void)reloadRoomWihtEventId:(NSString *)eventId
|
|
threadId:(NSString *)threadId
|
|
forceUpdateRoomMarker:(BOOL)forceUpdateRoomMarker
|
|
{
|
|
// Jump to the last unread event by using a temporary room data source initialized with the last unread event id.
|
|
MXWeakify(self);
|
|
[RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
|
|
initialEventId:eventId
|
|
threadId:threadId
|
|
andMatrixSession:self.mainSession
|
|
onComplete:^(id roomDataSource) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[roomDataSource finalizeInitialization];
|
|
|
|
// Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view.
|
|
self.centerBubblesTableViewContentOnTheInitialEventBottom = YES;
|
|
[self displayRoom:roomDataSource];
|
|
|
|
// Give the data source ownership to the room view controller.
|
|
self.hasRoomDataSourceOwnership = YES;
|
|
|
|
// Force the read marker update if needed (e.g if we jumped on the last unread message using the banner).
|
|
self.updateRoomReadMarker |= forceUpdateRoomMarker;
|
|
}];
|
|
}
|
|
|
|
#pragma mark - RemoveJitsiWidgetViewDelegate
|
|
|
|
- (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view
|
|
{
|
|
view.delegate = nil;
|
|
Widget *jitsiWidget = [self.customizedRoomDataSource jitsiWidget];
|
|
|
|
[self startActivityIndicator];
|
|
|
|
// close the widget
|
|
MXWeakify(self);
|
|
|
|
[[WidgetManager sharedManager] closeWidget:jitsiWidget.widgetId
|
|
inRoom:self.roomDataSource.room
|
|
success:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self stopActivityIndicator];
|
|
// we can wait for kWidgetManagerDidUpdateWidgetNotification, but we want to be faster
|
|
self.removeJitsiWidgetContainer.hidden = YES;
|
|
self.removeJitsiWidgetView.delegate = nil;
|
|
|
|
// end active call if exists
|
|
if ([self isRoomHavingAJitsiCall])
|
|
{
|
|
[self endActiveJitsiCall];
|
|
}
|
|
} failure:^(NSError *error) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self showJitsiErrorAsAlert:error];
|
|
[self stopActivityIndicator];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - VoiceMessageControllerDelegate
|
|
|
|
- (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageController *)voiceMessageController
|
|
{
|
|
NSString *message = [VectorL10n microphoneAccessNotGrantedForVoiceMessage:AppInfo.current.displayName];
|
|
|
|
[MXKTools checkAccessForMediaType:AVMediaTypeAudio
|
|
manualChangeMessage: message
|
|
showPopUpInViewController:self completionHandler:^(BOOL granted) {
|
|
|
|
}];
|
|
}
|
|
|
|
- (BOOL)voiceMessageControllerDidRequestRecording:(VoiceMessageController *)voiceMessageController
|
|
{
|
|
MXSession* session = self.roomDataSource.mxSession;
|
|
// Check whether the user is not already broadcasting here or in another room
|
|
if (session.voiceBroadcastService)
|
|
{
|
|
[self showAlertWithTitle:[VectorL10n voiceMessageBroadcastInProgressTitle] message:[VectorL10n voiceMessageBroadcastInProgressMessage]];
|
|
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController
|
|
didRequestSendForFileAtURL:(NSURL *)url
|
|
duration:(NSUInteger)duration
|
|
samples:(NSArray<NSNumber *> *)samples
|
|
completion:(void (^)(BOOL))completion
|
|
{
|
|
[self.roomDataSource sendVoiceMessage:url additionalContentParams:nil mimeType:nil duration:duration samples:samples success:^(NSString *eventId) {
|
|
MXLogDebug(@"Success with event id %@", eventId);
|
|
completion(YES);
|
|
} failure:^(NSError *error) {
|
|
MXLogError(@"Failed sending voice message");
|
|
completion(NO);
|
|
}];
|
|
}
|
|
|
|
#pragma mark - SpaceDetailPresenterDelegate
|
|
|
|
- (void)spaceDetailPresenterDidComplete:(SpaceDetailPresenter *)presenter
|
|
{
|
|
self.spaceDetailPresenter = nil;
|
|
}
|
|
|
|
- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didOpenSpaceWithId:(NSString *)spaceId
|
|
{
|
|
self.spaceDetailPresenter = nil;
|
|
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
|
|
}
|
|
|
|
- (void)spaceDetailPresenter:(SpaceDetailPresenter *)presenter didJoinSpaceWithId:(NSString *)spaceId
|
|
{
|
|
self.spaceDetailPresenter = nil;
|
|
[[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId];
|
|
}
|
|
|
|
#pragma mark - CompletionSuggestionCoordinatorBridgeDelegate
|
|
|
|
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
|
|
didRequestMentionForMember:(MXRoomMember *)member
|
|
textTrigger:(NSString *)textTrigger
|
|
{
|
|
[self removeTriggerTextFromComposer:textTrigger];
|
|
[self mention:member];
|
|
}
|
|
|
|
- (void)completionSuggestionCoordinatorBridgeDidRequestMentionForRoom:(CompletionSuggestionCoordinatorBridge *)coordinator
|
|
textTrigger:(NSString *)textTrigger
|
|
{
|
|
[self removeTriggerTextFromComposer:textTrigger];
|
|
[self.inputToolbarView pasteText:[CompletionSuggestionUserID.room stringByAppendingString:@" "]];
|
|
}
|
|
|
|
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator
|
|
didRequestCommand:(NSString *)command
|
|
textTrigger:(NSString *)textTrigger
|
|
{
|
|
[self removeTriggerTextFromComposer:textTrigger];
|
|
[self setCommand:command];
|
|
}
|
|
|
|
- (void)removeTriggerTextFromComposer:(NSString *)textTrigger
|
|
{
|
|
RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView;
|
|
Class roomInputToolbarViewClass = [RoomViewController mainToolbarClass];
|
|
|
|
// RTE handles removing the text trigger by itself.
|
|
if (roomInputToolbarViewClass == WysiwygInputToolbarView.class && RiotSettings.shared.enableWysiwygTextFormatting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (toolbar && textTrigger.length) {
|
|
NSMutableAttributedString *attributedTextMessage = [[NSMutableAttributedString alloc] initWithAttributedString:toolbar.attributedTextMessage];
|
|
[[attributedTextMessage mutableString] replaceOccurrencesOfString:textTrigger
|
|
withString:@""
|
|
options:NSBackwardsSearch | NSAnchoredSearch
|
|
range:NSMakeRange(0, attributedTextMessage.length)];
|
|
[toolbar setAttributedTextMessage:attributedTextMessage];
|
|
}
|
|
}
|
|
|
|
- (void)completionSuggestionCoordinatorBridge:(CompletionSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height
|
|
{
|
|
if (self.completionSuggestionContainerHeightConstraint.constant != height)
|
|
{
|
|
self.completionSuggestionContainerHeightConstraint.constant = height;
|
|
|
|
[self.view layoutIfNeeded];
|
|
}
|
|
}
|
|
|
|
#pragma mark - ThreadsCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)threadsCoordinatorBridgePresenterDelegateDidComplete:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
self.threadsBridgePresenter = nil;
|
|
}
|
|
|
|
- (void)threadsCoordinatorBridgePresenterDelegateDidSelect:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter roomId:(NSString *)roomId eventId:(NSString *)eventId
|
|
{
|
|
MXWeakify(self);
|
|
[self.threadsBridgePresenter dismissWithAnimated:YES completion:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (eventId)
|
|
{
|
|
[self highlightAndDisplayEvent:eventId completion:nil];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)threadsCoordinatorBridgePresenterDidDismissInteractively:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
self.threadsBridgePresenter = nil;
|
|
}
|
|
|
|
#pragma mark - ThreadsBetaCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)threadsBetaCoordinatorBridgePresenterDelegateDidTapEnable:(ThreadsBetaCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
MXWeakify(self);
|
|
[self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self cancelEventSelection];
|
|
[self.roomDataSource reload];
|
|
[self openThreadWithId:coordinatorBridgePresenter.threadId];
|
|
}];
|
|
}
|
|
|
|
- (void)threadsBetaCoordinatorBridgePresenterDelegateDidTapCancel:(ThreadsBetaCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
MXWeakify(self);
|
|
[self.threadsBetaBridgePresenter dismissWithAnimated:YES completion:^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
[self cancelEventSelection];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - MXThreadingServiceDelegate
|
|
|
|
- (void)threadingServiceDidUpdateThreads:(MXThreadingService *)service
|
|
{
|
|
[self updateThreadListBarButtonBadgeWith:service];
|
|
}
|
|
|
|
#pragma mark - RoomParticipantsInviteCoordinatorBridgePresenterDelegate
|
|
|
|
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidComplete:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
self.participantsInvitePresenter = nil;
|
|
}
|
|
|
|
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidStartLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[self startActivityIndicator];
|
|
}
|
|
|
|
- (void)roomParticipantsInviteCoordinatorBridgePresenterDidEndLoading:(RoomParticipantsInviteCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
[self stopActivityIndicator];
|
|
}
|
|
|
|
#pragma mark - Pills
|
|
/// Register provider for Pills.
|
|
- (void)registerPillAttachmentViewProviderIfNeeded
|
|
{
|
|
if (@available(iOS 15.0, *))
|
|
{
|
|
if (![NSTextAttachment textAttachmentViewProviderClassForFileType:PillsFormatter.pillUTType])
|
|
{
|
|
[NSTextAttachment registerTextAttachmentViewProviderClass:PillAttachmentViewProvider.class forFileType:PillsFormatter.pillUTType];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - ComposerCreateActionListBridgePresenter
|
|
|
|
- (void)composerCreateActionListBridgePresenterDelegateDidComplete:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter action:(enum ComposerCreateAction)action
|
|
{
|
|
|
|
[coordinatorBridgePresenter dismissWithAnimated:true completion:^{
|
|
switch (action) {
|
|
case ComposerCreateActionPhotoLibrary:
|
|
[self showMediaPickerAnimated:YES];
|
|
break;
|
|
case ComposerCreateActionStickers:
|
|
[self roomInputToolbarViewPresentStickerPicker];
|
|
break;
|
|
case ComposerCreateActionAttachments:
|
|
[self roomInputToolbarViewDidTapFileUpload];
|
|
break;
|
|
case ComposerCreateActionVoiceBroadcast:
|
|
[self roomInputToolbarViewDidTapVoiceBroadcast];
|
|
break;
|
|
case ComposerCreateActionPolls:
|
|
[self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self];
|
|
break;
|
|
case ComposerCreateActionLocation:
|
|
[self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self];
|
|
break;
|
|
case ComposerCreateActionCamera:
|
|
[self showCameraControllerAnimated:YES];
|
|
break;
|
|
}
|
|
self.composerCreateActionListBridgePresenter = nil;
|
|
}];
|
|
}
|
|
|
|
- (void)composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter enabled:(BOOL)enabled
|
|
{
|
|
[self togglePlainTextMode];
|
|
}
|
|
|
|
- (void)composerCreateActionListBridgePresenterDidDismissInteractively:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter
|
|
{
|
|
self.composerCreateActionListBridgePresenter = nil;
|
|
}
|
|
|
|
@end
|