talk-ios/NextcloudTalk/NCChatViewController.m

2327 строки
102 KiB
Objective-C

/**
* @copyright Copyright (c) 2020 Ivan Sein <ivan@nextcloud.com>
*
* @author Ivan Sein <ivan@nextcloud.com>
*
* @license GNU GPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#import <AVFoundation/AVFoundation.h>
#import "NCChatViewController.h"
#import "AFImageDownloader.h"
#import "CallKitManager.h"
#import "ChatMessageTableViewCell.h"
#import "DirectoryTableViewController.h"
#import "GroupedChatMessageTableViewCell.h"
#import "FileMessageTableViewCell.h"
#import "FTPopOverMenu.h"
#import "SystemMessageTableViewCell.h"
#import "MessageSeparatorTableViewCell.h"
#import "DateHeaderView.h"
#import "PlaceholderView.h"
#import "NCAPIController.h"
#import "NCAppBranding.h"
#import "NCChatController.h"
#import "NCChatMessage.h"
#import "NCDatabaseManager.h"
#import "NCMessageParameter.h"
#import "NCChatTitleView.h"
#import "NCMessageTextView.h"
#import "NCNavigationController.h"
#import "NCImageSessionManager.h"
#import "NCRoomsManager.h"
#import "NCSettingsController.h"
#import "NCUserInterfaceController.h"
#import "NCUtils.h"
#import "NSDate+DateTools.h"
#import "ReplyMessageView.h"
#import "QuotedMessageView.h"
#import "RoomInfoTableViewController.h"
#import "ShareConfirmationViewController.h"
#import "UIImageView+AFNetworking.h"
#import "UIImageView+Letters.h"
#import "UIView+Toast.h"
#import "BarButtonItemWithActivity.h"
#import "ShareItem.h"
#import "NCChatFileController.h"
#import <NCCommunication/NCCommunication.h>
#import <QuickLook/QuickLook.h>
typedef enum NCChatMessageAction {
kNCChatMessageActionReply = 1,
kNCChatMessageActionCopy,
kNCChatMessageActionResend,
kNCChatMessageActionDelete,
kNCChatMessageActionReplyPrivately,
kNCChatMessageActionOpenFileInNextcloud
} NCChatMessageAction;
@interface NCChatViewController () <UIGestureRecognizerDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIDocumentPickerDelegate, ShareConfirmationViewControllerDelegate, FileMessageTableViewCellDelegate, NCChatFileControllerDelegate, QLPreviewControllerDelegate, QLPreviewControllerDataSource>
@property (nonatomic, strong) NCChatController *chatController;
@property (nonatomic, strong) NCChatTitleView *titleView;
@property (nonatomic, strong) PlaceholderView *chatBackgroundView;
@property (nonatomic, strong) NSMutableDictionary *messages;
@property (nonatomic, strong) NSMutableArray *dateSections;
@property (nonatomic, strong) NSMutableArray *mentions;
@property (nonatomic, strong) NSMutableArray *autocompletionUsers;
@property (nonatomic, assign) BOOL hasRequestedInitialHistory;
@property (nonatomic, assign) BOOL hasReceiveInitialHistory;
@property (nonatomic, assign) BOOL hasReceiveNewMessages;
@property (nonatomic, assign) BOOL retrievingHistory;
@property (nonatomic, assign) BOOL isVisible;
@property (nonatomic, assign) BOOL hasJoinedRoom;
@property (nonatomic, assign) BOOL leftChatWithVisibleChatVC;
@property (nonatomic, assign) BOOL offlineMode;
@property (nonatomic, assign) BOOL hasStoredHistory;
@property (nonatomic, assign) BOOL hasStopped;
@property (nonatomic, assign) NSInteger lastReadMessage;
@property (nonatomic, strong) NCChatMessage *unreadMessagesSeparator;
@property (nonatomic, strong) NSIndexPath *unreadMessagesSeparatorIP;
@property (nonatomic, assign) NSInteger chatViewPresentedTimestamp;
@property (nonatomic, strong) UIActivityIndicatorView *loadingHistoryView;
@property (nonatomic, assign) NSIndexPath *firstUnreadMessageIP;
@property (nonatomic, strong) UIButton *unreadMessageButton;
@property (nonatomic, strong) NSTimer *lobbyCheckTimer;
@property (nonatomic, strong) ReplyMessageView *replyMessageView;
@property (nonatomic, strong) UIImagePickerController *imagePicker;
@property (nonatomic, strong) BarButtonItemWithActivity *videoCallButton;
@property (nonatomic, strong) BarButtonItemWithActivity *voiceCallButton;
@property (nonatomic, assign) BOOL isPreviewControllerShown;
@property (nonatomic, strong) NSString *previewControllerFilePath;
@end
@implementation NCChatViewController
NSString * const NCChatViewControllerReplyPrivatelyNotification = @"NCChatViewControllerReplyPrivatelyNotification";
- (instancetype)initForRoom:(NCRoom *)room
{
self = [super initWithTableViewStyle:UITableViewStylePlain];
if (self) {
self.room = room;
self.chatController = [[NCChatController alloc] initForRoom:room];
self.hidesBottomBarWhenPushed = YES;
// Fixes problem with tableView contentSize on iOS 11
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
// Register a SLKTextView subclass, if you need any special appearance and/or behavior customisation.
[self registerClassForTextView:[NCMessageTextView class]];
// Register ReplyMessageView class, conforming to SLKTypingIndicatorProtocol, as a custom typing indicator view.
[self registerClassForTypingIndicatorView:[ReplyMessageView class]];
// Set image downloader to file preview imageviews.
[FilePreviewImageView setSharedImageDownloader:[[NCAPIController sharedInstance] imageDownloader]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateRoom:) name:NCRoomsManagerDidUpdateRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didJoinRoom:) name:NCRoomsManagerDidJoinRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didLeaveRoom:) name:NCRoomsManagerDidLeaveRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveInitialChatHistory:) name:NCChatControllerDidReceiveInitialChatHistoryNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveInitialChatHistoryOffline:) name:NCChatControllerDidReceiveInitialChatHistoryOfflineNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveChatHistory:) name:NCChatControllerDidReceiveChatHistoryNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveChatMessages:) name:NCChatControllerDidReceiveChatMessagesNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didSendChatMessage:) name:NCChatControllerDidSendChatMessageNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveChatBlocked:) name:NCChatControllerDidReceiveChatBlockedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didRemoveTemporaryMessages:) name:NCChatControllerDidRemoveTemporaryMessagesNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
self.titleView = [[NCChatTitleView alloc] init];
self.titleView.frame = CGRectMake(0, 0, 800, 30);
self.titleView.autoresizingMask=UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.titleView.title addTarget:self action:@selector(titleButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.titleView = _titleView;
[self setTitleView];
[self configureActionItems];
// Disable room info, input bar and call buttons until joining the room
[self disableRoomControls];
self.messages = [[NSMutableDictionary alloc] init];
self.mentions = [[NSMutableArray alloc] init];
self.dateSections = [[NSMutableArray alloc] init];
self.bounces = NO;
self.shakeToClearEnabled = YES;
self.keyboardPanningEnabled = YES;
self.shouldScrollToBottomAfterKeyboardShows = YES;
self.inverted = NO;
[self.rightButton setTitle:@"" forState:UIControlStateNormal];
[self.rightButton setImage:[UIImage imageNamed:@"send"] forState:UIControlStateNormal];
self.rightButton.accessibilityLabel = NSLocalizedString(@"Send message", nil);
self.rightButton.accessibilityHint = NSLocalizedString(@"Double tap to send message", nil);
[self.leftButton setImage:[UIImage imageNamed:@"attachment"] forState:UIControlStateNormal];
self.leftButton.accessibilityLabel = NSLocalizedString(@"Share a file from your Nextcloud", nil);
self.leftButton.accessibilityHint = NSLocalizedString(@"Double tap to open file browser", nil);
self.textInputbar.autoHideRightButton = NO;
NSInteger chatMaxLength = [[NCSettingsController sharedInstance] chatMaxLengthConfigCapability];
self.textInputbar.maxCharCount = chatMaxLength;
self.textInputbar.counterStyle = SLKCounterStyleLimitExceeded;
self.textInputbar.counterPosition = SLKCounterPositionTop;
// Only show char counter when chat is limited to 1000 chars
if (chatMaxLength == kDefaultChatMaxLength) {
self.textInputbar.counterStyle = SLKCounterStyleCountdownReversed;
}
self.textInputbar.translucent = NO;
self.textInputbar.backgroundColor = [UIColor colorWithRed:247.0/255.0 green:247.0/255.0 blue:247.0/255.0 alpha:1.0]; //Default toolbar color
[self.textInputbar.editorTitle setTextColor:[UIColor darkGrayColor]];
[self.textInputbar.editorLeftButton setTintColor:[UIColor colorWithRed:0.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1.0]];
[self.textInputbar.editorRightButton setTintColor:[UIColor colorWithRed:0.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1.0]];
self.navigationController.navigationBar.tintColor = [NCAppBranding themeTextColor];
if (@available(iOS 13.0, *)) {
UIColor *themeColor = [NCAppBranding themeColor];
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
[appearance configureWithOpaqueBackground];
appearance.backgroundColor = themeColor;
appearance.titleTextAttributes = @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]};
self.navigationItem.standardAppearance = appearance;
self.navigationItem.compactAppearance = appearance;
self.navigationItem.scrollEdgeAppearance = appearance;
}
// Add long press gesture recognizer
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPressGesture.delegate = self;
[self.tableView addGestureRecognizer:longPressGesture];
self.longPressGesture = longPressGesture;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self.tableView registerClass:[ChatMessageTableViewCell class] forCellReuseIdentifier:ChatMessageCellIdentifier];
[self.tableView registerClass:[ChatMessageTableViewCell class] forCellReuseIdentifier:ReplyMessageCellIdentifier];
[self.tableView registerClass:[GroupedChatMessageTableViewCell class] forCellReuseIdentifier:GroupedChatMessageCellIdentifier];
[self.tableView registerClass:[FileMessageTableViewCell class] forCellReuseIdentifier:FileMessageCellIdentifier];
[self.tableView registerClass:[FileMessageTableViewCell class] forCellReuseIdentifier:GroupedFileMessageCellIdentifier];
[self.tableView registerClass:[SystemMessageTableViewCell class] forCellReuseIdentifier:SystemMessageCellIdentifier];
[self.tableView registerClass:[MessageSeparatorTableViewCell class] forCellReuseIdentifier:MessageSeparatorCellIdentifier];
[self.autoCompletionView registerClass:[ChatMessageTableViewCell class] forCellReuseIdentifier:AutoCompletionCellIdentifier];
[self registerPrefixesForAutoCompletion:@[@"@"]];
// Chat placeholder view
_chatBackgroundView = [[PlaceholderView alloc] init];
[_chatBackgroundView.placeholderView setHidden:YES];
[_chatBackgroundView.loadingView startAnimating];
self.tableView.backgroundView = _chatBackgroundView;
// Unread messages indicator
_firstUnreadMessageIP = nil;
_unreadMessageButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 126, 24)];
_unreadMessageButton.backgroundColor = [NCAppBranding themeColor];
[_unreadMessageButton setTitleColor:[NCAppBranding themeTextColor] forState:UIControlStateNormal];
_unreadMessageButton.titleLabel.font = [UIFont systemFontOfSize:12];
_unreadMessageButton.layer.cornerRadius = 12;
_unreadMessageButton.clipsToBounds = YES;
_unreadMessageButton.hidden = YES;
_unreadMessageButton.translatesAutoresizingMaskIntoConstraints = NO;
_unreadMessageButton.contentEdgeInsets = UIEdgeInsetsMake(0.0f, 10.0f, 0.0f, 10.0f);
_unreadMessageButton.titleLabel.minimumScaleFactor = 0.9f;
_unreadMessageButton.titleLabel.numberOfLines = 1;
_unreadMessageButton.titleLabel.adjustsFontSizeToFitWidth = YES;
NSString *buttonText = NSLocalizedString(@"↓ New messages", nil);
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:12]};
CGRect textSize = [buttonText boundingRectWithSize:CGSizeMake(300, 24) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:NULL];
CGFloat buttonWidth = textSize.size.width + 20;
[_unreadMessageButton addTarget:self action:@selector(unreadMessagesButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[_unreadMessageButton setTitle:buttonText forState:UIControlStateNormal];
// Unread messages separator
_unreadMessagesSeparator = [[NCChatMessage alloc] init];
_unreadMessagesSeparator.messageId = kUnreadMessagesSeparatorIdentifier;
self.hasStoredHistory = YES;
[self.view addSubview:_unreadMessageButton];
_chatViewPresentedTimestamp = [[NSDate date] timeIntervalSince1970];
_lastReadMessage = _room.lastReadMessage;
// Check if there's a stored pending message
if (_room.pendingMessage != nil) {
[self setChatMessage:self.room.pendingMessage];
}
NSDictionary *views = @{@"unreadMessagesButton": _unreadMessageButton,
@"textInputbar": self.textInputbar};
NSDictionary *metrics = @{@"buttonWidth": @(buttonWidth)};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[unreadMessagesButton(24)]-5-[textInputbar]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[unreadMessagesButton(buttonWidth)]-(>=0)-|" options:0 metrics:metrics views:views]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual
toItem:_unreadMessageButton attribute:NSLayoutAttributeCenterX multiplier:1.f constant:0.f]];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self checkRoomControlsAvailability];
if (!_hasReceiveInitialHistory && !_hasRequestedInitialHistory) {
_hasRequestedInitialHistory = YES;
[_chatController getInitialChatHistory];
}
_isVisible = YES;
if (!_offlineMode) {
[[NCRoomsManager sharedInstance] joinRoom:_room.token];
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self savePendingMessage];
_isVisible = NO;
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
// Leave chat when the view controller has been removed from its parent view.
if (self.isMovingFromParentViewController) {
[self leaveChat];
}
[_videoCallButton hideActivityIndicator];
[_voiceCallButton hideActivityIndicator];
}
- (void)stopChat
{
_hasStopped = YES;
[_chatController stopChatController];
[self cleanChat];
}
- (void)resumeChat
{
_hasStopped = NO;
if (!_hasReceiveInitialHistory && !_hasRequestedInitialHistory) {
_hasRequestedInitialHistory = YES;
[_chatController getInitialChatHistory];
}
}
- (void)leaveChat
{
[_lobbyCheckTimer invalidate];
[_chatController stopChatController];
// If this chat view controller is for the same room as the one owned by the rooms manager
// then we should not try to leave the chat. Since we will leave the chat when the
// chat view controller owned by rooms manager moves from parent view controller.
if ([[NCRoomsManager sharedInstance].chatViewController.room.token isEqualToString:_room.token] &&
[NCRoomsManager sharedInstance].chatViewController != self) {
return;
}
[[NCRoomsManager sharedInstance] leaveChatInRoom:_room.token];
// Remove chat view controller pointer if this chat is owned by rooms manager
// and the chat view is moving from parent view controller
if ([NCRoomsManager sharedInstance].chatViewController == self) {
[NCRoomsManager sharedInstance].chatViewController = nil;
}
}
- (void)setChatMessage:(NSString *)message
{
dispatch_async(dispatch_get_main_queue(), ^{
self.textView.text = message;
});
}
#pragma mark - App lifecycle notifications
-(void)appDidBecomeActive:(NSNotification*)notification
{
[self removeUnreadMessagesSeparator];
if (!_offlineMode) {
[[NCRoomsManager sharedInstance] joinRoom:_room.token];
}
}
-(void)appWillResignActive:(NSNotification*)notification
{
_hasReceiveNewMessages = NO;
_leftChatWithVisibleChatVC = YES;
[_chatController stopChatController];
[[NCRoomsManager sharedInstance] leaveChatInRoom:_room.token];
}
#pragma mark - Configuration
- (void)setTitleView
{
[_titleView.title setTitle:_room.displayName forState:UIControlStateNormal];
// Set room image
switch (_room.type) {
case kNCRoomTypeOneToOne:
{
// Request user avatar to the server and set it if exist
[_titleView.image setImageWithURLRequest:[[NCAPIController sharedInstance] createAvatarRequestForUser:_room.name andSize:96 usingAccount:[[NCDatabaseManager sharedInstance] activeAccount]]
placeholderImage:nil success:nil failure:nil];
}
break;
case kNCRoomTypeGroup:
[_titleView.image setImage:[UIImage imageNamed:@"group-bg"]];
break;
case kNCRoomTypePublic:
[_titleView.image setImage:(_room.hasPassword) ? [UIImage imageNamed:@"public-password-bg"] : [UIImage imageNamed:@"public-bg"]];
break;
case kNCRoomTypeChangelog:
[_titleView.image setImage:[UIImage imageNamed:@"changelog"]];
break;
default:
break;
}
// Set objectType image
if ([_room.objectType isEqualToString:NCRoomObjectTypeFile]) {
[_titleView.image setImage:[UIImage imageNamed:@"file-bg"]];
} else if ([_room.objectType isEqualToString:NCRoomObjectTypeSharePassword]) {
[_titleView.image setImage:[UIImage imageNamed:@"password-bg"]];
}
_titleView.title.accessibilityHint = NSLocalizedString(@"Double tap to go to conversation information", nil);
}
- (void)configureActionItems
{
UIImage *videoCallImage = [[UIImage imageNamed:@"videocall-action"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImage *voiceCallImage = [[UIImage imageNamed:@"call-action"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
CGFloat buttonWidth = 24.0;
CGFloat buttonPadding = 30.0;
_videoCallButton = [[BarButtonItemWithActivity alloc] initWithWidth:buttonWidth withImage:videoCallImage];
[_videoCallButton.innerButton addTarget:self action:@selector(videoCallButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
_videoCallButton.accessibilityLabel = NSLocalizedString(@"Video call", nil);
_videoCallButton.accessibilityHint = NSLocalizedString(@"Double tap to start a video call", nil);
_voiceCallButton = [[BarButtonItemWithActivity alloc] initWithWidth:buttonWidth withImage:voiceCallImage];
[_voiceCallButton.innerButton addTarget:self action:@selector(voiceCallButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
_voiceCallButton.accessibilityLabel = NSLocalizedString(@"Voice call", nil);
_voiceCallButton.accessibilityHint = NSLocalizedString(@"Double tap to start a voice call", nil);
UIBarButtonItem *fixedSpace =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
target:nil
action:nil];
fixedSpace.width = buttonPadding;
self.navigationItem.rightBarButtonItems = @[_videoCallButton, fixedSpace, _voiceCallButton];
}
#pragma mark - User Interface
- (void)disableRoomControls
{
_titleView.userInteractionEnabled = NO;
[_videoCallButton hideActivityIndicator];
[_voiceCallButton hideActivityIndicator];
[_videoCallButton setEnabled:NO];
[_voiceCallButton setEnabled:NO];
self.textInputbar.userInteractionEnabled = NO;
}
- (void)checkRoomControlsAvailability
{
if (_hasJoinedRoom) {
// Enable room info, input bar and call buttons
_titleView.userInteractionEnabled = YES;
[_videoCallButton setEnabled:YES];
[_voiceCallButton setEnabled:YES];
self.textInputbar.userInteractionEnabled = YES;
}
if (![_room userCanStartCall] && !_room.hasCall) {
// Disable call buttons
[_videoCallButton setEnabled:NO];
[_voiceCallButton setEnabled:NO];
}
if (_room.readOnlyState == NCRoomReadOnlyStateReadOnly || [self shouldPresentLobbyView]) {
// Hide text input
self.textInputbarHidden = YES;
// Disable call buttons
[_videoCallButton setEnabled:NO];
[_voiceCallButton setEnabled:NO];
} else if ([self isTextInputbarHidden]) {
// Show text input if it was hidden in a previous state
[self setTextInputbarHidden:NO animated:YES];
}
}
- (void)checkLobbyState
{
if ([self shouldPresentLobbyView]) {
[_chatBackgroundView.placeholderText setText:NSLocalizedString(@"You are currently waiting in the lobby", nil)];
[_chatBackgroundView.placeholderImage setImage:[UIImage imageNamed:@"lobby-placeholder"]];
if (_room.lobbyTimer > 0) {
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:_room.lobbyTimer];
NSString *meetingStart = [NCUtils readableDateFromDate:date];
NSString *placeHolderText = [NSString stringWithFormat:NSLocalizedString(@"You are currently waiting in the lobby.\nThis meeting is scheduled for\n%@", nil), meetingStart];
[_chatBackgroundView.placeholderText setText:placeHolderText];
[_chatBackgroundView.placeholderImage setImage:[UIImage imageNamed:@"lobby-placeholder"]];
}
[_chatBackgroundView.placeholderView setHidden:NO];
[_chatBackgroundView.loadingView stopAnimating];
[_chatBackgroundView.loadingView setHidden:YES];
// Clear current chat since chat history will be retrieve when lobby is disabled
[self cleanChat];
} else {
[_chatBackgroundView.placeholderText setText:NSLocalizedString(@"No messages yet, start the conversation!", nil)];
[_chatBackgroundView.placeholderImage setImage:[UIImage imageNamed:@"chat-placeholder"]];
[_chatBackgroundView.placeholderView setHidden:YES];
[_chatBackgroundView.loadingView startAnimating];
[_chatBackgroundView.loadingView setHidden:NO];
// Stop checking lobby flag
[_lobbyCheckTimer invalidate];
// Retrieve initial chat history
if (!_hasReceiveInitialHistory && !_hasRequestedInitialHistory) {
_hasRequestedInitialHistory = YES;
[_chatController getInitialChatHistory];
}
}
[self checkRoomControlsAvailability];
}
- (void)setOfflineFooterView
{
UILabel *footerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 350, 24)];
footerLabel.textAlignment = NSTextAlignmentCenter;
footerLabel.textColor = [UIColor lightGrayColor];
footerLabel.font = [UIFont systemFontOfSize:12.0];
footerLabel.backgroundColor = [UIColor clearColor];
footerLabel.text = NSLocalizedString(@"Offline, only showing downloaded messages", nil);
self.tableView.tableFooterView = footerLabel;
self.tableView.tableFooterView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
}
#pragma mark - Utils
- (NSInteger)getLastReadMessage
{
if ([[NCSettingsController sharedInstance] serverHasTalkCapability:kCapabilityChatReadMarker]) {
return _lastReadMessage;
}
return 0;
}
- (NSString *)getHeaderStringFromDate:(NSDate *)date
{
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateStyle = NSDateFormatterMediumStyle;
formatter.doesRelativeDateFormatting = YES;
return [formatter stringFromDate:date];
}
- (NSString *)createSendingMessage:(NSString *)text
{
NSString *sendingMessage = [text copy];
for (NCMessageParameter *mention in _mentions) {
sendingMessage = [sendingMessage stringByReplacingOccurrencesOfString:mention.name withString:mention.parameterId];
}
_mentions = [[NSMutableArray alloc] init];
return sendingMessage;
}
- (void)presentJoinError:(NSString *)alertMessage
{
NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Could not join %@", nil), _room.displayName];
if (_room.type == kNCRoomTypeOneToOne) {
alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Could not join conversation with %@", nil), _room.displayName];
}
UIAlertController * alert = [UIAlertController alertControllerWithTitle:alertTitle
message:alertMessage
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* okButton = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil)
style:UIAlertActionStyleDefault
handler:nil];
[alert addAction:okButton];
[[NCUserInterfaceController sharedInstance] presentAlertViewController:alert];
}
#pragma mark - Temporary messages
- (NCChatMessage *)createTemporaryMessage:(NSString *)text
{
NCChatMessage *temporaryMessage = [[NCChatMessage alloc] init];
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
temporaryMessage.accountId = activeAccount.accountId;
temporaryMessage.actorDisplayName = activeAccount.userDisplayName;
temporaryMessage.actorId = activeAccount.userId;
temporaryMessage.timestamp = [[NSDate date] timeIntervalSince1970];
temporaryMessage.token = _room.token;
NSString *sendingMessage = [text copy];
temporaryMessage.message = sendingMessage;
NSString * referenceId = [NSString stringWithFormat:@"temp-%f",[[NSDate date] timeIntervalSince1970] * 1000];
temporaryMessage.referenceId = [NCUtils sha1FromString:referenceId];
temporaryMessage.internalId = referenceId;
temporaryMessage.isTemporary = YES;
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
[realm addObject:temporaryMessage];
}];
NCChatMessage *unmanagedTemporaryMessage = [[NCChatMessage alloc] initWithValue:temporaryMessage];
return unmanagedTemporaryMessage;
}
- (void)appendTemporaryMessage:(NCChatMessage *)temporaryMessage
{
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger lastSectionBeforeUpdate = self->_dateSections.count - 1;
NSMutableArray *messages = [[NSMutableArray alloc] initWithObjects:temporaryMessage, nil];
[self appendMessages:messages inDictionary:self->_messages];
NSMutableArray *messagesForLastDate = [self->_messages objectForKey:[self->_dateSections lastObject]];
NSIndexPath *lastMessageIndexPath = [NSIndexPath indexPathForRow:messagesForLastDate.count - 1 inSection:self->_dateSections.count - 1];
[self.tableView beginUpdates];
NSInteger newLastSection = self->_dateSections.count - 1;
BOOL newSection = lastSectionBeforeUpdate != newLastSection;
if (newSection) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:newLastSection] withRowAnimation:UITableViewRowAnimationNone];
} else {
[self.tableView insertRowsAtIndexPaths:@[lastMessageIndexPath] withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView endUpdates];
[self.tableView scrollToRowAtIndexPath:lastMessageIndexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
});
}
- (void)removePermanentlyTemporaryMessage:(NCChatMessage *)temporaryMessage
{
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
NCChatMessage *managedTemporaryMessage = [NCChatMessage objectsWhere:@"referenceId = %@", temporaryMessage.referenceId].firstObject;
if (managedTemporaryMessage) {
[realm deleteObject:managedTemporaryMessage];
}
}];
[self removeTemporaryMessages:@[temporaryMessage]];
}
- (void)removeTemporaryMessages:(NSArray *)messages
{
dispatch_async(dispatch_get_main_queue(), ^{
for (NCChatMessage *message in messages) {
NSIndexPath *indexPath = [self indexPathForMessage:message];
if (indexPath) {
[self removeMessageAtIndexPath:indexPath];
}
}
});
}
- (void)setFailedStatusToMessageWithReferenceId:(NSString *)referenceId
{
NSMutableArray *reloadIndexPaths = [NSMutableArray new];
NSIndexPath *indexPath = [self indexPathForMessageWithReferenceId:referenceId];
if (indexPath) {
[reloadIndexPaths addObject:indexPath];
// Set failed status
NSDate *keyDate = [_dateSections objectAtIndex:indexPath.section];
NSMutableArray *messages = [_messages objectForKey:keyDate];
NCChatMessage *failedMessage = [messages objectAtIndex:indexPath.row];
failedMessage.sendingFailed = YES;
}
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:reloadIndexPaths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
#pragma mark - Action Methods
- (void)titleButtonPressed:(id)sender
{
RoomInfoTableViewController *roomInfoVC = [[RoomInfoTableViewController alloc] initForRoom:_room fromChatViewController:self];
[self.navigationController pushViewController:roomInfoVC animated:YES];
}
- (void)unreadMessagesButtonPressed:(id)sender
{
if (_firstUnreadMessageIP) {
[self.tableView scrollToRowAtIndexPath:_firstUnreadMessageIP atScrollPosition:UITableViewScrollPositionNone animated:YES];
}
}
- (void)videoCallButtonPressed:(id)sender
{
[_videoCallButton showActivityIndicator];
[[CallKitManager sharedInstance] startCall:_room.token withVideoEnabled:YES andDisplayName:_room.displayName withAccountId:_room.accountId];
}
- (void)voiceCallButtonPressed:(id)sender
{
[_voiceCallButton showActivityIndicator];
[[CallKitManager sharedInstance] startCall:_room.token withVideoEnabled:NO andDisplayName:_room.displayName withAccountId:_room.accountId];
}
- (void)sendChatMessage:(NSString *)message fromInputField:(BOOL)fromInputField
{
// Create temporary message
NSString *referenceId = nil;
if ([[NCSettingsController sharedInstance] serverHasTalkCapability:kCapabilityChatReferenceId]) {
NCChatMessage *temporaryMessage = [self createTemporaryMessage:message];
referenceId = temporaryMessage.referenceId;
[self appendTemporaryMessage:temporaryMessage];
}
// Send message
NSString *sendingText = [self createSendingMessage:message];
NSInteger replyTo = (_replyMessageView.isVisible && fromInputField) ? _replyMessageView.message.messageId : -1;
[_chatController sendChatMessage:sendingText replyTo:replyTo referenceId:referenceId];
}
- (void)didPressRightButton:(id)sender
{
[self sendChatMessage:self.textView.text fromInputField:YES];
[_replyMessageView dismiss];
[super didPressRightButton:sender];
// Input field is empty after send -> this clears a previously saved pending message
[self savePendingMessage];
}
- (void)didPressLeftButton:(id)sender
{
[self presentAttachmentsOptions];
[super didPressLeftButton:sender];
}
- (void)presentAttachmentsOptions
{
UIAlertController *optionsActionSheet = [UIAlertController alertControllerWithTitle:nil
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction *cameraAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Camera", nil)
style:UIAlertActionStyleDefault
handler:^void (UIAlertAction *action) {
[self checkAndPresentCamera];
}];
[cameraAction setValue:[[UIImage imageNamed:@"camera"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forKey:@"image"];
UIAlertAction *photoLibraryAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Photo Library", nil)
style:UIAlertActionStyleDefault
handler:^void (UIAlertAction *action) {
[self presentPhotoLibrary];
}];
[photoLibraryAction setValue:[[UIImage imageNamed:@"photos"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forKey:@"image"];
UIAlertAction *filesAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Files", nil)
style:UIAlertActionStyleDefault
handler:^void (UIAlertAction *action) {
[self presentDocumentPicker];
}];
[filesAction setValue:[[UIImage imageNamed:@"files"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forKey:@"image"];
UIAlertAction *ncFilesAction = [UIAlertAction actionWithTitle:filesAppName
style:UIAlertActionStyleDefault
handler:^void (UIAlertAction *action) {
[self presentNextcloudFilesBrowser];
}];
[ncFilesAction setValue:[[UIImage imageNamed:@"logo-action"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forKey:@"image"];
if ([UIImagePickerController isSourceTypeAvailable: UIImagePickerControllerSourceTypeCamera]) {
[optionsActionSheet addAction:cameraAction];
}
[optionsActionSheet addAction:photoLibraryAction];
[optionsActionSheet addAction:filesAction];
[optionsActionSheet addAction:ncFilesAction];
[optionsActionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
// Presentation on iPads
optionsActionSheet.popoverPresentationController.sourceView = self.leftButton;
optionsActionSheet.popoverPresentationController.sourceRect = self.leftButton.frame;
[self presentViewController:optionsActionSheet animated:YES completion:nil];
}
- (void)presentNextcloudFilesBrowser
{
DirectoryTableViewController *directoryVC = [[DirectoryTableViewController alloc] initWithPath:@"" inRoom:_room.token];
NCNavigationController *fileSharingNC = [[NCNavigationController alloc] initWithRootViewController:directoryVC];
[self presentViewController:fileSharingNC animated:YES completion:nil];
}
- (void)checkAndPresentCamera
{
// https://stackoverflow.com/a/20464727/2512312
NSString *mediaType = AVMediaTypeVideo;
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
if(authStatus == AVAuthorizationStatusAuthorized) {
[self presentCamera];
return;
} else if(authStatus == AVAuthorizationStatusNotDetermined){
[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL granted) {
if(granted){
[self presentCamera];
}
}];
return;
}
UIAlertController * alert = [UIAlertController
alertControllerWithTitle:NSLocalizedString(@"Could not access camera", nil)
message:NSLocalizedString(@"Camera access is not allowed. Check your settings.", nil)
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* okButton = [UIAlertAction
actionWithTitle:NSLocalizedString(@"OK", nil)
style:UIAlertActionStyleDefault
handler:nil];
[alert addAction:okButton];
[[NCUserInterfaceController sharedInstance] presentAlertViewController:alert];
}
- (void)presentCamera
{
dispatch_async(dispatch_get_main_queue(), ^{
_imagePicker = [[UIImagePickerController alloc] init];
_imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
_imagePicker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:_imagePicker.sourceType];
_imagePicker.delegate = self;
[self presentViewController:_imagePicker animated:YES completion:nil];
});
}
- (void)presentPhotoLibrary
{
dispatch_async(dispatch_get_main_queue(), ^{
_imagePicker = [[UIImagePickerController alloc] init];
_imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
_imagePicker.mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:_imagePicker.sourceType];
_imagePicker.delegate = self;
[self presentViewController:_imagePicker animated:YES completion:nil];
});
}
- (void)presentDocumentPicker
{
dispatch_async(dispatch_get_main_queue(), ^{
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeImport];
documentPicker.delegate = self;
[self presentViewController:documentPicker animated:YES completion:nil];
});
}
- (void)didPressReply:(NCChatMessage *)message {
// Use dispatch here to have a smooth animation with native contextmenu
dispatch_async(dispatch_get_main_queue(), ^{
self.replyMessageView = (ReplyMessageView *)self.typingIndicatorProxyView;
[self.replyMessageView dismiss];
[self.replyMessageView presentReplyViewWithMessage:message];
[self presentKeyboard:YES];
});
}
- (void)didPressReplyPrivately:(NCChatMessage *)message {
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
[userInfo setObject:message.actorId forKey:@"actorId"];
[[NSNotificationCenter defaultCenter] postNotificationName:NCChatViewControllerReplyPrivatelyNotification
object:self
userInfo:userInfo];
}
- (void)didPressResend:(NCChatMessage *)message {
[self removePermanentlyTemporaryMessage:message];
[self sendChatMessage:message.message fromInputField:NO];
}
- (void)didPressCopy:(NCChatMessage *)message {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = message.parsedMessage.string;
[self.view makeToast:NSLocalizedString(@"Message copied", nil) duration:1.5 position:CSToastPositionCenter];
}
- (void)didPressDelete:(NCChatMessage *)message {
[self removePermanentlyTemporaryMessage:message];
}
- (void)didPressOpenInNextcloud:(NCChatMessage *)message {
if (message.file) {
[NCUtils openFileInNextcloudAppOrBrowser:message.file.path withFileLink:message.file.link];
}
}
#pragma mark - UIImagePickerController Delegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId];
ShareConfirmationViewController *shareConfirmationVC = [[ShareConfirmationViewController alloc] initWithRoom:_room account:activeAccount serverCapabilities:serverCapabilities];
shareConfirmationVC.delegate = self;
shareConfirmationVC.isModal = YES;
NCNavigationController *navigationController = [[NCNavigationController alloc] initWithRootViewController:shareConfirmationVC];
NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:@"public.image"]) {
UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:navigationController animated:YES completion:^{
[shareConfirmationVC.shareItemController addItemWithImage:image];
}];
}];
} else if ([mediaType isEqualToString:@"public.movie"]) {
NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL];
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:navigationController animated:YES completion:^{
[shareConfirmationVC.shareItemController addItemWithURL:videoURL];
}];
}];
}
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - UIDocumentPickerViewController Delegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
{
[self shareDocumentsWithURLs:@[url] fromController:controller];
}
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
[self shareDocumentsWithURLs:urls fromController:controller];
}
- (void)shareDocumentsWithURLs:(NSArray<NSURL *> *)urls fromController:(UIDocumentPickerViewController *)controller {
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId];
ShareConfirmationViewController *shareConfirmationVC = [[ShareConfirmationViewController alloc] initWithRoom:_room account:activeAccount serverCapabilities:serverCapabilities];
shareConfirmationVC.delegate = self;
shareConfirmationVC.isModal = YES;
NCNavigationController *navigationController = [[NCNavigationController alloc] initWithRootViewController:shareConfirmationVC];
if (controller.documentPickerMode == UIDocumentPickerModeImport) {
[self presentViewController:navigationController animated:YES completion:^{
for (NSURL* url in urls) {
[shareConfirmationVC.shareItemController addItemWithURL:url];
}
}];
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
{
}
#pragma mark - ShareConfirmationViewController Delegate
- (void)shareConfirmationViewControllerDidFailed:(ShareConfirmationViewController *)viewController
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)shareConfirmationViewControllerDidFinish:(ShareConfirmationViewController *)viewController
{
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - Gesture recognizer
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if (@available(iOS 13.0, *)) {
// Use native contextmenus on iOS >= 13
return;
}
CGPoint point = [gestureRecognizer locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:point];
if (indexPath != nil && gestureRecognizer.state == UIGestureRecognizerStateBegan) {
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
if (!message.isSystemMessage) {
// Select cell
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
// Create menu
FTPopOverMenuConfiguration *menuConfiguration = [[FTPopOverMenuConfiguration alloc] init];
menuConfiguration.menuIconMargin = 12;
menuConfiguration.menuTextMargin = 12;
menuConfiguration.imageSize = CGSizeMake(20, 20);
menuConfiguration.separatorInset = UIEdgeInsetsMake(0, 44, 0, 0);
menuConfiguration.menuRowHeight = 44;
menuConfiguration.autoMenuWidth = YES;
menuConfiguration.textFont = [UIFont systemFontOfSize:15];
menuConfiguration.backgroundColor = [UIColor colorWithWhite:0.3 alpha:1];
menuConfiguration.borderWidth = 0;
menuConfiguration.shadowOpacity = 0;
menuConfiguration.roundedImage = NO;
menuConfiguration.defaultSelection = YES;
NSMutableArray *menuArray = [NSMutableArray new];
// Reply option
if (message.isReplyable) {
NSDictionary *replyInfo = [NSDictionary dictionaryWithObject:@(kNCChatMessageActionReply) forKey:@"action"];
FTPopOverMenuModel *replyModel = [[FTPopOverMenuModel alloc] initWithTitle:NSLocalizedString(@"Reply", nil) image:[UIImage imageNamed:@"reply"] userInfo:replyInfo];
[menuArray addObject:replyModel];
// Reply-privately option (only to other users and not in one-to-one)
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
if (_room.type != kNCRoomTypeOneToOne && [message.actorType isEqualToString:@"users"] && ![message.actorId isEqualToString:activeAccount.userId] )
{
NSDictionary *replyPrivatInfo = [NSDictionary dictionaryWithObject:@(kNCChatMessageActionReplyPrivately) forKey:@"action"];
FTPopOverMenuModel *replyPrivatModel = [[FTPopOverMenuModel alloc] initWithTitle:NSLocalizedString(@"Reply Privately", nil) image:[UIImage imageNamed:@"reply"] userInfo:replyPrivatInfo];
[menuArray addObject:replyPrivatModel];
}
}
// Re-send option
if (message.sendingFailed) {
NSDictionary *replyInfo = [NSDictionary dictionaryWithObject:@(kNCChatMessageActionResend) forKey:@"action"];
FTPopOverMenuModel *replyModel = [[FTPopOverMenuModel alloc] initWithTitle:NSLocalizedString(@"Resend", nil) image:[UIImage imageNamed:@"refresh"] userInfo:replyInfo];
[menuArray addObject:replyModel];
}
// Copy option
NSDictionary *copyInfo = [NSDictionary dictionaryWithObject:@(kNCChatMessageActionCopy) forKey:@"action"];
FTPopOverMenuModel *copyModel = [[FTPopOverMenuModel alloc] initWithTitle:NSLocalizedString(@"Copy", nil) image:[UIImage imageNamed:@"clippy"] userInfo:copyInfo];
[menuArray addObject:copyModel];
// Open in nextcloud option
if (message.file) {
NSDictionary *openInNextcloudInfo = [NSDictionary dictionaryWithObject:@(kNCChatMessageActionOpenFileInNextcloud) forKey:@"action"];
NSString *openInNextcloudTitle = [NSString stringWithFormat:NSLocalizedString(@"Open in %@", nil), filesAppName];
FTPopOverMenuModel *openInNextcloudModel = [[FTPopOverMenuModel alloc] initWithTitle:openInNextcloudTitle image:[[UIImage imageNamed:@"logo-action"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] userInfo:openInNextcloudInfo];
[menuArray addObject:openInNextcloudModel];
}
// Delete option
if (message.sendingFailed) {
NSDictionary *replyInfo = [NSDictionary dictionaryWithObject:@(kNCChatMessageActionDelete) forKey:@"action"];
FTPopOverMenuModel *replyModel = [[FTPopOverMenuModel alloc] initWithTitle:NSLocalizedString(@"Delete", nil) image:[UIImage imageNamed:@"delete"] userInfo:replyInfo];
[menuArray addObject:replyModel];
}
CGRect frame = [self.tableView rectForRowAtIndexPath:indexPath];
CGPoint yOffset = self.tableView.contentOffset;
CGRect cellRect = CGRectMake(frame.origin.x, (frame.origin.y - yOffset.y), frame.size.width, frame.size.height);
__weak NCChatViewController *weakSelf = self;
[FTPopOverMenu showFromSenderFrame:cellRect withMenuArray:menuArray imageArray:nil configuration:menuConfiguration doneBlock:^(NSInteger selectedIndex) {
[weakSelf.tableView deselectRowAtIndexPath:indexPath animated:YES];
FTPopOverMenuModel *model = [menuArray objectAtIndex:selectedIndex];
NCChatMessageAction action = (NCChatMessageAction)[[model.userInfo objectForKey:@"action"] integerValue];
switch (action) {
case kNCChatMessageActionReply:
{
[weakSelf didPressReply:message];
}
break;
case kNCChatMessageActionReplyPrivately:
{
[weakSelf didPressReplyPrivately:message];
}
break;
case kNCChatMessageActionCopy:
{
[weakSelf didPressCopy:message];
}
break;
case kNCChatMessageActionResend:
{
[weakSelf didPressResend:message];
}
break;
case kNCChatMessageActionOpenFileInNextcloud:
{
[weakSelf didPressOpenInNextcloud:message];
}
break;
case kNCChatMessageActionDelete:
{
[weakSelf didPressDelete:message];
}
break;
default:
break;
}
} dismissBlock:^{
[weakSelf.tableView deselectRowAtIndexPath:indexPath animated:YES];
}];
}
}
}
#pragma mark - UIScrollViewDelegate Methods
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[super scrollViewDidScroll:scrollView];
if ([scrollView isEqual:self.tableView] && scrollView.contentOffset.y < 0) {
if ([self couldRetireveHistory]) {
NSDate *dateSection = [_dateSections objectAtIndex:0];
NCChatMessage *firstMessage = [[_messages objectForKey:dateSection] objectAtIndex:0];
if ([_chatController hasHistoryFromMessageId:firstMessage.messageId]) {
_retrievingHistory = YES;
[self showLoadingHistoryView];
if (_offlineMode) {
[_chatController getHistoryBatchOfflineFromMessagesId:firstMessage.messageId];
} else {
[_chatController getHistoryBatchFromMessagesId:firstMessage.messageId];
}
}
}
}
if (_firstUnreadMessageIP) {
[self checkUnreadMessagesVisibility];
}
}
#pragma mark - UITextViewDelegate Methods
- (BOOL)textView:(SLKTextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
if ([text isEqualToString:@""]) {
UITextRange *selectedRange = [textView selectedTextRange];
NSInteger cursorOffset = [textView offsetFromPosition:textView.beginningOfDocument toPosition:selectedRange.start];
NSString *text = textView.text;
NSString *substring = [text substringToIndex:cursorOffset];
NSMutableString *lastPossibleMention = [[[substring componentsSeparatedByString:@"@"] lastObject] mutableCopy];
[lastPossibleMention insertString:@"@" atIndex:0];
for (NCMessageParameter *mention in _mentions) {
if ([lastPossibleMention isEqualToString:mention.name]) {
// Delete mention
textView.text = [[self.textView text] stringByReplacingOccurrencesOfString:lastPossibleMention withString:@""];
[_mentions removeObject:mention];
return NO;
}
}
}
return [super textView:textView shouldChangeTextInRange:range replacementText:text];
}
#pragma mark - Room Manager notifications
- (void)didUpdateRoom:(NSNotification *)notification
{
NCRoom *room = [notification.userInfo objectForKey:@"room"];
if (!room || ![room.token isEqualToString:_room.token]) {
return;
}
_room = room;
[self setTitleView];
[self checkLobbyState];
}
- (void)didJoinRoom:(NSNotification *)notification
{
NSString *token = [notification.userInfo objectForKey:@"token"];
if (![token isEqualToString:_room.token]) {
return;
}
NSError *error = [notification.userInfo objectForKey:@"error"];
if (error && _isVisible) {
_offlineMode = YES;
[self setOfflineFooterView];
[_chatController stopReceivingNewChatMessages];
[self presentJoinError:[notification.userInfo objectForKey:@"errorReason"]];
return;
}
_hasJoinedRoom = YES;
[self checkRoomControlsAvailability];
if (_hasStopped) {
return;
}
if (_leftChatWithVisibleChatVC && _hasReceiveInitialHistory) {
_leftChatWithVisibleChatVC = NO;
[_chatController startReceivingNewChatMessages];
} else if (!_hasReceiveInitialHistory && !_hasRequestedInitialHistory) {
_hasRequestedInitialHistory = YES;
[_chatController getInitialChatHistory];
}
}
- (void)didLeaveRoom:(NSNotification *)notification
{
[self disableRoomControls];
}
#pragma mark - Chat Controller notifications
- (void)didReceiveInitialChatHistory:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
if (notification.object != self->_chatController) {
return;
}
NSMutableArray *messages = [notification.userInfo objectForKey:@"messages"];
if (messages.count > 0) {
// Set last received message as last read message
NCChatMessage *lastReceivedMessage = [messages objectAtIndex:messages.count - 1];
self->_lastReadMessage = lastReceivedMessage.messageId;
[self appendMessages:messages inDictionary:self->_messages];
[self.tableView reloadData];
[self.tableView slk_scrollToBottomAnimated:NO];
} else {
[self->_chatBackgroundView.placeholderView setHidden:NO];
}
NSMutableArray *storedTemporaryMessages = [self->_chatController getTemporaryMessages];
if (storedTemporaryMessages.count > 0) {
[self insertMessages:storedTemporaryMessages];
[self.tableView reloadData];
[self.tableView slk_scrollToBottomAnimated:NO];
}
self->_hasReceiveInitialHistory = YES;
NSError *error = [notification.userInfo objectForKey:@"error"];
if (!error) {
[self->_chatController startReceivingNewChatMessages];
} else {
self->_offlineMode = YES;
[self->_chatController getInitialChatHistoryForOfflineMode];
}
});
}
- (void)didReceiveInitialChatHistoryOffline:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
if (notification.object != self->_chatController) {
return;
}
NSMutableArray *messages = [notification.userInfo objectForKey:@"messages"];
if (messages.count > 0) {
[self appendMessages:messages inDictionary:self->_messages];
[self setOfflineFooterView];
[self.tableView reloadData];
[self.tableView slk_scrollToBottomAnimated:NO];
} else {
[self->_chatBackgroundView.placeholderView setHidden:NO];
}
NSMutableArray *storedTemporaryMessages = [self->_chatController getTemporaryMessages];
if (storedTemporaryMessages.count > 0) {
[self insertMessages:storedTemporaryMessages];
[self.tableView reloadData];
[self.tableView slk_scrollToBottomAnimated:NO];
}
});
}
- (void)didReceiveChatHistory:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
if (notification.object != self->_chatController) {
return;
}
NSMutableArray *messages = [notification.userInfo objectForKey:@"messages"];
BOOL shouldAddBlockSeparator = [[notification.userInfo objectForKey:@"shouldAddBlockSeparator"] boolValue];
if (messages.count > 0) {
NSIndexPath *lastHistoryMessageIP = [self prependMessages:messages addingBlockSeparator:shouldAddBlockSeparator];
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:lastHistoryMessageIP atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
BOOL noMoreStoredHistory = [[notification.userInfo objectForKey:@"noMoreStoredHistory"] boolValue];
if (noMoreStoredHistory) {
self->_hasStoredHistory = NO;
}
self->_retrievingHistory = NO;
[self hideLoadingHistoryView];
});
}
- (void)didReceiveChatMessages:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = [notification.userInfo objectForKey:@"error"];
if (notification.object != self->_chatController || error) {
return;
}
BOOL firstNewMessagesAfterHistory = !self->_hasReceiveNewMessages;
self->_hasReceiveNewMessages = YES;
NSMutableArray *messages = [notification.userInfo objectForKey:@"messages"];
if (messages.count > 0) {
NSInteger lastSectionBeforeUpdate = self->_dateSections.count - 1;
BOOL unreadMessagesReceived = NO;
// Check if unread messages separator should be added
if (firstNewMessagesAfterHistory && [self getLastReadMessage] > 0 && messages.count > 0) {
unreadMessagesReceived = YES;
NSMutableArray *messagesForLastDateBeforeUpdate = [self->_messages objectForKey:[self->_dateSections lastObject]];
[messagesForLastDateBeforeUpdate addObject:self->_unreadMessagesSeparator];
self->_unreadMessagesSeparatorIP = [NSIndexPath indexPathForRow:messagesForLastDateBeforeUpdate.count - 1 inSection: self->_dateSections.count - 1];
[self->_messages setObject:messagesForLastDateBeforeUpdate forKey:[self->_dateSections lastObject]];
}
// Sort received messages
[self appendMessages:messages inDictionary:self->_messages];
NSMutableArray *messagesForLastDate = [self->_messages objectForKey:[self->_dateSections lastObject]];
NSIndexPath *lastMessageIndexPath = [NSIndexPath indexPathForRow:messagesForLastDate.count - 1 inSection:self->_dateSections.count - 1];
// Load messages in chat view
if (messages.count > 1 || unreadMessagesReceived) {
[self.tableView reloadData];
} else if (messages.count == 1) {
[self.tableView beginUpdates];
NSInteger newLastSection = self->_dateSections.count - 1;
BOOL newSection = lastSectionBeforeUpdate != newLastSection;
if (newSection) {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:newLastSection] withRowAnimation:UITableViewRowAnimationNone];
} else {
[self.tableView insertRowsAtIndexPaths:@[lastMessageIndexPath] withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView endUpdates];
}
BOOL newMessagesContainUserMessage = [self newMessagesContainUserMessage:messages];
// Remove unread messages separator when user writes a message
if (newMessagesContainUserMessage) {
[self removeUnreadMessagesSeparator];
// Update last message index path
lastMessageIndexPath = [NSIndexPath indexPathForRow:messagesForLastDate.count - 1 inSection:self->_dateSections.count - 1];
}
NCChatMessage *firstNewMessage = [messages objectAtIndex:0];
NSIndexPath *firstMessageIndexPath = [self indexPathForMessage:firstNewMessage];
// This variable is needed since several calls to receiveMessages API might be needed
// (if the number of unread messages is bigger than the "limit" in receiveMessages request)
// to receive all the unread messages.
BOOL areReallyNewMessages = firstNewMessage.timestamp >= self->_chatViewPresentedTimestamp;
// Position chat view
if (unreadMessagesReceived) {
// Dispatch it in the next cycle so reloadData is always completed.
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView scrollToRowAtIndexPath:firstMessageIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
} else if ([self shouldScrollOnNewMessages] || newMessagesContainUserMessage) {
[self.tableView scrollToRowAtIndexPath:lastMessageIndexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
} else if (!self->_firstUnreadMessageIP && areReallyNewMessages) {
[self showNewMessagesViewUntilIndexPath:firstMessageIndexPath];
}
// Set last received message as last read message
NCChatMessage *lastReceivedMessage = [messages objectAtIndex:messages.count - 1];
self->_lastReadMessage = lastReceivedMessage.messageId;
} else if (firstNewMessagesAfterHistory) {
// Now the chat is loaded after getting the initial history and the first new messages block.
// Even if there are no new messages, tableview should be reloaded and scrolled to the bottom
// as it was done when only initial history was loaded.
[self.tableView reloadData];
NSMutableArray *messagesForLastDate = [self->_messages objectForKey:[self->_dateSections lastObject]];
if (messagesForLastDate.count > 0) {
NSIndexPath *lastMessageIndexPath = [NSIndexPath indexPathForRow:messagesForLastDate.count - 1 inSection:self->_dateSections.count - 1];
[self.tableView scrollToRowAtIndexPath:lastMessageIndexPath atScrollPosition:UITableViewScrollPositionNone animated:NO];
}
}
if (firstNewMessagesAfterHistory) {
[self->_chatBackgroundView.loadingView stopAnimating];
[self->_chatBackgroundView.loadingView setHidden:YES];
}
});
}
- (void)didSendChatMessage:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
if (notification.object != self->_chatController) {
return;
}
NSError *error = [notification.userInfo objectForKey:@"error"];
NSString *message = [notification.userInfo objectForKey:@"message"];
NSString *referenceId = [notification.userInfo objectForKey:@"referenceId"];
if (error) {
if (referenceId) {
[self setFailedStatusToMessageWithReferenceId:referenceId];
} else {
self.textView.text = message;
UIAlertController * alert = [UIAlertController
alertControllerWithTitle:NSLocalizedString(@"Could not send the message", nil)
message:NSLocalizedString(@"An error occurred while sending the message", nil)
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* okButton = [UIAlertAction
actionWithTitle:NSLocalizedString(@"OK", nil)
style:UIAlertActionStyleDefault
handler:nil];
[alert addAction:okButton];
[[NCUserInterfaceController sharedInstance] presentAlertViewController:alert];
}
}
});
}
- (void)didReceiveChatBlocked:(NSNotification *)notification
{
if (notification.object != _chatController) {
return;
}
[self startObservingRoomLobbyFlag];
}
- (void)didRemoveTemporaryMessages:(NSNotification *)notification
{
if (notification.object != _chatController) {
return;
}
NSArray *removedTemporaryMessages = [notification.userInfo objectForKey:@"messages"];
[self removeTemporaryMessages:removedTemporaryMessages];
}
#pragma mark - Lobby functions
- (void)startObservingRoomLobbyFlag
{
[self updateRoomInformation];
dispatch_async(dispatch_get_main_queue(), ^{
[self->_lobbyCheckTimer invalidate];
self->_lobbyCheckTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(updateRoomInformation) userInfo:nil repeats:YES];
});
}
- (void)updateRoomInformation
{
[[NCRoomsManager sharedInstance] updateRoom:_room.token];
}
- (BOOL)shouldPresentLobbyView
{
return _room.lobbyState == NCRoomLobbyStateModeratorsOnly && !_room.canModerate;
}
#pragma mark - Chat functions
- (NSDate *)getKeyForDate:(NSDate *)date inDictionary:(NSDictionary *)dictionary
{
NSDate *keyDate = nil;
for (NSDate *key in dictionary.allKeys) {
if ([[NSCalendar currentCalendar] isDate:date inSameDayAsDate:key]) {
keyDate = key;
}
}
return keyDate;
}
- (NSIndexPath *)prependMessages:(NSMutableArray *)historyMessages addingBlockSeparator:(BOOL)shouldAddBlockSeparator
{
NSMutableDictionary *historyDict = [[NSMutableDictionary alloc] init];
[self appendMessages:historyMessages inDictionary:historyDict];
NSDate *chatSection = nil;
NSMutableArray *historyMessagesForSection = nil;
// Sort history sections
NSMutableArray *historySections = [NSMutableArray arrayWithArray:historyDict.allKeys];
[historySections sortUsingSelector:@selector(compare:)];
// Add every section in history that can't be merged with current chat messages
for (NSDate *historySection in historySections) {
historyMessagesForSection = [historyDict objectForKey:historySection];
chatSection = [self getKeyForDate:historySection inDictionary:_messages];
if (!chatSection) {
[_messages setObject:historyMessagesForSection forKey:historySection];
}
}
[self sortDateSections];
if (shouldAddBlockSeparator) {
// Chat block separator
NCChatMessage *blockSeparatorMessage = [[NCChatMessage alloc] init];
blockSeparatorMessage.messageId = kChatBlockSeparatorIdentifier;
[historyMessagesForSection addObject:blockSeparatorMessage];
}
NSMutableArray *lastHistoryMessages = [historyDict objectForKey:[historySections lastObject]];
NSIndexPath *lastHistoryMessageIP = [NSIndexPath indexPathForRow:lastHistoryMessages.count - 1 inSection:historySections.count - 1];
// Merge last section of history messages with first section in current chat
if (chatSection) {
NSMutableArray *chatMessages = [_messages objectForKey:chatSection];
NCChatMessage *lastHistoryMessage = [historyMessagesForSection lastObject];
NCChatMessage *firstChatMessage = [chatMessages firstObject];
firstChatMessage.isGroupMessage = [self shouldGroupMessage:firstChatMessage withMessage:lastHistoryMessage];
[historyMessagesForSection addObjectsFromArray:chatMessages];
[_messages setObject:historyMessagesForSection forKey:chatSection];
}
return lastHistoryMessageIP;
}
- (void)appendMessages:(NSMutableArray *)messages inDictionary:(NSMutableDictionary *)dictionary
{
for (NCChatMessage *newMessage in messages) {
NSDate *newMessageDate = [NSDate dateWithTimeIntervalSince1970: newMessage.timestamp];
NSDate *keyDate = [self getKeyForDate:newMessageDate inDictionary:dictionary];
NSMutableArray *messagesForDate = [dictionary objectForKey:keyDate];
if (messagesForDate) {
NCChatMessage *lastMessage = [messagesForDate lastObject];
newMessage.isGroupMessage = [self shouldGroupMessage:newMessage withMessage:lastMessage];
[messagesForDate addObject:newMessage];
} else {
NSMutableArray *newMessagesInDate = [NSMutableArray new];
[dictionary setObject:newMessagesInDate forKey:newMessageDate];
[newMessagesInDate addObject:newMessage];
}
}
[self sortDateSections];
}
- (void)insertMessages:(NSMutableArray *)messages
{
for (NCChatMessage *newMessage in messages) {
NSDate *newMessageDate = [NSDate dateWithTimeIntervalSince1970: newMessage.timestamp];
NSDate *keyDate = [self getKeyForDate:newMessageDate inDictionary:_messages];
NSMutableArray *messagesForDate = [_messages objectForKey:keyDate];
if (messagesForDate) {
for (int i = 0; i < messagesForDate.count; i++) {
NCChatMessage *currentMessage = [messagesForDate objectAtIndex:i];
if (currentMessage.timestamp > newMessage.timestamp) {
// Message inserted in between other messages
if (i > 0) {
NCChatMessage *previousMessage = [messagesForDate objectAtIndex:i-1];
newMessage.isGroupMessage = [self shouldGroupMessage:newMessage withMessage:previousMessage];
}
currentMessage.isGroupMessage = [self shouldGroupMessage:currentMessage withMessage:newMessage];
[messagesForDate insertObject:newMessage atIndex:i];
break;
// Message inserted at the end of a date section
} else if (i == messagesForDate.count - 1) {
newMessage.isGroupMessage = [self shouldGroupMessage:newMessage withMessage:currentMessage];
[messagesForDate addObject:newMessage];
break;
}
}
} else {
NSMutableArray *newMessagesInDate = [NSMutableArray new];
[_messages setObject:newMessagesInDate forKey:newMessageDate];
[newMessagesInDate addObject:newMessage];
}
}
[self sortDateSections];
}
- (NSIndexPath *)indexPathForMessage:(NCChatMessage *)message
{
NSDate *messageDate = [NSDate dateWithTimeIntervalSince1970: message.timestamp];
NSDate *keyDate = [self getKeyForDate:messageDate inDictionary:_messages];
NSInteger section = [_dateSections indexOfObject:keyDate];
if (NSNotFound != section) {
NSMutableArray *messages = [_messages objectForKey:keyDate];
for (int i = 0; i < messages.count; i++) {
NCChatMessage *currentMessage = messages[i];
if ((!currentMessage.isTemporary && currentMessage.messageId == message.messageId) ||
(currentMessage.isTemporary && [currentMessage.referenceId isEqualToString:message.referenceId])) {
return [NSIndexPath indexPathForRow:i inSection:section];
}
}
}
return nil;
}
- (NSIndexPath *)indexPathForMessageWithReferenceId:(NSString *)referenceId
{
for (NSInteger i = _dateSections.count - 1; i >= 0; i--) {
NSDate *keyDate = [_dateSections objectAtIndex:i];
NSMutableArray *messages = [_messages objectForKey:keyDate];
for (int j = 0; j < messages.count; j++) {
NCChatMessage *currentMessage = messages[j];
if ([currentMessage.referenceId isEqualToString:referenceId]) {
return [NSIndexPath indexPathForRow:j inSection:i];
}
}
}
return nil;
}
- (NSIndexPath *)removeMessageAtIndexPath:(NSIndexPath *)indexPath
{
NSDate *sectionKey = [_dateSections objectAtIndex:indexPath.section];
if (sectionKey) {
NSMutableArray *messages = [_messages objectForKey:sectionKey];
if (indexPath.row < messages.count) {
if (messages.count == 1) {
// Remove section
[_messages removeObjectForKey:sectionKey];
[self sortDateSections];
[self.tableView beginUpdates];
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
} else {
// Remove message
BOOL isLastMessage = indexPath.row == messages.count - 1;
[messages removeObjectAtIndex:indexPath.row];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
if (!isLastMessage) {
// Update the message next to removed message
NCChatMessage *nextMessage = [messages objectAtIndex:indexPath.row];
nextMessage.isGroupMessage = NO;
if (indexPath.row > 0) {
NCChatMessage *previousMessage = [messages objectAtIndex:indexPath.row - 1];
nextMessage.isGroupMessage = [self shouldGroupMessage:nextMessage withMessage:previousMessage];
}
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
}
}
}
return nil;
}
- (void)sortDateSections
{
_dateSections = [NSMutableArray arrayWithArray:_messages.allKeys];
[_dateSections sortUsingSelector:@selector(compare:)];
}
- (BOOL)shouldGroupMessage:(NCChatMessage *)newMessage withMessage:(NCChatMessage *)lastMessage
{
BOOL sameActor = [newMessage.actorId isEqualToString:lastMessage.actorId];
BOOL sameType = ([newMessage isSystemMessage] == [lastMessage isSystemMessage]);
BOOL timeDiff = (newMessage.timestamp - lastMessage.timestamp) < kChatMessageGroupTimeDifference;
return sameActor & sameType & timeDiff;
}
- (BOOL)couldRetireveHistory
{
return _hasReceiveInitialHistory && !_retrievingHistory && _dateSections.count > 0 && _hasStoredHistory;
}
- (void)showLoadingHistoryView
{
_loadingHistoryView = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
_loadingHistoryView.color = [UIColor darkGrayColor];
[_loadingHistoryView startAnimating];
self.tableView.tableHeaderView = _loadingHistoryView;
}
- (void)hideLoadingHistoryView
{
_loadingHistoryView = nil;
self.tableView.tableHeaderView = nil;
}
- (BOOL)shouldScrollOnNewMessages
{
if (_isVisible) {
// Scroll if table view is at the bottom (or 80px up)
CGFloat minimumOffset = (self.tableView.contentSize.height - self.tableView.frame.size.height) - 80;
if (self.tableView.contentOffset.y >= minimumOffset) {
return YES;
}
}
return NO;
}
- (BOOL)newMessagesContainUserMessage:(NSMutableArray *)messages
{
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
for (NCChatMessage *message in messages) {
if ([message.actorId isEqualToString:activeAccount.userId] && !message.isSystemMessage) {
return YES;
}
}
return NO;
}
- (void)showNewMessagesViewUntilIndexPath:(NSIndexPath *)messageIP
{
_firstUnreadMessageIP = messageIP;
_unreadMessageButton.hidden = NO;
// Check if unread messages are already visible
[self checkUnreadMessagesVisibility];
}
- (void)hideNewMessagesView
{
_firstUnreadMessageIP = nil;
_unreadMessageButton.hidden = YES;
}
- (void)removeUnreadMessagesSeparator
{
if (_unreadMessagesSeparatorIP) {
NSDate *separatorDate = [_dateSections objectAtIndex:_unreadMessagesSeparatorIP.section];
NSMutableArray *messages = [_messages objectForKey:separatorDate];
[messages removeObjectAtIndex:_unreadMessagesSeparatorIP.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:_unreadMessagesSeparatorIP] withRowAnimation:UITableViewRowAnimationTop];
_unreadMessagesSeparatorIP = nil;
}
}
- (void)checkUnreadMessagesVisibility
{
NSArray* visibleCellsIPs = [self.tableView indexPathsForVisibleRows];
if ([visibleCellsIPs containsObject:_firstUnreadMessageIP]) {
[self hideNewMessagesView];
}
}
- (void)cleanChat
{
_messages = [[NSMutableDictionary alloc] init];
_dateSections = [[NSMutableArray alloc] init];
_hasReceiveInitialHistory = NO;
_hasRequestedInitialHistory = NO;
_hasReceiveNewMessages = NO;
_unreadMessagesSeparatorIP = nil;
[self hideNewMessagesView];
[self.tableView reloadData];
}
- (void)savePendingMessage
{
_room.pendingMessage = self.textView.text;
[[NCRoomsManager sharedInstance] updateRoomLocal:_room];
}
#pragma mark - Autocompletion
- (void)didChangeAutoCompletionPrefix:(NSString *)prefix andWord:(NSString *)word
{
if ([prefix isEqualToString:@"@"]) {
[self showSuggestionsForString:word];
}
}
- (CGFloat)heightForAutoCompletionView
{
return kChatMessageCellMinimumHeight * self.autocompletionUsers.count;
}
- (void)showSuggestionsForString:(NSString *)string
{
self.autocompletionUsers = nil;
[[NCAPIController sharedInstance] getMentionSuggestionsInRoom:_room.token forString:string forAccount:[[NCDatabaseManager sharedInstance] activeAccount] withCompletionBlock:^(NSMutableArray *mentions, NSError *error) {
if (!error) {
self.autocompletionUsers = [[NSMutableArray alloc] initWithArray:mentions];
BOOL show = (self.autocompletionUsers.count > 0);
// Check if the '@' is still there
[self.textView lookForPrefixes:self.registeredPrefixes completion:^(NSString *prefix, NSString *word, NSRange wordRange) {
if (prefix.length > 0 && word.length > 0) {
[self showAutoCompletionView:show];
} else {
[self cancelAutoCompletion];
}
}];
}
}];
}
#pragma mark - UITableViewDataSource Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
if ([tableView isEqual:self.autoCompletionView]) {
return 1;
}
if ([tableView isEqual:self.tableView] && _dateSections.count > 0) {
self.tableView.backgroundView = nil;
} else {
self.tableView.backgroundView = _chatBackgroundView;
}
return _dateSections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if ([tableView isEqual:self.autoCompletionView]) {
return _autocompletionUsers.count;
}
NSDate *date = [_dateSections objectAtIndex:section];
NSMutableArray *messages = [_messages objectForKey:date];
return messages.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if ([tableView isEqual:self.autoCompletionView]) {
return nil;
}
NSDate *date = [_dateSections objectAtIndex:section];
return [self getHeaderStringFromDate:date];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if ([tableView isEqual:self.autoCompletionView]) {
return 0;
}
return kDateHeaderViewHeight;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if ([tableView isEqual:self.autoCompletionView]) {
return nil;
}
DateHeaderView *headerView = [[DateHeaderView alloc] init];
headerView.dateLabel.text = [self tableView:tableView titleForHeaderInSection:section];
headerView.dateLabel.layer.cornerRadius = 12;
headerView.dateLabel.clipsToBounds = YES;
return headerView;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([tableView isEqual:self.autoCompletionView]) {
NSDictionary *suggestion = [_autocompletionUsers objectAtIndex:indexPath.row];
NSString *suggestionId = [suggestion objectForKey:@"id"];
NSString *suggestionName = [suggestion objectForKey:@"label"];
NSString *suggestionSource = [suggestion objectForKey:@"source"];
NSString *suggestionUserStatus = [suggestion objectForKey:@"status"];
ChatMessageTableViewCell *suggestionCell = (ChatMessageTableViewCell *)[self.autoCompletionView dequeueReusableCellWithIdentifier:AutoCompletionCellIdentifier];
suggestionCell.titleLabel.text = suggestionName;
[suggestionCell setUserStatus:suggestionUserStatus];
if ([suggestionId isEqualToString:@"all"]) {
[suggestionCell.avatarView setImage:[UIImage imageNamed:@"group-bg"]];
} else if ([suggestionSource isEqualToString:@"guests"]) {
UIColor *guestAvatarColor = [UIColor colorWithRed:0.84 green:0.84 blue:0.84 alpha:1.0]; /*#d5d5d5*/
NSString *name = ([suggestionName isEqualToString:@"Guest"]) ? @"?" : suggestionName;
[suggestionCell.avatarView setImageWithString:name color:guestAvatarColor circular:true];
} else {
[suggestionCell.avatarView setImageWithURLRequest:[[NCAPIController sharedInstance] createAvatarRequestForUser:suggestionId andSize:96 usingAccount:[[NCDatabaseManager sharedInstance] activeAccount]]
placeholderImage:nil success:nil failure:nil];
}
return suggestionCell;
}
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
return [self getCellForMessage:message];
}
- (UITableViewCell *)getCellForMessage:(NCChatMessage *) message
{
UITableViewCell *cell = [UITableViewCell new];
if (message.messageId == kUnreadMessagesSeparatorIdentifier) {
MessageSeparatorTableViewCell *separatorCell = (MessageSeparatorTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessageSeparatorCellIdentifier];
separatorCell.messageId = message.messageId;
separatorCell.separatorLabel.text = NSLocalizedString(@"Unread messages", nil);
return separatorCell;
}
if (message.messageId == kChatBlockSeparatorIdentifier) {
MessageSeparatorTableViewCell *separatorCell = (MessageSeparatorTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:MessageSeparatorCellIdentifier];
separatorCell.messageId = message.messageId;
separatorCell.separatorLabel.text = NSLocalizedString(@"Some messages not shown, will be downloaded when online", nil);
return separatorCell;
}
if (message.isSystemMessage) {
SystemMessageTableViewCell *systemCell = (SystemMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:SystemMessageCellIdentifier];
systemCell.bodyTextView.attributedText = message.systemMessageFormat;
systemCell.messageId = message.messageId;
if (!message.isGroupMessage) {
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp];
systemCell.dateLabel.text = [NCUtils getTimeFromDate:date];
}
return systemCell;
}
if (message.file) {
NSString *fileCellIdentifier = (message.isGroupMessage) ? GroupedFileMessageCellIdentifier : FileMessageCellIdentifier;
FileMessageTableViewCell *fileCell = (FileMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:fileCellIdentifier];
fileCell.delegate = self;
[fileCell setupForMessage:message];
return fileCell;
}
if (message.parent) {
ChatMessageTableViewCell *normalCell = (ChatMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:ReplyMessageCellIdentifier];
normalCell.titleLabel.text = message.actorDisplayName;
normalCell.bodyTextView.attributedText = message.parsedMessage;
normalCell.messageId = message.messageId;
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp];
normalCell.dateLabel.text = [NCUtils getTimeFromDate:date];
if ([message.actorType isEqualToString:@"guests"]) {
normalCell.titleLabel.text = ([message.actorDisplayName isEqualToString:@""]) ? @"Guest" : message.actorDisplayName;
[normalCell setGuestAvatar:message.actorDisplayName];
} else if ([message.actorType isEqualToString:@"bots"]) {
if ([message.actorId isEqualToString:@"changelog"]) {
[normalCell setChangelogAvatar];
} else {
[normalCell setBotAvatar];
}
} else {
[normalCell.avatarView setImageWithURLRequest:[[NCAPIController sharedInstance] createAvatarRequestForUser:message.actorId andSize:96 usingAccount:[[NCDatabaseManager sharedInstance] activeAccount]]
placeholderImage:nil success:nil failure:nil];
}
// This check is just a workaround to fix the issue with the deleted parents returned by the API.
if (message.parent.message) {
normalCell.quotedMessageView.actorLabel.text = ([message.parent.actorDisplayName isEqualToString:@""]) ? @"Guest" : message.parent.actorDisplayName;
normalCell.quotedMessageView.messageLabel.text = message.parent.parsedMessage.string;
}
if (message.isTemporary){
[normalCell setDeliveryState:ChatMessageDeliveryStateSending];
}
if (message.sendingFailed) {
[normalCell setDeliveryState:ChatMessageDeliveryStateFailed];
}
return normalCell;
}
if (message.isGroupMessage) {
GroupedChatMessageTableViewCell *groupedCell = (GroupedChatMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:GroupedChatMessageCellIdentifier];
groupedCell.bodyTextView.attributedText = message.parsedMessage;
groupedCell.messageId = message.messageId;
if (message.isTemporary){
[groupedCell setDeliveryState:ChatMessageDeliveryStateSending];
}
if (message.sendingFailed) {
[groupedCell setDeliveryState:ChatMessageDeliveryStateFailed];
}
return groupedCell;
} else {
ChatMessageTableViewCell *normalCell = (ChatMessageTableViewCell *)[self.tableView dequeueReusableCellWithIdentifier:ChatMessageCellIdentifier];
normalCell.titleLabel.text = message.actorDisplayName;
normalCell.bodyTextView.attributedText = message.parsedMessage;
normalCell.messageId = message.messageId;
NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp];
normalCell.dateLabel.text = [NCUtils getTimeFromDate:date];
if ([message.actorType isEqualToString:@"guests"]) {
normalCell.titleLabel.text = ([message.actorDisplayName isEqualToString:@""]) ? @"Guest" : message.actorDisplayName;
[normalCell setGuestAvatar:message.actorDisplayName];
} else if ([message.actorType isEqualToString:@"bots"]) {
if ([message.actorId isEqualToString:@"changelog"]) {
[normalCell setChangelogAvatar];
} else {
[normalCell setBotAvatar];
}
} else {
[normalCell.avatarView setImageWithURLRequest:[[NCAPIController sharedInstance] createAvatarRequestForUser:message.actorId andSize:96 usingAccount:[[NCDatabaseManager sharedInstance] activeAccount]]
placeholderImage:nil success:nil failure:nil];
}
if (message.isTemporary){
[normalCell setDeliveryState:ChatMessageDeliveryStateSending];
}
if (message.sendingFailed) {
[normalCell setDeliveryState:ChatMessageDeliveryStateFailed];
}
return normalCell;
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([tableView isEqual:self.tableView]) {
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
CGFloat width = CGRectGetWidth(tableView.frame) - kChatMessageCellAvatarHeight;
if (@available(iOS 11.0, *)) {
width -= tableView.safeAreaInsets.left + tableView.safeAreaInsets.right;
}
return [self getCellHeightForMessage:message withWidth:width];
}
else {
return kChatMessageCellMinimumHeight;
}
}
- (CGFloat)getCellHeightForMessage:(NCChatMessage *)message withWidth:(CGFloat)width
{
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft;
if (message.messageId == kUnreadMessagesSeparatorIdentifier ||
message.messageId == kChatBlockSeparatorIdentifier) {
return kMessageSeparatorCellHeight;
}
CGFloat pointSize = [ChatMessageTableViewCell defaultFontSize];
NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:pointSize],
NSParagraphStyleAttributeName: paragraphStyle};
width -= (message.isSystemMessage)? 80.0 : 30.0; // 4*right(10) + dateLabel(40) : 3*right(10)
CGRect titleBounds = [message.actorDisplayName boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:NULL];
CGRect bodyBounds = [message.parsedMessage boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:NULL];
if (message.message.length == 0) {
return 0.0;
}
CGFloat height = CGRectGetHeight(titleBounds);
height += CGRectGetHeight(bodyBounds);
height += 40.0;
if (height < kChatMessageCellMinimumHeight) {
height = kChatMessageCellMinimumHeight;
}
if (message.parent) {
height += 60;
return height;
}
if (message.isGroupMessage || message.isSystemMessage) {
height = CGRectGetHeight(bodyBounds) + 20;
if (height < kGroupedChatMessageCellMinimumHeight) {
height = kGroupedChatMessageCellMinimumHeight;
}
}
if (message.file) {
height += kFileMessageCellFilePreviewHeight + 15;
}
return height;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([tableView isEqual:self.autoCompletionView]) {
NCMessageParameter *mention = [[NCMessageParameter alloc] init];
mention.parameterId = [NSString stringWithFormat:@"@%@", [self.autocompletionUsers[indexPath.row] objectForKey:@"id"]];
mention.name = [NSString stringWithFormat:@"@%@", [self.autocompletionUsers[indexPath.row] objectForKey:@"label"]];
// Guest mentions are wrapped with double quotes @"guest/<sha1(webrtc session id)>"
if ([[self.autocompletionUsers[indexPath.row] objectForKey:@"source"] isEqualToString:@"guests"]) {
mention.parameterId = [NSString stringWithFormat:@"@\"%@\"", [self.autocompletionUsers[indexPath.row] objectForKey:@"id"]];
}
// User-ids with a space should be wrapped in double quoutes
NSRange whiteSpaceRange = [mention.parameterId rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]];
if (whiteSpaceRange.location != NSNotFound) {
mention.parameterId = [NSString stringWithFormat:@"@\"%@\"", [self.autocompletionUsers[indexPath.row] objectForKey:@"id"]];
}
[_mentions addObject:mention];
NSMutableString *mentionString = [[self.autocompletionUsers[indexPath.row] objectForKey:@"label"] mutableCopy];
[mentionString appendString:@" "];
[self acceptAutoCompletionWithString:mentionString keepPrefix:YES];
} else {
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0))
{
if ([tableView isEqual:self.autoCompletionView]) {
return nil;
}
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
if (message.isSystemMessage) {
return nil;
}
NSMutableArray *actions = [[NSMutableArray alloc] init];
// Reply option
if (message.isReplyable) {
UIImage *replyImage = [[UIImage imageNamed:@"reply"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIAction *replyAction = [UIAction actionWithTitle:NSLocalizedString(@"Reply", nil) image:replyImage identifier:nil handler:^(UIAction *action){
[self didPressReply:message];
}];
[actions addObject:replyAction];
// Reply-privately option (only to other users and not in one-to-one)
TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount];
if (_room.type != kNCRoomTypeOneToOne && [message.actorType isEqualToString:@"users"] && ![message.actorId isEqualToString:activeAccount.userId] )
{
UIImage *replyPrivateImage = [[UIImage imageNamed:@"reply"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIAction *replyPrivateAction = [UIAction actionWithTitle:NSLocalizedString(@"Reply Privately", nil) image:replyPrivateImage identifier:nil handler:^(UIAction *action){
[self didPressReplyPrivately:message];
}];
[actions addObject:replyPrivateAction];
}
}
// Re-send option
if (message.sendingFailed) {
UIImage *resendImage = [[UIImage imageNamed:@"refresh"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIAction *resendAction = [UIAction actionWithTitle:NSLocalizedString(@"Resend", nil) image:resendImage identifier:nil handler:^(UIAction *action){
[self didPressResend:message];
}];
[actions addObject:resendAction];
}
// Copy option
UIImage *copyImage = [[UIImage imageNamed:@"clippy"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIAction *copyAction = [UIAction actionWithTitle:NSLocalizedString(@"Copy", nil) image:copyImage identifier:nil handler:^(UIAction *action){
[self didPressCopy:message];
}];
[actions addObject:copyAction];
// Open in nextcloud option
if (message.file) {
NSString *openInNextcloudTitle = [NSString stringWithFormat:NSLocalizedString(@"Open in %@", nil), filesAppName];
UIImage *nextcloudActionImage = [[UIImage imageNamed:@"logo-action"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIAction *openInNextcloudAction = [UIAction actionWithTitle:openInNextcloudTitle image:nextcloudActionImage identifier:nil handler:^(UIAction *action){
[self didPressOpenInNextcloud:message];
}];
[actions addObject:openInNextcloudAction];
}
// Delete option
if (message.sendingFailed) {
UIImage *deleteImage = [[UIImage imageNamed:@"delete"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIAction *deleteAction = [UIAction actionWithTitle:NSLocalizedString(@"Delete", nil) image:deleteImage identifier:nil handler:^(UIAction *action){
[self didPressDelete:message];
}];
deleteAction.attributes = UIMenuElementAttributesDestructive;
[actions addObject:deleteAction];
}
UIMenu *menu = [UIMenu menuWithTitle:@"" children:actions];
UIContextMenuConfiguration *configuration = [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:^UIViewController * _Nullable{
return [self getPreviewViewControllerForTableView:tableView withIndexPath:indexPath];
} actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
return menu;
}];
return configuration;
}
- (UIViewController *)getPreviewViewControllerForTableView:(UITableView *)tableView withIndexPath:(NSIndexPath *)indexPath
{
if (SLK_IS_IPAD) {
return nil;
}
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
// Remember grouped-status -> Create a previewView which always is a non-grouped-message
BOOL isGroupMessage = message.isGroupMessage;
message.isGroupMessage = NO;
CGFloat maxPreviewWidth = self.view.bounds.size.width;
CGFloat maxPreviewHeight = self.view.bounds.size.height * 0.6;
if (SLK_IS_IPHONE && SLK_IS_LANDSCAPE) {
maxPreviewWidth = self.view.bounds.size.width / 3;
}
UITableViewCell *previewView = [self getCellForMessage:message];
CGFloat maxTextWidth = maxPreviewWidth - kChatMessageCellAvatarHeight;
CGFloat cellHeight = [self getCellHeightForMessage:message withWidth:maxTextWidth];
// Cut the height if bigger than max height
if (cellHeight > maxPreviewHeight) {
cellHeight = maxPreviewHeight;
}
// Make sure the previewView has the correct size
previewView.contentView.frame = CGRectMake(0,0, maxPreviewWidth, cellHeight);
// Restore grouped-status
message.isGroupMessage = isGroupMessage;
UIViewController *previewController = [[UIViewController alloc] init];
[previewController.view addSubview:previewView.contentView];
previewController.preferredContentSize = previewView.contentView.frame.size;
return previewController;
}
#pragma mark - FileMessageTableViewCellDelegate
- (void)cellWantsToDownloadFile:(NCMessageFileParameter *)fileParameter
{
if (fileParameter.fileStatus && fileParameter.fileStatus.isDownloading) {
NSLog(@"File already downloading -> skipping new download");
return;
}
NCChatFileController *downloader = [[NCChatFileController alloc] init];
downloader.delegate = self;
[downloader downloadFileFromMessage:fileParameter];
}
#pragma mark - NCChatFileControllerDelegate
- (void)fileControllerDidLoadFile:(NCChatFileController *)fileController withFileStatus:(NCChatFileStatus *)fileStatus
{
if (_isPreviewControllerShown) {
// We are showing a file already, no need to open another one
return;
}
BOOL isFileCellStillVisible = NO;
for (NSIndexPath *indexPath in self.tableView.indexPathsForVisibleRows) {
NSDate *sectionDate = [_dateSections objectAtIndex:indexPath.section];
NCChatMessage *message = [[_messages objectForKey:sectionDate] objectAtIndex:indexPath.row];
if (message.file && [message.file.parameterId isEqualToString:fileStatus.fileId] && [message.file.path isEqualToString:fileStatus.filePath]) {
isFileCellStillVisible = YES;
break;
}
}
if (!isFileCellStillVisible) {
// Only open file when the corresponding cell is still visible on the screen
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self->_isPreviewControllerShown = YES;
self->_previewControllerFilePath = fileStatus.fileLocalPath;
QLPreviewController * preview = [[QLPreviewController alloc] init];
UIColor *themeColor = [NCAppBranding themeColor];
preview.dataSource = self;
preview.delegate = self;
preview.navigationController.navigationBar.tintColor = [NCAppBranding themeTextColor];
preview.navigationController.navigationBar.barTintColor = themeColor;
preview.tabBarController.tabBar.tintColor = themeColor;
if (@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
[appearance configureWithOpaqueBackground];
appearance.backgroundColor = themeColor;
appearance.titleTextAttributes = @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]};
preview.navigationItem.standardAppearance = appearance;
preview.navigationItem.compactAppearance = appearance;
preview.navigationItem.scrollEdgeAppearance = appearance;
}
[self.navigationController pushViewController:preview animated:YES];
});
}
#pragma mark - QLPreviewControllerDelegate/DataSource
- (NSInteger)numberOfPreviewItemsInPreviewController:(nonnull QLPreviewController *)controller {
return 1;
}
- (nonnull id<QLPreviewItem>)previewController:(nonnull QLPreviewController *)controller previewItemAtIndex:(NSInteger)index {
return [NSURL fileURLWithPath:_previewControllerFilePath];
}
- (void)previewControllerDidDismiss:(QLPreviewController *)controller
{
_isPreviewControllerShown = NO;
}
@end